From 5eb8f7ac0033904923e63593f6f1ac9d2a74fe89 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 12 Apr 2021 14:04:10 +0200 Subject: [PATCH 01/95] - Modify database to store favorites - Create entity - Create DAO --- .../32.json | 904 ++++++++++++++++++ .../app/global/db/AppDatabaseTest.kt | 5 + .../app/bookmarks/db/FavoriteEntity.kt | 28 + .../app/bookmarks/db/FavoritesDao.kt | 46 + .../duckduckgo/app/global/db/AppDatabase.kt | 15 +- 5 files changed, 996 insertions(+), 2 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/32.json create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoriteEntity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/32.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/32.json new file mode 100644 index 000000000000..5046018854f6 --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/32.json @@ -0,0 +1,904 @@ +{ + "formatVersion": 1, + "database": { + "version": 32, + "identityHash": "510e71539a55991713303ff3c2c46c09", + "entities": [ + { + "tableName": "tds_tracker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `defaultAction` TEXT NOT NULL, `ownerName` TEXT NOT NULL, `categories` TEXT NOT NULL, `rules` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultAction", + "columnName": "defaultAction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerName", + "columnName": "ownerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `displayName` TEXT NOT NULL, `prevalence` REAL NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevalence", + "columnName": "prevalence", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_domain_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `entityName` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entityName", + "columnName": "entityName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporary_tracking_whitelist", + "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": "user_whitelist", + "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": "https_bloom_filter_spec", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `bitCount` INTEGER NOT NULL, `errorRate` REAL NOT NULL, `totalEntries` INTEGER NOT NULL, `sha256` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bitCount", + "columnName": "bitCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "errorRate", + "columnName": "errorRate", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalEntries", + "columnName": "totalEntries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_false_positive_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": "network_leaderboard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`networkName`))", + "fields": [ + { + "fieldPath": "networkName", + "columnName": "networkName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "networkName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sites_visited", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tabId` TEXT NOT NULL, `url` TEXT, `title` TEXT, `skipHome` INTEGER NOT NULL, `viewed` INTEGER NOT NULL, `position` INTEGER NOT NULL, `tabPreviewFile` TEXT, `sourceTabId` TEXT, `deletable` INTEGER NOT NULL, PRIMARY KEY(`tabId`), FOREIGN KEY(`sourceTabId`) REFERENCES `tabs`(`tabId`) ON UPDATE SET NULL ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "skipHome", + "columnName": "skipHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewed", + "columnName": "viewed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabPreviewFile", + "columnName": "tabPreviewFile", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceTabId", + "columnName": "sourceTabId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deletable", + "columnName": "deletable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "SET NULL", + "columns": [ + "sourceTabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "tableName": "tab_selection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tabId` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`tabId`) REFERENCES `tabs`(`tabId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tab_selection_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tab_selection_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "tabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "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": [] + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "survey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`surveyId` TEXT NOT NULL, `url` TEXT, `daysInstalled` INTEGER, `status` TEXT NOT NULL, PRIMARY KEY(`surveyId`))", + "fields": [ + { + "fieldPath": "surveyId", + "columnName": "surveyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "daysInstalled", + "columnName": "daysInstalled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "surveyId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dismissed_cta", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ctaId` TEXT NOT NULL, PRIMARY KEY(`ctaId`))", + "fields": [ + { + "fieldPath": "ctaId", + "columnName": "ctaId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "ctaId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "search_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_days_used", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, PRIMARY KEY(`date`))", + "fields": [ + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_enjoyment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventType` INTEGER NOT NULL, `promptCount` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `primaryKey` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "eventType", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "promptCount", + "columnName": "promptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryKey", + "columnName": "primaryKey", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "primaryKey" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, PRIMARY KEY(`notificationId`))", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "notificationId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "privacy_protection_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `blocked_tracker_count` INTEGER NOT NULL, `upgrade_count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedTrackerCount", + "columnName": "blocked_tracker_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "upgradeCount", + "columnName": "upgrade_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UncaughtExceptionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exceptionSource` TEXT NOT NULL, `message` TEXT NOT NULL, `version` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exceptionSource", + "columnName": "exceptionSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tdsMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `eTag` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userStage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` INTEGER NOT NULL, `appStage` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appStage", + "columnName": "appStage", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "fireproofWebsites", + "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": "user_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "locationPermissions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `permission` INTEGER NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permission", + "columnName": "permission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "pixel_store", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pixelName` TEXT NOT NULL, `atb` TEXT NOT NULL, `additionalQueryParams` TEXT NOT NULL, `encodedQueryParams` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pixelName", + "columnName": "pixelName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atb", + "columnName": "atb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalQueryParams", + "columnName": "additionalQueryParams", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encodedQueryParams", + "columnName": "encodedQueryParams", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "auth_cookies_allowed_domains", + "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": [] + } + ], + "views": [], + "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, '510e71539a55991713303ff3c2c46c09')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index 8447a8da6841..b4757728c5ad 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -350,6 +350,11 @@ class AppDatabaseTest { createDatabaseAndMigrate(30, 31, migrationsProvider.MIGRATION_30_TO_31) } + @Test + fun whenMigratingFromVersion31To32ThenValidationSucceeds() { + createDatabaseAndMigrate(31, 32, migrationsProvider.MIGRATION_31_TO_32) + } + private fun givenUserStageIs(database: SupportSQLiteDatabase, appStage: AppStage) { database.execSQL("INSERT INTO `userStage` values (1, '${appStage.name}') ") } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoriteEntity.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoriteEntity.kt new file mode 100644 index 000000000000..ad813625ed51 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoriteEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 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 androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "favorites") +data class FavoriteEntity( + @PrimaryKey(autoGenerate = true) var id: Long = 0, + var title: String, + var url: String, + var position: Int +) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt new file mode 100644 index 000000000000..a2ee4f7bdf7a --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt @@ -0,0 +1,46 @@ +/* + * 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 androidx.lifecycle.LiveData +import androidx.room.* +import io.reactivex.Single + +@Dao +interface FavoritesDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(favorite: FavoriteEntity): Long + + @Query("select * from favorites") + fun favorites(): LiveData> + + @Query("select count(*) from favorites WHERE url LIKE :url") + fun favoritesCountByUrl(url: String): Int + + @Delete + fun delete(favorite: FavoriteEntity) + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(favoriteEntity: FavoriteEntity) + + @Query("select * from favorites") + fun favoritesObservable(): Single> + + @Query("select CAST(COUNT(*) AS BIT) from favorites") + suspend fun hasBookmarks(): Boolean +} 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 7172bc74b375..e9771b32ec29 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 @@ -24,6 +24,8 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.db.FavoriteEntity +import com.duckduckgo.app.bookmarks.db.FavoritesDao import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.cookies.db.AuthCookiesAllowedDomainsDao import com.duckduckgo.app.browser.cookies.db.AuthCookieAllowedDomainEntity @@ -67,7 +69,7 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.usage.search.SearchCountEntity @Database( - exportSchema = true, version = 31, + exportSchema = true, version = 32, entities = [ TdsTracker::class, TdsEntity::class, @@ -81,6 +83,7 @@ import com.duckduckgo.app.usage.search.SearchCountEntity TabEntity::class, TabSelectionEntity::class, BookmarkEntity::class, + FavoriteEntity::class, Survey::class, DismissedCta::class, SearchCountEntity::class, @@ -125,6 +128,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun networkLeaderboardDao(): NetworkLeaderboardDao abstract fun tabsDao(): TabsDao abstract fun bookmarksDao(): BookmarksDao + abstract fun favoritesDao(): FavoritesDao abstract fun surveyDao(): SurveyDao abstract fun dismissedCtaDao(): DismissedCtaDao abstract fun searchCountDao(): SearchCountDao @@ -397,6 +401,12 @@ class MigrationsProvider( } } + val MIGRATION_31_TO_32: Migration = object : Migration(30, 31) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `favorites` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `position` INTEGER NOT NULL)") + } + } + val BOOKMARKS_DB_ON_CREATE = object : RoomDatabase.Callback() { override fun onCreate(database: SupportSQLiteDatabase) { MIGRATION_29_TO_30.migrate(database) @@ -440,7 +450,8 @@ class MigrationsProvider( MIGRATION_27_TO_28, MIGRATION_28_TO_29, MIGRATION_29_TO_30, - MIGRATION_30_TO_31 + MIGRATION_30_TO_31, + MIGRATION_31_TO_32 ) @Deprecated( From 70d9d8d1842207073600e7b9421d1a9afd839f33 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 12 Apr 2021 14:16:03 +0200 Subject: [PATCH 02/95] Add favorite added to overflow menu --- .../java/com/duckduckgo/app/browser/BrowserStateModifier.kt | 2 ++ .../java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 1 + .../java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 3 +++ app/src/main/res/layout/popup_window_browser_menu.xml | 5 +++++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 12 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserStateModifier.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserStateModifier.kt index 8a997c782489..9c3b8e396dfb 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserStateModifier.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserStateModifier.kt @@ -30,6 +30,7 @@ class BrowserStateModifier { canReportSite = true, canSharePage = true, canAddBookmarks = true, + canAddFavorite = true, addToHomeEnabled = true ) } @@ -43,6 +44,7 @@ class BrowserStateModifier { canReportSite = false, canSharePage = false, canAddBookmarks = false, + canAddFavorite = false, addToHomeEnabled = false, canGoBack = false ) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index a53f9fd033de..423ba327a9f1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1759,6 +1759,7 @@ class BrowserTabFragment : refreshPopupMenuItem.isEnabled = browserShowing newTabPopupMenuItem.isEnabled = browserShowing addBookmarksPopupMenuItem?.isEnabled = viewState.canAddBookmarks + addFavoritePopupMenuItem?.isEnabled = viewState.canAddFavorite fireproofWebsitePopupMenuItem?.isEnabled = viewState.canFireproofSite fireproofWebsitePopupMenuItem?.isChecked = viewState.canFireproofSite && viewState.isFireproofWebsite sharePageMenuItem?.isEnabled = viewState.canSharePage diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 0d5857506f18..1fd6e7e92446 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -176,6 +176,7 @@ class BrowserTabViewModel( val showMenuButton: Boolean = true, val canSharePage: Boolean = false, val canAddBookmarks: Boolean = false, + val canAddFavorite: Boolean = false, val canFireproofSite: Boolean = false, val isFireproofWebsite: Boolean = false, val canGoBack: Boolean = false, @@ -803,6 +804,7 @@ class BrowserTabViewModel( browserViewState.value = currentBrowserViewState.copy( browserShowing = true, canAddBookmarks = true, + canAddFavorite = true, addToHomeEnabled = true, addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), canSharePage = true, @@ -930,6 +932,7 @@ class BrowserTabViewModel( val currentBrowserViewState = currentBrowserViewState() browserViewState.value = currentBrowserViewState.copy( canAddBookmarks = false, + canAddFavorite = false, addToHomeEnabled = false, addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), canSharePage = false, diff --git a/app/src/main/res/layout/popup_window_browser_menu.xml b/app/src/main/res/layout/popup_window_browser_menu.xml index b7e0959d5027..82cd5ea8f1e5 100644 --- a/app/src/main/res/layout/popup_window_browser_menu.xml +++ b/app/src/main/res/layout/popup_window_browser_menu.xml @@ -82,6 +82,11 @@ style="@style/BrowserTextMenuItem" android:text="@string/addBookmarkMenuTitle" /> + + Clear search input No compatible app installed Add Bookmark + Add Favorite Desktop Site From 65496326aaad9c07b6c174a2f6bc0d4031914665 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 12 Apr 2021 15:41:23 +0200 Subject: [PATCH 03/95] avoid reusing copy used in bookmarks screen in the browser screen --- .../main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 2 +- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 423ba327a9f1..f9f54ede894a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1088,7 +1088,7 @@ class BrowserTabFragment : } private fun bookmarkAdded(bookmarkId: Long, title: String?, url: String?) { - Snackbar.make(browserLayout, R.string.bookmarkEdited, Snackbar.LENGTH_LONG) + Snackbar.make(browserLayout, R.string.bookmarkAddedMessage, Snackbar.LENGTH_LONG) .setAction(R.string.edit) { val addBookmarkDialog = EditBookmarkDialogFragment.instance(bookmarkId, title, url) addBookmarkDialog.show(childFragmentManager, ADD_BOOKMARK_FRAGMENT_TAG) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c65499a0663..76974e90ec75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,8 @@ No compatible app installed Add Bookmark Add Favorite + Favorite added + Bookmark added Desktop Site From 0e33efe72f77ba1ba1a2acb0956c680d8198ca08 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 12 Apr 2021 15:41:52 +0200 Subject: [PATCH 04/95] configure dependency injection for FavoritesRepository --- .../app/bookmarks/di/BookmarksModule.kt | 37 +++++++++++++++++++ .../bookmarks/model/FavoritesRepository.kt | 37 +++++++++++++++++++ .../java/com/duckduckgo/app/di/DaoModule.kt | 3 ++ 3 files changed, 77 insertions(+) create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt new file mode 100644 index 000000000000..97a384ba31ec --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 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.di + +import com.duckduckgo.app.bookmarks.db.FavoritesDao +import com.duckduckgo.app.bookmarks.model.FavoritesDataRepository +import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.di.scopes.AppObjectGraph +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +@ContributesTo(scope = AppObjectGraph::class) +class BookmarksModule { + + @Provides + @Singleton + fun favoriteRepository(favoritesDao: FavoritesDao): FavoritesRepository { + return FavoritesDataRepository(favoritesDao) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt new file mode 100644 index 000000000000..adfa1b537029 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 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.model + +import com.duckduckgo.app.bookmarks.db.FavoriteEntity +import com.duckduckgo.app.bookmarks.db.FavoritesDao + +interface FavoritesRepository { + suspend fun insert(favorite: Favorite): Long +} + +data class Favorite( + var title: String, + var url: String +) + +class FavoritesDataRepository (private val favoritesDao: FavoritesDao) : FavoritesRepository { + + override suspend fun insert(favorite: Favorite): Long { + val lastPosition = favoritesDao.getLastPosition() ?: 0 + return favoritesDao.insert(FavoriteEntity(title = favorite.title, url = favorite.url, position = lastPosition + 1)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt index 58b231843e78..87d34a5e27c9 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt @@ -50,6 +50,9 @@ class DaoModule { @Provides fun providesBookmarksDao(database: AppDatabase) = database.bookmarksDao() + @Provides + fun providesFavoritesDao(database: AppDatabase) = database.favoritesDao() + @Provides fun providesTabsDao(database: AppDatabase) = database.tabsDao() From 9cd4caedc44b648adbda26a6712b0aeff604245e Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 12 Apr 2021 15:42:12 +0200 Subject: [PATCH 05/95] Allow users to add a favorite --- .../app/bookmarks/db/FavoritesDao.kt | 3 +++ .../app/browser/BrowserTabFragment.kt | 19 +++++++++++++++ .../app/browser/BrowserTabViewModel.kt | 23 ++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt index a2ee4f7bdf7a..ac75d466cdcc 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt @@ -43,4 +43,7 @@ interface FavoritesDao { @Query("select CAST(COUNT(*) AS BIT) from favorites") suspend fun hasBookmarks(): Boolean + + @Query("select position from favorites where id = ( select MAX(id) from favorites)") + suspend fun getLastPosition(): Int? } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index f9f54ede894a..cc01ccdd6603 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -533,6 +533,7 @@ class BrowserTabFragment : } is Command.LaunchNewTab -> browserActivity?.launchNewTab() is Command.ShowBookmarkAddedConfirmation -> bookmarkAdded(it.bookmarkId, it.title, it.url) + is Command.ShowFavoriteAddedConfirmation -> favoriteAdded(it.favoriteId, it.title, it.url) is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.fireproofWebsiteEntity) is Command.Navigate -> { navigate(it.url, it.headers) @@ -1097,6 +1098,16 @@ class BrowserTabFragment : .show() } + private fun favoriteAdded(favoriteId: Long, title: String, url: String) { + Snackbar.make(browserLayout, R.string.favoriteAddedMessage, Snackbar.LENGTH_LONG) + .setAction(R.string.edit) { + val addBookmarkDialog = EditBookmarkDialogFragment.instance(favoriteId, title, url) + addBookmarkDialog.show(childFragmentManager, ADD_FAVORITE_FRAGMENT_TAG) + addBookmarkDialog.listener = viewModel + } + .show() + } + private fun fireproofWebsiteConfirmation(entity: FireproofWebsiteEntity) { Snackbar.make( rootView, @@ -1398,6 +1409,7 @@ class BrowserTabFragment : private const val SKIP_HOME_ARG = "SKIP_HOME_ARG" private const val ADD_BOOKMARK_FRAGMENT_TAG = "ADD_BOOKMARK" + private const val ADD_FAVORITE_FRAGMENT_TAG = "ADD_FAVORITE" private const val KEYBOARD_DELAY = 200L private const val LAYOUT_TRANSITION_MS = 200L @@ -1506,6 +1518,13 @@ class BrowserTabFragment : viewModel.onBookmarkAddRequested() } } + onMenuItemClicked(view.addFavoritePopupMenuItem) { + launch { + //TODO: do we need a pixel here? + //pixel.fire(AppPixelName.MENU_ACTION_ADD_BOOKMARK_PRESSED.pixelName) + viewModel.onAddFavoriteMenuClicked() + } + } onMenuItemClicked(view.findInPageMenuItem) { pixel.fire(AppPixelName.MENU_ACTION_FIND_IN_PAGE_PRESSED) viewModel.onFindInPageSelected() diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 1fd6e7e92446..61c96fcbda72 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -41,6 +41,10 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.A 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.db.FavoriteEntity +import com.duckduckgo.app.bookmarks.db.FavoritesDao +import com.duckduckgo.app.bookmarks.model.Favorite +import com.duckduckgo.app.bookmarks.model.FavoritesRepository import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment.EditBookmarkListener import com.duckduckgo.app.brokensite.BrokenSiteData import com.duckduckgo.app.browser.BrowserTabViewModel.Command.* @@ -127,6 +131,7 @@ class BrowserTabViewModel( private val userWhitelistDao: UserWhitelistDao, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, + private val favoritesRepository: FavoritesRepository, private val fireproofWebsiteRepository: FireproofWebsiteRepository, private val locationPermissionsRepository: LocationPermissionsRepository, private val geoLocationPermissions: GeoLocationPermissions, @@ -255,6 +260,7 @@ class BrowserTabViewModel( class ShowFullScreen(val view: View) : Command() class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() class ShowBookmarkAddedConfirmation(val bookmarkId: Long, val title: String?, val url: String?) : Command() + class ShowFavoriteAddedConfirmation(val favoriteId: Long, val title: String, val url: String) : Command() class ShowFireproofWebSiteConfirmation(val fireproofWebsiteEntity: FireproofWebsiteEntity) : Command() object AskToDisableLoginDetection : Command() class AskToFireproofWebsite(val fireproofWebsite: FireproofWebsiteEntity) : Command() @@ -1303,6 +1309,20 @@ class BrowserTabViewModel( } } + suspend fun onAddFavoriteMenuClicked() { + val url = url ?: "" + val title = title ?: "" + val id = withContext(dispatchers.io()) { + if (url.isNotBlank()) { + faviconManager.persistFavicon(tabId, url) + } + favoritesRepository.insert(Favorite(title = title, url = url)) + } + withContext(dispatchers.main()) { + command.value = ShowBookmarkAddedConfirmation(id, title, url) + } + } + fun onFireproofWebsiteMenuClicked() { val domain = site?.domain ?: return viewModelScope.launch { @@ -1852,6 +1872,7 @@ class BrowserTabViewModelFactory @Inject constructor( private val userWhitelistDao: Provider, private val networkLeaderboardDao: Provider, private val bookmarksDao: Provider, + private val favoritesRepository: Provider, private val fireproofWebsiteRepository: Provider, private val locationPermissionsRepository: Provider, private val geoLocationPermissions: Provider, @@ -1878,7 +1899,7 @@ class BrowserTabViewModelFactory @Inject constructor( override fun create(modelClass: Class): T? { with(modelClass) { return when { - isAssignableFrom(BrowserTabViewModel::class.java) -> BrowserTabViewModel(statisticsUpdater.get(), queryUrlConverter.get(), duckDuckGoUrlDetector.get(), siteFactory.get(), tabRepository.get(), userWhitelistDao.get(), networkLeaderboardDao.get(), bookmarksDao.get(), fireproofWebsiteRepository.get(), locationPermissionsRepository.get(), geoLocationPermissions.get(), navigationAwareLoginDetector.get(), autoComplete.get(), appSettingsPreferencesStore.get(), longPressHandler.get(), webViewSessionStorage.get(), specialUrlDetector.get(), faviconManager.get(), addToHomeCapabilityDetector.get(), ctaViewModel.get(), searchCountDao.get(), pixel.get(), dispatchers, userEventsStore.get(), notificationDao.get(), useOurAppDetector.get(), variantManager.get(), fileDownloader.get(), globalPrivacyControl.get(), fireproofDialogsEventHandler.get()) as T + isAssignableFrom(BrowserTabViewModel::class.java) -> BrowserTabViewModel(statisticsUpdater.get(), queryUrlConverter.get(), duckDuckGoUrlDetector.get(), siteFactory.get(), tabRepository.get(), userWhitelistDao.get(), networkLeaderboardDao.get(), bookmarksDao.get(), favoritesRepository.get(), fireproofWebsiteRepository.get(), locationPermissionsRepository.get(), geoLocationPermissions.get(), navigationAwareLoginDetector.get(), autoComplete.get(), appSettingsPreferencesStore.get(), longPressHandler.get(), webViewSessionStorage.get(), specialUrlDetector.get(), faviconManager.get(), addToHomeCapabilityDetector.get(), ctaViewModel.get(), searchCountDao.get(), pixel.get(), dispatchers, userEventsStore.get(), notificationDao.get(), useOurAppDetector.get(), variantManager.get(), fileDownloader.get(), globalPrivacyControl.get(), fireproofDialogsEventHandler.get()) as T else -> null } } From ca10d300f2c3f87e6803a992be893346c4f8e4c3 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 12 Apr 2021 18:10:04 +0200 Subject: [PATCH 06/95] show favorite confirmation message when added --- .../main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 61c96fcbda72..91e2fc954635 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1319,7 +1319,7 @@ class BrowserTabViewModel( favoritesRepository.insert(Favorite(title = title, url = url)) } withContext(dispatchers.main()) { - command.value = ShowBookmarkAddedConfirmation(id, title, url) + command.value = ShowFavoriteAddedConfirmation(id, title, url) } } From 4322f454a854116a06f9348248786ae62637da0d Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 13 Apr 2021 12:35:45 +0200 Subject: [PATCH 07/95] Temp approach to show favorites and bookmarks in same screen --- .../app/bookmarks/db/FavoritesDao.kt | 4 +- .../bookmarks/model/FavoritesRepository.kt | 7 + .../app/bookmarks/ui/BookmarksActivity.kt | 225 +++++++++++++----- .../ui/BookmarksEntityQueryListener.kt | 2 +- .../app/bookmarks/ui/BookmarksViewModel.kt | 27 ++- 5 files changed, 196 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt index ac75d466cdcc..ba8414d77711 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt @@ -18,7 +18,9 @@ package com.duckduckgo.app.bookmarks.db import androidx.lifecycle.LiveData import androidx.room.* +import com.duckduckgo.app.tabs.model.TabEntity import io.reactivex.Single +import kotlinx.coroutines.flow.Flow @Dao interface FavoritesDao { @@ -27,7 +29,7 @@ interface FavoritesDao { fun insert(favorite: FavoriteEntity): Long @Query("select * from favorites") - fun favorites(): LiveData> + fun favorites(): Flow> @Query("select count(*) from favorites WHERE url LIKE :url") fun favoritesCountByUrl(url: String): Int diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index adfa1b537029..96c7c516c502 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -18,9 +18,12 @@ package com.duckduckgo.app.bookmarks.model import com.duckduckgo.app.bookmarks.db.FavoriteEntity import com.duckduckgo.app.bookmarks.db.FavoritesDao +import com.duckduckgo.app.tabs.model.TabEntity +import kotlinx.coroutines.flow.Flow interface FavoritesRepository { suspend fun insert(favorite: Favorite): Long + suspend fun favorites(): Flow> } data class Favorite( @@ -34,4 +37,8 @@ class FavoritesDataRepository (private val favoritesDao: FavoritesDao) : Favorit val lastPosition = favoritesDao.getLastPosition() ?: 0 return favoritesDao.insert(FavoriteEntity(title = favorite.title, url = favorite.url, position = lastPosition + 1)) } + + override suspend fun favorites(): Flow> { + return favoritesDao.favorites() + } } \ 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 index 81e274186dcc..49f2c2fa817d 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -30,9 +30,11 @@ import androidx.appcompat.widget.SearchView import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.model.Favorite import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.R.id.action_search @@ -45,15 +47,10 @@ import com.duckduckgo.app.global.view.html import com.duckduckgo.app.global.view.show import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_bookmarks.* -import kotlinx.android.synthetic.main.content_bookmarks.emptyBookmarks -import kotlinx.android.synthetic.main.content_bookmarks.recycler -import kotlinx.android.synthetic.main.include_toolbar.toolbar -import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.deleteBookmark -import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.editBookmark -import kotlinx.android.synthetic.main.view_bookmark_entry.view.favicon -import kotlinx.android.synthetic.main.view_bookmark_entry.view.overflowMenu -import kotlinx.android.synthetic.main.view_bookmark_entry.view.title -import kotlinx.android.synthetic.main.view_bookmark_entry.view.url +import kotlinx.android.synthetic.main.content_bookmarks.* +import kotlinx.android.synthetic.main.include_toolbar.* +import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.* +import kotlinx.android.synthetic.main.view_bookmark_entry.view.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -87,7 +84,7 @@ class BookmarksActivity : DuckDuckGoActivity() { Observer { viewState -> viewState?.let { if (it.showBookmarks) showBookmarks() else hideBookmarks() - adapter.bookmarks = it.bookmarks + adapter.bookmarkItems = it.favorites.map { BookmarksAdapter.BookmarkItems.Favorite(it) } + it.bookmarks.map { BookmarksAdapter.BookmarkItems.Bookmark(it) } invalidateOptionsMenu() } } @@ -175,89 +172,191 @@ class BookmarksActivity : DuckDuckGoActivity() { private val viewModel: BookmarksViewModel, private val lifecycleOwner: LifecycleOwner, private val faviconManager: FaviconManager - ) : Adapter() { + ) : RecyclerView.Adapter() { + + companion object { + const val FAVORITE_SECTION_TITLE_TYPE = 0 + const val BOOKMARK_SECTION_TITLE_TYPE = 1 + const val EMPTY_STATE_TYPE = 2 + const val BOOKMARK_TYPE = 3 + const val FAVORITE_TYPE = 4 + } + + sealed class BookmarkItems { + data class Bookmark(val bookmark: BookmarkEntity): BookmarkItems() + data class Favorite(val favorite: com.duckduckgo.app.bookmarks.model.Favorite): BookmarkItems() + } - var bookmarks: List = emptyList() + var bookmarkItems: List = emptyList() set(value) { field = value notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarksViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkScreenViewHolders { val inflater = LayoutInflater.from(parent.context) - val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) - return BookmarksViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) + return when (viewType) { + BOOKMARK_TYPE -> { + val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) + return BookmarkScreenViewHolders.BookmarksViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) + } + FAVORITE_TYPE -> { + val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) + return BookmarkScreenViewHolders.FavoriteViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) + } + else -> throw IllegalArgumentException("viewType not found") + } } - override fun onBindViewHolder(holder: BookmarksViewHolder, position: Int) { - holder.update(bookmarks[position]) + override fun getItemCount(): Int { + return bookmarkItems.size } - override fun getItemCount(): Int { - return bookmarks.size + override fun onBindViewHolder(holder: BookmarkScreenViewHolders, position: Int) { + when (holder) { + is BookmarkScreenViewHolders.BookmarksViewHolder -> { + holder.update((bookmarkItems[position] as BookmarkItems.Bookmark).bookmark) + } + is BookmarkScreenViewHolders.FavoriteViewHolder -> { + holder.update((bookmarkItems[position] as BookmarkItems.Favorite).favorite) + } + } + } + + override fun getItemViewType(position: Int): Int { + return when(bookmarkItems.get(position)){ + is BookmarkItems.Bookmark -> BOOKMARK_TYPE + is BookmarkItems.Favorite -> FAVORITE_TYPE + } } } - class BookmarksViewHolder( - private val layoutInflater: LayoutInflater, - itemView: View, - private val viewModel: BookmarksViewModel, - private val lifecycleOwner: LifecycleOwner, - private val faviconManager: FaviconManager - ) : ViewHolder(itemView) { + sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder(itemView) { + + class BookmarksViewHolder( + private val layoutInflater: LayoutInflater, + itemView: View, + private val viewModel: BookmarksViewModel, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager + ) : BookmarkScreenViewHolders(itemView) { - lateinit var bookmark: BookmarkEntity + lateinit var bookmark: BookmarkEntity - fun update(bookmark: BookmarkEntity) { - this.bookmark = bookmark + fun update(bookmark: BookmarkEntity) { + this.bookmark = bookmark - itemView.overflowMenu.contentDescription = itemView.context.getString( - R.string.bookmarkOverflowContentDescription, - bookmark.title - ) + itemView.overflowMenu.contentDescription = itemView.context.getString( + R.string.bookmarkOverflowContentDescription, + bookmark.title + ) - itemView.title.text = bookmark.title - itemView.url.text = parseDisplayUrl(bookmark.url) - loadFavicon(bookmark.url) + itemView.title.text = bookmark.title + itemView.url.text = parseDisplayUrl(bookmark.url) + loadFavicon(bookmark.url) - itemView.overflowMenu.setOnClickListener { - showOverFlowMenu(itemView.overflowMenu, bookmark) + itemView.overflowMenu.setOnClickListener { + showOverFlowMenu(itemView.overflowMenu, bookmark) + } + + itemView.setOnClickListener { + viewModel.onSelected(bookmark) + } } - itemView.setOnClickListener { - viewModel.onSelected(bookmark) + private fun loadFavicon(url: String) { + lifecycleOwner.lifecycleScope.launch { + faviconManager.loadToViewFromPersisted(url, itemView.favicon) + } } - } - private fun loadFavicon(url: String) { - lifecycleOwner.lifecycleScope.launch { - faviconManager.loadToViewFromPersisted(url, itemView.favicon) + private fun parseDisplayUrl(urlString: String): String { + val uri = Uri.parse(urlString) + return uri.baseHost ?: return urlString } - } - private fun parseDisplayUrl(urlString: String): String { - val uri = Uri.parse(urlString) - return uri.baseHost ?: return urlString - } + private fun showOverFlowMenu(anchor: ImageView, bookmark: BookmarkEntity) { + val popupMenu = BookmarksPopupMenu(layoutInflater) + val view = popupMenu.contentView + popupMenu.apply { + onMenuItemClicked(view.editBookmark) { editBookmark(bookmark) } + onMenuItemClicked(view.deleteBookmark) { deleteBookmark(bookmark) } + } + popupMenu.show(itemView, anchor) + } - private fun showOverFlowMenu(anchor: ImageView, bookmark: BookmarkEntity) { - val popupMenu = BookmarksPopupMenu(layoutInflater) - val view = popupMenu.contentView - popupMenu.apply { - onMenuItemClicked(view.editBookmark) { editBookmark(bookmark) } - onMenuItemClicked(view.deleteBookmark) { deleteBookmark(bookmark) } + private fun editBookmark(bookmark: BookmarkEntity) { + Timber.i("Editing bookmark ${bookmark.title}") + viewModel.onEditBookmarkRequested(bookmark) } - popupMenu.show(itemView, anchor) - } - private fun editBookmark(bookmark: BookmarkEntity) { - Timber.i("Editing bookmark ${bookmark.title}") - viewModel.onEditBookmarkRequested(bookmark) + private fun deleteBookmark(bookmark: BookmarkEntity) { + Timber.i("Deleting bookmark ${bookmark.title}") + viewModel.onDeleteRequested(bookmark) + } } - private fun deleteBookmark(bookmark: BookmarkEntity) { - Timber.i("Deleting bookmark ${bookmark.title}") - viewModel.onDeleteRequested(bookmark) + class FavoriteViewHolder( + private val layoutInflater: LayoutInflater, + itemView: View, + private val viewModel: BookmarksViewModel, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager + ) : BookmarkScreenViewHolders(itemView) { + + lateinit var favorite: Favorite + + fun update(favorite: Favorite) { + this.favorite = favorite + + itemView.overflowMenu.contentDescription = itemView.context.getString( + R.string.bookmarkOverflowContentDescription, + favorite.title + ) + + itemView.title.text = favorite.title + itemView.url.text = parseDisplayUrl(favorite.url) + loadFavicon(favorite.url) + + itemView.overflowMenu.setOnClickListener { + showOverFlowMenu(itemView.overflowMenu, favorite) + } + + itemView.setOnClickListener { + //viewModel.onSelected(favorite) + } + } + + private fun loadFavicon(url: String) { + lifecycleOwner.lifecycleScope.launch { + faviconManager.loadToViewFromPersisted(url, itemView.favicon) + } + } + + private fun parseDisplayUrl(urlString: String): String { + val uri = Uri.parse(urlString) + return uri.baseHost ?: return urlString + } + + private fun showOverFlowMenu(anchor: ImageView, favorite: Favorite) { + val popupMenu = BookmarksPopupMenu(layoutInflater) + val view = popupMenu.contentView + popupMenu.apply { + onMenuItemClicked(view.editBookmark) { editBookmark(favorite) } + onMenuItemClicked(view.deleteBookmark) { deleteBookmark(favorite) } + } + popupMenu.show(itemView, anchor) + } + + private fun editBookmark(favorite: Favorite) { + Timber.i("Editing favorite ${favorite.title}") + //viewModel.onEditBookmarkRequested(favorite) + } + + private fun deleteBookmark(favorite: Favorite) { + Timber.i("Deleting favorite ${favorite.title}") + //viewModel.onDeleteRequested(favorite) + } } } } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt index 578eaf44e3b8..785f420f2a67 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt @@ -26,7 +26,7 @@ class BookmarksEntityQueryListener( override fun onQueryTextChange(newText: String): Boolean { if (bookmarks != null) { - adapter.bookmarks = filter(newText, bookmarks) + //adapter.bookmarks = filter(newText, bookmarks) adapter.notifyDataSetChanged() } return true 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 index d8716bd4a551..3dfe78b94869 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -19,6 +19,9 @@ package com.duckduckgo.app.bookmarks.ui import androidx.lifecycle.* import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.db.FavoriteEntity +import com.duckduckgo.app.bookmarks.model.Favorite +import com.duckduckgo.app.bookmarks.model.FavoritesRepository import com.duckduckgo.app.bookmarks.ui.BookmarksViewModel.Command.* import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment.EditBookmarkListener import com.duckduckgo.app.browser.favicon.FaviconManager @@ -29,11 +32,14 @@ import com.duckduckgo.di.scopes.AppObjectGraph import com.squareup.anvil.annotations.ContributesMultibinding import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Provider class BookmarksViewModel( + private val favoritesRepository: FavoritesRepository, val dao: BookmarksDao, private val faviconManager: FaviconManager, private val dispatcherProvider: DispatcherProvider @@ -41,16 +47,16 @@ class BookmarksViewModel( data class ViewState( val showBookmarks: Boolean = false, + val showFavorites: Boolean = false, val enableSearch: Boolean = false, - val bookmarks: List = emptyList() + val bookmarks: List = emptyList(), + val favorites: List = emptyList() ) sealed class Command { - class OpenBookmark(val bookmark: BookmarkEntity) : Command() class ConfirmDeleteBookmark(val bookmark: BookmarkEntity) : Command() class ShowEditBookmark(val bookmark: BookmarkEntity) : Command() - } companion object { @@ -66,6 +72,11 @@ class BookmarksViewModel( init { viewState.value = ViewState() bookmarks.observeForever(bookmarksObserver) + viewModelScope.launch { + favoritesRepository.favorites().collect { + onFavoritesChanged(it) + } + } } override fun onCleared() { @@ -87,6 +98,13 @@ class BookmarksViewModel( ) } + private fun onFavoritesChanged(favorites: List) { + viewState.value = viewState.value?.copy( + showFavorites = favorites.isNotEmpty(), + favorites = favorites.map { Favorite(it.title, it.url) } + ) + } + fun onSelected(bookmark: BookmarkEntity) { command.value = OpenBookmark(bookmark) } @@ -116,6 +134,7 @@ class BookmarksViewModel( @ContributesMultibinding(AppObjectGraph::class) class BookmarksViewModelFactory @Inject constructor( + private val favoritesRepository: Provider, private val dao: Provider, private val faviconManager: Provider, private val dispatcherProvider: Provider @@ -123,7 +142,7 @@ class BookmarksViewModelFactory @Inject constructor( override fun create(modelClass: Class): T? { with(modelClass) { return when { - isAssignableFrom(BookmarksViewModel::class.java) -> (BookmarksViewModel(dao.get(), faviconManager.get(), dispatcherProvider.get()) as T) + isAssignableFrom(BookmarksViewModel::class.java) -> (BookmarksViewModel(favoritesRepository.get(), dao.get(), faviconManager.get(), dispatcherProvider.get()) as T) else -> null } } From 27221fb26a675face00171e08221bda6431b236a Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 14 Apr 2021 10:21:42 +0200 Subject: [PATCH 08/95] add recycler view dependency (to use ConcatAdapter) --- app/build.gradle | 1 + versions.properties | 1 + 2 files changed, 2 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index d5f75aff4631..b504e04e7aea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -134,6 +134,7 @@ dependencies { implementation AndroidX.appCompat implementation Google.android.material implementation AndroidX.constraintLayout + implementation AndroidX.recyclerView implementation AndroidX.swipeRefreshLayout implementation AndroidX.webkit implementation Square.okHttp3.okHttp diff --git a/versions.properties b/versions.properties index 4793565a8843..0a3800d2313b 100644 --- a/versions.properties +++ b/versions.properties @@ -8,6 +8,7 @@ version.androidx.appcompat=1.2.0 version.androidx.arch.core=2.1.0 version.androidx.constraintlayout=2.0.4 +version.androidx.recyclerview=1.2.0 version.androidx.core=1.3.2 version.androidx.fragment=1.2.5 version.androidx.legacy=1.0.0 From fbdacf6e15f014954c43c68b5dcb88fb2644e199 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 14 Apr 2021 10:21:54 +0200 Subject: [PATCH 09/95] fix migration error --- app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e9771b32ec29..27deff848d84 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 @@ -401,7 +401,7 @@ class MigrationsProvider( } } - val MIGRATION_31_TO_32: Migration = object : Migration(30, 31) { + val MIGRATION_31_TO_32: Migration = object : Migration(31, 32) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `favorites` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `position` INTEGER NOT NULL)") } From 275f0284023490339dfbb79eba362f8a52d0e9fe Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 14 Apr 2021 10:22:30 +0200 Subject: [PATCH 10/95] (wip) refactor to use ConcatAdapter: - bookmarks listed with section title --- .../app/bookmarks/ui/BookmarksActivity.kt | 185 +++++++++++------- app/src/main/res/values/strings.xml | 1 + 2 files changed, 114 insertions(+), 72 deletions(-) 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 index 49f2c2fa817d..aa75d0564095 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -30,9 +30,8 @@ import androidx.appcompat.widget.SearchView import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.Adapter -import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.model.Favorite import com.duckduckgo.app.browser.BrowserActivity @@ -51,6 +50,7 @@ import kotlinx.android.synthetic.main.content_bookmarks.* import kotlinx.android.synthetic.main.include_toolbar.* import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.* import kotlinx.android.synthetic.main.view_bookmark_entry.view.* +import kotlinx.android.synthetic.main.view_location_permissions_section_title.view.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -60,7 +60,8 @@ class BookmarksActivity : DuckDuckGoActivity() { @Inject lateinit var faviconManager: FaviconManager - lateinit var adapter: BookmarksAdapter + lateinit var bookmarksAdapter: BookmarksAdapter + lateinit var favoritesAdapter: FavoritesAdapter private var deleteDialog: AlertDialog? = null private val viewModel: BookmarksViewModel by bindViewModel() @@ -74,8 +75,9 @@ class BookmarksActivity : DuckDuckGoActivity() { } private fun setupBookmarksRecycler() { - adapter = BookmarksAdapter(layoutInflater, viewModel, this, faviconManager) - recycler.adapter = adapter + bookmarksAdapter = BookmarksAdapter(layoutInflater, viewModel, this, faviconManager) + favoritesAdapter = FavoritesAdapter(layoutInflater, viewModel, this, faviconManager) + recycler.adapter = ConcatAdapter(favoritesAdapter, bookmarksAdapter) } private fun observeViewModel() { @@ -84,7 +86,8 @@ class BookmarksActivity : DuckDuckGoActivity() { Observer { viewState -> viewState?.let { if (it.showBookmarks) showBookmarks() else hideBookmarks() - adapter.bookmarkItems = it.favorites.map { BookmarksAdapter.BookmarkItems.Favorite(it) } + it.bookmarks.map { BookmarksAdapter.BookmarkItems.Bookmark(it) } + favoritesAdapter.bookmarkItems = it.favorites.map { FavoritesAdapter.Favorite(it) } + bookmarksAdapter.bookmarkItems = it.bookmarks invalidateOptionsMenu() } } @@ -106,7 +109,7 @@ class BookmarksActivity : DuckDuckGoActivity() { menuInflater.inflate(bookmark_activity_menu, menu) val searchItem = menu?.findItem(action_search) val searchView = searchItem?.actionView as SearchView - searchView.setOnQueryTextListener(BookmarksEntityQueryListener(viewModel.viewState.value?.bookmarks, adapter)) + //searchView.setOnQueryTextListener(BookmarksEntityQueryListener(viewModel.viewState.value?.bookmarks, adapter)) return super.onCreateOptionsMenu(menu) } @@ -175,19 +178,14 @@ class BookmarksActivity : DuckDuckGoActivity() { ) : RecyclerView.Adapter() { companion object { - const val FAVORITE_SECTION_TITLE_TYPE = 0 - const val BOOKMARK_SECTION_TITLE_TYPE = 1 - const val EMPTY_STATE_TYPE = 2 - const val BOOKMARK_TYPE = 3 - const val FAVORITE_TYPE = 4 - } + const val BOOKMARK_SECTION_TITLE_TYPE = 0 + const val EMPTY_STATE_TYPE = 1 + const val BOOKMARK_TYPE = 2 - sealed class BookmarkItems { - data class Bookmark(val bookmark: BookmarkEntity): BookmarkItems() - data class Favorite(val favorite: com.duckduckgo.app.bookmarks.model.Favorite): BookmarkItems() + const val BOOKMARK_SECTION_TITLE_SIZE = 1 } - var bookmarkItems: List = emptyList() + var bookmarkItems: List = emptyList() set(value) { field = value notifyDataSetChanged() @@ -200,39 +198,50 @@ class BookmarksActivity : DuckDuckGoActivity() { val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) return BookmarkScreenViewHolders.BookmarksViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) } - FAVORITE_TYPE -> { - val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) - return BookmarkScreenViewHolders.FavoriteViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) + BOOKMARK_SECTION_TITLE_TYPE -> { + val view = inflater.inflate(R.layout.view_location_permissions_section_title, parent, false) + return BookmarkScreenViewHolders.SectionTitle(view) } else -> throw IllegalArgumentException("viewType not found") } } override fun getItemCount(): Int { - return bookmarkItems.size + return headerItemsSize() + bookmarkItems.size } override fun onBindViewHolder(holder: BookmarkScreenViewHolders, position: Int) { when (holder) { is BookmarkScreenViewHolders.BookmarksViewHolder -> { - holder.update((bookmarkItems[position] as BookmarkItems.Bookmark).bookmark) + holder.update(bookmarkItems[position - headerItemsSize()]) } - is BookmarkScreenViewHolders.FavoriteViewHolder -> { - holder.update((bookmarkItems[position] as BookmarkItems.Favorite).favorite) + is BookmarkScreenViewHolders.SectionTitle -> { + holder.bind() } } } override fun getItemViewType(position: Int): Int { - return when(bookmarkItems.get(position)){ - is BookmarkItems.Bookmark -> BOOKMARK_TYPE - is BookmarkItems.Favorite -> FAVORITE_TYPE + return if (position == 0 ) { + BOOKMARK_SECTION_TITLE_TYPE + } else { + BOOKMARK_TYPE } } + + private fun headerItemsSize(): Int { + return BOOKMARK_SECTION_TITLE_SIZE + } } sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder(itemView) { + class SectionTitle(itemView: View) : BookmarkScreenViewHolders(itemView) { + fun bind() { + itemView.locationPermissionsSectionTitle.setText(R.string.bookmarksSectionTitle) + } + } + class BookmarksViewHolder( private val layoutInflater: LayoutInflater, itemView: View, @@ -295,68 +304,100 @@ class BookmarksActivity : DuckDuckGoActivity() { viewModel.onDeleteRequested(bookmark) } } + } - class FavoriteViewHolder( - private val layoutInflater: LayoutInflater, - itemView: View, - private val viewModel: BookmarksViewModel, - private val lifecycleOwner: LifecycleOwner, - private val faviconManager: FaviconManager - ) : BookmarkScreenViewHolders(itemView) { - - lateinit var favorite: Favorite - - fun update(favorite: Favorite) { - this.favorite = favorite + class FavoritesAdapter( + private val layoutInflater: LayoutInflater, + private val viewModel: BookmarksViewModel, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager + ) : RecyclerView.Adapter() { - itemView.overflowMenu.contentDescription = itemView.context.getString( - R.string.bookmarkOverflowContentDescription, - favorite.title - ) + companion object { + const val FAVORITE_SECTION_TITLE_TYPE = 0 + const val BOOKMARK_SECTION_TITLE_TYPE = 1 + const val EMPTY_STATE_TYPE = 2 + const val BOOKMARK_TYPE = 3 + const val FAVORITE_TYPE = 4 + } - itemView.title.text = favorite.title - itemView.url.text = parseDisplayUrl(favorite.url) - loadFavicon(favorite.url) + data class Favorite(val favorite: com.duckduckgo.app.bookmarks.model.Favorite) - itemView.overflowMenu.setOnClickListener { - showOverFlowMenu(itemView.overflowMenu, favorite) - } + var bookmarkItems: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } - itemView.setOnClickListener { - //viewModel.onSelected(favorite) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + FAVORITE_TYPE -> { + val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) + return FavoriteViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) } + else -> throw IllegalArgumentException("viewType not found") } + } - private fun loadFavicon(url: String) { - lifecycleOwner.lifecycleScope.launch { - faviconManager.loadToViewFromPersisted(url, itemView.favicon) + override fun getItemCount(): Int { + return bookmarkItems.size + } + + override fun onBindViewHolder(holder: FavoriteViewHolder, position: Int) { + when (holder) { + is FavoriteViewHolder -> { + holder.update(bookmarkItems[position].favorite) } } + } - private fun parseDisplayUrl(urlString: String): String { - val uri = Uri.parse(urlString) - return uri.baseHost ?: return urlString - } + override fun getItemViewType(position: Int): Int { + return FAVORITE_TYPE + } + } - private fun showOverFlowMenu(anchor: ImageView, favorite: Favorite) { - val popupMenu = BookmarksPopupMenu(layoutInflater) - val view = popupMenu.contentView - popupMenu.apply { - onMenuItemClicked(view.editBookmark) { editBookmark(favorite) } - onMenuItemClicked(view.deleteBookmark) { deleteBookmark(favorite) } - } - popupMenu.show(itemView, anchor) + + class FavoriteViewHolder( + private val layoutInflater: LayoutInflater, + itemView: View, + private val viewModel: BookmarksViewModel, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager + ) : RecyclerView.ViewHolder(itemView) { + + lateinit var favorite: Favorite + + fun update(favorite: Favorite) { + this.favorite = favorite + + itemView.overflowMenu.contentDescription = itemView.context.getString( + R.string.bookmarkOverflowContentDescription, + favorite.title + ) + + itemView.title.text = favorite.title + itemView.url.text = parseDisplayUrl(favorite.url) + loadFavicon(favorite.url) + + itemView.overflowMenu.setOnClickListener { + //showOverFlowMenu(itemView.overflowMenu, favorite) } - private fun editBookmark(favorite: Favorite) { - Timber.i("Editing favorite ${favorite.title}") - //viewModel.onEditBookmarkRequested(favorite) + itemView.setOnClickListener { + //viewModel.onSelected(favorite) } + } - private fun deleteBookmark(favorite: Favorite) { - Timber.i("Deleting favorite ${favorite.title}") - //viewModel.onDeleteRequested(favorite) + private fun loadFavicon(url: String) { + lifecycleOwner.lifecycleScope.launch { + faviconManager.loadToViewFromPersisted(url, itemView.favicon) } } + + private fun parseDisplayUrl(urlString: String): String { + val uri = Uri.parse(urlString) + return uri.baseHost ?: return urlString + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 76974e90ec75..bebd698a312e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,6 +212,7 @@ More options for bookmark %s Bookmark added Deleted <b>%s</b> + Bookmarks Confirm From 2b30d23ea2ffb486c181c314166f9ab5d88d7811 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 14 Apr 2021 10:57:29 +0200 Subject: [PATCH 11/95] Add empty case hint as part of recycler view. --- .../app/bookmarks/ui/BookmarksActivity.kt | 45 ++++++++++++------- .../app/bookmarks/ui/BookmarksViewModel.kt | 2 - app/src/main/res/layout/content_bookmarks.xml | 12 ----- app/src/main/res/values/strings.xml | 1 + 4 files changed, 30 insertions(+), 30 deletions(-) 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 index aa75d0564095..be76a38ef2ab 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -50,6 +50,7 @@ import kotlinx.android.synthetic.main.content_bookmarks.* import kotlinx.android.synthetic.main.include_toolbar.* import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.* import kotlinx.android.synthetic.main.view_bookmark_entry.view.* +import kotlinx.android.synthetic.main.view_fireproof_website_empty_hint.view.* import kotlinx.android.synthetic.main.view_location_permissions_section_title.view.* import kotlinx.coroutines.launch import timber.log.Timber @@ -85,7 +86,6 @@ class BookmarksActivity : DuckDuckGoActivity() { this, Observer { viewState -> viewState?.let { - if (it.showBookmarks) showBookmarks() else hideBookmarks() favoritesAdapter.bookmarkItems = it.favorites.map { FavoritesAdapter.Favorite(it) } bookmarksAdapter.bookmarkItems = it.bookmarks invalidateOptionsMenu() @@ -124,16 +124,6 @@ class BookmarksActivity : DuckDuckGoActivity() { dialog.listener = viewModel } - private fun showBookmarks() { - recycler.show() - emptyBookmarks.gone() - } - - private fun hideBookmarks() { - recycler.gone() - emptyBookmarks.show() - } - private fun openBookmark(bookmark: BookmarkEntity) { startActivity(BrowserActivity.intent(this, bookmark.url)) finish() @@ -183,6 +173,7 @@ class BookmarksActivity : DuckDuckGoActivity() { const val BOOKMARK_TYPE = 2 const val BOOKMARK_SECTION_TITLE_SIZE = 1 + const val BOOKMARK_EMPTY_HINT_SIZE = 1 } var bookmarkItems: List = emptyList() @@ -202,12 +193,16 @@ class BookmarksActivity : DuckDuckGoActivity() { val view = inflater.inflate(R.layout.view_location_permissions_section_title, parent, false) return BookmarkScreenViewHolders.SectionTitle(view) } + EMPTY_STATE_TYPE -> { + val view = inflater.inflate(R.layout.view_fireproof_website_empty_hint, parent, false) + BookmarkScreenViewHolders.EmptyHint(view) + } else -> throw IllegalArgumentException("viewType not found") } } override fun getItemCount(): Int { - return headerItemsSize() + bookmarkItems.size + return headerItemsSize() + listSize() } override fun onBindViewHolder(holder: BookmarkScreenViewHolders, position: Int) { @@ -218,20 +213,32 @@ class BookmarksActivity : DuckDuckGoActivity() { is BookmarkScreenViewHolders.SectionTitle -> { holder.bind() } + is BookmarkScreenViewHolders.EmptyHint -> { + holder.bind() + } } } override fun getItemViewType(position: Int): Int { - return if (position == 0 ) { - BOOKMARK_SECTION_TITLE_TYPE - } else { - BOOKMARK_TYPE + return when { + position == 0 -> { + BOOKMARK_SECTION_TITLE_TYPE + } + bookmarkItems.isEmpty() -> { + EMPTY_STATE_TYPE + } + else -> { + BOOKMARK_TYPE + } } } private fun headerItemsSize(): Int { return BOOKMARK_SECTION_TITLE_SIZE } + + private fun listSize() = if (bookmarkItems.isEmpty()) BOOKMARK_EMPTY_HINT_SIZE else bookmarkItems.size + } sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -242,6 +249,12 @@ class BookmarksActivity : DuckDuckGoActivity() { } } + class EmptyHint(itemView: View) : BookmarkScreenViewHolders(itemView) { + fun bind() { + itemView.fireproofWebsiteEmptyHint.setText(R.string.bookmarksEmptyHint) + } + } + class BookmarksViewHolder( private val layoutInflater: LayoutInflater, itemView: View, 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 index 3dfe78b94869..edffb2360acb 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -46,7 +46,6 @@ class BookmarksViewModel( ) : EditBookmarkListener, ViewModel() { data class ViewState( - val showBookmarks: Boolean = false, val showFavorites: Boolean = false, val enableSearch: Boolean = false, val bookmarks: List = emptyList(), @@ -92,7 +91,6 @@ class BookmarksViewModel( private fun onBookmarksChanged(bookmarks: List) { viewState.value = viewState.value?.copy( - showBookmarks = bookmarks.isNotEmpty(), bookmarks = bookmarks, enableSearch = bookmarks.size > MIN_BOOKMARKS_FOR_SEARCH ) diff --git a/app/src/main/res/layout/content_bookmarks.xml b/app/src/main/res/layout/content_bookmarks.xml index dc72e71752ac..becebdf64bd3 100644 --- a/app/src/main/res/layout/content_bookmarks.xml +++ b/app/src/main/res/layout/content_bookmarks.xml @@ -29,16 +29,4 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listItem="@layout/view_bookmark_entry" /> - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bebd698a312e..522eda857447 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -213,6 +213,7 @@ Bookmark added Deleted <b>%s</b> Bookmarks + No bookmarks added yet Confirm From f5bb832bd17df2b38fc08dd4d4446ff6b27a7206 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 14 Apr 2021 14:46:49 +0200 Subject: [PATCH 12/95] - Extracted adapters out from BookmarkActivity class - Added logic to display empty hint in favorites adapter - Updated new UI elements to be displayed correctly and with expected copy --- .../app/bookmarks/ui/BookmarksActivity.kt | 274 +----------------- .../app/bookmarks/ui/BookmarksAdapter.kt | 190 ++++++++++++ .../ui/BookmarksEntityQueryListener.kt | 2 +- .../app/bookmarks/ui/BookmarksViewModel.kt | 12 + .../app/bookmarks/ui/FavoritesAdapter.kt | 197 +++++++++++++ .../res/layout/view_boomark_empty_hint.xml | 33 +++ app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/themes.xml | 2 + 9 files changed, 439 insertions(+), 274 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt create mode 100644 app/src/main/res/layout/view_boomark_empty_hint.xml 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 index be76a38ef2ab..fd0431dbb388 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -18,42 +18,24 @@ package com.duckduckgo.app.bookmarks.ui import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.bookmarks.db.BookmarkEntity -import com.duckduckgo.app.bookmarks.model.Favorite import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.R.id.action_search import com.duckduckgo.app.browser.R.menu.bookmark_activity_menu import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.global.DuckDuckGoActivity -import com.duckduckgo.app.global.baseHost -import com.duckduckgo.app.global.view.gone import com.duckduckgo.app.global.view.html -import com.duckduckgo.app.global.view.show import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_bookmarks.* import kotlinx.android.synthetic.main.content_bookmarks.* import kotlinx.android.synthetic.main.include_toolbar.* -import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.* -import kotlinx.android.synthetic.main.view_bookmark_entry.view.* -import kotlinx.android.synthetic.main.view_fireproof_website_empty_hint.view.* -import kotlinx.android.synthetic.main.view_location_permissions_section_title.view.* -import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject class BookmarksActivity : DuckDuckGoActivity() { @@ -86,7 +68,7 @@ class BookmarksActivity : DuckDuckGoActivity() { this, Observer { viewState -> viewState?.let { - favoritesAdapter.bookmarkItems = it.favorites.map { FavoritesAdapter.Favorite(it) } + favoritesAdapter.favoriteItems = it.favorites bookmarksAdapter.bookmarkItems = it.bookmarks invalidateOptionsMenu() } @@ -159,258 +141,4 @@ class BookmarksActivity : DuckDuckGoActivity() { // Fragment Tags private const val EDIT_BOOKMARK_FRAGMENT_TAG = "EDIT_BOOKMARK" } - - class BookmarksAdapter( - private val layoutInflater: LayoutInflater, - private val viewModel: BookmarksViewModel, - private val lifecycleOwner: LifecycleOwner, - private val faviconManager: FaviconManager - ) : RecyclerView.Adapter() { - - companion object { - const val BOOKMARK_SECTION_TITLE_TYPE = 0 - const val EMPTY_STATE_TYPE = 1 - const val BOOKMARK_TYPE = 2 - - const val BOOKMARK_SECTION_TITLE_SIZE = 1 - const val BOOKMARK_EMPTY_HINT_SIZE = 1 - } - - var bookmarkItems: List = emptyList() - set(value) { - field = value - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkScreenViewHolders { - val inflater = LayoutInflater.from(parent.context) - return when (viewType) { - BOOKMARK_TYPE -> { - val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) - return BookmarkScreenViewHolders.BookmarksViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) - } - BOOKMARK_SECTION_TITLE_TYPE -> { - val view = inflater.inflate(R.layout.view_location_permissions_section_title, parent, false) - return BookmarkScreenViewHolders.SectionTitle(view) - } - EMPTY_STATE_TYPE -> { - val view = inflater.inflate(R.layout.view_fireproof_website_empty_hint, parent, false) - BookmarkScreenViewHolders.EmptyHint(view) - } - else -> throw IllegalArgumentException("viewType not found") - } - } - - override fun getItemCount(): Int { - return headerItemsSize() + listSize() - } - - override fun onBindViewHolder(holder: BookmarkScreenViewHolders, position: Int) { - when (holder) { - is BookmarkScreenViewHolders.BookmarksViewHolder -> { - holder.update(bookmarkItems[position - headerItemsSize()]) - } - is BookmarkScreenViewHolders.SectionTitle -> { - holder.bind() - } - is BookmarkScreenViewHolders.EmptyHint -> { - holder.bind() - } - } - } - - override fun getItemViewType(position: Int): Int { - return when { - position == 0 -> { - BOOKMARK_SECTION_TITLE_TYPE - } - bookmarkItems.isEmpty() -> { - EMPTY_STATE_TYPE - } - else -> { - BOOKMARK_TYPE - } - } - } - - private fun headerItemsSize(): Int { - return BOOKMARK_SECTION_TITLE_SIZE - } - - private fun listSize() = if (bookmarkItems.isEmpty()) BOOKMARK_EMPTY_HINT_SIZE else bookmarkItems.size - - } - - sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder(itemView) { - - class SectionTitle(itemView: View) : BookmarkScreenViewHolders(itemView) { - fun bind() { - itemView.locationPermissionsSectionTitle.setText(R.string.bookmarksSectionTitle) - } - } - - class EmptyHint(itemView: View) : BookmarkScreenViewHolders(itemView) { - fun bind() { - itemView.fireproofWebsiteEmptyHint.setText(R.string.bookmarksEmptyHint) - } - } - - class BookmarksViewHolder( - private val layoutInflater: LayoutInflater, - itemView: View, - private val viewModel: BookmarksViewModel, - private val lifecycleOwner: LifecycleOwner, - private val faviconManager: FaviconManager - ) : BookmarkScreenViewHolders(itemView) { - - lateinit var bookmark: BookmarkEntity - - fun update(bookmark: BookmarkEntity) { - this.bookmark = bookmark - - itemView.overflowMenu.contentDescription = itemView.context.getString( - R.string.bookmarkOverflowContentDescription, - bookmark.title - ) - - itemView.title.text = bookmark.title - itemView.url.text = parseDisplayUrl(bookmark.url) - loadFavicon(bookmark.url) - - itemView.overflowMenu.setOnClickListener { - showOverFlowMenu(itemView.overflowMenu, bookmark) - } - - itemView.setOnClickListener { - viewModel.onSelected(bookmark) - } - } - - private fun loadFavicon(url: String) { - lifecycleOwner.lifecycleScope.launch { - faviconManager.loadToViewFromPersisted(url, itemView.favicon) - } - } - - private fun parseDisplayUrl(urlString: String): String { - val uri = Uri.parse(urlString) - return uri.baseHost ?: return urlString - } - - private fun showOverFlowMenu(anchor: ImageView, bookmark: BookmarkEntity) { - val popupMenu = BookmarksPopupMenu(layoutInflater) - val view = popupMenu.contentView - popupMenu.apply { - onMenuItemClicked(view.editBookmark) { editBookmark(bookmark) } - onMenuItemClicked(view.deleteBookmark) { deleteBookmark(bookmark) } - } - popupMenu.show(itemView, anchor) - } - - private fun editBookmark(bookmark: BookmarkEntity) { - Timber.i("Editing bookmark ${bookmark.title}") - viewModel.onEditBookmarkRequested(bookmark) - } - - private fun deleteBookmark(bookmark: BookmarkEntity) { - Timber.i("Deleting bookmark ${bookmark.title}") - viewModel.onDeleteRequested(bookmark) - } - } - } - - class FavoritesAdapter( - private val layoutInflater: LayoutInflater, - private val viewModel: BookmarksViewModel, - private val lifecycleOwner: LifecycleOwner, - private val faviconManager: FaviconManager - ) : RecyclerView.Adapter() { - - companion object { - const val FAVORITE_SECTION_TITLE_TYPE = 0 - const val BOOKMARK_SECTION_TITLE_TYPE = 1 - const val EMPTY_STATE_TYPE = 2 - const val BOOKMARK_TYPE = 3 - const val FAVORITE_TYPE = 4 - } - - data class Favorite(val favorite: com.duckduckgo.app.bookmarks.model.Favorite) - - var bookmarkItems: List = emptyList() - set(value) { - field = value - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteViewHolder { - val inflater = LayoutInflater.from(parent.context) - return when (viewType) { - FAVORITE_TYPE -> { - val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) - return FavoriteViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) - } - else -> throw IllegalArgumentException("viewType not found") - } - } - - override fun getItemCount(): Int { - return bookmarkItems.size - } - - override fun onBindViewHolder(holder: FavoriteViewHolder, position: Int) { - when (holder) { - is FavoriteViewHolder -> { - holder.update(bookmarkItems[position].favorite) - } - } - } - - override fun getItemViewType(position: Int): Int { - return FAVORITE_TYPE - } - } - - - class FavoriteViewHolder( - private val layoutInflater: LayoutInflater, - itemView: View, - private val viewModel: BookmarksViewModel, - private val lifecycleOwner: LifecycleOwner, - private val faviconManager: FaviconManager - ) : RecyclerView.ViewHolder(itemView) { - - lateinit var favorite: Favorite - - fun update(favorite: Favorite) { - this.favorite = favorite - - itemView.overflowMenu.contentDescription = itemView.context.getString( - R.string.bookmarkOverflowContentDescription, - favorite.title - ) - - itemView.title.text = favorite.title - itemView.url.text = parseDisplayUrl(favorite.url) - loadFavicon(favorite.url) - - itemView.overflowMenu.setOnClickListener { - //showOverFlowMenu(itemView.overflowMenu, favorite) - } - - itemView.setOnClickListener { - //viewModel.onSelected(favorite) - } - } - - private fun loadFavicon(url: String) { - lifecycleOwner.lifecycleScope.launch { - faviconManager.loadToViewFromPersisted(url, itemView.favicon) - } - } - - private fun parseDisplayUrl(urlString: String): String { - val uri = Uri.parse(urlString) - return uri.baseHost ?: return urlString - } - } } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt new file mode 100644 index 000000000000..f35e38de65aa --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021 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.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.global.baseHost +import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.* +import kotlinx.android.synthetic.main.view_bookmark_entry.view.* +import kotlinx.android.synthetic.main.view_boomark_empty_hint.view.* +import kotlinx.android.synthetic.main.view_location_permissions_section_title.view.* +import kotlinx.coroutines.launch +import timber.log.Timber + +class BookmarksAdapter( + private val layoutInflater: LayoutInflater, + private val viewModel: BookmarksViewModel, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager +) : RecyclerView.Adapter() { + + companion object { + const val BOOKMARK_SECTION_TITLE_TYPE = 0 + const val EMPTY_STATE_TYPE = 1 + const val BOOKMARK_TYPE = 2 + + const val BOOKMARK_SECTION_TITLE_SIZE = 1 + const val BOOKMARK_EMPTY_HINT_SIZE = 1 + } + + var bookmarkItems: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkScreenViewHolders { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + BOOKMARK_TYPE -> { + val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) + return BookmarkScreenViewHolders.BookmarksViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) + } + BOOKMARK_SECTION_TITLE_TYPE -> { + val view = inflater.inflate(R.layout.view_location_permissions_section_title, parent, false) + return BookmarkScreenViewHolders.SectionTitle(view) + } + EMPTY_STATE_TYPE -> { + val view = inflater.inflate(R.layout.view_boomark_empty_hint, parent, false) + BookmarkScreenViewHolders.EmptyHint(view) + } + else -> throw IllegalArgumentException("viewType not found") + } + } + + override fun getItemCount(): Int { + return headerItemsSize() + listSize() + } + + override fun onBindViewHolder(holder: BookmarkScreenViewHolders, position: Int) { + when (holder) { + is BookmarkScreenViewHolders.BookmarksViewHolder -> { + holder.update(bookmarkItems[position - headerItemsSize()]) + } + is BookmarkScreenViewHolders.SectionTitle -> { + holder.bind() + } + is BookmarkScreenViewHolders.EmptyHint -> { + holder.bind() + } + } + } + + override fun getItemViewType(position: Int): Int { + return when { + position == 0 -> { + BOOKMARK_SECTION_TITLE_TYPE + } + bookmarkItems.isEmpty() -> { + EMPTY_STATE_TYPE + } + else -> { + BOOKMARK_TYPE + } + } + } + + private fun headerItemsSize(): Int { + return BOOKMARK_SECTION_TITLE_SIZE + } + + private fun listSize() = if (bookmarkItems.isEmpty()) BOOKMARK_EMPTY_HINT_SIZE else bookmarkItems.size +} + +sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder(itemView) { + + class SectionTitle(itemView: View) : BookmarkScreenViewHolders(itemView) { + fun bind() { + itemView.locationPermissionsSectionTitle.setText(R.string.bookmarksSectionTitle) + } + } + + class EmptyHint(itemView: View) : BookmarkScreenViewHolders(itemView) { + fun bind() { + itemView.bookmarksEmptyHint.setText(R.string.bookmarksEmptyHint) + } + } + + class BookmarksViewHolder( + private val layoutInflater: LayoutInflater, + itemView: View, + private val viewModel: BookmarksViewModel, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager + ) : BookmarkScreenViewHolders(itemView) { + + fun update(bookmark: BookmarkEntity) { + itemView.overflowMenu.contentDescription = itemView.context.getString( + R.string.bookmarkOverflowContentDescription, + bookmark.title + ) + + itemView.title.text = bookmark.title + itemView.url.text = parseDisplayUrl(bookmark.url) + loadFavicon(bookmark.url) + + itemView.overflowMenu.setOnClickListener { + showOverFlowMenu(itemView.overflowMenu, bookmark) + } + + itemView.setOnClickListener { + viewModel.onSelected(bookmark) + } + } + + private fun loadFavicon(url: String) { + lifecycleOwner.lifecycleScope.launch { + faviconManager.loadToViewFromPersisted(url, itemView.favicon) + } + } + + private fun parseDisplayUrl(urlString: String): String { + val uri = Uri.parse(urlString) + return uri.baseHost ?: return urlString + } + + private fun showOverFlowMenu(anchor: ImageView, bookmark: BookmarkEntity) { + val popupMenu = BookmarksPopupMenu(layoutInflater) + val view = popupMenu.contentView + popupMenu.apply { + onMenuItemClicked(view.editBookmark) { editBookmark(bookmark) } + onMenuItemClicked(view.deleteBookmark) { deleteBookmark(bookmark) } + } + popupMenu.show(itemView, anchor) + } + + private fun editBookmark(bookmark: BookmarkEntity) { + Timber.i("Editing bookmark ${bookmark.title}") + viewModel.onEditBookmarkRequested(bookmark) + } + + private fun deleteBookmark(bookmark: BookmarkEntity) { + Timber.i("Deleting bookmark ${bookmark.title}") + viewModel.onDeleteRequested(bookmark) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt index 785f420f2a67..b045bc8e9936 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt @@ -21,7 +21,7 @@ import com.duckduckgo.app.bookmarks.db.BookmarkEntity class BookmarksEntityQueryListener( val bookmarks: List?, - val adapter: BookmarksActivity.BookmarksAdapter + val adapter: BookmarksAdapter ) : SearchView.OnQueryTextListener { override fun onQueryTextChange(newText: String): Boolean { 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 index edffb2360acb..cbfad4086e68 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -115,6 +115,18 @@ class BookmarksViewModel( command.value = ShowEditBookmark(bookmark) } + fun onSelected(favorite: Favorite) { + //command.value = OpenBookmark(favorite) + } + + fun onDeleteRequested(favorite: Favorite) { + //command.value = ConfirmDeleteBookmark(favorite) + } + + fun onEditFavoriteRequested(favorite: Favorite) { + //command.value = ShowEditBookmark(favorite) + } + fun delete(bookmark: BookmarkEntity) { viewModelScope.launch(dispatcherProvider.io() + NonCancellable) { faviconManager.deletePersistedFavicon(bookmark.url) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt new file mode 100644 index 000000000000..2eeef5326ffa --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021 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.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.model.Favorite +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.global.baseHost +import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.* +import kotlinx.android.synthetic.main.view_bookmark_entry.view.* +import kotlinx.android.synthetic.main.view_boomark_empty_hint.view.* +import kotlinx.android.synthetic.main.view_fireproof_website_empty_hint.view.* +import kotlinx.android.synthetic.main.view_fireproof_website_empty_hint.view.fireproofWebsiteEmptyHint +import kotlinx.android.synthetic.main.view_location_permissions_section_title.view.* +import kotlinx.coroutines.launch +import timber.log.Timber + +class FavoritesAdapter( + private val layoutInflater: LayoutInflater, + private val viewModel: BookmarksViewModel, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager +) : RecyclerView.Adapter() { + + companion object { + const val FAVORITE_SECTION_TITLE_TYPE = 0 + const val EMPTY_STATE_TYPE = 1 + const val FAVORITE_TYPE = 2 + + const val FAVORITE_SECTION_TITLE_SIZE = 1 + const val FAVORITE_EMPTY_HINT_SIZE = 1 + } + + var favoriteItems: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoritesScreenViewHolders { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + BookmarksAdapter.BOOKMARK_TYPE -> { + val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) + return FavoritesScreenViewHolders.FavoriteViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) + } + BookmarksAdapter.BOOKMARK_SECTION_TITLE_TYPE -> { + val view = inflater.inflate(R.layout.view_location_permissions_section_title, parent, false) + return FavoritesScreenViewHolders.SectionTitle(view) + } + BookmarksAdapter.EMPTY_STATE_TYPE -> { + val view = inflater.inflate(R.layout.view_boomark_empty_hint, parent, false) + FavoritesScreenViewHolders.EmptyHint(view) + } + else -> throw IllegalArgumentException("viewType not found") + } + } + + override fun getItemCount(): Int { + return headerItemsSize() + listSize() + } + + override fun onBindViewHolder(holder: FavoritesScreenViewHolders, position: Int) { + when (holder) { + is FavoritesScreenViewHolders.FavoriteViewHolder -> { + holder.update(favoriteItems[position - headerItemsSize()]) + } + is FavoritesScreenViewHolders.SectionTitle -> { + holder.bind() + } + is FavoritesScreenViewHolders.EmptyHint -> { + holder.bind() + } + } + } + + override fun getItemViewType(position: Int): Int { + return when { + position == 0 -> { + FAVORITE_SECTION_TITLE_TYPE + } + favoriteItems.isEmpty() -> { + EMPTY_STATE_TYPE + } + else -> { + FAVORITE_TYPE + } + } + } + + private fun headerItemsSize(): Int { + return FAVORITE_SECTION_TITLE_SIZE + } + + private fun listSize() = if (favoriteItems.isEmpty()) FAVORITE_EMPTY_HINT_SIZE else favoriteItems.size +} + +sealed class FavoritesScreenViewHolders(itemView: View) : RecyclerView.ViewHolder(itemView) { + + class SectionTitle(itemView: View) : FavoritesScreenViewHolders(itemView) { + fun bind() { + itemView.locationPermissionsSectionTitle.setText(R.string.favoritesSectionTitle) + } + } + + class EmptyHint(itemView: View) : FavoritesScreenViewHolders(itemView) { + fun bind() { + itemView.bookmarksEmptyHint.setText(R.string.favoritesEmptyHint) + } + } + + class FavoriteViewHolder( + private val layoutInflater: LayoutInflater, + itemView: View, + private val viewModel: BookmarksViewModel, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager + ) : FavoritesScreenViewHolders(itemView) { + + lateinit var favorite: Favorite + + fun update(favorite: Favorite) { + this.favorite = favorite + + itemView.overflowMenu.contentDescription = itemView.context.getString( + R.string.bookmarkOverflowContentDescription, + favorite.title + ) + + itemView.title.text = favorite.title + itemView.url.text = parseDisplayUrl(favorite.url) + loadFavicon(favorite.url) + + itemView.overflowMenu.setOnClickListener { + showOverFlowMenu(itemView.overflowMenu, favorite) + } + + itemView.setOnClickListener { + viewModel.onSelected(favorite) + } + } + + private fun loadFavicon(url: String) { + lifecycleOwner.lifecycleScope.launch { + faviconManager.loadToViewFromPersisted(url, itemView.favicon) + } + } + + private fun parseDisplayUrl(urlString: String): String { + val uri = Uri.parse(urlString) + return uri.baseHost ?: return urlString + } + + private fun showOverFlowMenu(anchor: ImageView, favorite: Favorite) { + val popupMenu = BookmarksPopupMenu(layoutInflater) + val view = popupMenu.contentView + popupMenu.apply { + onMenuItemClicked(view.editBookmark) { editFavorite(favorite) } + onMenuItemClicked(view.deleteBookmark) { deleteFavorite(favorite) } + } + popupMenu.show(itemView, anchor) + } + + private fun editFavorite(favorite: Favorite) { + Timber.i("Editing favorite ${favorite.title}") + viewModel.onEditFavoriteRequested(favorite) + } + + private fun deleteFavorite(favorite: Favorite) { + Timber.i("Deleting favorite ${favorite.title}") + viewModel.onDeleteRequested(favorite) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/view_boomark_empty_hint.xml b/app/src/main/res/layout/view_boomark_empty_hint.xml new file mode 100644 index 000000000000..baae4df15c8e --- /dev/null +++ b/app/src/main/res/layout/view_boomark_empty_hint.xml @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 79f18f6b0334..f100bd31d668 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -43,6 +43,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 522eda857447..a167f1e5785a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -213,7 +213,9 @@ Bookmark added Deleted <b>%s</b> Bookmarks + Favorites No bookmarks added yet + No favorites added yet Confirm diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 5294b42db226..cc4bde74d2ec 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -65,6 +65,7 @@ @color/white @color/almostBlack @color/white + @color/white @color/white @color/grayishTwo @color/white @@ -140,6 +141,7 @@ @color/white @color/almostBlackDark @color/almostBlackDark + @color/almostBlackDark @color/almostBlack @color/whiteSix @color/almostBlack From fb57c8e7996ac51f0a8e1aedc4e759117d5acd3d Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 14 Apr 2021 15:05:36 +0200 Subject: [PATCH 13/95] changed bookmark dialog copies to be generic --- .../app/bookmarks/ui/EditBookmarkDialogFragment.kt | 2 +- app/src/main/res/layout/edit_bookmark.xml | 4 ++-- app/src/main/res/values/strings.xml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/EditBookmarkDialogFragment.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/EditBookmarkDialogFragment.kt index d1c5aece3ee8..01f0364605d5 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/EditBookmarkDialogFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/EditBookmarkDialogFragment.kt @@ -44,7 +44,7 @@ class EditBookmarkDialogFragment : DialogFragment() { val alertBuilder = AlertDialog.Builder(requireActivity()) .setView(rootView) - .setTitle(R.string.bookmarkTitleEdit) + .setTitle(R.string.bookmarkDialogTitleEdit) .setPositiveButton(R.string.dialogSave) { _, _ -> userAcceptedDialog(titleInput, urlInput) } diff --git a/app/src/main/res/layout/edit_bookmark.xml b/app/src/main/res/layout/edit_bookmark.xml index 27e886f98627..4d6623159998 100644 --- a/app/src/main/res/layout/edit_bookmark.xml +++ b/app/src/main/res/layout/edit_bookmark.xml @@ -35,7 +35,7 @@ android:id="@+id/titleInput" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/bookmarkTitleHint" + android:hint="@string/bookmarkDialogTitleHint" android:inputType="text|textCapWords" /> @@ -53,7 +53,7 @@ android:id="@+id/urlInput" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/bookmarkUrlHint" + android:hint="@string/bookmarkDialogUrlHint" android:inputType="textUri" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a167f1e5785a..0731e9e0d64c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -205,9 +205,9 @@ Bookmarks Are you sure you want to delete bookmark <b>%s</b>? Bookmark added - Bookmark title - Bookmark URL - Edit bookmark + title + URL + Edit No bookmarks added yet More options for bookmark %s Bookmark added From c317d467f4d4ff8df7f124e67627683e499afece Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 14 Apr 2021 17:20:44 +0200 Subject: [PATCH 14/95] Refactor: introduced savedSite as sealed class to model bookmarks and favorites - be able to reuse dialog - be able to reuse command logic - be able to reuse same listeners - decouple from entity data objects from databases in UI --- .../bookmarks/ui/BookmarksViewModelTest.kt | 4 +- .../bookmarks/model/FavoritesRepository.kt | 63 ++++++++-- .../app/bookmarks/ui/BookmarksActivity.kt | 29 ++--- .../app/bookmarks/ui/BookmarksAdapter.kt | 12 +- .../app/bookmarks/ui/BookmarksViewModel.kt | 112 ++++++++++++------ .../ui/EditBookmarkDialogFragment.kt | 43 +++---- .../app/bookmarks/ui/FavoritesAdapter.kt | 6 +- .../app/browser/BrowserTabFragment.kt | 13 +- .../app/browser/BrowserTabViewModel.kt | 69 +++++++---- 9 files changed, 226 insertions(+), 125 deletions(-) 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 index 2e1cd32c9033..f1273e3e5d13 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt @@ -91,7 +91,7 @@ class BookmarksViewModelTest { val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.Command::class.java) verify(commandObserver).onChanged(captor.capture()) assertNotNull(captor.value) - assertTrue(captor.value is BookmarksViewModel.Command.OpenBookmark) + assertTrue(captor.value is BookmarksViewModel.Command.OpenSavedsite) } @Test @@ -100,7 +100,7 @@ class BookmarksViewModelTest { val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.Command::class.java) verify(commandObserver).onChanged(captor.capture()) assertNotNull(captor.value) - assertTrue(captor.value is BookmarksViewModel.Command.ConfirmDeleteBookmark) + assertTrue(captor.value is BookmarksViewModel.Command.ConfirmDeleteSavedSite) } @Test diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index 96c7c516c502..e767c3acb40d 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -18,27 +18,66 @@ package com.duckduckgo.app.bookmarks.model import com.duckduckgo.app.bookmarks.db.FavoriteEntity import com.duckduckgo.app.bookmarks.db.FavoritesDao -import com.duckduckgo.app.tabs.model.TabEntity import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMap +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.map +import java.io.Serializable interface FavoritesRepository { - suspend fun insert(favorite: Favorite): Long - suspend fun favorites(): Flow> + suspend fun insert(unsavedSite: SavedSite.UnsavedSite): SavedSite.Favorite + suspend fun insert(favorite: SavedSite.Favorite): SavedSite.Favorite + suspend fun update(favorite: SavedSite.Favorite) + suspend fun favorites(): Flow> + suspend fun delete(favorite: SavedSite.Favorite) } -data class Favorite( - var title: String, - var url: String -) +sealed class SavedSite(open val id: Long, + open val title: String, + open val url: String) : Serializable { + data class Favorite( + override val id: Long, + override val title: String, + override val url: String, + val position: Int + ) : SavedSite(id, title, url) -class FavoritesDataRepository (private val favoritesDao: FavoritesDao) : FavoritesRepository { + data class Bookmark( + override val id: Long, + override val title: String, + override val url: String + ) : SavedSite(id, title, url) - override suspend fun insert(favorite: Favorite): Long { + data class UnsavedSite( + override val title: String, + override val url: String): SavedSite(0, title, url) +} + + +class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : FavoritesRepository { + + override suspend fun insert(favorite: SavedSite.UnsavedSite): SavedSite.Favorite { val lastPosition = favoritesDao.getLastPosition() ?: 0 - return favoritesDao.insert(FavoriteEntity(title = favorite.title, url = favorite.url, position = lastPosition + 1)) + val favoriteEntity = FavoriteEntity(title = favorite.title, url = favorite.url, position = lastPosition + 1) + val id = favoritesDao.insert(favoriteEntity) + return SavedSite.Favorite(id, favoriteEntity.title, favoriteEntity.url, favoriteEntity.position) + } + + override suspend fun insert(favorite: SavedSite.Favorite): SavedSite.Favorite { + val favoriteEntity = FavoriteEntity(title = favorite.title, url = favorite.url, position = favorite.position) + val id = favoritesDao.insert(favoriteEntity) + return SavedSite.Favorite(id, favoriteEntity.title, favoriteEntity.url, favoriteEntity.position) + } + + override suspend fun update(favorite: SavedSite.Favorite) { + favoritesDao.update(FavoriteEntity(favorite.id, favorite.title, favorite.url, favorite.position)) + } + + override suspend fun favorites(): Flow> { + return favoritesDao.favorites().map { favorites -> favorites.map { SavedSite.Favorite(it.id, it.title, it.url, it.position) } } } - override suspend fun favorites(): Flow> { - return favoritesDao.favorites() + override suspend fun delete(favorite: SavedSite.Favorite) { + favoritesDao.delete(FavoriteEntity(favorite.id, favorite.title, favorite.url, favorite.position)) } } \ 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 index fd0431dbb388..3f1176be7bdd 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -24,7 +24,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.lifecycle.Observer import androidx.recyclerview.widget.ConcatAdapter -import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.R.id.action_search @@ -79,9 +79,9 @@ class BookmarksActivity : DuckDuckGoActivity() { this, Observer { when (it) { - is BookmarksViewModel.Command.ConfirmDeleteBookmark -> confirmDeleteBookmark(it.bookmark) - is BookmarksViewModel.Command.OpenBookmark -> openBookmark(it.bookmark) - is BookmarksViewModel.Command.ShowEditBookmark -> showEditBookmarkDialog(it.bookmark) + is BookmarksViewModel.Command.ConfirmDeleteSavedSite -> confirmDeleteSavedSite(it.savedSite) + is BookmarksViewModel.Command.OpenSavedsite -> openBookmark(it.savedSite) + is BookmarksViewModel.Command.ShowEditSavedSite -> showEditSavedSiteDialog(it.savedSite) } } ) @@ -100,32 +100,27 @@ class BookmarksActivity : DuckDuckGoActivity() { return super.onPrepareOptionsMenu(menu) } - private fun showEditBookmarkDialog(bookmark: BookmarkEntity) { - val dialog = EditBookmarkDialogFragment.instance(bookmark.id, bookmark.title, bookmark.url) + private fun showEditSavedSiteDialog(savedSite: SavedSite) { + val dialog = EditBookmarkDialogFragment.instance(savedSite) dialog.show(supportFragmentManager, EDIT_BOOKMARK_FRAGMENT_TAG) dialog.listener = viewModel } - private fun openBookmark(bookmark: BookmarkEntity) { - startActivity(BrowserActivity.intent(this, bookmark.url)) + private fun openBookmark(savedSite: SavedSite) { + startActivity(BrowserActivity.intent(this, savedSite.url)) finish() } - private fun confirmDeleteBookmark(bookmark: BookmarkEntity) { - val message = getString(R.string.bookmarkDeleteConfirmationMessage, bookmark.title).html(this) - viewModel.delete(bookmark) + private fun confirmDeleteSavedSite(savedSite: SavedSite) { + val message = getString(R.string.bookmarkDeleteConfirmationMessage, savedSite.title).html(this) + viewModel.delete(savedSite) Snackbar.make( bookmarkRootView, message, Snackbar.LENGTH_LONG ).setAction(R.string.fireproofWebsiteSnackbarAction) { - viewModel.insert(bookmark) + viewModel.insert(savedSite) }.show() - - } - - private fun delete(bookmark: BookmarkEntity) { - viewModel.delete(bookmark) } override fun onDestroy() { diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt index f35e38de65aa..4a448801f43c 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt @@ -24,7 +24,7 @@ import android.widget.ImageView import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView -import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.global.baseHost @@ -51,7 +51,7 @@ class BookmarksAdapter( const val BOOKMARK_EMPTY_HINT_SIZE = 1 } - var bookmarkItems: List = emptyList() + var bookmarkItems: List = emptyList() set(value) { field = value notifyDataSetChanged() @@ -137,7 +137,7 @@ sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder private val faviconManager: FaviconManager ) : BookmarkScreenViewHolders(itemView) { - fun update(bookmark: BookmarkEntity) { + fun update(bookmark: SavedSite.Bookmark) { itemView.overflowMenu.contentDescription = itemView.context.getString( R.string.bookmarkOverflowContentDescription, bookmark.title @@ -167,7 +167,7 @@ sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder return uri.baseHost ?: return urlString } - private fun showOverFlowMenu(anchor: ImageView, bookmark: BookmarkEntity) { + private fun showOverFlowMenu(anchor: ImageView, bookmark: SavedSite.Bookmark) { val popupMenu = BookmarksPopupMenu(layoutInflater) val view = popupMenu.contentView popupMenu.apply { @@ -177,12 +177,12 @@ sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder popupMenu.show(itemView, anchor) } - private fun editBookmark(bookmark: BookmarkEntity) { + private fun editBookmark(bookmark: SavedSite.Bookmark) { Timber.i("Editing bookmark ${bookmark.title}") viewModel.onEditBookmarkRequested(bookmark) } - private fun deleteBookmark(bookmark: BookmarkEntity) { + private fun deleteBookmark(bookmark: SavedSite.Bookmark) { Timber.i("Deleting bookmark ${bookmark.title}") viewModel.onDeleteRequested(bookmark) } 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 index cbfad4086e68..49638c2cce65 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -19,9 +19,10 @@ package com.duckduckgo.app.bookmarks.ui import androidx.lifecycle.* import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao -import com.duckduckgo.app.bookmarks.db.FavoriteEntity -import com.duckduckgo.app.bookmarks.model.Favorite import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.bookmarks.model.SavedSite +import com.duckduckgo.app.bookmarks.model.SavedSite.Bookmark +import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite import com.duckduckgo.app.bookmarks.ui.BookmarksViewModel.Command.* import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment.EditBookmarkListener import com.duckduckgo.app.browser.favicon.FaviconManager @@ -30,11 +31,10 @@ import com.duckduckgo.app.global.SingleLiveEvent import com.duckduckgo.app.global.plugins.view_model.ViewModelFactoryPlugin import com.duckduckgo.di.scopes.AppObjectGraph import com.squareup.anvil.annotations.ContributesMultibinding -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Provider @@ -48,14 +48,14 @@ class BookmarksViewModel( data class ViewState( val showFavorites: Boolean = false, val enableSearch: Boolean = false, - val bookmarks: List = emptyList(), + val bookmarks: List = emptyList(), val favorites: List = emptyList() ) sealed class Command { - class OpenBookmark(val bookmark: BookmarkEntity) : Command() - class ConfirmDeleteBookmark(val bookmark: BookmarkEntity) : Command() - class ShowEditBookmark(val bookmark: BookmarkEntity) : Command() + class OpenSavedsite(val savedSite: SavedSite) : Command() + class ConfirmDeleteSavedSite(val savedSite: SavedSite) : Command() + class ShowEditSavedSite(val savedSite: SavedSite) : Command() } companion object { @@ -65,8 +65,8 @@ class BookmarksViewModel( val viewState: MutableLiveData = MutableLiveData() val command: SingleLiveEvent = SingleLiveEvent() - private val bookmarks: LiveData> = dao.bookmarks() - private val bookmarksObserver = Observer> { onBookmarksChanged(it!!) } + private val bookmarks: LiveData> = dao.bookmarks().map { bookmarks -> bookmarks.map { Bookmark(it.id, it.title ?: "", it.url) } } + private val bookmarksObserver = Observer> { onBookmarksChanged(it!!) } init { viewState.value = ViewState() @@ -83,60 +83,101 @@ class BookmarksViewModel( bookmarks.removeObserver(bookmarksObserver) } - override fun onBookmarkEdited(id: Long, title: String, url: String) { - Schedulers.io().scheduleDirect { - dao.update(BookmarkEntity(id, title, url)) + override fun onSavedSiteEdited(savedSite: SavedSite) { + when (savedSite) { + is Bookmark -> { + viewModelScope.launch(dispatcherProvider.io()) { + editBookmark(savedSite) + } + } + is Favorite -> { + viewModelScope.launch(dispatcherProvider.io()) { + editFavorite(savedSite) + } + } + is SavedSite.UnsavedSite -> throw IllegalArgumentException("Illegal SavedSite to edit received") + } + } + + private suspend fun editBookmark(bookmark: Bookmark) { + withContext(dispatcherProvider.io()) { + dao.update(BookmarkEntity(bookmark.id, bookmark.title, bookmark.url)) + } + } + + private suspend fun editFavorite(favorite: Favorite) { + withContext(dispatcherProvider.io()) { + favoritesRepository.update(favorite) } } - private fun onBookmarksChanged(bookmarks: List) { + private fun onBookmarksChanged(bookmarks: List) { viewState.value = viewState.value?.copy( bookmarks = bookmarks, enableSearch = bookmarks.size > MIN_BOOKMARKS_FOR_SEARCH ) } - private fun onFavoritesChanged(favorites: List) { + private fun onFavoritesChanged(favorites: List) { viewState.value = viewState.value?.copy( showFavorites = favorites.isNotEmpty(), - favorites = favorites.map { Favorite(it.title, it.url) } + favorites = favorites ) } - fun onSelected(bookmark: BookmarkEntity) { - command.value = OpenBookmark(bookmark) + fun onSelected(bookmark: Bookmark) { + command.value = OpenSavedsite(bookmark) } - fun onDeleteRequested(bookmark: BookmarkEntity) { - command.value = ConfirmDeleteBookmark(bookmark) + fun onDeleteRequested(bookmark: Bookmark) { + command.value = ConfirmDeleteSavedSite(bookmark) } - fun onEditBookmarkRequested(bookmark: BookmarkEntity) { - command.value = ShowEditBookmark(bookmark) + fun onEditBookmarkRequested(bookmark: Bookmark) { + command.value = ShowEditSavedSite(bookmark) } fun onSelected(favorite: Favorite) { - //command.value = OpenBookmark(favorite) + command.value = OpenSavedsite(favorite) } fun onDeleteRequested(favorite: Favorite) { - //command.value = ConfirmDeleteBookmark(favorite) + command.value = ConfirmDeleteSavedSite(favorite) } fun onEditFavoriteRequested(favorite: Favorite) { - //command.value = ShowEditBookmark(favorite) + command.value = ShowEditSavedSite(favorite) } - fun delete(bookmark: BookmarkEntity) { - viewModelScope.launch(dispatcherProvider.io() + NonCancellable) { - faviconManager.deletePersistedFavicon(bookmark.url) - dao.delete(bookmark) + fun delete(savedSite: SavedSite) { + when (savedSite) { + is Bookmark -> { + viewModelScope.launch(dispatcherProvider.io() + NonCancellable) { + faviconManager.deletePersistedFavicon(savedSite.url) + dao.delete(BookmarkEntity(savedSite.id, savedSite.title, savedSite.url)) + } + } + is Favorite -> { + viewModelScope.launch(dispatcherProvider.io() + NonCancellable) { + faviconManager.deletePersistedFavicon(savedSite.url) + favoritesRepository.delete(savedSite) + } + } } } - fun insert(bookmark: BookmarkEntity) { - viewModelScope.launch(dispatcherProvider.io()) { - dao.insert(BookmarkEntity(title = bookmark.title, url = bookmark.url)) + fun insert(savedSite: SavedSite) { + when (savedSite) { + is Bookmark -> { + viewModelScope.launch(dispatcherProvider.io()) { + dao.insert(BookmarkEntity(title = savedSite.title, url = savedSite.url)) + } + } + is Favorite -> { + viewModelScope.launch(dispatcherProvider.io()) { + favoritesRepository.insert(savedSite) + } + } } } @@ -152,7 +193,12 @@ class BookmarksViewModelFactory @Inject constructor( override fun create(modelClass: Class): T? { with(modelClass) { return when { - isAssignableFrom(BookmarksViewModel::class.java) -> (BookmarksViewModel(favoritesRepository.get(), dao.get(), faviconManager.get(), dispatcherProvider.get()) as T) + isAssignableFrom(BookmarksViewModel::class.java) -> (BookmarksViewModel( + favoritesRepository.get(), + dao.get(), + faviconManager.get(), + dispatcherProvider.get() + ) as T) else -> null } } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/EditBookmarkDialogFragment.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/EditBookmarkDialogFragment.kt index 01f0364605d5..b6fe09bcaabe 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/EditBookmarkDialogFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/EditBookmarkDialogFragment.kt @@ -23,6 +23,7 @@ import android.view.WindowManager import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment +import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.view.hideKeyboard import com.duckduckgo.app.global.view.showKeyboard @@ -31,7 +32,7 @@ import org.jetbrains.anko.find class EditBookmarkDialogFragment : DialogFragment() { interface EditBookmarkListener { - fun onBookmarkEdited(id: Long, title: String, url: String) + fun onSavedSiteEdited(savedSite: SavedSite) } var listener: EditBookmarkListener? = null @@ -59,12 +60,18 @@ class EditBookmarkDialogFragment : DialogFragment() { } private fun userAcceptedDialog(titleInput: EditText, urlInput: EditText) { - listener?.onBookmarkEdited( - getExistingId(), - titleInput.text.toString(), - urlInput.text.toString() - ) - + when (val savedSite = getSavedSite()) { + is SavedSite.Bookmark -> { + listener?.onSavedSiteEdited( + savedSite.copy(title = titleInput.text.toString(), url = urlInput.text.toString()) + ) + } + is SavedSite.Favorite -> { + listener?.onSavedSiteEdited( + savedSite.copy(title = titleInput.text.toString(), url = urlInput.text.toString()) + ) + } + } titleInput.hideKeyboard() } @@ -79,34 +86,28 @@ class EditBookmarkDialogFragment : DialogFragment() { urlInput.setText(getExistingUrl()) } - private fun getExistingId(): Long = requireArguments().getLong(KEY_BOOKMARK_ID) - private fun getExistingTitle(): String? = requireArguments().getString(KEY_PREEXISTING_TITLE) - private fun getExistingUrl(): String? = requireArguments().getString(KEY_PREEXISTING_URL) + private fun getSavedSite(): SavedSite = requireArguments().getSerializable(KEY_SAVED_SITE) as SavedSite + private fun getExistingId(): Long = getSavedSite().id + private fun getExistingTitle(): String = getSavedSite().title + private fun getExistingUrl(): String = getSavedSite().url private fun validateBundleArguments() { if (arguments == null) throw IllegalArgumentException("Missing arguments bundle") val args = requireArguments() - if (!args.containsKey(KEY_PREEXISTING_TITLE) || - !args.containsKey(KEY_PREEXISTING_URL) - ) { + if (!args.containsKey(KEY_SAVED_SITE)) { throw IllegalArgumentException("Bundle arguments required [KEY_PREEXISTING_TITLE, KEY_PREEXISTING_URL]") } } companion object { - private const val KEY_BOOKMARK_ID = "KEY_BOOKMARK_ID" - private const val KEY_PREEXISTING_TITLE = "KEY_PREEXISTING_TITLE" - private const val KEY_PREEXISTING_URL = "KEY_PREEXISTING_URL" + private const val KEY_SAVED_SITE = "KEY_SAVED_SITE" - fun instance(bookmarkId: Long, title: String?, url: String?): EditBookmarkDialogFragment { + fun instance(savedSite: SavedSite): EditBookmarkDialogFragment { val dialog = EditBookmarkDialogFragment() val bundle = Bundle() - bundle.putLong(KEY_BOOKMARK_ID, bookmarkId) - bundle.putString(KEY_PREEXISTING_TITLE, title) - bundle.putString(KEY_PREEXISTING_URL, url) - + bundle.putSerializable(KEY_SAVED_SITE, savedSite) dialog.arguments = bundle return dialog } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt index 2eeef5326ffa..5ed1ddc04a2a 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt @@ -24,16 +24,14 @@ import android.widget.ImageView import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView -import com.duckduckgo.app.bookmarks.db.BookmarkEntity -import com.duckduckgo.app.bookmarks.model.Favorite +import com.duckduckgo.app.bookmarks.model.SavedSite +import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.global.baseHost import kotlinx.android.synthetic.main.popup_window_bookmarks_menu.view.* import kotlinx.android.synthetic.main.view_bookmark_entry.view.* import kotlinx.android.synthetic.main.view_boomark_empty_hint.view.* -import kotlinx.android.synthetic.main.view_fireproof_website_empty_hint.view.* -import kotlinx.android.synthetic.main.view_fireproof_website_empty_hint.view.fireproofWebsiteEmptyHint import kotlinx.android.synthetic.main.view_location_permissions_section_title.view.* import kotlinx.coroutines.launch import timber.log.Timber diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index cc01ccdd6603..9d6f3ea31a4b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -58,6 +58,7 @@ import androidx.fragment.app.transaction import androidx.lifecycle.* import androidx.recyclerview.widget.LinearLayoutManager import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion +import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment import com.duckduckgo.app.brokensite.BrokenSiteActivity import com.duckduckgo.app.brokensite.BrokenSiteData @@ -532,8 +533,8 @@ class BrowserTabFragment : openInNewBackgroundTab() } is Command.LaunchNewTab -> browserActivity?.launchNewTab() - is Command.ShowBookmarkAddedConfirmation -> bookmarkAdded(it.bookmarkId, it.title, it.url) - is Command.ShowFavoriteAddedConfirmation -> favoriteAdded(it.favoriteId, it.title, it.url) + is Command.ShowBookmarkAddedConfirmation -> bookmarkAdded(it.bookmark) + is Command.ShowFavoriteAddedConfirmation -> favoriteAdded(it.favorite) is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.fireproofWebsiteEntity) is Command.Navigate -> { navigate(it.url, it.headers) @@ -1088,20 +1089,20 @@ class BrowserTabFragment : return super.onContextItemSelected(item) } - private fun bookmarkAdded(bookmarkId: Long, title: String?, url: String?) { + private fun bookmarkAdded(bookmark: SavedSite.Bookmark) { Snackbar.make(browserLayout, R.string.bookmarkAddedMessage, Snackbar.LENGTH_LONG) .setAction(R.string.edit) { - val addBookmarkDialog = EditBookmarkDialogFragment.instance(bookmarkId, title, url) + val addBookmarkDialog = EditBookmarkDialogFragment.instance(bookmark) addBookmarkDialog.show(childFragmentManager, ADD_BOOKMARK_FRAGMENT_TAG) addBookmarkDialog.listener = viewModel } .show() } - private fun favoriteAdded(favoriteId: Long, title: String, url: String) { + private fun favoriteAdded(favorite: SavedSite.Favorite) { Snackbar.make(browserLayout, R.string.favoriteAddedMessage, Snackbar.LENGTH_LONG) .setAction(R.string.edit) { - val addBookmarkDialog = EditBookmarkDialogFragment.instance(favoriteId, title, url) + val addBookmarkDialog = EditBookmarkDialogFragment.instance(favorite) addBookmarkDialog.show(childFragmentManager, ADD_FAVORITE_FRAGMENT_TAG) addBookmarkDialog.listener = viewModel } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 91e2fc954635..b2825086d8dd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -41,10 +41,9 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.A 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.db.FavoriteEntity -import com.duckduckgo.app.bookmarks.db.FavoritesDao -import com.duckduckgo.app.bookmarks.model.Favorite import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.bookmarks.model.SavedSite +import com.duckduckgo.app.bookmarks.model.SavedSite.UnsavedSite import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment.EditBookmarkListener import com.duckduckgo.app.brokensite.BrokenSiteData import com.duckduckgo.app.browser.BrowserTabViewModel.Command.* @@ -259,8 +258,8 @@ class BrowserTabViewModel( object HideKeyboard : Command() class ShowFullScreen(val view: View) : Command() class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() - class ShowBookmarkAddedConfirmation(val bookmarkId: Long, val title: String?, val url: String?) : Command() - class ShowFavoriteAddedConfirmation(val favoriteId: Long, val title: String, val url: String) : Command() + class ShowBookmarkAddedConfirmation(val bookmark: SavedSite.Bookmark) : Command() + class ShowFavoriteAddedConfirmation(val favorite: SavedSite.Favorite) : Command() class ShowFireproofWebSiteConfirmation(val fireproofWebsiteEntity: FireproofWebsiteEntity) : Command() object AskToDisableLoginDetection : Command() class AskToFireproofWebsite(val fireproofWebsite: FireproofWebsiteEntity) : Command() @@ -1296,33 +1295,39 @@ class BrowserTabViewModel( } suspend fun onBookmarkAddRequested() { - val url = url ?: "" - val title = title ?: "" - val id = withContext(dispatchers.io()) { - if (url.isNotBlank()) { - faviconManager.persistFavicon(tabId, url) + val unsavedSite = createSiteToSave() + val savedBookmark = withContext(dispatchers.io()) { + if (unsavedSite.url.isNotBlank()) { + faviconManager.persistFavicon(tabId, unsavedSite.url) } - bookmarksDao.insert(BookmarkEntity(title = title, url = url)) + val bookmarkEntity = BookmarkEntity(title = unsavedSite.title, url = unsavedSite.url) + val id = bookmarksDao.insert(bookmarkEntity) + SavedSite.Bookmark(id, unsavedSite.title, unsavedSite.url) } withContext(dispatchers.main()) { - command.value = ShowBookmarkAddedConfirmation(id, title, url) + command.value = ShowBookmarkAddedConfirmation(savedBookmark) } } suspend fun onAddFavoriteMenuClicked() { - val url = url ?: "" - val title = title ?: "" - val id = withContext(dispatchers.io()) { - if (url.isNotBlank()) { - faviconManager.persistFavicon(tabId, url) + val unsavedSite = createSiteToSave() + val savedFavorite = withContext(dispatchers.io()) { + if (unsavedSite.url.isNotBlank()) { + faviconManager.persistFavicon(tabId, unsavedSite.url) } - favoritesRepository.insert(Favorite(title = title, url = url)) + favoritesRepository.insert(unsavedSite) } withContext(dispatchers.main()) { - command.value = ShowFavoriteAddedConfirmation(id, title, url) + command.value = ShowFavoriteAddedConfirmation(savedFavorite) } } + private fun createSiteToSave(): UnsavedSite { + val url = url ?: "" + val title = title ?: "" + return UnsavedSite(title, url) + } + fun onFireproofWebsiteMenuClicked() { val domain = site?.domain ?: return viewModelScope.launch { @@ -1382,15 +1387,31 @@ class BrowserTabViewModel( } } - override fun onBookmarkEdited(id: Long, title: String, url: String) { - viewModelScope.launch(dispatchers.io()) { - editBookmark(id, title, url) + override fun onSavedSiteEdited(savedSite: SavedSite) { + when(savedSite) { + is SavedSite.Bookmark -> { + viewModelScope.launch(dispatchers.io()) { + editBookmark(savedSite) + } + } + is SavedSite.Favorite -> { + viewModelScope.launch(dispatchers.io()) { + editFavorite(savedSite) + } + } + is UnsavedSite -> throw IllegalArgumentException("Illegal SavedSite to edit received") + } + } + + private suspend fun editBookmark(bookmark: SavedSite.Bookmark) { + withContext(dispatchers.io()) { + bookmarksDao.update(BookmarkEntity(bookmark.id, bookmark.title, bookmark.url)) } } - suspend fun editBookmark(id: Long, title: String, url: String) { + private suspend fun editFavorite(favorite: SavedSite.Favorite) { withContext(dispatchers.io()) { - bookmarksDao.update(BookmarkEntity(id, title, url)) + favoritesRepository.update(favorite) } } From beace1437300045d996f647949e2bc0f45f3c48d Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 15 Apr 2021 23:47:14 +0200 Subject: [PATCH 15/95] apply codestyle --- .../duckduckgo/app/bookmarks/db/FavoritesDao.kt | 2 -- .../app/bookmarks/di/BookmarksModule.kt | 2 +- .../app/bookmarks/model/FavoritesRepository.kt | 16 ++++++++-------- .../app/bookmarks/ui/BookmarksViewModel.kt | 14 ++++++++------ .../duckduckgo/app/browser/BrowserTabFragment.kt | 4 ++-- .../app/browser/BrowserTabViewModel.kt | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt index ba8414d77711..653c10a10b45 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt @@ -16,9 +16,7 @@ package com.duckduckgo.app.bookmarks.db -import androidx.lifecycle.LiveData import androidx.room.* -import com.duckduckgo.app.tabs.model.TabEntity import io.reactivex.Single import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt index 97a384ba31ec..45574b2cd4e6 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt @@ -34,4 +34,4 @@ class BookmarksModule { fun favoriteRepository(favoritesDao: FavoritesDao): FavoritesRepository { return FavoritesDataRepository(favoritesDao) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index e767c3acb40d..268f8c84e285 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -19,8 +19,6 @@ package com.duckduckgo.app.bookmarks.model import com.duckduckgo.app.bookmarks.db.FavoriteEntity import com.duckduckgo.app.bookmarks.db.FavoritesDao import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMap -import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map import java.io.Serializable @@ -32,9 +30,11 @@ interface FavoritesRepository { suspend fun delete(favorite: SavedSite.Favorite) } -sealed class SavedSite(open val id: Long, - open val title: String, - open val url: String) : Serializable { +sealed class SavedSite( + open val id: Long, + open val title: String, + open val url: String +) : Serializable { data class Favorite( override val id: Long, override val title: String, @@ -50,10 +50,10 @@ sealed class SavedSite(open val id: Long, data class UnsavedSite( override val title: String, - override val url: String): SavedSite(0, title, url) + override val url: String + ) : SavedSite(0, title, url) } - class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : FavoritesRepository { override suspend fun insert(favorite: SavedSite.UnsavedSite): SavedSite.Favorite { @@ -80,4 +80,4 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite override suspend fun delete(favorite: SavedSite.Favorite) { favoritesDao.delete(FavoriteEntity(favorite.id, favorite.title, favorite.url, favorite.position)) } -} \ 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 index 49638c2cce65..bf19c655d4e3 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -193,12 +193,14 @@ class BookmarksViewModelFactory @Inject constructor( override fun create(modelClass: Class): T? { with(modelClass) { return when { - isAssignableFrom(BookmarksViewModel::class.java) -> (BookmarksViewModel( - favoritesRepository.get(), - dao.get(), - faviconManager.get(), - dispatcherProvider.get() - ) as T) + isAssignableFrom(BookmarksViewModel::class.java) -> ( + BookmarksViewModel( + favoritesRepository.get(), + dao.get(), + faviconManager.get(), + dispatcherProvider.get() + ) as T + ) else -> null } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 9d6f3ea31a4b..912c33a44cbc 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1521,8 +1521,8 @@ class BrowserTabFragment : } onMenuItemClicked(view.addFavoritePopupMenuItem) { launch { - //TODO: do we need a pixel here? - //pixel.fire(AppPixelName.MENU_ACTION_ADD_BOOKMARK_PRESSED.pixelName) + // TODO: do we need a pixel here? + // pixel.fire(AppPixelName.MENU_ACTION_ADD_BOOKMARK_PRESSED.pixelName) viewModel.onAddFavoriteMenuClicked() } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index b2825086d8dd..c7d750566c9a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1388,7 +1388,7 @@ class BrowserTabViewModel( } override fun onSavedSiteEdited(savedSite: SavedSite) { - when(savedSite) { + when (savedSite) { is SavedSite.Bookmark -> { viewModelScope.launch(dispatchers.io()) { editBookmark(savedSite) From 1e190c203862aedb1fe35b0edef5971f6b816560 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 19 Apr 2021 10:48:46 +0200 Subject: [PATCH 16/95] Update only changed items in list --- .../app/bookmarks/ui/BookmarksActivity.kt | 6 +-- .../app/bookmarks/ui/BookmarksAdapter.kt | 50 ++++++++----------- .../ui/BookmarksEntityQueryListener.kt | 14 +++--- .../app/bookmarks/ui/BookmarksViewModel.kt | 17 ++++--- .../app/bookmarks/ui/FavoritesAdapter.kt | 46 ++++++++--------- .../app/bookmarks/ui/FavoritesDiffCallback.kt | 39 +++++++++++++++ 6 files changed, 104 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesDiffCallback.kt 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 index 3f1176be7bdd..bab121a27d6f 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -68,8 +68,8 @@ class BookmarksActivity : DuckDuckGoActivity() { this, Observer { viewState -> viewState?.let { - favoritesAdapter.favoriteItems = it.favorites - bookmarksAdapter.bookmarkItems = it.bookmarks + favoritesAdapter.favoriteItems = it.favorites.map { FavoritesAdapter.FavoriteItem(it) } + bookmarksAdapter.bookmarkItems = it.bookmarks.map { BookmarksAdapter.BookmarkItem(it) } invalidateOptionsMenu() } } @@ -91,7 +91,7 @@ class BookmarksActivity : DuckDuckGoActivity() { menuInflater.inflate(bookmark_activity_menu, menu) val searchItem = menu?.findItem(action_search) val searchView = searchItem?.actionView as SearchView - //searchView.setOnQueryTextListener(BookmarksEntityQueryListener(viewModel.viewState.value?.bookmarks, adapter)) + searchView.setOnQueryTextListener(BookmarksEntityQueryListener(viewModel.viewState.value?.bookmarks, bookmarksAdapter)) return super.onCreateOptionsMenu(menu) } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt index 4a448801f43c..46fa7f3a1a8f 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt @@ -23,6 +23,7 @@ import android.view.ViewGroup import android.widget.ImageView import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.browser.R @@ -40,23 +41,29 @@ class BookmarksAdapter( private val viewModel: BookmarksViewModel, private val lifecycleOwner: LifecycleOwner, private val faviconManager: FaviconManager -) : RecyclerView.Adapter() { +) : ListAdapter(BookmarksDiffCallback()) { companion object { const val BOOKMARK_SECTION_TITLE_TYPE = 0 const val EMPTY_STATE_TYPE = 1 const val BOOKMARK_TYPE = 2 - - const val BOOKMARK_SECTION_TITLE_SIZE = 1 - const val BOOKMARK_EMPTY_HINT_SIZE = 1 } - var bookmarkItems: List = emptyList() + interface BookmarksItemTypes + object Header : BookmarksItemTypes + object EmptyHint : BookmarksItemTypes + data class BookmarkItem(val bookmark: SavedSite.Bookmark) : BookmarksItemTypes + + var bookmarkItems: List = emptyList() set(value) { - field = value - notifyDataSetChanged() + field = generateNewList(value) + submitList(field) } + private fun generateNewList(value: List): List { + return listOf(Header) + (if (value.isEmpty()) listOf(EmptyHint) else value) + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkScreenViewHolders { val inflater = LayoutInflater.from(parent.context) return when (viewType) { @@ -76,14 +83,12 @@ class BookmarksAdapter( } } - override fun getItemCount(): Int { - return headerItemsSize() + listSize() - } + override fun getItemCount(): Int = bookmarkItems.size override fun onBindViewHolder(holder: BookmarkScreenViewHolders, position: Int) { when (holder) { is BookmarkScreenViewHolders.BookmarksViewHolder -> { - holder.update(bookmarkItems[position - headerItemsSize()]) + holder.update((bookmarkItems[position] as BookmarkItem).bookmark) } is BookmarkScreenViewHolders.SectionTitle -> { holder.bind() @@ -95,24 +100,13 @@ class BookmarksAdapter( } override fun getItemViewType(position: Int): Int { - return when { - position == 0 -> { - BOOKMARK_SECTION_TITLE_TYPE - } - bookmarkItems.isEmpty() -> { - EMPTY_STATE_TYPE - } - else -> { - BOOKMARK_TYPE - } - } - } + return when(bookmarkItems[position]) { + is Header -> BOOKMARK_SECTION_TITLE_TYPE + is EmptyHint -> EMPTY_STATE_TYPE + else -> BOOKMARK_TYPE - private fun headerItemsSize(): Int { - return BOOKMARK_SECTION_TITLE_SIZE + } } - - private fun listSize() = if (bookmarkItems.isEmpty()) BOOKMARK_EMPTY_HINT_SIZE else bookmarkItems.size } sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -187,4 +181,4 @@ sealed class BookmarkScreenViewHolders(itemView: View) : RecyclerView.ViewHolder viewModel.onDeleteRequested(bookmark) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt index b045bc8e9936..088a56844bef 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt @@ -18,16 +18,16 @@ package com.duckduckgo.app.bookmarks.ui import androidx.appcompat.widget.SearchView import com.duckduckgo.app.bookmarks.db.BookmarkEntity +import com.duckduckgo.app.bookmarks.model.SavedSite class BookmarksEntityQueryListener( - val bookmarks: List?, + val bookmarks: List?, val adapter: BookmarksAdapter ) : SearchView.OnQueryTextListener { override fun onQueryTextChange(newText: String): Boolean { if (bookmarks != null) { - //adapter.bookmarks = filter(newText, bookmarks) - adapter.notifyDataSetChanged() + adapter.bookmarkItems = filter(newText, bookmarks) } return true } @@ -36,11 +36,11 @@ class BookmarksEntityQueryListener( return false } - private fun filter(query: String, bookmarks: List): List { + private fun filter(query: String, bookmarks: List): List { val lowercaseQuery = query.toLowerCase() return bookmarks.filter { - val lowercaseTitle = it.title?.toLowerCase() - lowercaseTitle?.contains(lowercaseQuery) == true || it.url.contains(lowercaseQuery) - } + val lowercaseTitle = it.title.toLowerCase() + lowercaseTitle.contains(lowercaseQuery) || it.url.contains(lowercaseQuery) + }.map { BookmarksAdapter.BookmarkItem(it) } } } 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 index bf19c655d4e3..a8cdb11cf90a 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject import javax.inject.Provider @@ -112,6 +113,7 @@ class BookmarksViewModel( } private fun onBookmarksChanged(bookmarks: List) { + Timber.i("Bookmark received: $bookmarks") viewState.value = viewState.value?.copy( bookmarks = bookmarks, enableSearch = bookmarks.size > MIN_BOOKMARKS_FOR_SEARCH @@ -119,6 +121,7 @@ class BookmarksViewModel( } private fun onFavoritesChanged(favorites: List) { + Timber.i("Favorites received: $favorites") viewState.value = viewState.value?.copy( showFavorites = favorites.isNotEmpty(), favorites = favorites @@ -194,13 +197,13 @@ class BookmarksViewModelFactory @Inject constructor( with(modelClass) { return when { isAssignableFrom(BookmarksViewModel::class.java) -> ( - BookmarksViewModel( - favoritesRepository.get(), - dao.get(), - faviconManager.get(), - dispatcherProvider.get() - ) as T - ) + BookmarksViewModel( + favoritesRepository.get(), + dao.get(), + faviconManager.get(), + dispatcherProvider.get() + ) as T + ) else -> null } } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt index 5ed1ddc04a2a..35fcfd49b359 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt @@ -23,8 +23,8 @@ import android.view.ViewGroup import android.widget.ImageView import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.favicon.FaviconManager @@ -41,35 +41,41 @@ class FavoritesAdapter( private val viewModel: BookmarksViewModel, private val lifecycleOwner: LifecycleOwner, private val faviconManager: FaviconManager -) : RecyclerView.Adapter() { +) : ListAdapter(FavoritesDiffCallback()) { companion object { const val FAVORITE_SECTION_TITLE_TYPE = 0 const val EMPTY_STATE_TYPE = 1 const val FAVORITE_TYPE = 2 - - const val FAVORITE_SECTION_TITLE_SIZE = 1 - const val FAVORITE_EMPTY_HINT_SIZE = 1 } - var favoriteItems: List = emptyList() + interface FavoriteItemTypes + object Header: FavoriteItemTypes + object EmptyHint: FavoriteItemTypes + data class FavoriteItem(val favorite: Favorite): FavoriteItemTypes + + var favoriteItems: List = emptyList() set(value) { - field = value - notifyDataSetChanged() + field = generateNewList(value) + submitList(field) } + private fun generateNewList(value: List): List { + return listOf(Header) + (if (value.isEmpty()) listOf(EmptyHint) else value) + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoritesScreenViewHolders { val inflater = LayoutInflater.from(parent.context) return when (viewType) { - BookmarksAdapter.BOOKMARK_TYPE -> { + FAVORITE_TYPE -> { val view = inflater.inflate(R.layout.view_bookmark_entry, parent, false) return FavoritesScreenViewHolders.FavoriteViewHolder(layoutInflater, view, viewModel, lifecycleOwner, faviconManager) } - BookmarksAdapter.BOOKMARK_SECTION_TITLE_TYPE -> { + FAVORITE_SECTION_TITLE_TYPE -> { val view = inflater.inflate(R.layout.view_location_permissions_section_title, parent, false) return FavoritesScreenViewHolders.SectionTitle(view) } - BookmarksAdapter.EMPTY_STATE_TYPE -> { + EMPTY_STATE_TYPE -> { val view = inflater.inflate(R.layout.view_boomark_empty_hint, parent, false) FavoritesScreenViewHolders.EmptyHint(view) } @@ -78,13 +84,13 @@ class FavoritesAdapter( } override fun getItemCount(): Int { - return headerItemsSize() + listSize() + return favoriteItems.size } override fun onBindViewHolder(holder: FavoritesScreenViewHolders, position: Int) { when (holder) { is FavoritesScreenViewHolders.FavoriteViewHolder -> { - holder.update(favoriteItems[position - headerItemsSize()]) + holder.update((favoriteItems[position] as FavoriteItem).favorite) } is FavoritesScreenViewHolders.SectionTitle -> { holder.bind() @@ -96,11 +102,11 @@ class FavoritesAdapter( } override fun getItemViewType(position: Int): Int { - return when { - position == 0 -> { + return when(favoriteItems[position]) { + is Header -> { FAVORITE_SECTION_TITLE_TYPE } - favoriteItems.isEmpty() -> { + is EmptyHint -> { EMPTY_STATE_TYPE } else -> { @@ -108,12 +114,6 @@ class FavoritesAdapter( } } } - - private fun headerItemsSize(): Int { - return FAVORITE_SECTION_TITLE_SIZE - } - - private fun listSize() = if (favoriteItems.isEmpty()) FAVORITE_EMPTY_HINT_SIZE else favoriteItems.size } sealed class FavoritesScreenViewHolders(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -192,4 +192,4 @@ sealed class FavoritesScreenViewHolders(itemView: View) : RecyclerView.ViewHolde viewModel.onDeleteRequested(favorite) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesDiffCallback.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesDiffCallback.kt new file mode 100644 index 000000000000..7503215a6ea6 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesDiffCallback.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 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 androidx.recyclerview.widget.DiffUtil + +class BookmarksDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: BookmarksAdapter.BookmarksItemTypes, newItem: BookmarksAdapter.BookmarksItemTypes): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: BookmarksAdapter.BookmarksItemTypes, newItem: BookmarksAdapter.BookmarksItemTypes): Boolean { + return oldItem == newItem + } +} + +class FavoritesDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FavoritesAdapter.FavoriteItemTypes, newItem: FavoritesAdapter.FavoriteItemTypes): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: FavoritesAdapter.FavoriteItemTypes, newItem: FavoritesAdapter.FavoriteItemTypes): Boolean { + return oldItem == newItem + } +} \ No newline at end of file From a02ec9e9df5aa34a07915e5f88e8e88a3f3e6f42 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 19 Apr 2021 10:49:02 +0200 Subject: [PATCH 17/95] disable animator list to keep previous behavior --- .../java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt | 1 + 1 file changed, 1 insertion(+) 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 index bab121a27d6f..6b01fb0491aa 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -61,6 +61,7 @@ class BookmarksActivity : DuckDuckGoActivity() { bookmarksAdapter = BookmarksAdapter(layoutInflater, viewModel, this, faviconManager) favoritesAdapter = FavoritesAdapter(layoutInflater, viewModel, this, faviconManager) recycler.adapter = ConcatAdapter(favoritesAdapter, bookmarksAdapter) + recycler.setItemAnimator(null); } private fun observeViewModel() { From 19b062623a19cccf8f1955a7ac40aa07e842735c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 19 Apr 2021 12:18:02 +0200 Subject: [PATCH 18/95] Show favorites as quick access items in SystemSearchActivity (wip) --- .../favorites/FavoritesQuickAccessAdapter.kt | 83 +++++++++++++++++++ .../app/systemsearch/SystemSearchActivity.kt | 37 +++++++-- .../app/systemsearch/SystemSearchViewModel.kt | 52 +++++++++--- .../res/layout/activity_system_search.xml | 5 +- .../res/layout/include_quick_access_items.xml | 36 ++++++++ .../res/layout/view_quick_access_item.xml | 42 ++++++++++ 6 files changed, 236 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt create mode 100644 app/src/main/res/layout/include_quick_access_items.xml create mode 100644 app/src/main/res/layout/view_quick_access_item.xml diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt new file mode 100644 index 000000000000..d0f7b9239f95 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021 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.favorites + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.duckduckgo.app.bookmarks.model.SavedSite +import com.duckduckgo.app.bookmarks.ui.FavoritesAdapter +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessFavorite +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessViewHolder +import kotlinx.android.synthetic.main.view_quick_access_item.view.* +import kotlinx.coroutines.launch + +class FavoritesQuickAccessAdapter( + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager +) : ListAdapter(QuickAccessAdapterDiffCallback()) { + + data class QuickAccessFavorite(val favorite: SavedSite.Favorite) : FavoritesAdapter.FavoriteItemTypes + + class QuickAccessViewHolder( + private val layoutInflater: LayoutInflater, + itemView: View, + private val lifecycleOwner: LifecycleOwner, + private val faviconManager: FaviconManager + ) : RecyclerView.ViewHolder(itemView) { + + fun bind(item: QuickAccessFavorite) { + with(item.favorite) { + itemView.quickAccessTitle.text = title + loadFavicon(url) + } + } + + private fun loadFavicon(url: String) { + lifecycleOwner.lifecycleScope.launch { + faviconManager.loadToViewFromPersisted(url, itemView.quickAccessFavicon) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuickAccessViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.view_quick_access_item, parent, false) + return QuickAccessViewHolder(inflater, view, lifecycleOwner, faviconManager) + } + + override fun onBindViewHolder(holder: QuickAccessViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class QuickAccessAdapterDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: QuickAccessFavorite, newItem: QuickAccessFavorite): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: QuickAccessFavorite, newItem: QuickAccessFavorite): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index ff6e5af3b389..dbbd309f3a4c 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -29,10 +29,13 @@ import android.widget.Toast import android.widget.Toast.LENGTH_SHORT import androidx.core.view.isVisible import androidx.lifecycle.Observer +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter +import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter import com.duckduckgo.app.browser.omnibar.OmnibarScrolling import com.duckduckgo.app.fire.DataClearerForegroundAppRestartPixel import com.duckduckgo.app.global.DuckDuckGoActivity @@ -41,7 +44,6 @@ import com.duckduckgo.app.global.view.hideKeyboard import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.* -import com.duckduckgo.app.systemsearch.SystemSearchViewModel.SystemSearchResultsViewState import kotlinx.android.synthetic.main.activity_system_search.* import kotlinx.android.synthetic.main.activity_system_search.appBarLayout import kotlinx.android.synthetic.main.activity_system_search.autocompleteSuggestions @@ -52,6 +54,7 @@ import kotlinx.android.synthetic.main.activity_system_search.logo import kotlinx.android.synthetic.main.activity_system_search.omnibarTextInput import kotlinx.android.synthetic.main.activity_system_search.results import kotlinx.android.synthetic.main.activity_system_search.resultsContent +import kotlinx.android.synthetic.main.include_quick_access_items.* import kotlinx.android.synthetic.main.include_system_search_onboarding.* import javax.inject.Inject @@ -66,9 +69,13 @@ class SystemSearchActivity : DuckDuckGoActivity() { @Inject lateinit var dataClearerForegroundAppRestartPixel: DataClearerForegroundAppRestartPixel + @Inject + lateinit var faviconManager: FaviconManager + private val viewModel: SystemSearchViewModel by bindViewModel() private lateinit var autocompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter private lateinit var deviceAppSuggestionsAdapter: DeviceAppSuggestionsAdapter + private lateinit var quickAccessAdapter: FavoritesQuickAccessAdapter private val textChangeWatcher = object : TextChangedWatcher() { override fun afterTextChanged(editable: Editable) { @@ -88,6 +95,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { configureDaxButton() configureOmnibar() configureTextInput() + configureQuickAccessGrid() if (savedInstanceState == null) { intent?.let { sendLaunchPixels(it) } @@ -118,9 +126,15 @@ class SystemSearchActivity : DuckDuckGoActivity() { } ) viewModel.resultsViewState.observe( - this, - Observer { - it?.let { renderResultsViewState(it) } + this, { + when(it) { + is SystemSearchViewModel.Suggestions.SystemSearchResultsViewState -> { + renderResultsViewState(it) + } + is SystemSearchViewModel.Suggestions.QuickAccessItems -> { + renderQuickAccessItems(it) + } + } } ) viewModel.command.observe( @@ -161,6 +175,13 @@ class SystemSearchActivity : DuckDuckGoActivity() { deviceAppSuggestions.adapter = deviceAppSuggestionsAdapter } + private fun configureQuickAccessGrid() { + val layoutManager = GridLayoutManager(this, 4) + quickAccessRecyclerView.layoutManager = layoutManager + quickAccessAdapter = FavoritesQuickAccessAdapter(this, faviconManager) + quickAccessRecyclerView.adapter = quickAccessAdapter + } + private fun configureDaxButton() { logo.setOnClickListener { viewModel.userTappedDax() @@ -214,10 +235,16 @@ class SystemSearchActivity : DuckDuckGoActivity() { toggleButton.text = getString(toggleText) } - private fun renderResultsViewState(viewState: SystemSearchResultsViewState) { + private fun renderResultsViewState(viewState: SystemSearchViewModel.Suggestions.SystemSearchResultsViewState) { deviceLabel.isVisible = viewState.appResults.isNotEmpty() autocompleteSuggestionsAdapter.updateData(viewState.autocompleteResults.query, viewState.autocompleteResults.suggestions) deviceAppSuggestionsAdapter.updateData(viewState.appResults) + quickAccessRecyclerView.visibility = View.GONE + } + + private fun renderQuickAccessItems(it: SystemSearchViewModel.Suggestions.QuickAccessItems) { + quickAccessAdapter.submitList(it.favorites) + quickAccessRecyclerView.visibility = View.VISIBLE } private fun processCommand(command: SystemSearchViewModel.Command) { diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index ff587c7871b0..4599b3aad298 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -22,6 +22,8 @@ import androidx.lifecycle.viewModelScope import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoCompleteApi +import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter import com.duckduckgo.app.global.DefaultDispatcherProvider import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.SingleLiveEvent @@ -40,6 +42,7 @@ import io.reactivex.disposables.Disposable import io.reactivex.functions.BiFunction import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.TimeUnit @@ -53,6 +56,7 @@ class SystemSearchViewModel( private val autoComplete: AutoComplete, private val deviceAppLookup: DeviceAppLookup, private val pixel: Pixel, + private val favoritesRepository: FavoritesRepository, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() ) : ViewModel() { @@ -61,10 +65,14 @@ class SystemSearchViewModel( val expanded: Boolean = false ) - data class SystemSearchResultsViewState( - val autocompleteResults: AutoCompleteResult = AutoCompleteResult("", emptyList()), - val appResults: List = emptyList() - ) + sealed class Suggestions { + data class SystemSearchResultsViewState( + val autocompleteResults: AutoCompleteResult = AutoCompleteResult("", emptyList()), + val appResults: List = emptyList() + ) : Suggestions() + + data class QuickAccessItems(val favorites: List) : Suggestions() + } sealed class Command { object ClearInputText : Command() @@ -77,7 +85,7 @@ class SystemSearchViewModel( } val onboardingViewState: MutableLiveData = MutableLiveData() - val resultsViewState: MutableLiveData = MutableLiveData() + val resultsViewState: MutableLiveData = MutableLiveData() val command: SingleLiveEvent = SingleLiveEvent() private val resultsPublishSubject = PublishRelay.create() @@ -90,10 +98,15 @@ class SystemSearchViewModel( resetViewState() configureResults() refreshAppList() + viewModelScope.launch { + favoritesRepository.favorites().collect { favorite -> + resultsViewState.postValue(Suggestions.QuickAccessItems(favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) })) + } + } } private fun currentOnboardingState(): OnboardingViewState = onboardingViewState.value!! - private fun currentResultsState(): SystemSearchResultsViewState = resultsViewState.value!! + private fun currentResultsState(): Suggestions = resultsViewState.value!! fun resetViewState() { command.value = Command.ClearInputText @@ -114,7 +127,7 @@ class SystemSearchViewModel( private fun resetResultsState() { results = SystemSearchResult(AutoCompleteResult("", emptyList()), emptyList()) appsJob?.cancel() - resultsViewState.value = SystemSearchResultsViewState() + resultsViewState.value = Suggestions.QuickAccessItems(emptyList()) } private fun configureResults() { @@ -186,10 +199,18 @@ class SystemSearchViewModel( val updatedApps = if (hasMultiResults) appResults.take(RESULTS_MAX_RESULTS_PER_GROUP) else appResults resultsViewState.postValue( - currentResultsState().copy( - autocompleteResults = AutoCompleteResult(results.autocomplete.query, updatedSuggestions), - appResults = updatedApps - ) + when (val currentResultsState = currentResultsState()) { + is Suggestions.SystemSearchResultsViewState -> { + currentResultsState.copy( + autocompleteResults = AutoCompleteResult(results.autocomplete.query, updatedSuggestions), + appResults = updatedApps + ) + } + is Suggestions.QuickAccessItems -> Suggestions.SystemSearchResultsViewState( + autocompleteResults = AutoCompleteResult(results.autocomplete.query, updatedSuggestions), + appResults = updatedApps + ) + } ) } @@ -262,12 +283,19 @@ class SystemSearchViewModelFactory @Inject constructor( private val userStageStore: Provider, private val autoComplete: Provider, private val deviceAppLookup: Provider, + private val favoritesRepository: Provider, private val pixel: Provider ) : ViewModelFactoryPlugin { override fun create(modelClass: Class): T? { with(modelClass) { return when { - isAssignableFrom(SystemSearchViewModel::class.java) -> (SystemSearchViewModel(userStageStore.get(), autoComplete.get(), deviceAppLookup.get(), pixel.get()) as T) + isAssignableFrom(SystemSearchViewModel::class.java) -> (SystemSearchViewModel( + userStageStore.get(), + autoComplete.get(), + deviceAppLookup.get(), + pixel.get(), + favoritesRepository.get() + ) as T) else -> null } } diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index 21f3e5202d93..f688d720294c 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -110,14 +110,14 @@ - - + + + + + + + + + + diff --git a/app/src/main/res/layout/view_quick_access_item.xml b/app/src/main/res/layout/view_quick_access_item.xml new file mode 100644 index 000000000000..f1856b4b424f --- /dev/null +++ b/app/src/main/res/layout/view_quick_access_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file From 7fd8e6feb80a888c4e6a0b1163f95c184a00e351 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 19 Apr 2021 12:28:55 +0200 Subject: [PATCH 19/95] centering items in recyclerview --- app/src/main/res/layout/include_quick_access_items.xml | 5 ++++- app/src/main/res/layout/view_quick_access_item.xml | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/include_quick_access_items.xml b/app/src/main/res/layout/include_quick_access_items.xml index eb307b2cb3ed..462709adccb6 100644 --- a/app/src/main/res/layout/include_quick_access_items.xml +++ b/app/src/main/res/layout/include_quick_access_items.xml @@ -32,5 +32,8 @@ android:paddingBottom="8dp" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layout_behavior="@string/appbar_scrolling_view_behavior" - tools:listItem="@layout/view_quickAccessRecyclerView" /> + tools:itemCount="8" + tools:listItem="@layout/view_quick_access_item" + tools:showIn="@layout/activity_system_search" + tools:spanCount="4" /> diff --git a/app/src/main/res/layout/view_quick_access_item.xml b/app/src/main/res/layout/view_quick_access_item.xml index f1856b4b424f..a3336c7a502c 100644 --- a/app/src/main/res/layout/view_quick_access_item.xml +++ b/app/src/main/res/layout/view_quick_access_item.xml @@ -16,7 +16,7 @@ @@ -35,6 +35,8 @@ android:id="@+id/quickAccessTitle" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:gravity="center" android:ellipsize="end" android:maxLines="1" tools:text="Super long favorite title" /> From de62a449affce72d2f32596559bf83cb99997109 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 19 Apr 2021 22:46:01 +0200 Subject: [PATCH 20/95] drag-drop in System search activity --- .../app/bookmarks/db/FavoritesDao.kt | 17 +++++- .../bookmarks/model/FavoritesRepository.kt | 8 ++- .../app/bookmarks/ui/BookmarksActivity.kt | 2 +- .../app/bookmarks/ui/BookmarksAdapter.kt | 2 +- .../ui/BookmarksEntityQueryListener.kt | 1 - .../app/bookmarks/ui/BookmarksViewModel.kt | 14 ++--- .../app/bookmarks/ui/FavoritesAdapter.kt | 8 +-- .../app/bookmarks/ui/FavoritesDiffCallback.kt | 2 +- .../favorites/FavoritesQuickAccessAdapter.kt | 4 +- .../QuickAccessDragTouchItemListener.kt | 53 +++++++++++++++++++ .../app/systemsearch/SystemSearchActivity.kt | 18 ++++++- .../app/systemsearch/SystemSearchViewModel.kt | 23 +++++--- 12 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt index 653c10a10b45..d8ff573ec6a8 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt @@ -17,8 +17,11 @@ package com.duckduckgo.app.bookmarks.db import androidx.room.* +import com.duckduckgo.app.bookmarks.model.SavedSite import io.reactivex.Single import kotlinx.coroutines.flow.Flow +import org.jetbrains.anko.collections.forEachWithIndex +import java.util.* @Dao interface FavoritesDao { @@ -26,7 +29,7 @@ interface FavoritesDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(favorite: FavoriteEntity): Long - @Query("select * from favorites") + @Query("select * from favorites order by position") fun favorites(): Flow> @Query("select count(*) from favorites WHERE url LIKE :url") @@ -46,4 +49,16 @@ interface FavoritesDao { @Query("select position from favorites where id = ( select MAX(id) from favorites)") suspend fun getLastPosition(): Int? + + @Query("select * from favorites where id = :id") + fun favorite(id: Long): FavoriteEntity? + + @Transaction + suspend fun persistChanges(favorites: List) { + favorites.forEachWithIndex { index, favorite -> + val favoriteEntity = favorite(favorite.id) ?: return + favoriteEntity.position = index + update(favoriteEntity) + } + } } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index 268f8c84e285..813f7b5edc82 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.bookmarks.model import com.duckduckgo.app.bookmarks.db.FavoriteEntity import com.duckduckgo.app.bookmarks.db.FavoritesDao import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import java.io.Serializable @@ -26,6 +27,7 @@ interface FavoritesRepository { suspend fun insert(unsavedSite: SavedSite.UnsavedSite): SavedSite.Favorite suspend fun insert(favorite: SavedSite.Favorite): SavedSite.Favorite suspend fun update(favorite: SavedSite.Favorite) + suspend fun persistChanges(favorites: List) suspend fun favorites(): Flow> suspend fun delete(favorite: SavedSite.Favorite) } @@ -73,8 +75,12 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite favoritesDao.update(FavoriteEntity(favorite.id, favorite.title, favorite.url, favorite.position)) } + override suspend fun persistChanges(favorites: List) { + favoritesDao.persistChanges(favorites) + } + override suspend fun favorites(): Flow> { - return favoritesDao.favorites().map { favorites -> favorites.map { SavedSite.Favorite(it.id, it.title, it.url, it.position) } } + return favoritesDao.favorites().distinctUntilChanged().map { favorites -> favorites.map { SavedSite.Favorite(it.id, it.title, it.url, it.position) } } } override suspend fun delete(favorite: SavedSite.Favorite) { 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 index 6b01fb0491aa..afc99174acf9 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -61,7 +61,7 @@ class BookmarksActivity : DuckDuckGoActivity() { bookmarksAdapter = BookmarksAdapter(layoutInflater, viewModel, this, faviconManager) favoritesAdapter = FavoritesAdapter(layoutInflater, viewModel, this, faviconManager) recycler.adapter = ConcatAdapter(favoritesAdapter, bookmarksAdapter) - recycler.setItemAnimator(null); + recycler.setItemAnimator(null) } private fun observeViewModel() { diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt index 46fa7f3a1a8f..b304ff96ffee 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksAdapter.kt @@ -100,7 +100,7 @@ class BookmarksAdapter( } override fun getItemViewType(position: Int): Int { - return when(bookmarkItems[position]) { + return when (bookmarkItems[position]) { is Header -> BOOKMARK_SECTION_TITLE_TYPE is EmptyHint -> EMPTY_STATE_TYPE else -> BOOKMARK_TYPE diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt index 088a56844bef..b30f65b5db96 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksEntityQueryListener.kt @@ -17,7 +17,6 @@ package com.duckduckgo.app.bookmarks.ui import androidx.appcompat.widget.SearchView -import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.model.SavedSite class BookmarksEntityQueryListener( 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 index a8cdb11cf90a..cfdca6b3081e 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -197,13 +197,13 @@ class BookmarksViewModelFactory @Inject constructor( with(modelClass) { return when { isAssignableFrom(BookmarksViewModel::class.java) -> ( - BookmarksViewModel( - favoritesRepository.get(), - dao.get(), - faviconManager.get(), - dispatcherProvider.get() - ) as T - ) + BookmarksViewModel( + favoritesRepository.get(), + dao.get(), + faviconManager.get(), + dispatcherProvider.get() + ) as T + ) else -> null } } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt index 35fcfd49b359..7a35b80ca715 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesAdapter.kt @@ -50,9 +50,9 @@ class FavoritesAdapter( } interface FavoriteItemTypes - object Header: FavoriteItemTypes - object EmptyHint: FavoriteItemTypes - data class FavoriteItem(val favorite: Favorite): FavoriteItemTypes + object Header : FavoriteItemTypes + object EmptyHint : FavoriteItemTypes + data class FavoriteItem(val favorite: Favorite) : FavoriteItemTypes var favoriteItems: List = emptyList() set(value) { @@ -102,7 +102,7 @@ class FavoritesAdapter( } override fun getItemViewType(position: Int): Int { - return when(favoriteItems[position]) { + return when (favoriteItems[position]) { is Header -> { FAVORITE_SECTION_TITLE_TYPE } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesDiffCallback.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesDiffCallback.kt index 7503215a6ea6..6c55eada67dc 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesDiffCallback.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/FavoritesDiffCallback.kt @@ -36,4 +36,4 @@ class FavoritesDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: QuickAccessFavorite, newItem: QuickAccessFavorite): Boolean { - return oldItem == newItem + return oldItem.favorite.id == newItem.favorite.id } override fun areContentsTheSame(oldItem: QuickAccessFavorite, newItem: QuickAccessFavorite): Boolean { return oldItem == newItem } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt new file mode 100644 index 000000000000..a1a4db2a4ebe --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 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.favorites + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessFavorite +import timber.log.Timber + +class QuickAccessDragTouchItemListener( + private val favoritesQuickAccessAdapter: FavoritesQuickAccessAdapter, + private val dragDropListener: DragDropListener +) : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.START or ItemTouchHelper.END, + 0 +) { + interface DragDropListener { + fun onListChanged(listElements: List) + } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + Timber.i("QuickAccessDragTouchItemListener onMove ${viewHolder.bindingAdapterPosition} to ${target.bindingAdapterPosition}") + val items = favoritesQuickAccessAdapter.currentList.toMutableList() + val quickAccessFavorite = items[viewHolder.bindingAdapterPosition] + items.removeAt(viewHolder.bindingAdapterPosition) + items.add(target.bindingAdapterPosition, quickAccessFavorite) + favoritesQuickAccessAdapter.submitList(items) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + // noop + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + dragDropListener.onListChanged(favoritesQuickAccessAdapter.currentList) + } +} diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index dbbd309f3a4c..6ff2848d0de8 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -30,12 +30,14 @@ import android.widget.Toast.LENGTH_SHORT import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter +import com.duckduckgo.app.browser.favorites.QuickAccessDragTouchItemListener import com.duckduckgo.app.browser.omnibar.OmnibarScrolling import com.duckduckgo.app.fire.DataClearerForegroundAppRestartPixel import com.duckduckgo.app.global.DuckDuckGoActivity @@ -126,8 +128,9 @@ class SystemSearchActivity : DuckDuckGoActivity() { } ) viewModel.resultsViewState.observe( - this, { - when(it) { + this, + { + when (it) { is SystemSearchViewModel.Suggestions.SystemSearchResultsViewState -> { renderResultsViewState(it) } @@ -179,6 +182,17 @@ class SystemSearchActivity : DuckDuckGoActivity() { val layoutManager = GridLayoutManager(this, 4) quickAccessRecyclerView.layoutManager = layoutManager quickAccessAdapter = FavoritesQuickAccessAdapter(this, faviconManager) + val itemTouchHelper = ItemTouchHelper( + QuickAccessDragTouchItemListener( + quickAccessAdapter, + object : QuickAccessDragTouchItemListener.DragDropListener { + override fun onListChanged(listElements: List) { + viewModel.onQuickAccessListChanged(listElements) + } + } + ) + ) + itemTouchHelper.attachToRecyclerView(quickAccessRecyclerView) quickAccessRecyclerView.adapter = quickAccessAdapter } diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index 4599b3aad298..afc1de3c33a7 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -272,6 +272,13 @@ class SystemSearchViewModel( super.onCleared() } + fun onQuickAccessListChanged(newList: List) { + viewModelScope.launch(dispatchers.io()) { + Timber.i("Persist favorites $newList") + favoritesRepository.persistChanges(newList.map { it.favorite }) + } + } + companion object { private const val DEBOUNCE_TIME_MS = 200L private const val RESULTS_MAX_RESULTS_PER_GROUP = 4 @@ -289,13 +296,15 @@ class SystemSearchViewModelFactory @Inject constructor( override fun create(modelClass: Class): T? { with(modelClass) { return when { - isAssignableFrom(SystemSearchViewModel::class.java) -> (SystemSearchViewModel( - userStageStore.get(), - autoComplete.get(), - deviceAppLookup.get(), - pixel.get(), - favoritesRepository.get() - ) as T) + isAssignableFrom(SystemSearchViewModel::class.java) -> ( + SystemSearchViewModel( + userStageStore.get(), + autoComplete.get(), + deviceAppLookup.get(), + pixel.get(), + favoritesRepository.get() + ) as T + ) else -> null } } From 7405d30ed449eee2058efe47b42520ea49f1c21f Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 22 Apr 2021 11:00:33 +0200 Subject: [PATCH 21/95] UI improvements on quick access view --- .../res/layout/include_quick_access_items.xml | 1 + .../res/layout/view_quick_access_item.xml | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/layout/include_quick_access_items.xml b/app/src/main/res/layout/include_quick_access_items.xml index 462709adccb6..d245531ea82d 100644 --- a/app/src/main/res/layout/include_quick_access_items.xml +++ b/app/src/main/res/layout/include_quick_access_items.xml @@ -30,6 +30,7 @@ android:clipToPadding="false" android:paddingTop="8dp" android:paddingBottom="8dp" + android:overScrollMode="never" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:itemCount="8" diff --git a/app/src/main/res/layout/view_quick_access_item.xml b/app/src/main/res/layout/view_quick_access_item.xml index a3336c7a502c..881d6aab1f2a 100644 --- a/app/src/main/res/layout/view_quick_access_item.xml +++ b/app/src/main/res/layout/view_quick_access_item.xml @@ -21,24 +21,37 @@ android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:orientation="vertical"> + android:orientation="vertical" + android:layout_margin="15dp"> - + app:cardCornerRadius="6dp" + app:cardElevation="4dp"> + + + \ No newline at end of file From d82870fa69738967b048a7f1ed04f21641a81b75 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 22 Apr 2021 11:25:05 +0200 Subject: [PATCH 22/95] show/hide context menu depending on user interactions: - if user moves the item, hide menu --- .../favorites/FavoritesQuickAccessAdapter.kt | 39 ++++++++++++++++--- .../QuickAccessDragTouchItemListener.kt | 4 ++ .../app/systemsearch/SystemSearchActivity.kt | 8 +++- .../res/layout/view_quick_access_item.xml | 14 +++---- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index d4b1ee3bbfd2..e712db0c377a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -16,9 +16,9 @@ package com.duckduckgo.app.browser.favorites -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.os.Handler +import android.view.* +import androidx.constraintlayout.motion.widget.MotionController import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil @@ -32,10 +32,12 @@ import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAcc import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessViewHolder import kotlinx.android.synthetic.main.view_quick_access_item.view.* import kotlinx.coroutines.launch +import timber.log.Timber class FavoritesQuickAccessAdapter( private val lifecycleOwner: LifecycleOwner, - private val faviconManager: FaviconManager + private val faviconManager: FaviconManager, + private val onMoveListener: (RecyclerView.ViewHolder) -> Unit ) : ListAdapter(QuickAccessAdapterDiffCallback()) { data class QuickAccessFavorite(val favorite: SavedSite.Favorite) : FavoritesAdapter.FavoriteItemTypes @@ -44,13 +46,38 @@ class FavoritesQuickAccessAdapter( private val layoutInflater: LayoutInflater, itemView: View, private val lifecycleOwner: LifecycleOwner, - private val faviconManager: FaviconManager + private val faviconManager: FaviconManager, + private val onMoveListener: (RecyclerView.ViewHolder) -> Unit ) : RecyclerView.ViewHolder(itemView) { + private var menu: Menu? = null + fun bind(item: QuickAccessFavorite) { with(item.favorite) { itemView.quickAccessTitle.text = title loadFavicon(url) + itemView.quickAccessFaviconCard.setOnLongClickListener { + Timber.i("QuickAccessFav: longPress") + false + } + + itemView.quickAccessFaviconCard.setOnTouchListener { v, event -> + if(event.actionMasked == MotionEvent.ACTION_MOVE) { + Timber.i("QuickAccessFav: move") + onMoveListener(this@QuickAccessViewHolder) + Handler().post { + menu?.close() + } + } + false + } + + itemView.quickAccessFaviconCard.setOnCreateContextMenuListener { menu, v, menuInfo -> + this@QuickAccessViewHolder.menu = menu + Timber.i("QuickAccessFav: setOnCreateContextMenuListener") + val Edit: MenuItem = menu.add(Menu.NONE, 1, 1, "Edit") + val Delete: MenuItem = menu.add(Menu.NONE, 2, 2, "Delete") + } } } @@ -64,7 +91,7 @@ class FavoritesQuickAccessAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuickAccessViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.view_quick_access_item, parent, false) - return QuickAccessViewHolder(inflater, view, lifecycleOwner, faviconManager) + return QuickAccessViewHolder(inflater, view, lifecycleOwner, faviconManager, onMoveListener) } override fun onBindViewHolder(holder: QuickAccessViewHolder, position: Int) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt index a1a4db2a4ebe..b2570ae9195b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt @@ -32,6 +32,10 @@ class QuickAccessDragTouchItemListener( fun onListChanged(listElements: List) } + override fun isLongPressDragEnabled(): Boolean { + return false + } + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { Timber.i("QuickAccessDragTouchItemListener onMove ${viewHolder.bindingAdapterPosition} to ${target.bindingAdapterPosition}") val items = favoritesQuickAccessAdapter.currentList.toMutableList() diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index 6ff2848d0de8..ed188396b254 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -78,6 +78,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { private lateinit var autocompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter private lateinit var deviceAppSuggestionsAdapter: DeviceAppSuggestionsAdapter private lateinit var quickAccessAdapter: FavoritesQuickAccessAdapter + private lateinit var itemTouchHelper: ItemTouchHelper private val textChangeWatcher = object : TextChangedWatcher() { override fun afterTextChanged(editable: Editable) { @@ -181,8 +182,10 @@ class SystemSearchActivity : DuckDuckGoActivity() { private fun configureQuickAccessGrid() { val layoutManager = GridLayoutManager(this, 4) quickAccessRecyclerView.layoutManager = layoutManager - quickAccessAdapter = FavoritesQuickAccessAdapter(this, faviconManager) - val itemTouchHelper = ItemTouchHelper( + quickAccessAdapter = FavoritesQuickAccessAdapter(this, faviconManager, { viewHolder -> + itemTouchHelper.startDrag(viewHolder) + }) + itemTouchHelper = ItemTouchHelper( QuickAccessDragTouchItemListener( quickAccessAdapter, object : QuickAccessDragTouchItemListener.DragDropListener { @@ -192,6 +195,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { } ) ) + itemTouchHelper.attachToRecyclerView(quickAccessRecyclerView) quickAccessRecyclerView.adapter = quickAccessAdapter } diff --git a/app/src/main/res/layout/view_quick_access_item.xml b/app/src/main/res/layout/view_quick_access_item.xml index 881d6aab1f2a..42d71a212f23 100644 --- a/app/src/main/res/layout/view_quick_access_item.xml +++ b/app/src/main/res/layout/view_quick_access_item.xml @@ -18,18 +18,18 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:attr/selectableItemBackground" - android:clickable="true" - android:focusable="true" + xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" - android:layout_margin="15dp"> + android:padding="15dp"> - From 9335ee49cd1b9b040cea5695e705a2f0a8def760 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 22 Apr 2021 11:26:54 +0200 Subject: [PATCH 23/95] inline method --- .../app/browser/favorites/FavoritesQuickAccessAdapter.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index e712db0c377a..f8dbe6375d5a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -65,9 +65,7 @@ class FavoritesQuickAccessAdapter( if(event.actionMasked == MotionEvent.ACTION_MOVE) { Timber.i("QuickAccessFav: move") onMoveListener(this@QuickAccessViewHolder) - Handler().post { - menu?.close() - } + Handler().post { menu?.close() } } false } From 9b6008c381ca91995e9f68accba189d315b85bab Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 22 Apr 2021 11:52:30 +0200 Subject: [PATCH 24/95] fix: show quickaccess items when closing autocomplete --- .../com/duckduckgo/app/systemsearch/SystemSearchActivity.kt | 1 - .../duckduckgo/app/systemsearch/SystemSearchViewModel.kt | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index ed188396b254..c2a17e68ff1d 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -257,7 +257,6 @@ class SystemSearchActivity : DuckDuckGoActivity() { deviceLabel.isVisible = viewState.appResults.isNotEmpty() autocompleteSuggestionsAdapter.updateData(viewState.autocompleteResults.query, viewState.autocompleteResults.suggestions) deviceAppSuggestionsAdapter.updateData(viewState.appResults) - quickAccessRecyclerView.visibility = View.GONE } private fun renderQuickAccessItems(it: SystemSearchViewModel.Suggestions.QuickAccessItems) { diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index afc1de3c33a7..7601900ed137 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -91,6 +91,7 @@ class SystemSearchViewModel( private val resultsPublishSubject = PublishRelay.create() private var results = SystemSearchResult(AutoCompleteResult("", emptyList()), emptyList()) private var resultsDisposable: Disposable? = null + private var latestQuickAccessItems: Suggestions.QuickAccessItems = Suggestions.QuickAccessItems(emptyList()) private var appsJob: Job? = null @@ -100,7 +101,8 @@ class SystemSearchViewModel( refreshAppList() viewModelScope.launch { favoritesRepository.favorites().collect { favorite -> - resultsViewState.postValue(Suggestions.QuickAccessItems(favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) })) + latestQuickAccessItems = Suggestions.QuickAccessItems(favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) }) + resultsViewState.postValue(latestQuickAccessItems) } } } @@ -127,7 +129,7 @@ class SystemSearchViewModel( private fun resetResultsState() { results = SystemSearchResult(AutoCompleteResult("", emptyList()), emptyList()) appsJob?.cancel() - resultsViewState.value = Suggestions.QuickAccessItems(emptyList()) + resultsViewState.value = latestQuickAccessItems } private fun configureResults() { From 74603e90f7d6991b739520f23a5e27f627658a14 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 22 Apr 2021 12:45:47 +0200 Subject: [PATCH 25/95] edit/delete quickaccess items in SystemSearchActivity screen --- .../favorites/FavoritesQuickAccessAdapter.kt | 30 +++++++++--- .../app/systemsearch/SystemSearchActivity.kt | 46 +++++++++++++++++-- .../app/systemsearch/SystemSearchViewModel.kt | 46 ++++++++++++++++++- 3 files changed, 110 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index f8dbe6375d5a..0e4e748aa4c3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.browser.favorites import android.os.Handler import android.view.* -import androidx.constraintlayout.motion.widget.MotionController import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil @@ -37,7 +36,10 @@ import timber.log.Timber class FavoritesQuickAccessAdapter( private val lifecycleOwner: LifecycleOwner, private val faviconManager: FaviconManager, - private val onMoveListener: (RecyclerView.ViewHolder) -> Unit + private val onMoveListener: (RecyclerView.ViewHolder) -> Unit, + private val onItemSelected: (QuickAccessFavorite) -> Unit, + private val onEditClicked: (QuickAccessFavorite) -> Unit, + private val onDeleteClicked: (QuickAccessFavorite) -> Unit ) : ListAdapter(QuickAccessAdapterDiffCallback()) { data class QuickAccessFavorite(val favorite: SavedSite.Favorite) : FavoritesAdapter.FavoriteItemTypes @@ -47,7 +49,10 @@ class FavoritesQuickAccessAdapter( itemView: View, private val lifecycleOwner: LifecycleOwner, private val faviconManager: FaviconManager, - private val onMoveListener: (RecyclerView.ViewHolder) -> Unit + private val onMoveListener: (RecyclerView.ViewHolder) -> Unit, + private val onItemSelected: (QuickAccessFavorite) -> Unit, + private val onEditClicked: (QuickAccessFavorite) -> Unit, + private val onDeleteClicked: (QuickAccessFavorite) -> Unit ) : RecyclerView.ViewHolder(itemView) { private var menu: Menu? = null @@ -62,7 +67,7 @@ class FavoritesQuickAccessAdapter( } itemView.quickAccessFaviconCard.setOnTouchListener { v, event -> - if(event.actionMasked == MotionEvent.ACTION_MOVE) { + if (event.actionMasked == MotionEvent.ACTION_MOVE) { Timber.i("QuickAccessFav: move") onMoveListener(this@QuickAccessViewHolder) Handler().post { menu?.close() } @@ -73,9 +78,20 @@ class FavoritesQuickAccessAdapter( itemView.quickAccessFaviconCard.setOnCreateContextMenuListener { menu, v, menuInfo -> this@QuickAccessViewHolder.menu = menu Timber.i("QuickAccessFav: setOnCreateContextMenuListener") - val Edit: MenuItem = menu.add(Menu.NONE, 1, 1, "Edit") - val Delete: MenuItem = menu.add(Menu.NONE, 2, 2, "Delete") + val editMenuItem: MenuItem = menu.add(Menu.NONE, 1, 1, "Edit") + val deleteMenuItem: MenuItem = menu.add(Menu.NONE, 2, 2, "Delete") + editMenuItem.setOnMenuItemClickListener { + onEditClicked(item) + true + } + + deleteMenuItem.setOnMenuItemClickListener { + onDeleteClicked(item) + true + } } + + itemView.quickAccessFaviconCard.setOnClickListener { onItemSelected(item) } } } @@ -89,7 +105,7 @@ class FavoritesQuickAccessAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuickAccessViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.view_quick_access_item, parent, false) - return QuickAccessViewHolder(inflater, view, lifecycleOwner, faviconManager, onMoveListener) + return QuickAccessViewHolder(inflater, view, lifecycleOwner, faviconManager, onMoveListener, onItemSelected, onEditClicked, onDeleteClicked) } override fun onBindViewHolder(holder: QuickAccessViewHolder, position: Int) { diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index c2a17e68ff1d..067ddcf829b7 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -32,6 +32,8 @@ import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import com.duckduckgo.app.bookmarks.model.SavedSite +import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter @@ -43,9 +45,12 @@ import com.duckduckgo.app.fire.DataClearerForegroundAppRestartPixel import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.view.TextChangedWatcher import com.duckduckgo.app.global.view.hideKeyboard +import com.duckduckgo.app.global.view.html import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.* +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.activity_bookmarks.* import kotlinx.android.synthetic.main.activity_system_search.* import kotlinx.android.synthetic.main.activity_system_search.appBarLayout import kotlinx.android.synthetic.main.activity_system_search.autocompleteSuggestions @@ -58,6 +63,7 @@ import kotlinx.android.synthetic.main.activity_system_search.results import kotlinx.android.synthetic.main.activity_system_search.resultsContent import kotlinx.android.synthetic.main.include_quick_access_items.* import kotlinx.android.synthetic.main.include_system_search_onboarding.* +import timber.log.Timber import javax.inject.Inject class SystemSearchActivity : DuckDuckGoActivity() { @@ -131,6 +137,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { viewModel.resultsViewState.observe( this, { + Timber.i("SystemSearchActivity d: $it") when (it) { is SystemSearchViewModel.Suggestions.SystemSearchResultsViewState -> { renderResultsViewState(it) @@ -182,9 +189,21 @@ class SystemSearchActivity : DuckDuckGoActivity() { private fun configureQuickAccessGrid() { val layoutManager = GridLayoutManager(this, 4) quickAccessRecyclerView.layoutManager = layoutManager - quickAccessAdapter = FavoritesQuickAccessAdapter(this, faviconManager, { viewHolder -> - itemTouchHelper.startDrag(viewHolder) - }) + quickAccessAdapter = FavoritesQuickAccessAdapter( + this, faviconManager, + { viewHolder -> + itemTouchHelper.startDrag(viewHolder) + }, + { + viewModel.onQuickAccesItemClicked(it) + }, + { + viewModel.onEditQuickAccessItemRequested(it) + }, + { + confirmDeleteSavedSite(it.favorite) + } + ) itemTouchHelper = ItemTouchHelper( QuickAccessDragTouchItemListener( quickAccessAdapter, @@ -210,6 +229,12 @@ class SystemSearchActivity : DuckDuckGoActivity() { resultsContent.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> updateScroll() } } + private fun showEditSavedSiteDialog(savedSite: SavedSite) { + val dialog = EditBookmarkDialogFragment.instance(savedSite) + dialog.show(supportFragmentManager, "EDIT_BOOKMARK") + dialog.listener = viewModel + } + private fun updateScroll() { val scrollable = resultsContent.height > (results.height - results.paddingTop - results.paddingBottom) if (scrollable) { @@ -289,6 +314,9 @@ class SystemSearchActivity : DuckDuckGoActivity() { is EditQuery -> { editQuery(command.query) } + is LaunchEditDialog -> { + showEditSavedSiteDialog(command.savedSite) + } } } @@ -297,6 +325,18 @@ class SystemSearchActivity : DuckDuckGoActivity() { omnibarTextInput.setSelection(query.length) } + private fun confirmDeleteSavedSite(savedSite: SavedSite) { + val message = getString(R.string.bookmarkDeleteConfirmationMessage, savedSite.title).html(this) + viewModel.deleteQuickAccessItem(savedSite) + Snackbar.make( + rootView, + message, + Snackbar.LENGTH_LONG + ).setAction(R.string.fireproofWebsiteSnackbarAction) { + viewModel.insertQuickAccessItem(savedSite) + }.show() + } + private fun launchDuckDuckGo() { startActivity(BrowserActivity.intent(this)) finish() diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index 7601900ed137..7fb806daabd0 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -23,6 +23,9 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoCompleteApi import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.bookmarks.model.SavedSite +import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment +import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter import com.duckduckgo.app.global.DefaultDispatcherProvider import com.duckduckgo.app.global.DispatcherProvider @@ -42,6 +45,7 @@ import io.reactivex.disposables.Disposable import io.reactivex.functions.BiFunction import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import timber.log.Timber @@ -57,8 +61,9 @@ class SystemSearchViewModel( private val deviceAppLookup: DeviceAppLookup, private val pixel: Pixel, private val favoritesRepository: FavoritesRepository, + private val faviconManager: FaviconManager, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() -) : ViewModel() { +) : ViewModel(), EditBookmarkDialogFragment.EditBookmarkListener { data class OnboardingViewState( val visible: Boolean, @@ -78,6 +83,7 @@ class SystemSearchViewModel( object ClearInputText : Command() object LaunchDuckDuckGo : Command() data class LaunchBrowser(val query: String) : Command() + data class LaunchEditDialog(val savedSite: SavedSite) : Command() data class LaunchDeviceApplication(val deviceApp: DeviceApp) : Command() data class ShowAppNotFoundMessage(val appName: String) : Command() object DismissKeyboard : Command() @@ -281,10 +287,44 @@ class SystemSearchViewModel( } } + fun onQuickAccesItemClicked(it: FavoritesQuickAccessAdapter.QuickAccessFavorite) { + command.value = Command.LaunchBrowser(it.favorite.url) + } + + fun onEditQuickAccessItemRequested(it: FavoritesQuickAccessAdapter.QuickAccessFavorite) { + command.value = Command.LaunchEditDialog(it.favorite) + } + companion object { private const val DEBOUNCE_TIME_MS = 200L private const val RESULTS_MAX_RESULTS_PER_GROUP = 4 } + + override fun onSavedSiteEdited(savedSite: SavedSite) { + when (savedSite) { + is SavedSite.Favorite -> { + viewModelScope.launch(dispatchers.io()) { + favoritesRepository.update(savedSite) + } + } + else -> throw IllegalArgumentException("Illegal SavedSite to edit received") + } + } + + fun deleteQuickAccessItem(savedSite: SavedSite) { + val favorite = savedSite as? SavedSite.Favorite ?: return + viewModelScope.launch(dispatchers.io() + NonCancellable) { + faviconManager.deletePersistedFavicon(savedSite.url) + favoritesRepository.delete(favorite) + } + } + + fun insertQuickAccessItem(savedSite: SavedSite) { + val favorite = savedSite as? SavedSite.Favorite ?: return + viewModelScope.launch(dispatchers.io()) { + favoritesRepository.insert(favorite) + } + } } @ContributesMultibinding(AppObjectGraph::class) @@ -293,6 +333,7 @@ class SystemSearchViewModelFactory @Inject constructor( private val autoComplete: Provider, private val deviceAppLookup: Provider, private val favoritesRepository: Provider, + private val faviconManager: Provider, private val pixel: Provider ) : ViewModelFactoryPlugin { override fun create(modelClass: Class): T? { @@ -304,7 +345,8 @@ class SystemSearchViewModelFactory @Inject constructor( autoComplete.get(), deviceAppLookup.get(), pixel.get(), - favoritesRepository.get() + favoritesRepository.get(), + faviconManager.get() ) as T ) else -> null From e798955cfdc7850f5ebd45f616a744b12a6e350d Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 22 Apr 2021 17:24:22 +0200 Subject: [PATCH 26/95] show quickAccess items in BrowserTabFragment --- .../app/browser/BrowserTabFragment.kt | 83 +++++++++++++++++-- .../app/browser/BrowserTabViewModel.kt | 45 +++++++++- .../res/layout/include_new_browser_tab.xml | 6 ++ .../res/layout/include_quick_access_items.xml | 2 +- 4 files changed, 124 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 912c33a44cbc..51d66e9d552d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -56,6 +56,8 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.commitNow import androidx.fragment.app.transaction import androidx.lifecycle.* +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion import com.duckduckgo.app.bookmarks.model.SavedSite @@ -72,6 +74,9 @@ import com.duckduckgo.app.browser.downloader.DownloadFailReason import com.duckduckgo.app.browser.downloader.FileDownloadNotificationManager import com.duckduckgo.app.browser.downloader.FileDownloader import com.duckduckgo.app.browser.downloader.FileDownloader.PendingFileDownload +import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter +import com.duckduckgo.app.browser.favorites.QuickAccessDragTouchItemListener import com.duckduckgo.app.browser.filechooser.FileChooserIntentBuilder import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore import com.duckduckgo.app.browser.logindetection.DOMLoginDetector @@ -116,6 +121,7 @@ import kotlinx.android.synthetic.main.include_find_in_page.* import kotlinx.android.synthetic.main.include_new_browser_tab.* import kotlinx.android.synthetic.main.include_omnibar_toolbar.* import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.* +import kotlinx.android.synthetic.main.include_quick_access_items.* import kotlinx.android.synthetic.main.popup_window_browser_menu.view.* import kotlinx.coroutines.* import org.jetbrains.anko.longToast @@ -202,6 +208,9 @@ class BrowserTabFragment : @Inject lateinit var thirdPartyCookieManager: ThirdPartyCookieManager + @Inject + lateinit var faviconManager: FaviconManager + var messageFromPreviousTab: Message? = null private val initialUrl get() = requireArguments().getString(URL_EXTRA_ARG) @@ -221,6 +230,10 @@ class BrowserTabFragment : private lateinit var decorator: BrowserTabFragmentDecorator + private lateinit var quickAccessAdapter: FavoritesQuickAccessAdapter + + private lateinit var itemTouchHelper: ItemTouchHelper + private val viewModel: BrowserTabViewModel by lazy { val viewModel = ViewModelProvider(this, viewModelFactory).get(BrowserTabViewModel::class.java) viewModel.loadData(tabId, initialUrl, skipHome) @@ -304,6 +317,7 @@ class BrowserTabFragment : configureOmnibarTextInput() configureFindInPage() configureAutoComplete() + configureQuickAccessGrid() decorator.decorateWithFeatures() @@ -487,15 +501,13 @@ class BrowserTabFragment : newTabLayout.show() appBarLayout.setExpanded(true) webView?.onPause() - webView?.hide() - homeBackgroundLogo.showLogo() + webView?.gone() } private fun showBrowser() { newTabLayout.gone() webView?.show() webView?.onResume() - homeBackgroundLogo.hideLogo() } fun submitQuery(query: String) { @@ -890,6 +902,40 @@ class BrowserTabFragment : autoCompleteSuggestionsList.adapter = autoCompleteSuggestionsAdapter } + private fun configureQuickAccessGrid() { + val layoutManager = GridLayoutManager(requireContext(), 4) + quickAccessRecyclerView.layoutManager = layoutManager + quickAccessAdapter = FavoritesQuickAccessAdapter( + this, faviconManager, + { viewHolder -> + itemTouchHelper.startDrag(viewHolder) + }, + { + //viewModel.onQuickAccesItemClicked(it) + }, + { + //viewModel.onEditQuickAccessItemRequested(it) + }, + { + //confirmDeleteSavedSite(it.favorite) + } + ) + itemTouchHelper = ItemTouchHelper( + QuickAccessDragTouchItemListener( + quickAccessAdapter, + object : QuickAccessDragTouchItemListener.DragDropListener { + override fun onListChanged(listElements: List) { + //viewModel.onQuickAccessListChanged(listElements) + } + } + ) + ) + + itemTouchHelper.attachToRecyclerView(quickAccessRecyclerView) + quickAccessRecyclerView.adapter = quickAccessAdapter + } + + private fun configurePrivacyGrade() { toolbar.privacyGradeButton.setOnClickListener { browserActivity?.launchPrivacyDashboard() @@ -1838,17 +1884,18 @@ class BrowserTabFragment : lastSeenCtaViewState = viewState removeNewTabLayoutClickListener() if (viewState.cta != null) { - showCta(viewState.cta) + showCta(viewState.cta, viewState.favorites) } else { hideHomeCta() hideDaxCta() + showHomeBackground(viewState.favorites) } } } - private fun showCta(configuration: Cta) { + private fun showCta(configuration: Cta, favorites: List) { when (configuration) { - is HomePanelCta -> showHomeCta(configuration) + is HomePanelCta -> showHomeCta(configuration, favorites) is DaxBubbleCta -> showDaxCta(configuration) is DialogCta -> showDaxDialogCta(configuration) } @@ -1872,7 +1919,7 @@ class BrowserTabFragment : } private fun showDaxCta(configuration: DaxBubbleCta) { - homeBackgroundLogo.hideLogo() + hideHomeBackground() hideHomeCta() configuration.showCta(daxCtaContainer) newTabLayout.setOnClickListener { daxCtaContainer.dialogTextCta.finishAnimation() } @@ -1883,17 +1930,35 @@ class BrowserTabFragment : newTabLayout.setOnClickListener(null) } - private fun showHomeCta(configuration: HomePanelCta) { + private fun showHomeCta(configuration: HomePanelCta, favorites: List) { hideDaxCta() if (ctaContainer.isEmpty()) { renderHomeCta() } else { configuration.showCta(ctaContainer) } - homeBackgroundLogo.showLogo() + showHomeBackground(favorites) viewModel.onCtaShown() } + private fun showHomeBackground(favorites: List) { + Timber.i("BrowserTab favs: showHomeBackground $favorites") + if (favorites.isEmpty()) { + homeBackgroundLogo.showLogo() + quickAccessRecyclerView.visibility = GONE + } else { + homeBackgroundLogo.hideLogo() + quickAccessAdapter.submitList(favorites) + quickAccessRecyclerView.visibility = VISIBLE + } + } + + private fun hideHomeBackground() { + Timber.i("BrowserTab favs: hideHomeBackground") + homeBackgroundLogo.hideLogo() + quickAccessRecyclerView.visibility = GONE + } + private fun hideDaxCta() { dialogTextCta.cancelAnimation() daxCtaContainer.hide() diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index c7d750566c9a..0041dbffad54 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -56,6 +56,7 @@ import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.downloader.DownloadFailReason import com.duckduckgo.app.browser.downloader.FileDownloader import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter import com.duckduckgo.app.browser.logindetection.FireproofDialogsEventHandler import com.duckduckgo.app.browser.logindetection.FireproofDialogsEventHandler.Event import com.duckduckgo.app.browser.logindetection.LoginDetected @@ -102,6 +103,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter import com.duckduckgo.app.surrogates.SurrogateResponse import com.duckduckgo.app.survey.model.Survey +import com.duckduckgo.app.systemsearch.SystemSearchViewModel import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.trackerdetection.model.TrackingEvent @@ -164,7 +166,8 @@ class BrowserTabViewModel( } data class CtaViewState( - val cta: Cta? = null + val cta: Cta? = null, + val favorites: List = emptyList() ) data class BrowserViewState( @@ -407,6 +410,12 @@ class BrowserTabViewModel( } } } + viewModelScope.launch { + favoritesRepository.favorites().collect { favorite -> + Timber.i("BrowserTab favs: collect $favorite") + ctaViewState.postValue(currentCtaViewState().copy(favorites = favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) })) + } + } } fun loadData(tabId: String, initialUrl: String?, skipHome: Boolean) { @@ -1920,7 +1929,39 @@ class BrowserTabViewModelFactory @Inject constructor( override fun create(modelClass: Class): T? { with(modelClass) { return when { - isAssignableFrom(BrowserTabViewModel::class.java) -> BrowserTabViewModel(statisticsUpdater.get(), queryUrlConverter.get(), duckDuckGoUrlDetector.get(), siteFactory.get(), tabRepository.get(), userWhitelistDao.get(), networkLeaderboardDao.get(), bookmarksDao.get(), favoritesRepository.get(), fireproofWebsiteRepository.get(), locationPermissionsRepository.get(), geoLocationPermissions.get(), navigationAwareLoginDetector.get(), autoComplete.get(), appSettingsPreferencesStore.get(), longPressHandler.get(), webViewSessionStorage.get(), specialUrlDetector.get(), faviconManager.get(), addToHomeCapabilityDetector.get(), ctaViewModel.get(), searchCountDao.get(), pixel.get(), dispatchers, userEventsStore.get(), notificationDao.get(), useOurAppDetector.get(), variantManager.get(), fileDownloader.get(), globalPrivacyControl.get(), fireproofDialogsEventHandler.get()) as T + isAssignableFrom(BrowserTabViewModel::class.java) -> BrowserTabViewModel( + statisticsUpdater.get(), + queryUrlConverter.get(), + duckDuckGoUrlDetector.get(), + siteFactory.get(), + tabRepository.get(), + userWhitelistDao.get(), + networkLeaderboardDao.get(), + bookmarksDao.get(), + favoritesRepository.get(), + fireproofWebsiteRepository.get(), + locationPermissionsRepository.get(), + geoLocationPermissions.get(), + navigationAwareLoginDetector.get(), + autoComplete.get(), + appSettingsPreferencesStore.get(), + longPressHandler.get(), + webViewSessionStorage.get(), + specialUrlDetector.get(), + faviconManager.get(), + addToHomeCapabilityDetector.get(), + ctaViewModel.get(), + searchCountDao.get(), + pixel.get(), + dispatchers, + userEventsStore.get(), + notificationDao.get(), + useOurAppDetector.get(), + variantManager.get(), + fileDownloader.get(), + globalPrivacyControl.get(), + fireproofDialogsEventHandler.get() + ) as T else -> null } } diff --git a/app/src/main/res/layout/include_new_browser_tab.xml b/app/src/main/res/layout/include_new_browser_tab.xml index 6d0a9c9595b4..af558afd8336 100644 --- a/app/src/main/res/layout/include_new_browser_tab.xml +++ b/app/src/main/res/layout/include_new_browser_tab.xml @@ -25,6 +25,12 @@ tools:context="com.duckduckgo.app.browser.BrowserActivity" tools:showIn="@layout/fragment_browser_tab"> + + Date: Thu, 22 Apr 2021 17:32:56 +0200 Subject: [PATCH 27/95] fix: be able to drag and drop when user goes back to home screen from browsing --- .../main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 64396f2b5fe0..cd98d3ecd710 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -499,14 +499,15 @@ class BrowserTabFragment : private fun showHome() { errorSnackbar.dismiss() newTabLayout.show() + browserLayout.gone() appBarLayout.setExpanded(true) webView?.onPause() webView?.hide() - swipeRefreshContainer.isEnabled = false } private fun showBrowser() { newTabLayout.gone() + browserLayout.show() webView?.show() webView?.onResume() } From b003e635173d58bbb14683ad1ef497fe7f7587f7 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Sat, 24 Apr 2021 08:53:35 +0200 Subject: [PATCH 28/95] fix: favorites replacing cta when posting from background --- .../main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 0041dbffad54..84a46a3c9d9d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -413,7 +413,7 @@ class BrowserTabViewModel( viewModelScope.launch { favoritesRepository.favorites().collect { favorite -> Timber.i("BrowserTab favs: collect $favorite") - ctaViewState.postValue(currentCtaViewState().copy(favorites = favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) })) + ctaViewState.value = currentCtaViewState().copy(favorites = favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) }) } } } From 86ff6585e148b4b44d76716ebaf5ef69445fa4c4 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 26 Apr 2021 10:29:23 +0200 Subject: [PATCH 29/95] browseractivity quick access items interactions --- .../app/browser/BrowserTabFragment.kt | 29 ++++++++++++++++--- .../app/browser/BrowserTabViewModel.kt | 25 ++++++++++++++++ .../app/systemsearch/SystemSearchActivity.kt | 5 +++- .../app/systemsearch/SystemSearchViewModel.kt | 5 ++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index cd98d3ecd710..e0015a241110 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -113,13 +113,20 @@ import com.duckduckgo.app.widget.ui.AddWidgetInstructionsActivity import com.duckduckgo.widget.SearchWidgetLight import com.google.android.material.snackbar.Snackbar import dagger.android.support.AndroidSupportInjection +import kotlinx.android.synthetic.main.activity_system_search.* import kotlinx.android.synthetic.main.fragment_browser_tab.* +import kotlinx.android.synthetic.main.fragment_browser_tab.rootView import kotlinx.android.synthetic.main.include_cta_buttons.view.* import kotlinx.android.synthetic.main.include_dax_dialog_cta.* import kotlinx.android.synthetic.main.include_dax_dialog_cta.view.* import kotlinx.android.synthetic.main.include_find_in_page.* import kotlinx.android.synthetic.main.include_new_browser_tab.* import kotlinx.android.synthetic.main.include_omnibar_toolbar.* +import kotlinx.android.synthetic.main.include_omnibar_toolbar.appBarLayout +import kotlinx.android.synthetic.main.include_omnibar_toolbar.clearTextButton +import kotlinx.android.synthetic.main.include_omnibar_toolbar.omnibarTextInput +import kotlinx.android.synthetic.main.include_omnibar_toolbar.toolbar +import kotlinx.android.synthetic.main.include_omnibar_toolbar.toolbarContainer import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.* import kotlinx.android.synthetic.main.include_quick_access_items.* import kotlinx.android.synthetic.main.popup_window_browser_menu.view.* @@ -549,6 +556,7 @@ class BrowserTabFragment : is Command.LaunchNewTab -> browserActivity?.launchNewTab() is Command.ShowBookmarkAddedConfirmation -> bookmarkAdded(it.bookmark) is Command.ShowFavoriteAddedConfirmation -> favoriteAdded(it.favorite) + is Command.DeleteSavedSiteConfirmation -> confirmDeleteSavedSite(it.savedSite) is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.fireproofWebsiteEntity) is Command.Navigate -> { navigate(it.url, it.headers) @@ -633,6 +641,7 @@ class BrowserTabFragment : is Command.ConvertBlobToDataUri -> convertBlobToDataUri(it) is Command.RequestFileDownload -> requestFileDownload(it.url, it.contentDisposition, it.mimeType, it.requestUserConfirmation) is Command.ChildTabClosed -> processUriForThirdPartyCookies() + is Command.SubmitQuery -> submitQuery(it.url) } } @@ -913,13 +922,13 @@ class BrowserTabFragment : itemTouchHelper.startDrag(viewHolder) }, { - //viewModel.onQuickAccesItemClicked(it) + viewModel.onQuickAccesItemClicked(it.favorite) }, { - //viewModel.onEditQuickAccessItemRequested(it) + viewModel.onSavedSiteEdited(it.favorite) }, { - //confirmDeleteSavedSite(it.favorite) + viewModel.onDeleteQuickAccessItemRequested(it.favorite) } ) itemTouchHelper = ItemTouchHelper( @@ -937,7 +946,6 @@ class BrowserTabFragment : quickAccessRecyclerView.adapter = quickAccessAdapter } - private fun configurePrivacyGrade() { toolbar.privacyGradeButton.setOnClickListener { browserActivity?.launchPrivacyDashboard() @@ -1157,6 +1165,18 @@ class BrowserTabFragment : .show() } + private fun confirmDeleteSavedSite(savedSite: SavedSite) { + val message = getString(R.string.bookmarkDeleteConfirmationMessage, savedSite.title).html(requireContext()) + viewModel.deleteQuickAccessItem(savedSite) + Snackbar.make( + rootView, + message, + Snackbar.LENGTH_LONG + ).setAction(R.string.fireproofWebsiteSnackbarAction) { + viewModel.insertQuickAccessItem(savedSite) + }.show() + } + private fun fireproofWebsiteConfirmation(entity: FireproofWebsiteEntity) { Snackbar.make( rootView, @@ -1882,6 +1902,7 @@ class BrowserTabFragment : } renderIfChanged(viewState, lastSeenCtaViewState) { + Timber.i("BrowserTab favs: renderIfChanged $viewState") lastSeenCtaViewState = viewState removeNewTabLayoutClickListener() if (viewState.cta != null) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 84a46a3c9d9d..ae4ef1d70d36 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -263,11 +263,13 @@ class BrowserTabViewModel( class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() class ShowBookmarkAddedConfirmation(val bookmark: SavedSite.Bookmark) : Command() class ShowFavoriteAddedConfirmation(val favorite: SavedSite.Favorite) : Command() + class DeleteSavedSiteConfirmation(val savedSite: SavedSite) : Command() class ShowFireproofWebSiteConfirmation(val fireproofWebsiteEntity: FireproofWebsiteEntity) : Command() object AskToDisableLoginDetection : Command() class AskToFireproofWebsite(val fireproofWebsite: FireproofWebsiteEntity) : Command() class ShareLink(val url: String) : Command() class CopyLink(val url: String) : Command() + class SubmitQuery(val url: String) : Command() class FindInPageCommand(val searchTerm: String) : Command() class BrokenSiteFeedback(val data: BrokenSiteData) : Command() object DismissFindInPage : Command() @@ -1412,6 +1414,10 @@ class BrowserTabViewModel( } } + fun onDeleteQuickAccessItemRequested(savedSite: SavedSite) { + command.value = DeleteSavedSiteConfirmation(savedSite) + } + private suspend fun editBookmark(bookmark: SavedSite.Bookmark) { withContext(dispatchers.io()) { bookmarksDao.update(BookmarkEntity(bookmark.id, bookmark.title, bookmark.url)) @@ -1881,6 +1887,25 @@ class BrowserTabViewModel( } } + fun onQuickAccesItemClicked(it: SavedSite) { + command.value = SubmitQuery(it.url) + } + + fun deleteQuickAccessItem(savedSite: SavedSite) { + val favorite = savedSite as? SavedSite.Favorite ?: return + viewModelScope.launch(dispatchers.io() + NonCancellable) { + faviconManager.deletePersistedFavicon(savedSite.url) + favoritesRepository.delete(favorite) + } + } + + fun insertQuickAccessItem(savedSite: SavedSite) { + val favorite = savedSite as? SavedSite.Favorite ?: return + viewModelScope.launch(dispatchers.io()) { + favoritesRepository.insert(favorite) + } + } + companion object { private const val FIXED_PROGRESS = 50 diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index 067ddcf829b7..e8e745ea30f9 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -201,7 +201,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { viewModel.onEditQuickAccessItemRequested(it) }, { - confirmDeleteSavedSite(it.favorite) + viewModel.onDeleteQuickAccessItemRequested(it) } ) itemTouchHelper = ItemTouchHelper( @@ -317,6 +317,9 @@ class SystemSearchActivity : DuckDuckGoActivity() { is LaunchEditDialog -> { showEditSavedSiteDialog(command.savedSite) } + is DeleteSavedSiteConfirmation -> { + confirmDeleteSavedSite(command.savedSite) + } } } diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index 7fb806daabd0..9a5417bf0b6e 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -84,6 +84,7 @@ class SystemSearchViewModel( object LaunchDuckDuckGo : Command() data class LaunchBrowser(val query: String) : Command() data class LaunchEditDialog(val savedSite: SavedSite) : Command() + data class DeleteSavedSiteConfirmation(val savedSite: SavedSite) : Command() data class LaunchDeviceApplication(val deviceApp: DeviceApp) : Command() data class ShowAppNotFoundMessage(val appName: String) : Command() object DismissKeyboard : Command() @@ -295,6 +296,10 @@ class SystemSearchViewModel( command.value = Command.LaunchEditDialog(it.favorite) } + fun onDeleteQuickAccessItemRequested(it: FavoritesQuickAccessAdapter.QuickAccessFavorite) { + command.value = Command.DeleteSavedSiteConfirmation(it.favorite) + } + companion object { private const val DEBOUNCE_TIME_MS = 200L private const val RESULTS_MAX_RESULTS_PER_GROUP = 4 From 78a6250d2150decc22559e0651cf14d9d5d4860a Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 26 Apr 2021 11:40:16 +0200 Subject: [PATCH 30/95] explore ontouch callback --- .../com/duckduckgo/app/browser/BrowserChromeClient.kt | 10 ++++++++++ .../com/duckduckgo/app/browser/BrowserTabViewModel.kt | 4 ++++ .../duckduckgo/app/browser/WebViewClientListener.kt | 1 + .../duckduckgo/app/browser/favicon/FaviconPersister.kt | 2 ++ 4 files changed, 17 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt index 05a9ff96ea8d..1c16c26fad87 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt @@ -85,9 +85,19 @@ class BrowserChromeClient @Inject constructor(private val uncaughtExceptionRepos } override fun onReceivedIcon(webView: WebView, icon: Bitmap) { + Timber.i("Favicon favicon: ${webView.url}") webViewClientListener?.iconReceived(icon) } + override fun onReceivedTouchIconUrl(view: WebView?, url: String?, precomposed: Boolean) { + val visitedUrl = view?.url ?: return + val iconUrl = url ?: return + webViewClientListener?.iconReceived(visitedUrl, iconUrl) + super.onReceivedTouchIconUrl(view, url, precomposed) + } + + + override fun onReceivedTitle(view: WebView, title: String) { try { webViewClientListener?.titleReceived(title) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index ae4ef1d70d36..d19637311330 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -643,6 +643,10 @@ class BrowserTabViewModel( } } + override fun iconReceived(visitedUrl: String, iconUrl: String) { + //noop + } + override fun isDesktopSiteEnabled(): Boolean = currentBrowserViewState().isDesktopBrowsingMode override fun closeCurrentTab() { 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 92b66cf4f722..59a54df3d470 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -60,5 +60,6 @@ interface WebViewClientListener { fun loginDetected() fun dosAttackDetected() fun iconReceived(icon: Bitmap) + fun iconReceived(visitedUrl: String, iconUrl: String) fun prefetchFavicon(url: String) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt index 7a40d8cff161..a37d2d3595bb 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt @@ -24,6 +24,7 @@ import com.duckduckgo.app.global.file.FileDeleter import com.duckduckgo.app.global.sha256 import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext +import timber.log.Timber import java.io.File import java.io.FileOutputStream @@ -109,6 +110,7 @@ class FileBasedFaviconPersister( val existingFavicon = BitmapFactory.decodeFile(existingFile.absolutePath) existingFavicon?.let { + Timber.i("Favicon favicon size: ${it.width} x ${it.height}") if (it.width > bitmap.width) { return null // Stored file has better quality } From 48aa86e86ca96d0d19d2dca6b82b53e5cc8c0224 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 27 Apr 2021 13:00:23 +0200 Subject: [PATCH 31/95] (exploration) fetching ontouch favicons --- .../app/browser/BrowserChromeClient.kt | 1 + .../app/browser/BrowserTabViewModel.kt | 24 ++++++++++--- .../app/browser/WebViewClientListener.kt | 2 +- .../app/browser/favicon/FaviconManager.kt | 36 ++++++++++++++++--- .../app/browser/favicon/FaviconPersister.kt | 3 +- .../com/duckduckgo/app/global/UriExtension.kt | 9 +++++ 6 files changed, 64 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt index 93c848daa97e..e4543438ab09 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt @@ -90,6 +90,7 @@ class BrowserChromeClient @Inject constructor(private val uncaughtExceptionRepos } override fun onReceivedTouchIconUrl(view: WebView?, url: String?, precomposed: Boolean) { + Timber.i("Favicon touch icon: ${view?.url}, $url") val visitedUrl = view?.url ?: return val iconUrl = url ?: return webViewClientListener?.iconReceived(visitedUrl, iconUrl) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 07709dd699bd..4e689c8e9e04 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -634,10 +634,11 @@ class BrowserTabViewModel( } } - override fun prefetchFavicon(url: String) { - faviconPrefetchJob?.cancel() + override fun prefetchFavicon(faviconUrl: String) { + //faviconPrefetchJob?.cancel() faviconPrefetchJob = viewModelScope.launch { - val faviconFile = faviconManager.prefetchToTemp(tabId, url) + Timber.i("Favicon prefetch $faviconUrl") + val faviconFile = faviconManager.prefetchToTemp(tabId, faviconUrl) if (faviconFile != null) { tabRepository.updateTabFavicon(tabId, faviconFile.name) } @@ -662,7 +663,22 @@ class BrowserTabViewModel( } override fun iconReceived(visitedUrl: String, iconUrl: String) { - // noop + val currentTab = tabRepository.liveSelectedTab.value ?: return + val currentUrl = currentTab.url ?: return + if (currentUrl.toUri().host != visitedUrl.toUri().host) { + pixel.enqueueFire(AppPixelName.FAVICON_WRONG_URL_ERROR) + Timber.d("Favicon received for a url $visitedUrl, different than the current one $currentUrl") + return + } + + //faviconPrefetchJob?.cancel() + faviconPrefetchJob = viewModelScope.launch { + Timber.i("Favicon prefetch $iconUrl") + val faviconFile = faviconManager.prefetchToTemp(tabId, iconUrl, visitedUrl) + if (faviconFile != null) { + tabRepository.updateTabFavicon(tabId, faviconFile.name) + } + } } override fun isDesktopSiteEnabled(): Boolean = currentBrowserViewState().isDesktopBrowsingMode 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 5b6596c3e238..29aa65b3d870 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -61,5 +61,5 @@ interface WebViewClientListener { fun dosAttackDetected() fun iconReceived(url: String, icon: Bitmap) fun iconReceived(visitedUrl: String, iconUrl: String) - fun prefetchFavicon(url: String) + fun prefetchFavicon(faviconUrl: String) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt index 831d6af36b1b..680f3b35697d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt @@ -28,13 +28,15 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.domain import com.duckduckgo.app.global.faviconLocation +import com.duckduckgo.app.global.touchFaviconLocation import com.duckduckgo.app.location.data.LocationPermissionsRepository import kotlinx.coroutines.withContext +import timber.log.Timber import java.io.File interface FaviconManager { suspend fun saveToTemp(subFolder: String, icon: Bitmap, url: String): File? - suspend fun prefetchToTemp(subFolder: String, url: String): File? + suspend fun prefetchToTemp(subFolder: String, faviconUrl: String, origin: String = faviconUrl): File? suspend fun persistFavicon(subFolder: String, url: String) suspend fun loadFromTemp(subFolder: String, url: String): Bitmap? suspend fun loadToViewFromTemp(subFolder: String, url: String, view: ImageView) @@ -75,12 +77,21 @@ class DuckDuckGoFaviconManager constructor( loadToViewFromDirectory(url, FAVICON_TEMP_DIR, subFolder, view) } - override suspend fun prefetchToTemp(subFolder: String, url: String): File? { - val domain = url.toUri().domain() ?: return null - val favicon = downloadFromUrl(url) + override suspend fun prefetchToTemp(subFolder: String, faviconUrl: String, origin: String): File? { + val domain = origin.toUri().domain() ?: return null + + val favicon = if(faviconUrl == origin) { + downloadFromUrl(faviconUrl) + } else { + Timber.i("Favicon downloaded from $faviconUrl") + faviconDownloader.getFaviconFromUrl(faviconUrl.toUri()) + } + return if (favicon != null) { + Timber.i("Favicon downloaded for $domain") saveToFile(FAVICON_TEMP_DIR, subFolder, favicon, domain) } else { + Timber.i("Favicon downloaded null for $domain") null } } @@ -128,7 +139,14 @@ class DuckDuckGoFaviconManager constructor( private suspend fun downloadFromUrl(url: String): Bitmap? { val faviconUrl = getFaviconUrl(url) ?: return null - return faviconDownloader.getFaviconFromUrl(faviconUrl) + val touchFaviconUrl = getTouchFaviconUrl(url) ?: return null + faviconDownloader.getFaviconFromUrl(touchFaviconUrl)?.let { + Timber.i("Favicon downloaded from $touchFaviconUrl") + return it + } ?: faviconDownloader.getFaviconFromUrl(faviconUrl).let { + Timber.i("Favicon downloaded from $faviconUrl") + return it + } } private fun getFaviconUrl(url: String): Uri? { @@ -139,6 +157,14 @@ class DuckDuckGoFaviconManager constructor( } } + private fun getTouchFaviconUrl(url: String): Uri? { + return if (url.toUri().host.isNullOrBlank()) { + "https://$url".toUri().touchFaviconLocation() + } else { + url.toUri().touchFaviconLocation() + } + } + private suspend fun loadFaviconToView(file: File, view: ImageView) { faviconDownloader.loadFaviconToView(file, view) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt index a37d2d3595bb..5d2c519464da 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt @@ -106,7 +106,7 @@ class FileBasedFaviconPersister( val existingFile = fileForFavicon(directory, subFolder, domain) if (existingFile.exists()) { - + Timber.i("Favicon favicon exists for $domain") val existingFavicon = BitmapFactory.decodeFile(existingFile.absolutePath) existingFavicon?.let { @@ -119,6 +119,7 @@ class FileBasedFaviconPersister( val faviconFile = prepareDestinationFile(directory, subFolder, domain) writeBytesToFile(faviconFile, bitmap) + Timber.i("Favicon favicon stored") return if (faviconFile.exists()) { faviconFile diff --git a/app/src/main/java/com/duckduckgo/app/global/UriExtension.kt b/app/src/main/java/com/duckduckgo/app/global/UriExtension.kt index 233b00d681f1..b104b5bb414d 100644 --- a/app/src/main/java/com/duckduckgo/app/global/UriExtension.kt +++ b/app/src/main/java/com/duckduckgo/app/global/UriExtension.kt @@ -88,6 +88,7 @@ fun Uri.domain(): String? = this.host // to obtain a favicon for a website, we go directly to the site and look for /favicon.ico private const val faviconBaseUrlFormat = "%s://%s/favicon.ico" +private const val touchFaviconBaseUrlFormat = "%s://%s/apple-touch-icon.png" fun Uri?.faviconLocation(): Uri? { if (this == null) return null @@ -97,6 +98,14 @@ fun Uri?.faviconLocation(): Uri? { return parse(String.format(faviconBaseUrlFormat, if (isHttps) "https" else "http", host)) } +fun Uri?.touchFaviconLocation(): Uri? { + if (this == null) return null + val host = this.host + if (host.isNullOrBlank()) return null + val isHttps = this.isHttps + return parse(String.format(touchFaviconBaseUrlFormat, if (isHttps) "https" else "http", host)) +} + fun Uri.getValidUrl(): ValidUrl? { val validHost = host ?: return null val validBaseHost = baseHost ?: return null From dd8ce134df13904762ab9879b11a845a3e936c8c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 27 Apr 2021 14:52:31 +0200 Subject: [PATCH 32/95] avoid flashing when quick adapter updates handles edit functionality on browser tab persist changes in favorites position on browser tab --- .../com/duckduckgo/app/browser/BrowserTabFragment.kt | 11 +++++++++-- .../duckduckgo/app/browser/BrowserTabViewModel.kt | 12 ++++++++++++ .../browser/favorites/FavoritesQuickAccessAdapter.kt | 3 ++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index a4f798ec7b4e..66c250e51ae1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -563,6 +563,7 @@ class BrowserTabFragment : is Command.LaunchNewTab -> browserActivity?.launchNewTab() is Command.ShowBookmarkAddedConfirmation -> bookmarkAdded(it.bookmark) is Command.ShowFavoriteAddedConfirmation -> favoriteAdded(it.favorite) + is Command.ShowEditSavedSiteDialog -> editSavedSite(it.savedSite) is Command.DeleteSavedSiteConfirmation -> confirmDeleteSavedSite(it.savedSite) is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.fireproofWebsiteEntity) is Command.Navigate -> { @@ -950,7 +951,7 @@ class BrowserTabFragment : viewModel.onQuickAccesItemClicked(it.favorite) }, { - viewModel.onSavedSiteEdited(it.favorite) + viewModel.onEditSavedSiteRequested(it.favorite) }, { viewModel.onDeleteQuickAccessItemRequested(it.favorite) @@ -961,7 +962,7 @@ class BrowserTabFragment : quickAccessAdapter, object : QuickAccessDragTouchItemListener.DragDropListener { override fun onListChanged(listElements: List) { - // viewModel.onQuickAccessListChanged(listElements) + viewModel.onQuickAccessListChanged(listElements) } } ) @@ -1191,6 +1192,12 @@ class BrowserTabFragment : .show() } + private fun editSavedSite(savedSite: SavedSite) { + val addBookmarkDialog = EditBookmarkDialogFragment.instance(savedSite) + addBookmarkDialog.show(childFragmentManager, ADD_FAVORITE_FRAGMENT_TAG) + addBookmarkDialog.listener = viewModel + } + private fun confirmDeleteSavedSite(savedSite: SavedSite) { val message = getString(R.string.bookmarkDeleteConfirmationMessage, savedSite.title).html(requireContext()) viewModel.deleteQuickAccessItem(savedSite) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 4e689c8e9e04..0caabecd0300 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -267,6 +267,7 @@ class BrowserTabViewModel( class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() class ShowBookmarkAddedConfirmation(val bookmark: SavedSite.Bookmark) : Command() class ShowFavoriteAddedConfirmation(val favorite: SavedSite.Favorite) : Command() + class ShowEditSavedSiteDialog(val savedSite: SavedSite) : Command() class DeleteSavedSiteConfirmation(val savedSite: SavedSite) : Command() class ShowFireproofWebSiteConfirmation(val fireproofWebsiteEntity: FireproofWebsiteEntity) : Command() object AskToDisableLoginDetection : Command() @@ -1452,6 +1453,10 @@ class BrowserTabViewModel( } } + fun onEditSavedSiteRequested(savedSite: SavedSite) { + command.value = ShowEditSavedSiteDialog(savedSite) + } + fun onDeleteQuickAccessItemRequested(savedSite: SavedSite) { command.value = DeleteSavedSiteConfirmation(savedSite) } @@ -1974,6 +1979,13 @@ class BrowserTabViewModel( } } + fun onQuickAccessListChanged(newList: List) { + viewModelScope.launch(dispatchers.io()) { + Timber.i("Persist favorites $newList") + favoritesRepository.persistChanges(newList.map { it.favorite }) + } + } + companion object { private const val FIXED_PROGRESS = 50 diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index 0e4e748aa4c3..9b7d8909a8ec 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -119,6 +119,7 @@ class QuickAccessAdapterDiffCallback : DiffUtil.ItemCallback Date: Tue, 27 Apr 2021 23:22:12 +0200 Subject: [PATCH 33/95] Draft: replacing placeholder for custom drawable --- .../app/browser/favicon/FaviconDownloader.kt | 17 +- .../app/browser/favicon/FaviconManager.kt | 4 +- .../favorites/FavoritesQuickAccessAdapter.kt | 4 + .../app/global/view/FaviconImageView.kt | 180 ++++++++++++++++++ .../res/layout/view_quick_access_item.xml | 5 + 5 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/global/view/FaviconImageView.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt index 21158c8a5282..c1f258d50f05 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt @@ -25,6 +25,7 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.global.view.FaviconImageView import kotlinx.coroutines.withContext import java.io.File import javax.inject.Inject @@ -32,8 +33,8 @@ import javax.inject.Inject interface FaviconDownloader { suspend fun getFaviconFromDisk(file: File): Bitmap? suspend fun getFaviconFromUrl(uri: Uri): Bitmap? - suspend fun loadFaviconToView(file: File, view: ImageView) - suspend fun loadDefaultFaviconToView(view: ImageView) + suspend fun loadFaviconToView(file: File, view: ImageView, domain: String = "") + suspend fun loadDefaultFaviconToView(view: ImageView, domain: String = "") } class GlideFaviconDownloader @Inject constructor( @@ -67,22 +68,22 @@ class GlideFaviconDownloader @Inject constructor( } } - override suspend fun loadFaviconToView(file: File, view: ImageView) { + override suspend fun loadFaviconToView(file: File, view: ImageView, domain: String) { withContext(dispatcherProvider.main()) { Glide.with(context) .load(file) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) - .placeholder(R.drawable.ic_globe_gray_16dp) - .error(R.drawable.ic_globe_gray_16dp) + .placeholder(FaviconImageView.getDrawableForDomain(view.context, domain)) + .error(FaviconImageView.getDrawableForDomain(view.context, domain)) .into(view) } } - override suspend fun loadDefaultFaviconToView(view: ImageView) { + override suspend fun loadDefaultFaviconToView(view: ImageView, domain: String) { withContext(dispatcherProvider.main()) { - view.setImageDrawable(ContextCompat.getDrawable(view.context, R.drawable.ic_globe_gray_16dp)) + //view.setImageDrawable(ContextCompat.getDrawable(view.context, R.drawable.ic_globe_gray_16dp)) + view.setImageDrawable(FaviconImageView.getDrawableForDomain(view.context, domain)) } } - } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt index 680f3b35697d..08758f642525 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt @@ -67,13 +67,13 @@ class DuckDuckGoFaviconManager constructor( override suspend fun loadToViewFromPersisted(url: String, view: ImageView) { // avoid displaying the previous favicon when the holder is recycled - faviconDownloader.loadDefaultFaviconToView(view) + faviconDownloader.loadDefaultFaviconToView(view, url) loadToViewFromDirectory(url, FAVICON_PERSISTED_DIR, NO_SUBFOLDER, view) } override suspend fun loadToViewFromTemp(subFolder: String, url: String, view: ImageView) { // avoid displaying the previous favicon when the holder is recycled - faviconDownloader.loadDefaultFaviconToView(view) + faviconDownloader.loadDefaultFaviconToView(view, url) loadToViewFromDirectory(url, FAVICON_TEMP_DIR, subFolder, view) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index 9b7d8909a8ec..416a928d4b80 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser.favorites import android.os.Handler import android.view.* +import androidx.core.net.toUri import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil @@ -29,6 +30,7 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessFavorite import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessViewHolder +import com.duckduckgo.app.global.baseHost import kotlinx.android.synthetic.main.view_quick_access_item.view.* import kotlinx.coroutines.launch import timber.log.Timber @@ -66,6 +68,8 @@ class FavoritesQuickAccessAdapter( false } + itemView.quickAccessFaviconImage.name = item.favorite.url.toUri().baseHost ?: "" + itemView.quickAccessFaviconCard.setOnTouchListener { v, event -> if (event.actionMasked == MotionEvent.ACTION_MOVE) { Timber.i("QuickAccessFav: move") diff --git a/app/src/main/java/com/duckduckgo/app/global/view/FaviconImageView.kt b/app/src/main/java/com/duckduckgo/app/global/view/FaviconImageView.kt new file mode 100644 index 000000000000..37b593cc6704 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/view/FaviconImageView.kt @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2021 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.global.view + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.core.content.ContextCompat.getColor +import androidx.core.graphics.toColorInt +import androidx.core.net.toUri +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.global.baseHost +import com.google.android.a.a +import okio.ByteString.Companion.encodeUtf8 +import timber.log.Timber +import java.util.* +import kotlin.math.absoluteValue + + +class FaviconImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) { + + var name : String = "" + set(value) { + field = value + updateImageView() + } + + private val letter + get() = name.firstOrNull().toString().toUpperCase(Locale.getDefault()) + + + private fun updateImageView() { + + val drawable = object : Drawable() { + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = domainToColor(name) + } + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = getColor(context, R.color.white) + typeface = Typeface.SANS_SERIF + } + + override fun draw(canvas: Canvas) { + val centerX = bounds.width() * 0.5f + val centerY = bounds.height() * 0.5f + textPaint.textSize = (bounds.width() / 2).toFloat() + val textWidth: Float = textPaint.measureText(letter) * 0.5f + val textBaseLineHeight = textPaint.fontMetrics.ascent * -0.4f; + canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), 10f, 10f, backgroundPaint) + canvas.drawText(letter, centerX - textWidth, centerY + textBaseLineHeight, textPaint) + } + + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + } + + override fun getOpacity(): Int { + return PixelFormat.UNKNOWN + } + } + + setImageDrawable(drawable) + } + + private fun domainToColor(domain: String): Int { + return domain.encodeUtf8().toByteArray().fold(5381L) { acc, byte -> + (acc shl 5) + acc + byte.toLong() + }.absoluteValue.let { + palette[(it % palette.size).toInt()] + }.toColorInt() + } + + private val palette = listOf( + "#94B3AF", + "#727998", + "#645468", + "#4D5F7F", + "#855DB6", + "#5E5ADB", + "#678FFF", + "#6BB4EF", + "#4A9BAE", + "#66C4C6", + "#55D388", + "#99DB7A", + "#ECCC7B", + "#E7A538", + "#DD6B4C", + "#D65D62" + ) + + companion object { + fun getDrawableForDomain(context: Context, domain: String): Drawable { + val baseHost = domain.toUri().baseHost ?: throw IllegalArgumentException("domain should be a valid domain") + + return object : Drawable() { + private val palette = listOf( + "#94B3AF", + "#727998", + "#645468", + "#4D5F7F", + "#855DB6", + "#5E5ADB", + "#678FFF", + "#6BB4EF", + "#4A9BAE", + "#66C4C6", + "#55D388", + "#99DB7A", + "#ECCC7B", + "#E7A538", + "#DD6B4C", + "#D65D62" + ) + + private val letter + get() = baseHost.firstOrNull().toString().toUpperCase(Locale.getDefault()) + + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = domainToColor(baseHost) + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = getColor(context, R.color.white) + typeface = Typeface.SANS_SERIF + } + + override fun draw(canvas: Canvas) { + val centerX = bounds.width() * 0.5f + val centerY = bounds.height() * 0.5f + textPaint.textSize = (bounds.width() / 2).toFloat() + val textWidth: Float = textPaint.measureText(letter) * 0.5f + val textBaseLineHeight = textPaint.fontMetrics.ascent * -0.4f; + canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), 10f, 10f, backgroundPaint) + canvas.drawText(letter, centerX - textWidth, centerY + textBaseLineHeight, textPaint) + } + + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + } + + override fun getOpacity(): Int { + return PixelFormat.UNKNOWN + } + + private fun domainToColor(domain: String): Int { + return domain.encodeUtf8().toByteArray().fold(5381L) { acc, byte -> + (acc shl 5) + acc + byte.toLong() + }.absoluteValue.let { + val index = (it % palette.size).toInt() + palette[index] + }.toColorInt() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/view_quick_access_item.xml b/app/src/main/res/layout/view_quick_access_item.xml index 42d71a212f23..e40a3c6405a5 100644 --- a/app/src/main/res/layout/view_quick_access_item.xml +++ b/app/src/main/res/layout/view_quick_access_item.xml @@ -41,6 +41,11 @@ android:src="@drawable/ic_globe_gray_16dp" /> + + Date: Wed, 28 Apr 2021 12:56:31 +0200 Subject: [PATCH 34/95] update favicon for favorites when received a new one with more quality --- .../java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt | 4 ++-- .../duckduckgo/app/bookmarks/model/FavoritesRepository.kt | 4 ++++ .../com/duckduckgo/app/browser/BrowserChromeClient.kt | 4 ++-- .../com/duckduckgo/app/browser/BrowserTabViewModel.kt | 8 ++++---- .../com/duckduckgo/app/browser/WebViewClientListener.kt | 2 +- .../com/duckduckgo/app/browser/favicon/FaviconManager.kt | 5 ++++- .../com/duckduckgo/app/browser/favicon/FaviconModule.kt | 3 +++ .../app/browser/favorites/FavoritesQuickAccessAdapter.kt | 2 +- app/src/main/res/layout/view_quick_access_item.xml | 4 ++-- 9 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt index d8ff573ec6a8..0f194369e931 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt @@ -32,8 +32,8 @@ interface FavoritesDao { @Query("select * from favorites order by position") fun favorites(): Flow> - @Query("select count(*) from favorites WHERE url LIKE :url") - fun favoritesCountByUrl(url: String): Int + @Query("select count(*) from favorites WHERE url LIKE :domain") + fun favoritesCountByUrl(domain: String): Int @Delete fun delete(favorite: FavoriteEntity) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index 813f7b5edc82..dc59109189c8 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.map import java.io.Serializable interface FavoritesRepository { + suspend fun favoritesCountByDomain(domain: String): Int suspend fun insert(unsavedSite: SavedSite.UnsavedSite): SavedSite.Favorite suspend fun insert(favorite: SavedSite.Favorite): SavedSite.Favorite suspend fun update(favorite: SavedSite.Favorite) @@ -57,6 +58,9 @@ sealed class SavedSite( } class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : FavoritesRepository { + override suspend fun favoritesCountByDomain(domain: String): Int { + return favoritesDao.favoritesCountByUrl(domain) + } override suspend fun insert(favorite: SavedSite.UnsavedSite): SavedSite.Favorite { val lastPosition = favoritesDao.getLastPosition() ?: 0 diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt index e4543438ab09..44f0301ab67c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt @@ -85,12 +85,12 @@ class BrowserChromeClient @Inject constructor(private val uncaughtExceptionRepos } override fun onReceivedIcon(webView: WebView, icon: Bitmap) { - Timber.i("Favicon favicon: ${webView.url}") + Timber.i("Favicon bitmap received: ${webView.url}") webViewClientListener?.iconReceived(webView.url, icon) } override fun onReceivedTouchIconUrl(view: WebView?, url: String?, precomposed: Boolean) { - Timber.i("Favicon touch icon: ${view?.url}, $url") + Timber.i("Favicon touch received: ${view?.url}, $url") val visitedUrl = view?.url ?: return val iconUrl = url ?: return webViewClientListener?.iconReceived(visitedUrl, iconUrl) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 0caabecd0300..68bd6e830a4c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -635,11 +635,11 @@ class BrowserTabViewModel( } } - override fun prefetchFavicon(faviconUrl: String) { + override fun prefetchFavicon(url: String) { //faviconPrefetchJob?.cancel() faviconPrefetchJob = viewModelScope.launch { - Timber.i("Favicon prefetch $faviconUrl") - val faviconFile = faviconManager.prefetchToTemp(tabId, faviconUrl) + Timber.i("Favicon prefetch for $url") + val faviconFile = faviconManager.prefetchToTemp(subFolder = tabId, faviconUrl = url) if (faviconFile != null) { tabRepository.updateTabFavicon(tabId, faviconFile.name) } @@ -674,7 +674,7 @@ class BrowserTabViewModel( //faviconPrefetchJob?.cancel() faviconPrefetchJob = viewModelScope.launch { - Timber.i("Favicon prefetch $iconUrl") + Timber.i("Favicon prefetch touch $iconUrl") val faviconFile = faviconManager.prefetchToTemp(tabId, iconUrl, visitedUrl) if (faviconFile != null) { tabRepository.updateTabFavicon(tabId, faviconFile.name) 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 29aa65b3d870..5b6596c3e238 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -61,5 +61,5 @@ interface WebViewClientListener { fun dosAttackDetected() fun iconReceived(url: String, icon: Bitmap) fun iconReceived(visitedUrl: String, iconUrl: String) - fun prefetchFavicon(faviconUrl: String) + fun prefetchFavicon(url: String) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt index 08758f642525..e968a976e1a6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt @@ -21,6 +21,7 @@ import android.net.Uri import android.widget.ImageView import androidx.core.net.toUri import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.model.FavoritesRepository import com.duckduckgo.app.browser.favicon.FileBasedFaviconPersister.Companion.FAVICON_PERSISTED_DIR import com.duckduckgo.app.browser.favicon.FileBasedFaviconPersister.Companion.FAVICON_TEMP_DIR import com.duckduckgo.app.browser.favicon.FileBasedFaviconPersister.Companion.NO_SUBFOLDER @@ -51,6 +52,7 @@ class DuckDuckGoFaviconManager constructor( private val bookmarksDao: BookmarksDao, private val fireproofWebsiteRepository: FireproofWebsiteRepository, private val locationPermissionsRepository: LocationPermissionsRepository, + private val favoritesRepository: FavoritesRepository, private val faviconDownloader: FaviconDownloader, private val dispatcherProvider: DispatcherProvider ) : FaviconManager { @@ -199,7 +201,8 @@ class DuckDuckGoFaviconManager constructor( return withContext(dispatcherProvider.io()) { bookmarksDao.bookmarksCountByUrl(query) + locationPermissionsRepository.permissionEntitiesCountByDomain(query) + - fireproofWebsiteRepository.fireproofWebsitesCountByDomain(domain) + fireproofWebsiteRepository.fireproofWebsitesCountByDomain(domain) + + favoritesRepository.favoritesCountByDomain(query) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconModule.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconModule.kt index 3f14e993accd..fa5684716a4d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconModule.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser.favicon import android.content.Context import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.model.FavoritesRepository import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.location.data.LocationPermissionsRepository @@ -35,6 +36,7 @@ class FaviconModule { bookmarksDao: BookmarksDao, fireproofWebsiteRepository: FireproofWebsiteRepository, locationPermissionsRepository: LocationPermissionsRepository, + favoritesRepository: FavoritesRepository, faviconDownloader: FaviconDownloader, dispatcherProvider: DispatcherProvider ): FaviconManager { @@ -43,6 +45,7 @@ class FaviconModule { bookmarksDao, fireproofWebsiteRepository, locationPermissionsRepository, + favoritesRepository, faviconDownloader, dispatcherProvider ) diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index 416a928d4b80..ee9df62834e6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -68,7 +68,7 @@ class FavoritesQuickAccessAdapter( false } - itemView.quickAccessFaviconImage.name = item.favorite.url.toUri().baseHost ?: "" + //itemView.quickAccessFaviconImage.name = item.favorite.url.toUri().baseHost ?: "" itemView.quickAccessFaviconCard.setOnTouchListener { v, event -> if (event.actionMasked == MotionEvent.ACTION_MOVE) { diff --git a/app/src/main/res/layout/view_quick_access_item.xml b/app/src/main/res/layout/view_quick_access_item.xml index e40a3c6405a5..fd6620570da0 100644 --- a/app/src/main/res/layout/view_quick_access_item.xml +++ b/app/src/main/res/layout/view_quick_access_item.xml @@ -41,10 +41,10 @@ android:src="@drawable/ic_globe_gray_16dp" /> - + android:layout_height="56dp"/> --> Date: Wed, 28 Apr 2021 18:11:41 +0200 Subject: [PATCH 35/95] New placeholder for favicons implemented as extension function --- .../app/browser/favicon/FaviconDownloader.kt | 25 -- .../app/browser/favicon/FaviconManager.kt | 22 +- .../app/global/view/FaviconImageView.kt | 214 ++++++------------ 3 files changed, 84 insertions(+), 177 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt index c1f258d50f05..c0a07d5e15ab 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconDownloader.kt @@ -19,13 +19,9 @@ package com.duckduckgo.app.browser.favicon import android.content.Context import android.graphics.Bitmap import android.net.Uri -import android.widget.ImageView -import androidx.core.content.ContextCompat import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.DispatcherProvider -import com.duckduckgo.app.global.view.FaviconImageView import kotlinx.coroutines.withContext import java.io.File import javax.inject.Inject @@ -33,8 +29,6 @@ import javax.inject.Inject interface FaviconDownloader { suspend fun getFaviconFromDisk(file: File): Bitmap? suspend fun getFaviconFromUrl(uri: Uri): Bitmap? - suspend fun loadFaviconToView(file: File, view: ImageView, domain: String = "") - suspend fun loadDefaultFaviconToView(view: ImageView, domain: String = "") } class GlideFaviconDownloader @Inject constructor( @@ -67,23 +61,4 @@ class GlideFaviconDownloader @Inject constructor( }.getOrNull() } } - - override suspend fun loadFaviconToView(file: File, view: ImageView, domain: String) { - withContext(dispatcherProvider.main()) { - Glide.with(context) - .load(file) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) - .placeholder(FaviconImageView.getDrawableForDomain(view.context, domain)) - .error(FaviconImageView.getDrawableForDomain(view.context, domain)) - .into(view) - } - } - - override suspend fun loadDefaultFaviconToView(view: ImageView, domain: String) { - withContext(dispatcherProvider.main()) { - //view.setImageDrawable(ContextCompat.getDrawable(view.context, R.drawable.ic_globe_gray_16dp)) - view.setImageDrawable(FaviconImageView.getDrawableForDomain(view.context, domain)) - } - } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt index e968a976e1a6..3ea0faa7bc3e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt @@ -30,6 +30,8 @@ import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.domain import com.duckduckgo.app.global.faviconLocation import com.duckduckgo.app.global.touchFaviconLocation +import com.duckduckgo.app.global.view.loadDefaultFavicon +import com.duckduckgo.app.global.view.loadFavicon import com.duckduckgo.app.location.data.LocationPermissionsRepository import kotlinx.coroutines.withContext import timber.log.Timber @@ -69,21 +71,21 @@ class DuckDuckGoFaviconManager constructor( override suspend fun loadToViewFromPersisted(url: String, view: ImageView) { // avoid displaying the previous favicon when the holder is recycled - faviconDownloader.loadDefaultFaviconToView(view, url) + view.loadDefaultFavicon(url) loadToViewFromDirectory(url, FAVICON_PERSISTED_DIR, NO_SUBFOLDER, view) } override suspend fun loadToViewFromTemp(subFolder: String, url: String, view: ImageView) { // avoid displaying the previous favicon when the holder is recycled - faviconDownloader.loadDefaultFaviconToView(view, url) + view.loadDefaultFavicon(url) loadToViewFromDirectory(url, FAVICON_TEMP_DIR, subFolder, view) } override suspend fun prefetchToTemp(subFolder: String, faviconUrl: String, origin: String): File? { val domain = origin.toUri().domain() ?: return null - val favicon = if(faviconUrl == origin) { - downloadFromUrl(faviconUrl) + val favicon = if (faviconUrl == origin) { + downloadFromUrl(faviconUrl) } else { Timber.i("Favicon downloaded from $faviconUrl") faviconDownloader.getFaviconFromUrl(faviconUrl.toUri()) @@ -135,7 +137,7 @@ class DuckDuckGoFaviconManager constructor( val domain = extractDomain(url) val cachedFavicon = getFaviconForDomainOrFallback(directory, subFolder, domain) cachedFavicon?.let { - loadFaviconToView(cachedFavicon, view) + view.loadFavicon(cachedFavicon, url) } } @@ -167,10 +169,6 @@ class DuckDuckGoFaviconManager constructor( } } - private suspend fun loadFaviconToView(file: File, view: ImageView) { - faviconDownloader.loadFaviconToView(file, view) - } - private suspend fun replacePersistedFavicons(icon: Bitmap, domain: String) { if (persistedFaviconsForDomain(domain) > 0) { saveToFile(FAVICON_PERSISTED_DIR, NO_SUBFOLDER, icon, domain) @@ -200,9 +198,9 @@ class DuckDuckGoFaviconManager constructor( val query = "%$domain%" return withContext(dispatcherProvider.io()) { bookmarksDao.bookmarksCountByUrl(query) + - locationPermissionsRepository.permissionEntitiesCountByDomain(query) + - fireproofWebsiteRepository.fireproofWebsitesCountByDomain(domain) + - favoritesRepository.favoritesCountByDomain(query) + locationPermissionsRepository.permissionEntitiesCountByDomain(query) + + fireproofWebsiteRepository.fireproofWebsitesCountByDomain(domain) + + favoritesRepository.favoritesCountByDomain(query) } } diff --git a/app/src/main/java/com/duckduckgo/app/global/view/FaviconImageView.kt b/app/src/main/java/com/duckduckgo/app/global/view/FaviconImageView.kt index 37b593cc6704..408d0c8508ce 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/FaviconImageView.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/FaviconImageView.kt @@ -19,162 +19,96 @@ package com.duckduckgo.app.global.view import android.content.Context import android.graphics.* import android.graphics.drawable.Drawable -import android.util.AttributeSet +import android.widget.ImageView import androidx.core.content.ContextCompat.getColor import androidx.core.graphics.toColorInt import androidx.core.net.toUri +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.baseHost -import com.google.android.a.a import okio.ByteString.Companion.encodeUtf8 -import timber.log.Timber +import java.io.File import java.util.* import kotlin.math.absoluteValue - -class FaviconImageView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) { - - var name : String = "" - set(value) { - field = value - updateImageView() - } - - private val letter - get() = name.firstOrNull().toString().toUpperCase(Locale.getDefault()) - - - private fun updateImageView() { - - val drawable = object : Drawable() { - private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = domainToColor(name) - } - private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = getColor(context, R.color.white) - typeface = Typeface.SANS_SERIF - } - - override fun draw(canvas: Canvas) { - val centerX = bounds.width() * 0.5f - val centerY = bounds.height() * 0.5f - textPaint.textSize = (bounds.width() / 2).toFloat() - val textWidth: Float = textPaint.measureText(letter) * 0.5f - val textBaseLineHeight = textPaint.fontMetrics.ascent * -0.4f; - canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), 10f, 10f, backgroundPaint) - canvas.drawText(letter, centerX - textWidth, centerY + textBaseLineHeight, textPaint) - } - - override fun setAlpha(alpha: Int) { - } - - override fun setColorFilter(colorFilter: ColorFilter?) { - } - - override fun getOpacity(): Int { - return PixelFormat.UNKNOWN - } +fun ImageView.loadFavicon(file: File, domain: String) { + val defaultDrawable = generateDefaultDrawable(this.context, domain) + Glide.with(context) + .load(file) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .placeholder(defaultDrawable) + .error(defaultDrawable) + .into(this) +} + +fun ImageView.loadDefaultFavicon(domain: String) { + this.setImageDrawable(generateDefaultDrawable(this.context, domain)) +} + +private fun generateDefaultDrawable(context: Context, domain: String): Drawable { + return object : Drawable() { + private val baseHost: String = domain.toUri().baseHost ?: throw IllegalArgumentException("domain should be a valid domain") + + private val letter + get() = baseHost.firstOrNull().toString().toUpperCase(Locale.getDefault()) + + private val palette = listOf( + "#94B3AF", + "#727998", + "#645468", + "#4D5F7F", + "#855DB6", + "#5E5ADB", + "#678FFF", + "#6BB4EF", + "#4A9BAE", + "#66C4C6", + "#55D388", + "#99DB7A", + "#ECCC7B", + "#E7A538", + "#DD6B4C", + "#D65D62" + ) + + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = domainToColor(baseHost) } - setImageDrawable(drawable) - } - - private fun domainToColor(domain: String): Int { - return domain.encodeUtf8().toByteArray().fold(5381L) { acc, byte -> - (acc shl 5) + acc + byte.toLong() - }.absoluteValue.let { - palette[(it % palette.size).toInt()] - }.toColorInt() - } - - private val palette = listOf( - "#94B3AF", - "#727998", - "#645468", - "#4D5F7F", - "#855DB6", - "#5E5ADB", - "#678FFF", - "#6BB4EF", - "#4A9BAE", - "#66C4C6", - "#55D388", - "#99DB7A", - "#ECCC7B", - "#E7A538", - "#DD6B4C", - "#D65D62" - ) - - companion object { - fun getDrawableForDomain(context: Context, domain: String): Drawable { - val baseHost = domain.toUri().baseHost ?: throw IllegalArgumentException("domain should be a valid domain") - - return object : Drawable() { - private val palette = listOf( - "#94B3AF", - "#727998", - "#645468", - "#4D5F7F", - "#855DB6", - "#5E5ADB", - "#678FFF", - "#6BB4EF", - "#4A9BAE", - "#66C4C6", - "#55D388", - "#99DB7A", - "#ECCC7B", - "#E7A538", - "#DD6B4C", - "#D65D62" - ) - - private val letter - get() = baseHost.firstOrNull().toString().toUpperCase(Locale.getDefault()) - - private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = domainToColor(baseHost) - } - - private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = getColor(context, R.color.white) - typeface = Typeface.SANS_SERIF - } + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = getColor(context, R.color.white) + typeface = Typeface.SANS_SERIF + } - override fun draw(canvas: Canvas) { - val centerX = bounds.width() * 0.5f - val centerY = bounds.height() * 0.5f - textPaint.textSize = (bounds.width() / 2).toFloat() - val textWidth: Float = textPaint.measureText(letter) * 0.5f - val textBaseLineHeight = textPaint.fontMetrics.ascent * -0.4f; - canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), 10f, 10f, backgroundPaint) - canvas.drawText(letter, centerX - textWidth, centerY + textBaseLineHeight, textPaint) - } + override fun draw(canvas: Canvas) { + val centerX = bounds.width() * 0.5f + val centerY = bounds.height() * 0.5f + textPaint.textSize = (bounds.width() / 2).toFloat() + val textWidth: Float = textPaint.measureText(letter) * 0.5f + val textBaseLineHeight = textPaint.fontMetrics.ascent * -0.4f; + canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), 10f, 10f, backgroundPaint) + canvas.drawText(letter, centerX - textWidth, centerY + textBaseLineHeight, textPaint) + } - override fun setAlpha(alpha: Int) { - } + override fun setAlpha(alpha: Int) { + } - override fun setColorFilter(colorFilter: ColorFilter?) { - } + override fun setColorFilter(colorFilter: ColorFilter?) { + } - override fun getOpacity(): Int { - return PixelFormat.UNKNOWN - } + override fun getOpacity(): Int { + return PixelFormat.UNKNOWN + } - private fun domainToColor(domain: String): Int { - return domain.encodeUtf8().toByteArray().fold(5381L) { acc, byte -> - (acc shl 5) + acc + byte.toLong() - }.absoluteValue.let { - val index = (it % palette.size).toInt() - palette[index] - }.toColorInt() - } - } + private fun domainToColor(domain: String): Int { + return domain.encodeUtf8().toByteArray().fold(5381L) { acc, byte -> + (acc shl 5) + acc + byte.toLong() + }.absoluteValue.let { + val index = (it % palette.size).toInt() + palette[index] + }.toColorInt() } } } \ No newline at end of file From e813d74cacf218a1f6558bc2154a363b1a84c069 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 28 Apr 2021 23:12:47 +0200 Subject: [PATCH 36/95] calculate grid view items and margins so they appear centered --- .../app/browser/BrowserTabFragment.kt | 20 +++++++++++++++++-- .../favorites/FavoritesQuickAccessAdapter.kt | 4 ++++ .../app/systemsearch/SystemSearchActivity.kt | 19 +++++++++++++----- .../app/tabs/ui/GridViewColumnCalculator.kt | 14 +++++++++++++ .../res/layout/include_quick_access_items.xml | 7 +------ .../res/layout/view_quick_access_item.xml | 5 ----- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 66c250e51ae1..51511626207a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -76,6 +76,7 @@ import com.duckduckgo.app.browser.downloader.FileDownloader import com.duckduckgo.app.browser.downloader.FileDownloader.PendingFileDownload import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.Companion.QUICK_ACCESS_ITEM_MAX_SIZE_DP import com.duckduckgo.app.browser.favorites.QuickAccessDragTouchItemListener import com.duckduckgo.app.browser.filechooser.FileChooserIntentBuilder import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore @@ -109,7 +110,9 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.FIRE_BUTTON_STATE import com.duckduckgo.app.survey.model.Survey import com.duckduckgo.app.survey.ui.SurveyActivity +import com.duckduckgo.app.systemsearch.SystemSearchActivity import com.duckduckgo.app.tabs.model.TabEntity +import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator import com.duckduckgo.app.tabs.ui.TabSwitcherActivity import com.duckduckgo.app.widget.ui.AddWidgetInstructionsActivity import com.duckduckgo.widget.SearchWidgetLight @@ -223,6 +226,9 @@ class BrowserTabFragment : @Inject lateinit var faviconManager: FaviconManager + @Inject + lateinit var gridViewColumnCalculator: GridViewColumnCalculator + var messageFromPreviousTab: Message? = null private val initialUrl get() = requireArguments().getString(URL_EXTRA_ARG) @@ -940,8 +946,7 @@ class BrowserTabFragment : } private fun configureQuickAccessGrid() { - val layoutManager = GridLayoutManager(requireContext(), 4) - quickAccessRecyclerView.layoutManager = layoutManager + configureQuickAccessGridLayout() quickAccessAdapter = FavoritesQuickAccessAdapter( this, faviconManager, { viewHolder -> @@ -972,6 +977,14 @@ class BrowserTabFragment : quickAccessRecyclerView.adapter = quickAccessAdapter } + private fun configureQuickAccessGridLayout() { + val numOfColumns = gridViewColumnCalculator.calculateNumberOfColumns(QUICK_ACCESS_ITEM_MAX_SIZE_DP, QUICK_ACCESS_GRID_MAX_COLUMNS) + val layoutManager = GridLayoutManager(requireContext(), numOfColumns) + quickAccessRecyclerView.layoutManager = layoutManager + val sidePadding = gridViewColumnCalculator.calculateSidePadding(QUICK_ACCESS_ITEM_MAX_SIZE_DP, numOfColumns) + quickAccessRecyclerView.setPadding(sidePadding, 8.toPx(), sidePadding, 8.toPx()) + } + private fun configurePrivacyGrade() { toolbar.privacyGradeButton.setOnClickListener { browserActivity?.launchPrivacyDashboard() @@ -1310,6 +1323,7 @@ class BrowserTabFragment : if (ctaContainer.isNotEmpty()) { renderer.renderHomeCta() } + configureQuickAccessGridLayout() } fun onBackPressed(): Boolean { @@ -1545,6 +1559,8 @@ class BrowserTabFragment : private const val DEFAULT_CIRCLE_TARGET_TIMES_1_5 = 96 + private const val QUICK_ACCESS_GRID_MAX_COLUMNS = 6 + fun newInstance(tabId: String, query: String? = null, skipHome: Boolean): BrowserTabFragment { val fragment = BrowserTabFragment() val args = Bundle() diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index ee9df62834e6..b94aa451e719 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -44,6 +44,10 @@ class FavoritesQuickAccessAdapter( private val onDeleteClicked: (QuickAccessFavorite) -> Unit ) : ListAdapter(QuickAccessAdapterDiffCallback()) { + companion object { + const val QUICK_ACCESS_ITEM_MAX_SIZE_DP = 100 + } + data class QuickAccessFavorite(val favorite: SavedSite.Favorite) : FavoritesAdapter.FavoriteItemTypes class QuickAccessViewHolder( diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index e8e745ea30f9..ab9a5e39e9a6 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.systemsearch import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent +import android.graphics.Rect import android.os.Bundle import android.text.Editable import android.view.KeyEvent @@ -32,6 +33,7 @@ import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment import com.duckduckgo.app.browser.BrowserActivity @@ -39,16 +41,16 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.Companion.QUICK_ACCESS_ITEM_MAX_SIZE_DP import com.duckduckgo.app.browser.favorites.QuickAccessDragTouchItemListener import com.duckduckgo.app.browser.omnibar.OmnibarScrolling import com.duckduckgo.app.fire.DataClearerForegroundAppRestartPixel import com.duckduckgo.app.global.DuckDuckGoActivity -import com.duckduckgo.app.global.view.TextChangedWatcher -import com.duckduckgo.app.global.view.hideKeyboard -import com.duckduckgo.app.global.view.html +import com.duckduckgo.app.global.view.* import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.* +import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_bookmarks.* import kotlinx.android.synthetic.main.activity_system_search.* @@ -65,6 +67,7 @@ import kotlinx.android.synthetic.main.include_quick_access_items.* import kotlinx.android.synthetic.main.include_system_search_onboarding.* import timber.log.Timber import javax.inject.Inject +import kotlin.math.min class SystemSearchActivity : DuckDuckGoActivity() { @@ -80,6 +83,9 @@ class SystemSearchActivity : DuckDuckGoActivity() { @Inject lateinit var faviconManager: FaviconManager + @Inject + lateinit var gridViewColumnCalculator: GridViewColumnCalculator + private val viewModel: SystemSearchViewModel by bindViewModel() private lateinit var autocompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter private lateinit var deviceAppSuggestionsAdapter: DeviceAppSuggestionsAdapter @@ -187,7 +193,8 @@ class SystemSearchActivity : DuckDuckGoActivity() { } private fun configureQuickAccessGrid() { - val layoutManager = GridLayoutManager(this, 4) + val numOfColumns = gridViewColumnCalculator.calculateNumberOfColumns(QUICK_ACCESS_ITEM_MAX_SIZE_DP, QUICK_ACCESS_GRID_MAX_COLUMNS) + val layoutManager = GridLayoutManager(this, numOfColumns) quickAccessRecyclerView.layoutManager = layoutManager quickAccessAdapter = FavoritesQuickAccessAdapter( this, faviconManager, @@ -217,6 +224,8 @@ class SystemSearchActivity : DuckDuckGoActivity() { itemTouchHelper.attachToRecyclerView(quickAccessRecyclerView) quickAccessRecyclerView.adapter = quickAccessAdapter + val sidePadding = gridViewColumnCalculator.calculateSidePadding(QUICK_ACCESS_ITEM_MAX_SIZE_DP, numOfColumns) + quickAccessRecyclerView.setPadding(sidePadding, 8.toPx(), sidePadding, 8.toPx()) } private fun configureDaxButton() { @@ -381,10 +390,10 @@ class SystemSearchActivity : DuckDuckGoActivity() { } companion object { - const val NOTIFICATION_SEARCH_EXTRA = "NOTIFICATION_SEARCH_EXTRA" const val WIDGET_SEARCH_EXTRA = "WIDGET_SEARCH_EXTRA" const val NEW_SEARCH_ACTION = "com.duckduckgo.mobile.android.NEW_SEARCH" + private const val QUICK_ACCESS_GRID_MAX_COLUMNS = 6 fun fromWidget(context: Context): Intent { val intent = Intent(context, SystemSearchActivity::class.java) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/GridViewColumnCalculator.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/GridViewColumnCalculator.kt index 1d37a74825e5..949de775db26 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/GridViewColumnCalculator.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/GridViewColumnCalculator.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.tabs.ui import android.content.Context import com.duckduckgo.app.global.view.toDp +import com.duckduckgo.app.global.view.toPx import kotlin.math.min class GridViewColumnCalculator(val context: Context) { @@ -29,4 +30,17 @@ class GridViewColumnCalculator(val context: Context) { return min(maxColumns, numberOfColumns) } + /** + * Given a numOfColumns and their width, calculate sides padding to center all items on the GridView. + * RecyclerView should have a match_parent width to avoid clipping items if drag-drop enabled. + * + * @return start/end padding in pixels + */ + fun calculateSidePadding(columnWidthDp: Int, numOfColumns: Int): Int { + val displayMetrics = context.resources.displayMetrics + val screenWidthDp = displayMetrics.widthPixels.toDp() + val columnsWidth = columnWidthDp * numOfColumns + val remainingSpace = screenWidthDp - columnsWidth + return if (remainingSpace <= 0) 0 else (remainingSpace / 2).toPx() + } } diff --git a/app/src/main/res/layout/include_quick_access_items.xml b/app/src/main/res/layout/include_quick_access_items.xml index 4ff6cb3752ac..f784fba07618 100644 --- a/app/src/main/res/layout/include_quick_access_items.xml +++ b/app/src/main/res/layout/include_quick_access_items.xml @@ -19,19 +19,14 @@ + xmlns:tools="http://schemas.android.com/tools"> - - Date: Wed, 28 Apr 2021 23:13:25 +0200 Subject: [PATCH 37/95] apply codestyle --- .../java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 1 - .../java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 4 ++-- .../com/duckduckgo/app/browser/favicon/FaviconManager.kt | 6 +++--- .../app/browser/favorites/FavoritesQuickAccessAdapter.kt | 6 ++---- .../java/com/duckduckgo/app/global/view/FaviconImageView.kt | 4 ++-- .../com/duckduckgo/app/systemsearch/SystemSearchActivity.kt | 3 --- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 51511626207a..141aee410e90 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -110,7 +110,6 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.FIRE_BUTTON_STATE import com.duckduckgo.app.survey.model.Survey import com.duckduckgo.app.survey.ui.SurveyActivity -import com.duckduckgo.app.systemsearch.SystemSearchActivity import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator import com.duckduckgo.app.tabs.ui.TabSwitcherActivity diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 68bd6e830a4c..9cd7390923d3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -636,7 +636,7 @@ class BrowserTabViewModel( } override fun prefetchFavicon(url: String) { - //faviconPrefetchJob?.cancel() + // faviconPrefetchJob?.cancel() faviconPrefetchJob = viewModelScope.launch { Timber.i("Favicon prefetch for $url") val faviconFile = faviconManager.prefetchToTemp(subFolder = tabId, faviconUrl = url) @@ -672,7 +672,7 @@ class BrowserTabViewModel( return } - //faviconPrefetchJob?.cancel() + // faviconPrefetchJob?.cancel() faviconPrefetchJob = viewModelScope.launch { Timber.i("Favicon prefetch touch $iconUrl") val faviconFile = faviconManager.prefetchToTemp(tabId, iconUrl, visitedUrl) diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt index 3ea0faa7bc3e..3ae52f2fcc0f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt @@ -198,9 +198,9 @@ class DuckDuckGoFaviconManager constructor( val query = "%$domain%" return withContext(dispatcherProvider.io()) { bookmarksDao.bookmarksCountByUrl(query) + - locationPermissionsRepository.permissionEntitiesCountByDomain(query) + - fireproofWebsiteRepository.fireproofWebsitesCountByDomain(domain) + - favoritesRepository.favoritesCountByDomain(query) + locationPermissionsRepository.permissionEntitiesCountByDomain(query) + + fireproofWebsiteRepository.fireproofWebsitesCountByDomain(domain) + + favoritesRepository.favoritesCountByDomain(query) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index b94aa451e719..5d5080aa8dd7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.browser.favorites import android.os.Handler import android.view.* -import androidx.core.net.toUri import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil @@ -30,7 +29,6 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessFavorite import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessViewHolder -import com.duckduckgo.app.global.baseHost import kotlinx.android.synthetic.main.view_quick_access_item.view.* import kotlinx.coroutines.launch import timber.log.Timber @@ -72,7 +70,7 @@ class FavoritesQuickAccessAdapter( false } - //itemView.quickAccessFaviconImage.name = item.favorite.url.toUri().baseHost ?: "" + // itemView.quickAccessFaviconImage.name = item.favorite.url.toUri().baseHost ?: "" itemView.quickAccessFaviconCard.setOnTouchListener { v, event -> if (event.actionMasked == MotionEvent.ACTION_MOVE) { @@ -128,6 +126,6 @@ class QuickAccessAdapterDiffCallback : DiffUtil.ItemCallback Date: Thu, 29 Apr 2021 00:29:49 +0200 Subject: [PATCH 38/95] integrate favorites in autocomplete --- .../app/autocomplete/api/AutoComplete.kt | 61 ++++++++++++++----- .../bookmarks/model/FavoritesRepository.kt | 9 ++- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt index f38c7bcd0989..da907486c761 100644 --- a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt +++ b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt @@ -22,6 +22,8 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.A import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.global.UriString import com.duckduckgo.app.global.baseHost import com.duckduckgo.app.global.toStringDropScheme @@ -48,7 +50,8 @@ interface AutoComplete { class AutoCompleteApi @Inject constructor( private val autoCompleteService: AutoCompleteService, - private val bookmarksDao: BookmarksDao + private val bookmarksDao: BookmarksDao, + private val favoritesRepository: FavoritesRepository ) : AutoComplete { override fun autoComplete(query: String): Observable { @@ -57,7 +60,15 @@ class AutoCompleteApi @Inject constructor( return Observable.just(AutoCompleteResult(query = query, suggestions = emptyList())) } - return getAutoCompleteBookmarkResults(query).zipWith( + val savedSitesObservable = getAutoCompleteBookmarkResults(query) + .zipWith( + getAutoCompleteFavoritesResults(query), + { bookmarks, favorites -> + (favorites + bookmarks).take(2) + } + ) + + return savedSitesObservable.zipWith( getAutoCompleteSearchResults(query), BiFunction { bookmarksResults, searchResults -> AutoCompleteResult( @@ -91,21 +102,43 @@ class AutoCompleteApi @Inject constructor( .onErrorReturn { emptyList() } .toObservable() - private fun rankBookmarks(query: String, bookmarks: List): List { + private fun getAutoCompleteFavoritesResults(query: String) = + favoritesRepository.favoritesObservable() + .map { rankFavorites(query, it) } + .flattenAsObservable { it } + .map { + AutoCompleteBookmarkSuggestion(phrase = it.url.toUri().toStringDropScheme(), title = it.title.orEmpty(), url = it.url) + } + .distinctUntilChanged() + .take(2) + .toList() + .onErrorReturn { emptyList() } + .toObservable() + + private fun rankBookmarks(query: String, bookmarks: List): List { return bookmarks.asSequence() - .map { RankedBookmark(bookmarkEntity = it) } + .map { SavedSite.Bookmark(it.id, it.title ?: "", it.url) } + .sortByRank(query) + } + + private fun rankFavorites(query: String, favorites: List): List { + return favorites.asSequence().sortByRank(query) + } + + private fun Sequence.sortByRank(query: String): List { + return this.map { RankedBookmark(savedSite = it) } .map { scoreTitle(it, query) } .map { scoreTokens(it, query) } .filter { it.score >= 0 } .sortedByDescending { it.score } - .map { it.bookmarkEntity } + .map { it.savedSite } .toList() } private fun scoreTitle(rankedBookmark: RankedBookmark, query: String): RankedBookmark { - if (rankedBookmark.bookmarkEntity.title?.startsWith(query, ignoreCase = true) == true) { + if (rankedBookmark.savedSite.title.startsWith(query, ignoreCase = true)) { rankedBookmark.score += 200 - } else if (rankedBookmark.bookmarkEntity.title?.contains(" $query", ignoreCase = true) == true) { + } else if (rankedBookmark.savedSite.title.contains(" $query", ignoreCase = true)) { rankedBookmark.score += 100 } @@ -113,13 +146,13 @@ class AutoCompleteApi @Inject constructor( } private fun scoreTokens(rankedBookmark: RankedBookmark, query: String): RankedBookmark { - val bookmark = rankedBookmark.bookmarkEntity - val domain = bookmark.url.toUri().baseHost + val savedSite = rankedBookmark.savedSite + val domain = savedSite.url.toUri().baseHost val tokens = query.split(" ") if (tokens.size > 1) { tokens.forEach { token -> - if (bookmark.title?.startsWith(token, ignoreCase = true) == false && - bookmark.title?.contains(" $token", ignoreCase = true) == false && + if (!savedSite.title.startsWith(token, ignoreCase = true) && + !savedSite.title.contains(" $token", ignoreCase = true) && domain?.startsWith(token, ignoreCase = true) == false ) { return rankedBookmark @@ -130,11 +163,11 @@ class AutoCompleteApi @Inject constructor( if (domain?.startsWith(tokens.first(), ignoreCase = true) == true) { rankedBookmark.score += 300 - } else if (bookmark.title?.startsWith(tokens.first(), ignoreCase = true) == true) { + } else if (savedSite.title.startsWith(tokens.first(), ignoreCase = true)) { rankedBookmark.score += 50 } - } else if (bookmark.url.redactSchemeAndWwwSubDomain().startsWith(tokens.first().trimEnd { it == '/' }, ignoreCase = true)) { + } else if (savedSite.url.redactSchemeAndWwwSubDomain().startsWith(tokens.first().trimEnd { it == '/' }, ignoreCase = true)) { rankedBookmark.score += 300 } @@ -146,7 +179,7 @@ class AutoCompleteApi @Inject constructor( } private data class RankedBookmark( - val bookmarkEntity: BookmarkEntity, + val savedSite: SavedSite, var score: Int = BOOKMARK_SCORE ) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index dc59109189c8..5318a51ae661 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.bookmarks.model import com.duckduckgo.app.bookmarks.db.FavoriteEntity import com.duckduckgo.app.bookmarks.db.FavoritesDao +import io.reactivex.Single import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -25,6 +26,7 @@ import java.io.Serializable interface FavoritesRepository { suspend fun favoritesCountByDomain(domain: String): Int + fun favoritesObservable(): Single> suspend fun insert(unsavedSite: SavedSite.UnsavedSite): SavedSite.Favorite suspend fun insert(favorite: SavedSite.Favorite): SavedSite.Favorite suspend fun update(favorite: SavedSite.Favorite) @@ -62,6 +64,9 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite return favoritesDao.favoritesCountByUrl(domain) } + override fun favoritesObservable() = + favoritesDao.favoritesObservable().map { favorites -> favorites.mapToSavedSites() } + override suspend fun insert(favorite: SavedSite.UnsavedSite): SavedSite.Favorite { val lastPosition = favoritesDao.getLastPosition() ?: 0 val favoriteEntity = FavoriteEntity(title = favorite.title, url = favorite.url, position = lastPosition + 1) @@ -84,10 +89,12 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite } override suspend fun favorites(): Flow> { - return favoritesDao.favorites().distinctUntilChanged().map { favorites -> favorites.map { SavedSite.Favorite(it.id, it.title, it.url, it.position) } } + return favoritesDao.favorites().distinctUntilChanged().map { favorites -> favorites.mapToSavedSites() } } override suspend fun delete(favorite: SavedSite.Favorite) { favoritesDao.delete(FavoriteEntity(favorite.id, favorite.title, favorite.url, favorite.position)) } + + private fun List.mapToSavedSites(): List = this.map { SavedSite.Favorite(it.id, it.title, it.url, it.position) } } From 6156b4b818d9cbdbc0dcdc74b48b2a8966f91d90 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 29 Apr 2021 11:55:24 +0200 Subject: [PATCH 39/95] pending changes --- .../com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt | 1 + .../java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index 5318a51ae661..5adb68432c3a 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -53,6 +53,7 @@ sealed class SavedSite( override val url: String ) : SavedSite(id, title, url) + // TODO: review this data class data class UnsavedSite( override val title: String, override val url: String 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 index cfdca6b3081e..9d1e88aa459c 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -156,6 +156,7 @@ class BookmarksViewModel( when (savedSite) { is Bookmark -> { viewModelScope.launch(dispatcherProvider.io() + NonCancellable) { + // TODO: don't delete favicon for urls that can be a fireproof / favorites / location faviconManager.deletePersistedFavicon(savedSite.url) dao.delete(BookmarkEntity(savedSite.id, savedSite.title, savedSite.url)) } From 6aa48eaa49cdf6041407262ffe3112ff9ca01f3b Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 29 Apr 2021 12:15:06 +0200 Subject: [PATCH 40/95] new 33.json after merging develop --- .../33.json | 904 ++++++++++++++++++ 1 file changed, 904 insertions(+) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/33.json diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/33.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/33.json new file mode 100644 index 000000000000..4c8e00921ce8 --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/33.json @@ -0,0 +1,904 @@ +{ + "formatVersion": 1, + "database": { + "version": 33, + "identityHash": "510e71539a55991713303ff3c2c46c09", + "entities": [ + { + "tableName": "tds_tracker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `defaultAction` TEXT NOT NULL, `ownerName` TEXT NOT NULL, `categories` TEXT NOT NULL, `rules` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultAction", + "columnName": "defaultAction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerName", + "columnName": "ownerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `displayName` TEXT NOT NULL, `prevalence` REAL NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevalence", + "columnName": "prevalence", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_domain_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `entityName` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entityName", + "columnName": "entityName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporary_tracking_whitelist", + "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": "user_whitelist", + "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": "https_bloom_filter_spec", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `bitCount` INTEGER NOT NULL, `errorRate` REAL NOT NULL, `totalEntries` INTEGER NOT NULL, `sha256` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bitCount", + "columnName": "bitCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "errorRate", + "columnName": "errorRate", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalEntries", + "columnName": "totalEntries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_false_positive_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": "network_leaderboard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`networkName`))", + "fields": [ + { + "fieldPath": "networkName", + "columnName": "networkName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "networkName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sites_visited", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tabId` TEXT NOT NULL, `url` TEXT, `title` TEXT, `skipHome` INTEGER NOT NULL, `viewed` INTEGER NOT NULL, `position` INTEGER NOT NULL, `tabPreviewFile` TEXT, `sourceTabId` TEXT, `deletable` INTEGER NOT NULL, PRIMARY KEY(`tabId`), FOREIGN KEY(`sourceTabId`) REFERENCES `tabs`(`tabId`) ON UPDATE SET NULL ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "skipHome", + "columnName": "skipHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewed", + "columnName": "viewed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabPreviewFile", + "columnName": "tabPreviewFile", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceTabId", + "columnName": "sourceTabId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deletable", + "columnName": "deletable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "SET NULL", + "columns": [ + "sourceTabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "tableName": "tab_selection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tabId` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`tabId`) REFERENCES `tabs`(`tabId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tab_selection_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tab_selection_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "tabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "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": [] + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "survey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`surveyId` TEXT NOT NULL, `url` TEXT, `daysInstalled` INTEGER, `status` TEXT NOT NULL, PRIMARY KEY(`surveyId`))", + "fields": [ + { + "fieldPath": "surveyId", + "columnName": "surveyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "daysInstalled", + "columnName": "daysInstalled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "surveyId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dismissed_cta", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ctaId` TEXT NOT NULL, PRIMARY KEY(`ctaId`))", + "fields": [ + { + "fieldPath": "ctaId", + "columnName": "ctaId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "ctaId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "search_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_days_used", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, PRIMARY KEY(`date`))", + "fields": [ + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_enjoyment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventType` INTEGER NOT NULL, `promptCount` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `primaryKey` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "eventType", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "promptCount", + "columnName": "promptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryKey", + "columnName": "primaryKey", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "primaryKey" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, PRIMARY KEY(`notificationId`))", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "notificationId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "privacy_protection_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `blocked_tracker_count` INTEGER NOT NULL, `upgrade_count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedTrackerCount", + "columnName": "blocked_tracker_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "upgradeCount", + "columnName": "upgrade_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UncaughtExceptionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exceptionSource` TEXT NOT NULL, `message` TEXT NOT NULL, `version` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exceptionSource", + "columnName": "exceptionSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tdsMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `eTag` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userStage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` INTEGER NOT NULL, `appStage` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appStage", + "columnName": "appStage", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "fireproofWebsites", + "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": "user_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "locationPermissions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `permission` INTEGER NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permission", + "columnName": "permission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "pixel_store", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pixelName` TEXT NOT NULL, `atb` TEXT NOT NULL, `additionalQueryParams` TEXT NOT NULL, `encodedQueryParams` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pixelName", + "columnName": "pixelName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atb", + "columnName": "atb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalQueryParams", + "columnName": "additionalQueryParams", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encodedQueryParams", + "columnName": "encodedQueryParams", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "auth_cookies_allowed_domains", + "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": [] + } + ], + "views": [], + "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, '510e71539a55991713303ff3c2c46c09')" + ] + } +} \ No newline at end of file From cd50fac2def219b1e4d86c6427ef7a3523813169 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 29 Apr 2021 15:04:43 +0200 Subject: [PATCH 41/95] show divider between favorites and bookmarks --- .../app/bookmarks/ui/BookmarksActivity.kt | 5 ++- .../app/global/view/DividerAdapter.kt | 38 +++++++++++++++++++ app/src/main/res/layout/view_item_divider.xml | 17 +++++++++ app/src/main/res/values/styles.xml | 8 ++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/global/view/DividerAdapter.kt create mode 100644 app/src/main/res/layout/view_item_divider.xml 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 index afc99174acf9..42274003d796 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -31,6 +31,7 @@ import com.duckduckgo.app.browser.R.id.action_search import com.duckduckgo.app.browser.R.menu.bookmark_activity_menu import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.global.DuckDuckGoActivity +import com.duckduckgo.app.global.view.DividerAdapter import com.duckduckgo.app.global.view.html import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_bookmarks.* @@ -60,8 +61,8 @@ class BookmarksActivity : DuckDuckGoActivity() { private fun setupBookmarksRecycler() { bookmarksAdapter = BookmarksAdapter(layoutInflater, viewModel, this, faviconManager) favoritesAdapter = FavoritesAdapter(layoutInflater, viewModel, this, faviconManager) - recycler.adapter = ConcatAdapter(favoritesAdapter, bookmarksAdapter) - recycler.setItemAnimator(null) + recycler.adapter = ConcatAdapter(favoritesAdapter, DividerAdapter(), bookmarksAdapter) + recycler.itemAnimator = null } private fun observeViewModel() { diff --git a/app/src/main/java/com/duckduckgo/app/global/view/DividerAdapter.kt b/app/src/main/java/com/duckduckgo/app/global/view/DividerAdapter.kt new file mode 100644 index 000000000000..a10aee801ad6 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/view/DividerAdapter.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 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.global.view + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.duckduckgo.app.browser.R + +class DividerAdapter : RecyclerView.Adapter() { + + class DividerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DividerViewHolder { + return DividerViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_item_divider, parent, false)) + } + + override fun onBindViewHolder(holder: DividerViewHolder, position: Int) { + // noop + } + + override fun getItemCount(): Int = 1 +} diff --git a/app/src/main/res/layout/view_item_divider.xml b/app/src/main/res/layout/view_item_divider.xml new file mode 100644 index 000000000000..ba737b054139 --- /dev/null +++ b/app/src/main/res/layout/view_item_divider.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index cc18525a511f..9ce851c7110f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -343,6 +343,14 @@ start + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index cc4bde74d2ec..85b75e5b295f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -65,9 +65,9 @@ @color/white @color/almostBlack @color/white - @color/white + @color/white @color/white - @color/grayishTwo + @color/grayishTwo @color/white @color/warGreyTwo @color/grayishTwo @@ -141,11 +141,11 @@ @color/white @color/almostBlackDark @color/almostBlackDark - @color/almostBlackDark + @color/almostBlackDark @color/almostBlack @color/whiteSix @color/almostBlack - @color/warmerGray + @color/warmerGray @color/almostBlackDark @color/pinkish_grey_two @color/warmerGray From 6cafd308143805f50689422c6eb49894e9b797a9 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 13 May 2021 16:18:38 +0200 Subject: [PATCH 65/95] fix: top margin for quick access grid --- app/src/main/res/layout/include_quick_access_items.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/include_quick_access_items.xml b/app/src/main/res/layout/include_quick_access_items.xml index f784fba07618..34b4ce3923a8 100644 --- a/app/src/main/res/layout/include_quick_access_items.xml +++ b/app/src/main/res/layout/include_quick_access_items.xml @@ -27,6 +27,7 @@ android:layout_height="match_parent" android:clipToPadding="false" android:overScrollMode="never" + android:paddingTop="10dp" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:itemCount="8" tools:listItem="@layout/view_quick_access_item" From 1e4532fc4df21219a321b5afbc15dd53f289fbb5 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 13 May 2021 16:18:49 +0200 Subject: [PATCH 66/95] fix: typo on copy --- 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 b825a1fdf1ca..bdf2739070cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -205,7 +205,7 @@ Bookmarks Are you sure you want to delete bookmark <b>%s</b>? Bookmark added - title + Title URL Edit No bookmarks added yet From 85cb7349c324ad3928c956fdea808b5788a8526c Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Thu, 13 May 2021 16:26:49 +0200 Subject: [PATCH 67/95] fix top padding on quick access grid --- app/src/main/res/layout/include_quick_access_items.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/include_quick_access_items.xml b/app/src/main/res/layout/include_quick_access_items.xml index 34b4ce3923a8..824a157bd8ee 100644 --- a/app/src/main/res/layout/include_quick_access_items.xml +++ b/app/src/main/res/layout/include_quick_access_items.xml @@ -27,7 +27,7 @@ android:layout_height="match_parent" android:clipToPadding="false" android:overScrollMode="never" - android:paddingTop="10dp" + android:paddingTop="15dp" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:itemCount="8" tools:listItem="@layout/view_quick_access_item" From 7b9461e3c58951120eaf0d52f26f059949567c0b Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 14 May 2021 14:07:06 +0200 Subject: [PATCH 68/95] Fix 3 dots clickable are in for favorite/boomark items --- app/src/main/res/layout/view_saved_site_entry.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/view_saved_site_entry.xml b/app/src/main/res/layout/view_saved_site_entry.xml index ddaad9c2917c..9721d5dc4aae 100644 --- a/app/src/main/res/layout/view_saved_site_entry.xml +++ b/app/src/main/res/layout/view_saved_site_entry.xml @@ -22,7 +22,6 @@ android:paddingTop="14dp" android:paddingBottom="14dp" android:paddingStart="16dp" - android:paddingEnd="10dp" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true"> @@ -84,6 +83,7 @@ android:layout_height="0dp" android:background="?selectableItemBackground" android:paddingStart="14dp" + android:paddingEnd="10dp" android:scaleType="center" android:src="@drawable/ic_overflow" app:layout_constraintBottom_toBottomOf="parent" From 55fba5de01b7a4c4e4e3f41050c7116eb346f467 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Sat, 15 May 2021 23:37:28 +0200 Subject: [PATCH 69/95] BookmarksViewModelTest --- .../bookmarks/ui/BookmarksViewModelTest.kt | 92 ++++++++++++++----- .../app/bookmarks/ui/BookmarksViewModel.kt | 46 +++++----- 2 files changed, 94 insertions(+), 44 deletions(-) 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 index ee757b64a26c..4972b4817a18 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt @@ -23,6 +23,8 @@ import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.InstantSchedulersRule import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.bookmarks.service.BookmarksManager import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.runBlocking @@ -30,9 +32,12 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import org.junit.After -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue +import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test @@ -53,26 +58,32 @@ class BookmarksViewModelTest { @Suppress("unused") val coroutineRule = CoroutineTestRule() + private val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.Command::class.java) + private val commandObserver: Observer = mock() + private val liveData = MutableLiveData>() private val viewStateObserver: Observer = mock() - private val commandObserver: Observer = mock() private val bookmarksDao: BookmarksDao = mock() + private val favoritesRepository: FavoritesRepository = mock() private val faviconManager: FaviconManager = mock() private val bookmarksManager: BookmarksManager = mock() - private val bookmark = BookmarkEntity(title = "title", url = "www.example.com") + private val bookmark = SavedSite.Bookmark(id= 0, title = "title", url = "www.example.com") + private val favorite = SavedSite.Favorite(id= 0, title = "title", url = "www.example.com", position = 0) + private val bookmarkEntity = BookmarkEntity(id = bookmark.id, title = bookmark.title, url = bookmark.url) private val testee: BookmarksViewModel by lazy { - val model = BookmarksViewModel(bookmarksDao, faviconManager, bookmarksManager, coroutineRule.testDispatcherProvider) + val model = BookmarksViewModel(favoritesRepository, bookmarksDao, faviconManager, bookmarksManager, coroutineRule.testDispatcherProvider) model.viewState.observeForever(viewStateObserver) model.command.observeForever(commandObserver) model } @Before - fun before() { + fun before() = coroutineRule.runBlocking { liveData.value = emptyList() whenever(bookmarksDao.getBookmarks()).thenReturn(liveData) + whenever(favoritesRepository.favorites()).thenReturn(flowOf()) } @After @@ -82,15 +93,53 @@ class BookmarksViewModelTest { } @Test - fun whenBookmarkDeletedThenDaoUpdated() { + fun whenBookmarkInsertedThenDaoUpdated() { + testee.insert(bookmark) + + verify(bookmarksDao).insert(bookmarkEntity) + } + + @Test + fun whenFavoriteInsertedThenRepositoryUpdated()= coroutineRule.runBlocking { + testee.insert(favorite) + + verify(favoritesRepository).insert(favorite) + } + + @Test + fun whenBookmarkDeletedThenDaoUpdated() = coroutineRule.runBlocking { testee.delete(bookmark) - verify(bookmarksDao).delete(bookmark) + + verify(faviconManager).deletePersistedFavicon(bookmark.url) + verify(bookmarksDao).delete(bookmarkEntity) + } + + @Test + fun whenFavoriteDeletedThenDeleteFromRepository() = coroutineRule.runBlocking { + testee.delete(favorite) + + verify(faviconManager).deletePersistedFavicon(favorite.url) + verify(favoritesRepository).delete(favorite) + } + + @Test + fun whenBookmarkEditedThenDaoUpdated() = coroutineRule.runBlocking { + testee.onSavedSiteEdited(bookmark) + + verify(bookmarksDao).update(bookmarkEntity) + } + + @Test + fun whenFavoriteEditedThenRepositoryUpdated() = coroutineRule.runBlocking { + testee.onSavedSiteEdited(favorite) + + verify(favoritesRepository).update(favorite) } @Test - fun whenBookmarkSelectedThenOpenCommand() { + fun whenSavedSiteSelectedThenOpenCommand() { 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.OpenSavedSite) @@ -98,8 +147,8 @@ class BookmarksViewModelTest { @Test fun whenDeleteRequestedThenConfirmCommand() { - testee.onDeleteRequested(bookmark) - val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.Command::class.java) + testee.onDeleteSavedSiteRequested(bookmark) + verify(commandObserver).onChanged(captor.capture()) assertNotNull(captor.value) assertTrue(captor.value is BookmarksViewModel.Command.ConfirmDeleteSavedSite) @@ -115,14 +164,15 @@ class BookmarksViewModelTest { } @Test - fun whenBookmarkDeletedThenFaviconDeleted() = coroutineRule.runBlocking { - testee.delete(bookmark) - verify(faviconManager).deletePersistedFavicon(bookmark.url) - } - - @Test - fun whenBookmarkInsertedThenDaoUpdated() { - testee.insert(bookmark) - verify(bookmarksDao).insert(bookmark) + fun whenFavoritesChangedThenObserverNotified() = coroutineRule.runBlocking { + whenever(favoritesRepository.favorites()).thenReturn(flow { + emit(emptyList()) + emit(listOf(favorite)) + }) + testee + val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.ViewState::class.java) + verify(viewStateObserver).onChanged(captor.capture()) + assertNotNull(captor.value) + assertEquals(1, captor.value.favorites.size) } } 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 index 8e145275033c..655e6c9da05f 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -106,29 +106,6 @@ class BookmarksViewModel( } } - private suspend fun editBookmark(bookmark: Bookmark) { - withContext(dispatcherProvider.io()) { - dao.update(BookmarkEntity(bookmark.id, bookmark.title, bookmark.url)) - } - } - - private suspend fun editFavorite(favorite: Favorite) { - withContext(dispatcherProvider.io()) { - favoritesRepository.update(favorite) - } - } - - private fun onBookmarksChanged(bookmarks: List) { - viewState.value = viewState.value?.copy( - bookmarks = bookmarks, - enableSearch = bookmarks.size > MIN_BOOKMARKS_FOR_SEARCH - ) - } - - private fun onFavoritesChanged(favorites: List) { - viewState.value = viewState.value?.copy(favorites = favorites) - } - fun onSelected(savedSite: SavedSite) { command.value = OpenSavedSite(savedSite) } @@ -190,6 +167,29 @@ class BookmarksViewModel( } } } + + private suspend fun editBookmark(bookmark: Bookmark) { + withContext(dispatcherProvider.io()) { + dao.update(BookmarkEntity(bookmark.id, bookmark.title, bookmark.url)) + } + } + + private suspend fun editFavorite(favorite: Favorite) { + withContext(dispatcherProvider.io()) { + favoritesRepository.update(favorite) + } + } + + private fun onBookmarksChanged(bookmarks: List) { + viewState.value = viewState.value?.copy( + bookmarks = bookmarks, + enableSearch = bookmarks.size > MIN_BOOKMARKS_FOR_SEARCH + ) + } + + private fun onFavoritesChanged(favorites: List) { + viewState.value = viewState.value?.copy(favorites = favorites) + } } @ContributesMultibinding(AppObjectGraph::class) From c8c35622a3dd37908bd903cdc9477c4d6a7a2ee9 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Sun, 16 May 2021 00:10:18 +0200 Subject: [PATCH 70/95] AutoCompleteTest --- .../autocomplete/api/AutoCompleteApiTest.kt | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt b/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt index ba00a6312009..69f6625c4167 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt @@ -19,6 +19,9 @@ package com.duckduckgo.app.autocomplete.api import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.bookmarks.model.SavedSite +import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite import com.nhaarman.mockitokotlin2.whenever import io.reactivex.Observable import io.reactivex.Single @@ -36,12 +39,15 @@ class AutoCompleteApiTest { @Mock private lateinit var mockBookmarksDao: BookmarksDao + @Mock + private lateinit var mockFavoritesRepository: FavoritesRepository + private lateinit var testee: AutoCompleteApi @Before fun before() { MockitoAnnotations.openMocks(this) - testee = AutoCompleteApi(mockAutoCompleteService, mockBookmarksDao) + testee = AutoCompleteApi(mockAutoCompleteService, mockBookmarksDao, mockFavoritesRepository) } @Test @@ -56,6 +62,7 @@ class AutoCompleteApiTest { fun whenReturnBookmarkSuggestionsThenPhraseIsURLBaseHost() { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(emptyList())) whenever(mockBookmarksDao.bookmarksObservable()).thenReturn(Single.just(listOf(BookmarkEntity(0, "title", "https://example.com")))) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("title").test() val value = result.values()[0] as AutoCompleteResult @@ -64,9 +71,10 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteDoesNotMatchBookmarksReturnEmptyBookmarkList() { + fun whenAutoCompleteDoesNotMatchAnySavedSiteReturnEmptySavedSiteList() { whenever(mockAutoCompleteService.autoComplete("wrong")).thenReturn(Observable.just(emptyList())) whenever(mockBookmarksDao.bookmarksObservable()).thenReturn(Single.just(listOf(BookmarkEntity(0, "title", "https://example.com")))) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(listOf(favorite(title = "title")))) val result = testee.autoComplete("wrong").test() val value = result.values()[0] as AutoCompleteResult @@ -87,6 +95,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("title").test() val value = result.values()[0] as AutoCompleteResult @@ -100,6 +109,41 @@ class AutoCompleteApiTest { ) } + @Test + fun whenAutoCompleteReturnsMultipleSavedSitesHitsThenLimitToMaxOfTwoFavoritesFirst() { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(emptyList())) + whenever(mockBookmarksDao.bookmarksObservable()).thenReturn( + Single.just( + listOf( + BookmarkEntity(0, "title", "https://example.com"), + BookmarkEntity(0, "title", "https://foo.com"), + BookmarkEntity(0, "title", "https://bar.com"), + BookmarkEntity(0, "title", "https://baz.com") + ) + ) + ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn( + Single.just( + listOf( + favorite(title = "title", url = "https://favexample.com"), + favorite(title = "title", url = "https://favfoo.com"), + favorite(title = "title", url = "https://favbar.com"), + favorite(title = "title", url = "https://favbaz.com") + ) + )) + + val result = testee.autoComplete("title").test() + val value = result.values()[0] as AutoCompleteResult + + assertEquals( + listOf( + AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion(phrase = "favexample.com", "title", "https://favexample.com"), + AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion(phrase = "favfoo.com", "title", "https://favfoo.com"), + ), + value.suggestions + ) + } + @Test fun whenAutoCompleteReturnsDuplicatedItemsThenDedup() { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( @@ -122,6 +166,16 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn( + Single.just( + listOf( + favorite(title ="title example", url = "https://example.com"), + favorite(title ="title foo", url = "https://foo.com/path/to/foo"), + favorite(title ="title foo", url = "https://foo.com"), + favorite(title ="title bar", url = "https://bar.com") + ) + ) + ) val result = testee.autoComplete("title").test() val value = result.values()[0] as AutoCompleteResult @@ -137,8 +191,26 @@ class AutoCompleteApiTest { value.suggestions ) } + @Test + fun whenReturnOneBookmarkAndOneFavoriteSuggestionsThenShowBothFavoriteFirst() { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(emptyList())) + whenever(mockBookmarksDao.bookmarksObservable()).thenReturn(Single.just(listOf(BookmarkEntity(0, "title", "https://example.com")))) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(listOf(Favorite(0, "title", "https://favexample.com", 1)))) + val result = testee.autoComplete("title").test() + val value = result.values()[0] as AutoCompleteResult + + assertEquals( + listOf( + AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion(phrase = "favexample.com", "title", "https://favexample.com"), + AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion(phrase = "example.com", "title", "https://example.com") + ), + value.suggestions + ) + } + + @Test fun whenAutoCompleteReturnsDuplicatedItemsThenDedupConsideringQueryParams() { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( Observable.just( @@ -159,6 +231,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("title").test() val value = result.values()[0] as AutoCompleteResult @@ -188,6 +261,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("title").test() val value = result.values()[0] as AutoCompleteResult @@ -214,6 +288,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("foo").test() val value = result.values()[0] as AutoCompleteResult @@ -238,6 +313,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("cnn").test() val value = result.values()[0] as AutoCompleteResult @@ -262,6 +338,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("reddit.com/").test() val value = result.values()[0] as AutoCompleteResult @@ -286,6 +363,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("reddit.com///").test() val value = result.values()[0] as AutoCompleteResult @@ -310,6 +388,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("reddit.com/r//").test() val value = result.values()[0] as AutoCompleteResult @@ -333,6 +412,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete("reddit").test() val value = result.values()[0] as AutoCompleteResult @@ -360,6 +440,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete(query).test() val value = result.values()[0] as AutoCompleteResult @@ -381,6 +462,7 @@ class AutoCompleteApiTest { ) ) ) + whenever(mockFavoritesRepository.favoritesObservable()).thenReturn(Single.just(emptyList())) val result = testee.autoComplete(query).test() val value = result.values()[0] as AutoCompleteResult @@ -393,4 +475,11 @@ class AutoCompleteApiTest { value.suggestions ) } + + private fun favorite( + id: Long = 0, + title: String = "title", + url: String = "https://example.com", + position: Int = 1 + ) = Favorite(id, title, url, position) } From d745c4dcf4c81dcb19bc311fabf2e53cf1f0fea4 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Sun, 16 May 2021 19:29:51 +0200 Subject: [PATCH 71/95] Favorites repository tested --- .../model/FavoritesDataRepositoryTest.kt | 156 ++++++++++++++++++ .../app/bookmarks/db/FavoritesDao.kt | 2 +- .../bookmarks/model/FavoritesRepository.kt | 4 +- .../app/browser/BrowserTabViewModel.kt | 2 +- .../app/systemsearch/SystemSearchViewModel.kt | 2 +- 5 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt new file mode 100644 index 000000000000..6aecf3fe1e2c --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2021 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.model + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.bookmarks.db.FavoriteEntity +import com.duckduckgo.app.bookmarks.db.FavoritesDao +import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite +import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.runBlocking +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class FavoritesDataRepositoryTest { + @get:Rule + @Suppress("unused") + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + @Suppress("unused") + val coroutineRule = CoroutineTestRule() + + private lateinit var db: AppDatabase + private lateinit var favoritesDao: FavoritesDao + private lateinit var repository: FavoritesRepository + + @Before + fun before() { + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) + .allowMainThreadQueries() + .build() + favoritesDao = db.favoritesDao() + repository = FavoritesDataRepository(favoritesDao) + } + + @Test + fun whenInsertFavoriteThenReturnSavedSite() = coroutineRule.runBlocking { + givenNoFavoritesStored() + + val savedSite = repository.insert("title", "http://example.com") + + assertEquals("title", savedSite.title) + assertEquals("http://example.com", savedSite.url) + assertEquals(1, savedSite.position) + } + + @Test + fun whenInsertFavoriteWithoutTitleThenSavedSiteUsesUrlAsTitle() = coroutineRule.runBlocking { + givenNoFavoritesStored() + + val savedSite = repository.insert("", "http://example.com") + + assertEquals("http://example.com", savedSite.title) + assertEquals("http://example.com", savedSite.url) + assertEquals(1, savedSite.position) + } + + @Test + fun whenUserHasFavoritesAndInsertFavoriteThenSavedSiteUsesNextPosition() = coroutineRule.runBlocking { + givenMoreFavoritesStored() + + val savedSite = repository.insert("Favorite", "http://favexample.com") + + assertEquals("Favorite", savedSite.title) + assertEquals("http://favexample.com", savedSite.url) + assertEquals(2, savedSite.position) + } + + @Test + fun whenDataSourceChangesThenNewListReceived() = coroutineRule.runBlocking { + givenNoFavoritesStored() + + repository.insert("Favorite", "http://favexample.com") + + val testObserver = repository.favoritesObservable().test() + val lastState = testObserver.assertNoErrors().values().last() + assertEquals(1, lastState.size) + assertEquals(Favorite(1, "Favorite", "http://favexample.com", 1), lastState.first()) + } + + @Test + fun whenFavoriteUpdatedThenDatabaseChanged() = coroutineRule.runBlocking { + val favorite = Favorite(1, "Favorite", "http://favexample.com", 1) + givenFavorite(favorite) + val updatedFavorite = favorite.copy(position = 3) + + repository.update(updatedFavorite) + + assertFavoriteExistsInDb(updatedFavorite) + } + + @Test + fun whenListReceivedThenUpdateItemsWithNewPositionInDatabase() = coroutineRule.runBlocking { + val favorite = Favorite(1, "Favorite", "http://favexample.com", 1) + val favorite2 = Favorite(2, "Favorite2", "http://favexample2.com", 2) + givenFavorite(favorite, favorite2) + + repository.updateWithPosition(listOf(favorite2, favorite)) + + assertFavoriteExistsInDb(favorite2.copy(position = 0)) + assertFavoriteExistsInDb(favorite.copy(position = 1)) + } + + @Test + fun whenFavoriteDeletedThenDatabaseUpdated() = coroutineRule.runBlocking { + val favorite = Favorite(1, "Favorite", "http://favexample.com", 1) + givenFavorite(favorite) + + repository.delete(favorite) + + assertNull(favoritesDao.favorite(favorite.id)) + } + + private fun givenFavorite(vararg favorite: Favorite) { + favorite.forEach { + favoritesDao.insert(FavoriteEntity(it.id, it.title, it.url, it.position)) + } + } + + private fun givenMoreFavoritesStored() { + favoritesDao.insert(FavoriteEntity(title = "title", url = "http://example.com", position = 0)) + favoritesDao.insert(FavoriteEntity(title = "title 2", url = "http://other.com", position = 1)) + } + + private suspend fun givenNoFavoritesStored() { + assertNull(favoritesDao.getLastPosition()) + } + + private fun assertFavoriteExistsInDb(favorite: Favorite) { + val storedFavorite = favoritesDao.favorite(favorite.id) ?: error("Favorite not found in database") + assertEquals(storedFavorite.title, favorite.title) + assertEquals(storedFavorite.url, favorite.url) + assertEquals(storedFavorite.position, favorite.position) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt index 821f8ab42ec2..75f536808e3f 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt @@ -49,7 +49,7 @@ interface FavoritesDao { fun favorite(id: Long): FavoriteEntity? @Transaction - suspend fun persistChanges(favorites: List) { + fun persistChanges(favorites: List) { favorites.forEachIndexed { index, favorite -> val favoriteEntity = favorite(favorite.id) ?: return favoriteEntity.position = index diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index 939525c22d86..9be25700f84a 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -30,7 +30,7 @@ interface FavoritesRepository { suspend fun insert(title: String, url: String): SavedSite.Favorite suspend fun insert(favorite: SavedSite.Favorite) suspend fun update(favorite: SavedSite.Favorite) - suspend fun persistChanges(favorites: List) + suspend fun updateWithPosition(favorites: List) suspend fun favorites(): Flow> suspend fun delete(favorite: SavedSite.Favorite) } @@ -81,7 +81,7 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite favoritesDao.update(FavoriteEntity(favorite.id, favorite.titleOrFallback(), favorite.url, favorite.position)) } - override suspend fun persistChanges(favorites: List) { + override suspend fun updateWithPosition(favorites: List) { favoritesDao.persistChanges(favorites) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 0a9c86acb918..416e8ef76783 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1984,7 +1984,7 @@ class BrowserTabViewModel( fun onQuickAccessListChanged(newList: List) { viewModelScope.launch(dispatchers.io()) { Timber.i("Persist favorites $newList") - favoritesRepository.persistChanges(newList.map { it.favorite }) + favoritesRepository.updateWithPosition(newList.map { it.favorite }) } } diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index 6b802306aecf..09f143dfb25e 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -284,7 +284,7 @@ class SystemSearchViewModel( fun onQuickAccessListChanged(newList: List) { viewModelScope.launch(dispatchers.io()) { Timber.i("Persist favorites $newList") - favoritesRepository.persistChanges(newList.map { it.favorite }) + favoritesRepository.updateWithPosition(newList.map { it.favorite }) } } From a431ba4aa833268055d23d231c17ec61a0e00b24 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 17 May 2021 12:13:03 +0200 Subject: [PATCH 72/95] update tests for faviconmanager --- .../favicon/DuckDuckGoFaviconManagerTest.kt | 116 +++++++----------- 1 file changed, 44 insertions(+), 72 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt index 1cb12fd29889..5d192805d1df 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt @@ -16,28 +16,30 @@ package com.duckduckgo.app.browser.favicon +import android.content.Context import android.graphics.Bitmap import android.widget.ImageView import androidx.core.net.toUri +import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.model.FavoritesRepository import com.duckduckgo.app.browser.favicon.FileBasedFaviconPersister.Companion.FAVICON_PERSISTED_DIR import com.duckduckgo.app.browser.favicon.FileBasedFaviconPersister.Companion.FAVICON_TEMP_DIR import com.duckduckgo.app.browser.favicon.FileBasedFaviconPersister.Companion.NO_SUBFOLDER import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.global.faviconLocation +import com.duckduckgo.app.global.view.loadFavicon import com.duckduckgo.app.location.data.LocationPermissionsDao import com.duckduckgo.app.location.data.LocationPermissionsRepository import com.duckduckgo.app.runBlocking -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import com.nhaarman.mockitokotlin2.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.withContext +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before import org.junit.Rule @@ -52,102 +54,76 @@ class DuckDuckGoFaviconManagerTest { private val mockFaviconPersister: FaviconPersister = mock() private val mockBookmarksDao: BookmarksDao = mock() + private val mockFavoriteRepository: FavoritesRepository = mock() private val mockFireproofWebsiteDao: FireproofWebsiteDao = mock() private val mockLocationPermissionsDao: LocationPermissionsDao = mock() private val mockFaviconDownloader: FaviconDownloader = mock() private val mockFile: File = File("test") + private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext private lateinit var testee: FaviconManager @Before - fun setup() { + fun setup() = coroutineRule.runBlocking { + whenever(mockFavoriteRepository.favoritesCountByDomain(any())).thenReturn(0) testee = DuckDuckGoFaviconManager( mockFaviconPersister, mockBookmarksDao, FireproofWebsiteRepository(mockFireproofWebsiteDao, coroutineRule.testDispatcherProvider, mock()), LocationPermissionsRepository(mockLocationPermissionsDao, mock(), coroutineRule.testDispatcherProvider), + mockFavoriteRepository, mockFaviconDownloader, coroutineRule.testDispatcherProvider ) } @Test - fun whenLoadFromTempIfFileExistsThenGetFaviconFromDisk() = coroutineRule.runBlocking { + fun whenLoadFromDiskIfFileExistsInTempThenGetFaviconFromDisk() = coroutineRule.runBlocking { givenFaviconExistsInTemp() - testee.loadFromTemp("subfolder", "example.com") + testee.loadFromDisk("subfolder", "example.com") verify(mockFaviconDownloader).getFaviconFromDisk(any()) } @Test - fun whenLoadFromTempIfFileDoesNotExistThenDoNothing() = coroutineRule.runBlocking { - testee.loadFromTemp("subfolder", "example.com") - - verify(mockFaviconDownloader, never()).getFaviconFromDisk(any()) - } - - @Test - fun whenLoadToViewFromPersistedThenLoadView() = coroutineRule.runBlocking { + fun whenLoadFromDiskIfFileExistsInPersistedThenGetFaviconFromDisk() = coroutineRule.runBlocking { givenFaviconExistsInDirectory(FAVICON_PERSISTED_DIR) - val view: ImageView = mock() - testee.loadToViewFromPersisted("example.com", view) + testee.loadFromDisk("subfolder", "example.com") - verify(mockFaviconDownloader).loadFaviconToView(mockFile, view) + verify(mockFaviconDownloader).getFaviconFromDisk(any()) } @Test - fun whenLoadToViewFromPersistedIfCannotFindFaviconThenDownloadFromUrl() = coroutineRule.runBlocking { - val view: ImageView = mock() - val url = "https://example.com" + fun whenLoadFromDiskIfFileDoesNotExistThenDoNothing() = coroutineRule.runBlocking { + testee.loadFromDisk("subfolder", "example.com") - testee.loadToViewFromPersisted(url, view) - - verify(mockFaviconDownloader).getFaviconFromUrl(url.toUri().faviconLocation()!!) + verify(mockFaviconDownloader, never()).getFaviconFromDisk(any()) } - @Test - fun whenLoadToViewFromPersistedIfCannotFindFaviconThenLoadDefaultFaviconIntoView() = coroutineRule.runBlocking { - val view: ImageView = mock() + @Test @UiThreadTest + fun whenLoadToViewFromLocalOrFallbackIfCannotFindFaviconThenDownloadFromUrl() = coroutineRule.runBlocking { + val view = ImageView(context) val url = "https://example.com" - testee.loadToViewFromPersisted(url, view) - - verify(mockFaviconDownloader).loadDefaultFaviconToView(view) - } - - @Test - fun whenLoadToViewFromTempThenLoadView() = coroutineRule.runBlocking { - givenFaviconExistsInDirectory(FAVICON_TEMP_DIR) - val view: ImageView = mock() - - testee.loadToViewFromTemp("subFolder", "example.com", view) + testee.loadToViewFromLocalOrFallback(url = url, view = view) - verify(mockFaviconDownloader).loadFaviconToView(mockFile, view) + verify(mockFaviconDownloader).getFaviconFromUrl(url.toUri().faviconLocation()!!) } - @Test - fun whenLoadToViewFromTempIfCannotFindFaviconThenDownloadFromUrl() = coroutineRule.runBlocking { - val view: ImageView = mock() + @Test @UiThreadTest + fun whenLoadToViewFromLocalOrFallbackWithTabIdIfCannotFindFaviconThenDownloadFromUrl() = coroutineRule.runBlocking { + val view = ImageView(context) val url = "https://example.com" - testee.loadToViewFromTemp("subFolder", url, view) + testee.loadToViewFromLocalOrFallback("subFolder", "example.com", view) verify(mockFaviconDownloader).getFaviconFromUrl(url.toUri().faviconLocation()!!) } @Test - fun whenLoadToViewFromTempIfCannotFindFaviconThenLoadDefaultFaviconIntoView() = coroutineRule.runBlocking { - val view = ImageView(InstrumentationRegistry.getInstrumentation().targetContext) - - testee.loadToViewFromTemp("subFolder", "example.com", view) - - verify(mockFaviconDownloader).loadDefaultFaviconToView(view) - } - - @Test - fun whenPrefetchToTempThenGetFaviconFromUrlAndStoreFile() = coroutineRule.runBlocking { + fun whenTryFetchFaviconForUrlThenGetFaviconFromUrlAndStoreFile() = coroutineRule.runBlocking { val bitmap = asBitmap() val url = "https://example.com" whenever(mockFaviconDownloader.getFaviconFromUrl(url.toUri().faviconLocation()!!)).thenReturn(bitmap) @@ -158,7 +134,7 @@ class DuckDuckGoFaviconManagerTest { } @Test - fun whenPrefetchToTempAndCannotDownloadThenReturnNull() = coroutineRule.runBlocking { + fun whenTryFetchFaviconForUrlAndCannotDownloadThenReturnNull() = coroutineRule.runBlocking { val url = "https://example.com" whenever(mockFaviconDownloader.getFaviconFromUrl(url.toUri().faviconLocation()!!)).thenReturn(null) @@ -168,39 +144,32 @@ class DuckDuckGoFaviconManagerTest { } @Test - fun whenPrefetchToTempAndDomainDoesNotExistThenReturnNull() = coroutineRule.runBlocking { - val file = testee.tryFetchFaviconForUrl("subFolder", "example.com") - - assertNull(file) - } - - @Test - fun whenSaveToTempIfFaviconHasBetterQualityThenReplacePersistedFavicons() = coroutineRule.runBlocking { + fun whenStoreFaviconIfFaviconHasBetterQualityThenReplacePersistedFavicons() = coroutineRule.runBlocking { val bitmap = asBitmap() - whenever(mockFireproofWebsiteDao.fireproofWebsitesCountByDomain(any())).thenReturn(1) + givenFaviconShouldBePersisted() whenever(mockFaviconPersister.store(FAVICON_TEMP_DIR, "subFolder", bitmap, "example.com")).thenReturn(File("example")) - testee.storeFavicon("subFolder", bitmap, "example.com") + testee.storeFavicon("subFolder", FaviconSource.ImageFavicon(bitmap, "example.com")) verify(mockFaviconPersister).store(FAVICON_PERSISTED_DIR, NO_SUBFOLDER, bitmap, "example.com") } @Test - fun whenSaveToTempIfFaviconDoesNotHaveBetterQualityThenDoNotReplacePersistedFavicons() = coroutineRule.runBlocking { + fun whenStoreFaviconIfFaviconDoesNotHaveBetterQualityThenDoNotReplacePersistedFavicons() = coroutineRule.runBlocking { val bitmap = asBitmap() - whenever(mockFireproofWebsiteDao.fireproofWebsitesCountByDomain(any())).thenReturn(1) + givenFaviconShouldBePersisted() whenever(mockFaviconPersister.store(FAVICON_TEMP_DIR, "subFolder", bitmap, "example.com")).thenReturn(null) - testee.storeFavicon("subFolder", bitmap, "example.com") + testee.storeFavicon("subFolder", FaviconSource.ImageFavicon(bitmap, "example.com")) verify(mockFaviconPersister, never()).store(FAVICON_PERSISTED_DIR, NO_SUBFOLDER, bitmap, "example.com") } @Test - fun whenSaveToTempThenStoreFile() = coroutineRule.runBlocking { + fun whenStoreFaviconThenStoreFile() = coroutineRule.runBlocking { val bitmap = asBitmap() - testee.storeFavicon("subFolder", bitmap, "example.com") + testee.storeFavicon("subFolder", FaviconSource.ImageFavicon(bitmap, "example.com")) verify(mockFaviconPersister).store(FAVICON_TEMP_DIR, "subFolder", bitmap, "example.com") } @@ -223,7 +192,7 @@ class DuckDuckGoFaviconManagerTest { @Test fun whenDeletePersistedFaviconIfNoRemainingFaviconsInDatabaseThenDeleteFavicon() = coroutineRule.runBlocking { - whenever(mockFireproofWebsiteDao.fireproofWebsitesCountByDomain(any())).thenReturn(1) + givenFaviconShouldBePersisted() testee.deletePersistedFavicon("example.com") verify(mockFaviconPersister).deletePersistedFavicon("example.com") @@ -262,4 +231,7 @@ class DuckDuckGoFaviconManagerTest { whenever(mockFaviconPersister.faviconFile(eq(directory), any(), any())).thenReturn(mockFile) } + private fun givenFaviconShouldBePersisted() { + whenever(mockFireproofWebsiteDao.fireproofWebsitesCountByDomain(any())).thenReturn(1) + } } From 8afb9d7984e41d3d8108976805ec7949c6d5fa17 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 17 May 2021 12:24:47 +0200 Subject: [PATCH 73/95] Update tests on BrowserTabViewModel --- .../app/browser/BrowserTabViewModelTest.kt | 53 ++++++++++++------- .../bookmarks/model/FavoritesRepository.kt | 4 +- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 9372ed1c511e..1d2cc61d3e9c 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -38,6 +38,8 @@ import com.duckduckgo.app.autocomplete.api.AutoCompleteApi import com.duckduckgo.app.autocomplete.api.AutoCompleteService import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao +import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.browser.BrowserTabViewModel.Command import com.duckduckgo.app.browser.BrowserTabViewModel.Command.Navigate import com.duckduckgo.app.browser.BrowserTabViewModel.FireButton @@ -46,6 +48,7 @@ import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.OpenInNewTab import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.downloader.FileDownloader import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.favicon.FaviconSource import com.duckduckgo.app.browser.logindetection.FireproofDialogsEventHandler import com.duckduckgo.app.browser.logindetection.LoginDetected import com.duckduckgo.app.browser.logindetection.NavigationAwareLoginDetector @@ -148,8 +151,7 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.never -import org.mockito.Mockito.verify +import org.mockito.Mockito.* import org.mockito.MockitoAnnotations import org.mockito.internal.util.DefaultMockingDetails import java.io.File @@ -263,6 +265,9 @@ class BrowserTabViewModelTest { @Mock private lateinit var mockEmailManager: EmailManager + @Mock + private lateinit var mockFavoritesRepository: FavoritesRepository + private val lazyFaviconManager = Lazy { mockFaviconManager } private lateinit var mockAutoCompleteApi: AutoCompleteApi @@ -303,12 +308,13 @@ class BrowserTabViewModelTest { fireproofWebsiteDao = db.fireproofWebsiteDao() locationPermissionsDao = db.locationPermissionsDao() - mockAutoCompleteApi = AutoCompleteApi(mockAutoCompleteService, mockBookmarksDao) + mockAutoCompleteApi = AutoCompleteApi(mockAutoCompleteService, mockBookmarksDao, mockFavoritesRepository) val fireproofWebsiteRepository = FireproofWebsiteRepository(fireproofWebsiteDao, coroutineRule.testDispatcherProvider, lazyFaviconManager) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(dismissedCtaDaoChannel.consumeAsFlow()) whenever(mockTabRepository.flowTabs).thenReturn(flowOf(emptyList())) whenever(mockEmailManager.signedInFlow()).thenReturn(emailStateFlow.asStateFlow()) + whenever(mockFavoritesRepository.favorites()).thenReturn(flowOf()) ctaViewModel = CtaViewModel( mockAppInstallStore, @@ -375,7 +381,8 @@ class BrowserTabViewModelTest { fileDownloader = mockFileDownloader, globalPrivacyControl = GlobalPrivacyControlManager(mockSettingsStore), fireproofDialogsEventHandler = fireproofDialogsEventHandler, - emailManager = mockEmailManager + emailManager = mockEmailManager, + favoritesRepository = mockFavoritesRepository ) testee.loadData("abc", null, false) @@ -527,10 +534,17 @@ class BrowserTabViewModelTest { @Test fun whenBookmarkEditedThenDaoIsUpdated() = coroutineRule.runBlocking { - testee.editBookmark(0, "A title", "www.example.com") + testee.onSavedSiteEdited(SavedSite.Bookmark(0, "A title", "www.example.com")) verify(mockBookmarksDao).update(BookmarkEntity(title = "A title", url = "www.example.com")) } + @Test + fun whenFavoriteEditedThenRepositoryUpdated() = coroutineRule.runBlocking { + val favorite = SavedSite.Favorite(0, "A title", "www.example.com", 1) + testee.onSavedSiteEdited(favorite) + verify(mockFavoritesRepository).update(favorite) + } + @Test fun whenBookmarkAddedThenDaoIsUpdatedAndUserNotified() = coroutineRule.runBlocking { loadUrl("www.example.com", "A title") @@ -1399,19 +1413,17 @@ class BrowserTabViewModelTest { testee.titleReceived("Foo Title") testee.onBookmarkAddRequested() val command = captureCommands().value as Command.ShowBookmarkAddedConfirmation - assertEquals("foo.com", command.url) - assertEquals("Foo Title", command.title) + assertEquals("foo.com", command.bookmark.url) + assertEquals("Foo Title", command.bookmark.title) } @Test - fun whenNoSiteAndUserSelectsToAddBookmarkThenBookmarkAddedWithBlankTitleAndUrl() = coroutineRule.runBlocking { + fun whenNoSiteAndUserSelectsToAddBookmarkThenBookmarkIsNotAdded() = coroutineRule.runBlocking { whenever(mockBookmarksDao.insert(any())).thenReturn(1) + testee.onBookmarkAddRequested() - verify(mockBookmarksDao).insert(BookmarkEntity(title = "", url = "")) - val command = captureCommands().value as Command.ShowBookmarkAddedConfirmation - assertEquals(1, command.bookmarkId) - assertEquals("", command.title) - assertEquals("", command.url) + + verify(mockBookmarksDao, times(0)).insert(any()) } @Test @@ -1695,7 +1707,8 @@ class BrowserTabViewModelTest { @Test fun whenUserSelectToEditQueryThenMoveCaretToTheEnd() = coroutineRule.runBlocking { testee.onUserSelectedToEditQuery("foo") - assertTrue(omnibarViewState().shouldMoveCaretToEnd) + + assertCommandIssued() } @Test @@ -2915,7 +2928,7 @@ class BrowserTabViewModelTest { testee.iconReceived("https://example.com", bitmap) - verify(mockFaviconManager).storeFavicon("TAB_ID", bitmap, "https://example.com") + verify(mockFaviconManager).storeFavicon("TAB_ID", FaviconSource.ImageFavicon(bitmap, "https://example.com")) } @Test @@ -2923,7 +2936,7 @@ class BrowserTabViewModelTest { givenOneActiveTabSelected() val bitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565) val file = File("test") - whenever(mockFaviconManager.storeFavicon(any(), any(), any())).thenReturn(file) + whenever(mockFaviconManager.storeFavicon(any(), any())).thenReturn(file) testee.iconReceived("https://example.com", bitmap) @@ -2934,7 +2947,7 @@ class BrowserTabViewModelTest { fun whenIconReceivedIfNotCorrectlySavedThenDoNotUpdateTabFavicon() = coroutineRule.runBlocking { givenOneActiveTabSelected() val bitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565) - whenever(mockFaviconManager.storeFavicon(any(), any(), any())).thenReturn(null) + whenever(mockFaviconManager.storeFavicon(any(), any())).thenReturn(null) testee.iconReceived("https://example.com", bitmap) @@ -2946,7 +2959,7 @@ class BrowserTabViewModelTest { givenOneActiveTabSelected() val bitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565) val file = File("test") - whenever(mockFaviconManager.storeFavicon(any(), any(), any())).thenReturn(file) + whenever(mockFaviconManager.storeFavicon(any(), any())).thenReturn(file) testee.iconReceived("https://notexample.com", bitmap) @@ -3021,7 +3034,7 @@ class BrowserTabViewModelTest { fun whenOnPinPageToHomeSelectedThenAddHomeShortcutCommandIssuedWithFavicon() = coroutineRule.runBlocking { val url = "http://example.com" val bitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565) - whenever(mockFaviconManager.loadFromTemp(any(), any())).thenReturn(bitmap) + whenever(mockFaviconManager.loadFromDisk(any(), any())).thenReturn(bitmap) loadUrl(url, "A title") testee.onPinPageToHomeSelected() @@ -3036,7 +3049,7 @@ class BrowserTabViewModelTest { @Test fun whenOnPinPageToHomeSelectedAndFaviconDoesNotExistThenAddHomeShortcutCommandIssuedWithoutFavicon() = coroutineRule.runBlocking { val url = "http://example.com" - whenever(mockFaviconManager.loadFromTemp(any(), any())).thenReturn(null) + whenever(mockFaviconManager.loadFromDisk(any(), any())).thenReturn(null) loadUrl(url, "A title") testee.onPinPageToHomeSelected() diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index 9be25700f84a..fb41c153d840 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -31,7 +31,7 @@ interface FavoritesRepository { suspend fun insert(favorite: SavedSite.Favorite) suspend fun update(favorite: SavedSite.Favorite) suspend fun updateWithPosition(favorites: List) - suspend fun favorites(): Flow> + fun favorites(): Flow> suspend fun delete(favorite: SavedSite.Favorite) } @@ -85,7 +85,7 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite favoritesDao.persistChanges(favorites) } - override suspend fun favorites(): Flow> { + override fun favorites(): Flow> { return favoritesDao.favorites().distinctUntilChanged().map { favorites -> favorites.mapToSavedSites() } } From 356e7458aab1abbdd91256c624bc6b0b9bc6c357 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 17 May 2021 12:28:49 +0200 Subject: [PATCH 74/95] remove unnecessary suspend operator --- .../app/bookmarks/db/FavoritesDao.kt | 2 +- .../bookmarks/model/FavoritesRepository.kt | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt index 75f536808e3f..14cca451d2d9 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/db/FavoritesDao.kt @@ -43,7 +43,7 @@ interface FavoritesDao { fun favoritesObservable(): Single> @Query("select position from favorites where position = ( select MAX(position) from favorites)") - suspend fun getLastPosition(): Int? + fun getLastPosition(): Int? @Query("select * from favorites where id = :id") fun favorite(id: Long): FavoriteEntity? diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index fb41c153d840..6f9152e2edfc 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -25,14 +25,14 @@ import kotlinx.coroutines.flow.map import java.io.Serializable interface FavoritesRepository { - suspend fun favoritesCountByDomain(domain: String): Int + fun favoritesCountByDomain(domain: String): Int fun favoritesObservable(): Single> - suspend fun insert(title: String, url: String): SavedSite.Favorite - suspend fun insert(favorite: SavedSite.Favorite) - suspend fun update(favorite: SavedSite.Favorite) - suspend fun updateWithPosition(favorites: List) + fun insert(title: String, url: String): SavedSite.Favorite + fun insert(favorite: SavedSite.Favorite) + fun update(favorite: SavedSite.Favorite) + fun updateWithPosition(favorites: List) fun favorites(): Flow> - suspend fun delete(favorite: SavedSite.Favorite) + fun delete(favorite: SavedSite.Favorite) } sealed class SavedSite( @@ -55,14 +55,14 @@ sealed class SavedSite( } class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : FavoritesRepository { - override suspend fun favoritesCountByDomain(domain: String): Int { + override fun favoritesCountByDomain(domain: String): Int { return favoritesDao.favoritesCountByUrl(domain) } override fun favoritesObservable() = favoritesDao.favoritesObservable().map { favorites -> favorites.mapToSavedSites() } - override suspend fun insert(title: String, url: String): SavedSite.Favorite { + override fun insert(title: String, url: String): SavedSite.Favorite { val titleOrFallback = title.takeIf { it.isNotEmpty() } ?: url val lastPosition = favoritesDao.getLastPosition() ?: 0 val favoriteEntity = FavoriteEntity(title = titleOrFallback, url = url, position = lastPosition + 1) @@ -70,18 +70,18 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite return SavedSite.Favorite(id, favoriteEntity.title, favoriteEntity.url, favoriteEntity.position) } - override suspend fun insert(favorite: SavedSite.Favorite) { + override fun insert(favorite: SavedSite.Favorite) { if (favorite.url.isEmpty()) return val favoriteEntity = FavoriteEntity(title = favorite.titleOrFallback(), url = favorite.url, position = favorite.position) favoritesDao.insert(favoriteEntity) } - override suspend fun update(favorite: SavedSite.Favorite) { + override fun update(favorite: SavedSite.Favorite) { if (favorite.url.isEmpty()) return favoritesDao.update(FavoriteEntity(favorite.id, favorite.titleOrFallback(), favorite.url, favorite.position)) } - override suspend fun updateWithPosition(favorites: List) { + override fun updateWithPosition(favorites: List) { favoritesDao.persistChanges(favorites) } @@ -89,7 +89,7 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite return favoritesDao.favorites().distinctUntilChanged().map { favorites -> favorites.mapToSavedSites() } } - override suspend fun delete(favorite: SavedSite.Favorite) { + override fun delete(favorite: SavedSite.Favorite) { favoritesDao.delete(FavoriteEntity(favorite.id, favorite.title, favorite.url, favorite.position)) } From 1b0d9f6d43020309ca30489e2ee4360e2673b442 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 17 May 2021 14:43:56 +0200 Subject: [PATCH 75/95] Quick access grid show title in 2 lines --- app/src/main/res/layout/view_quick_access_item.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/view_quick_access_item.xml b/app/src/main/res/layout/view_quick_access_item.xml index 344136db8e9e..40c4ae8dbf72 100644 --- a/app/src/main/res/layout/view_quick_access_item.xml +++ b/app/src/main/res/layout/view_quick_access_item.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" - android:padding="15dp" + android:paddingTop="15dp" + android:paddingBottom="15dp" android:clipToPadding="false"> @@ -50,7 +53,9 @@ android:layout_gravity="center_horizontal" android:ellipsize="end" android:gravity="center" - android:maxLines="1" + android:maxLines="2" + android:paddingStart="6dp" + android:paddingEnd="6dp" android:paddingTop="8dp" android:textColor="?normalTextColor" android:textSize="12sp" From 1f6c923c573f48b4b1828c2ad9ee6192f9c55b84 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 17 May 2021 17:03:44 +0200 Subject: [PATCH 76/95] added new tests to cover logic in BrowserTabViewModel --- .../app/browser/BrowserTabViewModelTest.kt | 136 ++++++++++++++++-- 1 file changed, 127 insertions(+), 9 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 1d2cc61d3e9c..f63d4c3e6046 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -40,6 +40,7 @@ import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.bookmarks.model.FavoritesRepository import com.duckduckgo.app.bookmarks.model.SavedSite +import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite import com.duckduckgo.app.browser.BrowserTabViewModel.Command import com.duckduckgo.app.browser.BrowserTabViewModel.Command.Navigate import com.duckduckgo.app.browser.BrowserTabViewModel.FireButton @@ -49,6 +50,7 @@ import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.downloader.FileDownloader import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favicon.FaviconSource +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessFavorite import com.duckduckgo.app.browser.logindetection.FireproofDialogsEventHandler import com.duckduckgo.app.browser.logindetection.LoginDetected import com.duckduckgo.app.browser.logindetection.NavigationAwareLoginDetector @@ -515,21 +517,24 @@ class BrowserTabViewModelTest { } @Test - fun whenBrowsingAndUrlPresentThenAddBookmarkButtonEnabled() { + fun whenBrowsingAndUrlPresentThenAddBookmarkFavoriteButtonsEnabled() { loadUrl("www.example.com", isBrowserShowing = true) assertTrue(browserViewState().canAddBookmarks) + assertTrue(browserViewState().canAddFavorite) } @Test - fun whenBrowsingAndNoUrlThenAddBookmarkButtonDisabled() { + fun whenBrowsingAndNoUrlThenAddBookmarkFavoriteButtonsDisabled() { loadUrl(null, isBrowserShowing = true) assertFalse(browserViewState().canAddBookmarks) + assertFalse(browserViewState().canAddFavorite) } @Test - fun whenNotBrowsingAndUrlPresentThenAddBookmarkButtonDisabled() { + fun whenNotBrowsingAndUrlPresentThenAddBookmarkFavoriteButtonsDisabled() { loadUrl("www.example.com", isBrowserShowing = false) assertFalse(browserViewState().canAddBookmarks) + assertFalse(browserViewState().canAddFavorite) } @Test @@ -540,7 +545,7 @@ class BrowserTabViewModelTest { @Test fun whenFavoriteEditedThenRepositoryUpdated() = coroutineRule.runBlocking { - val favorite = SavedSite.Favorite(0, "A title", "www.example.com", 1) + val favorite = Favorite(0, "A title", "www.example.com", 1) testee.onSavedSiteEdited(favorite) verify(mockFavoritesRepository).update(favorite) } @@ -555,6 +560,65 @@ class BrowserTabViewModelTest { assertTrue(commandCaptor.lastValue is Command.ShowBookmarkAddedConfirmation) } + @Test + fun whenFavoriteAddedThenRepositoryUpdatedAndUserNotified() = coroutineRule.runBlocking { + val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + loadUrl("www.example.com", "A title") + whenever(mockFavoritesRepository.insert(any(), any())).thenReturn(savedSite) + + testee.onAddFavoriteMenuClicked() + + verify(mockFavoritesRepository).insert(title = "A title", url = "www.example.com") + assertCommandIssued() + } + + @Test + fun whenNoSiteAndUserSelectsToAddFavoriteThenSiteIsNotAdded() = coroutineRule.runBlocking { + + testee.onAddFavoriteMenuClicked() + + verify(mockFavoritesRepository, times(0)).insert(any(), any()) + } + + @Test + fun whenQuickAccessItemClickedThenSubmitNewQuery() { + val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + + testee.onQuickAccesItemClicked(savedSite) + + assertCommandIssued { + assertEquals("http://example.com", this.url) + } + } + + @Test + fun whenQuickAccessDeletedThenRepositoryUpdated() { + val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + + testee.deleteQuickAccessItem(savedSite) + + verify(mockFavoritesRepository).delete(savedSite) + } + + @Test + fun whenQuickAccessInsertedThenRepositoryUpdated() { + val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + + testee.insertQuickAccessItem(savedSite) + + verify(mockFavoritesRepository).insert(savedSite) + } + + @Test + fun whenQuickAccessListChangedThenRepositoryUpdated() { + val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + val savedSites = listOf(QuickAccessFavorite(savedSite)) + + testee.onQuickAccessListChanged(savedSites) + + verify(mockFavoritesRepository).updateWithPosition(listOf(savedSite)) + } + @Test fun whenTrackerDetectedThenNetworkLeaderboardUpdated() { val networkEntity = TestEntity("Network1", "Network1", 10.0) @@ -1037,6 +1101,15 @@ class BrowserTabViewModelTest { assertFalse(autoCompleteViewState().showSuggestions) } + @Test + fun whenOmnibarFocusedAndUserHasFavoritesThenAutoCompleteShowsFavorites() { + testee.autoCompleteViewState.value = autoCompleteViewState().copy(favorites = listOf(QuickAccessFavorite(Favorite(1, "title", "http://example.com", 1)))) + doReturn(true).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled + testee.onOmnibarInputStateChanged("foo", true, hasQueryChanged = false) + assertTrue(autoCompleteViewState().showSuggestions) + assertTrue(autoCompleteViewState().searchResults.suggestions.isEmpty()) + } + @Test fun whenEnteringQueryWithAutoCompleteDisabledThenAutoCompleteSuggestionsNotShown() { doReturn(false).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled @@ -2215,10 +2288,11 @@ class BrowserTabViewModelTest { } @Test - fun whenUserBrowsingPressesBackThenCannotAddBookmark() { + fun whenUserBrowsingPressesBackThenCannotAddBookmarkOrFavorite() { setupNavigation(skipHome = false, isBrowsing = true, canGoBack = false) assertTrue(testee.onUserPressedBack()) assertFalse(browserViewState().canAddBookmarks) + assertFalse(browserViewState().canAddFavorite) } @Test @@ -2271,11 +2345,12 @@ class BrowserTabViewModelTest { } @Test - fun whenUserBrowsingPressesBackAndForwardThenCanAddBookmark() { + fun whenUserBrowsingPressesBackAndForwardThenCanAddBookmarkOrFavorite() { setupNavigation(skipHome = false, isBrowsing = true, canGoBack = false) testee.onUserPressedBack() testee.onUserPressedForward() assertTrue(browserViewState().canAddBookmarks) + assertTrue(browserViewState().canAddFavorite) } @Test @@ -2892,7 +2967,7 @@ class BrowserTabViewModelTest { } @Test - fun whenPrefetchFaviconThenPrefetchToTemp() = coroutineRule.runBlocking { + fun whenPrefetchFaviconThenFetchFaviconForCurrentTab() = coroutineRule.runBlocking { val url = "https://www.example.com/" givenCurrentSite(url) testee.prefetchFavicon(url) @@ -2922,7 +2997,7 @@ class BrowserTabViewModelTest { } @Test - fun whenIconReceivedThenSaveToTemp() = coroutineRule.runBlocking { + fun whenIconReceivedThenStoreFavicon() = coroutineRule.runBlocking { givenOneActiveTabSelected() val bitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565) @@ -2955,7 +3030,7 @@ class BrowserTabViewModelTest { } @Test - fun whenIconReceivedFromPreviousUrkThenDontUpdateTabFavicon() = coroutineRule.runBlocking { + fun whenIconReceivedFromPreviousUrlThenDontUpdateTabFavicon() = coroutineRule.runBlocking { givenOneActiveTabSelected() val bitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565) val file = File("test") @@ -2967,6 +3042,49 @@ class BrowserTabViewModelTest { verify(mockTabRepository, never()).updateTabFavicon("TAB_ID", file.name) } + @Test + fun whenUrlIconReceivedThenStoreFavicon() = coroutineRule.runBlocking { + givenOneActiveTabSelected() + + testee.iconReceived("https://example.com", "https://example.com/favicon.png") + + verify(mockFaviconManager).storeFavicon("TAB_ID", FaviconSource.UrlFavicon("https://example.com/favicon.png", "https://example.com")) + } + + @Test + fun whenUrlIconReceivedIfCorrectlySavedThenUpdateTabFavicon() = coroutineRule.runBlocking { + givenOneActiveTabSelected() + val file = File("test") + whenever(mockFaviconManager.storeFavicon(any(), any())).thenReturn(file) + + testee.iconReceived("https://example.com", "https://example.com/favicon.png") + + verify(mockTabRepository).updateTabFavicon("TAB_ID", file.name) + } + + @Test + fun whenUrlIconReceivedIfNotCorrectlySavedThenDoNotUpdateTabFavicon() = coroutineRule.runBlocking { + givenOneActiveTabSelected() + whenever(mockFaviconManager.storeFavicon(any(), any())).thenReturn(null) + + testee.iconReceived("https://example.com", "https://example.com/favicon.png") + + verify(mockTabRepository, never()).updateTabFavicon(any(), any()) + } + + @Test + fun whenUrlIconReceivedFromPreviousUrlThenDontUpdateTabFavicon() = coroutineRule.runBlocking { + givenOneActiveTabSelected() + val bitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565) + val file = File("test") + whenever(mockFaviconManager.storeFavicon(any(), any())).thenReturn(file) + + testee.iconReceived("https://notexample.com", "https://example.com/favicon.png") + + verify(mockPixel).enqueueFire(AppPixelName.FAVICON_WRONG_URL_ERROR) + verify(mockFaviconManager, never()).storeFavicon(any(), any()) + } + @Test fun whenOnSiteLocationPermissionSelectedAndPermissionIsAllowAlwaysThenPersistFavicon() = coroutineRule.runBlocking { val url = "http://example.com" From 567c3be746e2e1665647d376f6b40633cdfb3456 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 17 May 2021 17:06:44 +0200 Subject: [PATCH 77/95] apply code style --- .../autocomplete/api/AutoCompleteApiTest.kt | 22 ++++++++-------- .../model/FavoritesDataRepositoryTest.kt | 2 +- .../bookmarks/ui/BookmarksViewModelTest.kt | 18 ++++++------- .../favicon/DuckDuckGoFaviconManagerTest.kt | 4 --- .../systemsearch/SystemSearchViewModelTest.kt | 25 +++++++++++-------- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt b/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt index 69f6625c4167..7068d558462f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt @@ -20,7 +20,6 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.bookmarks.model.FavoritesRepository -import com.duckduckgo.app.bookmarks.model.SavedSite import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite import com.nhaarman.mockitokotlin2.whenever import io.reactivex.Observable @@ -124,13 +123,14 @@ class AutoCompleteApiTest { ) whenever(mockFavoritesRepository.favoritesObservable()).thenReturn( Single.just( - listOf( - favorite(title = "title", url = "https://favexample.com"), - favorite(title = "title", url = "https://favfoo.com"), - favorite(title = "title", url = "https://favbar.com"), - favorite(title = "title", url = "https://favbaz.com") + listOf( + favorite(title = "title", url = "https://favexample.com"), + favorite(title = "title", url = "https://favfoo.com"), + favorite(title = "title", url = "https://favbar.com"), + favorite(title = "title", url = "https://favbaz.com") + ) ) - )) + ) val result = testee.autoComplete("title").test() val value = result.values()[0] as AutoCompleteResult @@ -169,10 +169,10 @@ class AutoCompleteApiTest { whenever(mockFavoritesRepository.favoritesObservable()).thenReturn( Single.just( listOf( - favorite(title ="title example", url = "https://example.com"), - favorite(title ="title foo", url = "https://foo.com/path/to/foo"), - favorite(title ="title foo", url = "https://foo.com"), - favorite(title ="title bar", url = "https://bar.com") + favorite(title = "title example", url = "https://example.com"), + favorite(title = "title foo", url = "https://foo.com/path/to/foo"), + favorite(title = "title foo", url = "https://foo.com"), + favorite(title = "title bar", url = "https://bar.com") ) ) ) diff --git a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt index 6aecf3fe1e2c..ca8533558281 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt @@ -153,4 +153,4 @@ class FavoritesDataRepositoryTest { assertEquals(storedFavorite.url, favorite.url) assertEquals(storedFavorite.position, favorite.position) } -} \ 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 index 4972b4817a18..0ce1892ab0e0 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt @@ -32,8 +32,6 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import org.junit.After @@ -68,8 +66,8 @@ class BookmarksViewModelTest { private val faviconManager: FaviconManager = mock() private val bookmarksManager: BookmarksManager = mock() - private val bookmark = SavedSite.Bookmark(id= 0, title = "title", url = "www.example.com") - private val favorite = SavedSite.Favorite(id= 0, title = "title", url = "www.example.com", position = 0) + private val bookmark = SavedSite.Bookmark(id = 0, title = "title", url = "www.example.com") + private val favorite = SavedSite.Favorite(id = 0, title = "title", url = "www.example.com", position = 0) private val bookmarkEntity = BookmarkEntity(id = bookmark.id, title = bookmark.title, url = bookmark.url) private val testee: BookmarksViewModel by lazy { @@ -100,7 +98,7 @@ class BookmarksViewModelTest { } @Test - fun whenFavoriteInsertedThenRepositoryUpdated()= coroutineRule.runBlocking { + fun whenFavoriteInsertedThenRepositoryUpdated() = coroutineRule.runBlocking { testee.insert(favorite) verify(favoritesRepository).insert(favorite) @@ -165,10 +163,12 @@ class BookmarksViewModelTest { @Test fun whenFavoritesChangedThenObserverNotified() = coroutineRule.runBlocking { - whenever(favoritesRepository.favorites()).thenReturn(flow { - emit(emptyList()) - emit(listOf(favorite)) - }) + whenever(favoritesRepository.favorites()).thenReturn( + flow { + emit(emptyList()) + emit(listOf(favorite)) + } + ) testee val captor: ArgumentCaptor = ArgumentCaptor.forClass(BookmarksViewModel.ViewState::class.java) verify(viewStateObserver).onChanged(captor.capture()) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt index 5d192805d1df..3369818df0d9 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt @@ -31,15 +31,11 @@ import com.duckduckgo.app.browser.favicon.FileBasedFaviconPersister.Companion.NO import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.global.faviconLocation -import com.duckduckgo.app.global.view.loadFavicon import com.duckduckgo.app.location.data.LocationPermissionsDao import com.duckduckgo.app.location.data.LocationPermissionsRepository import com.duckduckgo.app.runBlocking import com.nhaarman.mockitokotlin2.* -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.withContext -import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before import org.junit.Rule diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index c4d614b7d70a..3635e26bb92a 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -24,12 +24,15 @@ import com.duckduckgo.app.InstantSchedulersRule import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.pixels.AppPixelName.* import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchDuckDuckGo +import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Suggestions.SystemSearchResultsViewState import com.nhaarman.mockitokotlin2.* import io.reactivex.Observable import kotlinx.coroutines.test.TestCoroutineDispatcher @@ -56,6 +59,8 @@ class SystemSearchViewModelTest { private val mockUserStageStore: UserStageStore = mock() private val mockDeviceAppLookup: DeviceAppLookup = mock() private val mockAutoComplete: AutoComplete = mock() + private val mockFavoritesRepository: FavoritesRepository = mock() + private val mockFaviconManager: FaviconManager = mock() private val mockPixel: Pixel = mock() private val commandObserver: Observer = mock() @@ -69,7 +74,7 @@ class SystemSearchViewModelTest { whenever(mockAutoComplete.autoComplete(BLANK_QUERY)).thenReturn(Observable.just(autocompleteBlankResult)) whenever(mockDeviceAppLookup.query(QUERY)).thenReturn(appQueryResult) whenever(mockDeviceAppLookup.query(BLANK_QUERY)).thenReturn(appBlankResult) - testee = SystemSearchViewModel(mockUserStageStore, mockAutoComplete, mockDeviceAppLookup, mockPixel, coroutineRule.testDispatcherProvider) + testee = SystemSearchViewModel(mockUserStageStore, mockAutoComplete, mockDeviceAppLookup, mockPixel, mockFavoritesRepository, mockFaviconManager, coroutineRule.testDispatcherProvider) testee.command.observeForever(commandObserver) } @@ -102,7 +107,7 @@ class SystemSearchViewModelTest { fun whenDatabaseIsSlowThenIntroducingTextDoesNotCrashTheApp() = coroutineRule.runBlocking { (coroutineRule.testDispatcherProvider.io() as TestCoroutineDispatcher).pauseDispatcher() testee = - SystemSearchViewModel(givenEmptyUserStageStore(), mockAutoComplete, mockDeviceAppLookup, mockPixel, coroutineRule.testDispatcherProvider) + SystemSearchViewModel(givenEmptyUserStageStore(), mockAutoComplete, mockDeviceAppLookup, mockPixel, mockFavoritesRepository, mockFaviconManager, coroutineRule.testDispatcherProvider) testee.resetViewState() testee.userUpdatedQuery("test") @@ -152,10 +157,10 @@ class SystemSearchViewModelTest { fun whenUserUpdatesQueryThenViewStateUpdated() = coroutineRule.runBlocking { testee.userUpdatedQuery(QUERY) - val newViewState = testee.resultsViewState.value + val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState assertNotNull(newViewState) - assertEquals(appQueryResult, newViewState?.appResults) - assertEquals(autocompleteQueryResult, newViewState?.autocompleteResults) + assertEquals(appQueryResult, newViewState.appResults) + assertEquals(autocompleteQueryResult, newViewState.autocompleteResults) } @Test @@ -163,10 +168,10 @@ class SystemSearchViewModelTest { testee.userUpdatedQuery(QUERY) testee.userUpdatedQuery("$QUERY ") - val newViewState = testee.resultsViewState.value + val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState assertNotNull(newViewState) - assertEquals(appQueryResult, newViewState?.appResults) - assertEquals(autocompleteQueryResult, newViewState?.autocompleteResults) + assertEquals(appQueryResult, newViewState.appResults) + assertEquals(autocompleteQueryResult, newViewState.autocompleteResults) } @Test @@ -174,7 +179,7 @@ class SystemSearchViewModelTest { testee.userUpdatedQuery(QUERY) testee.userRequestedClear() - val newViewState = testee.resultsViewState.value + val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState assertNotNull(newViewState) assertTrue(newViewState!!.appResults.isEmpty()) assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults) @@ -185,7 +190,7 @@ class SystemSearchViewModelTest { testee.userUpdatedQuery(QUERY) testee.userUpdatedQuery(BLANK_QUERY) - val newViewState = testee.resultsViewState.value + val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState assertNotNull(newViewState) assertTrue(newViewState!!.appResults.isEmpty()) assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults) From a37529fb74bb5117929f47d280f0cfd1a480373a Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 17 May 2021 18:08:16 +0200 Subject: [PATCH 78/95] unit test new logic in SystemSearchViewModelTest --- .../systemsearch/SystemSearchViewModelTest.kt | 81 +++++++++++++++++-- .../app/systemsearch/SystemSearchViewModel.kt | 22 +++-- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index 3635e26bb92a..12d8b51cd085 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -25,7 +25,9 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessFavorite import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.pixels.Pixel @@ -35,6 +37,7 @@ import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchDuckD import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Suggestions.SystemSearchResultsViewState import com.nhaarman.mockitokotlin2.* import io.reactivex.Observable +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.runBlockingTest import org.junit.After @@ -74,6 +77,7 @@ class SystemSearchViewModelTest { whenever(mockAutoComplete.autoComplete(BLANK_QUERY)).thenReturn(Observable.just(autocompleteBlankResult)) whenever(mockDeviceAppLookup.query(QUERY)).thenReturn(appQueryResult) whenever(mockDeviceAppLookup.query(BLANK_QUERY)).thenReturn(appBlankResult) + whenever(mockFavoritesRepository.favorites()).thenReturn(flowOf()) testee = SystemSearchViewModel(mockUserStageStore, mockAutoComplete, mockDeviceAppLookup, mockPixel, mockFavoritesRepository, mockFaviconManager, coroutineRule.testDispatcherProvider) testee.command.observeForever(commandObserver) } @@ -179,10 +183,7 @@ class SystemSearchViewModelTest { testee.userUpdatedQuery(QUERY) testee.userRequestedClear() - val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState - assertNotNull(newViewState) - assertTrue(newViewState!!.appResults.isEmpty()) - assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults) + assertTrue(testee.resultsViewState.value is SystemSearchViewModel.Suggestions.QuickAccessItems) } @Test @@ -190,10 +191,7 @@ class SystemSearchViewModelTest { testee.userUpdatedQuery(QUERY) testee.userUpdatedQuery(BLANK_QUERY) - val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState - assertNotNull(newViewState) - assertTrue(newViewState!!.appResults.isEmpty()) - assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults) + assertTrue(testee.resultsViewState.value is SystemSearchViewModel.Suggestions.QuickAccessItems) } @Test @@ -276,6 +274,73 @@ class SystemSearchViewModelTest { assertEquals(Command.EditQuery(query), commandCaptor.lastValue) } + @Test + fun whenQuickAccessItemClickedThenLaunchBrowser() { + val quickAccessItem = QuickAccessFavorite(Favorite(1, "title", "http://example.com", 0)) + + testee.onQuickAccesItemClicked(quickAccessItem) + + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertEquals(Command.LaunchBrowser(quickAccessItem.favorite.url), commandCaptor.lastValue) + } + + @Test + fun whenQuickAccessItemEditRequestedThenLaunchEditDialog() { + val quickAccessItem = QuickAccessFavorite(Favorite(1, "title", "http://example.com", 0)) + + testee.onEditQuickAccessItemRequested(quickAccessItem) + + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertEquals(Command.LaunchEditDialog(quickAccessItem.favorite), commandCaptor.lastValue) + } + + @Test + fun whenQuickAccessItemDeleteRequestedThenShowDeleteConfirmation() { + val quickAccessItem = QuickAccessFavorite(Favorite(1, "title", "http://example.com", 0)) + + testee.onDeleteQuickAccessItemRequested(quickAccessItem) + + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertEquals(Command.DeleteSavedSiteConfirmation(quickAccessItem.favorite), commandCaptor.lastValue) + } + + @Test + fun whenQuickAccessEditedThenRepositoryUpdated() { + val savedSite = Favorite(1, "title", "http://example.com", 0) + + testee.onSavedSiteEdited(savedSite) + + verify(mockFavoritesRepository).update(savedSite) + } + + @Test + fun whenQuickAccessDeletedThenRepositoryUpdated() { + val savedSite = Favorite(1, "title", "http://example.com", 0) + + testee.deleteQuickAccessItem(savedSite) + + verify(mockFavoritesRepository).delete(savedSite) + } + + @Test + fun whenQuickAccessInsertedThenRepositoryUpdated() { + val savedSite = Favorite(1, "title", "http://example.com", 0) + + testee.insertQuickAccessItem(savedSite) + + verify(mockFavoritesRepository).insert(savedSite) + } + + @Test + fun whenQuickAccessListChangedThenRepositoryUpdated() { + val savedSite = Favorite(1, "title", "http://example.com", 0) + val savedSites = listOf(QuickAccessFavorite(savedSite)) + + testee.onQuickAccessListChanged(savedSites) + + verify(mockFavoritesRepository).updateWithPosition(listOf(savedSite)) + } + private suspend fun whenOnboardingShowing() { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.NEW) testee.resetViewState() diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index 09f143dfb25e..fc13057448a0 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -317,17 +317,25 @@ class SystemSearchViewModel( } fun deleteQuickAccessItem(savedSite: SavedSite) { - val favorite = savedSite as? SavedSite.Favorite ?: return - viewModelScope.launch(dispatchers.io() + NonCancellable) { - faviconManager.deletePersistedFavicon(savedSite.url) - favoritesRepository.delete(favorite) + when (savedSite) { + is SavedSite.Favorite -> { + viewModelScope.launch(dispatchers.io() + NonCancellable) { + faviconManager.deletePersistedFavicon(savedSite.url) + favoritesRepository.delete(savedSite) + } + } + else -> throw IllegalArgumentException("Illegal SavedSite to delete received") } } fun insertQuickAccessItem(savedSite: SavedSite) { - val favorite = savedSite as? SavedSite.Favorite ?: return - viewModelScope.launch(dispatchers.io()) { - favoritesRepository.insert(favorite) + when (savedSite) { + is SavedSite.Favorite -> { + viewModelScope.launch(dispatchers.io()) { + favoritesRepository.insert(savedSite) + } + } + else -> throw IllegalArgumentException("Illegal SavedSite to delete received") } } } From 66d79da378a52de19bd1e6a2795f9d06fe180a81 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 17 May 2021 18:14:54 +0200 Subject: [PATCH 79/95] add test for initial state --- .../app/systemsearch/SystemSearchViewModelTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index 12d8b51cd085..7d7433bc8fe3 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -341,6 +341,17 @@ class SystemSearchViewModelTest { verify(mockFavoritesRepository).updateWithPosition(listOf(savedSite)) } + @Test + fun whenUserHasFavoritesThenInitialStateShowsFavorites() { + val savedSite = Favorite(1, "title", "http://example.com", 0) + whenever(mockFavoritesRepository.favorites()).thenReturn(flowOf(listOf(savedSite))) + testee = SystemSearchViewModel(mockUserStageStore, mockAutoComplete, mockDeviceAppLookup, mockPixel, mockFavoritesRepository, mockFaviconManager, coroutineRule.testDispatcherProvider) + + val viewState = testee.resultsViewState.value as SystemSearchViewModel.Suggestions.QuickAccessItems + assertEquals(1, viewState.favorites.size) + assertEquals(savedSite, viewState.favorites.first().favorite) + } + private suspend fun whenOnboardingShowing() { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.NEW) testee.resetViewState() From 37fd84056e1d5ff98751dc06ab23285ec986a821 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 18 May 2021 14:54:38 +0200 Subject: [PATCH 80/95] fix padding for quick access grid on autocomplete --- app/src/main/res/layout/fragment_browser_tab.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index 5eb777246bd0..8ed0883f089e 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -54,6 +54,7 @@ android:layout_height="match_parent" android:background="?toolbarBgColor" android:backgroundTint="?toolbarBgColor" + android:paddingTop="15dp" android:clipToPadding="false" android:elevation="4dp" android:visibility="gone" From b9ce0fec5cdf27e25a73fbdfa6315981d12104b3 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 18 May 2021 14:55:12 +0200 Subject: [PATCH 81/95] Address product review feedback around autocomplete --- .../app/browser/BrowserTabFragment.kt | 12 +++++----- .../app/browser/BrowserTabViewModel.kt | 22 +++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index a9361c59865e..1d15f992ce24 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1793,15 +1793,15 @@ class BrowserTabFragment : renderIfChanged(viewState, lastSeenAutoCompleteViewState) { lastSeenAutoCompleteViewState = viewState - if (viewState.showSuggestions) { - if (viewState.searchResults.suggestions.isNotEmpty()) { - autoCompleteSuggestionsList.show() - quickAccessSuggestionsRecyclerView.gone() - autoCompleteSuggestionsAdapter.updateData(viewState.searchResults.query, viewState.searchResults.suggestions) - } else if (viewState.favorites.isNotEmpty()) { + if (viewState.showSuggestions || viewState.showFavorites) { + if (viewState.favorites.isNotEmpty() && viewState.showFavorites) { autoCompleteSuggestionsList.gone() quickAccessSuggestionsRecyclerView.show() omnibarQuickAccessAdapter.submitList(viewState.favorites) + } else { + autoCompleteSuggestionsList.show() + quickAccessSuggestionsRecyclerView.gone() + autoCompleteSuggestionsAdapter.updateData(viewState.searchResults.query, viewState.searchResults.suggestions) } } else { autoCompleteSuggestionsList.gone() diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 416e8ef76783..dcdd77c1a001 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -244,6 +244,7 @@ class BrowserTabViewModel( data class AutoCompleteViewState( val showSuggestions: Boolean = false, + val showFavorites: Boolean = false, val searchResults: AutoCompleteResult = AutoCompleteResult("", emptyList()), val favorites: List = emptyList() ) @@ -592,7 +593,7 @@ class BrowserTabViewModel( findInPageViewState.value = FindInPageViewState(visible = false, canFindInPage = true) omnibarViewState.value = currentOmnibarViewState().copy(omnibarText = trimmedInput, shouldMoveCaretToEnd = false) browserViewState.value = currentBrowserViewState().copy(browserShowing = true, showClearButton = false) - autoCompleteViewState.value = currentAutoCompleteViewState().copy(showSuggestions = false, searchResults = AutoCompleteResult("", emptyList())) + autoCompleteViewState.value = currentAutoCompleteViewState().copy(showSuggestions = false, showFavorites = false, searchResults = AutoCompleteResult("", emptyList())) } private fun getUrlHeaders(): Map = globalPrivacyControl.getHeaders() @@ -1306,13 +1307,15 @@ class BrowserTabViewModel( currentAutoCompleteViewState().searchResults } - val favoritesAvailable = currentAutoCompleteViewState().favorites.isNotEmpty() - - val currentOmnibarViewState = currentOmnibarViewState() val autoCompleteSuggestionsEnabled = appSettingsPreferencesStore.autoCompleteSuggestionsEnabled - var showAutoCompleteSuggestions = hasFocus && query.isNotBlank() && hasQueryChanged && autoCompleteSuggestionsEnabled - if (!showAutoCompleteSuggestions) { - showAutoCompleteSuggestions = hasFocus && query.isNotBlank() && !hasQueryChanged && favoritesAvailable && autoCompleteSuggestionsEnabled + val showAutoCompleteSuggestions = hasFocus && query.isNotBlank() && hasQueryChanged && autoCompleteSuggestionsEnabled + val showFavoritesAsSuggestions= if (!showAutoCompleteSuggestions) { + val urlFocused = hasFocus && query.isNotBlank() && !hasQueryChanged && UriString.isWebUrl(query) + val emptyQueryBrowsing = query.isBlank() && currentBrowserViewState().browserShowing + val favoritesAvailable = currentAutoCompleteViewState().favorites.isNotEmpty() + hasFocus && (urlFocused || emptyQueryBrowsing) && favoritesAvailable + } else { + false } val showClearButton = hasFocus && query.isNotBlank() val showControls = !hasFocus || query.isBlank() @@ -1324,7 +1327,7 @@ class BrowserTabViewModel( privacyGradeViewState.value = currentPrivacyGradeState().copy(showEmptyGrade = false) } - omnibarViewState.value = currentOmnibarViewState.copy(isEditing = hasFocus) + omnibarViewState.value = currentOmnibarViewState().copy(isEditing = hasFocus) val currentBrowserViewState = currentBrowserViewState() browserViewState.value = currentBrowserViewState.copy( @@ -1343,7 +1346,8 @@ class BrowserTabViewModel( Timber.d("showPrivacyGrade=$showPrivacyGrade, showSearchIcon=$showSearchIcon, showClearButton=$showClearButton") - autoCompleteViewState.value = currentAutoCompleteViewState().copy(showSuggestions = showAutoCompleteSuggestions, searchResults = autoCompleteSearchResults) + autoCompleteViewState.value = currentAutoCompleteViewState() + .copy(showSuggestions = showAutoCompleteSuggestions, showFavorites = showFavoritesAsSuggestions, searchResults = autoCompleteSearchResults) if (hasQueryChanged && hasFocus && autoCompleteSuggestionsEnabled) { autoCompletePublishSubject.accept(query.trim()) From fbda7339691f6f500467ccbc5706fcba7bf5a1e5 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Tue, 18 May 2021 15:46:12 +0200 Subject: [PATCH 82/95] fix margins on 2 lines title for grid quick access items --- app/src/main/res/layout/view_quick_access_item.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/view_quick_access_item.xml b/app/src/main/res/layout/view_quick_access_item.xml index 40c4ae8dbf72..905462fe82e0 100644 --- a/app/src/main/res/layout/view_quick_access_item.xml +++ b/app/src/main/res/layout/view_quick_access_item.xml @@ -54,8 +54,8 @@ android:ellipsize="end" android:gravity="center" android:maxLines="2" - android:paddingStart="6dp" - android:paddingEnd="6dp" + android:paddingStart="11dp" + android:paddingEnd="11dp" android:paddingTop="8dp" android:textColor="?normalTextColor" android:textSize="12sp" From b01a23b33942a62928beb93e150f7d34d0cc9cff Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 19 May 2021 12:56:17 +0200 Subject: [PATCH 83/95] test new changes from product review --- .../com/duckduckgo/app/browser/BrowserTabViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index f63d4c3e6046..575888cf7975 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -1102,12 +1102,12 @@ class BrowserTabViewModelTest { } @Test - fun whenOmnibarFocusedAndUserHasFavoritesThenAutoCompleteShowsFavorites() { + fun whenOmnibarFocusedWithUrlAndUserHasFavoritesThenAutoCompleteShowsFavorites() { testee.autoCompleteViewState.value = autoCompleteViewState().copy(favorites = listOf(QuickAccessFavorite(Favorite(1, "title", "http://example.com", 1)))) doReturn(true).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled - testee.onOmnibarInputStateChanged("foo", true, hasQueryChanged = false) - assertTrue(autoCompleteViewState().showSuggestions) - assertTrue(autoCompleteViewState().searchResults.suggestions.isEmpty()) + testee.onOmnibarInputStateChanged("https://example.com", true, hasQueryChanged = false) + assertFalse(autoCompleteViewState().showSuggestions) + assertTrue(autoCompleteViewState().showFavorites) } @Test From 86ceb79d84971f8104226dab0928d806a33ebc6a Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 19 May 2021 13:05:07 +0200 Subject: [PATCH 84/95] removed unnecessary logs --- .../app/browser/BrowserTabFragment.kt | 3 --- .../app/browser/BrowserTabViewModel.kt | 5 +---- .../app/browser/favicon/FaviconManager.kt | 17 +---------------- .../app/browser/favicon/FaviconPersister.kt | 3 --- .../favorites/FavoritesQuickAccessAdapter.kt | 6 +----- .../QuickAccessDragTouchItemListener.kt | 2 -- .../app/systemsearch/SystemSearchActivity.kt | 7 ++----- .../app/systemsearch/SystemSearchViewModel.kt | 1 - 8 files changed, 5 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 1d15f992ce24..a8c2ff0bc85c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1998,7 +1998,6 @@ class BrowserTabFragment : } renderIfChanged(viewState, lastSeenCtaViewState) { - Timber.i("BrowserTab favs: renderIfChanged $viewState") lastSeenCtaViewState = viewState removeNewTabLayoutClickListener() if (viewState.cta != null) { @@ -2060,7 +2059,6 @@ class BrowserTabFragment : } private fun showHomeBackground(favorites: List) { - Timber.i("BrowserTab favs: showHomeBackground $favorites") if (favorites.isEmpty()) { homeBackgroundLogo.showLogo() quickAccessRecyclerView.visibility = GONE @@ -2072,7 +2070,6 @@ class BrowserTabFragment : } private fun hideHomeBackground() { - Timber.i("BrowserTab favs: hideHomeBackground") homeBackgroundLogo.hideLogo() quickAccessRecyclerView.visibility = GONE } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index dcdd77c1a001..210ced9bf5aa 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -431,7 +431,6 @@ class BrowserTabViewModel( viewModelScope.launch { favoritesRepository.favorites().collect { favorite -> - Timber.i("BrowserTab favs: collect $favorite") val favorites = favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) } ctaViewState.value = currentCtaViewState().copy(favorites = favorites) autoCompleteViewState.value = currentAutoCompleteViewState().copy(favorites = favorites) @@ -644,7 +643,6 @@ class BrowserTabViewModel( override fun prefetchFavicon(url: String) { faviconPrefetchJob?.cancel() faviconPrefetchJob = viewModelScope.launch { - Timber.i("Favicon prefetch for $url") val faviconFile = faviconManager.tryFetchFaviconForUrl(tabId = tabId, url = url) if (faviconFile != null) { tabRepository.updateTabFavicon(tabId, faviconFile.name) @@ -1309,7 +1307,7 @@ class BrowserTabViewModel( val autoCompleteSuggestionsEnabled = appSettingsPreferencesStore.autoCompleteSuggestionsEnabled val showAutoCompleteSuggestions = hasFocus && query.isNotBlank() && hasQueryChanged && autoCompleteSuggestionsEnabled - val showFavoritesAsSuggestions= if (!showAutoCompleteSuggestions) { + val showFavoritesAsSuggestions = if (!showAutoCompleteSuggestions) { val urlFocused = hasFocus && query.isNotBlank() && !hasQueryChanged && UriString.isWebUrl(query) val emptyQueryBrowsing = query.isBlank() && currentBrowserViewState().browserShowing val favoritesAvailable = currentAutoCompleteViewState().favorites.isNotEmpty() @@ -1987,7 +1985,6 @@ class BrowserTabViewModel( fun onQuickAccessListChanged(newList: List) { viewModelScope.launch(dispatchers.io()) { - Timber.i("Persist favorites $newList") favoritesRepository.updateWithPosition(newList.map { it.favorite }) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt index ed5d4b4f92c1..82a0dc9adf39 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconManager.kt @@ -33,7 +33,6 @@ import com.duckduckgo.app.global.touchFaviconLocation import com.duckduckgo.app.global.view.loadFavicon import com.duckduckgo.app.location.data.LocationPermissionsRepository import kotlinx.coroutines.withContext -import timber.log.Timber import java.io.File interface FaviconManager { @@ -69,7 +68,6 @@ class DuckDuckGoFaviconManager constructor( is FaviconSource.ImageFavicon -> { val domain = faviconSource.url.extractDomain() ?: return null invalidateCacheIfNewDomain(tabId, domain) - Timber.i("Favicon received for ${faviconSource.url}") Pair(domain, faviconSource.icon) } is FaviconSource.UrlFavicon -> { @@ -78,7 +76,6 @@ class DuckDuckGoFaviconManager constructor( if (shouldSkipNetworkRequest(tabId, faviconSource)) return null val bitmap = faviconDownloader.getFaviconFromUrl(faviconSource.faviconUrl.toUri()) ?: return null addFaviconUrlToCache(tabId, faviconSource) - Timber.i("Favicon downloaded for $domain from ${faviconSource.faviconUrl}") Pair(domain, bitmap) } } @@ -92,10 +89,8 @@ class DuckDuckGoFaviconManager constructor( val favicon = downloadFaviconFor(domain) return if (favicon != null) { - Timber.i("Favicon downloaded for $domain") - return saveFavicon(tabId, favicon, domain) + saveFavicon(tabId, favicon, domain) } else { - Timber.i("Favicon downloaded null for $domain") null } } @@ -106,15 +101,9 @@ class DuckDuckGoFaviconManager constructor( var cachedFavicon: File? = null if (tabId != null) { cachedFavicon = faviconPersister.faviconFile(FAVICON_TEMP_DIR, tabId, domain) - if (cachedFavicon != null) { - Timber.i("Favicon loaded from temp") - } } if (cachedFavicon == null) { cachedFavicon = faviconPersister.faviconFile(FAVICON_PERSISTED_DIR, NO_SUBFOLDER, domain) - if (cachedFavicon != null) { - Timber.i("Favicon loaded from persisted") - } } return if (cachedFavicon != null) { @@ -126,7 +115,6 @@ class DuckDuckGoFaviconManager constructor( val bitmap = loadFromDisk(tabId, url) if (bitmap == null) { - Timber.i("No favicon loaded") view.loadFavicon(bitmap, url) val domain = url.extractDomain() ?: return tryRemoteFallbackFavicon(subFolder = tabId, domain)?.let { @@ -175,12 +163,9 @@ class DuckDuckGoFaviconManager constructor( private suspend fun downloadFaviconFor(domain: String): Bitmap? { val faviconUrl = getFaviconUrl(domain) ?: return null val touchFaviconUrl = getTouchFaviconUrl(domain) ?: return null - Timber.i("Favicon will try on $faviconUrl or $touchFaviconUrl") faviconDownloader.getFaviconFromUrl(touchFaviconUrl)?.let { - Timber.i("Favicon downloaded from $touchFaviconUrl") return it } ?: faviconDownloader.getFaviconFromUrl(faviconUrl).let { - Timber.i("Favicon downloaded from $faviconUrl") return it } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt index 711832364ad1..a08b4193d3b9 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favicon/FaviconPersister.kt @@ -110,9 +110,7 @@ class FileBasedFaviconPersister( val existingFavicon = BitmapFactory.decodeFile(existingFile.absolutePath) existingFavicon?.let { - Timber.i("Favicon exists with size: ${it.width} x ${it.height}") if (it.width > bitmap.width) { - Timber.i("Favicon dicarded") return null // Stored file has better quality } } @@ -120,7 +118,6 @@ class FileBasedFaviconPersister( val faviconFile = prepareDestinationFile(directory, subFolder, domain) writeBytesToFile(faviconFile, bitmap) - Timber.i("Favicon favicon stored for $domain size: ${bitmap.width} x ${bitmap.height}") return if (faviconFile.exists()) { faviconFile diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt index 159fa680105e..c60abc00a82c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/FavoritesQuickAccessAdapter.kt @@ -104,14 +104,11 @@ class FavoritesQuickAccessAdapter( itemView.quickAccessFaviconCard.setOnTouchListener { v, event -> when (event.actionMasked) { MotionEvent.ACTION_MOVE -> { - Timber.i("QuickAccessFav: move!") if (itemState != ItemState.LongPress) return@setOnTouchListener false - Timber.i("QuickAccessFav: onMoveListener") onMoveListener(this@QuickAccessViewHolder) } MotionEvent.ACTION_UP -> { - Timber.i("QuickAccessFav: up!") onItemReleased() } } @@ -173,13 +170,12 @@ class FavoritesQuickAccessAdapter( } override fun onItemReleased() { - Timber.i("QuickAccessFav: onItemReleased") scaleDownFavicon() itemView.quickAccessTitle.alpha = 1f itemState = ItemState.Stale } - fun loadFavicon(url: String) { + private fun loadFavicon(url: String) { lifecycleOwner.lifecycleScope.launch { faviconManager.loadToViewFromLocalOrFallback(url = url, view = itemView.quickAccessFavicon) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt b/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt index f131bdf9e34a..558931ffcf76 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/favorites/QuickAccessDragTouchItemListener.kt @@ -20,7 +20,6 @@ import android.graphics.Canvas import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.browser.favorites.FavoritesQuickAccessAdapter.QuickAccessFavorite -import timber.log.Timber class QuickAccessDragTouchItemListener( private val favoritesQuickAccessAdapter: FavoritesQuickAccessAdapter, @@ -38,7 +37,6 @@ class QuickAccessDragTouchItemListener( } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { - Timber.v("QuickAccessDragTouchItemListener onMove ${viewHolder.bindingAdapterPosition} to ${target.bindingAdapterPosition}") val items = favoritesQuickAccessAdapter.currentList.toMutableList() val quickAccessFavorite = items[viewHolder.bindingAdapterPosition] items.removeAt(viewHolder.bindingAdapterPosition) diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index 56599227d4c1..a67024587ee2 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -28,7 +28,6 @@ import android.widget.TextView import android.widget.Toast import android.widget.Toast.LENGTH_SHORT import androidx.core.view.isVisible -import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -62,7 +61,6 @@ import kotlinx.android.synthetic.main.activity_system_search.results import kotlinx.android.synthetic.main.activity_system_search.resultsContent import kotlinx.android.synthetic.main.include_quick_access_items.* import kotlinx.android.synthetic.main.include_system_search_onboarding.* -import timber.log.Timber import javax.inject.Inject class SystemSearchActivity : DuckDuckGoActivity() { @@ -132,14 +130,13 @@ class SystemSearchActivity : DuckDuckGoActivity() { private fun configureObservers() { viewModel.onboardingViewState.observe( this, - Observer { + { it?.let { renderOnboardingViewState(it) } } ) viewModel.resultsViewState.observe( this, { - Timber.i("SystemSearchActivity d: $it") when (it) { is SystemSearchViewModel.Suggestions.SystemSearchResultsViewState -> { renderResultsViewState(it) @@ -152,7 +149,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { ) viewModel.command.observe( this, - Observer { + { processCommand(it) } ) diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index fc13057448a0..c2d8fccd88b6 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -283,7 +283,6 @@ class SystemSearchViewModel( fun onQuickAccessListChanged(newList: List) { viewModelScope.launch(dispatchers.io()) { - Timber.i("Persist favorites $newList") favoritesRepository.updateWithPosition(newList.map { it.favorite }) } } From c5b49560848f92ad4ab1b16c58c77c95d4fdff51 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 10:52:16 +0200 Subject: [PATCH 85/95] remove unnecessary coroutine blocks from unit tests --- .../model/FavoritesDataRepositoryTest.kt | 20 ++++++++----------- .../favicon/DuckDuckGoFaviconManagerTest.kt | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt index ca8533558281..b12b5503b8dd 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt @@ -37,10 +37,6 @@ class FavoritesDataRepositoryTest { @Suppress("unused") var instantTaskExecutorRule = InstantTaskExecutorRule() - @get:Rule - @Suppress("unused") - val coroutineRule = CoroutineTestRule() - private lateinit var db: AppDatabase private lateinit var favoritesDao: FavoritesDao private lateinit var repository: FavoritesRepository @@ -55,7 +51,7 @@ class FavoritesDataRepositoryTest { } @Test - fun whenInsertFavoriteThenReturnSavedSite() = coroutineRule.runBlocking { + fun whenInsertFavoriteThenReturnSavedSite() { givenNoFavoritesStored() val savedSite = repository.insert("title", "http://example.com") @@ -66,7 +62,7 @@ class FavoritesDataRepositoryTest { } @Test - fun whenInsertFavoriteWithoutTitleThenSavedSiteUsesUrlAsTitle() = coroutineRule.runBlocking { + fun whenInsertFavoriteWithoutTitleThenSavedSiteUsesUrlAsTitle() { givenNoFavoritesStored() val savedSite = repository.insert("", "http://example.com") @@ -77,7 +73,7 @@ class FavoritesDataRepositoryTest { } @Test - fun whenUserHasFavoritesAndInsertFavoriteThenSavedSiteUsesNextPosition() = coroutineRule.runBlocking { + fun whenUserHasFavoritesAndInsertFavoriteThenSavedSiteUsesNextPosition() { givenMoreFavoritesStored() val savedSite = repository.insert("Favorite", "http://favexample.com") @@ -88,7 +84,7 @@ class FavoritesDataRepositoryTest { } @Test - fun whenDataSourceChangesThenNewListReceived() = coroutineRule.runBlocking { + fun whenDataSourceChangesThenNewListReceived() { givenNoFavoritesStored() repository.insert("Favorite", "http://favexample.com") @@ -100,7 +96,7 @@ class FavoritesDataRepositoryTest { } @Test - fun whenFavoriteUpdatedThenDatabaseChanged() = coroutineRule.runBlocking { + fun whenFavoriteUpdatedThenDatabaseChanged() { val favorite = Favorite(1, "Favorite", "http://favexample.com", 1) givenFavorite(favorite) val updatedFavorite = favorite.copy(position = 3) @@ -111,7 +107,7 @@ class FavoritesDataRepositoryTest { } @Test - fun whenListReceivedThenUpdateItemsWithNewPositionInDatabase() = coroutineRule.runBlocking { + fun whenListReceivedThenUpdateItemsWithNewPositionInDatabase() { val favorite = Favorite(1, "Favorite", "http://favexample.com", 1) val favorite2 = Favorite(2, "Favorite2", "http://favexample2.com", 2) givenFavorite(favorite, favorite2) @@ -123,7 +119,7 @@ class FavoritesDataRepositoryTest { } @Test - fun whenFavoriteDeletedThenDatabaseUpdated() = coroutineRule.runBlocking { + fun whenFavoriteDeletedThenDatabaseUpdated() { val favorite = Favorite(1, "Favorite", "http://favexample.com", 1) givenFavorite(favorite) @@ -143,7 +139,7 @@ class FavoritesDataRepositoryTest { favoritesDao.insert(FavoriteEntity(title = "title 2", url = "http://other.com", position = 1)) } - private suspend fun givenNoFavoritesStored() { + private fun givenNoFavoritesStored() { assertNull(favoritesDao.getLastPosition()) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt index 3369818df0d9..2f8c1b272fe6 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/favicon/DuckDuckGoFaviconManagerTest.kt @@ -60,7 +60,7 @@ class DuckDuckGoFaviconManagerTest { private lateinit var testee: FaviconManager @Before - fun setup() = coroutineRule.runBlocking { + fun setup() { whenever(mockFavoriteRepository.favoritesCountByDomain(any())).thenReturn(0) testee = DuckDuckGoFaviconManager( mockFaviconPersister, From 49cc54ae047757972d8a768f509544b7ec242bb9 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 10:52:30 +0200 Subject: [PATCH 86/95] add verify to ensure favicon manager removes favicon --- .../duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index 7d7433bc8fe3..5beebbaafb54 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -314,12 +314,13 @@ class SystemSearchViewModelTest { } @Test - fun whenQuickAccessDeletedThenRepositoryUpdated() { + fun whenQuickAccessDeletedThenRepositoryUpdated() = coroutineRule.runBlocking { val savedSite = Favorite(1, "title", "http://example.com", 0) testee.deleteQuickAccessItem(savedSite) verify(mockFavoritesRepository).delete(savedSite) + verify(mockFaviconManager).deletePersistedFavicon(savedSite.url) } @Test From 3c7a63f063527e55628ac6f86e051e0592c23e70 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 11:21:09 +0200 Subject: [PATCH 87/95] Move logic to remove favicons when removing favorites inside FavoritesRepository --- .../model/FavoritesDataRepositoryTest.kt | 15 +++++++++++++-- .../app/bookmarks/ui/BookmarksViewModelTest.kt | 1 - .../app/browser/BrowserTabViewModelTest.kt | 2 +- .../app/systemsearch/SystemSearchViewModelTest.kt | 1 - .../app/bookmarks/di/BookmarksModule.kt | 6 ++++-- .../app/bookmarks/model/FavoritesRepository.kt | 12 +++++++++--- .../app/bookmarks/ui/BookmarksViewModel.kt | 1 - .../duckduckgo/app/browser/BrowserTabViewModel.kt | 1 - .../app/systemsearch/SystemSearchViewModel.kt | 1 - 9 files changed, 27 insertions(+), 13 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt index b12b5503b8dd..2a693aa6241f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/model/FavoritesDataRepositoryTest.kt @@ -23,13 +23,17 @@ import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.bookmarks.db.FavoriteEntity import com.duckduckgo.app.bookmarks.db.FavoritesDao import com.duckduckgo.app.bookmarks.model.SavedSite.Favorite +import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.runBlocking +import com.nhaarman.mockitokotlin2.mock import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test +import dagger.Lazy +import org.mockito.Mockito.verify @ExperimentalCoroutinesApi class FavoritesDataRepositoryTest { @@ -37,6 +41,12 @@ class FavoritesDataRepositoryTest { @Suppress("unused") var instantTaskExecutorRule = InstantTaskExecutorRule() + @get:Rule + @Suppress("unused") + val coroutineRule = CoroutineTestRule() + + private val mockFaviconManager: FaviconManager = mock() + private val lazyFaviconManager = Lazy { mockFaviconManager } private lateinit var db: AppDatabase private lateinit var favoritesDao: FavoritesDao private lateinit var repository: FavoritesRepository @@ -47,7 +57,7 @@ class FavoritesDataRepositoryTest { .allowMainThreadQueries() .build() favoritesDao = db.favoritesDao() - repository = FavoritesDataRepository(favoritesDao) + repository = FavoritesDataRepository(favoritesDao, lazyFaviconManager) } @Test @@ -119,13 +129,14 @@ class FavoritesDataRepositoryTest { } @Test - fun whenFavoriteDeletedThenDatabaseUpdated() { + fun whenFavoriteDeletedThenDatabaseUpdated() = coroutineRule.runBlocking { val favorite = Favorite(1, "Favorite", "http://favexample.com", 1) givenFavorite(favorite) repository.delete(favorite) assertNull(favoritesDao.favorite(favorite.id)) + verify(mockFaviconManager).deletePersistedFavicon(favorite.url) } private fun givenFavorite(vararg favorite: Favorite) { 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 index 0ce1892ab0e0..7cb1a42f0165 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt @@ -116,7 +116,6 @@ class BookmarksViewModelTest { fun whenFavoriteDeletedThenDeleteFromRepository() = coroutineRule.runBlocking { testee.delete(favorite) - verify(faviconManager).deletePersistedFavicon(favorite.url) verify(favoritesRepository).delete(favorite) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 33121cc6a6cb..7ef919a48cf5 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -592,7 +592,7 @@ class BrowserTabViewModelTest { } @Test - fun whenQuickAccessDeletedThenRepositoryUpdated() { + fun whenQuickAccessDeletedThenRepositoryUpdated() = coroutineRule.runBlocking { val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) testee.deleteQuickAccessItem(savedSite) diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index 5beebbaafb54..ac53f1b19230 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -320,7 +320,6 @@ class SystemSearchViewModelTest { testee.deleteQuickAccessItem(savedSite) verify(mockFavoritesRepository).delete(savedSite) - verify(mockFaviconManager).deletePersistedFavicon(savedSite.url) } @Test diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt index 81ba48ba2164..33496a7924cc 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/di/BookmarksModule.kt @@ -31,8 +31,10 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.bookmarks.db.FavoritesDao import com.duckduckgo.app.bookmarks.model.FavoritesDataRepository import com.duckduckgo.app.bookmarks.model.FavoritesRepository +import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.di.scopes.AppObjectGraph import com.squareup.anvil.annotations.ContributesTo +import dagger.Lazy import dagger.Module import dagger.Provides import javax.inject.Singleton @@ -80,7 +82,7 @@ class BookmarksModule { @Provides @Singleton - fun favoriteRepository(favoritesDao: FavoritesDao): FavoritesRepository { - return FavoritesDataRepository(favoritesDao) + fun favoriteRepository(favoritesDao: FavoritesDao, faviconManager: Lazy): FavoritesRepository { + return FavoritesDataRepository(favoritesDao, faviconManager) } } diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt index 6f9152e2edfc..05915a1a0e53 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/model/FavoritesRepository.kt @@ -18,6 +18,8 @@ package com.duckduckgo.app.bookmarks.model import com.duckduckgo.app.bookmarks.db.FavoriteEntity import com.duckduckgo.app.bookmarks.db.FavoritesDao +import com.duckduckgo.app.browser.favicon.FaviconManager +import dagger.Lazy import io.reactivex.Single import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -32,7 +34,7 @@ interface FavoritesRepository { fun update(favorite: SavedSite.Favorite) fun updateWithPosition(favorites: List) fun favorites(): Flow> - fun delete(favorite: SavedSite.Favorite) + suspend fun delete(favorite: SavedSite.Favorite) } sealed class SavedSite( @@ -54,7 +56,10 @@ sealed class SavedSite( ) : SavedSite(id, title, url) } -class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : FavoritesRepository { +class FavoritesDataRepository( + private val favoritesDao: FavoritesDao, + private val faviconManager: Lazy, +) : FavoritesRepository { override fun favoritesCountByDomain(domain: String): Int { return favoritesDao.favoritesCountByUrl(domain) } @@ -89,7 +94,8 @@ class FavoritesDataRepository(private val favoritesDao: FavoritesDao) : Favorite return favoritesDao.favorites().distinctUntilChanged().map { favorites -> favorites.mapToSavedSites() } } - override fun delete(favorite: SavedSite.Favorite) { + override suspend fun delete(favorite: SavedSite.Favorite) { + faviconManager.get().deletePersistedFavicon(favorite.url) favoritesDao.delete(FavoriteEntity(favorite.id, favorite.title, favorite.url, favorite.position)) } 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 index 655e6c9da05f..76062e3a3853 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -128,7 +128,6 @@ class BookmarksViewModel( } is Favorite -> { viewModelScope.launch(dispatcherProvider.io() + NonCancellable) { - faviconManager.deletePersistedFavicon(savedSite.url) favoritesRepository.delete(savedSite) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 9cbd614ca38d..bc2c86779f78 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1972,7 +1972,6 @@ class BrowserTabViewModel( fun deleteQuickAccessItem(savedSite: SavedSite) { val favorite = savedSite as? SavedSite.Favorite ?: return viewModelScope.launch(dispatchers.io() + NonCancellable) { - faviconManager.deletePersistedFavicon(savedSite.url) favoritesRepository.delete(favorite) } } diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index c2d8fccd88b6..c5b1aa64f717 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -319,7 +319,6 @@ class SystemSearchViewModel( when (savedSite) { is SavedSite.Favorite -> { viewModelScope.launch(dispatchers.io() + NonCancellable) { - faviconManager.deletePersistedFavicon(savedSite.url) favoritesRepository.delete(savedSite) } } From a8a53b2228cb25b30b1614e475dd131d22293b92 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 11:55:57 +0200 Subject: [PATCH 88/95] move delete logic inside viewmodel --- .../duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt | 8 ++++---- .../com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt | 1 - .../com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) 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 index 7cb1a42f0165..a219656bcddd 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt @@ -105,16 +105,16 @@ class BookmarksViewModelTest { } @Test - fun whenBookmarkDeletedThenDaoUpdated() = coroutineRule.runBlocking { - testee.delete(bookmark) + fun whenBookmarkDeleteRequestedThenDaoUpdated() = coroutineRule.runBlocking { + testee.onDeleteSavedSiteRequested(bookmark) verify(faviconManager).deletePersistedFavicon(bookmark.url) verify(bookmarksDao).delete(bookmarkEntity) } @Test - fun whenFavoriteDeletedThenDeleteFromRepository() = coroutineRule.runBlocking { - testee.delete(favorite) + fun whenFavoriteDeleteRequestedThenDeleteFromRepository() = coroutineRule.runBlocking { + testee.onDeleteSavedSiteRequested(favorite) verify(favoritesRepository).delete(favorite) } 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 index 160b6d77942d..cf46485a8829 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksActivity.kt @@ -210,7 +210,6 @@ class BookmarksActivity : DuckDuckGoActivity() { private fun confirmDeleteSavedSite(savedSite: SavedSite) { val message = getString(R.string.bookmarkDeleteConfirmationMessage, savedSite.title).html(this) - viewModel.delete(savedSite) Snackbar.make( bookmarkRootView, message, 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 index 76062e3a3853..f68a9a17eae5 100644 --- a/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModel.kt @@ -115,10 +115,11 @@ class BookmarksViewModel( } fun onDeleteSavedSiteRequested(savedSite: SavedSite) { + delete(savedSite) command.value = ConfirmDeleteSavedSite(savedSite) } - fun delete(savedSite: SavedSite) { + private fun delete(savedSite: SavedSite) { when (savedSite) { is Bookmark -> { viewModelScope.launch(dispatcherProvider.io() + NonCancellable) { From 906374c512e6060c706e3002d3b95f375c983be0 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 11:56:13 +0200 Subject: [PATCH 89/95] use extension functions for consistency to hide/show views --- .../java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 48ff887db572..06f4c5a8bdfc 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -2067,17 +2067,17 @@ class BrowserTabFragment : private fun showHomeBackground(favorites: List) { if (favorites.isEmpty()) { homeBackgroundLogo.showLogo() - quickAccessRecyclerView.visibility = GONE + quickAccessRecyclerView.gone() } else { homeBackgroundLogo.hideLogo() quickAccessAdapter.submitList(favorites) - quickAccessRecyclerView.visibility = VISIBLE + quickAccessRecyclerView.show() } } private fun hideHomeBackground() { homeBackgroundLogo.hideLogo() - quickAccessRecyclerView.visibility = GONE + quickAccessRecyclerView.gone() } private fun hideDaxCta() { From 2ec8c148283d928393e61f221a43461d7ec99e77 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 12:26:41 +0200 Subject: [PATCH 90/95] Refactor: use same command when savedSite added instead of 2 --- .../app/browser/BrowserTabViewModelTest.kt | 20 ++++++------- .../app/browser/BrowserTabFragment.kt | 30 +++++++------------ .../app/browser/BrowserTabViewModel.kt | 7 ++--- 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 7ef919a48cf5..75315d100cee 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -557,19 +557,19 @@ class BrowserTabViewModelTest { testee.onBookmarkAddRequested() verify(mockBookmarksDao).insert(BookmarkEntity(title = "A title", url = "www.example.com")) verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) - assertTrue(commandCaptor.lastValue is Command.ShowBookmarkAddedConfirmation) + assertTrue(commandCaptor.lastValue is Command.ShowSavedSiteAddedConfirmation) } @Test fun whenFavoriteAddedThenRepositoryUpdatedAndUserNotified() = coroutineRule.runBlocking { - val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + val savedSite = Favorite(1, "title", "http://example.com", 0) loadUrl("www.example.com", "A title") whenever(mockFavoritesRepository.insert(any(), any())).thenReturn(savedSite) testee.onAddFavoriteMenuClicked() verify(mockFavoritesRepository).insert(title = "A title", url = "www.example.com") - assertCommandIssued() + assertCommandIssued() } @Test @@ -582,7 +582,7 @@ class BrowserTabViewModelTest { @Test fun whenQuickAccessItemClickedThenSubmitNewQuery() { - val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + val savedSite = Favorite(1, "title", "http://example.com", 0) testee.onQuickAccesItemClicked(savedSite) @@ -593,7 +593,7 @@ class BrowserTabViewModelTest { @Test fun whenQuickAccessDeletedThenRepositoryUpdated() = coroutineRule.runBlocking { - val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + val savedSite = Favorite(1, "title", "http://example.com", 0) testee.deleteQuickAccessItem(savedSite) @@ -602,7 +602,7 @@ class BrowserTabViewModelTest { @Test fun whenQuickAccessInsertedThenRepositoryUpdated() { - val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + val savedSite = Favorite(1, "title", "http://example.com", 0) testee.insertQuickAccessItem(savedSite) @@ -611,7 +611,7 @@ class BrowserTabViewModelTest { @Test fun whenQuickAccessListChangedThenRepositoryUpdated() { - val savedSite = SavedSite.Favorite(1, "title", "http://example.com", 0) + val savedSite = Favorite(1, "title", "http://example.com", 0) val savedSites = listOf(QuickAccessFavorite(savedSite)) testee.onQuickAccessListChanged(savedSites) @@ -1485,9 +1485,9 @@ class BrowserTabViewModelTest { loadUrl("foo.com") testee.titleReceived("Foo Title") testee.onBookmarkAddRequested() - val command = captureCommands().value as Command.ShowBookmarkAddedConfirmation - assertEquals("foo.com", command.bookmark.url) - assertEquals("Foo Title", command.bookmark.title) + val command = captureCommands().value as Command.ShowSavedSiteAddedConfirmation + assertEquals("foo.com", command.savedSite.url) + assertEquals("Foo Title", command.savedSite.title) } @Test diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 06f4c5a8bdfc..55f3a1b34b75 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -563,8 +563,7 @@ class BrowserTabFragment : openInNewBackgroundTab() } is Command.LaunchNewTab -> browserActivity?.launchNewTab() - is Command.ShowBookmarkAddedConfirmation -> bookmarkAdded(it.bookmark) - is Command.ShowFavoriteAddedConfirmation -> favoriteAdded(it.favorite) + is Command.ShowSavedSiteAddedConfirmation -> savedSiteAdded(it.savedSite) is Command.ShowEditSavedSiteDialog -> editSavedSite(it.savedSite) is Command.DeleteSavedSiteConfirmation -> confirmDeleteSavedSite(it.savedSite) is Command.ShowFireproofWebSiteConfirmation -> fireproofWebsiteConfirmation(it.fireproofWebsiteEntity) @@ -1204,21 +1203,15 @@ class BrowserTabFragment : return super.onContextItemSelected(item) } - private fun bookmarkAdded(bookmark: SavedSite.Bookmark) { - Snackbar.make(browserLayout, R.string.bookmarkAddedMessage, Snackbar.LENGTH_LONG) - .setAction(R.string.edit) { - val addBookmarkDialog = EditSavedSiteDialogFragment.instance(bookmark) - addBookmarkDialog.show(childFragmentManager, ADD_BOOKMARK_FRAGMENT_TAG) - addBookmarkDialog.listener = viewModel - } - .show() - } - - private fun favoriteAdded(favorite: SavedSite.Favorite) { - Snackbar.make(browserLayout, R.string.favoriteAddedMessage, Snackbar.LENGTH_LONG) + private fun savedSiteAdded(savedSite: SavedSite) { + val snackbarMessage = when(savedSite) { + is SavedSite.Bookmark -> R.string.bookmarkAddedMessage + is SavedSite.Favorite -> R.string.favoriteAddedMessage + } + Snackbar.make(browserLayout, snackbarMessage, Snackbar.LENGTH_LONG) .setAction(R.string.edit) { - val addBookmarkDialog = EditSavedSiteDialogFragment.instance(favorite) - addBookmarkDialog.show(childFragmentManager, ADD_FAVORITE_FRAGMENT_TAG) + val addBookmarkDialog = EditSavedSiteDialogFragment.instance(savedSite) + addBookmarkDialog.show(childFragmentManager, ADD_SAVED_SITE_FRAGMENT_TAG) addBookmarkDialog.listener = viewModel } .show() @@ -1226,7 +1219,7 @@ class BrowserTabFragment : private fun editSavedSite(savedSite: SavedSite) { val addBookmarkDialog = EditSavedSiteDialogFragment.instance(savedSite) - addBookmarkDialog.show(childFragmentManager, ADD_FAVORITE_FRAGMENT_TAG) + addBookmarkDialog.show(childFragmentManager, ADD_SAVED_SITE_FRAGMENT_TAG) addBookmarkDialog.listener = viewModel } @@ -1566,8 +1559,7 @@ class BrowserTabFragment : private const val URL_EXTRA_ARG = "URL_EXTRA_ARG" private const val SKIP_HOME_ARG = "SKIP_HOME_ARG" - private const val ADD_BOOKMARK_FRAGMENT_TAG = "ADD_BOOKMARK" - private const val ADD_FAVORITE_FRAGMENT_TAG = "ADD_FAVORITE" + private const val ADD_SAVED_SITE_FRAGMENT_TAG = "ADD_SAVED_SITE" private const val KEYBOARD_DELAY = 200L private const val LAYOUT_TRANSITION_MS = 200L diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index bc2c86779f78..0c710a27feaf 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -269,8 +269,7 @@ class BrowserTabViewModel( object HideKeyboard : Command() class ShowFullScreen(val view: View) : Command() class DownloadImage(val url: String, val requestUserConfirmation: Boolean) : Command() - class ShowBookmarkAddedConfirmation(val bookmark: SavedSite.Bookmark) : Command() - class ShowFavoriteAddedConfirmation(val favorite: SavedSite.Favorite) : Command() + class ShowSavedSiteAddedConfirmation(val savedSite: SavedSite) : Command() class ShowEditSavedSiteDialog(val savedSite: SavedSite) : Command() class DeleteSavedSiteConfirmation(val savedSite: SavedSite) : Command() class ShowFireproofWebSiteConfirmation(val fireproofWebsiteEntity: FireproofWebsiteEntity) : Command() @@ -1365,7 +1364,7 @@ class BrowserTabViewModel( SavedSite.Bookmark(id, title, url) } withContext(dispatchers.main()) { - command.value = ShowBookmarkAddedConfirmation(savedBookmark) + command.value = ShowSavedSiteAddedConfirmation(savedBookmark) } } @@ -1380,7 +1379,7 @@ class BrowserTabViewModel( } else null }?.let { withContext(dispatchers.main()) { - command.value = ShowFavoriteAddedConfirmation(it) + command.value = ShowSavedSiteAddedConfirmation(it) } } } From 5c50e43beece949050c4986a134499849e267e85 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 12:28:50 +0200 Subject: [PATCH 91/95] use launchIn for consistency --- .../duckduckgo/app/browser/BrowserTabViewModel.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 0c710a27feaf..3bbe9cd87350 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -429,13 +429,11 @@ class BrowserTabViewModel( browserViewState.value = currentBrowserViewState().copy(isEmailSignedIn = isSignedIn) }.launchIn(viewModelScope) - viewModelScope.launch { - favoritesRepository.favorites().collect { favorite -> - val favorites = favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) } - ctaViewState.value = currentCtaViewState().copy(favorites = favorites) - autoCompleteViewState.value = currentAutoCompleteViewState().copy(favorites = favorites) - } - } + favoritesRepository.favorites().onEach { favorite -> + val favorites = favorite.map { FavoritesQuickAccessAdapter.QuickAccessFavorite(it) } + ctaViewState.value = currentCtaViewState().copy(favorites = favorites) + autoCompleteViewState.value = currentAutoCompleteViewState().copy(favorites = favorites) + }.launchIn(viewModelScope) } fun loadData(tabId: String, initialUrl: String?, skipHome: Boolean) { From 0cc4f8add5a12f075802c982386f764e374c3afe Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 12:29:13 +0200 Subject: [PATCH 92/95] Use io dispatcher when launching coroutine --- .../main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 3bbe9cd87350..1d198b578925 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -656,7 +656,7 @@ class BrowserTabViewModel( Timber.d("Favicon received for a url $url, different than the current one $currentUrl") return } - viewModelScope.launch { + viewModelScope.launch(dispatchers.io()) { val faviconFile = faviconManager.storeFavicon(currentTab.tabId, ImageFavicon(icon, url)) faviconFile?.let { tabRepository.updateTabFavicon(tabId, faviconFile.name) From 9e8d0c31175a3357511020113bbd4ffbac530136 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 12:54:25 +0200 Subject: [PATCH 93/95] move delete decision inside viewmodel and refactor test --- .../duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt | 4 ++-- .../com/duckduckgo/app/systemsearch/SystemSearchActivity.kt | 1 - .../com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index ac53f1b19230..6a7fa0512253 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -314,10 +314,10 @@ class SystemSearchViewModelTest { } @Test - fun whenQuickAccessDeletedThenRepositoryUpdated() = coroutineRule.runBlocking { + fun whenQuickAccessDeleteRequestedThenRepositoryUpdated() = coroutineRule.runBlocking { val savedSite = Favorite(1, "title", "http://example.com", 0) - testee.deleteQuickAccessItem(savedSite) + testee.onDeleteQuickAccessItemRequested(QuickAccessFavorite(savedSite)) verify(mockFavoritesRepository).delete(savedSite) } diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index a67024587ee2..97b40bd60c4f 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -324,7 +324,6 @@ class SystemSearchActivity : DuckDuckGoActivity() { private fun confirmDeleteSavedSite(savedSite: SavedSite) { val message = getString(R.string.bookmarkDeleteConfirmationMessage, savedSite.title).html(this) - viewModel.deleteQuickAccessItem(savedSite) Snackbar.make( rootView, message, diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index c5b1aa64f717..50fb251ff02e 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -296,6 +296,7 @@ class SystemSearchViewModel( } fun onDeleteQuickAccessItemRequested(it: FavoritesQuickAccessAdapter.QuickAccessFavorite) { + deleteQuickAccessItem(it.favorite) command.value = Command.DeleteSavedSiteConfirmation(it.favorite) } @@ -315,7 +316,7 @@ class SystemSearchViewModel( } } - fun deleteQuickAccessItem(savedSite: SavedSite) { + private fun deleteQuickAccessItem(savedSite: SavedSite) { when (savedSite) { is SavedSite.Favorite -> { viewModelScope.launch(dispatchers.io() + NonCancellable) { From c804fed0f6066c688ea3439e9bbcb744e6a81805 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 15:50:27 +0200 Subject: [PATCH 94/95] move strings back to untranslated strings --- app/src/main/res/values/string-untranslated.xml | 13 +++++++++++++ app/src/main/res/values/strings.xml | 13 +++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index f0155860be8f..31b0c86146a1 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -40,4 +40,17 @@ We only use your anonymous location to deliver better results, closer to you. You can always change your mind later. + + + Add Favorite + Favorite added + Bookmark added + Title + URL + Edit + Bookmarks + Favorites + No bookmarks added yet + No favorites added yet + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bdf2739070cf..14c05dfef1d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,9 +32,6 @@ Clear search input No compatible app installed Add Bookmark - Add Favorite - Favorite added - Bookmark added Desktop Site @@ -205,17 +202,13 @@ Bookmarks Are you sure you want to delete bookmark <b>%s</b>? Bookmark added - Title - URL - Edit + Bookmark title + Bookmark URL + Edit bookmark No bookmarks added yet More options for bookmark %s Bookmark added Deleted <b>%s</b> - Bookmarks - Favorites - No bookmarks added yet - No favorites added yet Export You don\'t have bookmarks, nothing will be exported Couldn\'t export any bookmarks, something went wrong From 59f8da14652186afe453241736f102bca90911fc Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 24 May 2021 16:18:47 +0200 Subject: [PATCH 95/95] apply codestyle --- .../main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 2 +- .../main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 55f3a1b34b75..51b955602c2b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1204,7 +1204,7 @@ class BrowserTabFragment : } private fun savedSiteAdded(savedSite: SavedSite) { - val snackbarMessage = when(savedSite) { + val snackbarMessage = when (savedSite) { is SavedSite.Bookmark -> R.string.bookmarkAddedMessage is SavedSite.Favorite -> R.string.favoriteAddedMessage } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 1d198b578925..f3680d74ed46 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -119,7 +119,6 @@ import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.collect import timber.log.Timber import java.io.File import java.util.*