From 96d6b309ec394dafb98ab944c842fda905af649e Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Wed, 13 Apr 2022 19:41:07 +0800 Subject: [PATCH 001/154] Migrate database --- .../6.json | 737 ++++++++++++++++++ .../newpipe/database/DatabaseMigrationTest.kt | 5 + .../org/schabi/newpipe/NewPipeDatabase.java | 4 +- .../schabi/newpipe/database/AppDatabase.java | 4 +- .../schabi/newpipe/database/Migrations.java | 42 +- .../playlist/model/PlaylistEntity.java | 12 + 6 files changed, 800 insertions(+), 4 deletions(-) create mode 100644 app/schemas/org.schabi.newpipe.database.AppDatabase/6.json diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/6.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/6.json new file mode 100644 index 00000000000..34d457f83ea --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/6.json @@ -0,0 +1,737 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "cc9c4d84f52f49105b1c4216b948b5f7", + "entities": [ + { + "tableName": "subscriptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notificationMode", + "columnName": "notification_mode", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "streams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "streamType", + "columnName": "stream_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploaderUrl", + "columnName": "uploader_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viewCount", + "columnName": "view_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "textualUploadDate", + "columnName": "textual_upload_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadDate", + "columnName": "upload_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isUploadDateApproximation", + "columnName": "is_upload_date_approximation", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "stream_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessDate", + "columnName": "access_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatCount", + "columnName": "repeat_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id", + "access_date" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "stream_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressMillis", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT, `display_index` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayIndex", + "columnName": "display_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "playlist_stream_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "playlistUid", + "columnName": "playlist_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "join_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "playlist_id", + "join_index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" + }, + { + "name": "index_playlist_stream_join_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "remote_playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_remote_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id", + "subscription_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_feed_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_group", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_feed_group_sort_order", + "unique": false, + "columnNames": [ + "sort_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed_group_subscription_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "feedGroupId", + "columnName": "group_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "group_id", + "subscription_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_feed_group_subscription_join_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "feed_group", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "group_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_last_updated", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "subscription_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + } + ], + "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, 'cc9c4d84f52f49105b1c4216b948b5f7')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index 28dea13e9f0..6d05a45bf9e 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -84,6 +84,11 @@ class DatabaseMigrationTest { true, Migrations.MIGRATION_4_5 ) + testHelper.runMigrationsAndValidate( + AppDatabase.DATABASE_NAME, Migrations.DB_VER_6, + true, Migrations.MIGRATION_5_6 + ) + val migratedDatabaseV3 = getMigratedDatabase() val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 402d4648d7f..fc3423994e8 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -5,6 +5,7 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3; import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4; import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5; +import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6; import android.content.Context; import android.database.Cursor; @@ -24,7 +25,8 @@ private NewPipeDatabase() { private static AppDatabase getDatabase(final Context context) { return Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, + MIGRATION_5_6) .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 28ddc818494..563e80b1780 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -1,6 +1,6 @@ package org.schabi.newpipe.database; -import static org.schabi.newpipe.database.Migrations.DB_VER_5; +import static org.schabi.newpipe.database.Migrations.DB_VER_6; import androidx.room.Database; import androidx.room.RoomDatabase; @@ -38,7 +38,7 @@ FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, FeedLastUpdatedEntity.class }, - version = DB_VER_5 + version = DB_VER_6 ) public abstract class AppDatabase extends RoomDatabase { public static final String DATABASE_NAME = "newpipe.db"; diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 7de08442c34..a8f093ba017 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -23,6 +23,7 @@ public final class Migrations { public static final int DB_VER_3 = 3; public static final int DB_VER_4 = 4; public static final int DB_VER_5 = 5; + public static final int DB_VER_6 = 6; private static final String TAG = Migrations.class.getName(); public static final boolean DEBUG = MainActivity.DEBUG; @@ -184,7 +185,46 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) { @Override public void migrate(@NonNull final SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " - + "INTEGER NOT NULL DEFAULT 0"); + + "INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_5_6 = new Migration(DB_VER_5, DB_VER_6) { + @Override + public void migrate(@NonNull final SupportSQLiteDatabase database) { + try { + database.beginTransaction(); + + // create a temp table to initialize display_index + database.execSQL("CREATE TABLE `playlists_tmp` " + + "(`uid` INTEGER NOT NULL, " + + "`name` TEXT, `thumbnail_url` TEXT," + + "`display_index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)"); + database.execSQL("INSERT INTO `playlists_tmp` (`uid`, `name`, `thumbnail_url`)" + + "SELECT `uid`, `name`, `thumbnail_url` FROM `playlists`"); + + // drop the old table and create new one + database.execSQL("DROP TABLE `playlists`"); + database.execSQL("CREATE TABLE `playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT, `thumbnail_url` TEXT," + + "`display_index` INTEGER NOT NULL UNIQUE)"); + + // insert temp data into the new table + // set display_index start from zero + database.execSQL("INSERT INTO `playlists` SELECT * FROM `playlists_tmp`"); + database.execSQL("UPDATE `playlists` SET `display_index` = `display_index` - 1"); + + // drop tmp table + database.execSQL("DROP TABLE `playlists_tmp`"); + + // create index on the new table + database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); + + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } } }; diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java index 71abf27322b..c1ae0a2b368 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -15,6 +15,7 @@ public class PlaylistEntity { public static final String PLAYLIST_ID = "uid"; public static final String PLAYLIST_NAME = "name"; public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; + public static final String PLAYLIST_DISPLAY_INDEX = "display_index"; @PrimaryKey(autoGenerate = true) @ColumnInfo(name = PLAYLIST_ID) @@ -26,6 +27,9 @@ public class PlaylistEntity { @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) private String thumbnailUrl; + @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) + private long displayIndex = 0; + public PlaylistEntity(final String name, final String thumbnailUrl) { this.name = name; this.thumbnailUrl = thumbnailUrl; @@ -54,4 +58,12 @@ public String getThumbnailUrl() { public void setThumbnailUrl(final String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; } + + public long getDisplayIndex() { + return displayIndex; + } + + public void setDisplayIndex(final long displayIndex) { + this.displayIndex = displayIndex; + } } From c34549a47d0c39050ddd34357e43adfce56ea638 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Wed, 13 Apr 2022 21:35:38 +0800 Subject: [PATCH 002/154] Update database migrations and getter/setter --- .../6.json | 12 ++++-- .../schabi/newpipe/database/Migrations.java | 42 ++++++++++++------- .../database/playlist/PlaylistLocalItem.java | 1 + .../playlist/PlaylistMetadataEntry.java | 6 ++- .../playlist/dao/PlaylistStreamDAO.java | 2 + .../playlist/model/PlaylistRemoteEntity.java | 25 +++++++++++ .../local/bookmark/BookmarkFragment.java | 1 - .../local/playlist/LocalPlaylistManager.java | 15 +++++-- 8 files changed, 82 insertions(+), 22 deletions(-) diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/6.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/6.json index 34d457f83ea..3ef363e3086 100644 --- a/app/schemas/org.schabi.newpipe.database.AppDatabase/6.json +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/6.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 6, - "identityHash": "cc9c4d84f52f49105b1c4216b948b5f7", + "identityHash": "9ffc14521c566beed378d77430de3f0c", "entities": [ { "tableName": "subscriptions", @@ -447,7 +447,7 @@ }, { "tableName": "remote_playlists", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)", "fields": [ { "fieldPath": "uid", @@ -485,6 +485,12 @@ "affinity": "TEXT", "notNull": false }, + { + "fieldPath": "displayIndex", + "columnName": "display_index", + "affinity": "INTEGER", + "notNull": true + }, { "fieldPath": "streamCount", "columnName": "stream_count", @@ -731,7 +737,7 @@ "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, 'cc9c4d84f52f49105b1c4216b948b5f7')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9ffc14521c566beed378d77430de3f0c')" ] } } \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index a8f093ba017..ffca6cca58c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -195,31 +195,45 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) { try { database.beginTransaction(); + // update playlists // create a temp table to initialize display_index database.execSQL("CREATE TABLE `playlists_tmp` " - + "(`uid` INTEGER NOT NULL, " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + "`name` TEXT, `thumbnail_url` TEXT," - + "`display_index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)"); + + "`display_index` INTEGER NOT NULL DEFAULT 0)"); database.execSQL("INSERT INTO `playlists_tmp` (`uid`, `name`, `thumbnail_url`)" + "SELECT `uid`, `name`, `thumbnail_url` FROM `playlists`"); - // drop the old table and create new one + // replace the old table database.execSQL("DROP TABLE `playlists`"); - database.execSQL("CREATE TABLE `playlists` " - + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "`name` TEXT, `thumbnail_url` TEXT," - + "`display_index` INTEGER NOT NULL UNIQUE)"); + database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`"); - // insert temp data into the new table - // set display_index start from zero - database.execSQL("INSERT INTO `playlists` SELECT * FROM `playlists_tmp`"); - database.execSQL("UPDATE `playlists` SET `display_index` = `display_index` - 1"); + // create index on the new table + database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); - // drop tmp table - database.execSQL("DROP TABLE `playlists_tmp`"); + + // update remote_playlists + // create a temp table to initialize display_index + database.execSQL("CREATE TABLE `remote_playlists_tmp` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + + "`thumbnail_url` TEXT, `uploader` TEXT, " + + "`display_index` INTEGER NOT NULL DEFAULT 0," + + "`stream_count` INTEGER)"); + database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " + + "`name`, `url`, `thumbnail_url`, `uploader`, `stream_count`)" + + "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " + + "`stream_count` FROM `remote_playlists`"); + + // replace the old table + database.execSQL("DROP TABLE `remote_playlists`"); + database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`"); // create index on the new table - database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); + database.execSQL("CREATE INDEX `index_remote_playlists_name` " + + "ON `remote_playlists` (`name`)"); + database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + + "ON `remote_playlists` (`service_id`, `url`)"); database.setTransactionSuccessful(); } finally { diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 43dbd89ea46..bc17c51c43e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -14,6 +14,7 @@ public interface PlaylistLocalItem extends LocalItem { static List merge( final List localPlaylists, final List remotePlaylists) { + // todo: merge algorithm final List items = new ArrayList<>( localPlaylists.size() + remotePlaylists.size()); items.addAll(localPlaylists); diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index a13894030a6..29bad45dc4c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -2,6 +2,7 @@ import androidx.room.ColumnInfo; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; @@ -15,14 +16,17 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem { public final String name; @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) public final String thumbnailUrl; + @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) + public final long displayIndex; @ColumnInfo(name = PLAYLIST_STREAM_COUNT) public final long streamCount; public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl, - final long streamCount) { + final long displayIndex, final long streamCount) { this.uid = uid; this.name = name; this.thumbnailUrl = thumbnailUrl; + this.displayIndex = displayIndex; this.streamCount = streamCount; } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index 4941d939507..3fb96a21ffc 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -15,6 +15,7 @@ import io.reactivex.rxjava3.core.Flowable; import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; @@ -75,6 +76,7 @@ default Flowable> listByService(final int serviceId) @Transaction @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", " + + PLAYLIST_DISPLAY_INDEX + ", " + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + " FROM " + PLAYLIST_TABLE diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java index 2e9a15d7dac..1fddfa73236 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -31,6 +31,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { public static final String REMOTE_PLAYLIST_URL = "url"; public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; + public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index"; public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; @PrimaryKey(autoGenerate = true) @@ -52,6 +53,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) private String uploader; + @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX) + private long displayIndex; + @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) private Long streamCount; @@ -66,6 +70,19 @@ public PlaylistRemoteEntity(final int serviceId, final String name, final String this.streamCount = streamCount; } + @Ignore + public PlaylistRemoteEntity(final int serviceId, final String name, final String url, + final String thumbnailUrl, final String uploader, + final long displayIndex, final Long streamCount) { + this.serviceId = serviceId; + this.name = name; + this.url = url; + this.thumbnailUrl = thumbnailUrl; + this.uploader = uploader; + this.displayIndex = displayIndex; + this.streamCount = streamCount; + } + @Ignore public PlaylistRemoteEntity(final PlaylistInfo info) { this(info.getServiceId(), info.getName(), info.getUrl(), @@ -136,6 +153,14 @@ public void setUploader(final String uploader) { this.uploader = uploader; } + public long getDisplayIndex() { + return displayIndex; + } + + public void setDisplayIndex(final long displayIndex) { + this.displayIndex = displayIndex; + } + public Long getStreamCount() { return streamCount; } diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index f272a8831f4..2f36cbd55eb 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -308,7 +308,6 @@ private void changeLocalPlaylistName(final long id, final String name) { + "with new name=[" + name + "] items"); } - localPlaylistManager.renamePlaylist(id, name); final Disposable disposable = localPlaylistManager.renamePlaylist(id, name) .observeOn(AndroidSchedulers.mainThread()) .subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError( diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java index 33296aa8433..aabda1bf064 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java @@ -96,12 +96,17 @@ public Single deletePlaylist(final long playlistId) { } public Maybe renamePlaylist(final long playlistId, final String name) { - return modifyPlaylist(playlistId, name, null); + return modifyPlaylist(playlistId, name, null, -1); } public Maybe changePlaylistThumbnail(final long playlistId, final String thumbnailUrl) { - return modifyPlaylist(playlistId, null, thumbnailUrl); + return modifyPlaylist(playlistId, null, thumbnailUrl, -1); + } + + public Maybe changePlaylistDisplayIndex(final long playlistId, + final long displayIndex) { + return modifyPlaylist(playlistId, null, null, displayIndex); } public String getPlaylistThumbnail(final long playlistId) { @@ -110,7 +115,8 @@ public String getPlaylistThumbnail(final long playlistId) { private Maybe modifyPlaylist(final long playlistId, @Nullable final String name, - @Nullable final String thumbnailUrl) { + @Nullable final String thumbnailUrl, + final long displayIndex) { return playlistTable.getPlaylist(playlistId) .firstElement() .filter(playlistEntities -> !playlistEntities.isEmpty()) @@ -122,6 +128,9 @@ private Maybe modifyPlaylist(final long playlistId, if (thumbnailUrl != null) { playlist.setThumbnailUrl(thumbnailUrl); } + if (displayIndex != -1) { + playlist.setDisplayIndex(displayIndex); + } return playlistTable.update(playlist); }).subscribeOn(Schedulers.io()); } From 270a541a7c98e975d6c848fce9ee232b6620432b Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Wed, 13 Apr 2022 22:46:24 +0800 Subject: [PATCH 003/154] Implement algorithm to merge playlists --- .../database/playlist/PlaylistLocalItem.java | 58 ++++++++++++++++--- .../playlist/PlaylistMetadataEntry.java | 5 ++ .../playlist/model/PlaylistRemoteEntity.java | 1 + 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index bc17c51c43e..ae81ce3f5c0 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -11,18 +11,62 @@ public interface PlaylistLocalItem extends LocalItem { String getOrderingName(); + long getDisplayIndex(); + static List merge( final List localPlaylists, final List remotePlaylists) { - // todo: merge algorithm - final List items = new ArrayList<>( + + // Merge localPlaylists and remotePlaylists by displayIndex. + // If two items have the same displayIndex, sort them in CASE_INSENSITIVE_ORDER. + // This algorithm is similar to the merge operation in merge sort. + + final List result = new ArrayList<>( localPlaylists.size() + remotePlaylists.size()); - items.addAll(localPlaylists); - items.addAll(remotePlaylists); + final List itemsWithSameIndex = new ArrayList<>(); + int i = 0; + int j = 0; + while (i < localPlaylists.size()) { + while (j < remotePlaylists.size()) { + if (remotePlaylists.get(j).getDisplayIndex() + <= localPlaylists.get(i).getDisplayIndex()) { + addItem(result, remotePlaylists.get(j), itemsWithSameIndex); + j++; + } else { + break; + } + } + addItem(result, localPlaylists.get(i), itemsWithSameIndex); + i++; + } + addItemsWithSameIndex(result, itemsWithSameIndex); + + // If displayIndex does not match actual index, update displayIndex. + // This may happen when a new list is created with default displayIndex = 0. + // todo: update displayIndex - Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName, - Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))); + return result; + } + + static void addItem(final List result, final PlaylistLocalItem item, + final List itemsWithSameIndex) { + if (!itemsWithSameIndex.isEmpty() + && itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) { + // The new item has a different displayIndex, + // add previous items with same index to the result. + addItemsWithSameIndex(result, itemsWithSameIndex); + itemsWithSameIndex.clear(); + } + itemsWithSameIndex.add(item); + } - return items; + static void addItemsWithSameIndex(final List result, + final List itemsWithSameIndex) { + if (itemsWithSameIndex.size() > 1) { + Collections.sort(itemsWithSameIndex, + Comparator.comparing(PlaylistLocalItem::getOrderingName, + Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))); + } + result.addAll(itemsWithSameIndex); } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index 29bad45dc4c..f54ffff134d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -39,4 +39,9 @@ public LocalItemType getLocalItemType() { public String getOrderingName() { return name; } + + @Override + public long getDisplayIndex() { + return displayIndex; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java index 1fddfa73236..4545267697c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -153,6 +153,7 @@ public void setUploader(final String uploader) { this.uploader = uploader; } + @Override public long getDisplayIndex() { return displayIndex; } From ba8370bcfd980089ef5984b6ca55e4b072a69fee Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Thu, 14 Apr 2022 12:13:42 +0800 Subject: [PATCH 004/154] Save changes to the database and bugfix --- .../database/playlist/PlaylistLocalItem.java | 14 ++-- .../playlist/dao/PlaylistRemoteDAO.java | 4 ++ .../playlist/model/PlaylistEntity.java | 6 +- .../newpipe/local/LocalItemListAdapter.java | 1 + .../local/bookmark/BookmarkFragment.java | 65 ++++++++++++++++++- .../local/playlist/LocalPlaylistManager.java | 5 +- .../local/playlist/RemotePlaylistManager.java | 15 +++++ 7 files changed, 100 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index ae81ce3f5c0..5bf50cd97eb 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -24,6 +24,12 @@ static List merge( final List result = new ArrayList<>( localPlaylists.size() + remotePlaylists.size()); final List itemsWithSameIndex = new ArrayList<>(); + + // The data from database may not be in the displayIndex order + Collections.sort(localPlaylists, + Comparator.comparingLong(PlaylistMetadataEntry::getDisplayIndex)); + Collections.sort(remotePlaylists, + Comparator.comparingLong(PlaylistRemoteEntity::getDisplayIndex)); int i = 0; int j = 0; while (i < localPlaylists.size()) { @@ -41,10 +47,6 @@ static List merge( } addItemsWithSameIndex(result, itemsWithSameIndex); - // If displayIndex does not match actual index, update displayIndex. - // This may happen when a new list is created with default displayIndex = 0. - // todo: update displayIndex - return result; } @@ -52,8 +54,8 @@ static void addItem(final List result, final PlaylistLocalIte final List itemsWithSameIndex) { if (!itemsWithSameIndex.isEmpty() && itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) { - // The new item has a different displayIndex, - // add previous items with same index to the result. + // The new item has a different displayIndex, add previous items with same + // index to the result. addItemsWithSameIndex(result, itemsWithSameIndex); itemsWithSameIndex.clear(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java index 6bb84942817..ade85746471 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java @@ -31,6 +31,10 @@ public interface PlaylistRemoteDAO extends BasicDAO { + " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") Flowable> listByService(int serviceId); + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + + REMOTE_PLAYLIST_ID + " = :playlistId") + Flowable> getPlaylist(long playlistId); + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") Flowable> getPlaylist(long serviceId, String url); diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java index c1ae0a2b368..82f697ab3e3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -2,6 +2,7 @@ import androidx.room.ColumnInfo; import androidx.room.Entity; +import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; @@ -28,11 +29,12 @@ public class PlaylistEntity { private String thumbnailUrl; @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) - private long displayIndex = 0; + private long displayIndex; - public PlaylistEntity(final String name, final String thumbnailUrl) { + public PlaylistEntity(final String name, final String thumbnailUrl, final long displayIndex) { this.name = name; this.thumbnailUrl = thumbnailUrl; + this.displayIndex = displayIndex; } public long getUid() { diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 05e2fdac083..e2bfd59778e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -142,6 +142,7 @@ public void removeItem(final LocalItem data) { } public boolean swapItems(final int fromAdapterPosition, final int toAdapterPosition) { + // todo: reuse this code? final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition); final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition); diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index 2f36cbd55eb..6d2fbfbfcf5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -199,6 +199,13 @@ public void onSubscribe(final Subscription s) { @Override public void onNext(final List subscriptions) { + + // If displayIndex does not match actual index, update displayIndex. + // This may happen when a new list is created + // or on the first run after database update + // or displayIndex is not continuous for some reason. + checkDisplayIndexUpdate(subscriptions); + handleResult(subscriptions); if (databaseSubscription != null) { databaseSubscription.request(1); @@ -212,7 +219,8 @@ public void onError(final Throwable exception) { } @Override - public void onComplete() { } + public void onComplete() { + } }; } @@ -316,5 +324,60 @@ private void changeLocalPlaylistName(final long id, final String name) { "Changing playlist name"))); disposables.add(disposable); } + + private void changeLocalPlaylistDisplayIndex(final long id, final long displayIndex) { + + if (localPlaylistManager == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "Updating local playlist id=[" + id + "] " + + "with new display_index=[" + displayIndex + "]"); + } + + final Disposable disposable = + localPlaylistManager.changePlaylistDisplayIndex(id, displayIndex) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError( + new ErrorInfo(throwable, + UserAction.REQUESTED_BOOKMARK, + "Changing local playlist display_index"))); + disposables.add(disposable); + } + + private void changeRemotePlaylistDisplayIndex(final long id, final long displayIndex) { + + if (remotePlaylistManager == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "Updating remote playlist id=[" + id + "] " + + "with new display_index=[" + displayIndex + "]"); + } + + final Disposable disposable = + remotePlaylistManager.changePlaylistDisplayIndex(id, displayIndex) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError( + new ErrorInfo(throwable, + UserAction.REQUESTED_BOOKMARK, + "Changing remote playlist display_index"))); + disposables.add(disposable); + } + + private void checkDisplayIndexUpdate(@NonNull final List result) { + for (int i = 0; i < result.size(); i++) { + final PlaylistLocalItem item = result.get(i); + if (item.getDisplayIndex() != i) { + if (item instanceof PlaylistMetadataEntry) { + changeLocalPlaylistDisplayIndex(((PlaylistMetadataEntry) item).uid, i); + } else if (item instanceof PlaylistRemoteEntity) { + changeRemotePlaylistDisplayIndex(((PlaylistRemoteEntity) item).getUid(), i); + } + } + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java index aabda1bf064..47817f9e473 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java @@ -40,8 +40,11 @@ public Maybe> createPlaylist(final String name, final List database.runInTransaction(() -> upsertStreams(playlistTable.insert(newPlaylist), streams, 0)) diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java index 5221139e34f..b49f149d64f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java @@ -8,6 +8,7 @@ import java.util.List; import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -33,6 +34,20 @@ public Single deletePlaylist(final long playlistId) { .subscribeOn(Schedulers.io()); } + public Maybe changePlaylistDisplayIndex(final long playlistId, + final long displayIndex) { + return playlistRemoteTable.getPlaylist(playlistId) + .firstElement() + .filter(playlistRemoteEntities -> !playlistRemoteEntities.isEmpty()) + .map(playlistRemoteEntities -> { + final PlaylistRemoteEntity playlist = playlistRemoteEntities.get(0); + if (displayIndex != -1) { + playlist.setDisplayIndex(displayIndex); + } + return playlistRemoteTable.update(playlist); + }).subscribeOn(Schedulers.io()); + } + public Single onBookmark(final PlaylistInfo playlistInfo) { return Single.fromCallable(() -> { final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); From bfb56b4144f1fec9680dbafd5fe79420d731e0a7 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Thu, 14 Apr 2022 16:59:52 +0800 Subject: [PATCH 005/154] UI design and behavior --- .../playlist/model/PlaylistEntity.java | 1 - .../local/bookmark/BookmarkFragment.java | 207 +++++++++++++----- .../local/holder/LocalPlaylistItemHolder.java | 21 +- .../holder/RemotePlaylistItemHolder.java | 22 +- .../layout/list_playlist_bookmark_item.xml | 84 +++++++ 5 files changed, 282 insertions(+), 53 deletions(-) create mode 100644 app/src/main/res/layout/list_playlist_bookmark_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java index 82f697ab3e3..cdbbdebc08f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -2,7 +2,6 @@ import androidx.room.ColumnInfo; import androidx.room.Entity; -import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index 6d2fbfbfcf5..63c63c1e036 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.local.bookmark; +import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; + import android.os.Bundle; import android.os.Parcelable; import android.text.InputType; @@ -12,6 +14,8 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -41,6 +45,7 @@ import io.reactivex.rxjava3.disposables.Disposable; public final class BookmarkFragment extends BaseLocalListFragment, Void> { + private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; @State protected Parcelable itemsListState; @@ -48,6 +53,7 @@ public final class BookmarkFragment extends BaseLocalListFragment() { @Override public void selected(final LocalItem selectedItem) { @@ -126,6 +135,14 @@ public void held(final LocalItem selectedItem) { showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem); } } + + @Override + public void drag(final LocalItem selectedItem, + final RecyclerView.ViewHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } + } }); } @@ -166,6 +183,7 @@ public void onDestroyView() { } databaseSubscription = null; + itemTouchHelper = null; } @Override @@ -255,56 +273,9 @@ protected void resetFragment() { } } - /////////////////////////////////////////////////////////////////////////// - // Utils - /////////////////////////////////////////////////////////////////////////// - - private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { - showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); - } - - private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { - final DialogEditTextBinding dialogBinding - = DialogEditTextBinding.inflate(getLayoutInflater()); - dialogBinding.dialogEditText.setHint(R.string.name); - dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT); - dialogBinding.dialogEditText.setText(selectedItem.name); - - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setView(dialogBinding.getRoot()) - .setPositiveButton(R.string.rename_playlist, (dialog, which) -> - changeLocalPlaylistName( - selectedItem.uid, - dialogBinding.dialogEditText.getText().toString())) - .setNegativeButton(R.string.cancel, null) - .setNeutralButton(R.string.delete, (dialog, which) -> { - showDeleteDialog(selectedItem.name, - localPlaylistManager.deletePlaylist(selectedItem.uid)); - dialog.dismiss(); - }) - .create() - .show(); - } - - private void showDeleteDialog(final String name, final Single deleteReactor) { - if (activity == null || disposables == null) { - return; - } - - new AlertDialog.Builder(activity) - .setTitle(name) - .setMessage(R.string.delete_playlist_prompt) - .setCancelable(true) - .setPositiveButton(R.string.delete, (dialog, i) -> - disposables.add(deleteReactor - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> { /*Do nothing on success*/ }, throwable -> - showError(new ErrorInfo(throwable, - UserAction.REQUESTED_BOOKMARK, - "Deleting playlist"))))) - .setNegativeButton(R.string.cancel, null) - .show(); - } + /*////////////////////////////////////////////////////////////////////////// + // Playlist Metadata Manipulation + //////////////////////////////////////////////////////////////////////////*/ private void changeLocalPlaylistName(final long id, final String name) { if (localPlaylistManager == null) { @@ -379,5 +350,141 @@ private void checkDisplayIndexUpdate(@NonNull final List resu } } } + + private void saveImmediate() { + if (localPlaylistManager == null || remotePlaylistManager == null + || itemListAdapter == null) { + return; + } + // todo: debounce + /* + // List must be loaded and modified in order to save + if (isLoadingComplete == null || isModified == null + || !isLoadingComplete.get() || !isModified.get()) { + Log.w(TAG, "Attempting to save playlist when local playlist " + + "is not loaded or not modified: playlist id=[" + playlistId + "]"); + return; + } + */ + // todo: is it correct? + final List items = itemListAdapter.getItemsList(); + for (int i = 0; i < items.size(); i++) { + final LocalItem item = items.get(i); + if (item instanceof PlaylistMetadataEntry) { + changeLocalPlaylistDisplayIndex(((PlaylistMetadataEntry) item).uid, i); + } else if (item instanceof PlaylistRemoteEntity) { + changeLocalPlaylistDisplayIndex(((PlaylistRemoteEntity) item).getUid(), i); + } + } + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + int directions = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + if (shouldUseGridLayout(requireContext())) { + directions |= ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; + } + return new ItemTouchHelper.SimpleCallback(directions, + ItemTouchHelper.ACTION_STATE_IDLE) { + @Override + public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView, + final int viewSize, + final int viewSizeOutOfBounds, + final int totalSize, + final long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, + viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, + Math.abs(standardSpeed)); + return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } + + @Override + public boolean onMove(@NonNull final RecyclerView recyclerView, + @NonNull final RecyclerView.ViewHolder source, + @NonNull final RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() + || itemListAdapter == null) { + return false; + } + + // todo: is it correct + final int sourceIndex = source.getBindingAdapterPosition(); + final int targetIndex = target.getBindingAdapterPosition(); + final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); + if (isSwapped) { + // todo + //saveChanges(); + saveImmediate(); + } + return isSwapped; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, + final int swipeDir) { + } + }; + } + + /////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////// + + private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { + showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); + } + + private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { + final DialogEditTextBinding dialogBinding + = DialogEditTextBinding.inflate(getLayoutInflater()); + dialogBinding.dialogEditText.setHint(R.string.name); + dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT); + dialogBinding.dialogEditText.setText(selectedItem.name); + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setView(dialogBinding.getRoot()) + .setPositiveButton(R.string.rename_playlist, (dialog, which) -> + changeLocalPlaylistName( + selectedItem.uid, + dialogBinding.dialogEditText.getText().toString())) + .setNegativeButton(R.string.cancel, null) + .setNeutralButton(R.string.delete, (dialog, which) -> { + showDeleteDialog(selectedItem.name, + localPlaylistManager.deletePlaylist(selectedItem.uid)); + dialog.dismiss(); + }) + .create() + .show(); + } + + private void showDeleteDialog(final String name, final Single deleteReactor) { + if (activity == null || disposables == null) { + return; + } + + new AlertDialog.Builder(activity) + .setTitle(name) + .setMessage(R.string.delete_playlist_prompt) + .setCancelable(true) + .setPositiveButton(R.string.delete, (dialog, i) -> + disposables.add(deleteReactor + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> { /*Do nothing on success*/ }, throwable -> + showError(new ErrorInfo(throwable, + UserAction.REQUESTED_BOOKMARK, + "Deleting playlist"))))) + .setNegativeButton(R.string.cancel, null) + .show(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index f8c5176ec2d..57a94470969 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -1,8 +1,10 @@ package org.schabi.newpipe.local.holder; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.local.LocalItemBuilder; @@ -13,13 +15,16 @@ import java.time.format.DateTimeFormatter; public class LocalPlaylistItemHolder extends PlaylistItemHolder { + private final View itemHandleView; + public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { - super(infoItemBuilder, parent); + this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent); } LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); + itemHandleView = itemView.findViewById(R.id.itemHandle); } @Override @@ -38,6 +43,20 @@ public void updateFromItem(final LocalItem localItem, PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView); + itemHandleView.setOnTouchListener(getOnTouchListener(item)); + super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); } + + private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) { + return (view, motionEvent) -> { + view.performClick(); + if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null + && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + itemBuilder.getOnItemSelectedListener().drag(item, + LocalPlaylistItemHolder.this); + } + return false; + }; + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 440353ac71c..9ecfa6979a2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -1,8 +1,11 @@ package org.schabi.newpipe.local.holder; import android.text.TextUtils; +import android.view.MotionEvent; +import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.NewPipe; @@ -14,14 +17,17 @@ import java.time.format.DateTimeFormatter; public class RemotePlaylistItemHolder extends PlaylistItemHolder { + private final View itemHandleView; + public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { - super(infoItemBuilder, parent); + this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent); } RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); + itemHandleView = itemView.findViewById(R.id.itemHandle); } @Override @@ -46,6 +52,20 @@ public void updateFromItem(final LocalItem localItem, PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); + itemHandleView.setOnTouchListener(getOnTouchListener(item)); + super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); } + + private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) { + return (view, motionEvent) -> { + view.performClick(); + if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null + && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + itemBuilder.getOnItemSelectedListener().drag(item, + RemotePlaylistItemHolder.this); + } + return false; + }; + } } diff --git a/app/src/main/res/layout/list_playlist_bookmark_item.xml b/app/src/main/res/layout/list_playlist_bookmark_item.xml new file mode 100644 index 00000000000..642ea294949 --- /dev/null +++ b/app/src/main/res/layout/list_playlist_bookmark_item.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + From 3c4882569931f91de0fc5dd0a980739781d6526c Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Fri, 15 Apr 2022 20:44:54 +0800 Subject: [PATCH 006/154] Debounced saver & bugfix & clean code --- .../database/playlist/PlaylistLocalItem.java | 4 + .../playlist/PlaylistMetadataEntry.java | 2 +- .../database/playlist/dao/PlaylistDAO.java | 14 + .../playlist/dao/PlaylistStreamDAO.java | 14 +- .../playlist/model/PlaylistEntity.java | 11 + .../playlist/model/PlaylistRemoteEntity.java | 2 +- .../newpipe/local/LocalItemListAdapter.java | 1 - .../local/bookmark/BookmarkFragment.java | 276 +++++++++++++----- .../local/dialog/PlaylistAppendDialog.java | 2 +- .../local/playlist/LocalPlaylistManager.java | 24 +- .../local/playlist/RemotePlaylistManager.java | 26 +- 11 files changed, 283 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 5bf50cd97eb..47c6dd6175a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -45,6 +45,10 @@ static List merge( addItem(result, localPlaylists.get(i), itemsWithSameIndex); i++; } + while (j < remotePlaylists.size()) { + addItem(result, remotePlaylists.get(j), itemsWithSameIndex); + j++; + } addItemsWithSameIndex(result, itemsWithSameIndex); return result; diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index f54ffff134d..ff80049a30a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -17,7 +17,7 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem { @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) public final String thumbnailUrl; @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) - public final long displayIndex; + public long displayIndex; @ColumnInfo(name = PLAYLIST_STREAM_COUNT) public final long streamCount; diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java index 70aaa3b2dc9..d8071e0af3a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java @@ -2,6 +2,7 @@ import androidx.room.Dao; import androidx.room.Query; +import androidx.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; @@ -36,4 +37,17 @@ default Flowable> listByService(final int serviceId) { @Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE) Flowable getCount(); + + @Transaction + default long upsertPlaylist(final PlaylistEntity playlist) { + final long playlistId = playlist.getUid(); + + if (playlistId == -1) { + // This situation is probably impossible. + return insert(playlist); + } else { + update(playlist); + return playlistId; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index 3fb96a21ffc..0fce984f38b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -82,7 +82,19 @@ default Flowable> listByService(final int serviceId) + " FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID - + " GROUP BY " + JOIN_PLAYLIST_ID + + " GROUP BY " + PLAYLIST_ID + " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") Flowable> getPlaylistMetadata(); + + @Transaction + @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", " + + PLAYLIST_DISPLAY_INDEX + ", " + + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + + + " FROM " + PLAYLIST_TABLE + + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + + " GROUP BY " + PLAYLIST_ID + + " ORDER BY " + PLAYLIST_DISPLAY_INDEX) + Flowable> getDisplayIndexOrderedPlaylistMetadata(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java index cdbbdebc08f..272e8a5bcdd 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -2,12 +2,15 @@ import androidx.room.ColumnInfo; import androidx.room.Entity; +import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; + @Entity(tableName = PLAYLIST_TABLE, indices = {@Index(value = {PLAYLIST_NAME})}) public class PlaylistEntity { @@ -36,6 +39,14 @@ public PlaylistEntity(final String name, final String thumbnailUrl, final long d this.displayIndex = displayIndex; } + @Ignore + public PlaylistEntity(final PlaylistMetadataEntry item) { + this.uid = item.uid; + this.name = item.name; + this.thumbnailUrl = item.thumbnailUrl; + this.displayIndex = item.displayIndex; + } + public long getUid() { return uid; } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java index 4545267697c..adea2738bda 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -54,7 +54,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { private String uploader; @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX) - private long displayIndex; + private long displayIndex = -1; // Make sure the new item is on the top @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) private Long streamCount; diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index e2bfd59778e..05e2fdac083 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -142,7 +142,6 @@ public void removeItem(final LocalItem data) { } public boolean swapItems(final int fromAdapterPosition, final int toAdapterPosition) { - // todo: reuse this code? final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition); final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition); diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index 63c63c1e036..4eac0f5ead4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -6,6 +6,7 @@ import android.os.Parcelable; import android.text.InputType; import android.util.Log; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,21 +31,32 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.local.BaseLocalListFragment; +import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder; +import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.subjects.PublishSubject; public final class BookmarkFragment extends BaseLocalListFragment, Void> { + // todo: add to playlists, item handle should be invisible + + // Save the list 10s after the last change occurred + private static final long SAVE_DEBOUNCE_MILLIS = 10000; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; @State protected Parcelable itemsListState; @@ -55,6 +67,16 @@ public final class BookmarkFragment extends BaseLocalListFragment debouncedSaveSignal; + + /* Has the playlist been fully loaded from db */ + private AtomicBoolean isLoadingComplete; + /* Has the playlist been modified (e.g. items reordered or deleted) */ + private AtomicBoolean isModified; + + // Map from (uid, local/remote item) to the saved display index in the database. + private Map, Long> displayIndexInDatabase; + /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Creation /////////////////////////////////////////////////////////////////////////// @@ -69,6 +91,12 @@ public void onCreate(final Bundle savedInstanceState) { localPlaylistManager = new LocalPlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database); disposables = new CompositeDisposable(); + + debouncedSaveSignal = PublishSubject.create(); + isLoadingComplete = new AtomicBoolean(); + isModified = new AtomicBoolean(); + + displayIndexInDatabase = new HashMap<>(); } @Nullable @@ -154,6 +182,10 @@ public void drag(final LocalItem selectedItem, public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); + disposables.add(getDebouncedSaver()); + isLoadingComplete.set(false); + isModified.set(false); + Flowable.combineLatest(localPlaylistManager.getPlaylists(), remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) .onBackpressureLatest() @@ -169,6 +201,9 @@ public void startLoading(final boolean forceLoad) { public void onPause() { super.onPause(); itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + + // Save on exit + saveImmediate(); } @Override @@ -189,14 +224,22 @@ public void onDestroyView() { @Override public void onDestroy() { super.onDestroy(); + if (debouncedSaveSignal != null) { + debouncedSaveSignal.onComplete(); + } if (disposables != null) { disposables.dispose(); } + debouncedSaveSignal = null; disposables = null; localPlaylistManager = null; remotePlaylistManager = null; itemsListState = null; + + isLoadingComplete = null; + isModified = null; + displayIndexInDatabase = null; } /////////////////////////////////////////////////////////////////////////// @@ -208,6 +251,8 @@ private Subscriber> getPlaylistsSubscriber() { @Override public void onSubscribe(final Subscription s) { showLoading(); + isLoadingComplete.set(false); + if (databaseSubscription != null) { databaseSubscription.cancel(); } @@ -217,14 +262,11 @@ public void onSubscribe(final Subscription s) { @Override public void onNext(final List subscriptions) { - - // If displayIndex does not match actual index, update displayIndex. - // This may happen when a new list is created - // or on the first run after database update - // or displayIndex is not continuous for some reason. - checkDisplayIndexUpdate(subscriptions); - - handleResult(subscriptions); + if (isModified == null || !isModified.get()) { + checkDisplayIndexModified(subscriptions); + handleResult(subscriptions); + isLoadingComplete.set(true); + } if (databaseSubscription != null) { databaseSubscription.request(1); } @@ -296,86 +338,170 @@ private void changeLocalPlaylistName(final long id, final String name) { disposables.add(disposable); } - private void changeLocalPlaylistDisplayIndex(final long id, final long displayIndex) { - - if (localPlaylistManager == null) { + private void deleteItem(final PlaylistLocalItem item) { + if (itemListAdapter == null) { return; } + itemListAdapter.removeItem(item); - if (DEBUG) { - Log.d(TAG, "Updating local playlist id=[" + id + "] " - + "with new display_index=[" + displayIndex + "]"); - } - - final Disposable disposable = - localPlaylistManager.changePlaylistDisplayIndex(id, displayIndex) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError( - new ErrorInfo(throwable, - UserAction.REQUESTED_BOOKMARK, - "Changing local playlist display_index"))); - disposables.add(disposable); + saveChanges(); } - private void changeRemotePlaylistDisplayIndex(final long id, final long displayIndex) { - - if (remotePlaylistManager == null) { + private void checkDisplayIndexModified(@NonNull final List result) { + if (isModified != null && isModified.get()) { return; } - if (DEBUG) { - Log.d(TAG, "Updating remote playlist id=[" + id + "] " - + "with new display_index=[" + displayIndex + "]"); - } - - final Disposable disposable = - remotePlaylistManager.changePlaylistDisplayIndex(id, displayIndex) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError( - new ErrorInfo(throwable, - UserAction.REQUESTED_BOOKMARK, - "Changing remote playlist display_index"))); - disposables.add(disposable); - } + displayIndexInDatabase.clear(); - private void checkDisplayIndexUpdate(@NonNull final List result) { + // If the display index does not match actual index in the list, update the display index. + // This may happen when a new list is created + // or on the first run after database update + // or displayIndex is not continuous for some reason. + boolean isDisplayIndexModified = false; for (int i = 0; i < result.size(); i++) { final PlaylistLocalItem item = result.get(i); if (item.getDisplayIndex() != i) { - if (item instanceof PlaylistMetadataEntry) { - changeLocalPlaylistDisplayIndex(((PlaylistMetadataEntry) item).uid, i); - } else if (item instanceof PlaylistRemoteEntity) { - changeRemotePlaylistDisplayIndex(((PlaylistRemoteEntity) item).getUid(), i); - } + isDisplayIndexModified = true; + } + + // Updating display index in the item does not affect the value inserts into + // database, which will be recalculated during the database update. Updating + // display index in the item here is to determine whether it is recently modified. + // Save the index read from the database. + if (item instanceof PlaylistMetadataEntry) { + + displayIndexInDatabase.put(new Pair<>(((PlaylistMetadataEntry) item).uid, + LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM), item.getDisplayIndex()); + ((PlaylistMetadataEntry) item).displayIndex = i; + + } else if (item instanceof PlaylistRemoteEntity) { + + displayIndexInDatabase.put(new Pair<>(((PlaylistRemoteEntity) item).getUid(), + LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM), + item.getDisplayIndex()); + ((PlaylistRemoteEntity) item).setDisplayIndex(i); + } } + + if (isDisplayIndexModified) { + saveChanges(); + } + } + + private void saveChanges() { + if (isModified == null || debouncedSaveSignal == null) { + return; + } + + isModified.set(true); + debouncedSaveSignal.onNext(System.currentTimeMillis()); + } + + private Disposable getDebouncedSaver() { + if (debouncedSaveSignal == null) { + return Disposable.empty(); + } + + return debouncedSaveSignal + .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> saveImmediate(), throwable -> + showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE, + "Debounced saver"))); } private void saveImmediate() { - if (localPlaylistManager == null || remotePlaylistManager == null - || itemListAdapter == null) { + if (itemListAdapter == null) { return; } - // todo: debounce - /* + // List must be loaded and modified in order to save if (isLoadingComplete == null || isModified == null || !isLoadingComplete.get() || !isModified.get()) { - Log.w(TAG, "Attempting to save playlist when local playlist " - + "is not loaded or not modified: playlist id=[" + playlistId + "]"); + Log.w(TAG, "Attempting to save playlists in bookmark when bookmark " + + "is not loaded or playlists not modified"); return; } - */ - // todo: is it correct? + final List items = itemListAdapter.getItemsList(); + final List localItemsUpdate = new ArrayList<>(); + final List localItemsDeleteUid = new ArrayList<>(); + final List remoteItemsUpdate = new ArrayList<>(); + final List remoteItemsDeleteUid = new ArrayList<>(); + + // Calculate display index for (int i = 0; i < items.size(); i++) { final LocalItem item = items.get(i); + if (item instanceof PlaylistMetadataEntry) { - changeLocalPlaylistDisplayIndex(((PlaylistMetadataEntry) item).uid, i); + ((PlaylistMetadataEntry) item).displayIndex = i; + + final Long uid = ((PlaylistMetadataEntry) item).uid; + final Pair key = new Pair<>(uid, + LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM); + final Long databaseIndex = displayIndexInDatabase.remove(key); + + if (databaseIndex != null) { + if (databaseIndex != i) { + localItemsUpdate.add((PlaylistMetadataEntry) item); + } + } else { + // This should be impossible. + continue; + } } else if (item instanceof PlaylistRemoteEntity) { - changeLocalPlaylistDisplayIndex(((PlaylistRemoteEntity) item).getUid(), i); + ((PlaylistRemoteEntity) item).setDisplayIndex(i); + + final Long uid = ((PlaylistRemoteEntity) item).getUid(); + final Pair key = new Pair<>(uid, + LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM); + final Long databaseIndex = displayIndexInDatabase.remove(key); + + if (databaseIndex != null) { + if (databaseIndex != i) { + remoteItemsUpdate.add((PlaylistRemoteEntity) item); + } + } else { + // This should be impossible. + continue; + } + } + } + + // Find deleted items + for (final Pair key : displayIndexInDatabase.keySet()) { + if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) { + localItemsDeleteUid.add(key.first); + } else if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) { + remoteItemsDeleteUid.add(key.first); } } + + displayIndexInDatabase.clear(); + + // 1. Update local playlists + // 2. Update remote playlists + // 3. Set isModified false + disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> disposables.add(remotePlaylistManager.updatePlaylists( + remoteItemsUpdate, remoteItemsDeleteUid) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + if (isModified != null) { + isModified.set(false); + } + }, + throwable -> showError(new ErrorInfo(throwable, + UserAction.REQUESTED_BOOKMARK, + "Saving playlist")) + )), + throwable -> showError(new ErrorInfo(throwable, + UserAction.REQUESTED_BOOKMARK, "Saving playlist")) + )); + } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { @@ -404,17 +530,26 @@ public boolean onMove(@NonNull final RecyclerView recyclerView, @NonNull final RecyclerView.ViewHolder target) { if (source.getItemViewType() != target.getItemViewType() || itemListAdapter == null) { - return false; + // Allow swap LocalPlaylistItemHolder and RemotePlaylistItemHolder. + if (!( + ( + (source instanceof LocalPlaylistItemHolder) + || (source instanceof RemotePlaylistItemHolder) + ) + && ( + (target instanceof LocalPlaylistItemHolder) + || (target instanceof RemotePlaylistItemHolder) + ) + )) { + return false; + } } - // todo: is it correct final int sourceIndex = source.getBindingAdapterPosition(); final int targetIndex = target.getBindingAdapterPosition(); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); if (isSwapped) { - // todo - //saveChanges(); - saveImmediate(); + saveChanges(); } return isSwapped; } @@ -441,7 +576,7 @@ public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, /////////////////////////////////////////////////////////////////////////// private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { - showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); + showDeleteDialog(item.getName(), item); } private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { @@ -459,15 +594,14 @@ private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { dialogBinding.dialogEditText.getText().toString())) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.delete, (dialog, which) -> { - showDeleteDialog(selectedItem.name, - localPlaylistManager.deletePlaylist(selectedItem.uid)); + showDeleteDialog(selectedItem.name, selectedItem); dialog.dismiss(); }) .create() .show(); } - private void showDeleteDialog(final String name, final Single deleteReactor) { + private void showDeleteDialog(final String name, final PlaylistLocalItem item) { if (activity == null || disposables == null) { return; } @@ -476,13 +610,7 @@ private void showDeleteDialog(final String name, final Single deleteRea .setTitle(name) .setMessage(R.string.delete_playlist_prompt) .setCancelable(true) - .setPositiveButton(R.string.delete, (dialog, i) -> - disposables.add(deleteReactor - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> { /*Do nothing on success*/ }, throwable -> - showError(new ErrorInfo(throwable, - UserAction.REQUESTED_BOOKMARK, - "Deleting playlist"))))) + .setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item)) .setNegativeButton(R.string.cancel, null) .show(); } diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java index a874cdd621c..58a10af220d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java @@ -85,7 +85,7 @@ public void selected(final LocalItem selectedItem) { final View newPlaylistButton = view.findViewById(R.id.newPlaylist); newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); - playlistDisposables.add(playlistManager.getPlaylists() + playlistDisposables.add(playlistManager.getDisplayIndexOrderedPlaylists() .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onPlaylistsReceived)); } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java index 47817f9e473..c68a22b011b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java @@ -41,6 +41,7 @@ public Maybe> createPlaylist(final String name, final List streamIds) })).subscribeOn(Schedulers.io()); } + public Completable updatePlaylists(final List updateItems, + final List deletedItems) { + final List items = new ArrayList<>(updateItems.size()); + for (final PlaylistMetadataEntry item : updateItems) { + items.add(new PlaylistEntity(item)); + } + return Completable.fromRunnable(() -> database.runInTransaction(() -> { + for (final Long uid: deletedItems) { + playlistTable.deletePlaylist(uid); + } + for (final PlaylistEntity item: items) { + playlistTable.upsertPlaylist(item); + } + })).subscribeOn(Schedulers.io()); + } + public Flowable> getPlaylists() { return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); } + public Flowable> getDisplayIndexOrderedPlaylists() { + return playlistStreamTable.getDisplayIndexOrderedPlaylistMetadata() + .subscribeOn(Schedulers.io()); + } + public Flowable> getPlaylistStreams(final long playlistId) { return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); } @@ -107,7 +129,7 @@ public Maybe changePlaylistThumbnail(final long playlistId, return modifyPlaylist(playlistId, null, thumbnailUrl, -1); } - public Maybe changePlaylistDisplayIndex(final long playlistId, + public Maybe updatePlaylistDisplayIndex(final long playlistId, final long displayIndex) { return modifyPlaylist(playlistId, null, null, displayIndex); } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java index b49f149d64f..1dbd726aef1 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java @@ -7,16 +7,18 @@ import java.util.List; +import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; public class RemotePlaylistManager { + private final AppDatabase database; private final PlaylistRemoteDAO playlistRemoteTable; public RemotePlaylistManager(final AppDatabase db) { + database = db; playlistRemoteTable = db.playlistRemoteDAO(); } @@ -34,18 +36,16 @@ public Single deletePlaylist(final long playlistId) { .subscribeOn(Schedulers.io()); } - public Maybe changePlaylistDisplayIndex(final long playlistId, - final long displayIndex) { - return playlistRemoteTable.getPlaylist(playlistId) - .firstElement() - .filter(playlistRemoteEntities -> !playlistRemoteEntities.isEmpty()) - .map(playlistRemoteEntities -> { - final PlaylistRemoteEntity playlist = playlistRemoteEntities.get(0); - if (displayIndex != -1) { - playlist.setDisplayIndex(displayIndex); - } - return playlistRemoteTable.update(playlist); - }).subscribeOn(Schedulers.io()); + public Completable updatePlaylists(final List updateItems, + final List deletedItems) { + return Completable.fromRunnable(() -> database.runInTransaction(() -> { + for (final Long uid: deletedItems) { + playlistRemoteTable.deletePlaylist(uid); + } + for (final PlaylistRemoteEntity item: updateItems) { + playlistRemoteTable.upsert(item); + } + })).subscribeOn(Schedulers.io()); } public Single onBookmark(final PlaylistInfo playlistInfo) { From 0aa08a5e4049952bc830522b3053d347199ee3f7 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Fri, 15 Apr 2022 23:19:24 +0800 Subject: [PATCH 007/154] Use new item holder --- .../newpipe/local/LocalItemListAdapter.java | 21 ++++-- .../local/bookmark/BookmarkFragment.java | 18 ++--- .../LocalBookmarkPlaylistItemHolder.java | 63 ++++++++++++++++ .../local/holder/LocalPlaylistItemHolder.java | 19 +---- .../RemoteBookmarkPlaylistItemHolder.java | 71 +++++++++++++++++++ .../holder/RemotePlaylistItemHolder.java | 20 +----- 6 files changed, 163 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/local/holder/LocalBookmarkPlaylistItemHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/local/holder/RemoteBookmarkPlaylistItemHolder.java diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 05e2fdac083..161d35ee591 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -13,6 +13,7 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.holder.LocalItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder; @@ -20,6 +21,7 @@ import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder; import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder; import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder; +import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder; import org.schabi.newpipe.util.FallbackViewHolder; @@ -66,6 +68,8 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; @@ -74,6 +78,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter, Void> { - // todo: add to playlists, item handle should be invisible // Save the list 10s after the last change occurred private static final long SAVE_DEBOUNCE_MILLIS = 10000; @@ -126,6 +125,8 @@ public void onResume() { @Override protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); + + itemListAdapter.setUseItemHandle(true); } @Override @@ -530,15 +531,16 @@ public boolean onMove(@NonNull final RecyclerView recyclerView, @NonNull final RecyclerView.ViewHolder target) { if (source.getItemViewType() != target.getItemViewType() || itemListAdapter == null) { - // Allow swap LocalPlaylistItemHolder and RemotePlaylistItemHolder. + // Allow swap LocalBookmarkPlaylistItemHolder and + // RemoteBookmarkPlaylistItemHolder. if (!( ( - (source instanceof LocalPlaylistItemHolder) - || (source instanceof RemotePlaylistItemHolder) + (source instanceof LocalBookmarkPlaylistItemHolder) + || (source instanceof RemoteBookmarkPlaylistItemHolder) ) && ( - (target instanceof LocalPlaylistItemHolder) - || (target instanceof RemotePlaylistItemHolder) + (target instanceof LocalBookmarkPlaylistItemHolder) + || (target instanceof RemoteBookmarkPlaylistItemHolder) ) )) { return false; diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalBookmarkPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalBookmarkPlaylistItemHolder.java new file mode 100644 index 00000000000..697ef1072bd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalBookmarkPlaylistItemHolder.java @@ -0,0 +1,63 @@ +package org.schabi.newpipe.local.holder; + +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.local.LocalItemBuilder; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.PicassoHelper; + +import java.time.format.DateTimeFormatter; + +public class LocalBookmarkPlaylistItemHolder extends PlaylistItemHolder { + private final View itemHandleView; + + public LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, + final ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent); + } + + LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + itemHandleView = itemView.findViewById(R.id.itemHandle); + } + + @Override + public void updateFromItem(final LocalItem localItem, + final HistoryRecordManager historyRecordManager, + final DateTimeFormatter dateTimeFormatter) { + if (!(localItem instanceof PlaylistMetadataEntry)) { + return; + } + final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; + + itemTitleView.setText(item.name); + itemStreamCountView.setText(Localization.localizeStreamCountMini( + itemStreamCountView.getContext(), item.streamCount)); + itemUploaderView.setVisibility(View.INVISIBLE); + + PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView); + + itemHandleView.setOnTouchListener(getOnTouchListener(item)); + + super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); + } + + private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) { + return (view, motionEvent) -> { + view.performClick(); + if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null + && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + itemBuilder.getOnItemSelectedListener().drag(item, + LocalBookmarkPlaylistItemHolder.this); + } + return false; + }; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 57a94470969..2cfc9446326 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.local.holder; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -15,16 +14,14 @@ import java.time.format.DateTimeFormatter; public class LocalPlaylistItemHolder extends PlaylistItemHolder { - private final View itemHandleView; public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent); + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); } LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); - itemHandleView = itemView.findViewById(R.id.itemHandle); } @Override @@ -43,20 +40,6 @@ public void updateFromItem(final LocalItem localItem, PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView); - itemHandleView.setOnTouchListener(getOnTouchListener(item)); - super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); } - - private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) { - return (view, motionEvent) -> { - view.performClick(); - if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null - && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - itemBuilder.getOnItemSelectedListener().drag(item, - LocalPlaylistItemHolder.this); - } - return false; - }; - } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemoteBookmarkPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemoteBookmarkPlaylistItemHolder.java new file mode 100644 index 00000000000..345223b08ca --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemoteBookmarkPlaylistItemHolder.java @@ -0,0 +1,71 @@ +package org.schabi.newpipe.local.holder; + +import android.text.TextUtils; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.local.LocalItemBuilder; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.PicassoHelper; + +import java.time.format.DateTimeFormatter; + +public class RemoteBookmarkPlaylistItemHolder extends PlaylistItemHolder { + private final View itemHandleView; + + public RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, + final ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent); + } + + RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + itemHandleView = itemView.findViewById(R.id.itemHandle); + } + + @Override + public void updateFromItem(final LocalItem localItem, + final HistoryRecordManager historyRecordManager, + final DateTimeFormatter dateTimeFormatter) { + if (!(localItem instanceof PlaylistRemoteEntity)) { + return; + } + final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; + + itemTitleView.setText(item.getName()); + itemStreamCountView.setText(Localization.localizeStreamCountMini( + itemStreamCountView.getContext(), item.getStreamCount())); + // Here is where the uploader name is set in the bookmarked playlists library + if (!TextUtils.isEmpty(item.getUploader())) { + itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), + NewPipe.getNameOfService(item.getServiceId()))); + } else { + itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId())); + } + + PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); + + itemHandleView.setOnTouchListener(getOnTouchListener(item)); + + super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); + } + + private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) { + return (view, motionEvent) -> { + view.performClick(); + if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null + && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + itemBuilder.getOnItemSelectedListener().drag(item, + RemoteBookmarkPlaylistItemHolder.this); + } + return false; + }; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 9ecfa6979a2..d2059bfed67 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -1,8 +1,6 @@ package org.schabi.newpipe.local.holder; import android.text.TextUtils; -import android.view.MotionEvent; -import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.R; @@ -17,17 +15,15 @@ import java.time.format.DateTimeFormatter; public class RemotePlaylistItemHolder extends PlaylistItemHolder { - private final View itemHandleView; public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent); + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); } RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); - itemHandleView = itemView.findViewById(R.id.itemHandle); } @Override @@ -52,20 +48,6 @@ public void updateFromItem(final LocalItem localItem, PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); - itemHandleView.setOnTouchListener(getOnTouchListener(item)); - super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); } - - private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) { - return (view, motionEvent) -> { - view.performClick(); - if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null - && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - itemBuilder.getOnItemSelectedListener().drag(item, - RemotePlaylistItemHolder.this); - } - return false; - }; - } } From c24aed054f9da7dcde634faf570f0314d44f6a87 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Sat, 16 Apr 2022 12:00:02 +0800 Subject: [PATCH 008/154] Fix sonar warning and typo --- .../schabi/newpipe/database/Migrations.java | 16 ++--- .../newpipe/local/LocalItemListAdapter.java | 15 +++-- .../local/bookmark/BookmarkFragment.java | 64 +++++++++---------- .../LocalBookmarkPlaylistItemHolder.java | 11 +--- .../local/holder/LocalPlaylistItemHolder.java | 3 +- .../RemoteBookmarkPlaylistItemHolder.java | 19 +----- .../holder/RemotePlaylistItemHolder.java | 3 +- 7 files changed, 53 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index ffca6cca58c..1013899ca91 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -195,8 +195,8 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) { try { database.beginTransaction(); - // update playlists - // create a temp table to initialize display_index + // Update playlists. + // Create a temp table to initialize display_index. database.execSQL("CREATE TABLE `playlists_tmp` " + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + "`name` TEXT, `thumbnail_url` TEXT," @@ -204,16 +204,16 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) { database.execSQL("INSERT INTO `playlists_tmp` (`uid`, `name`, `thumbnail_url`)" + "SELECT `uid`, `name`, `thumbnail_url` FROM `playlists`"); - // replace the old table + // Replace the old table. database.execSQL("DROP TABLE `playlists`"); database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`"); - // create index on the new table + // Create index on the new table. database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); - // update remote_playlists - // create a temp table to initialize display_index + // Update remote_playlists. + // Create a temp table to initialize display_index. database.execSQL("CREATE TABLE `remote_playlists_tmp` " + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " @@ -225,11 +225,11 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) { + "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " + "`stream_count` FROM `remote_playlists`"); - // replace the old table + // Replace the old table. database.execSQL("DROP TABLE `remote_playlists`"); database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`"); - // create index on the new table + // Create index on the new table. database.execSQL("CREATE INDEX `index_remote_playlists_name` " + "ON `remote_playlists` (`name`)"); database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 161d35ee591..5c22cee240d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -256,12 +256,17 @@ public int getItemViewType(int position) { switch (item.getLocalItemType()) { case PLAYLIST_LOCAL_ITEM: - return useItemHandle ? LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE : (useGridVariant - ? LOCAL_PLAYLIST_GRID_HOLDER_TYPE : LOCAL_PLAYLIST_HOLDER_TYPE); + if (useItemHandle) { + return LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE; + } + return useGridVariant ? LOCAL_PLAYLIST_GRID_HOLDER_TYPE + : LOCAL_PLAYLIST_HOLDER_TYPE; case PLAYLIST_REMOTE_ITEM: - return useItemHandle ? REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE : (useGridVariant - ? REMOTE_PLAYLIST_GRID_HOLDER_TYPE : REMOTE_PLAYLIST_HOLDER_TYPE); - + if (useItemHandle) { + return REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE; + } + return useGridVariant ? REMOTE_PLAYLIST_GRID_HOLDER_TYPE + : REMOTE_PLAYLIST_HOLDER_TYPE; case PLAYLIST_STREAM_ITEM: return useGridVariant ? STREAM_PLAYLIST_GRID_HOLDER_TYPE : STREAM_PLAYLIST_HOLDER_TYPE; diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index daa89a4f020..14100ef816e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -54,7 +54,7 @@ public final class BookmarkFragment extends BaseLocalListFragment, Void> { - // Save the list 10s after the last change occurred + // Save the list 10 seconds after the last change occurred private static final long SAVE_DEBOUNCE_MILLIS = 10000; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; @State @@ -281,6 +281,7 @@ public void onError(final Throwable exception) { @Override public void onComplete() { + // Do nothing. } }; } @@ -444,13 +445,13 @@ private void saveImmediate() { LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM); final Long databaseIndex = displayIndexInDatabase.remove(key); + // The database index should not be null because inserting new item into database + // is not dealt here. NullPointerException has occurred once, but I can't reproduce + // it. Enhance robustness here. if (databaseIndex != null) { if (databaseIndex != i) { localItemsUpdate.add((PlaylistMetadataEntry) item); } - } else { - // This should be impossible. - continue; } } else if (item instanceof PlaylistRemoteEntity) { ((PlaylistRemoteEntity) item).setDisplayIndex(i); @@ -464,9 +465,6 @@ private void saveImmediate() { if (databaseIndex != i) { remoteItemsUpdate.add((PlaylistRemoteEntity) item); } - } else { - // This should be impossible. - continue; } } } @@ -489,16 +487,16 @@ private void saveImmediate() { .observeOn(AndroidSchedulers.mainThread()) .subscribe(() -> disposables.add(remotePlaylistManager.updatePlaylists( remoteItemsUpdate, remoteItemsDeleteUid) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - if (isModified != null) { - isModified.set(false); - } - }, - throwable -> showError(new ErrorInfo(throwable, - UserAction.REQUESTED_BOOKMARK, - "Saving playlist")) - )), + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + if (isModified != null) { + isModified.set(false); + } + }, + throwable -> showError(new ErrorInfo(throwable, + UserAction.REQUESTED_BOOKMARK, + "Saving playlist")) + )), throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, "Saving playlist")) )); @@ -529,22 +527,21 @@ public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView public boolean onMove(@NonNull final RecyclerView recyclerView, @NonNull final RecyclerView.ViewHolder source, @NonNull final RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType() - || itemListAdapter == null) { - // Allow swap LocalBookmarkPlaylistItemHolder and - // RemoteBookmarkPlaylistItemHolder. - if (!( - ( - (source instanceof LocalBookmarkPlaylistItemHolder) - || (source instanceof RemoteBookmarkPlaylistItemHolder) - ) - && ( - (target instanceof LocalBookmarkPlaylistItemHolder) - || (target instanceof RemoteBookmarkPlaylistItemHolder) - ) - )) { - return false; - } + + // Allow swap LocalBookmarkPlaylistItemHolder and RemoteBookmarkPlaylistItemHolder. + if (itemListAdapter == null + || source.getItemViewType() != target.getItemViewType() + && !( + ( + (source instanceof LocalBookmarkPlaylistItemHolder) + || (source instanceof RemoteBookmarkPlaylistItemHolder) + ) + && ( + (target instanceof LocalBookmarkPlaylistItemHolder) + || (target instanceof RemoteBookmarkPlaylistItemHolder) + )) + ) { + return false; } final int sourceIndex = source.getBindingAdapterPosition(); @@ -569,6 +566,7 @@ public boolean isItemViewSwipeEnabled() { @Override public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int swipeDir) { + // Do nothing. } }; } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalBookmarkPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalBookmarkPlaylistItemHolder.java index 697ef1072bd..16130009b6e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalBookmarkPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalBookmarkPlaylistItemHolder.java @@ -9,12 +9,10 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.PicassoHelper; import java.time.format.DateTimeFormatter; -public class LocalBookmarkPlaylistItemHolder extends PlaylistItemHolder { +public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder { private final View itemHandleView; public LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, @@ -37,13 +35,6 @@ public void updateFromItem(final LocalItem localItem, } final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; - itemTitleView.setText(item.name); - itemStreamCountView.setText(Localization.localizeStreamCountMini( - itemStreamCountView.getContext(), item.streamCount)); - itemUploaderView.setVisibility(View.INVISIBLE); - - PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView); - itemHandleView.setOnTouchListener(getOnTouchListener(item)); super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 2cfc9446326..50bbcd56676 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -3,7 +3,6 @@ import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.local.LocalItemBuilder; @@ -16,7 +15,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + super(infoItemBuilder, parent); } LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemoteBookmarkPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemoteBookmarkPlaylistItemHolder.java index 345223b08ca..6d61d1e08bf 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemoteBookmarkPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemoteBookmarkPlaylistItemHolder.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.local.holder; -import android.text.TextUtils; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -8,15 +7,12 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.PicassoHelper; import java.time.format.DateTimeFormatter; -public class RemoteBookmarkPlaylistItemHolder extends PlaylistItemHolder { +public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder { private final View itemHandleView; public RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, @@ -39,19 +35,6 @@ public void updateFromItem(final LocalItem localItem, } final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; - itemTitleView.setText(item.getName()); - itemStreamCountView.setText(Localization.localizeStreamCountMini( - itemStreamCountView.getContext(), item.getStreamCount())); - // Here is where the uploader name is set in the bookmarked playlists library - if (!TextUtils.isEmpty(item.getUploader())) { - itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), - NewPipe.getNameOfService(item.getServiceId()))); - } else { - itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId())); - } - - PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); - itemHandleView.setOnTouchListener(getOnTouchListener(item)); super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index d2059bfed67..865b2c4f719 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -3,7 +3,6 @@ import android.text.TextUtils; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.NewPipe; @@ -18,7 +17,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + super(infoItemBuilder, parent); } RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, From bd1aae8d66d68ca511740a7b3765b513354c4e6e Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Sat, 16 Apr 2022 12:44:24 +0800 Subject: [PATCH 009/154] Fix sonar warning --- .../newpipe/local/bookmark/BookmarkFragment.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index 14100ef816e..b1910781776 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -446,12 +446,10 @@ private void saveImmediate() { final Long databaseIndex = displayIndexInDatabase.remove(key); // The database index should not be null because inserting new item into database - // is not dealt here. NullPointerException has occurred once, but I can't reproduce - // it. Enhance robustness here. - if (databaseIndex != null) { - if (databaseIndex != i) { - localItemsUpdate.add((PlaylistMetadataEntry) item); - } + // is not handled here. NullPointerException has occurred once, but I can't + // reproduce it. Enhance robustness here. + if (databaseIndex != null && databaseIndex != i) { + localItemsUpdate.add((PlaylistMetadataEntry) item); } } else if (item instanceof PlaylistRemoteEntity) { ((PlaylistRemoteEntity) item).setDisplayIndex(i); @@ -461,10 +459,8 @@ private void saveImmediate() { LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM); final Long databaseIndex = displayIndexInDatabase.remove(key); - if (databaseIndex != null) { - if (databaseIndex != i) { - remoteItemsUpdate.add((PlaylistRemoteEntity) item); - } + if (databaseIndex != null && databaseIndex != i) { + remoteItemsUpdate.add((PlaylistRemoteEntity) item); } } } From bb5390d63a6737ce7b24add776e9e017ac544ae6 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Sun, 17 Apr 2022 14:53:02 +0800 Subject: [PATCH 010/154] Reuse DebounceSaver --- .../database/playlist/PlaylistLocalItem.java | 8 +- .../local/bookmark/BookmarkFragment.java | 81 +++++++------------ .../local/playlist/LocalPlaylistFragment.java | 73 ++++++----------- .../schabi/newpipe/util/DebounceSavable.java | 15 ++++ .../schabi/newpipe/util/DebounceSaver.java | 68 ++++++++++++++++ 5 files changed, 142 insertions(+), 103 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/DebounceSavable.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 47c6dd6175a..0e7beba4129 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -17,15 +17,15 @@ static List merge( final List localPlaylists, final List remotePlaylists) { - // Merge localPlaylists and remotePlaylists by displayIndex. - // If two items have the same displayIndex, sort them in CASE_INSENSITIVE_ORDER. + // Merge localPlaylists and remotePlaylists by display index. + // If two items have the same display index, sort them in CASE_INSENSITIVE_ORDER. // This algorithm is similar to the merge operation in merge sort. final List result = new ArrayList<>( localPlaylists.size() + remotePlaylists.size()); final List itemsWithSameIndex = new ArrayList<>(); - // The data from database may not be in the displayIndex order + // The data from database may not be in the display index order Collections.sort(localPlaylists, Comparator.comparingLong(PlaylistMetadataEntry::getDisplayIndex)); Collections.sort(remotePlaylists, @@ -58,7 +58,7 @@ static void addItem(final List result, final PlaylistLocalIte final List itemsWithSameIndex) { if (!itemsWithSameIndex.isEmpty() && itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) { - // The new item has a different displayIndex, add previous items with same + // The new item has a different display index, add previous items with same // index to the result. addItemsWithSameIndex(result, itemsWithSameIndex); itemsWithSameIndex.clear(); diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index b1910781776..ceeb980c7e0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -35,6 +35,8 @@ import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.util.DebounceSavable; +import org.schabi.newpipe.util.DebounceSaver; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; @@ -42,7 +44,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; @@ -50,12 +51,10 @@ import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.subjects.PublishSubject; -public final class BookmarkFragment extends BaseLocalListFragment, Void> { +public final class BookmarkFragment extends BaseLocalListFragment, Void> + implements DebounceSavable { - // Save the list 10 seconds after the last change occurred - private static final long SAVE_DEBOUNCE_MILLIS = 10000; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; @State protected Parcelable itemsListState; @@ -66,12 +65,10 @@ public final class BookmarkFragment extends BaseLocalListFragment debouncedSaveSignal; - - /* Has the playlist been fully loaded from db */ + /* Have the bookmarked playlists been fully loaded from db */ private AtomicBoolean isLoadingComplete; - /* Has the playlist been modified (e.g. items reordered or deleted) */ - private AtomicBoolean isModified; + + private DebounceSaver debounceSaver; // Map from (uid, local/remote item) to the saved display index in the database. private Map, Long> displayIndexInDatabase; @@ -91,9 +88,8 @@ public void onCreate(final Bundle savedInstanceState) { remotePlaylistManager = new RemotePlaylistManager(database); disposables = new CompositeDisposable(); - debouncedSaveSignal = PublishSubject.create(); isLoadingComplete = new AtomicBoolean(); - isModified = new AtomicBoolean(); + debounceSaver = new DebounceSaver(10000, this); displayIndexInDatabase = new HashMap<>(); } @@ -183,9 +179,11 @@ public void drag(final LocalItem selectedItem, public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); - disposables.add(getDebouncedSaver()); + if (debounceSaver != null) { + disposables.add(debounceSaver.getDebouncedSaver()); + debounceSaver.setIsModified(false); + } isLoadingComplete.set(false); - isModified.set(false); Flowable.combineLatest(localPlaylistManager.getPlaylists(), remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) @@ -225,21 +223,20 @@ public void onDestroyView() { @Override public void onDestroy() { super.onDestroy(); - if (debouncedSaveSignal != null) { - debouncedSaveSignal.onComplete(); + if (debounceSaver != null) { + debounceSaver.getDebouncedSaveSignal().onComplete(); } if (disposables != null) { disposables.dispose(); } - debouncedSaveSignal = null; + debounceSaver = null; disposables = null; localPlaylistManager = null; remotePlaylistManager = null; itemsListState = null; isLoadingComplete = null; - isModified = null; displayIndexInDatabase = null; } @@ -263,7 +260,7 @@ public void onSubscribe(final Subscription s) { @Override public void onNext(final List subscriptions) { - if (isModified == null || !isModified.get()) { + if (debounceSaver == null || !debounceSaver.getIsModified()) { checkDisplayIndexModified(subscriptions); handleResult(subscriptions); isLoadingComplete.set(true); @@ -346,11 +343,11 @@ private void deleteItem(final PlaylistLocalItem item) { } itemListAdapter.removeItem(item); - saveChanges(); + debounceSaver.saveChanges(); } private void checkDisplayIndexModified(@NonNull final List result) { - if (isModified != null && isModified.get()) { + if (debounceSaver != null && debounceSaver.getIsModified()) { return; } @@ -358,8 +355,9 @@ private void checkDisplayIndexModified(@NonNull final List re // If the display index does not match actual index in the list, update the display index. // This may happen when a new list is created - // or on the first run after database update - // or displayIndex is not continuous for some reason. + // or on the first run after database migration + // or display index is not continuous for some reason + // or the user changes the display index. boolean isDisplayIndexModified = false; for (int i = 0; i < result.size(); i++) { final PlaylistLocalItem item = result.get(i); @@ -388,40 +386,19 @@ private void checkDisplayIndexModified(@NonNull final List re } if (isDisplayIndexModified) { - saveChanges(); - } - } - - private void saveChanges() { - if (isModified == null || debouncedSaveSignal == null) { - return; + debounceSaver.saveChanges(); } - - isModified.set(true); - debouncedSaveSignal.onNext(System.currentTimeMillis()); } - private Disposable getDebouncedSaver() { - if (debouncedSaveSignal == null) { - return Disposable.empty(); - } - - return debouncedSaveSignal - .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> saveImmediate(), throwable -> - showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE, - "Debounced saver"))); - } - - private void saveImmediate() { + @Override + public void saveImmediate() { if (itemListAdapter == null) { return; } // List must be loaded and modified in order to save - if (isLoadingComplete == null || isModified == null - || !isLoadingComplete.get() || !isModified.get()) { + if (isLoadingComplete == null || debounceSaver == null + || !isLoadingComplete.get() || !debounceSaver.getIsModified()) { Log.w(TAG, "Attempting to save playlists in bookmark when bookmark " + "is not loaded or playlists not modified"); return; @@ -485,8 +462,8 @@ private void saveImmediate() { remoteItemsUpdate, remoteItemsDeleteUid) .observeOn(AndroidSchedulers.mainThread()) .subscribe(() -> { - if (isModified != null) { - isModified.set(false); + if (debounceSaver != null) { + debounceSaver.setIsModified(false); } }, throwable -> showError(new ErrorInfo(throwable, @@ -544,7 +521,7 @@ public boolean onMove(@NonNull final RecyclerView recyclerView, final int targetIndex = target.getBindingAdapterPosition(); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); if (isSwapped) { - saveChanges(); + debounceSaver.saveChanges(); } return isSwapped; } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 0eb56d7169c..d929c93a55d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -46,6 +46,8 @@ import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.util.DebounceSavable; +import org.schabi.newpipe.util.DebounceSaver; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; @@ -55,7 +57,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; @@ -64,11 +65,9 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -import io.reactivex.rxjava3.subjects.PublishSubject; -public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { - // Save the list 10 seconds after the last change occurred - private static final long SAVE_DEBOUNCE_MILLIS = 10000; +public class LocalPlaylistFragment extends BaseLocalListFragment, Void> + implements DebounceSavable { private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; @State protected Long playlistId; @@ -85,13 +84,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment debouncedSaveSignal; private CompositeDisposable disposables; /* Has the playlist been fully loaded from db */ private AtomicBoolean isLoadingComplete; - /* Has the playlist been modified (e.g. items reordered or deleted) */ - private AtomicBoolean isModified; + + private DebounceSaver debounceSaver; + /* Is the playlist currently being processed to remove watched videos */ private boolean isRemovingWatched = false; @@ -109,12 +108,11 @@ public static LocalPlaylistFragment getInstance(final long playlistId, final Str public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext())); - debouncedSaveSignal = PublishSubject.create(); disposables = new CompositeDisposable(); isLoadingComplete = new AtomicBoolean(); - isModified = new AtomicBoolean(); + debounceSaver = new DebounceSaver(10000, this); } @Override @@ -220,10 +218,13 @@ public void startLoading(final boolean forceLoad) { if (disposables != null) { disposables.clear(); } - disposables.add(getDebouncedSaver()); + + if (debounceSaver != null) { + disposables.add(debounceSaver.getDebouncedSaver()); + debounceSaver.setIsModified(false); + } isLoadingComplete.set(false); - isModified.set(false); playlistManager.getPlaylistStreams(playlistId) .onBackpressureLatest() @@ -285,19 +286,18 @@ public void onDestroyView() { @Override public void onDestroy() { super.onDestroy(); - if (debouncedSaveSignal != null) { - debouncedSaveSignal.onComplete(); + if (debounceSaver != null) { + debounceSaver.getDebouncedSaveSignal().onComplete(); } if (disposables != null) { disposables.dispose(); } - debouncedSaveSignal = null; + debounceSaver = null; playlistManager = null; disposables = null; isLoadingComplete = null; - isModified = null; } /////////////////////////////////////////////////////////////////////////// @@ -321,7 +321,7 @@ public void onSubscribe(final Subscription s) { @Override public void onNext(final List streams) { // Skip handling the result after it has been modified - if (isModified == null || !isModified.get()) { + if (debounceSaver == null || !debounceSaver.getIsModified()) { handleResult(streams); isLoadingComplete.set(true); } @@ -441,7 +441,7 @@ public void removeWatchedStreams(final boolean removePartiallyWatched) { itemListAdapter.clearStreamItemList(); itemListAdapter.addItems(notWatchedItems); - saveChanges(); + debounceSaver.saveChanges(); if (thumbnailVideoRemoved) { @@ -609,39 +609,18 @@ private void deleteItem(final PlaylistStreamEntry item) { } setVideoCount(itemListAdapter.getItemsList().size()); - saveChanges(); + debounceSaver.saveChanges(); } - private void saveChanges() { - if (isModified == null || debouncedSaveSignal == null) { - return; - } - - isModified.set(true); - debouncedSaveSignal.onNext(System.currentTimeMillis()); - } - - private Disposable getDebouncedSaver() { - if (debouncedSaveSignal == null) { - return Disposable.empty(); - } - - return debouncedSaveSignal - .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> saveImmediate(), throwable -> - showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE, - "Debounced saver"))); - } - - private void saveImmediate() { + @Override + public void saveImmediate() { if (playlistManager == null || itemListAdapter == null) { return; } // List must be loaded and modified in order to save - if (isLoadingComplete == null || isModified == null - || !isLoadingComplete.get() || !isModified.get()) { + if (isLoadingComplete == null || debounceSaver == null + || !isLoadingComplete.get() || !debounceSaver.getIsModified()) { Log.w(TAG, "Attempting to save playlist when local playlist " + "is not loaded or not modified: playlist id=[" + playlistId + "]"); return; @@ -664,8 +643,8 @@ private void saveImmediate() { .observeOn(AndroidSchedulers.mainThread()) .subscribe( () -> { - if (isModified != null) { - isModified.set(false); + if (debounceSaver != null) { + debounceSaver.setIsModified(false); } }, throwable -> showError(new ErrorInfo(throwable, @@ -708,7 +687,7 @@ public boolean onMove(@NonNull final RecyclerView recyclerView, final int targetIndex = target.getBindingAdapterPosition(); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); if (isSwapped) { - saveChanges(); + debounceSaver.saveChanges(); } return isSwapped; } diff --git a/app/src/main/java/org/schabi/newpipe/util/DebounceSavable.java b/app/src/main/java/org/schabi/newpipe/util/DebounceSavable.java new file mode 100644 index 00000000000..189dce9c64e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/DebounceSavable.java @@ -0,0 +1,15 @@ +package org.schabi.newpipe.util; + +import org.schabi.newpipe.error.ErrorInfo; + +public interface DebounceSavable { + + /** + * Execute operations to save the data.
+ * Must set {@link DebounceSaver#setIsModified(boolean)} false in this method manually + * after the data has been saved. + */ + void saveImmediate(); + + void showError(ErrorInfo errorInfo); +} diff --git a/app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java b/app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java new file mode 100644 index 00000000000..b17d7a29cc5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java @@ -0,0 +1,68 @@ +package org.schabi.newpipe.util; + +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.UserAction; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.subjects.PublishSubject; + +public class DebounceSaver { + + private final long saveDebounceMillis; + + private final PublishSubject debouncedSaveSignal; + + private final DebounceSavable debounceSavable; + + // Has the object been modified + private final AtomicBoolean isModified; + + + /** + * Creates a new {@code DebounceSaver}. + * + * @param saveDebounceMillis Save the object milliseconds later after the last change + * occurred. + * @param debounceSavable The object containing data to be saved. + */ + public DebounceSaver(final long saveDebounceMillis, final DebounceSavable debounceSavable) { + this.saveDebounceMillis = saveDebounceMillis; + debouncedSaveSignal = PublishSubject.create(); + this.debounceSavable = debounceSavable; + this.isModified = new AtomicBoolean(); + } + + public boolean getIsModified() { + return isModified.get(); + } + + public void setIsModified(final boolean isModified) { + this.isModified.set(isModified); + } + + public PublishSubject getDebouncedSaveSignal() { + return debouncedSaveSignal; + } + + public Disposable getDebouncedSaver() { + return debouncedSaveSignal + .debounce(saveDebounceMillis, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> debounceSavable.saveImmediate(), throwable -> + debounceSavable.showError(new ErrorInfo(throwable, + UserAction.SOMETHING_ELSE, "Debounced saver"))); + } + + public void saveChanges() { + if (isModified == null || debouncedSaveSignal == null) { + return; + } + + isModified.set(true); + debouncedSaveSignal.onNext(System.currentTimeMillis()); + } +} From 6526ff1612ad430325a5bf8af7b425055306a0e6 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Sun, 17 Apr 2022 20:20:20 +0800 Subject: [PATCH 011/154] Add tests --- .../newpipe/database/DatabaseMigrationTest.kt | 92 ++++++++++++++- .../local/bookmark/BookmarkFragment.java | 3 +- .../playlist/PlaylistLocalItemTest.java | 105 ++++++++++++++++++ 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index 6d05a45bf9e..73b6313db00 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -13,6 +13,8 @@ import org.junit.Assert.assertNull import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.schabi.newpipe.database.playlist.model.PlaylistEntity +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity import org.schabi.newpipe.extractor.stream.StreamType @RunWith(AndroidJUnit4::class) @@ -21,13 +23,17 @@ class DatabaseMigrationTest { private const val DEFAULT_SERVICE_ID = 0 private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4" private const val DEFAULT_TITLE = "Test Title" + private const val DEFAULT_NAME = "Test Name" private val DEFAULT_TYPE = StreamType.VIDEO_STREAM private const val DEFAULT_DURATION = 480L private const val DEFAULT_UPLOADER_NAME = "Uploader Test" private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg" - private const val DEFAULT_SECOND_SERVICE_ID = 0 + private const val DEFAULT_SECOND_SERVICE_ID = 1 private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc" + + private const val DEFAULT_THIRD_SERVICE_ID = 2 + private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" } @get:Rule @@ -123,6 +129,90 @@ class DatabaseMigrationTest { assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation) } + @Test + fun migrateDatabaseFrom5to6() { + val databaseInV5 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_5) + + val localUid1: Long + val localUid2: Long + val remoteUid1: Long + val remoteUid2: Long + databaseInV5.run { + localUid1 = insert( + "playlists", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("name", DEFAULT_NAME + "1") + put("thumbnail_url", DEFAULT_THUMBNAIL) + } + ) + localUid2 = insert( + "playlists", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("name", DEFAULT_NAME + "2") + put("thumbnail_url", DEFAULT_THUMBNAIL) + } + ) + delete( + "playlists", "uid = ?", + Array(1) { localUid1 } + ) + remoteUid1 = insert( + "remote_playlists", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("service_id", DEFAULT_SERVICE_ID) + put("url", DEFAULT_URL) + } + ) + remoteUid2 = insert( + "remote_playlists", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("service_id", DEFAULT_SECOND_SERVICE_ID) + put("url", DEFAULT_SECOND_URL) + } + ) + delete( + "remote_playlists", "uid = ?", + Array(1) { remoteUid2 } + ) + close() + } + + testHelper.runMigrationsAndValidate( + AppDatabase.DATABASE_NAME, Migrations.DB_VER_6, + true, Migrations.MIGRATION_5_6 + ) + + val migratedDatabaseV6 = getMigratedDatabase() + var localListFromDB = migratedDatabaseV6.playlistDAO().all.blockingFirst() + var remoteListFromDB = migratedDatabaseV6.playlistRemoteDAO().all.blockingFirst() + + assertEquals(1, localListFromDB.size) + assertEquals(localUid2, localListFromDB[0].uid) + assertEquals(0, localListFromDB[0].displayIndex) + assertEquals(1, remoteListFromDB.size) + assertEquals(remoteUid1, remoteListFromDB[0].uid) + assertEquals(0, remoteListFromDB[0].displayIndex) + + val localUid3 = migratedDatabaseV6.playlistDAO().insert( + PlaylistEntity(DEFAULT_NAME + "3", DEFAULT_THUMBNAIL, -1) + ) + val remoteUid3 = migratedDatabaseV6.playlistRemoteDAO().insert( + PlaylistRemoteEntity( + DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL, + DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10 + ) + ) + + localListFromDB = migratedDatabaseV6.playlistDAO().all.blockingFirst() + remoteListFromDB = migratedDatabaseV6.playlistRemoteDAO().all.blockingFirst() + assertEquals(2, localListFromDB.size) + assertEquals(localUid3, localListFromDB[1].uid) + assertEquals(-1, localListFromDB[1].displayIndex) + assertEquals(2, remoteListFromDB.size) + assertEquals(remoteUid3, remoteListFromDB[1].uid) + assertEquals(-1, remoteListFromDB[1].displayIndex) + } + private fun getMigratedDatabase(): AppDatabase { val database: AppDatabase = Room.databaseBuilder( ApplicationProvider.getApplicationContext(), diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index ceeb980c7e0..a797195252c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -385,7 +385,7 @@ private void checkDisplayIndexModified(@NonNull final List re } } - if (isDisplayIndexModified) { + if (debounceSaver != null && isDisplayIndexModified) { debounceSaver.saveChanges(); } } @@ -588,4 +588,3 @@ private void showDeleteDialog(final String name, final PlaylistLocalItem item) { .show(); } } - diff --git a/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java new file mode 100644 index 00000000000..e5f717144a8 --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java @@ -0,0 +1,105 @@ +package org.schabi.newpipe.database.playlist; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; + +import java.util.ArrayList; +import java.util.List; + +public class PlaylistLocalItemTest { + @Test + public void emptyPlaylists() { + final List localPlaylists = new ArrayList<>(); + final List remotePlaylists = new ArrayList<>(); + final List mergedPlaylists = + PlaylistLocalItem.merge(localPlaylists, remotePlaylists); + + assertEquals(0, mergedPlaylists.size()); + } + + @Test + public void onlyLocalPlaylists() { + final List localPlaylists = new ArrayList<>(); + final List remotePlaylists = new ArrayList<>(); + localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", 2, 1)); + localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", 1, 1)); + localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", 0, 1)); + final List mergedPlaylists = + PlaylistLocalItem.merge(localPlaylists, remotePlaylists); + + assertEquals(3, mergedPlaylists.size()); + assertEquals(0, mergedPlaylists.get(0).getDisplayIndex()); + assertEquals(1, mergedPlaylists.get(1).getDisplayIndex()); + assertEquals(2, mergedPlaylists.get(2).getDisplayIndex()); + } + + @Test + public void onlyRemotePlaylists() { + final List localPlaylists = new ArrayList<>(); + final List remotePlaylists = new ArrayList<>(); + remotePlaylists.add(new PlaylistRemoteEntity( + 1, "name1", "url1", "", "", 2, 1L)); + remotePlaylists.add(new PlaylistRemoteEntity( + 2, "name2", "url2", "", "", 1, 1L)); + remotePlaylists.add(new PlaylistRemoteEntity( + 3, "name3", "url3", "", "", 0, 1L)); + final List mergedPlaylists = + PlaylistLocalItem.merge(localPlaylists, remotePlaylists); + + assertEquals(3, mergedPlaylists.size()); + assertEquals(0, mergedPlaylists.get(0).getDisplayIndex()); + assertEquals(1, mergedPlaylists.get(1).getDisplayIndex()); + assertEquals(2, mergedPlaylists.get(2).getDisplayIndex()); + } + + @Test + public void sameIndexWithDifferentName() { + final List localPlaylists = new ArrayList<>(); + final List remotePlaylists = new ArrayList<>(); + localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", 0, 1)); + localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", 1, 1)); + remotePlaylists.add(new PlaylistRemoteEntity( + 1, "name3", "url1", "", "", 0, 1L)); + remotePlaylists.add(new PlaylistRemoteEntity( + 2, "name4", "url2", "", "", 1, 1L)); + final List mergedPlaylists = + PlaylistLocalItem.merge(localPlaylists, remotePlaylists); + + assertEquals(4, mergedPlaylists.size()); + assertTrue(mergedPlaylists.get(0) instanceof PlaylistMetadataEntry); + assertEquals("name1", ((PlaylistMetadataEntry) mergedPlaylists.get(0)).name); + assertTrue(mergedPlaylists.get(1) instanceof PlaylistRemoteEntity); + assertEquals("name3", ((PlaylistRemoteEntity) mergedPlaylists.get(1)).getName()); + assertTrue(mergedPlaylists.get(2) instanceof PlaylistMetadataEntry); + assertEquals("name2", ((PlaylistMetadataEntry) mergedPlaylists.get(2)).name); + assertTrue(mergedPlaylists.get(3) instanceof PlaylistRemoteEntity); + assertEquals("name4", ((PlaylistRemoteEntity) mergedPlaylists.get(3)).getName()); + } + + @Test + public void sameNameWithDifferentIndex() { + final List localPlaylists = new ArrayList<>(); + final List remotePlaylists = new ArrayList<>(); + localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", 1, 1)); + localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", 3, 1)); + remotePlaylists.add(new PlaylistRemoteEntity( + 1, "name1", "url1", "", "", 0, 1L)); + remotePlaylists.add(new PlaylistRemoteEntity( + 2, "name2", "url2", "", "", 2, 1L)); + final List mergedPlaylists = + PlaylistLocalItem.merge(localPlaylists, remotePlaylists); + + assertEquals(4, mergedPlaylists.size()); + assertTrue(mergedPlaylists.get(0) instanceof PlaylistRemoteEntity); + assertEquals("name1", ((PlaylistRemoteEntity) mergedPlaylists.get(0)).getName()); + assertTrue(mergedPlaylists.get(1) instanceof PlaylistMetadataEntry); + assertEquals("name1", ((PlaylistMetadataEntry) mergedPlaylists.get(1)).name); + assertTrue(mergedPlaylists.get(2) instanceof PlaylistRemoteEntity); + assertEquals("name2", ((PlaylistRemoteEntity) mergedPlaylists.get(2)).getName()); + assertTrue(mergedPlaylists.get(3) instanceof PlaylistMetadataEntry); + assertEquals("name2", ((PlaylistMetadataEntry) mergedPlaylists.get(3)).name); + } +} From d32490a4be288348a8377c84dbf8c82253240bf6 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Wed, 11 May 2022 16:47:34 +0800 Subject: [PATCH 012/154] Create sub-package and default interval for DebounceSaver & sort playlists in db --- .../database/playlist/PlaylistLocalItem.java | 6 +----- .../database/playlist/dao/PlaylistRemoteDAO.java | 5 +++++ .../newpipe/local/bookmark/BookmarkFragment.java | 10 +++++----- .../local/playlist/LocalPlaylistFragment.java | 6 +++--- .../local/playlist/RemotePlaylistManager.java | 4 ++++ .../newpipe/settings/SelectPlaylistFragment.java | 4 ++-- .../util/{ => debounce}/DebounceSavable.java | 2 +- .../util/{ => debounce}/DebounceSaver.java | 15 ++++++++++++++- 8 files changed, 35 insertions(+), 17 deletions(-) rename app/src/main/java/org/schabi/newpipe/util/{ => debounce}/DebounceSavable.java (89%) rename app/src/main/java/org/schabi/newpipe/util/{ => debounce}/DebounceSaver.java (81%) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 0e7beba4129..8b01a636af5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -16,6 +16,7 @@ public interface PlaylistLocalItem extends LocalItem { static List merge( final List localPlaylists, final List remotePlaylists) { + // The playlists from the database must be in the display index order. // Merge localPlaylists and remotePlaylists by display index. // If two items have the same display index, sort them in CASE_INSENSITIVE_ORDER. @@ -25,11 +26,6 @@ static List merge( localPlaylists.size() + remotePlaylists.size()); final List itemsWithSameIndex = new ArrayList<>(); - // The data from database may not be in the display index order - Collections.sort(localPlaylists, - Comparator.comparingLong(PlaylistMetadataEntry::getDisplayIndex)); - Collections.sort(remotePlaylists, - Comparator.comparingLong(PlaylistRemoteEntity::getDisplayIndex)); int i = 0; int j = 0; while (i < localPlaylists.size()) { diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java index ade85746471..8118bc40f16 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java @@ -11,6 +11,7 @@ import io.reactivex.rxjava3.core.Flowable; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; @@ -39,6 +40,10 @@ public interface PlaylistRemoteDAO extends BasicDAO { + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") Flowable> getPlaylist(long serviceId, String url); + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + + " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX) + Flowable> getDisplayIndexOrderedPlaylists(); + @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + REMOTE_PLAYLIST_URL + " = :url " + "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index a797195252c..b0833dd9cc0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -35,8 +35,8 @@ import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; -import org.schabi.newpipe.util.DebounceSavable; -import org.schabi.newpipe.util.DebounceSaver; +import org.schabi.newpipe.util.debounce.DebounceSavable; +import org.schabi.newpipe.util.debounce.DebounceSaver; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; @@ -89,7 +89,7 @@ public void onCreate(final Bundle savedInstanceState) { disposables = new CompositeDisposable(); isLoadingComplete = new AtomicBoolean(); - debounceSaver = new DebounceSaver(10000, this); + debounceSaver = new DebounceSaver(this); displayIndexInDatabase = new HashMap<>(); } @@ -185,8 +185,8 @@ public void startLoading(final boolean forceLoad) { } isLoadingComplete.set(false); - Flowable.combineLatest(localPlaylistManager.getPlaylists(), - remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) + Flowable.combineLatest(localPlaylistManager.getDisplayIndexOrderedPlaylists(), + remotePlaylistManager.getDisplayIndexOrderedPlaylists(), PlaylistLocalItem::merge) .onBackpressureLatest() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getPlaylistsSubscriber()); diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index d929c93a55d..1b8302cac51 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -46,8 +46,8 @@ import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.util.DebounceSavable; -import org.schabi.newpipe.util.DebounceSaver; +import org.schabi.newpipe.util.debounce.DebounceSavable; +import org.schabi.newpipe.util.debounce.DebounceSaver; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; @@ -112,7 +112,7 @@ public void onCreate(final Bundle savedInstanceState) { disposables = new CompositeDisposable(); isLoadingComplete = new AtomicBoolean(); - debounceSaver = new DebounceSaver(10000, this); + debounceSaver = new DebounceSaver(this); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java index 1dbd726aef1..45d4ef644ef 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java @@ -26,6 +26,10 @@ public Flowable> getPlaylists() { return playlistRemoteTable.getAll().subscribeOn(Schedulers.io()); } + public Flowable> getDisplayIndexOrderedPlaylists() { + return playlistRemoteTable.getDisplayIndexOrderedPlaylists().subscribeOn(Schedulers.io()); + } + public Flowable> getPlaylist(final PlaylistInfo info) { return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl()) .subscribeOn(Schedulers.io()); diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java index e8491d52cda..cc47c3f1c7c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -90,8 +90,8 @@ private void loadPlaylists() { final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database); final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database); - disposable = Flowable.combineLatest(localPlaylistManager.getPlaylists(), - remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) + disposable = Flowable.combineLatest(localPlaylistManager.getDisplayIndexOrderedPlaylists(), + remotePlaylistManager.getDisplayIndexOrderedPlaylists(), PlaylistLocalItem::merge) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::displayPlaylists, this::onError); } diff --git a/app/src/main/java/org/schabi/newpipe/util/DebounceSavable.java b/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSavable.java similarity index 89% rename from app/src/main/java/org/schabi/newpipe/util/DebounceSavable.java rename to app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSavable.java index 189dce9c64e..acc515dd670 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DebounceSavable.java +++ b/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSavable.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.util; +package org.schabi.newpipe.util.debounce; import org.schabi.newpipe.error.ErrorInfo; diff --git a/app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java b/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java similarity index 81% rename from app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java rename to app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java index b17d7a29cc5..367174ab79a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DebounceSaver.java +++ b/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.util; +package org.schabi.newpipe.util.debounce; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; @@ -21,6 +21,9 @@ public class DebounceSaver { // Has the object been modified private final AtomicBoolean isModified; + // Default 10 seconds + private static final long DEFAULT_SAVE_DEBOUNCE_MILLIS = 10000; + /** * Creates a new {@code DebounceSaver}. @@ -36,6 +39,16 @@ public DebounceSaver(final long saveDebounceMillis, final DebounceSavable deboun this.isModified = new AtomicBoolean(); } + /** + * Creates a new {@code DebounceSaver}. Save the object 10 seconds later after the last change + * occurred. + * + * @param debounceSavable The object containing data to be saved. + */ + public DebounceSaver(final DebounceSavable debounceSavable) { + this(DEFAULT_SAVE_DEBOUNCE_MILLIS, debounceSavable); + } + public boolean getIsModified() { return isModified.get(); } From ba394a7ab4d3a0c1ee705565c8543e9160d9ebb5 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Wed, 11 May 2022 18:08:14 +0800 Subject: [PATCH 013/154] Update test and Javadoc --- .../database/playlist/PlaylistLocalItem.java | 27 ++++++++++-- .../playlist/PlaylistLocalItemTest.java | 41 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 8b01a636af5..352d12d6bdd 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -13,13 +13,34 @@ public interface PlaylistLocalItem extends LocalItem { long getDisplayIndex(); + /** + * Merge localPlaylists and remotePlaylists by the display index. + * If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}. + * + * @param localPlaylists local playlists in the display index order + * @param remotePlaylists remote playlists in the display index order + * @return merged playlists + */ static List merge( final List localPlaylists, final List remotePlaylists) { - // The playlists from the database must be in the display index order. - // Merge localPlaylists and remotePlaylists by display index. - // If two items have the same display index, sort them in CASE_INSENSITIVE_ORDER. + for (int i = 1; i < localPlaylists.size(); i++) { + if (localPlaylists.get(i).getDisplayIndex() + < localPlaylists.get(i - 1).getDisplayIndex()) { + throw new IllegalArgumentException( + "localPlaylists is not in the display index order"); + } + } + + for (int i = 1; i < remotePlaylists.size(); i++) { + if (remotePlaylists.get(i).getDisplayIndex() + < remotePlaylists.get(i - 1).getDisplayIndex()) { + throw new IllegalArgumentException( + "remotePlaylists is not in the display index order"); + } + } + // This algorithm is similar to the merge operation in merge sort. final List result = new ArrayList<>( diff --git a/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java index e5f717144a8..98f6110376f 100644 --- a/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java +++ b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java @@ -24,16 +24,26 @@ public void emptyPlaylists() { public void onlyLocalPlaylists() { final List localPlaylists = new ArrayList<>(); final List remotePlaylists = new ArrayList<>(); - localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", 2, 1)); + localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", 0, 1)); localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", 1, 1)); - localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", 0, 1)); + localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", 3, 1)); final List mergedPlaylists = PlaylistLocalItem.merge(localPlaylists, remotePlaylists); assertEquals(3, mergedPlaylists.size()); assertEquals(0, mergedPlaylists.get(0).getDisplayIndex()); assertEquals(1, mergedPlaylists.get(1).getDisplayIndex()); - assertEquals(2, mergedPlaylists.get(2).getDisplayIndex()); + assertEquals(3, mergedPlaylists.get(2).getDisplayIndex()); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidLocalPlaylists() { + final List localPlaylists = new ArrayList<>(); + final List remotePlaylists = new ArrayList<>(); + localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", 2, 1)); + localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", 1, 1)); + localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", 0, 1)); + PlaylistLocalItem.merge(localPlaylists, remotePlaylists); } @Test @@ -41,18 +51,31 @@ public void onlyRemotePlaylists() { final List localPlaylists = new ArrayList<>(); final List remotePlaylists = new ArrayList<>(); remotePlaylists.add(new PlaylistRemoteEntity( - 1, "name1", "url1", "", "", 2, 1L)); + 1, "name1", "url1", "", "", 1, 1L)); remotePlaylists.add(new PlaylistRemoteEntity( - 2, "name2", "url2", "", "", 1, 1L)); + 2, "name2", "url2", "", "", 2, 1L)); remotePlaylists.add(new PlaylistRemoteEntity( - 3, "name3", "url3", "", "", 0, 1L)); + 3, "name3", "url3", "", "", 4, 1L)); final List mergedPlaylists = PlaylistLocalItem.merge(localPlaylists, remotePlaylists); assertEquals(3, mergedPlaylists.size()); - assertEquals(0, mergedPlaylists.get(0).getDisplayIndex()); - assertEquals(1, mergedPlaylists.get(1).getDisplayIndex()); - assertEquals(2, mergedPlaylists.get(2).getDisplayIndex()); + assertEquals(1, mergedPlaylists.get(0).getDisplayIndex()); + assertEquals(2, mergedPlaylists.get(1).getDisplayIndex()); + assertEquals(4, mergedPlaylists.get(2).getDisplayIndex()); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidRemotePlaylists() { + final List localPlaylists = new ArrayList<>(); + final List remotePlaylists = new ArrayList<>(); + remotePlaylists.add(new PlaylistRemoteEntity( + 1, "name1", "url1", "", "", 1, 1L)); + remotePlaylists.add(new PlaylistRemoteEntity( + 2, "name2", "url2", "", "", 3, 1L)); + remotePlaylists.add(new PlaylistRemoteEntity( + 3, "name3", "url3", "", "", 0, 1L)); + PlaylistLocalItem.merge(localPlaylists, remotePlaylists); } @Test From 9ecef6f01103fc3aa0c719dd7abea4169d06c7d6 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Thu, 23 Jun 2022 19:20:16 +0800 Subject: [PATCH 014/154] Add abstract methods in PlaylistLocalItem & rename setIsModified --- .../database/playlist/PlaylistLocalItem.java | 4 ++++ .../playlist/PlaylistMetadataEntry.java | 14 +++++++++-- .../playlist/model/PlaylistEntity.java | 4 ++-- .../playlist/model/PlaylistRemoteEntity.java | 2 ++ .../local/bookmark/BookmarkFragment.java | 23 +++++++++---------- .../local/dialog/PlaylistAppendDialog.java | 4 ++-- .../local/playlist/LocalPlaylistFragment.java | 4 ++-- .../settings/SelectPlaylistFragment.java | 2 +- .../newpipe/util/debounce/DebounceSaver.java | 4 ++-- 9 files changed, 38 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 352d12d6bdd..3d58d3f7c21 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -13,6 +13,10 @@ public interface PlaylistLocalItem extends LocalItem { long getDisplayIndex(); + long getUid(); + + void setDisplayIndex(long displayIndex); + /** * Merge localPlaylists and remotePlaylists by the display index. * If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}. diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index ff80049a30a..f1ead0fa40d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -11,13 +11,13 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem { public static final String PLAYLIST_STREAM_COUNT = "streamCount"; @ColumnInfo(name = PLAYLIST_ID) - public final long uid; + private final long uid; @ColumnInfo(name = PLAYLIST_NAME) public final String name; @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) public final String thumbnailUrl; @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) - public long displayIndex; + private long displayIndex; @ColumnInfo(name = PLAYLIST_STREAM_COUNT) public final long streamCount; @@ -44,4 +44,14 @@ public String getOrderingName() { public long getDisplayIndex() { return displayIndex; } + + @Override + public long getUid() { + return uid; + } + + @Override + public void setDisplayIndex(final long displayIndex) { + this.displayIndex = displayIndex; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java index 272e8a5bcdd..508b555085a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -41,10 +41,10 @@ public PlaylistEntity(final String name, final String thumbnailUrl, final long d @Ignore public PlaylistEntity(final PlaylistMetadataEntry item) { - this.uid = item.uid; + this.uid = item.getUid(); this.name = item.name; this.thumbnailUrl = item.thumbnailUrl; - this.displayIndex = item.displayIndex; + this.displayIndex = item.getDisplayIndex(); } public long getUid() { diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java index adea2738bda..82baed82ca8 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -105,6 +105,7 @@ && getStreamCount() == info.getStreamCount() && TextUtils.equals(getUploader(), info.getUploaderName()); } + @Override public long getUid() { return uid; } @@ -158,6 +159,7 @@ public long getDisplayIndex() { return displayIndex; } + @Override public void setDisplayIndex(final long displayIndex) { this.displayIndex = displayIndex; } diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index b0833dd9cc0..e9cf8323918 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -139,7 +139,7 @@ public void selected(final LocalItem selectedItem) { if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid, + NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(), entry.name); } else if (selectedItem instanceof PlaylistRemoteEntity) { @@ -181,7 +181,7 @@ public void startLoading(final boolean forceLoad) { if (debounceSaver != null) { disposables.add(debounceSaver.getDebouncedSaver()); - debounceSaver.setIsModified(false); + debounceSaver.setNoChangesToSave(); } isLoadingComplete.set(false); @@ -371,16 +371,15 @@ private void checkDisplayIndexModified(@NonNull final List re // Save the index read from the database. if (item instanceof PlaylistMetadataEntry) { - displayIndexInDatabase.put(new Pair<>(((PlaylistMetadataEntry) item).uid, + displayIndexInDatabase.put(new Pair<>(item.getUid(), LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM), item.getDisplayIndex()); - ((PlaylistMetadataEntry) item).displayIndex = i; + item.setDisplayIndex(i); } else if (item instanceof PlaylistRemoteEntity) { - displayIndexInDatabase.put(new Pair<>(((PlaylistRemoteEntity) item).getUid(), - LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM), - item.getDisplayIndex()); - ((PlaylistRemoteEntity) item).setDisplayIndex(i); + displayIndexInDatabase.put(new Pair<>(item.getUid(), + LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM), item.getDisplayIndex()); + item.setDisplayIndex(i); } } @@ -415,9 +414,9 @@ public void saveImmediate() { final LocalItem item = items.get(i); if (item instanceof PlaylistMetadataEntry) { - ((PlaylistMetadataEntry) item).displayIndex = i; + ((PlaylistMetadataEntry) item).setDisplayIndex(i); - final Long uid = ((PlaylistMetadataEntry) item).uid; + final Long uid = ((PlaylistMetadataEntry) item).getUid(); final Pair key = new Pair<>(uid, LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM); final Long databaseIndex = displayIndexInDatabase.remove(key); @@ -463,7 +462,7 @@ public void saveImmediate() { .observeOn(AndroidSchedulers.mainThread()) .subscribe(() -> { if (debounceSaver != null) { - debounceSaver.setIsModified(false); + debounceSaver.setNoChangesToSave(); } }, throwable -> showError(new ErrorInfo(throwable, @@ -563,7 +562,7 @@ private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { builder.setView(dialogBinding.getRoot()) .setPositiveButton(R.string.rename_playlist, (dialog, which) -> changeLocalPlaylistName( - selectedItem.uid, + selectedItem.getUid(), dialogBinding.dialogEditText.getText().toString())) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.delete, (dialog, which) -> { diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java index 58a10af220d..a778e6578eb 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java @@ -147,12 +147,12 @@ private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) { playlistDisposables.add(manager - .changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl()) + .changePlaylistThumbnail(playlist.getUid(), streams.get(0).getThumbnailUrl()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> successToast.show())); } - playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams) + playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> successToast.show())); diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 1b8302cac51..d129e658e53 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -221,7 +221,7 @@ public void startLoading(final boolean forceLoad) { if (debounceSaver != null) { disposables.add(debounceSaver.getDebouncedSaver()); - debounceSaver.setIsModified(false); + debounceSaver.setNoChangesToSave(); } isLoadingComplete.set(false); @@ -644,7 +644,7 @@ public void saveImmediate() { .subscribe( () -> { if (debounceSaver != null) { - debounceSaver.setIsModified(false); + debounceSaver.setNoChangesToSave(); } }, throwable -> showError(new ErrorInfo(throwable, diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java index cc47c3f1c7c..905a44fd11f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -118,7 +118,7 @@ private void clickedItem(final int position) { if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - onSelectedListener.onLocalPlaylistSelected(entry.uid, entry.name); + onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name); } else if (selectedItem instanceof PlaylistRemoteEntity) { final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); diff --git a/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java b/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java index 367174ab79a..911e978ff47 100644 --- a/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java +++ b/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java @@ -53,8 +53,8 @@ public boolean getIsModified() { return isModified.get(); } - public void setIsModified(final boolean isModified) { - this.isModified.set(isModified); + public void setNoChangesToSave() { + isModified.set(false); } public PublishSubject getDebouncedSaveSignal() { From 4e401bc059ae1ede7f452fcfc68dd6d8c8987500 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Thu, 23 Jun 2022 20:36:21 +0800 Subject: [PATCH 015/154] Update playlists in parallel --- .../local/bookmark/BookmarkFragment.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index e9cf8323918..9b93cc3e6c9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -454,21 +454,16 @@ public void saveImmediate() { // 1. Update local playlists // 2. Update remote playlists - // 3. Set isModified false + // 3. Set NoChangesToSave disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid) + .mergeWith(remotePlaylistManager.updatePlaylists( + remoteItemsUpdate, remoteItemsDeleteUid)) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> disposables.add(remotePlaylistManager.updatePlaylists( - remoteItemsUpdate, remoteItemsDeleteUid) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - if (debounceSaver != null) { - debounceSaver.setNoChangesToSave(); - } - }, - throwable -> showError(new ErrorInfo(throwable, - UserAction.REQUESTED_BOOKMARK, - "Saving playlist")) - )), + .subscribe(() -> { + if (debounceSaver != null) { + debounceSaver.setNoChangesToSave(); + } + }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, "Saving playlist")) )); From 898a936064f19830779b0bf22f7c75b4bd4dd4df Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Thu, 23 Jun 2022 23:19:59 +0800 Subject: [PATCH 016/154] Update index modification logic & redo sorting in the merge algorithm --- .../database/playlist/PlaylistLocalItem.java | 23 ++--- .../local/bookmark/BookmarkFragment.java | 86 +++++++------------ .../local/playlist/LocalPlaylistFragment.java | 6 +- .../newpipe/util/debounce/DebounceSaver.java | 2 +- .../playlist/PlaylistLocalItemTest.java | 23 ----- 5 files changed, 39 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 3d58d3f7c21..4314b0f82f0 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -21,29 +21,18 @@ public interface PlaylistLocalItem extends LocalItem { * Merge localPlaylists and remotePlaylists by the display index. * If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}. * - * @param localPlaylists local playlists in the display index order - * @param remotePlaylists remote playlists in the display index order + * @param localPlaylists local playlists + * @param remotePlaylists remote playlists * @return merged playlists */ static List merge( final List localPlaylists, final List remotePlaylists) { - for (int i = 1; i < localPlaylists.size(); i++) { - if (localPlaylists.get(i).getDisplayIndex() - < localPlaylists.get(i - 1).getDisplayIndex()) { - throw new IllegalArgumentException( - "localPlaylists is not in the display index order"); - } - } - - for (int i = 1; i < remotePlaylists.size(); i++) { - if (remotePlaylists.get(i).getDisplayIndex() - < remotePlaylists.get(i - 1).getDisplayIndex()) { - throw new IllegalArgumentException( - "remotePlaylists is not in the display index order"); - } - } + Collections.sort(localPlaylists, + Comparator.comparingLong(PlaylistMetadataEntry::getDisplayIndex)); + Collections.sort(remotePlaylists, + Comparator.comparingLong(PlaylistRemoteEntity::getDisplayIndex)); // This algorithm is similar to the merge operation in merge sort. diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index 9b93cc3e6c9..200fde56206 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -41,9 +41,7 @@ import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; @@ -70,8 +68,7 @@ public final class BookmarkFragment extends BaseLocalListFragment, Long> displayIndexInDatabase; + private List> deletedItems; /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Creation @@ -89,9 +86,9 @@ public void onCreate(final Bundle savedInstanceState) { disposables = new CompositeDisposable(); isLoadingComplete = new AtomicBoolean(); - debounceSaver = new DebounceSaver(this); + debounceSaver = new DebounceSaver(3000, this); - displayIndexInDatabase = new HashMap<>(); + deletedItems = new ArrayList<>(); } @Nullable @@ -186,7 +183,8 @@ public void startLoading(final boolean forceLoad) { isLoadingComplete.set(false); Flowable.combineLatest(localPlaylistManager.getDisplayIndexOrderedPlaylists(), - remotePlaylistManager.getDisplayIndexOrderedPlaylists(), PlaylistLocalItem::merge) + remotePlaylistManager.getDisplayIndexOrderedPlaylists(), + PlaylistLocalItem::merge) .onBackpressureLatest() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getPlaylistsSubscriber()); @@ -237,7 +235,7 @@ public void onDestroy() { itemsListState = null; isLoadingComplete = null; - displayIndexInDatabase = null; + deletedItems = null; } /////////////////////////////////////////////////////////////////////////// @@ -343,7 +341,15 @@ private void deleteItem(final PlaylistLocalItem item) { } itemListAdapter.removeItem(item); - debounceSaver.saveChanges(); + if (item instanceof PlaylistMetadataEntry) { + deletedItems.add(new Pair<>(item.getUid(), + LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)); + } else if (item instanceof PlaylistRemoteEntity) { + deletedItems.add(new Pair<>(item.getUid(), + LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)); + } + + debounceSaver.setHasChangesToSave(); } private void checkDisplayIndexModified(@NonNull final List result) { @@ -351,9 +357,7 @@ private void checkDisplayIndexModified(@NonNull final List re return; } - displayIndexInDatabase.clear(); - - // If the display index does not match actual index in the list, update the display index. + // Check if the display index does not match the actual index in the list. // This may happen when a new list is created // or on the first run after database migration // or display index is not continuous for some reason @@ -363,29 +367,12 @@ private void checkDisplayIndexModified(@NonNull final List re final PlaylistLocalItem item = result.get(i); if (item.getDisplayIndex() != i) { isDisplayIndexModified = true; - } - - // Updating display index in the item does not affect the value inserts into - // database, which will be recalculated during the database update. Updating - // display index in the item here is to determine whether it is recently modified. - // Save the index read from the database. - if (item instanceof PlaylistMetadataEntry) { - - displayIndexInDatabase.put(new Pair<>(item.getUid(), - LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM), item.getDisplayIndex()); - item.setDisplayIndex(i); - - } else if (item instanceof PlaylistRemoteEntity) { - - displayIndexInDatabase.put(new Pair<>(item.getUid(), - LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM), item.getDisplayIndex()); - item.setDisplayIndex(i); - + break; } } if (debounceSaver != null && isDisplayIndexModified) { - debounceSaver.saveChanges(); + debounceSaver.setHasChangesToSave(); } } @@ -414,43 +401,28 @@ public void saveImmediate() { final LocalItem item = items.get(i); if (item instanceof PlaylistMetadataEntry) { - ((PlaylistMetadataEntry) item).setDisplayIndex(i); - - final Long uid = ((PlaylistMetadataEntry) item).getUid(); - final Pair key = new Pair<>(uid, - LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM); - final Long databaseIndex = displayIndexInDatabase.remove(key); - - // The database index should not be null because inserting new item into database - // is not handled here. NullPointerException has occurred once, but I can't - // reproduce it. Enhance robustness here. - if (databaseIndex != null && databaseIndex != i) { + if (((PlaylistMetadataEntry) item).getDisplayIndex() != i) { + ((PlaylistMetadataEntry) item).setDisplayIndex(i); localItemsUpdate.add((PlaylistMetadataEntry) item); } } else if (item instanceof PlaylistRemoteEntity) { - ((PlaylistRemoteEntity) item).setDisplayIndex(i); - - final Long uid = ((PlaylistRemoteEntity) item).getUid(); - final Pair key = new Pair<>(uid, - LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM); - final Long databaseIndex = displayIndexInDatabase.remove(key); - - if (databaseIndex != null && databaseIndex != i) { + if (((PlaylistRemoteEntity) item).getDisplayIndex() != i) { + ((PlaylistRemoteEntity) item).setDisplayIndex(i); remoteItemsUpdate.add((PlaylistRemoteEntity) item); } } } // Find deleted items - for (final Pair key : displayIndexInDatabase.keySet()) { - if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) { - localItemsDeleteUid.add(key.first); - } else if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) { - remoteItemsDeleteUid.add(key.first); + for (final Pair item : deletedItems) { + if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) { + localItemsDeleteUid.add(item.first); + } else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) { + remoteItemsDeleteUid.add(item.first); } } - displayIndexInDatabase.clear(); + deletedItems.clear(); // 1. Update local playlists // 2. Update remote playlists @@ -515,7 +487,7 @@ public boolean onMove(@NonNull final RecyclerView recyclerView, final int targetIndex = target.getBindingAdapterPosition(); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); if (isSwapped) { - debounceSaver.saveChanges(); + debounceSaver.setHasChangesToSave(); } return isSwapped; } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index d129e658e53..10e5aea15cc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -441,7 +441,7 @@ public void removeWatchedStreams(final boolean removePartiallyWatched) { itemListAdapter.clearStreamItemList(); itemListAdapter.addItems(notWatchedItems); - debounceSaver.saveChanges(); + debounceSaver.setHasChangesToSave(); if (thumbnailVideoRemoved) { @@ -609,7 +609,7 @@ private void deleteItem(final PlaylistStreamEntry item) { } setVideoCount(itemListAdapter.getItemsList().size()); - debounceSaver.saveChanges(); + debounceSaver.setHasChangesToSave(); } @Override @@ -687,7 +687,7 @@ public boolean onMove(@NonNull final RecyclerView recyclerView, final int targetIndex = target.getBindingAdapterPosition(); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); if (isSwapped) { - debounceSaver.saveChanges(); + debounceSaver.setHasChangesToSave(); } return isSwapped; } diff --git a/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java b/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java index 911e978ff47..5bd5cdd55f4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java +++ b/app/src/main/java/org/schabi/newpipe/util/debounce/DebounceSaver.java @@ -70,7 +70,7 @@ public Disposable getDebouncedSaver() { UserAction.SOMETHING_ELSE, "Debounced saver"))); } - public void saveChanges() { + public void setHasChangesToSave() { if (isModified == null || debouncedSaveSignal == null) { return; } diff --git a/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java index 98f6110376f..ab6315d9170 100644 --- a/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java +++ b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java @@ -36,16 +36,6 @@ public void onlyLocalPlaylists() { assertEquals(3, mergedPlaylists.get(2).getDisplayIndex()); } - @Test(expected = IllegalArgumentException.class) - public void invalidLocalPlaylists() { - final List localPlaylists = new ArrayList<>(); - final List remotePlaylists = new ArrayList<>(); - localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", 2, 1)); - localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", 1, 1)); - localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", 0, 1)); - PlaylistLocalItem.merge(localPlaylists, remotePlaylists); - } - @Test public void onlyRemotePlaylists() { final List localPlaylists = new ArrayList<>(); @@ -65,19 +55,6 @@ public void onlyRemotePlaylists() { assertEquals(4, mergedPlaylists.get(2).getDisplayIndex()); } - @Test(expected = IllegalArgumentException.class) - public void invalidRemotePlaylists() { - final List localPlaylists = new ArrayList<>(); - final List remotePlaylists = new ArrayList<>(); - remotePlaylists.add(new PlaylistRemoteEntity( - 1, "name1", "url1", "", "", 1, 1L)); - remotePlaylists.add(new PlaylistRemoteEntity( - 2, "name2", "url2", "", "", 3, 1L)); - remotePlaylists.add(new PlaylistRemoteEntity( - 3, "name3", "url3", "", "", 0, 1L)); - PlaylistLocalItem.merge(localPlaylists, remotePlaylists); - } - @Test public void sameIndexWithDifferentName() { final List localPlaylists = new ArrayList<>(); From 8ad7bf60d7ed3bcde5bda0d601ae5d920c042eb3 Mon Sep 17 00:00:00 2001 From: GGAutomaton <32899400+GGAutomaton@users.noreply.github.com> Date: Thu, 23 Jun 2022 23:31:56 +0800 Subject: [PATCH 017/154] Delete saveImmediate warnings & add comments --- .../org/schabi/newpipe/local/bookmark/BookmarkFragment.java | 3 +-- .../schabi/newpipe/local/playlist/LocalPlaylistFragment.java | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index 200fde56206..4be4838ec3c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -66,6 +66,7 @@ public final class BookmarkFragment extends BaseLocalListFragment> deletedItems; @@ -385,8 +386,6 @@ public void saveImmediate() { // List must be loaded and modified in order to save if (isLoadingComplete == null || debounceSaver == null || !isLoadingComplete.get() || !debounceSaver.getIsModified()) { - Log.w(TAG, "Attempting to save playlists in bookmark when bookmark " - + "is not loaded or playlists not modified"); return; } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 10e5aea15cc..5ec51166030 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -621,8 +621,6 @@ public void saveImmediate() { // List must be loaded and modified in order to save if (isLoadingComplete == null || debounceSaver == null || !isLoadingComplete.get() || !debounceSaver.getIsModified()) { - Log.w(TAG, "Attempting to save playlist when local playlist " - + "is not loaded or not modified: playlist id=[" + playlistId + "]"); return; } From 90f0809029a257f62c442eede1735e2386e8aed1 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Wed, 16 Aug 2023 21:24:55 +0200 Subject: [PATCH 018/154] Trim search string and remove duplicate records from the database Co-authored-by: Yingwei Zheng --- .../8.json | 737 ++++++++++++++++++ .../newpipe/database/DatabaseMigrationTest.kt | 61 +- .../org/schabi/newpipe/NewPipeDatabase.java | 3 +- .../schabi/newpipe/database/AppDatabase.java | 4 +- .../schabi/newpipe/database/Migrations.java | 10 + .../fragments/list/search/SearchFragment.java | 42 +- 6 files changed, 842 insertions(+), 15 deletions(-) create mode 100644 app/schemas/org.schabi.newpipe.database.AppDatabase/8.json diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/8.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/8.json new file mode 100644 index 00000000000..d4a89567b8f --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/8.json @@ -0,0 +1,737 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "012fc8e7ad3333f1597347f34e76a513", + "entities": [ + { + "tableName": "subscriptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notificationMode", + "columnName": "notification_mode", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "streams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "streamType", + "columnName": "stream_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploaderUrl", + "columnName": "uploader_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viewCount", + "columnName": "view_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "textualUploadDate", + "columnName": "textual_upload_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadDate", + "columnName": "upload_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isUploadDateApproximation", + "columnName": "is_upload_date_approximation", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "stream_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessDate", + "columnName": "access_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatCount", + "columnName": "repeat_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id", + "access_date" + ] + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "stream_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressMillis", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isThumbnailPermanent", + "columnName": "is_thumbnail_permanent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailStreamId", + "columnName": "thumbnail_stream_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "playlist_stream_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "playlistUid", + "columnName": "playlist_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "join_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "playlist_id", + "join_index" + ] + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" + }, + { + "name": "index_playlist_stream_join_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "remote_playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_remote_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id", + "subscription_id" + ] + }, + "indices": [ + { + "name": "index_feed_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_group", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_feed_group_sort_order", + "unique": false, + "columnNames": [ + "sort_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed_group_subscription_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "feedGroupId", + "columnName": "group_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "group_id", + "subscription_id" + ] + }, + "indices": [ + { + "name": "index_feed_group_subscription_join_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "feed_group", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "group_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_last_updated", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "subscription_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + } + ], + "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, '012fc8e7ad3333f1597347f34e76a513')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index 88e7372051b..cd048ac44af 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -8,6 +8,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNull import org.junit.Rule import org.junit.Test @@ -25,8 +26,11 @@ class DatabaseMigrationTest { private const val DEFAULT_UPLOADER_NAME = "Uploader Test" private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg" - private const val DEFAULT_SECOND_SERVICE_ID = 0 + private const val DEFAULT_SECOND_SERVICE_ID = 1 private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc" + + private const val DEFAULT_SEARCH1 = " abc " + private const val DEFAULT_SEARCH2 = " abc" } @get:Rule @@ -106,6 +110,11 @@ class DatabaseMigrationTest { Migrations.MIGRATION_6_7 ) + testHelper.runMigrationsAndValidate( + AppDatabase.DATABASE_NAME, Migrations.DB_VER_6, + true, Migrations.MIGRATION_5_6 + ) + val migratedDatabaseV3 = getMigratedDatabase() val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() @@ -140,6 +149,56 @@ class DatabaseMigrationTest { assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation) } + @Test + fun migrateDatabaseFrom5to6() { + val databaseInV5 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_5) + + databaseInV5.run { + insert( + "search_history", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("service_id", DEFAULT_SERVICE_ID) + put("search", DEFAULT_SEARCH1) + } + ) + insert( + "search_history", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("service_id", DEFAULT_SERVICE_ID) + put("search", DEFAULT_SEARCH2) + } + ) + insert( + "search_history", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("service_id", DEFAULT_SECOND_SERVICE_ID) + put("search", DEFAULT_SEARCH1) + } + ) + insert( + "search_history", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("service_id", DEFAULT_SECOND_SERVICE_ID) + put("search", DEFAULT_SEARCH2) + } + ) + close() + } + + testHelper.runMigrationsAndValidate( + AppDatabase.DATABASE_NAME, Migrations.DB_VER_6, + true, Migrations.MIGRATION_5_6 + ) + + val migratedDatabaseV6 = getMigratedDatabase() + val listFromDB = migratedDatabaseV6.searchHistoryDAO().all.blockingFirst() + + assertEquals(2, listFromDB.size) + assertEquals("abc", listFromDB[0].search) + assertEquals("abc", listFromDB[1].search) + assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId) + } + private fun getMigratedDatabase(): AppDatabase { val database: AppDatabase = Room.databaseBuilder( ApplicationProvider.getApplicationContext(), diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 856fbff8b6d..c4f9feba7a8 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -7,6 +7,7 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5; import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6; import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7; +import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8; import android.content.Context; import android.database.Cursor; @@ -27,7 +28,7 @@ private static AppDatabase getDatabase(final Context context) { return Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, - MIGRATION_5_6, MIGRATION_6_7) + MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8) .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 03e39cd4395..d03823e66dd 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -1,6 +1,6 @@ package org.schabi.newpipe.database; -import static org.schabi.newpipe.database.Migrations.DB_VER_7; +import static org.schabi.newpipe.database.Migrations.DB_VER_8; import androidx.room.Database; import androidx.room.RoomDatabase; @@ -38,7 +38,7 @@ FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, FeedLastUpdatedEntity.class }, - version = DB_VER_7 + version = DB_VER_8 ) public abstract class AppDatabase extends RoomDatabase { public static final String DATABASE_NAME = "newpipe.db"; diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 1886b87c2d8..65c5626a52a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -25,6 +25,7 @@ public final class Migrations { public static final int DB_VER_5 = 5; public static final int DB_VER_6 = 6; public static final int DB_VER_7 = 7; + public static final int DB_VER_8 = 8; private static final String TAG = Migrations.class.getName(); public static final boolean DEBUG = MainActivity.DEBUG; @@ -235,6 +236,15 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) { } }; + public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) { + @Override + public void migrate(@NonNull final SupportSQLiteDatabase database) { + database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM " + + "(SELECT id FROM search_history GROUP BY trim(search), service_id) tmp)"); + database.execSQL("UPDATE search_history SET search = trim(search)"); + } + }; + private Migrations() { } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 26a28322921..87c48b69cdf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -384,6 +384,7 @@ public void readFrom(@NonNull final Queue savedObjects) throws Exception @Override public void onSaveInstanceState(@NonNull final Bundle bundle) { + searchEditText.setText(searchEditText.getText().toString().trim()); searchString = searchEditText != null ? searchEditText.getText().toString() : searchString; @@ -396,8 +397,8 @@ public void onSaveInstanceState(@NonNull final Bundle bundle) { @Override public void reloadContent() { - if (!TextUtils.isEmpty(searchString) - || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { + if (!TextUtils.isEmpty(searchString) || (searchEditText != null + && TextUtils.getTrimmedLength(searchEditText.getText()) > 0)) { search(!TextUtils.isEmpty(searchString) ? searchString : searchEditText.getText().toString(), this.contentFilter, ""); @@ -494,7 +495,8 @@ private void showSearchOnStart() { } searchEditText.setText(searchString); - if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) { + if (TextUtils.isEmpty(searchString) + || TextUtils.getTrimmedLength(searchEditText.getText()) == 0) { searchToolbarContainer.setTranslationX(100); searchToolbarContainer.setAlpha(0.0f); searchToolbarContainer.setVisibility(View.VISIBLE); @@ -518,7 +520,7 @@ private void initSearchListeners() { if (DEBUG) { Log.d(TAG, "onClick() called with: v = [" + v + "]"); } - if (TextUtils.isEmpty(searchEditText.getText())) { + if (TextUtils.getTrimmedLength(searchEditText.getText()) == 0) { NavigationHelper.gotoMainFragment(getFM()); return; } @@ -580,9 +582,12 @@ public void onSuggestionItemLongClick(final SuggestionItem item) { searchEditText.removeTextChangedListener(textWatcher); } textWatcher = new TextWatcher() { + private boolean isPastedText = false; + @Override public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { + isPastedText = TextUtils.isEmpty(s) && after > 1; } @Override @@ -597,8 +602,13 @@ public void afterTextChanged(final Editable s) { s.removeSpan(span); } - final String newText = searchEditText.getText().toString(); + final String newText = searchEditText.getText().toString().trim(); suggestionPublisher.onNext(newText); + + if (isPastedText) { + // trim pasted text + searchEditText.setText(newText); + } } }; searchEditText.addTextChangedListener(textWatcher); @@ -613,6 +623,7 @@ public void afterTextChanged(final Editable s) { } else if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { + searchEditText.setText(searchEditText.getText().toString().trim()); search(searchEditText.getText().toString(), new String[0], ""); return true; } @@ -717,9 +728,9 @@ private Observable> getLocalSuggestionsObservable( .getRelatedSearches(query, similarQueryLimit, 25) .toObservable() .map(searchHistoryEntries -> - searchHistoryEntries.stream() - .map(entry -> new SuggestionItem(true, entry)) - .collect(Collectors.toList())); + searchHistoryEntries.stream() + .map(entry -> new SuggestionItem(true, entry)) + .collect(Collectors.toList())); } private Observable> getRemoteSuggestionsObservable(final String query) { @@ -786,12 +797,12 @@ private void initSuggestionObserver() { } else if (listNotification.isOnError() && listNotification.getError() != null && !ExceptionUtils.isInterruptedCaused( - listNotification.getError())) { + listNotification.getError())) { showSnackBarError(new ErrorInfo(listNotification.getError(), UserAction.GET_SUGGESTIONS, searchString, serviceId)); } }, throwable -> showSnackBarError(new ErrorInfo( - throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId))); + throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId))); } @Override @@ -804,6 +815,11 @@ private void search(final String theSearchString, final String theSortFilter) { if (DEBUG) { Log.d(TAG, "search() called with: query = [" + theSearchString + "]"); + final String trimmedSearchString = theSearchString.trim(); + if (!trimmedSearchString.equals(theSearchString)) { + Log.d(TAG, "The precondition is not satisfied. " + + "\"theSearchString\" is not allowed to have leading or trailing spaces"); + } } if (theSearchString.isEmpty()) { return; @@ -839,7 +855,8 @@ private void search(final String theSearchString, disposables.add(historyRecordManager.onSearched(serviceId, theSearchString) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - ignored -> { }, + ignored -> { + }, throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED, theSearchString, serviceId)) )); @@ -973,6 +990,9 @@ public void handleResult(@NonNull final SearchInfo result) { } searchSuggestion = result.getSearchSuggestion(); + if (searchSuggestion != null) { + searchSuggestion = searchSuggestion.trim(); + } isCorrectedSearch = result.isCorrectedSearch(); // List cannot be bundled without creating some containers From 4af5b5f6f21c28b718224690a6239d13f019f032 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Wed, 16 Aug 2023 22:01:58 +0200 Subject: [PATCH 019/154] Fix database migration and string trimming Co-authored-by: Yingwei Zheng --- .../newpipe/database/DatabaseMigrationTest.kt | 23 +++++++++++-------- .../schabi/newpipe/database/Migrations.java | 4 ++-- .../fragments/list/search/SearchFragment.java | 17 ++++---------- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index cd048ac44af..c0c608a5826 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -111,8 +111,10 @@ class DatabaseMigrationTest { ) testHelper.runMigrationsAndValidate( - AppDatabase.DATABASE_NAME, Migrations.DB_VER_6, - true, Migrations.MIGRATION_5_6 + AppDatabase.DATABASE_NAME, + Migrations.DB_VER_8, + true, + Migrations.MIGRATION_7_8 ) val migratedDatabaseV3 = getMigratedDatabase() @@ -150,10 +152,13 @@ class DatabaseMigrationTest { } @Test - fun migrateDatabaseFrom5to6() { - val databaseInV5 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_5) + fun migrateDatabaseFrom7to8() { + val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7) + + val defaultSearch1 = " abc " + val defaultSearch2 = " abc" - databaseInV5.run { + databaseInV7.run { insert( "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { @@ -186,12 +191,12 @@ class DatabaseMigrationTest { } testHelper.runMigrationsAndValidate( - AppDatabase.DATABASE_NAME, Migrations.DB_VER_6, - true, Migrations.MIGRATION_5_6 + AppDatabase.DATABASE_NAME, Migrations.DB_VER_8, + true, Migrations.MIGRATION_7_8 ) - val migratedDatabaseV6 = getMigratedDatabase() - val listFromDB = migratedDatabaseV6.searchHistoryDAO().all.blockingFirst() + val migratedDatabaseV8 = getMigratedDatabase() + val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst() assertEquals(2, listFromDB.size) assertEquals("abc", listFromDB[0].search) diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 65c5626a52a..4b1a34dd6fd 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -239,8 +239,8 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) { public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) { @Override public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM " - + "(SELECT id FROM search_history GROUP BY trim(search), service_id) tmp)"); + database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT " + + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)"); database.execSQL("UPDATE search_history SET search = trim(search)"); } }; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 87c48b69cdf..df4a47bc523 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.fragments.list.search; import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; import static java.util.Arrays.asList; @@ -398,7 +399,7 @@ public void onSaveInstanceState(@NonNull final Bundle bundle) { @Override public void reloadContent() { if (!TextUtils.isEmpty(searchString) || (searchEditText != null - && TextUtils.getTrimmedLength(searchEditText.getText()) > 0)) { + && !isBlank(searchEditText.getText().toString()))) { search(!TextUtils.isEmpty(searchString) ? searchString : searchEditText.getText().toString(), this.contentFilter, ""); @@ -496,7 +497,7 @@ private void showSearchOnStart() { searchEditText.setText(searchString); if (TextUtils.isEmpty(searchString) - || TextUtils.getTrimmedLength(searchEditText.getText()) == 0) { + || isBlank(searchEditText.getText().toString())) { searchToolbarContainer.setTranslationX(100); searchToolbarContainer.setAlpha(0.0f); searchToolbarContainer.setVisibility(View.VISIBLE); @@ -520,7 +521,7 @@ private void initSearchListeners() { if (DEBUG) { Log.d(TAG, "onClick() called with: v = [" + v + "]"); } - if (TextUtils.getTrimmedLength(searchEditText.getText()) == 0) { + if (isBlank(searchEditText.getText().toString())) { NavigationHelper.gotoMainFragment(getFM()); return; } @@ -582,12 +583,9 @@ public void onSuggestionItemLongClick(final SuggestionItem item) { searchEditText.removeTextChangedListener(textWatcher); } textWatcher = new TextWatcher() { - private boolean isPastedText = false; - @Override public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { - isPastedText = TextUtils.isEmpty(s) && after > 1; } @Override @@ -604,11 +602,6 @@ public void afterTextChanged(final Editable s) { final String newText = searchEditText.getText().toString().trim(); suggestionPublisher.onNext(newText); - - if (isPastedText) { - // trim pasted text - searchEditText.setText(newText); - } } }; searchEditText.addTextChangedListener(textWatcher); @@ -817,7 +810,7 @@ private void search(final String theSearchString, Log.d(TAG, "search() called with: query = [" + theSearchString + "]"); final String trimmedSearchString = theSearchString.trim(); if (!trimmedSearchString.equals(theSearchString)) { - Log.d(TAG, "The precondition is not satisfied. " + Log.w(TAG, "The precondition is not satisfied. " + "\"theSearchString\" is not allowed to have leading or trailing spaces"); } } From 881d04ba1ea6caa1cd88fe1b3cd0325f6a5ed89d Mon Sep 17 00:00:00 2001 From: Yingwei Zheng Date: Wed, 4 May 2022 20:20:19 +0800 Subject: [PATCH 020/154] Refactor database migration test and string trimming --- .../newpipe/database/DatabaseMigrationTest.kt | 11 +++---- .../fragments/list/search/SearchFragment.java | 29 ++++++++++++------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index c0c608a5826..19053ba97af 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -28,9 +28,6 @@ class DatabaseMigrationTest { private const val DEFAULT_SECOND_SERVICE_ID = 1 private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc" - - private const val DEFAULT_SEARCH1 = " abc " - private const val DEFAULT_SEARCH2 = " abc" } @get:Rule @@ -163,28 +160,28 @@ class DatabaseMigrationTest { "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { put("service_id", DEFAULT_SERVICE_ID) - put("search", DEFAULT_SEARCH1) + put("search", defaultSearch1) } ) insert( "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { put("service_id", DEFAULT_SERVICE_ID) - put("search", DEFAULT_SEARCH2) + put("search", defaultSearch2) } ) insert( "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { put("service_id", DEFAULT_SECOND_SERVICE_ID) - put("search", DEFAULT_SEARCH1) + put("search", defaultSearch1) } ) insert( "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { put("service_id", DEFAULT_SECOND_SERVICE_ID) - put("search", DEFAULT_SEARCH2) + put("search", defaultSearch2) } ) close() diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index df4a47bc523..07d41c1605b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -385,9 +385,8 @@ public void readFrom(@NonNull final Queue savedObjects) throws Exception @Override public void onSaveInstanceState(@NonNull final Bundle bundle) { - searchEditText.setText(searchEditText.getText().toString().trim()); searchString = searchEditText != null - ? searchEditText.getText().toString() + ? getSearchEditString().trim() : searchString; super.onSaveInstanceState(bundle); } @@ -399,10 +398,10 @@ public void onSaveInstanceState(@NonNull final Bundle bundle) { @Override public void reloadContent() { if (!TextUtils.isEmpty(searchString) || (searchEditText != null - && !isBlank(searchEditText.getText().toString()))) { + && !isSearchEditBlank())) { search(!TextUtils.isEmpty(searchString) ? searchString - : searchEditText.getText().toString(), this.contentFilter, ""); + : getSearchEditString(), this.contentFilter, ""); } else { if (searchEditText != null) { searchEditText.setText(""); @@ -497,7 +496,7 @@ private void showSearchOnStart() { searchEditText.setText(searchString); if (TextUtils.isEmpty(searchString) - || isBlank(searchEditText.getText().toString())) { + || isSearchEditBlank()) { searchToolbarContainer.setTranslationX(100); searchToolbarContainer.setAlpha(0.0f); searchToolbarContainer.setVisibility(View.VISIBLE); @@ -521,7 +520,7 @@ private void initSearchListeners() { if (DEBUG) { Log.d(TAG, "onClick() called with: v = [" + v + "]"); } - if (isBlank(searchEditText.getText().toString())) { + if (isSearchEditBlank()) { NavigationHelper.gotoMainFragment(getFM()); return; } @@ -600,7 +599,7 @@ public void afterTextChanged(final Editable s) { s.removeSpan(span); } - final String newText = searchEditText.getText().toString().trim(); + final String newText = getSearchEditString().trim(); suggestionPublisher.onNext(newText); } }; @@ -616,8 +615,8 @@ public void afterTextChanged(final Editable s) { } else if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { - searchEditText.setText(searchEditText.getText().toString().trim()); - search(searchEditText.getText().toString(), new String[0], ""); + searchEditText.setText(getSearchEditString().trim()); + search(getSearchEditString(), new String[0], ""); return true; } return false; @@ -692,7 +691,7 @@ private void showDeleteSuggestionDialog(final SuggestionItem item) { .observeOn(AndroidSchedulers.mainThread()) .subscribe( howManyDeleted -> suggestionPublisher - .onNext(searchEditText.getText().toString()), + .onNext(getSearchEditString()), throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Deleting item failed"))); @@ -942,6 +941,14 @@ private void setQuery(final int theServiceId, sortFilter = theSortFilter; } + private String getSearchEditString() { + return searchEditText.getText().toString(); + } + + private Boolean isSearchEditBlank() { + return isBlank(getSearchEditString()); + } + /*////////////////////////////////////////////////////////////////////////// // Suggestion Results //////////////////////////////////////////////////////////////////////////*/ @@ -1087,7 +1094,7 @@ public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHo .observeOn(AndroidSchedulers.mainThread()) .subscribe( howManyDeleted -> suggestionPublisher - .onNext(searchEditText.getText().toString()), + .onNext(getSearchEditString()), throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Deleting item failed"))); disposables.add(onDelete); From ef40ac7bb37a1e10214e48bd1b9ee82f88d69fd3 Mon Sep 17 00:00:00 2001 From: Yingwei Zheng Date: Thu, 5 May 2022 14:15:19 +0800 Subject: [PATCH 021/154] Fix a typo --- .../schabi/newpipe/fragments/list/search/SearchFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 07d41c1605b..ebdde2f9568 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -945,7 +945,7 @@ private String getSearchEditString() { return searchEditText.getText().toString(); } - private Boolean isSearchEditBlank() { + private boolean isSearchEditBlank() { return isBlank(getSearchEditString()); } From 15fd47c7f24c79b0be2e0e98f216c3140e88c8fb Mon Sep 17 00:00:00 2001 From: TobiGr Date: Wed, 16 Aug 2023 22:18:53 +0200 Subject: [PATCH 022/154] Apply review --- .../newpipe/database/DatabaseMigrationTest.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index 19053ba97af..65f41d8fae0 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -13,6 +13,7 @@ import org.junit.Assert.assertNull import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.schabi.newpipe.extractor.ServiceList import org.schabi.newpipe.extractor.stream.StreamType @RunWith(AndroidJUnit4::class) @@ -26,7 +27,7 @@ class DatabaseMigrationTest { private const val DEFAULT_UPLOADER_NAME = "Uploader Test" private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg" - private const val DEFAULT_SECOND_SERVICE_ID = 1 + private const val DEFAULT_SECOND_SERVICE_ID = 0 private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc" } @@ -155,32 +156,37 @@ class DatabaseMigrationTest { val defaultSearch1 = " abc " val defaultSearch2 = " abc" + val serviceId = DEFAULT_SERVICE_ID // YouTube + // Use id different to YouTube because two searches with the same query + // but different service are considered not equal. + val otherServiceId = ServiceList.SoundCloud.serviceId + databaseInV7.run { insert( "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { - put("service_id", DEFAULT_SERVICE_ID) + put("service_id", serviceId) put("search", defaultSearch1) } ) insert( "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { - put("service_id", DEFAULT_SERVICE_ID) + put("service_id", serviceId) put("search", defaultSearch2) } ) insert( "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { - put("service_id", DEFAULT_SECOND_SERVICE_ID) + put("service_id", otherServiceId) put("search", defaultSearch1) } ) insert( "search_history", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { - put("service_id", DEFAULT_SECOND_SERVICE_ID) + put("service_id", otherServiceId) put("search", defaultSearch2) } ) From 9118ecd68fa8793df4db46958e4d63015d5e1c61 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Thu, 17 Aug 2023 16:51:31 +0200 Subject: [PATCH 023/154] Remove unnecessary debug warning and use JDoc instead --- .../fragments/list/search/SearchFragment.java | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index ebdde2f9568..558f8df4474 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -802,40 +802,42 @@ protected void doInitialLoadLogic() { // no-op } - private void search(final String theSearchString, + /** + * Perform a search. + * @param theSearchString the trimmed search string + * @param theContentFilter the content filter to use. FIXME: unused param + * @param theSortFilter FIXME: unused param + */ + private void search(@NonNull final String theSearchString, final String[] theContentFilter, final String theSortFilter) { if (DEBUG) { Log.d(TAG, "search() called with: query = [" + theSearchString + "]"); - final String trimmedSearchString = theSearchString.trim(); - if (!trimmedSearchString.equals(theSearchString)) { - Log.w(TAG, "The precondition is not satisfied. " - + "\"theSearchString\" is not allowed to have leading or trailing spaces"); - } } if (theSearchString.isEmpty()) { return; } + // Check if theSearchString is a URL which can be opened by NewPipe directly + // and open it if possible. try { final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString); - if (streamingService != null) { - showLoading(); - disposables.add(Observable - .fromCallable(() -> NavigationHelper.getIntentByLink(activity, - streamingService, theSearchString)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(intent -> { - getFM().popBackStackImmediate(); - activity.startActivity(intent); - }, throwable -> showTextError(getString(R.string.unsupported_url)))); - return; - } + showLoading(); + disposables.add(Observable + .fromCallable(() -> NavigationHelper.getIntentByLink(activity, + streamingService, theSearchString)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(intent -> { + getFM().popBackStackImmediate(); + activity.startActivity(intent); + }, throwable -> showTextError(getString(R.string.unsupported_url)))); + return; } catch (final Exception ignored) { // Exception occurred, it's not a url } + // prepare search lastSearchedString = this.searchString; this.searchString = theSearchString; infoListAdapter.clearStreamItemList(); @@ -844,6 +846,7 @@ private void search(final String theSearchString, searchBinding.searchMetaInfoSeparator, disposables); hideKeyboardSearch(); + // store search query if search history is enabled disposables.add(historyRecordManager.onSearched(serviceId, theSearchString) .observeOn(AndroidSchedulers.mainThread()) .subscribe( @@ -852,6 +855,8 @@ private void search(final String theSearchString, throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED, theSearchString, serviceId)) )); + + // load search results suggestionPublisher.onNext(theSearchString); startLoading(false); } From 5d101e7b88a824272ff78512c537432a489a1b31 Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Mon, 24 Oct 2022 09:54:22 +1100 Subject: [PATCH 024/154] Added reset button but not working as intended. --- .../settings/AppearanceSettingsFragment.java | 18 ++++++++++++++++++ .../settings/ResetSettingsFragment.java | 14 ++++++++++++++ app/src/main/res/values/settings_keys.xml | 2 ++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/main_settings.xml | 8 ++++++++ gradle/wrapper/gradle-wrapper.properties | 2 ++ settings.gradle | 10 +++++----- 7 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index ef0e8670ce1..c8fa3e392bf 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -80,4 +80,22 @@ private void applyThemeChange(final String beginningThemeKey, ActivityCompat.recreate(getActivity()); } } + + public void resetToDefault() { + final String themeKey = getString(R.string.theme_key); + final String startThemeKey = defaultPreferences + .getString(themeKey, getString(R.string.default_theme_value)); + final String autoDeviceThemeKey = getString(R.string.auto_device_theme_key); + if (startThemeKey.equals(autoDeviceThemeKey)) { + applyThemeChange(startThemeKey, themeKey, autoDeviceThemeKey); + } else { + if (startThemeKey.equals(R.string.light_theme_key)) { + removePreference(getString(R.string.light_theme_key)); + } else if (startThemeKey.equals(R.string.dark_theme_key)) { + removePreference(getString(R.string.dark_theme_key)); + } else { + removePreference(getString(R.string.black_theme_key)); + } + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java new file mode 100644 index 00000000000..94936f6caba --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java @@ -0,0 +1,14 @@ +package org.schabi.newpipe.settings; + +import android.os.Bundle; + +public class ResetSettingsFragment extends BasePreferenceFragment { + + private AppearanceSettingsFragment appearanceSettings; + @Override + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { + addPreferencesFromResourceRegistry(); + + appearanceSettings.resetToDefault(); + } +} diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 51abe14fbbf..de883420c1d 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -222,6 +222,8 @@ prefer_original_audio prefer_descriptive_audio last_resize_mode + + reset_pref_screen_key debug_pref_screen_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5bbffaff09..f8393813933 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -151,6 +151,7 @@ History and cache Appearance Debug + Reset All Settings Updates Player notification Configure current playing stream notification diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml index 1b1c17e855d..a569f391c7b 100644 --- a/app/src/main/res/xml/main_settings.xml +++ b/app/src/main/res/xml/main_settings.xml @@ -53,4 +53,12 @@ android:key="@string/debug_pref_screen_key" android:title="@string/settings_category_debug_title" app:iconSpaceReserved="false" /> + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2c3425d49ec..fcaba806acf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,3 +1,4 @@ +#Fri Oct 21 13:32:17 AEDT 2022 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f @@ -5,3 +6,4 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index 0338fde6c55..a0aed95275d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,8 +4,8 @@ include ':app' // We assume, that NewPipe and NewPipe Extractor have the same parent directory. // If this is not the case, please change the path in includeBuild(). -//includeBuild('../NewPipeExtractor') { -// dependencySubstitution { -// substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor') -// } -//} +includeBuild('../NewPipeExtractor') { + dependencySubstitution { + substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor') + } +} From aa1847189ba9503636eb567ac64ca199c06e6c62 Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Tue, 25 Oct 2022 13:18:31 +1100 Subject: [PATCH 025/154] Added reset button but slightly working as intended. --- .../settings/AppearanceSettingsFragment.java | 18 ------------------ .../settings/ResetSettingsFragment.java | 13 +++++++++++-- .../settings/SettingsResourceRegistry.java | 1 + app/src/main/res/xml/reset_settings.xml | 4 ++++ 4 files changed, 16 insertions(+), 20 deletions(-) create mode 100644 app/src/main/res/xml/reset_settings.xml diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index c8fa3e392bf..ef0e8670ce1 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -80,22 +80,4 @@ private void applyThemeChange(final String beginningThemeKey, ActivityCompat.recreate(getActivity()); } } - - public void resetToDefault() { - final String themeKey = getString(R.string.theme_key); - final String startThemeKey = defaultPreferences - .getString(themeKey, getString(R.string.default_theme_value)); - final String autoDeviceThemeKey = getString(R.string.auto_device_theme_key); - if (startThemeKey.equals(autoDeviceThemeKey)) { - applyThemeChange(startThemeKey, themeKey, autoDeviceThemeKey); - } else { - if (startThemeKey.equals(R.string.light_theme_key)) { - removePreference(getString(R.string.light_theme_key)); - } else if (startThemeKey.equals(R.string.dark_theme_key)) { - removePreference(getString(R.string.dark_theme_key)); - } else { - removePreference(getString(R.string.black_theme_key)); - } - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java index 94936f6caba..aa7f528c60d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java @@ -2,13 +2,22 @@ import android.os.Bundle; +import androidx.core.app.ActivityCompat; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ThemeHelper; + public class ResetSettingsFragment extends BasePreferenceFragment { - private AppearanceSettingsFragment appearanceSettings; @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResourceRegistry(); - appearanceSettings.resetToDefault(); + // reset appearance to light theme + defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); + defaultPreferences.edit().putString(getString(R.string.theme_key), + getString(R.string.light_theme_key)).apply(); + ThemeHelper.setDayNightMode(requireContext(), "light_theme"); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java index b3d0741bb44..20d6555a514 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -41,6 +41,7 @@ private SettingsResourceRegistry() { add(UpdateSettingsFragment.class, R.xml.update_settings); add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings); add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings); + add(ResetSettingsFragment.class, R.xml.main_settings); } private SettingRegistryEntry add( diff --git a/app/src/main/res/xml/reset_settings.xml b/app/src/main/res/xml/reset_settings.xml new file mode 100644 index 00000000000..624ed13aec2 --- /dev/null +++ b/app/src/main/res/xml/reset_settings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 58517d1d27571b8f563656b42c472d9624ee2893 Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Wed, 26 Oct 2022 17:09:53 +1100 Subject: [PATCH 026/154] Added reset button but working as intended for theme. --- .../settings/DebugSettingsFragment.java | 13 +++++++++++ .../settings/ResetSettingsFragment.java | 23 ------------------- .../settings/SettingsResourceRegistry.java | 2 -- app/src/main/res/values/settings_keys.xml | 2 +- app/src/main/res/xml/debug_settings.xml | 5 ++++ app/src/main/res/xml/main_settings.xml | 8 ------- app/src/main/res/xml/reset_settings.xml | 4 ---- 7 files changed, 19 insertions(+), 38 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java delete mode 100644 app/src/main/res/xml/reset_settings.xml diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 0f4c9765e5d..690220da772 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -10,7 +10,9 @@ import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.local.feed.notifications.NotificationWorker; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.PicassoHelper; +import org.schabi.newpipe.util.ThemeHelper; import java.util.Optional; @@ -35,6 +37,8 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro findPreference(getString(R.string.show_error_snackbar_key)); final Preference createErrorNotificationPreference = findPreference(getString(R.string.create_error_notification_key)); + final Preference resetSettings = + findPreference(getString(R.string.reset_settings)); assert allowHeapDumpingPreference != null; assert showMemoryLeaksPreference != null; @@ -86,6 +90,15 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro new ErrorInfo(new RuntimeException(DUMMY), UserAction.UI_ERROR, DUMMY)); return true; }); + + // reset appearance to light theme + resetSettings.setOnPreferenceClickListener(preference -> { + defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); + defaultPreferences.edit().putString(getString(R.string.theme_key), + getString(R.string.light_theme_key)).apply(); + ThemeHelper.setDayNightMode(requireContext(), "light_theme"); + return true; + }); } /** diff --git a/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java deleted file mode 100644 index aa7f528c60d..00000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/ResetSettingsFragment.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.schabi.newpipe.settings; - -import android.os.Bundle; - -import androidx.core.app.ActivityCompat; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.ThemeHelper; - -public class ResetSettingsFragment extends BasePreferenceFragment { - - @Override - public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResourceRegistry(); - - // reset appearance to light theme - defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); - defaultPreferences.edit().putString(getString(R.string.theme_key), - getString(R.string.light_theme_key)).apply(); - ThemeHelper.setDayNightMode(requireContext(), "light_theme"); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java index 20d6555a514..2fe4f42edd8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -30,7 +30,6 @@ public final class SettingsResourceRegistry { private SettingsResourceRegistry() { add(MainSettingsFragment.class, R.xml.main_settings).setSearchable(false); - add(AppearanceSettingsFragment.class, R.xml.appearance_settings); add(ContentSettingsFragment.class, R.xml.content_settings); add(DebugSettingsFragment.class, R.xml.debug_settings).setSearchable(false); @@ -41,7 +40,6 @@ private SettingsResourceRegistry() { add(UpdateSettingsFragment.class, R.xml.update_settings); add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings); add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings); - add(ResetSettingsFragment.class, R.xml.main_settings); } private SettingRegistryEntry add( diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index de883420c1d..42d4349007a 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -223,7 +223,7 @@ prefer_descriptive_audio last_resize_mode - reset_pref_screen_key + reset_settings debug_pref_screen_key diff --git a/app/src/main/res/xml/debug_settings.xml b/app/src/main/res/xml/debug_settings.xml index 84bb281f31e..66836f13855 100644 --- a/app/src/main/res/xml/debug_settings.xml +++ b/app/src/main/res/xml/debug_settings.xml @@ -71,4 +71,9 @@ android:title="@string/create_error_notification" app:singleLineTitle="false" app:iconSpaceReserved="false" /> + diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml index a569f391c7b..1b1c17e855d 100644 --- a/app/src/main/res/xml/main_settings.xml +++ b/app/src/main/res/xml/main_settings.xml @@ -53,12 +53,4 @@ android:key="@string/debug_pref_screen_key" android:title="@string/settings_category_debug_title" app:iconSpaceReserved="false" /> - diff --git a/app/src/main/res/xml/reset_settings.xml b/app/src/main/res/xml/reset_settings.xml deleted file mode 100644 index 624ed13aec2..00000000000 --- a/app/src/main/res/xml/reset_settings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file From 2103a040922227ad16c92a938eb5671d68e20569 Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Wed, 26 Oct 2022 19:13:41 +1100 Subject: [PATCH 027/154] Cleaned up xml files. --- app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/xml/debug_settings.xml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 42d4349007a..ea283e62c21 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -222,6 +222,7 @@ prefer_original_audio prefer_descriptive_audio last_resize_mode + reset_settings diff --git a/app/src/main/res/xml/debug_settings.xml b/app/src/main/res/xml/debug_settings.xml index 66836f13855..511bcd19ad3 100644 --- a/app/src/main/res/xml/debug_settings.xml +++ b/app/src/main/res/xml/debug_settings.xml @@ -72,7 +72,6 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> From 076e9eee01ca3f55f56124ce407364182ff8278d Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Sat, 29 Oct 2022 08:58:11 +1100 Subject: [PATCH 028/154] Added alert dialogue and restarts the app when resetting settings. --- .../settings/DebugSettingsFragment.java | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 690220da772..56c88f7302b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.settings; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; @@ -10,9 +12,8 @@ import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.local.feed.notifications.NotificationWorker; -import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PicassoHelper; -import org.schabi.newpipe.util.ThemeHelper; import java.util.Optional; @@ -91,12 +92,30 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro return true; }); - // reset appearance to light theme + // Resets all settings by deleting shared preference and restarting the app + // A dialogue will pop up to confirm if user intends to reset all settings resetSettings.setOnPreferenceClickListener(preference -> { - defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); - defaultPreferences.edit().putString(getString(R.string.theme_key), - getString(R.string.light_theme_key)).apply(); - ThemeHelper.setDayNightMode(requireContext(), "light_theme"); + final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setMessage("Resetting all settings will discard " + + "all of your preferred settings and restarts the app. " + + "Are you sure you want to do this?"); + builder.setCancelable(true); + builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialogInterface, final int i) { + NavigationHelper.restartApp(getActivity()); + } + }); + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialogInterface, final int i) { + } + }); + final AlertDialog alertDialog = builder.create(); + alertDialog.show(); +// SharedPreferences sharedPreferences = +// PreferenceManager.getDefaultSharedPreferences(requireContext()); +// sharedPreferences = null; return true; }); } From 5c46412faac23f27940cba860c8e8a1219375edd Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Sat, 29 Oct 2022 09:19:04 +1100 Subject: [PATCH 029/154] Changed alert dialogue. --- .../java/org/schabi/newpipe/settings/DebugSettingsFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 56c88f7302b..88addd73e05 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -98,7 +98,7 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setMessage("Resetting all settings will discard " + "all of your preferred settings and restarts the app. " - + "Are you sure you want to do this?"); + + "Are you sure you want to proceed?"); builder.setCancelable(true); builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() { @Override From 23de9bf93ed6629afc8080ffa6a3f2467d0f76b1 Mon Sep 17 00:00:00 2001 From: Zhidong Piao Date: Sat, 29 Oct 2022 09:38:00 +1100 Subject: [PATCH 030/154] clear shared preference xmls --- .../newpipe/settings/DebugSettingsFragment.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 88addd73e05..5c5f6a03911 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -3,9 +3,11 @@ import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import androidx.preference.Preference; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorInfo; @@ -94,6 +96,7 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro // Resets all settings by deleting shared preference and restarting the app // A dialogue will pop up to confirm if user intends to reset all settings + assert resetSettings != null; resetSettings.setOnPreferenceClickListener(preference -> { final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setMessage("Resetting all settings will discard " @@ -113,9 +116,13 @@ public void onClick(final DialogInterface dialogInterface, final int i) { }); final AlertDialog alertDialog = builder.create(); alertDialog.show(); -// SharedPreferences sharedPreferences = -// PreferenceManager.getDefaultSharedPreferences(requireContext()); -// sharedPreferences = null; + + // delete all shared preferences xml files. + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(requireContext()); + sharedPreferences.edit().clear().apply(); + + return true; }); } From 81ad50e82a0eb183cd348259445e8ffe25d9f6c8 Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Sat, 29 Oct 2022 09:52:22 +1100 Subject: [PATCH 031/154] Added delete xml method inside the yes dialogue. --- .../settings/DebugSettingsFragment.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 5c5f6a03911..5952fbe5bbf 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.settings; import android.app.AlertDialog; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -98,31 +97,27 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro // A dialogue will pop up to confirm if user intends to reset all settings assert resetSettings != null; resetSettings.setOnPreferenceClickListener(preference -> { + // Show Alert Dialogue final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setMessage("Resetting all settings will discard " + "all of your preferred settings and restarts the app. " + "Are you sure you want to proceed?"); builder.setCancelable(true); - builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialogInterface, final int i) { - NavigationHelper.restartApp(getActivity()); + builder.setPositiveButton("Yes", (dialogInterface, i) -> { + // Deletes all shared preferences xml files. + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(requireContext()); + sharedPreferences.edit().clear().apply(); + // Restarts the app + if (getActivity() == null) { + return; } + NavigationHelper.restartApp(getActivity()); }); - builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialogInterface, final int i) { - } + builder.setNegativeButton("Cancel", (dialogInterface, i) -> { }); final AlertDialog alertDialog = builder.create(); alertDialog.show(); - - // delete all shared preferences xml files. - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - sharedPreferences.edit().clear().apply(); - - return true; }); } From 06d256294f31e944cc41ff9af12b13ce10b39202 Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Sat, 29 Oct 2022 10:24:11 +1100 Subject: [PATCH 032/154] Revert changes made to dev. --- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fcaba806acf..28d3f9e02dd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,3 @@ -#Fri Oct 21 13:32:17 AEDT 2022 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f @@ -7,3 +6,4 @@ networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index a0aed95275d..0338fde6c55 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,8 +4,8 @@ include ':app' // We assume, that NewPipe and NewPipe Extractor have the same parent directory. // If this is not the case, please change the path in includeBuild(). -includeBuild('../NewPipeExtractor') { - dependencySubstitution { - substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor') - } -} +//includeBuild('../NewPipeExtractor') { +// dependencySubstitution { +// substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor') +// } +//} From a239a26b17c1259b1e261d3bcf0c99b1e1581059 Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Sat, 29 Oct 2022 10:26:11 +1100 Subject: [PATCH 033/154] Revert changes made to dev. --- .../org/schabi/newpipe/settings/SettingsResourceRegistry.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java index 2fe4f42edd8..b3d0741bb44 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -30,6 +30,7 @@ public final class SettingsResourceRegistry { private SettingsResourceRegistry() { add(MainSettingsFragment.class, R.xml.main_settings).setSearchable(false); + add(AppearanceSettingsFragment.class, R.xml.appearance_settings); add(ContentSettingsFragment.class, R.xml.content_settings); add(DebugSettingsFragment.class, R.xml.debug_settings).setSearchable(false); From 25a73090f5bb36f77cb77edca11f89c99513f49d Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Sat, 29 Oct 2022 10:29:16 +1100 Subject: [PATCH 034/154] Revert changes made to dev. --- app/src/main/res/values/settings_keys.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index ea283e62c21..51abe14fbbf 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -223,9 +223,6 @@ prefer_descriptive_audio last_resize_mode - - reset_settings - debug_pref_screen_key allow_heap_dumping_key From 40a3e1b18a4c2abfd412c875d83912ea6d710def Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Sat, 29 Oct 2022 10:30:41 +1100 Subject: [PATCH 035/154] Revert committed file change of settings key. --- app/src/main/res/values/settings_keys.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 51abe14fbbf..ea283e62c21 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -223,6 +223,9 @@ prefer_descriptive_audio last_resize_mode + + reset_settings + debug_pref_screen_key allow_heap_dumping_key From d7ef9b1f0c1cbba8cf216ab696a0ba93a281c24b Mon Sep 17 00:00:00 2001 From: Vincent Tanumihardja Date: Sat, 29 Oct 2022 11:33:10 +1100 Subject: [PATCH 036/154] Added strings to strings.xml to allow translation. --- .../schabi/newpipe/settings/DebugSettingsFragment.java | 8 +++----- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 5952fbe5bbf..f2a4e1190ba 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -99,11 +99,9 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro resetSettings.setOnPreferenceClickListener(preference -> { // Show Alert Dialogue final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setMessage("Resetting all settings will discard " - + "all of your preferred settings and restarts the app. " - + "Are you sure you want to proceed?"); + builder.setMessage(R.string.reset_all_settings); builder.setCancelable(true); - builder.setPositiveButton("Yes", (dialogInterface, i) -> { + builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { // Deletes all shared preferences xml files. final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); @@ -114,7 +112,7 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro } NavigationHelper.restartApp(getActivity()); }); - builder.setNegativeButton("Cancel", (dialogInterface, i) -> { + builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> { }); final AlertDialog alertDialog = builder.create(); alertDialog.show(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f8393813933..9c843784e2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -595,6 +595,8 @@ Download finished %s downloads finished + + Resetting all settings will discard all of your preferred settings and restarts the app. Are you sure you want to proceed? Generate unique name Overwrite From ad0855ac83715179d1be3068492f136271e78072 Mon Sep 17 00:00:00 2001 From: vincetzr <110076924+vincetzr@users.noreply.github.com> Date: Sat, 29 Oct 2022 21:21:26 +1100 Subject: [PATCH 037/154] Update app/src/main/res/xml/debug_settings.xml Ensuring title to be fully displayed on small devices. Co-authored-by: Tobi --- app/src/main/res/xml/debug_settings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/xml/debug_settings.xml b/app/src/main/res/xml/debug_settings.xml index 511bcd19ad3..1c66dbdb39d 100644 --- a/app/src/main/res/xml/debug_settings.xml +++ b/app/src/main/res/xml/debug_settings.xml @@ -74,5 +74,6 @@ From f2e352832a12d1282a4b6b7dddf389cf8e7887cf Mon Sep 17 00:00:00 2001 From: TobiGr Date: Sun, 17 Sep 2023 17:21:59 +0200 Subject: [PATCH 038/154] Create new settings category: Backup and restore Following settings have been move to the new category: - import database (from ContenttSettings) - export database (from ContenttSettings) - reset settings (from DebugSettings) --- .../BackupRestoreSettingsFragment.java | 271 ++++++++++++++++++ .../settings/ContentSettingsFragment.java | 217 -------------- .../settings/DebugSettingsFragment.java | 32 --- .../newpipe/settings/NewPipeSettings.java | 1 + .../settings/SettingsResourceRegistry.java | 1 + app/src/main/res/values/settings_keys.xml | 1 - app/src/main/res/values/strings.xml | 1 + .../main/res/xml/backup_restore_settings.xml | 24 ++ app/src/main/res/xml/content_settings.xml | 14 - app/src/main/res/xml/debug_settings.xml | 5 - app/src/main/res/xml/main_settings.xml | 6 + 11 files changed, 304 insertions(+), 269 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java create mode 100644 app/src/main/res/xml/backup_restore_settings.xml diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java new file mode 100644 index 00000000000..bc24fbe8120 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -0,0 +1,271 @@ +package org.schabi.newpipe.settings; + +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; +import org.schabi.newpipe.streams.io.StoredFileHelper; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ZipHelper; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Objects; + +public class BackupRestoreSettingsFragment extends BasePreferenceFragment { + + private static final String ZIP_MIME_TYPE = "application/zip"; + + private final SimpleDateFormat exportDateFormat = + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + private ContentSettingsManager manager; + private String importExportDataPathKey; + private final ActivityResultLauncher requestImportPathLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + this::requestImportPathResult); + private final ActivityResultLauncher requestExportPathLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + this::requestExportPathResult); + + + @Override + public void onCreatePreferences(@Nullable final Bundle savedInstanceState, + @Nullable final String rootKey) { + final File homeDir = ContextCompat.getDataDir(requireContext()); + Objects.requireNonNull(homeDir); + manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir)); + manager.deleteSettingsFile(); + + importExportDataPathKey = getString(R.string.import_export_data_path); + + + addPreferencesFromResourceRegistry(); + + final Preference importDataPreference = requirePreference(R.string.import_data); + importDataPreference.setOnPreferenceClickListener((Preference p) -> { + NoFileManagerSafeGuard.launchSafe( + requestImportPathLauncher, + StoredFileHelper.getPicker(requireContext(), + ZIP_MIME_TYPE, getImportExportDataUri()), + TAG, + getContext() + ); + + return true; + }); + + final Preference exportDataPreference = requirePreference(R.string.export_data); + exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { + NoFileManagerSafeGuard.launchSafe( + requestExportPathLauncher, + StoredFileHelper.getNewPicker(requireContext(), + "NewPipeData-" + exportDateFormat.format(new Date()) + ".zip", + ZIP_MIME_TYPE, getImportExportDataUri()), + TAG, + getContext() + ); + + return true; + }); + + final Preference resetSettings = findPreference(getString(R.string.reset_settings)); + // Resets all settings by deleting shared preference and restarting the app + // A dialogue will pop up to confirm if user intends to reset all settings + assert resetSettings != null; + resetSettings.setOnPreferenceClickListener(preference -> { + // Show Alert Dialogue + final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setMessage(R.string.reset_all_settings); + builder.setCancelable(true); + builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { + // Deletes all shared preferences xml files. + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(requireContext()); + sharedPreferences.edit().clear().apply(); + // Restarts the app + if (getActivity() == null) { + return; + } + NavigationHelper.restartApp(getActivity()); + }); + builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> { + }); + final AlertDialog alertDialog = builder.create(); + alertDialog.show(); + return true; + }); + } + + private void requestExportPathResult(final ActivityResult result) { + assureCorrectAppLanguage(requireContext()); + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + // will be saved only on success + final Uri lastExportDataUri = result.getData().getData(); + + final StoredFileHelper file = new StoredFileHelper( + requireContext(), result.getData().getData(), ZIP_MIME_TYPE); + + exportDatabase(file, lastExportDataUri); + } + } + + private void requestImportPathResult(final ActivityResult result) { + assureCorrectAppLanguage(requireContext()); + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + // will be saved only on success + final Uri lastImportDataUri = result.getData().getData(); + + final StoredFileHelper file = new StoredFileHelper( + requireContext(), result.getData().getData(), ZIP_MIME_TYPE); + + new androidx.appcompat.app.AlertDialog.Builder(requireActivity()) + .setMessage(R.string.override_current_data) + .setPositiveButton(R.string.ok, (d, id) -> + importDatabase(file, lastImportDataUri)) + .setNegativeButton(R.string.cancel, (d, id) -> + d.cancel()) + .show(); + } + } + + private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) { + try { + //checkpoint before export + NewPipeDatabase.checkpoint(); + + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(requireContext()); + manager.exportDatabase(preferences, file); + + saveLastImportExportDataUri(exportDataUri); // save export path only on success + Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT) + .show(); + } catch (final Exception e) { + ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e); + } + } + + private void importDatabase(final StoredFileHelper file, final Uri importDataUri) { + // check if file is supported + if (!ZipHelper.isValidZipFile(file)) { + Toast.makeText(requireContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) + .show(); + return; + } + + try { + if (!manager.ensureDbDirectoryExists()) { + throw new IOException("Could not create databases dir"); + } + + if (!manager.extractDb(file)) { + Toast.makeText(requireContext(), R.string.could_not_import_all_files, + Toast.LENGTH_LONG) + .show(); + } + + // if settings file exist, ask if it should be imported. + if (manager.extractSettings(file)) { + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle(R.string.import_settings) + .setNegativeButton(R.string.cancel, (dialog, which) -> { + dialog.dismiss(); + finishImport(importDataUri); + }) + .setPositiveButton(R.string.ok, (dialog, which) -> { + dialog.dismiss(); + final Context context = requireContext(); + final SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + manager.loadSharedPreferences(prefs); + cleanImport(context, prefs); + finishImport(importDataUri); + }) + .show(); + } else { + finishImport(importDataUri); + } + } catch (final Exception e) { + ErrorUtil.showUiErrorSnackbar(this, "Importing database", e); + } + } + + /** + * Remove settings that are not supposed to be imported on different devices + * and reset them to default values. + * @param context the context used for the import + * @param prefs the preferences used while running the import + */ + private void cleanImport(@NonNull final Context context, + @NonNull final SharedPreferences prefs) { + // Check if media tunnelling needs to be disabled automatically, + // if it was disabled automatically in the imported preferences. + final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key); + final String automaticTunnelingKey = + context.getString(R.string.disabled_media_tunneling_automatically_key); + // R.string.disable_media_tunneling_key should always be true + // if R.string.disabled_media_tunneling_automatically_key equals 1, + // but we double check here just to be sure and to avoid regressions + // caused by possible later modification of the media tunneling functionality. + // R.string.disabled_media_tunneling_automatically_key == 0: + // automatic value overridden by user in settings + // R.string.disabled_media_tunneling_automatically_key == -1: not set + final boolean wasMediaTunnelingDisabledAutomatically = + prefs.getInt(automaticTunnelingKey, -1) == 1 + && prefs.getBoolean(tunnelingKey, false); + if (wasMediaTunnelingDisabledAutomatically) { + prefs.edit() + .putInt(automaticTunnelingKey, -1) + .putBoolean(tunnelingKey, false) + .apply(); + NewPipeSettings.setMediaTunneling(context); + } + } + + /** + * Save import path and restart system. + * + * @param importDataUri The import path to save + */ + private void finishImport(final Uri importDataUri) { + // save import path only on success + saveLastImportExportDataUri(importDataUri); + // restart app to properly load db + NavigationHelper.restartApp(requireActivity()); + } + + private Uri getImportExportDataUri() { + final String path = defaultPreferences.getString(importExportDataPathKey, null); + return isBlank(path) ? null : Uri.parse(path); + } + + private void saveLastImportExportDataUri(final Uri importExportDataUri) { + final SharedPreferences.Editor editor = defaultPreferences.edit() + .putString(importExportDataPathKey, importExportDataUri.toString()); + editor.apply(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index ee34f01dd32..ec3b1b2d75f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -1,104 +1,34 @@ package org.schabi.newpipe.settings; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -import android.app.Activity; import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.widget.Toast; -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; import androidx.preference.Preference; -import androidx.preference.PreferenceManager; import org.schabi.newpipe.DownloaderImpl; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.Localization; -import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; -import org.schabi.newpipe.streams.io.StoredFileHelper; -import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PicassoHelper; -import org.schabi.newpipe.util.ZipHelper; -import java.io.File; import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.Objects; public class ContentSettingsFragment extends BasePreferenceFragment { - private static final String ZIP_MIME_TYPE = "application/zip"; - - private final SimpleDateFormat exportDateFormat = - new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - - private ContentSettingsManager manager; - - private String importExportDataPathKey; private String youtubeRestrictedModeEnabledKey; private Localization initialSelectedLocalization; private ContentCountry initialSelectedContentCountry; private String initialLanguage; - private final ActivityResultLauncher requestImportPathLauncher = - registerForActivityResult(new StartActivityForResult(), this::requestImportPathResult); - private final ActivityResultLauncher requestExportPathLauncher = - registerForActivityResult(new StartActivityForResult(), this::requestExportPathResult); @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - final File homeDir = ContextCompat.getDataDir(requireContext()); - Objects.requireNonNull(homeDir); - manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir)); - manager.deleteSettingsFile(); - - importExportDataPathKey = getString(R.string.import_export_data_path); youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); addPreferencesFromResourceRegistry(); - final Preference importDataPreference = requirePreference(R.string.import_data); - importDataPreference.setOnPreferenceClickListener((Preference p) -> { - NoFileManagerSafeGuard.launchSafe( - requestImportPathLauncher, - StoredFileHelper.getPicker(requireContext(), - ZIP_MIME_TYPE, getImportExportDataUri()), - TAG, - getContext() - ); - - return true; - }); - - final Preference exportDataPreference = requirePreference(R.string.export_data); - exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { - NoFileManagerSafeGuard.launchSafe( - requestExportPathLauncher, - StoredFileHelper.getNewPicker(requireContext(), - "NewPipeData-" + exportDateFormat.format(new Date()) + ".zip", - ZIP_MIME_TYPE, getImportExportDataUri()), - TAG, - getContext() - ); - - return true; - }); - initialSelectedLocalization = org.schabi.newpipe.util.Localization .getPreferredLocalization(requireContext()); initialSelectedContentCountry = org.schabi.newpipe.util.Localization @@ -154,151 +84,4 @@ public void onDestroy() { NewPipe.setupLocalization(selectedLocalization, selectedContentCountry); } } - - private void requestExportPathResult(final ActivityResult result) { - assureCorrectAppLanguage(getContext()); - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - // will be saved only on success - final Uri lastExportDataUri = result.getData().getData(); - - final StoredFileHelper file = - new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE); - - exportDatabase(file, lastExportDataUri); - } - } - - private void requestImportPathResult(final ActivityResult result) { - assureCorrectAppLanguage(getContext()); - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - // will be saved only on success - final Uri lastImportDataUri = result.getData().getData(); - - final StoredFileHelper file = - new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE); - - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.override_current_data) - .setPositiveButton(R.string.ok, (d, id) -> - importDatabase(file, lastImportDataUri)) - .setNegativeButton(R.string.cancel, (d, id) -> - d.cancel()) - .show(); - } - } - - private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) { - try { - //checkpoint before export - NewPipeDatabase.checkpoint(); - - final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(requireContext()); - manager.exportDatabase(preferences, file); - - saveLastImportExportDataUri(exportDataUri); // save export path only on success - Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); - } catch (final Exception e) { - ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e); - } - } - - private void importDatabase(final StoredFileHelper file, final Uri importDataUri) { - // check if file is supported - if (!ZipHelper.isValidZipFile(file)) { - Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) - .show(); - return; - } - - try { - if (!manager.ensureDbDirectoryExists()) { - throw new IOException("Could not create databases dir"); - } - - if (!manager.extractDb(file)) { - Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) - .show(); - } - - // if settings file exist, ask if it should be imported. - if (manager.extractSettings(file)) { - new AlertDialog.Builder(requireContext()) - .setTitle(R.string.import_settings) - .setNegativeButton(R.string.cancel, (dialog, which) -> { - dialog.dismiss(); - finishImport(importDataUri); - }) - .setPositiveButton(R.string.ok, (dialog, which) -> { - dialog.dismiss(); - final Context context = requireContext(); - final SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - manager.loadSharedPreferences(prefs); - cleanImport(context, prefs); - finishImport(importDataUri); - }) - .show(); - } else { - finishImport(importDataUri); - } - } catch (final Exception e) { - ErrorUtil.showUiErrorSnackbar(this, "Importing database", e); - } - } - - /** - * Remove settings that are not supposed to be imported on different devices - * and reset them to default values. - * @param context the context used for the import - * @param prefs the preferences used while running the import - */ - private void cleanImport(@NonNull final Context context, - @NonNull final SharedPreferences prefs) { - // Check if media tunnelling needs to be disabled automatically, - // if it was disabled automatically in the imported preferences. - final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key); - final String automaticTunnelingKey = - context.getString(R.string.disabled_media_tunneling_automatically_key); - // R.string.disable_media_tunneling_key should always be true - // if R.string.disabled_media_tunneling_automatically_key equals 1, - // but we double check here just to be sure and to avoid regressions - // caused by possible later modification of the media tunneling functionality. - // R.string.disabled_media_tunneling_automatically_key == 0: - // automatic value overridden by user in settings - // R.string.disabled_media_tunneling_automatically_key == -1: not set - final boolean wasMediaTunnelingDisabledAutomatically = - prefs.getInt(automaticTunnelingKey, -1) == 1 - && prefs.getBoolean(tunnelingKey, false); - if (wasMediaTunnelingDisabledAutomatically) { - prefs.edit() - .putInt(automaticTunnelingKey, -1) - .putBoolean(tunnelingKey, false) - .apply(); - NewPipeSettings.setMediaTunneling(context); - } - } - - /** - * Save import path and restart system. - * - * @param importDataUri The import path to save - */ - private void finishImport(final Uri importDataUri) { - // save import path only on success - saveLastImportExportDataUri(importDataUri); - // restart app to properly load db - NavigationHelper.restartApp(requireActivity()); - } - - private Uri getImportExportDataUri() { - final String path = defaultPreferences.getString(importExportDataPathKey, null); - return isBlank(path) ? null : Uri.parse(path); - } - - private void saveLastImportExportDataUri(final Uri importExportDataUri) { - final SharedPreferences.Editor editor = defaultPreferences.edit() - .putString(importExportDataPathKey, importExportDataUri.toString()); - editor.apply(); - } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index f2a4e1190ba..0f4c9765e5d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -1,19 +1,15 @@ package org.schabi.newpipe.settings; -import android.app.AlertDialog; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import androidx.preference.Preference; -import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.local.feed.notifications.NotificationWorker; -import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PicassoHelper; import java.util.Optional; @@ -39,8 +35,6 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro findPreference(getString(R.string.show_error_snackbar_key)); final Preference createErrorNotificationPreference = findPreference(getString(R.string.create_error_notification_key)); - final Preference resetSettings = - findPreference(getString(R.string.reset_settings)); assert allowHeapDumpingPreference != null; assert showMemoryLeaksPreference != null; @@ -92,32 +86,6 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro new ErrorInfo(new RuntimeException(DUMMY), UserAction.UI_ERROR, DUMMY)); return true; }); - - // Resets all settings by deleting shared preference and restarting the app - // A dialogue will pop up to confirm if user intends to reset all settings - assert resetSettings != null; - resetSettings.setOnPreferenceClickListener(preference -> { - // Show Alert Dialogue - final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setMessage(R.string.reset_all_settings); - builder.setCancelable(true); - builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { - // Deletes all shared preferences xml files. - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - sharedPreferences.edit().clear().apply(); - // Restarts the app - if (getActivity() == null) { - return; - } - NavigationHelper.restartApp(getActivity()); - }); - builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> { - }); - final AlertDialog alertDialog = builder.create(); - alertDialog.show(); - return true; - }); } /** diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index b85b95eb03b..f280324cf64 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -63,6 +63,7 @@ public static void initSettings(final Context context) { PreferenceManager.setDefaultValues(context, R.xml.player_notification_settings, true); PreferenceManager.setDefaultValues(context, R.xml.update_settings, true); PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true); + PreferenceManager.setDefaultValues(context, R.xml.backup_restore_settings, true); saveDefaultVideoDownloadDirectory(context); saveDefaultAudioDownloadDirectory(context); diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java index b3d0741bb44..06e0a7c1eae 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -41,6 +41,7 @@ private SettingsResourceRegistry() { add(UpdateSettingsFragment.class, R.xml.update_settings); add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings); add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings); + add(BackupRestoreSettingsFragment.class, R.xml.backup_restore_settings); } private SettingRegistryEntry add( diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index ea283e62c21..30efca080c9 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -223,7 +223,6 @@ prefer_descriptive_audio last_resize_mode - reset_settings diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c843784e2e..314eaa9bd6a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -155,6 +155,7 @@ Updates Player notification Configure current playing stream notification + Backup and restore Playing in background Playing in popup mode Content diff --git a/app/src/main/res/xml/backup_restore_settings.xml b/app/src/main/res/xml/backup_restore_settings.xml new file mode 100644 index 00000000000..b8fee0aa6ee --- /dev/null +++ b/app/src/main/res/xml/backup_restore_settings.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index 73a849af75e..dac5dccc166 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -124,20 +124,6 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> - - - - - diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml index 1b1c17e855d..5f96989f979 100644 --- a/app/src/main/res/xml/main_settings.xml +++ b/app/src/main/res/xml/main_settings.xml @@ -47,6 +47,12 @@ android:title="@string/settings_category_updates_title" app:iconSpaceReserved="false" /> + + Date: Sun, 17 Sep 2023 17:31:45 +0200 Subject: [PATCH 039/154] Remove strange change --- gradle/wrapper/gradle-wrapper.properties | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 28d3f9e02dd..2c3425d49ec 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -5,5 +5,3 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists From df2e0be08dd36c89b747a31dc3ab40d016f9d1e6 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Sun, 17 Sep 2023 17:35:52 +0200 Subject: [PATCH 040/154] Add summary to reset preference --- app/src/main/res/values/strings.xml | 5 +++-- app/src/main/res/xml/backup_restore_settings.xml | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 314eaa9bd6a..1dbffa4ae81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -151,7 +151,6 @@ History and cache Appearance Debug - Reset All Settings Updates Player notification Configure current playing stream notification @@ -596,8 +595,10 @@ Download finished %s downloads finished + Reset settings + Reset all settings to their default values - Resetting all settings will discard all of your preferred settings and restarts the app. Are you sure you want to proceed? + Resetting all settings will discard all of your preferred settings and restarts the app.\n\nAre you sure you want to proceed? Generate unique name Overwrite diff --git a/app/src/main/res/xml/backup_restore_settings.xml b/app/src/main/res/xml/backup_restore_settings.xml index b8fee0aa6ee..ef6a3cde3a6 100644 --- a/app/src/main/res/xml/backup_restore_settings.xml +++ b/app/src/main/res/xml/backup_restore_settings.xml @@ -18,7 +18,8 @@ \ No newline at end of file From 8f4cd032b76412f8f05ee2800021162fc89e9795 Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 11 Apr 2023 10:47:40 +0200 Subject: [PATCH 041/154] Remove mini variant and move upload date to top in comments --- .../newpipe/info_list/InfoItemBuilder.java | 6 +- .../newpipe/info_list/InfoListAdapter.java | 12 ++-- ...Holder.java => CommentInfoItemHolder.java} | 43 ++++++------ .../holder/CommentsInfoItemHolder.java | 63 ----------------- app/src/main/res/layout/fragment_comments.xml | 2 +- ...omments_item.xml => list_comment_item.xml} | 14 +--- .../res/layout/list_comments_mini_item.xml | 69 ------------------- 7 files changed, 30 insertions(+), 179 deletions(-) rename app/src/main/java/org/schabi/newpipe/info_list/holder/{CommentsMiniInfoItemHolder.java => CommentInfoItemHolder.java} (90%) delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java rename app/src/main/res/layout/{list_comments_item.xml => list_comment_item.xml} (89%) delete mode 100644 app/src/main/res/layout/list_comments_mini_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index 68f19ee9714..d959c63277c 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -13,8 +13,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; -import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder; -import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder; +import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; @@ -87,8 +86,7 @@ private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent, return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent); case COMMENT: - return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) - : new CommentsInfoItemHolder(this, parent); + return new CommentInfoItemHolder(this, parent); default: throw new RuntimeException("InfoType not expected = " + infoType.name()); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index a13f0e5aaba..575568c00f9 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -21,8 +21,7 @@ import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; -import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder; -import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder; +import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder; @@ -79,8 +78,7 @@ public class InfoListAdapter extends RecyclerView.Adapter openCommentAuthor(item)); + final String uploadDate; + if (item.getUploadDate() != null) { + uploadDate = Localization.relativeTime(item.getUploadDate().offsetDateTime()); + } else { + uploadDate = item.getTextualUploadDate(); + } + itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(), uploadDate)); - itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); + itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE); + itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE); try { streamService = NewPipe.getService(item.getServiceId()); @@ -136,12 +144,6 @@ public void updateFromItem(final InfoItem infoItem, itemLikesCountView.setText("-"); } - if (item.getUploadDate() != null) { - itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate() - .offsetDateTime())); - } else { - itemPublishedTime.setText(item.getTextualUploadDate()); - } itemView.setOnClickListener(view -> { toggleEllipsize(); @@ -150,7 +152,6 @@ public void updateFromItem(final InfoItem infoItem, } }); - itemView.setOnLongClickListener(view -> { if (DeviceUtils.isTv(itemBuilder.getContext())) { openCommentAuthor(item); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java deleted file mode 100644 index 4fc2d9f84b8..00000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.schabi.newpipe.info_list.holder; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.comments.CommentsInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.local.history.HistoryRecordManager; - -/* - * Created by Christian Schabesberger on 12.02.17. - * - * Copyright (C) Christian Schabesberger 2016 - * ChannelInfoItemHolder .java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder { - public final TextView itemTitleView; - private final ImageView itemHeartView; - private final ImageView itemPinnedView; - - public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) { - super(infoItemBuilder, R.layout.list_comments_item, parent); - - itemTitleView = itemView.findViewById(R.id.itemTitleView); - itemHeartView = itemView.findViewById(R.id.detail_heart_image_view); - itemPinnedView = itemView.findViewById(R.id.detail_pinned_view); - } - - @Override - public void updateFromItem(final InfoItem infoItem, - final HistoryRecordManager historyRecordManager) { - super.updateFromItem(infoItem, historyRecordManager); - - if (!(infoItem instanceof CommentsInfoItem)) { - return; - } - final CommentsInfoItem item = (CommentsInfoItem) infoItem; - - itemTitleView.setText(item.getUploaderName()); - - itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE); - - itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE); - } -} diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml index b1b644d8c05..2a8c747cd63 100644 --- a/app/src/main/res/layout/fragment_comments.xml +++ b/app/src/main/res/layout/fragment_comments.xml @@ -9,7 +9,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" - tools:listitem="@layout/list_comments_item" /> + tools:listitem="@layout/list_comment_item" /> + tools:text="Author Name, Lorem ipsum · 5 months ago" /> - - diff --git a/app/src/main/res/layout/list_comments_mini_item.xml b/app/src/main/res/layout/list_comments_mini_item.xml deleted file mode 100644 index 606a237c5fc..00000000000 --- a/app/src/main/res/layout/list_comments_mini_item.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - From 4c709b2c4d1aaa6c8568e5f7845f314072fef643 Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 11 Apr 2023 10:58:32 +0200 Subject: [PATCH 042/154] Use start/end instead of left/right in comment layout --- app/src/main/res/layout/list_comment_item.xml | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/layout/list_comment_item.xml b/app/src/main/res/layout/list_comment_item.xml index 26fcc97272e..3e0cce2e7e3 100644 --- a/app/src/main/res/layout/list_comment_item.xml +++ b/app/src/main/res/layout/list_comment_item.xml @@ -15,10 +15,9 @@ android:layout_width="42dp" android:layout_height="42dp" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_alignParentTop="true" - android:layout_marginLeft="3dp" - android:layout_marginRight="@dimen/comment_item_avatar_right_margin" + android:layout_marginStart="3dp" + android:layout_marginEnd="@dimen/comment_item_avatar_right_margin" android:focusable="false" android:src="@drawable/placeholder_person" app:shapeAppearance="@style/CircularImageView" @@ -29,7 +28,7 @@ android:layout_width="@dimen/video_item_detail_pinned_image_width" android:layout_height="@dimen/video_item_detail_pinned_image_height" android:layout_alignParentTop="true" - android:layout_marginRight="@dimen/video_item_detail_pinned_right_margin" + android:layout_marginEnd="@dimen/video_item_detail_pinned_right_margin" android:layout_toEndOf="@+id/itemThumbnailView" android:contentDescription="@string/detail_pinned_comment_view_description" android:src="@drawable/ic_pin" @@ -56,18 +55,16 @@ android:layout_below="@id/itemTitleView" android:layout_marginBottom="@dimen/channel_item_description_to_details_margin" android:layout_toEndOf="@+id/itemThumbnailView" - android:layout_toRightOf="@+id/itemThumbnailView" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="@dimen/comment_item_content_text_size" tools:text="Comment Content, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit" /> - @@ -76,8 +73,8 @@ android:layout_width="wrap_content" android:layout_height="@dimen/video_item_detail_like_image_height" android:layout_below="@id/itemCommentContentView" - android:layout_marginLeft="@dimen/video_item_detail_like_margin" - android:layout_toRightOf="@id/detail_thumbs_up_img_view" + android:layout_marginStart="@dimen/video_item_detail_like_margin" + android:layout_toEndOf="@id/detail_thumbs_up_img_view" android:lines="1" android:textAppearance="?android:attr/textAppearanceMedium" android:textSize="@dimen/video_item_detail_likes_text_size" @@ -89,8 +86,8 @@ android:layout_width="@dimen/video_item_detail_heart_image_size" android:layout_height="@dimen/video_item_detail_heart_image_size" android:layout_below="@id/itemCommentContentView" - android:layout_marginLeft="@dimen/video_item_detail_heart_margin" - android:layout_toRightOf="@+id/detail_thumbs_up_count_view" + android:layout_marginStart="@dimen/video_item_detail_heart_margin" + android:layout_toEndOf="@+id/detail_thumbs_up_count_view" android:contentDescription="@string/detail_heart_img_view_description" android:src="@drawable/ic_heart" android:visibility="gone" From 059db6fb31880b279ea30bc45c5048d7941cc30a Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 11 Apr 2023 14:56:04 +0200 Subject: [PATCH 043/154] Add replies button to comments --- .../holder/CommentInfoItemHolder.java | 56 ++++++++++++++----- .../org/schabi/newpipe/util/Localization.java | 5 ++ app/src/main/res/layout/list_comment_item.xml | 38 ++++++++----- app/src/main/res/values/strings.xml | 4 ++ 4 files changed, 74 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentInfoItemHolder.java index afe641c1859..f1ac72e8960 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentInfoItemHolder.java @@ -9,6 +9,7 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; @@ -57,15 +58,20 @@ public class CommentInfoItemHolder extends InfoItemHolder { private final RelativeLayout itemRoot; private final ImageView itemThumbnailView; private final TextView itemContentView; + private final ImageView itemThumbsUpView; private final TextView itemLikesCountView; private final TextView itemTitleView; private final ImageView itemHeartView; private final ImageView itemPinnedView; + private final Button repliesButton; private final CompositeDisposable disposables = new CompositeDisposable(); - @Nullable private Description commentText; - @Nullable private StreamingService streamService; - @Nullable private String streamUrl; + @Nullable + private Description commentText; + @Nullable + private StreamingService streamService; + @Nullable + private String streamUrl; public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) { @@ -74,10 +80,12 @@ public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder, itemRoot = itemView.findViewById(R.id.itemRoot); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemContentView = itemView.findViewById(R.id.itemCommentContentView); + itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view); itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view); itemTitleView = itemView.findViewById(R.id.itemTitleView); itemHeartView = itemView.findViewById(R.id.detail_heart_image_view); itemPinnedView = itemView.findViewById(R.id.detail_pinned_view); + repliesButton = itemView.findViewById(R.id.replies_button); commentHorizontalPadding = (int) infoItemBuilder.getContext() .getResources().getDimension(R.dimen.comments_horizontal_padding); @@ -97,6 +105,8 @@ public void updateFromItem(final InfoItem infoItem, } final CommentsInfoItem item = (CommentsInfoItem) infoItem; + + // load the author avatar PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView); if (ImageStrategy.shouldLoadImages()) { itemThumbnailView.setVisibility(View.VISIBLE); @@ -109,6 +119,10 @@ public void updateFromItem(final InfoItem infoItem, } itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); + + // setup the top row, with pinned icon, author name and comment date + itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE); + final String uploadDate; if (item.getUploadDate() != null) { uploadDate = Localization.relativeTime(item.getUploadDate().offsetDateTime()); @@ -117,9 +131,29 @@ public void updateFromItem(final InfoItem infoItem, } itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(), uploadDate)); - itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE); + + // setup bottom row, with likes, heart and replies button + if (item.getLikeCount() >= 0) { + itemLikesCountView.setText( + Localization.shortCount( + itemBuilder.getContext(), + item.getLikeCount())); + } else { + itemLikesCountView.setText("-"); + } + itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE); + final boolean hasReplies = item.getReplies() != null; + repliesButton.setOnClickListener(hasReplies ? (v) -> openRepliesFragment() : null); + repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE); + repliesButton.setText(hasReplies + ? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : ""); + ((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin = + hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext()); + + + // setup comment content and click listeners to expand/ellipsize it try { streamService = NewPipe.getService(item.getServiceId()); } catch (final ExtractionException e) { @@ -135,16 +169,6 @@ public void updateFromItem(final InfoItem infoItem, //noinspection ClickableViewAccessibility itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); - if (item.getLikeCount() >= 0) { - itemLikesCountView.setText( - Localization.shortCount( - itemBuilder.getContext(), - item.getLikeCount())); - } else { - itemLikesCountView.setText("-"); - } - - itemView.setOnClickListener(view -> { toggleEllipsize(); if (itemBuilder.getOnCommentsSelectedListener() != null) { @@ -278,4 +302,8 @@ private void linkifyCommentContentView(@Nullable final Consumer onComp onCompletion); } } + + private void openRepliesFragment() { + // TODO + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index c4034252de3..20d21d76b49 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -209,6 +209,11 @@ public static String deletedDownloadCount(final Context context, final int delet deletedCount, shortCount(context, deletedCount)); } + public static String replyCount(final Context context, final int replyCount) { + return getQuantity(context, R.plurals.replies, 0, replyCount, + String.valueOf(replyCount)); + } + public static String getDurationString(final long duration) { final String output; diff --git a/app/src/main/res/layout/list_comment_item.xml b/app/src/main/res/layout/list_comment_item.xml index 3e0cce2e7e3..631ab204b3f 100644 --- a/app/src/main/res/layout/list_comment_item.xml +++ b/app/src/main/res/layout/list_comment_item.xml @@ -16,7 +16,6 @@ android:layout_height="42dp" android:layout_alignParentStart="true" android:layout_alignParentTop="true" - android:layout_marginStart="3dp" android:layout_marginEnd="@dimen/comment_item_avatar_right_margin" android:focusable="false" android:src="@drawable/placeholder_person" @@ -31,39 +30,37 @@ android:layout_marginEnd="@dimen/video_item_detail_pinned_right_margin" android:layout_toEndOf="@+id/itemThumbnailView" android:contentDescription="@string/detail_pinned_comment_view_description" - android:src="@drawable/ic_pin" - android:visibility="gone" - tools:visibility="visible" /> + android:src="@drawable/ic_pin" /> + tools:text="Author Name, Lorem ipsum • 5 months ago" /> + tools:text="@tools:sample/lorem/random[1]" /> @@ -71,26 +68,37 @@ + android:src="@drawable/ic_heart" /> + +