From 405d6bee782f841b6c3e1123f3f6ed6202856d00 Mon Sep 17 00:00:00 2001 From: TotalCaesar659 Date: Sun, 4 Feb 2018 21:10:23 +0000 Subject: [PATCH 001/276] Translated using Weblate (Russian) Currently translated at 94.3% (252 of 267 strings) --- app/src/main/res/values-ru/strings.xml | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index eb3a21a81..58fb004c7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -293,4 +293,38 @@ Воспроизвести Воспроизвести в фоне Воспроизвести в окне +Ни одного потокового проигрывателя не было найдено (вы можете установить VLC) + Страна контента по умолчанию + Сервис + Всегда + Только один раз + + Переключить ориентацию + Перейти в фон + Перейти в окно + Перейти в главное окно + + Ошибка проигрывателя без возможности восстановления + Внешние проигрыватели не поддерживают ссылки этих типов + Неверная ссылка + Потоки видео не найдены + Потоки аудио не найдены + + Пожертвовать + NewPipe разрабатывается добровольцами в свободное время ради вашего удобства использования программы. Пора воздать должное, разработчики могут сделать NewPipe еще лучше с чашечкой кофе! + Воздать должное + Веб-сайт + Для получения более подробной информации и последних новостей о NewPipe посетите наш веб-сайт. + Открыть лоток + Закрыть лоток + Открыть с помощью предпочитаемого проигрывателя + Предпочитаемый проигрыватель + + Проигрыватель видео + Фоновый проигрыватель + Проигрыватель в окне + Всегда спрашивать + + Получение информации… + Загрузка запрашиваемого контента From bfc7718a21a74d410ebd431acbbb73b496936366 Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Mon, 5 Feb 2018 12:33:06 +0000 Subject: [PATCH 002/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 86.1% (230 of 267 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8d95ff204..3ce670163 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -260,4 +260,12 @@ 無法播放此串流 發生無法復原的播放器錯誤 從播放器錯誤中恢復 + 在背景或是影片詳細資訊頁面上按下浮模按鈕時顯示提示 + 外部播放器不支援這類型的連結 + 無效的 URL + 找不到影片串流 + 找不到音訊串流 + + 贊助 + 網站 From 5eae235b3cd6965520eabd741a03f8564acfb034 Mon Sep 17 00:00:00 2001 From: Oleh Ilnytskyi Date: Mon, 5 Feb 2018 15:32:33 +0000 Subject: [PATCH 003/276] Translated using Weblate (Ukrainian) Currently translated at 38.2% (102 of 267 strings) --- app/src/main/res/values-uk/strings.xml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0c726f938..5e61dc282 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -125,4 +125,24 @@ Скопійовано до буферу обміну. Будь ласка, оберіть теку для завантаження. - +Потоковий програвач не знайдено (ви можете встановити VLC для відтворення) + Відкрити у спливаючому вікні + Певні роздільності НЕ МАТИМУТЬ звуку якщо цей параметр увімкнено + NewPipe у спливаючому вікні + Підписатися + Ви підписалися + Ви відписалися від каналу + Неможливо змінити підписку + Неможливо оновити підписку + + Головна + Підписки + + Новинки + + Фон + Спливаюче вікно + + Роздільна здатність спливаючого вікна за замовчуванням + Не всі пристрої підтримують програвання 2K/4K відео + From 2a0e5d6835c4eac349ea57d4c8de1fe367e42ce5 Mon Sep 17 00:00:00 2001 From: Emanuele Petriglia Date: Tue, 6 Feb 2018 08:53:09 +0000 Subject: [PATCH 004/276] Translated using Weblate (Italian) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-it/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 354c413a9..88c13ead9 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -336,4 +336,14 @@ Raccogliendo informazioni… Il contenuto richiesto sta caricando - +Importa database + Esporta database + sovrascriverà la cronologia corrente e le iscrizioni + Esporta la cronologia, le iscrizioni e le playlist. + Esportazione completa + Importazione completa + Nessun file Zip valido + ATTENZIONE: Non posso importare tutti i file. + Questo sovrascriverà le tue impostazioni attuali. + + From e973868a903450ea768cf6b3af456adc2a14687f Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Tue, 6 Feb 2018 11:23:33 +0000 Subject: [PATCH 005/276] Translated using Weblate (Dutch) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-nl/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 9b06d8888..7dfa78f01 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -332,4 +332,14 @@ te openen in pop-upmodus Info ophalen… De gevraagde inhoud is aan het laden - +Database importeren + Database exporteren + Dit zal je huidige geschiedenis en abonnementen overschrijven + Exporteer geschiedenis, abonnementen en speellijsten. + Export voltooid + Import voltooid + Geen geldig zip-bestand + WAARSCHUWING: kon niet alle bestanden importeren. + Dit zal je huidige configuratie overschrijven. + + From 059378eedf7ce53b75663c407912a79a3aba3736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Freddy=20Mor=C3=A1n=20Jr?= Date: Tue, 6 Feb 2018 19:07:07 +0000 Subject: [PATCH 006/276] Translated using Weblate (Spanish) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-es/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index af9ca97b3..2207dc002 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -331,4 +331,14 @@ abrir en modo popup Obteniendo información… El contenido solicitado se está cargando - +Importar base de datos + Exportar base de datos + Reemplazará su historial actual y sus suscripciones + Exportar historial, suscripciones y listas de reproducción. + Exportación completa + Importación completa + Archivo zip no válido + ADVERTENCIA: no se pudieron importar todos los archivos. + Esto reemplazará su configuración actual. + + From d3168a9022185be93b73cb939edb042f4e589210 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Wed, 7 Feb 2018 10:22:27 +0100 Subject: [PATCH 007/276] Fix opening SoundCloud links --- app/src/main/AndroidManifest.xml | 15 +++++++++++++++ .../org/schabi/newpipe/util/NavigationHelper.java | 5 ----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f0a8d45e0..bc3dc62e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -169,6 +169,21 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 8894af9df..51cdd4cfe 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -20,7 +20,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.about.AboutActivity; import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.AudioStream; @@ -419,10 +418,6 @@ public class NavigationHelper { } public static Intent getIntentByLink(Context context, StreamingService service, String url) throws ExtractionException { - if (service != ServiceList.YouTube.getService()) { - throw new ExtractionException("Service not supported at the moment"); - } - StreamingService.LinkType linkType = service.getLinkTypeByUrl(url); if (linkType == StreamingService.LinkType.NONE) { From 13d1974a5b58c72fde0a79bf87e4f6ab20773c35 Mon Sep 17 00:00:00 2001 From: BurningKarl Date: Tue, 6 Feb 2018 13:32:43 +0000 Subject: [PATCH 008/276] Translated using Weblate (German) Currently translated at 99.2% (274 of 276 strings) --- app/src/main/res/values-de/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6c13ca985..a93dfd541 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -328,4 +328,12 @@ Informationen werden abgerufen… Der angeforderte Inhalt wird geladen - +Datenbank importieren + Datenbank exportieren + Wird deinen Verlauf und deine Abos überschreiben + Verlauf, Abos und Playlists exportieren. + Keine gültige Zip-Datei + WARNUNG: Nicht alle Dateien konnten importiert werden. + Dies wird deine aktuellen Einstellungen überschreiben. + + From 756fb795d6ba1035075c1ba3a9fe3d786c92a6f9 Mon Sep 17 00:00:00 2001 From: Oleh Ilnytskyi Date: Tue, 6 Feb 2018 14:11:35 +0000 Subject: [PATCH 009/276] Translated using Weblate (Ukrainian) Currently translated at 68.8% (190 of 276 strings) --- app/src/main/res/values-uk/strings.xml | 108 +++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5e61dc282..171329cda 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -20,12 +20,12 @@ Шлях для завантаження відео Вкажіть шлях для завантаження відео - Вкажіть шлях для завантаження аудіо файлів. + Вкажіть шлях для завантаження аудіо файлів - Шлях де будуть зберігатись завантажені відео. + Шлях де будуть зберігатись завантажені відео Шлях для завантаження аудіо Шлях де будуть зберігатись завантажені аудіо файли - Автоматично відтворювати при виклику з іншого додатку + Автоматично відтворювати Автоматично відтворювати відео коли NewPipe викликано з іншого додатку. Типова роздільна здатність Відтворювати за допомогою Kodi @@ -63,7 +63,7 @@ Не вдалося проаналізувати веб-сайт. Не вдалося повністю проаналізувати веб-сайт. Контент не доступний. - Заблоковано GEMA. + Заблоковано GEMA Не вдалося налаштувати меню завантаження. Це пряма трансляція. Це ще не підтримується. Не вдалося отримати ніякий потік. @@ -143,6 +143,102 @@ Фон Спливаюче вікно - Роздільна здатність спливаючого вікна за замовчуванням + Типова роздільна здатність спливаючого вікна Не всі пристрої підтримують програвання 2K/4K відео - + Показувати більші роздільні здатності + Відео формат за замовчуванням + Пам\'ятати розмір спливаючого вікна та положення + Пам\'ятати останній розмір та позицію спливаючого вікна + Керування жестами + Використовувати жести для контролю яскравості та гучності програвача + Шукати схожі + Показувати схожі під час пошуку + Історія пошуку + Зберігати пошукові запити локально + Історія + Вести облік перегляду відео + Відновити фокус + Продовжувати відтворення опісля переривання (наприклад телефонний дзвінок) + Відображати вказівку Утримуйте для додачі + Країна контенту за замовчуванням + Сервіс + Програвач + Поведінка + Історія + Спливаюче вікно + Відворення у спливаючому вікні + Додано в чергу у фоні + Додано в чергу в спливаючому вікні + Плейлист + Фільтрувати + Оновити + Очистити + Зміна розміру + Найкраща роздільна здатність + Відміна + Грати всі + Щоразу + Тільки тепер + + NewPipe сповіщення + Сповіщення для фонового та спливаючого плеєра NewPipe + + [Невідомо] + + Перемкнутися у Фон + Перемкнутися у Спливаюче вікно + Перемкнутися у Головне + + Імпортувати базу + Експортувати базу + Це перезапише наявну історію та підписки + Експортувати історію, підписки та плейлісти. + Помилка при відворенні потоку + Відновлююсь після помилки програвача + Помилкова лінка URL + Не знайдено відео потоків + Не знайдено аудіо потоків + + Звіт користувача + Без підписників + Без відео + Помилкова лінка URL або Інтернет не доступний + Цей дозвіл має бути відкритим +\nу спливаючому вікні + + + Завантажити + Допустимі символи у іменах файлів + "Невірні символи будуть скоректовані цим " + Символ заміни + + Літери та цифри + Більше спеціальних символів + + Про NewPipe + Налаштування + Про + Неможливо завантажити ліцензію + Ліцензії + Внесок + Переглянути на GitHub + Підтримати + Вебсайт + Історія + Історію стерто + Експорт завершено + Іморт завершено + Топ 50 + Новинки + Деталі + Налаштування аудіо + Улюблений програвач + + Відео програвач + Фоновий програвач + Віконний програвач + Щоразу питати + + Отримую інформацію… + Контент завантажується + From 629549d76fa925eec42ab07d182fc7737c0d6ad1 Mon Sep 17 00:00:00 2001 From: r2308145 Date: Wed, 7 Feb 2018 18:19:31 +0000 Subject: [PATCH 010/276] Translated using Weblate (Czech) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-cs/strings.xml | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b4c6c5845..130da7092 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -307,4 +307,33 @@ otevření ve vyskakovacím okně Otevřít Drawer Zavřít Drawer - + Nenalezen přehrávač streamu (pro přehrání můžete nainstalovat např. VLC) + Vždy + Pouze jednou + + Importovat databázi + Exportovat databázi + Přepíše vaši dosavadní historii a odběry + Exportuje historii, odběry a playlisty + Externí přehrávače nepodporují tyto druhy odkazů + Neplatná URL + Nenalezeny žádné video streamy + Nenalezeny žádné audio streamy + + Export dokončen + Import dokončen + Bez platného Zip souboru + UPOZORNĚNÍ: Nelze importovat všechny soubory. + Tímto se anuluje vaše aktuální nastavení. + + Otevřít preferovaným přehrávačem + Preferovaný přehrávač + + Video přehrávač + Přehrávač na pozadí + Přehrávač v okně + Vždy se ptát + + Získávám informace… + Požadovaný obsah se načítá + From efc7049dfdda5a2ebb4566cbc2225428cd98eb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C3=B3jcik=2Eonline?= <35582382+wojcik-online@users.noreply.github.com> Date: Wed, 7 Feb 2018 19:24:25 +0100 Subject: [PATCH 011/276] Control media volume with the hardware buttons. Control media volume with the hardware buttons in the MainActivity. This is how the YouTube app works. Closes #1072. --- app/src/main/java/org/schabi/newpipe/MainActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 9e8f3fa76..85bb04d6d 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -95,6 +95,8 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) { initFragments(); } + + setVolumeControlStream(AudioManager.STREAM_MUSIC); setSupportActionBar(findViewById(R.id.toolbar)); setupDrawer(); From 7ea0862f9518e92957bf4dd940897e2661f8e344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C3=B3jcik=2Eonline?= <35582382+wojcik-online@users.noreply.github.com> Date: Wed, 7 Feb 2018 19:35:04 +0100 Subject: [PATCH 012/276] import AudioManager --- app/src/main/java/org/schabi/newpipe/MainActivity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 85bb04d6d..c698f7fa9 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -22,6 +22,7 @@ package org.schabi.newpipe; import android.content.Intent; import android.content.SharedPreferences; +import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; From e7b4b8805586c9f6497dac9a2952c03f43f944cb Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Wed, 7 Feb 2018 19:38:57 +0100 Subject: [PATCH 013/276] Revert "Control media volume with the hardware buttons." --- app/src/main/java/org/schabi/newpipe/MainActivity.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index c698f7fa9..9e8f3fa76 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -22,7 +22,6 @@ package org.schabi.newpipe; import android.content.Intent; import android.content.SharedPreferences; -import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -96,8 +95,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) { initFragments(); } - - setVolumeControlStream(AudioManager.STREAM_MUSIC); setSupportActionBar(findViewById(R.id.toolbar)); setupDrawer(); From ef0659f4368f6790ae3336899d796ba803e0e46f Mon Sep 17 00:00:00 2001 From: ScratchBuild Date: Thu, 8 Feb 2018 01:16:04 +0000 Subject: [PATCH 014/276] Translated using Weblate (Japanese) Currently translated at 72.4% (200 of 276 strings) --- app/src/main/res/values-ja/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 8d28fa00e..7433a997b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -278,4 +278,5 @@ ポップアップ再生に変更 メイン再生に変更 + 動画プレイヤーが見つかりません (VLCをインストールして再生できます) From 2e8d86575eae849cab29c1299d687ecb52c112b5 Mon Sep 17 00:00:00 2001 From: thami simo Date: Thu, 8 Feb 2018 00:50:04 +0000 Subject: [PATCH 015/276] Translated using Weblate (Arabic) Currently translated at 94.2% (260 of 276 strings) --- app/src/main/res/values-ar/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 24edb6043..ba26d6ed6 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -306,4 +306,9 @@ العنوان خاطئ \@string/preferred_player_settings_title + لم يتم العثور على مشغل بث (يمكنك تثبيت VLC لتشغيله) + استيراد قاعدة البيانات + تصدير قاعدة البيانات + "سيتجاوز سجل التاريخ والاشتراكات الحالية " + تصدير سجل, الاشتراكات وقوائم التشغيل. From 1f4f87d3bddd0b1041e5eb450f7ce3a483a7481d Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Wed, 7 Feb 2018 05:44:14 +0000 Subject: [PATCH 016/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 95.6% (264 of 276 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3ce670163..b088fd4bd 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -268,4 +268,40 @@ 贊助 網站 + 匯入資料庫 + 匯出資料庫 + 將覆蓋您目前的歷史記錄和訂閱 + 匯出歷史記錄、訂閱和播放清單。 + 返回 + 欲了解更多關於 NewPipe 的資訊和最新近況,請造訪我們的網站。 + 首頁內容 + 空白頁面 + 互動導覽頁面 + 訂閱頁面 + 提要頁面 + 頻道頁面 + 選擇頻道 + 尚未訂閱頻道 + 選擇互動導覽 + 匯出完成 + 匯入完成 + 無有效的 Zip 檔案 + 警告:無法匯入所有檔案。 + 這將覆蓋您目前的設定。 + + 互動導覽 + 動向 + 前 50 + 最新和熱門 + 背景播放 + 懸浮視窗播放 + 移除 + 詳細資訊 + 音訊設定 + 在背景佇列 + 在懸浮視窗佇列 + 在此開始播放 + 在背景這裡開始 + 在懸浮視窗這裡開始 + From 77a06c760479acdb931da7e129916cea9256c399 Mon Sep 17 00:00:00 2001 From: r2308145 Date: Wed, 7 Feb 2018 18:20:14 +0000 Subject: [PATCH 017/276] Translated using Weblate (Czech) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-cs/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 130da7092..5db84181c 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -314,7 +314,7 @@ otevření ve vyskakovacím okně Importovat databázi Exportovat databázi Přepíše vaši dosavadní historii a odběry - Exportuje historii, odběry a playlisty + Exportuje historii, odběry a playlisty. Externí přehrávače nepodporují tyto druhy odkazů Neplatná URL Nenalezeny žádné video streamy From 420d28c713ed26706a4de66cf6b56943be16e8c6 Mon Sep 17 00:00:00 2001 From: Rintaro matsuo Date: Thu, 8 Feb 2018 01:17:46 +0000 Subject: [PATCH 018/276] Translated using Weblate (Japanese) Currently translated at 72.4% (200 of 276 strings) --- app/src/main/res/values-ja/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7433a997b..cfcb89b3b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -9,7 +9,7 @@ 保存 検索 設定 - %1$s の間違いではありませんか + もしかして: %1$s 共有 ブラウザを選択 回転 From 40844dcd769206febae8069518212b78147f373c Mon Sep 17 00:00:00 2001 From: ScratchBuild Date: Thu, 8 Feb 2018 01:18:39 +0000 Subject: [PATCH 019/276] Translated using Weblate (Japanese) Currently translated at 72.4% (200 of 276 strings) --- app/src/main/res/values-ja/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index cfcb89b3b..7b17e9ba3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -37,7 +37,7 @@ アップローダー サムネイル 低評価 高評価 - 外部プレイヤーを使用する + 外部プレイヤーを使用する 外部プレイヤーを使用する バックグラウンドで再生中 From 960fd9be383d30ea0430e60b2548ed416d0d2c8b Mon Sep 17 00:00:00 2001 From: Rintaro matsuo Date: Thu, 8 Feb 2018 01:19:04 +0000 Subject: [PATCH 020/276] Translated using Weblate (Japanese) Currently translated at 72.4% (200 of 276 strings) --- app/src/main/res/values-ja/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7b17e9ba3..140c434a3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -177,7 +177,7 @@ ポップアップ サイズを変更 - このオプションが有効になっているとき、一部の解像度ではオーディオがありません + このオプションが有効になっているとき、一部の解像度では音声がありません。 プレーヤーのジェスチャー コントロール ジェスチャーを使用してプレーヤーの明るさと音量をコントロールする 検索候補 From f71242a0362bec9d23260e43c2f25eea817f24c2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 15 Jan 2018 12:30:52 -0800 Subject: [PATCH 021/276] -Added schema for local playlist and stream statistics. -Added normalized schema for stream history. -Added managers for specialized database access for stream and local playlist. --- .../org/schabi/newpipe/NewPipeDatabase.java | 6 + .../schabi/newpipe/database/AppDatabase.java | 27 ++- .../org/schabi/newpipe/database/BasicDAO.java | 3 - .../database/{history => }/Converters.java | 14 +- .../playlist/PlaylistMetadataEntry.java | 36 ++++ .../database/playlist/dao/PlaylistDAO.java | 35 ++++ .../playlist/dao/PlaylistStreamDAO.java | 69 ++++++++ .../playlist/model/PlaylistEntity.java | 59 +++++++ .../playlist/model/PlaylistStreamEntity.java | 77 +++++++++ .../stream/StreamStatisticsEntry.java | 54 ++++++ .../database/stream/dao/StreamDAO.java | 57 +++++++ .../database/stream/dao/StreamHistoryDAO.java | 54 ++++++ .../database/stream/model/StreamEntity.java | 154 ++++++++++++++++++ .../stream/model/StreamHistoryEntity.java | 58 +++++++ .../subscription/SubscriptionEntity.java | 3 +- .../playlist/LocalPlaylistManager.java | 78 +++++++++ .../playlist/StreamRecordManager.java | 47 ++++++ .../stored/LocalPlaylistInfoItem.java | 30 ++++ .../stored/StreamStatisticsInfoItem.java | 38 +++++ .../org/schabi/newpipe/player/BasePlayer.java | 36 ++-- .../org/schabi/newpipe/util/Constants.java | 1 + 21 files changed, 913 insertions(+), 23 deletions(-) rename app/src/main/java/org/schabi/newpipe/database/{history => }/Converters.java (63%) create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 7111abcf7..4da1c63f2 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -28,4 +28,10 @@ public final class NewPipeDatabase { return databaseInstance; } + + @NonNull + public static AppDatabase getInstance(Context context) { + if (databaseInstance == null) init(context); + return databaseInstance; + } } 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 21868e3c2..e09687ce4 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -4,16 +4,31 @@ import android.arch.persistence.room.Database; import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.TypeConverters; -import org.schabi.newpipe.database.history.Converters; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.WatchHistoryEntry; +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistEntity; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; @TypeConverters({Converters.class}) -@Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false) +@Database( + entities = { + SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class, + StreamEntity.class, StreamHistoryEntity.class, PlaylistEntity.class, + PlaylistStreamEntity.class + }, + version = 1, + exportSchema = false +) public abstract class AppDatabase extends RoomDatabase { public static final String DATABASE_NAME = "newpipe.db"; @@ -23,4 +38,12 @@ public abstract class AppDatabase extends RoomDatabase { public abstract WatchHistoryDAO watchHistoryDAO(); public abstract SearchHistoryDAO searchHistoryDAO(); + + public abstract StreamDAO streamDAO(); + + public abstract StreamHistoryDAO streamHistoryDAO(); + + public abstract PlaylistDAO playlistDAO(); + + public abstract PlaylistStreamDAO playlistStreamDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index 03a94508b..425c122ca 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -23,9 +23,6 @@ public interface BasicDAO { @Insert(onConflict = OnConflictStrategy.FAIL) List insertAll(final Collection entities); - @Insert(onConflict = OnConflictStrategy.REPLACE) - long upsert(final Entity entity); - /* Searches */ Flowable> getAll(); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/Converters.java b/app/src/main/java/org/schabi/newpipe/database/Converters.java similarity index 63% rename from app/src/main/java/org/schabi/newpipe/database/history/Converters.java rename to app/src/main/java/org/schabi/newpipe/database/Converters.java index 093c741f1..d48fbfaf1 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/Converters.java +++ b/app/src/main/java/org/schabi/newpipe/database/Converters.java @@ -1,7 +1,9 @@ -package org.schabi.newpipe.database.history; +package org.schabi.newpipe.database; import android.arch.persistence.room.TypeConverter; +import org.schabi.newpipe.extractor.stream.StreamType; + import java.util.Date; public class Converters { @@ -25,4 +27,14 @@ public class Converters { public static Long dateToTimestamp(Date date) { return date == null ? null : date.getTime(); } + + @TypeConverter + public static StreamType streamTypeOf(String value) { + return StreamType.valueOf(value); + } + + @TypeConverter + public static String stringOf(StreamType streamType) { + return streamType.name(); + } } 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 new file mode 100644 index 000000000..53ae3d48a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -0,0 +1,36 @@ +package org.schabi.newpipe.database.playlist; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; + +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; + +public class PlaylistMetadataEntry { + final public static String PLAYLIST_STREAM_COUNT = "streamCount"; + + @ColumnInfo(name = PLAYLIST_ID) + final public long uid; + @ColumnInfo(name = PLAYLIST_NAME) + final public String name; + @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = PLAYLIST_STREAM_COUNT) + final public long streamCount; + + public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) { + this.uid = uid; + this.name = name; + this.thumbnailUrl = thumbnailUrl; + this.streamCount = streamCount; + } + + public LocalPlaylistInfoItem toStoredPlaylistInfoItem() { + LocalPlaylistInfoItem storedPlaylistInfoItem = new LocalPlaylistInfoItem(uid, name); + storedPlaylistInfoItem.setThumbnailUrl(thumbnailUrl); + storedPlaylistInfoItem.setStreamCount(streamCount); + return storedPlaylistInfoItem; + } +} 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 new file mode 100644 index 000000000..b337769bc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java @@ -0,0 +1,35 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +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; + +@Dao +public abstract class PlaylistDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + PLAYLIST_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + PLAYLIST_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") + public abstract Flowable> getPlaylist(final long 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 new file mode 100644 index 000000000..b9f325aa2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -0,0 +1,69 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*; +import static org.schabi.newpipe.database.stream.model.StreamEntity.*; + +@Dao +public abstract class PlaylistStreamDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE + + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") + public abstract void deleteBatch(final long playlistId); + + @Query("SELECT MAX(" + JOIN_INDEX + ")" + + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") + public abstract Flowable getMaximumIndexOf(final long playlistId); + + @Transaction + @Query("SELECT " + STREAM_ID + ", " + STREAM_SERVICE_ID + ", " + STREAM_URL + ", " + + STREAM_TITLE + ", " + STREAM_TYPE + ", " + STREAM_UPLOADER + ", " + + STREAM_DURATION + ", " + STREAM_THUMBNAIL_URL + + + " FROM " + STREAM_TABLE + " INNER JOIN " + + // get ids of streams of the given playlist + "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE " + + JOIN_PLAYLIST_ID + " = :playlistId)" + + + // then merge with the stream metadata + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + JOIN_INDEX + " ASC") + public abstract Flowable> getOrderedStreamsOf(long playlistId); + + @Transaction + @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + + PLAYLIST_THUMBNAIL_URL + ", COUNT(*) AS " + PLAYLIST_STREAM_COUNT + + + " FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + PLAYLIST_STREAM_JOIN_TABLE + "." + JOIN_PLAYLIST_ID + + " GROUP BY " + JOIN_PLAYLIST_ID) + public abstract Flowable> getPlaylistMetadata(); +} 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 new file mode 100644 index 000000000..a3ec1b5f2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -0,0 +1,59 @@ +package org.schabi.newpipe.database.playlist.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; + +import java.util.Date; + +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; + +@Entity(tableName = PLAYLIST_TABLE, + indices = {@Index(value = {PLAYLIST_NAME})}) +public class PlaylistEntity { + final public static String PLAYLIST_TABLE = "playlists"; + final public static String PLAYLIST_ID = "uid"; + final public static String PLAYLIST_NAME = "name"; + final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = PLAYLIST_ID) + private long uid = 0; + + @ColumnInfo(name = PLAYLIST_NAME) + private String name; + + @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) + private String thumbnailUrl; + + public PlaylistEntity(String name, String thumbnailUrl) { + this.name = name; + this.thumbnailUrl = thumbnailUrl; + } + + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java new file mode 100644 index 000000000..3d71f7e70 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java @@ -0,0 +1,77 @@ +package org.schabi.newpipe.database.playlist.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.ForeignKey; +import android.arch.persistence.room.Index; + +import org.schabi.newpipe.database.stream.model.StreamEntity; + +import static android.arch.persistence.room.ForeignKey.CASCADE; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; + +@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE, + primaryKeys = {JOIN_PLAYLIST_ID, JOIN_STREAM_ID, JOIN_INDEX}, + indices = { + @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true), + @Index(value = {JOIN_STREAM_ID}) + }, + foreignKeys = { + @ForeignKey(entity = PlaylistEntity.class, + parentColumns = PlaylistEntity.PLAYLIST_ID, + childColumns = JOIN_PLAYLIST_ID, + onDelete = CASCADE, onUpdate = CASCADE, deferred = true), + @ForeignKey(entity = StreamEntity.class, + parentColumns = StreamEntity.STREAM_ID, + childColumns = JOIN_STREAM_ID, + onDelete = CASCADE, onUpdate = CASCADE, deferred = true) + }) +public class PlaylistStreamEntity { + + final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join"; + final public static String JOIN_PLAYLIST_ID = "playlist_id"; + final public static String JOIN_STREAM_ID = "stream_id"; + final public static String JOIN_INDEX = "join_index"; + + @ColumnInfo(name = JOIN_PLAYLIST_ID) + private long playlistUid; + + @ColumnInfo(name = JOIN_STREAM_ID) + private long streamUid; + + @ColumnInfo(name = JOIN_INDEX) + private int index; + + public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) { + this.playlistUid = playlistUid; + this.streamUid = streamUid; + this.index = index; + } + + public long getPlaylistUid() { + return playlistUid; + } + + public long getStreamUid() { + return streamUid; + } + + public int getIndex() { + return index; + } + + public void setPlaylistUid(long playlistUid) { + this.playlistUid = playlistUid; + } + + public void setStreamUid(long streamUid) { + this.streamUid = streamUid; + } + + public void setIndex(int index) { + this.index = index; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java new file mode 100644 index 000000000..5893394c5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -0,0 +1,54 @@ +package org.schabi.newpipe.database.stream; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamType; + +import java.util.Date; + +public class StreamStatisticsEntry { + final public static String STREAM_LATEST_DATE = "latestAccess"; + final public static String STREAM_WATCH_COUNT = "watchCount"; + + @ColumnInfo(name = StreamEntity.STREAM_ID) + final public long uid; + @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) + final public int serviceId; + @ColumnInfo(name = StreamEntity.STREAM_URL) + final public String url; + @ColumnInfo(name = StreamEntity.STREAM_TITLE) + final public String title; + @ColumnInfo(name = StreamEntity.STREAM_TYPE) + final public StreamType streamType; + @ColumnInfo(name = StreamEntity.STREAM_DURATION) + final public long duration; + @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) + final public String uploader; + @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) + final public long streamId; + @ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE) + final public Date latestAccessDate; + @ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT) + final public long watchCount; + + public StreamStatisticsEntry(long uid, int serviceId, String url, String title, + StreamType streamType, long duration, String uploader, + String thumbnailUrl, long streamId, Date latestAccessDate, + long watchCount) { + this.uid = uid; + this.serviceId = serviceId; + this.url = url; + this.title = title; + this.streamType = streamType; + this.duration = duration; + this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl; + this.streamId = streamId; + this.latestAccessDate = latestAccessDate; + this.watchCount = watchCount; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java new file mode 100644 index 000000000..f7807ef42 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -0,0 +1,57 @@ +package org.schabi.newpipe.database.stream.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; + +@Dao +public abstract class StreamDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + STREAM_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + STREAM_TABLE) + public abstract int deleteAll(); + + @Override + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId") + public abstract Flowable> listByService(int serviceId); + + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + + STREAM_URL + " LIKE :url AND " + + STREAM_SERVICE_ID + " = :serviceId") + public abstract Flowable> getStream(long serviceId, String url); + + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + + STREAM_URL + " LIKE :url AND " + + STREAM_SERVICE_ID + " = :serviceId") + abstract List getStreamInternal(long serviceId, String url); + + @Transaction + public long upsert(StreamEntity stream) { + final List streams = getStreamInternal(stream.getServiceId(), stream.getUrl()); + + final long uid; + if (streams.isEmpty()) { + uid = insert(stream); + } else { + uid = streams.get(0).getUid(); + stream.setUid(uid); + update(stream); + } + return uid; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java new file mode 100644 index 000000000..19c7b9e90 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -0,0 +1,54 @@ +package org.schabi.newpipe.database.stream.dao; + + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; +import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; + +@Dao +public abstract class StreamHistoryDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + STREAM_HISTORY_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") + public abstract int deleteHistory(final long streamId); + + @Query("SELECT * FROM " + STREAM_TABLE + + + // Select the latest entry and watch count for each stream id on history table + " INNER JOIN " + + "(SELECT " + JOIN_STREAM_ID + ", " + + " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " + + " COUNT(*) AS " + STREAM_WATCH_COUNT + + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + public abstract Flowable> getStatistics(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java new file mode 100644 index 000000000..27d0aa7e1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -0,0 +1,154 @@ +package org.schabi.newpipe.database.stream.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Ignore; +import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.util.Constants; + +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; + +@Entity(tableName = STREAM_TABLE, + indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)}) +public class StreamEntity { + + final public static String STREAM_TABLE = "streams"; + final public static String STREAM_ID = "uid"; + final public static String STREAM_SERVICE_ID = "service_id"; + final public static String STREAM_URL = "url"; + final public static String STREAM_TITLE = "title"; + final public static String STREAM_TYPE = "streamType"; + final public static String STREAM_UPLOADER = "uploader"; + final public static String STREAM_DURATION = "duration"; + final public static String STREAM_THUMBNAIL_URL = "thumbnail_url"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = STREAM_ID) + private long uid = 0; + + @ColumnInfo(name = STREAM_SERVICE_ID) + private int serviceId = Constants.NO_SERVICE_ID; + + @ColumnInfo(name = STREAM_URL) + private String url; + + @ColumnInfo(name = STREAM_TITLE) + private String title; + + @ColumnInfo(name = STREAM_TYPE) + private StreamType streamType; + + @ColumnInfo(name = STREAM_DURATION) + private Long duration; + + @ColumnInfo(name = STREAM_UPLOADER) + private String uploader; + + @ColumnInfo(name = STREAM_THUMBNAIL_URL) + private String thumbnailUrl; + + public StreamEntity(final int serviceId, final String title, final String url, + final StreamType streamType, final String thumbnailUrl, final String uploader, + final long duration) { + this.serviceId = serviceId; + this.title = title; + this.url = url; + this.streamType = streamType; + this.thumbnailUrl = thumbnailUrl; + this.uploader = uploader; + this.duration = duration; + } + + @Ignore + public StreamEntity(final StreamInfoItem item) { + this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url, + item.uploader_name, item.duration); + } + + @Ignore + public StreamEntity(final StreamInfo info) { + this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url, + info.uploader_name, info.duration); + } + + @Ignore + public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { + StreamInfoItem item = new StreamInfoItem( + getServiceId(), getUrl(), getTitle(), getStreamType()); + item.setThumbnailUrl(getThumbnailUrl()); + item.setUploaderName(getUploader()); + item.setDuration(getDuration()); + return item; + } + + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + + public int getServiceId() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId = serviceId; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public StreamType getStreamType() { + return streamType; + } + + public void setStreamType(StreamType type) { + this.streamType = type; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public String getUploader() { + return uploader; + } + + public void setUploader(String uploader) { + this.uploader = uploader; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java new file mode 100644 index 000000000..d937a29ed --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java @@ -0,0 +1,58 @@ +package org.schabi.newpipe.database.stream.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.ForeignKey; +import android.arch.persistence.room.Index; +import android.support.annotation.NonNull; + +import java.util.Date; + +import static android.arch.persistence.room.ForeignKey.CASCADE; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; + +@Entity(tableName = STREAM_HISTORY_TABLE, + primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, + // No need to index for timestamp as they will almost always be unique + indices = {@Index(value = {JOIN_STREAM_ID})}, + foreignKeys = { + @ForeignKey(entity = StreamEntity.class, + parentColumns = StreamEntity.STREAM_ID, + childColumns = JOIN_STREAM_ID, + onDelete = CASCADE, onUpdate = CASCADE) + }) +public class StreamHistoryEntity { + final public static String STREAM_HISTORY_TABLE = "stream_history"; + final public static String JOIN_STREAM_ID = "stream_id"; + final public static String STREAM_ACCESS_DATE = "access_date"; + + @ColumnInfo(name = JOIN_STREAM_ID) + private long streamUid; + + @NonNull + @ColumnInfo(name = STREAM_ACCESS_DATE) + private Date accessDate; + + public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) { + this.streamUid = streamUid; + this.accessDate = accessDate; + } + + public long getStreamUid() { + return streamUid; + } + + public void setStreamUid(long streamUid) { + this.streamUid = streamUid; + } + + public Date getAccessDate() { + return accessDate; + } + + public void setAccessDate(@NonNull Date accessDate) { + this.accessDate = accessDate; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index e71088ac9..60eb0c3d3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -50,8 +50,7 @@ public class SubscriptionEntity { return uid; } - /* Keep this package-private since UID should always be auto generated by Room impl */ - void setUid(long uid) { + public void setUid(long uid) { this.uid = uid; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java new file mode 100644 index 000000000..db32a392e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java @@ -0,0 +1,78 @@ +package org.schabi.newpipe.fragments.playlist; + +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistEntity; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.Maybe; + +public class LocalPlaylistManager { + + private final AppDatabase database; + private final StreamDAO streamTable; + private final PlaylistDAO playlistTable; + private final PlaylistStreamDAO playlistStreamTable; + + public LocalPlaylistManager(final AppDatabase db) { + database = db; + streamTable = db.streamDAO(); + playlistTable = db.playlistDAO(); + playlistStreamTable = db.playlistStreamDAO(); + } + + public Maybe> createPlaylist(final String name, final List streams) { + // Disallow creation of empty playlists until user is able to select thumbnail + if (streams.isEmpty()) return Maybe.empty(); + final StreamEntity defaultStream = streams.get(0); + final PlaylistEntity newPlaylist = new PlaylistEntity(name, defaultStream.getThumbnailUrl()); + + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final long playlistId = playlistTable.insert(newPlaylist); + + List joinEntities = new ArrayList<>(streams.size()); + for (int index = 0; index < streams.size(); index++) { + // Upsert streams and get their ids + final long streamId = streamTable.upsert(streams.get(index)); + joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, index)); + } + + return playlistStreamTable.insertAll(joinEntities); + })); + } + + public Maybe appendToPlaylist(final long playlistId, final StreamEntity stream) { + final Maybe streamIdFuture = Maybe.fromCallable(() -> streamTable.upsert(stream)); + final Maybe joinIndexFuture = + playlistStreamTable.getMaximumIndexOf(playlistId).firstElement(); + + return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) -> + playlistStreamTable.insert(new PlaylistStreamEntity(playlistId, + streamId, currentMaxJoinIndex + 1)) + ); + } + + public Completable updateJoin(final long playlistId, final List streamIds) { + List joinEntities = new ArrayList<>(streamIds.size()); + for (int i = 0; i < streamIds.size(); i++) { + joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(i), i)); + } + + return Completable.fromRunnable(() -> database.runInTransaction(() -> { + playlistStreamTable.deleteBatch(playlistId); + playlistStreamTable.insertAll(joinEntities); + })); + } + + public Maybe> getPlaylists() { + return playlistStreamTable.getPlaylistMetadata().firstElement(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java new file mode 100644 index 000000000..bd5bd36a2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.fragments.playlist; + +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; + +import java.util.Date; +import java.util.List; + +import io.reactivex.MaybeObserver; +import io.reactivex.Single; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +public class StreamRecordManager { + + private final AppDatabase database; + private final StreamDAO streamTable; + private final StreamHistoryDAO historyTable; + + public StreamRecordManager(final AppDatabase db) { + database = db; + streamTable = db.streamDAO(); + historyTable = db.streamHistoryDAO(); + } + + public int onChanged(final StreamInfoItem infoItem) { + // Only existing streams are updated + return streamTable.update(new StreamEntity(infoItem)); + } + + public Single onViewed(final StreamInfo info) { + return Single.fromCallable(() -> database.runInTransaction(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return historyTable.insert(new StreamHistoryEntity(streamId, new Date())); + })).subscribeOn(Schedulers.io()); + } + + public int removeHistory(final long streamId) { + return historyTable.deleteHistory(streamId); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java new file mode 100644 index 000000000..63f61cc43 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java @@ -0,0 +1,30 @@ +package org.schabi.newpipe.info_list.stored; + +import org.schabi.newpipe.extractor.InfoItem; + +import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; +import static org.schabi.newpipe.util.Constants.NO_URL; + +public class LocalPlaylistInfoItem extends InfoItem { + private final long playlistId; + private long streamCount; + + public LocalPlaylistInfoItem(final long playlistId, final String name) { + super(InfoType.PLAYLIST, NO_SERVICE_ID, NO_URL, name); + + this.playlistId = playlistId; + this.streamCount = streamCount; + } + + public long getPlaylistId() { + return playlistId; + } + + public long getStreamCount() { + return streamCount; + } + + public void setStreamCount(long streamCount) { + this.streamCount = streamCount; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java new file mode 100644 index 000000000..ef82826ba --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.info_list.stored; + +import org.schabi.newpipe.extractor.InfoItem; + +import java.util.Date; + +public class StreamStatisticsInfoItem extends InfoItem { + private final long streamId; + + private Date latestAccessDate; + private long watchCount; + + public StreamStatisticsInfoItem(final long streamId, final int serviceId, + final String url, final String name) { + super(InfoType.STREAM, serviceId, url, name); + this.streamId = streamId; + } + + public long getStreamId() { + return streamId; + } + + public Date getLatestAccessDate() { + return latestAccessDate; + } + + public void setLatestAccessDate(Date latestAccessDate) { + this.latestAccessDate = latestAccessDate; + } + + public long getWatchCount() { + return watchCount; + } + + public void setWatchCount(long watchCount) { + this.watchCount = watchCount; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 55a73d484..ad2200bfc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -61,8 +61,10 @@ import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.fragments.playlist.StreamRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; @@ -77,9 +79,9 @@ import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Predicate; +import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; @@ -147,6 +149,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected DefaultExtractorsFactory extractorsFactory; protected Disposable progressUpdateReactor; + protected CompositeDisposable databaseUpdateReactor; + + protected StreamRecordManager recordManager; //////////////////////////////////////////////////////////////////////////*/ @@ -172,6 +177,12 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void initPlayer() { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); + if (recordManager == null) { + recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); + } + if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); + databaseUpdateReactor = new CompositeDisposable(); + final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); final LoadControl loadControl = new LoadController(context); @@ -193,18 +204,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen private Disposable getProgressReactor() { return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .filter(new Predicate() { - @Override - public boolean test(Long aLong) throws Exception { - return isProgressLoopRunning(); - } - }) - .subscribe(new Consumer() { - @Override - public void accept(Long aLong) throws Exception { - triggerProgressUpdate(); - } - }); + .filter(ignored -> isProgressLoopRunning()) + .subscribe(ignored -> triggerProgressUpdate()); } public void handleIntent(Intent intent) { @@ -281,6 +282,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (playQueue != null) playQueue.dispose(); if (playbackManager != null) playbackManager.dispose(); if (audioReactor != null) audioReactor.abandonAudioFocus(); + if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); } public void destroy() { @@ -291,6 +293,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen trackSelector = null; simpleExoPlayer = null; + recordManager = null; } public MediaSource buildMediaSource(String url, String overrideExtension) { @@ -668,10 +671,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen "], queue index=[" + playQueue.getIndex() + "]"); } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { final long startPos = info != null ? info.start_position : 0; - if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos)); + if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + + " at: " + getTimeString((int)startPos)); simpleExoPlayer.seekTo(currentSourceIndex, startPos); } + databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); + recordManager.removeRecord(); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java index a6aec96e2..4238424d9 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -12,4 +12,5 @@ public class Constants { public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; public static final int NO_SERVICE_ID = -1; + public static final String NO_URL = ""; } From 38946e4b0f28501fd357b3d4f894646aa7d49146 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 16 Jan 2018 11:48:52 -0800 Subject: [PATCH 022/276] -Added UI for creating playlist. -Added UI for appending item to playlists. -Added mini variant of playlist info item. --- .../database/stream/dao/StreamHistoryDAO.java | 3 +- .../playlist/LocalPlaylistManager.java | 10 +- .../playlist/PlaylistAppendDialog.java | 147 ++++++++++++++++++ .../playlist/PlaylistCreationDialog.java | 91 +++++++++++ .../playlist/StreamRecordManager.java | 27 ++++ .../newpipe/info_list/InfoItemBuilder.java | 3 +- .../newpipe/info_list/InfoItemDialog.java | 5 +- .../newpipe/info_list/InfoListAdapter.java | 6 +- .../holder/PlaylistInfoItemHolder.java | 51 +----- .../holder/PlaylistMiniInfoItemHolder.java | 62 ++++++++ .../stored/LocalPlaylistInfoItem.java | 16 +- .../stored/StreamStatisticsInfoItem.java | 9 +- .../org/schabi/newpipe/player/BasePlayer.java | 1 - .../res/layout/dialog_create_playlist.xml | 22 +++ app/src/main/res/layout/dialog_playlists.xml | 57 +++++++ .../res/layout/list_playlist_mini_item.xml | 70 +++++++++ app/src/main/res/values/strings.xml | 6 + 17 files changed, 508 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java create mode 100644 app/src/main/res/layout/dialog_create_playlist.xml create mode 100644 app/src/main/res/layout/dialog_playlists.xml create mode 100644 app/src/main/res/layout/list_playlist_mini_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java index 19c7b9e90..522c03522 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -48,7 +48,6 @@ public abstract class StreamHistoryDAO implements BasicDAO " COUNT(*) AS " + STREAM_WATCH_COUNT + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + - " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + - " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) public abstract Flowable> getStatistics(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java index db32a392e..911b3c7fd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java @@ -14,6 +14,8 @@ import java.util.List; import io.reactivex.Completable; import io.reactivex.Maybe; +import io.reactivex.Scheduler; +import io.reactivex.schedulers.Schedulers; public class LocalPlaylistManager { @@ -46,7 +48,7 @@ public class LocalPlaylistManager { } return playlistStreamTable.insertAll(joinEntities); - })); + })).subscribeOn(Schedulers.io()); } public Maybe appendToPlaylist(final long playlistId, final StreamEntity stream) { @@ -57,7 +59,7 @@ public class LocalPlaylistManager { return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) -> playlistStreamTable.insert(new PlaylistStreamEntity(playlistId, streamId, currentMaxJoinIndex + 1)) - ); + ).subscribeOn(Schedulers.io()); } public Completable updateJoin(final long playlistId, final List streamIds) { @@ -73,6 +75,8 @@ public class LocalPlaylistManager { } public Maybe> getPlaylists() { - return playlistStreamTable.getPlaylistMetadata().firstElement(); + return playlistStreamTable.getPlaylistMetadata() + .firstElement() + .subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java new file mode 100644 index 000000000..bee3b347e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java @@ -0,0 +1,147 @@ +package org.schabi.newpipe.fragments.playlist; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +public class PlaylistAppendDialog extends DialogFragment { + private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); + private static final String INFO_KEY = "info_key"; + + private StreamInfo streamInfo; + + private View newPlaylistButton; + private RecyclerView playlistRecyclerView; + private InfoListAdapter playlistAdapter; + + public static PlaylistAppendDialog newInstance(final StreamInfo info) { + PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + dialog.setInfo(info); + return dialog; + } + + private void setInfo(StreamInfo info) { + this.streamInfo = info; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + playlistAdapter = new InfoListAdapter(getActivity()); + playlistAdapter.useMiniItemVariants(true); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + Serializable serial = savedInstanceState.getSerializable(INFO_KEY); + if (serial instanceof StreamInfo) streamInfo = (StreamInfo) serial; + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.dialog_playlists, container); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + newPlaylistButton = view.findViewById(R.id.newPlaylist); + playlistRecyclerView = view.findViewById(R.id.playlist_list); + playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + playlistRecyclerView.setAdapter(playlistAdapter); + + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + + newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); + + playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(PlaylistInfoItem selectedItem) { + if (!(selectedItem instanceof LocalPlaylistInfoItem)) return; + final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); + final Toast successToast = + Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); + + playlistManager.appendToPlaylist(playlistId, new StreamEntity(streamInfo)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> successToast.show()); + + getDialog().dismiss(); + } + + @Override + public void held(PlaylistInfoItem selectedItem) {} + }); + + playlistManager.getPlaylists() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(metadataEntries -> { + if (metadataEntries.isEmpty()) { + openCreatePlaylistDialog(); + } + + List playlistInfoItems = new ArrayList<>(metadataEntries.size()); + for (final PlaylistMetadataEntry metadataEntry : metadataEntries) { + playlistInfoItems.add(metadataEntry.toStoredPlaylistInfoItem()); + } + + playlistAdapter.clearStreamItemList(); + playlistAdapter.addInfoItemList(playlistInfoItems); + playlistRecyclerView.setVisibility(View.VISIBLE); + + getDialog().setCanceledOnTouchOutside(true); + }); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(INFO_KEY, streamInfo); + } + + /*////////////////////////////////////////////////////////////////////////// + // Helper + //////////////////////////////////////////////////////////////////////////*/ + + public void openCreatePlaylistDialog() { + if (streamInfo == null || getFragmentManager() == null) return; + + getDialog().dismiss(); + PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java new file mode 100644 index 000000000..15e787e2a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java @@ -0,0 +1,91 @@ +package org.schabi.newpipe.fragments.playlist; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; + +import java.util.Collections; +import java.util.List; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +public class PlaylistCreationDialog extends DialogFragment { + private static final String TAG = PlaylistCreationDialog.class.getCanonicalName(); + private static final boolean DEBUG = MainActivity.DEBUG; + + private static final String INFO_KEY = "info_key"; + + private StreamInfo streamInfo; + + public static PlaylistCreationDialog newInstance(final StreamInfo info) { + PlaylistCreationDialog dialog = new PlaylistCreationDialog(); + dialog.setInfo(info); + return dialog; + } + + private void setInfo(final StreamInfo info) { + this.streamInfo = info; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (streamInfo != null) { + outState.putSerializable(INFO_KEY, streamInfo); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + if (savedInstanceState != null && streamInfo == null) { + final Object infoCandidate = savedInstanceState.getSerializable(INFO_KEY); + if (infoCandidate != null && infoCandidate instanceof StreamInfo) { + streamInfo = (StreamInfo) infoCandidate; + } + } + + if (streamInfo == null) return super.onCreateDialog(savedInstanceState); + + View dialogView = View.inflate(getContext(), + R.layout.dialog_create_playlist, null); + EditText nameInput = dialogView.findViewById(R.id.playlist_name); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.create_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> { + final String name = nameInput.getText().toString(); + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + final List streams = + Collections.singletonList(new StreamEntity(streamInfo)); + + playlistManager.createPlaylist(name, streams) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> Toast.makeText(getActivity(), + "Playlist " + name + " successfully created", + Toast.LENGTH_SHORT).show()); + }); + + return dialogBuilder.create(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java index bd5bd36a2..31f6284eb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java @@ -44,4 +44,31 @@ public class StreamRecordManager { public int removeHistory(final long streamId) { return historyTable.deleteHistory(streamId); } + + public void removeRecord() { + historyTable.getStatistics().firstElement().subscribe( + new MaybeObserver>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(List streamStatisticsEntries) { + hashCode(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + } + ); + } } 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 ab3d73149..c81235623 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 @@ -16,6 +16,7 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; @@ -75,7 +76,7 @@ public class InfoItemBuilder { case CHANNEL: return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent); case PLAYLIST: - return new PlaylistInfoItemHolder(this, parent); + return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent); default: Log.e(TAG, "Trollolo"); throw new RuntimeException("InfoType not expected = " + infoType.name()); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java index cdb2191e5..88aa76887 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java @@ -19,7 +19,7 @@ public class InfoItemDialog { @NonNull final StreamInfoItem info, @NonNull final String[] commands, @NonNull final DialogInterface.OnClickListener actions) { - this(activity, commands, actions, info.getName(), info.uploader_name); + this(activity, commands, actions, info.getName(), info.getUploaderName()); } public InfoItemDialog(@NonNull final Activity activity, @@ -28,8 +28,7 @@ public class InfoItemDialog { @NonNull final String title, @Nullable final String additionalDetail) { - final LayoutInflater inflater = activity.getLayoutInflater(); - final View bannerView = inflater.inflate(R.layout.dialog_title, null); + final View bannerView = View.inflate(activity, R.layout.dialog_title, null); bannerView.setSelected(true); TextView titleView = bannerView.findViewById(R.id.itemTitleView); 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 806b348d7..5494eae23 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 @@ -15,6 +15,7 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; @@ -52,6 +53,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + if (itemBuilder.getOnPlaylistSelectedListener() != null) { + itemBuilder.getOnPlaylistSelectedListener().selected(item); + } + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java index 63f61cc43..3ac5fabb7 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java @@ -1,30 +1,20 @@ package org.schabi.newpipe.info_list.stored; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; import static org.schabi.newpipe.util.Constants.NO_URL; -public class LocalPlaylistInfoItem extends InfoItem { +public class LocalPlaylistInfoItem extends PlaylistInfoItem { private final long playlistId; - private long streamCount; public LocalPlaylistInfoItem(final long playlistId, final String name) { - super(InfoType.PLAYLIST, NO_SERVICE_ID, NO_URL, name); + super(NO_SERVICE_ID, NO_URL, name); this.playlistId = playlistId; - this.streamCount = streamCount; } public long getPlaylistId() { return playlistId; } - - public long getStreamCount() { - return streamCount; - } - - public void setStreamCount(long streamCount) { - this.streamCount = streamCount; - } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java index ef82826ba..76984d363 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java @@ -1,18 +1,19 @@ package org.schabi.newpipe.info_list.stored; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; -public class StreamStatisticsInfoItem extends InfoItem { +public class StreamStatisticsInfoItem extends StreamInfoItem { private final long streamId; private Date latestAccessDate; private long watchCount; public StreamStatisticsInfoItem(final long streamId, final int serviceId, - final String url, final String name) { - super(InfoType.STREAM, serviceId, url, name); + final String url, final String name, final StreamType type) { + super(serviceId, url, name, type); this.streamId = streamId; } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index ad2200bfc..ca863fc8a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -81,7 +81,6 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; diff --git a/app/src/main/res/layout/dialog_create_playlist.xml b/app/src/main/res/layout/dialog_create_playlist.xml new file mode 100644 index 000000000..b42d3101f --- /dev/null +++ b/app/src/main/res/layout/dialog_create_playlist.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml new file mode 100644 index 000000000..5abe91a8e --- /dev/null +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_playlist_mini_item.xml b/app/src/main/res/layout/list_playlist_mini_item.xml new file mode 100644 index 000000000..3e854bb8e --- /dev/null +++ b/app/src/main/res/layout/list_playlist_mini_item.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d05d088d..c94453570 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,6 +224,7 @@ Start Pause Play + Create Delete Checksum @@ -353,6 +354,7 @@ YouTube SoundCloud + @string/preferred_player_settings_title Open with preferred player @@ -365,4 +367,8 @@ Getting info… "The requested content is loading" + + + Create New Playlist + Name From ba9d0d77075b32a8a8e7f7600e0bedf1b777c3f3 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 16 Jan 2018 21:12:03 -0800 Subject: [PATCH 023/276] -Added basic UI for local playlists. -Added UI for watch history and most played fragments. -Added stream state table for storing playback timestamp and future usage. -Enabled playlist deletion. --- .../schabi/newpipe/database/AppDatabase.java | 8 +- .../database/playlist/dao/PlaylistDAO.java | 3 + .../stream/StreamStatisticsEntry.java | 13 + .../database/stream/dao/StreamStateDAO.java | 33 ++ .../stream/model/StreamStateEntity.java | 51 +++ .../newpipe/fragments/MainFragment.java | 22 +- .../fragments/local/BookmarkFragment.java | 318 ++++++++++++++++ .../local/HistoryPlaylistFragment.java | 323 ++++++++++++++++ .../local/LocalPlaylistFragment.java | 356 ++++++++++++++++++ .../LocalPlaylistManager.java | 18 +- .../fragments/local/MostPlayedFragment.java | 35 ++ .../PlaylistAppendDialog.java | 7 +- .../PlaylistCreationDialog.java | 2 +- .../StreamRecordManager.java | 38 +- .../fragments/local/WatchHistoryFragment.java | 36 ++ .../holder/PlaylistMiniInfoItemHolder.java | 8 + .../org/schabi/newpipe/player/BasePlayer.java | 3 +- .../newpipe/playlist/SinglePlayQueue.java | 18 +- .../schabi/newpipe/util/NavigationHelper.java | 27 ++ app/src/main/res/layout/bookmark_header.xml | 81 ++++ .../main/res/layout/fragment_bookmarks.xml | 44 +++ app/src/main/res/layout/fragment_feed.xml | 2 +- .../main/res/layout/fragment_subscription.xml | 2 +- ...eed_empty_view.xml => list_empty_view.xml} | 0 .../res/layout/list_playlist_mini_item.xml | 2 +- .../main/res/layout/local_playlist_header.xml | 48 +++ app/src/main/res/values/settings_keys.xml | 2 + app/src/main/res/values/strings.xml | 4 + 28 files changed, 1446 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java rename app/src/main/java/org/schabi/newpipe/fragments/{playlist => local}/LocalPlaylistManager.java (84%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java rename app/src/main/java/org/schabi/newpipe/fragments/{playlist => local}/PlaylistAppendDialog.java (98%) rename app/src/main/java/org/schabi/newpipe/fragments/{playlist => local}/PlaylistCreationDialog.java (98%) rename app/src/main/java/org/schabi/newpipe/fragments/{playlist => local}/StreamRecordManager.java (56%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java create mode 100644 app/src/main/res/layout/bookmark_header.xml create mode 100644 app/src/main/res/layout/fragment_bookmarks.xml rename app/src/main/res/layout/{subscription_feed_empty_view.xml => list_empty_view.xml} (100%) create mode 100644 app/src/main/res/layout/local_playlist_header.xml 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 e09687ce4..dedbfbf68 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -14,8 +14,10 @@ import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; @@ -23,8 +25,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; @Database( entities = { SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class, - StreamEntity.class, StreamHistoryEntity.class, PlaylistEntity.class, - PlaylistStreamEntity.class + StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, + PlaylistEntity.class, PlaylistStreamEntity.class }, version = 1, exportSchema = false @@ -43,6 +45,8 @@ public abstract class AppDatabase extends RoomDatabase { public abstract StreamHistoryDAO streamHistoryDAO(); + public abstract StreamStateDAO streamStateDAO(); + public abstract PlaylistDAO playlistDAO(); public abstract PlaylistStreamDAO playlistStreamDAO(); 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 b337769bc..88d5645af 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 @@ -32,4 +32,7 @@ public abstract class PlaylistDAO implements BasicDAO { @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") public abstract Flowable> getPlaylist(final long playlistId); + + @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") + public abstract int deletePlaylist(final long playlistId); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java index 5893394c5..722cff5cd 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -4,7 +4,9 @@ import android.arch.persistence.room.ColumnInfo; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import java.util.Date; @@ -51,4 +53,15 @@ public class StreamStatisticsEntry { this.latestAccessDate = latestAccessDate; this.watchCount = watchCount; } + + public StreamStatisticsInfoItem toStreamStatisticsInfoItem() { + StreamStatisticsInfoItem item = + new StreamStatisticsInfoItem(uid, serviceId, url, title, streamType); + item.setDuration(duration); + item.setUploaderName(uploader); + item.setThumbnailUrl(thumbnailUrl); + item.setLatestAccessDate(latestAccessDate); + item.setWatchCount(watchCount); + return item; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java new file mode 100644 index 000000000..f89f2f7ef --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java @@ -0,0 +1,33 @@ +package org.schabi.newpipe.database.stream.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; + +@Dao +public abstract class StreamStateDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + STREAM_STATE_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + STREAM_STATE_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") + public abstract int deleteState(final long streamId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java new file mode 100644 index 000000000..15940a964 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -0,0 +1,51 @@ +package org.schabi.newpipe.database.stream.model; + + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.ForeignKey; + +import static android.arch.persistence.room.ForeignKey.CASCADE; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; + +@Entity(tableName = STREAM_STATE_TABLE, + primaryKeys = {JOIN_STREAM_ID}, + foreignKeys = { + @ForeignKey(entity = StreamEntity.class, + parentColumns = StreamEntity.STREAM_ID, + childColumns = JOIN_STREAM_ID, + onDelete = CASCADE, onUpdate = CASCADE) + }) +public class StreamStateEntity { + final public static String STREAM_STATE_TABLE = "stream_state"; + final public static String JOIN_STREAM_ID = "stream_id"; + final public static String STREAM_PROGRESS_TIME = "progress_time"; + + @ColumnInfo(name = JOIN_STREAM_ID) + private long streamUid; + + @ColumnInfo(name = STREAM_PROGRESS_TIME) + private long progressTime; + + public StreamStateEntity(long streamUid, long progressTime) { + this.streamUid = streamUid; + this.progressTime = progressTime; + } + + public long getStreamUid() { + return streamUid; + } + + public void setStreamUid(long streamUid) { + this.streamUid = streamUid; + } + + public long getProgressTime() { + return progressTime; + } + + public void setProgressTime(long progressTime) { + this.progressTime = progressTime; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 3a8c7569c..e76b97086 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskList; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; +import org.schabi.newpipe.fragments.local.BookmarkFragment; import org.schabi.newpipe.fragments.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -87,9 +88,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte if (isSubscriptionsPageOnlySelected()) { tabLayout.getTabAt(0).setIcon(channelIcon); + tabLayout.getTabAt(1).setText(R.string.tab_bookmarks); } else { tabLayout.getTabAt(0).setIcon(whatsHotIcon); tabLayout.getTabAt(1).setIcon(channelIcon); + tabLayout.getTabAt(2).setText(R.string.tab_bookmarks); } } @@ -147,7 +150,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } private class PagerAdapter extends FragmentPagerAdapter { - PagerAdapter(FragmentManager fm) { super(fm); } @@ -158,7 +160,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte case 0: return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment(); case 1: - return new SubscriptionFragment(); + if(PreferenceManager.getDefaultSharedPreferences(getActivity()) + .getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key)) + .equals(getString(R.string.subscription_page_key))) { + return new BookmarkFragment(); + } else { + return new SubscriptionFragment(); + } + case 2: + return new BookmarkFragment(); default: return new BlankFragment(); } @@ -172,7 +182,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte @Override public int getCount() { - return isSubscriptionsPageOnlySelected() ? 1 : 2; + return isSubscriptionsPageOnlySelected() ? 2 : 3; } } @@ -187,6 +197,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } private Fragment getMainPageFragment() { + if (getActivity() == null) return new BlankFragment(); + try { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); @@ -216,6 +228,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); fragment.useAsFrontPage(true); return fragment; + } else if (setMainPage.equals(getString(R.string.bookmark_page_key))) { + final BookmarkFragment fragment = new BookmarkFragment(); + fragment.useAsFrontPage(true); + return fragment; } else { return new BlankFragment(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java new file mode 100644 index 000000000..ecbd416ee --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java @@ -0,0 +1,318 @@ +package org.schabi.newpipe.fragments.local; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import icepick.State; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class BookmarkFragment extends BaseStateFragment> { + private View watchHistoryButton; + private View mostWatchedButton; + + private InfoListAdapter infoListAdapter; + private RecyclerView itemsList; + + @State + protected Parcelable itemsListState; + + private Subscription databaseSubscription; + private CompositeDisposable disposables = new CompositeDisposable(); + private LocalPlaylistManager localPlaylistManager; + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if(isVisibleToUser && activity != null && activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle(R.string.tab_bookmarks); + } + } + + + @Override + public void onAttach(Context context) { + super.onAttach(context); + infoListAdapter = new InfoListAdapter(activity); + localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context)); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + Bundle savedInstanceState) { + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayShowTitleEnabled(true); + } + + activity.setTitle(R.string.tab_bookmarks); + if(useAsFrontPage) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false); + } + return inflater.inflate(R.layout.fragment_bookmarks, container, false); + } + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + if (disposables != null) disposables.clear(); + if (databaseSubscription != null) databaseSubscription.cancel(); + + super.onDestroyView(); + } + + @Override + public void onDestroy() { + if (disposables != null) disposables.dispose(); + if (databaseSubscription != null) databaseSubscription.cancel(); + + disposables = null; + databaseSubscription = null; + localPlaylistManager = null; + + super.onDestroy(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + infoListAdapter = new InfoListAdapter(getActivity()); + itemsList = rootView.findViewById(R.id.items_list); + itemsList.setLayoutManager(new LinearLayoutManager(activity)); + + final View headerRootLayout = activity.getLayoutInflater() + .inflate(R.layout.bookmark_header, itemsList, false); + watchHistoryButton = headerRootLayout.findViewById(R.id.watchHistory); + mostWatchedButton = headerRootLayout.findViewById(R.id.mostWatched); + + infoListAdapter.setHeader(headerRootLayout); + infoListAdapter.useMiniItemVariants(true); + + itemsList.setAdapter(infoListAdapter); + } + + @Override + protected void initListeners() { + super.initListeners(); + + infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(PlaylistInfoItem selectedItem) { + // Requires the parent fragment to find holder for fragment replacement + if (selectedItem instanceof LocalPlaylistInfoItem && getParentFragment() != null) { + final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); + + NavigationHelper.openLocalPlaylistFragment( + getParentFragment().getFragmentManager(), + playlistId, + selectedItem.getName() + ); + } + } + + @Override + public void held(PlaylistInfoItem selectedItem) { + if (selectedItem instanceof LocalPlaylistInfoItem) { + showPlaylistDialog((LocalPlaylistInfoItem) selectedItem); + } + } + }); + + watchHistoryButton.setOnClickListener(view -> { + if (getParentFragment() != null) { + NavigationHelper.openWatchHistoryFragment(getParentFragment().getFragmentManager()); + } + }); + + mostWatchedButton.setOnClickListener(view -> { + if (getParentFragment() != null) { + NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager()); + } + }); + } + + private void showPlaylistDialog(final LocalPlaylistInfoItem item) { + final Context context = getContext(); + if (context == null || context.getResources() == null || getActivity() == null) return; + + final String[] commands = new String[]{ + context.getResources().getString(R.string.delete_playlist) + }; + + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + switch (i) { + case 0: + final Toast deleteSuccessful = + Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT); + disposables.add(localPlaylistManager.deletePlaylist(item.getPlaylistId()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> deleteSuccessful.show())); + break; + default: + break; + } + }; + + final String videoCount = getResources().getQuantityString(R.plurals.videos, + (int) item.getStreamCount(), (int) item.getStreamCount()); + new InfoItemDialog(getActivity(), commands, actions, item.getName(), videoCount).show(); + } + + private void resetFragment() { + if (disposables != null) disposables.clear(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + } + + /////////////////////////////////////////////////////////////////////////// + // Subscriptions Loader + /////////////////////////////////////////////////////////////////////////// + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + + localPlaylistManager.getPlaylists() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getSubscriptionSubscriber()); + } + + private Subscriber> getSubscriptionSubscriber() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List subscriptions) { + handleResult(subscriptions); + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + BookmarkFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + + infoListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + } else { + infoListAdapter.addInfoItemList(infoItemsOf(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + hideLoading(); + } + } + + + private List infoItemsOf(List playlists) { + List playlistInfoItems = new ArrayList<>(playlists.size()); + for (final PlaylistMetadataEntry playlist : playlists) { + playlistInfoItems.add(playlist.toStoredPlaylistInfoItem()); + } + Collections.sort(playlistInfoItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); + return playlistInfoItems; + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + animateView(itemsList, false, 100); + } + + @Override + public void hideLoading() { + super.hideLoading(); + animateView(itemsList, true, 200); + } + + @Override + public void showEmptyState() { + super.showEmptyState(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "Bookmark", R.string.general_error); + return true; + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java new file mode 100644 index 000000000..3941df6c0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java @@ -0,0 +1,323 @@ +package org.schabi.newpipe.fragments.local; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.ArrayList; +import java.util.List; + +import icepick.State; +import io.reactivex.android.schedulers.AndroidSchedulers; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public abstract class HistoryPlaylistFragment + extends BaseListFragment, Void> { + + private View headerRootLayout; + private View playlistControl; + private View headerPlayAllButton; + private View headerPopupButton; + private View headerBackgroundButton; + + @State + protected Parcelable itemsListState; + + /* Used for independent events */ + private Subscription databaseSubscription; + private StreamRecordManager recordManager; + + /////////////////////////////////////////////////////////////////////////// + // Abstracts + /////////////////////////////////////////////////////////////////////////// + + protected abstract String getName(); + + protected abstract List processResult(final List results); + + protected abstract String getAdditionalDetail(final StreamStatisticsInfoItem infoItem); + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onAttach(Context context) { + super.onAttach(context); + recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_playlist, container, false); + } + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + if (databaseSubscription != null) databaseSubscription.cancel(); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = null; + recordManager = null; + + super.onDestroy(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + infoListAdapter.useMiniItemVariants(true); + + setFragmentTitle(getName()); + } + + @Override + protected View getListHeader() { + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control, + itemsList, false); + playlistControl = headerRootLayout.findViewById(R.id.playlist_control); + headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); + headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); + headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); + + return headerRootLayout; + } + + @Override + protected void initListeners() { + super.initListeners(); + + infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(StreamInfoItem selectedItem) { + if (getParentFragment() == null) return; + // Requires the parent fragment to find holder for fragment replacement + NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(), + selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + } + + @Override + public void held(StreamInfoItem selectedItem) { + showStreamDialog(selectedItem); + } + }); + + } + + @Override + protected void showStreamDialog(final StreamInfoItem item) { + final Context context = getContext(); + final Activity activity = getActivity(); + if (context == null || context.getResources() == null + || getActivity() == null || !(item instanceof StreamStatisticsInfoItem)) return; + + final String[] commands = new String[]{ + context.getResources().getString(R.string.enqueue_on_background), + context.getResources().getString(R.string.enqueue_on_popup), + context.getResources().getString(R.string.start_here_on_main), + context.getResources().getString(R.string.start_here_on_background), + context.getResources().getString(R.string.start_here_on_popup), + }; + + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + break; + case 3: + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + break; + case 4: + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + break; + default: + break; + } + }; + + final String detail = getAdditionalDetail((StreamStatisticsInfoItem) item); + new InfoItemDialog(getActivity(), commands, actions, item.getName(), detail).show(); + } + + private void resetFragment() { + if (databaseSubscription != null) databaseSubscription.cancel(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + } + + /////////////////////////////////////////////////////////////////////////// + // Loader + /////////////////////////////////////////////////////////////////////////// + + @Override + public void showLoading() { + super.showLoading(); + animateView(headerRootLayout, false, 200); + animateView(itemsList, false, 100); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + + recordManager.getStatistics() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getHistoryObserver()); + } + + private Subscriber> getHistoryObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List streams) { + handleResult(streams); + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + HistoryPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + infoListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + return; + } + + animateView(headerRootLayout, true, 100); + animateView(itemsList, true, 300); + + infoListAdapter.addInfoItemList(processResult(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + + playlistControl.setVisibility(View.VISIBLE); + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + hideLoading(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void loadMoreItems() { + // Do nothing + } + + @Override + protected boolean hasMoreItems() { + return false; + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "History", R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected void setFragmentTitle(final String title) { + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle(title); + } + } + + private PlayQueue getPlayQueue() { + return getPlayQueue(0); + } + + private PlayQueue getPlayQueue(final int index) { + final List infoItems = infoListAdapter.getItemsList(); + List streamInfoItems = new ArrayList<>(infoItems.size()); + for (final InfoItem item : infoItems) { + if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + } + return new SinglePlayQueue(streamInfoItems, index); + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java new file mode 100644 index 000000000..6709b1bad --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -0,0 +1,356 @@ +package org.schabi.newpipe.fragments.local; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.ArrayList; +import java.util.List; + +import icepick.State; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class LocalPlaylistFragment extends BaseListFragment, Void> { + + private View headerRootLayout; + private TextView headerTitleView; + private TextView headerStreamCount; + private View playlistControl; + + private View headerPlayAllButton; + private View headerPopupButton; + private View headerBackgroundButton; + + @State + protected long playlistId; + @State + protected String name; + @State + protected Parcelable itemsListState; + + /* Used for independent events */ + private CompositeDisposable disposables = new CompositeDisposable(); + private Subscription databaseSubscription; + private LocalPlaylistManager playlistManager; + + public static LocalPlaylistFragment getInstance(long playlistId, String name) { + LocalPlaylistFragment instance = new LocalPlaylistFragment(); + instance.setInitialData(playlistId, name); + return instance; + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onAttach(Context context) { + super.onAttach(context); + playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context)); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_playlist, container, false); + } + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + if (disposables != null) disposables.clear(); + + super.onDestroyView(); + } + + @Override + public void onDestroy() { + if (disposables != null) disposables.dispose(); + if (databaseSubscription != null) databaseSubscription.cancel(); + + disposables = null; + databaseSubscription = null; + playlistManager = null; + + super.onDestroy(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + infoListAdapter.useMiniItemVariants(true); + + setFragmentTitle(name); + } + + @Override + protected View getListHeader() { + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.local_playlist_header, + itemsList, false); + + headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); + headerTitleView.setSelected(true); + + headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count); + playlistControl = headerRootLayout.findViewById(R.id.playlist_control); + headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); + headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); + headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); + + return headerRootLayout; + } + + @Override + protected void initListeners() { + super.initListeners(); + + infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(StreamInfoItem selectedItem) { + if (getParentFragment() == null) return; + // Requires the parent fragment to find holder for fragment replacement + NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(), + selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + } + + @Override + public void held(StreamInfoItem selectedItem) { + showStreamDialog(selectedItem); + } + }); + + } + + @Override + protected void showStreamDialog(final StreamInfoItem item) { + final Context context = getContext(); + final Activity activity = getActivity(); + if (context == null || context.getResources() == null || getActivity() == null) return; + + final String[] commands = new String[]{ + context.getResources().getString(R.string.enqueue_on_background), + context.getResources().getString(R.string.enqueue_on_popup), + context.getResources().getString(R.string.start_here_on_main), + context.getResources().getString(R.string.start_here_on_background), + context.getResources().getString(R.string.start_here_on_popup), + }; + + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + break; + case 3: + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + break; + case 4: + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + break; + default: + break; + } + }; + + new InfoItemDialog(getActivity(), item, commands, actions).show(); + } + + private void resetFragment() { + if (disposables != null) disposables.clear(); + if (databaseSubscription != null) databaseSubscription.cancel(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + } + + /////////////////////////////////////////////////////////////////////////// + // Loader + /////////////////////////////////////////////////////////////////////////// + + @Override + public void showLoading() { + super.showLoading(); + animateView(headerRootLayout, false, 200); + animateView(itemsList, false, 100); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + + playlistManager.getPlaylist(playlistId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistObserver()); + } + + private Subscriber> getPlaylistObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List streams) { + handleResult(streams); + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + LocalPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + infoListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + return; + } + + animateView(headerRootLayout, true, 100); + animateView(itemsList, true, 300); + + infoListAdapter.addInfoItemList(getStreamItems(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + + playlistControl.setVisibility(View.VISIBLE); + headerStreamCount.setText( + getResources().getQuantityString(R.plurals.videos, result.size(), result.size())); + + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + hideLoading(); + } + + + private List getStreamItems(final List streams) { + List items = new ArrayList<>(streams.size()); + for (final StreamEntity stream : streams) { + items.add(stream.toStreamInfoItem()); + } + return items; + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void loadMoreItems() { + // Do nothing + } + + @Override + protected boolean hasMoreItems() { + return false; + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "Subscriptions", R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected void setInitialData(long playlistId, String name) { + this.playlistId = playlistId; + this.name = !TextUtils.isEmpty(name) ? name : ""; + } + + protected void setFragmentTitle(final String title) { + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle(title); + } + if (headerTitleView != null) { + headerTitleView.setText(title); + } + } + + private PlayQueue getPlayQueue() { + return getPlayQueue(0); + } + + private PlayQueue getPlayQueue(final int index) { + final List infoItems = infoListAdapter.getItemsList(); + List streamInfoItems = new ArrayList<>(infoItems.size()); + for (final InfoItem item : infoItems) { + if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + } + return new SinglePlayQueue(streamInfoItems, index); + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java similarity index 84% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 911b3c7fd..bf7bc14c8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; @@ -13,8 +13,9 @@ import java.util.ArrayList; import java.util.List; import io.reactivex.Completable; +import io.reactivex.Flowable; import io.reactivex.Maybe; -import io.reactivex.Scheduler; +import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; public class LocalPlaylistManager { @@ -74,9 +75,16 @@ public class LocalPlaylistManager { })); } - public Maybe> getPlaylists() { - return playlistStreamTable.getPlaylistMetadata() - .firstElement() + public Flowable> getPlaylists() { + return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); + } + + public Flowable> getPlaylist(final long playlistId) { + return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); + } + + public Single deletePlaylist(final long playlistId) { + return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId)) .subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java new file mode 100644 index 000000000..466b1d569 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java @@ -0,0 +1,35 @@ +package org.schabi.newpipe.fragments.local; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MostPlayedFragment extends HistoryPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_most_played); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + ((Long) right.watchCount).compareTo(left.watchCount)); + + List items = new ArrayList<>(results.size()); + for (final StreamStatisticsEntry stream : results) { + items.add(stream.toStreamStatisticsInfoItem()); + } + return items; + } + + @Override + protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) { + final int watchCount = (int) infoItem.getWatchCount(); + return getResources().getQuantityString(R.plurals.views, watchCount, watchCount); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index bee3b347e..6fad839f1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import android.content.Context; import android.os.Bundle; @@ -113,6 +113,7 @@ public class PlaylistAppendDialog extends DialogFragment { .subscribe(metadataEntries -> { if (metadataEntries.isEmpty()) { openCreatePlaylistDialog(); + return; } List playlistInfoItems = new ArrayList<>(metadataEntries.size()); @@ -123,8 +124,6 @@ public class PlaylistAppendDialog extends DialogFragment { playlistAdapter.clearStreamItemList(); playlistAdapter.addInfoItemList(playlistInfoItems); playlistRecyclerView.setVisibility(View.VISIBLE); - - getDialog().setCanceledOnTouchOutside(true); }); } @@ -141,7 +140,7 @@ public class PlaylistAppendDialog extends DialogFragment { public void openCreatePlaylistDialog() { if (streamInfo == null || getFragmentManager() == null) return; - getDialog().dismiss(); PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); + getDialog().dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 15e787e2a..843b84de6 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import android.app.AlertDialog; import android.app.Dialog; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java similarity index 56% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java index 31f6284eb..458ec4da2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; @@ -7,14 +7,12 @@ import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import java.util.Date; import java.util.List; -import io.reactivex.MaybeObserver; +import io.reactivex.Flowable; import io.reactivex.Single; -import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class StreamRecordManager { @@ -29,11 +27,6 @@ public class StreamRecordManager { historyTable = db.streamHistoryDAO(); } - public int onChanged(final StreamInfoItem infoItem) { - // Only existing streams are updated - return streamTable.update(new StreamEntity(infoItem)); - } - public Single onViewed(final StreamInfo info) { return Single.fromCallable(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); @@ -45,30 +38,7 @@ public class StreamRecordManager { return historyTable.deleteHistory(streamId); } - public void removeRecord() { - historyTable.getStatistics().firstElement().subscribe( - new MaybeObserver>() { - - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onSuccess(List streamStatisticsEntries) { - hashCode(); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - } - ); + public Flowable> getStatistics() { + return historyTable.getStatistics(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java new file mode 100644 index 000000000..794872954 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java @@ -0,0 +1,36 @@ +package org.schabi.newpipe.fragments.local; + +import android.text.format.DateFormat; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class WatchHistoryFragment extends HistoryPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_watch_history); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + right.latestAccessDate.compareTo(left.latestAccessDate)); + + List items = new ArrayList<>(results.size()); + for (final StreamStatisticsEntry stream : results) { + items.add(stream.toStreamStatisticsInfoItem()); + } + return items; + } + + @Override + protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) { + return DateFormat.getLongDateFormat(getContext()).format(infoItem.getLatestAccessDate()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index 2e8919575..50b551c61 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -47,6 +47,14 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { itemBuilder.getOnPlaylistSelectedListener().selected(item); } }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnPlaylistSelectedListener() != null) { + itemBuilder.getOnPlaylistSelectedListener().held(item); + } + return true; + }); } /** diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index ca863fc8a..3cf169ecd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -64,7 +64,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.fragments.playlist.StreamRecordManager; +import org.schabi.newpipe.fragments.local.StreamRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; @@ -676,7 +676,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); - recordManager.removeRecord(); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java index ae74528eb..9c4d2fb39 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java @@ -3,19 +3,29 @@ package org.schabi.newpipe.playlist; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; public final class SinglePlayQueue extends PlayQueue { public SinglePlayQueue(final StreamInfoItem item) { - this(new PlayQueueItem(item)); + super(0, Collections.singletonList(new PlayQueueItem(item))); } public SinglePlayQueue(final StreamInfo info) { - this(new PlayQueueItem(info)); + super(0, Collections.singletonList(new PlayQueueItem(info))); } - private SinglePlayQueue(final PlayQueueItem playQueueItem) { - super(0, Collections.singletonList(playQueueItem)); + public SinglePlayQueue(final List items, final int index) { + super(index, playQueueItemsOf(items)); + } + + private static List playQueueItemsOf(List items) { + List playQueueItems = new ArrayList<>(items.size()); + for (final StreamInfoItem item : items) { + playQueueItems.add(new PlayQueueItem(item)); + } + return playQueueItems; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 8894af9df..7ffbf07ed 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -34,6 +34,9 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; +import org.schabi.newpipe.fragments.local.MostPlayedFragment; +import org.schabi.newpipe.fragments.local.WatchHistoryFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -323,6 +326,30 @@ public class NavigationHelper { .commit(); } + public static void openLocalPlaylistFragment(FragmentManager fragmentManager, long playlistId, String name) { + if (name == null) name = ""; + fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, name)) + .addToBackStack(null) + .commit(); + } + + public static void openWatchHistoryFragment(FragmentManager fragmentManager) { + fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .replace(R.id.fragment_holder, new WatchHistoryFragment()) + .addToBackStack(null) + .commit(); + } + + public static void openMostPlayedFragment(FragmentManager fragmentManager) { + fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .replace(R.id.fragment_holder, new MostPlayedFragment()) + .addToBackStack(null) + .commit(); + } /*////////////////////////////////////////////////////////////////////////// // Through Intents //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/res/layout/bookmark_header.xml b/app/src/main/res/layout/bookmark_header.xml new file mode 100644 index 000000000..b087a5157 --- /dev/null +++ b/app/src/main/res/layout/bookmark_header.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookmarks.xml b/app/src/main/res/layout/fragment_bookmarks.xml new file mode 100644 index 000000000..56e13225f --- /dev/null +++ b/app/src/main/res/layout/fragment_bookmarks.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index 0868d8233..d45060440 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -26,7 +26,7 @@ diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml new file mode 100644 index 000000000..0ceee5d9a --- /dev/null +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..14216dd88 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -119,12 +119,14 @@ subscription_page_key kiosk_page channel_page + bookmark_page @string/blank_page_key @string/kiosk_page_key @string/feed_page_key @string/subscription_page_key @string/channel_page_key + @string/bookmark_page_key main_page_selected_service main_page_selected_channel_name diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c94453570..df5b15c19 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ Main Subscriptions + Bookmarks What\'s New @@ -304,6 +305,8 @@ History cleared Item deleted Do you want to delete this item from search history? + Watch History + Most Played Content of main page @@ -370,5 +373,6 @@ Create New Playlist + Delete Playlist Name From 3c314ced0ae0896e0dbc3c1a0246f311da867045 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 17 Jan 2018 13:24:59 -0800 Subject: [PATCH 024/276] -Bump database version to 2. -Added migration script for upgrading database from version 1 to 2. -Fixed database name of stream type in stream entity. --- .../org/schabi/newpipe/NewPipeDatabase.java | 8 ++-- .../schabi/newpipe/database/AppDatabase.java | 10 ++-- .../schabi/newpipe/database/Migrations.java | 47 +++++++++++++++++++ .../database/stream/dao/StreamHistoryDAO.java | 2 +- .../database/stream/model/StreamEntity.java | 4 +- .../fragments/local/StreamRecordManager.java | 2 +- 6 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/Migrations.java diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 4da1c63f2..15d9cf389 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 android.support.annotation.NonNull; import org.schabi.newpipe.database.AppDatabase; import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; +import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12; public final class NewPipeDatabase { @@ -17,9 +18,10 @@ public final class NewPipeDatabase { } public static void init(Context context) { - databaseInstance = Room.databaseBuilder(context.getApplicationContext(), - AppDatabase.class, DATABASE_NAME - ).build(); + databaseInstance = Room + .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) + .addMigrations(MIGRATION_11_12) + .build(); } @NonNull 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 dedbfbf68..d5a9164dc 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -10,17 +10,19 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; -import org.schabi.newpipe.database.stream.dao.StreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; + @TypeConverters({Converters.class}) @Database( entities = { @@ -28,7 +30,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, PlaylistEntity.class, PlaylistStreamEntity.class }, - version = 1, + version = DB_VER_12_0, exportSchema = false ) public abstract class AppDatabase extends RoomDatabase { diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java new file mode 100644 index 000000000..f1aa52392 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.database; + +import android.arch.persistence.db.SupportSQLiteDatabase; +import android.arch.persistence.room.migration.Migration; +import android.support.annotation.NonNull; + +public class Migrations { + + public static final int DB_VER_11_0 = 1; + public static final int DB_VER_12_0 = 2; + + public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); + database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`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 )"); + database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)"); + database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `stream_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)"); + database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)"); + database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)"); + + // Populate streams table with existing entries in watch history + // Latest data first, thus ignoring older entries with the same indices + database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " + + "stream_type, duration, uploader, thumbnail_url) " + + + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + + "uploader, thumbnail_url " + + + "FROM watch_history " + + "ORDER BY creation_date DESC"); + + // Once the streams have PKs, join them with the normalized history table + // and populate it with the remaining data from watch history + database.execSQL("INSERT INTO stream_history (stream_id, access_date)" + + "SELECT uid, creation_date " + + "FROM watch_history INNER JOIN streams " + + "ON watch_history.service_id == streams.service_id " + + "AND watch_history.url == streams.url " + + "ORDER BY creation_date DESC"); + } + }; +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java index 522c03522..527d151ea 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -37,7 +37,7 @@ public abstract class StreamHistoryDAO implements BasicDAO } @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - public abstract int deleteHistory(final long streamId); + public abstract int deleteStreamHistory(final long streamId); @Query("SELECT * FROM " + STREAM_TABLE + diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index 27d0aa7e1..c7ef889b9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -24,9 +24,9 @@ public class StreamEntity { final public static String STREAM_SERVICE_ID = "service_id"; final public static String STREAM_URL = "url"; final public static String STREAM_TITLE = "title"; - final public static String STREAM_TYPE = "streamType"; - final public static String STREAM_UPLOADER = "uploader"; + final public static String STREAM_TYPE = "stream_type"; final public static String STREAM_DURATION = "duration"; + final public static String STREAM_UPLOADER = "uploader"; final public static String STREAM_THUMBNAIL_URL = "thumbnail_url"; @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java index 458ec4da2..993ed58da 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java @@ -35,7 +35,7 @@ public class StreamRecordManager { } public int removeHistory(final long streamId) { - return historyTable.deleteHistory(streamId); + return historyTable.deleteStreamHistory(streamId); } public Flowable> getStatistics() { From 4ae81a2de428aca9a518341b5ee66066e26296a0 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 17 Jan 2018 13:53:32 -0800 Subject: [PATCH 025/276] -Deprecating database get instance without context. -Added comments to migrations. --- app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java | 2 ++ .../main/java/org/schabi/newpipe/database/Migrations.java | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 15d9cf389..9cd56fca4 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -21,10 +21,12 @@ public final class NewPipeDatabase { databaseInstance = Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_11_12) + .fallbackToDestructiveMigration() .build(); } @NonNull + @Deprecated public static AppDatabase getInstance() { if (databaseInstance == null) throw new RuntimeException("Database not initialized"); 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 f1aa52392..72b0d2126 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -12,6 +12,14 @@ public class Migrations { public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { + /* + * Unfortunately these queries must be hardcoded due to the possibility of + * schema and names changing at a later date, thus invalidating the older migration + * scripts if names are not hardcoded. + * */ + + // Not much we can do about this, since room doesn't create tables before migration. + // It's either this or blasting the entire database anew. database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); From 9bd26798b669296acbcd3021cee2427e4bcf8bb8 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 17 Jan 2018 14:32:09 -0800 Subject: [PATCH 026/276] -Added icon for adding stream to playlist. -Renamed HistoryPlaylistFragment to StatisticsPlaylistFragment. --- .../fragments/detail/VideoDetailFragment.java | 9 ++++++++ .../local/LocalPlaylistFragment.java | 3 +-- .../fragments/local/MostPlayedFragment.java | 2 +- ...t.java => StatisticsPlaylistFragment.java} | 8 +++---- .../fragments/local/WatchHistoryFragment.java | 2 +- .../ic_playlist_add_black_24dp.png | Bin 0 -> 106 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 107 bytes .../ic_playlist_add_black_24dp.png | Bin 0 -> 100 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 101 bytes .../ic_playlist_add_black_24dp.png | Bin 0 -> 113 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 109 bytes .../ic_playlist_add_black_24dp.png | Bin 0 -> 129 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 113 bytes .../ic_playlist_add_black_24dp.png | Bin 0 -> 128 bytes .../ic_playlist_add_white_24dp.png | Bin 0 -> 111 bytes .../main/res/layout/fragment_video_detail.xml | 21 +++++++++++++++++- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/values/styles.xml | 2 ++ 19 files changed, 40 insertions(+), 10 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/local/{HistoryPlaylistFragment.java => StatisticsPlaylistFragment.java} (96%) create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_add_white_24dp.png diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index c7b61eceb..7f8afdbe8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -58,6 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; @@ -145,6 +146,7 @@ public class VideoDetailFragment extends BaseStateFragment implement private TextView detailControlsBackground; private TextView detailControlsPopup; + private TextView detailControlsAddToPlaylist; private TextView appendControlsDetail; private LinearLayout videoDescriptionRootLayout; @@ -327,6 +329,11 @@ public class VideoDetailFragment extends BaseStateFragment implement case R.id.detail_controls_popup: openPopupPlayer(false); break; + case R.id.detail_controls_playlist_append: + if (getFragmentManager() != null && currentInfo != null) { + PlaylistAppendDialog.newInstance(currentInfo).show(getFragmentManager(), TAG); + } + break; case R.id.detail_uploader_root_layout: if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { Log.w(TAG, "Can't open channel because we got no channel URL"); @@ -429,6 +436,7 @@ public class VideoDetailFragment extends BaseStateFragment implement detailControlsBackground = rootView.findViewById(R.id.detail_controls_background); detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); + detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); @@ -479,6 +487,7 @@ public class VideoDetailFragment extends BaseStateFragment implement thumbnailBackgroundButton.setOnClickListener(this); detailControlsBackground.setOnClickListener(this); detailControlsPopup.setOnClickListener(this); + detailControlsAddToPlaylist.setOnClickListener(this); relatedStreamExpandButton.setOnClickListener(this); detailControlsBackground.setLongClickable(true); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 6709b1bad..44ecfb924 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -144,9 +144,8 @@ public class LocalPlaylistFragment extends BaseListFragment, infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(StreamInfoItem selectedItem) { - if (getParentFragment() == null) return; // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(), + NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java index 466b1d569..7862cf2f4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java @@ -9,7 +9,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class MostPlayedFragment extends HistoryPlaylistFragment { +public class MostPlayedFragment extends StatisticsPlaylistFragment { @Override protected String getName() { return getString(R.string.title_most_played); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java similarity index 96% rename from app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java index 3941df6c0..8db1f8780 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java @@ -35,7 +35,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public abstract class HistoryPlaylistFragment +public abstract class StatisticsPlaylistFragment extends BaseListFragment, Void> { private View headerRootLayout; @@ -130,9 +130,7 @@ public abstract class HistoryPlaylistFragment infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(StreamInfoItem selectedItem) { - if (getParentFragment() == null) return; - // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(), + NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); } @@ -231,7 +229,7 @@ public abstract class HistoryPlaylistFragment @Override public void onError(Throwable exception) { - HistoryPlaylistFragment.this.onError(exception); + StatisticsPlaylistFragment.this.onError(exception); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java index 794872954..2a4b8cfb0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java @@ -11,7 +11,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class WatchHistoryFragment extends HistoryPlaylistFragment { +public class WatchHistoryFragment extends StatisticsPlaylistFragment { @Override protected String getName() { return getString(R.string.title_watch_history); diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..731b42590633cb2654a0c553b297f87e209cdf09 GIT binary patch literal 106 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;p{I*uNCjiEL{EZ9Pt*UFhZjsT zOs)xNu`hI-(x8|nu(04DJNtY`XSHi?yi#HbA`A=@6CWrnHJ!i))Xd=N>gTe~DWM4f D-jN$L literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4fb76e1784aa8d42add6d5dcfc91c304c6c201d8 GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>`VBp6OsFEs^HOeH~n!3+##lh0a!q&!_5Ln;`P z75Gp5|KI5IDCjdoD+{AZ(7*YA|FfkCDfxA%@G#8Nlb=@fz2!d06i-(_mvv4FO#njp B9#a4S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d7a7514a84072ca393b32a30aa1427ab8ed37bb5 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1ZBG}+kP60R1*x8<|2Gs*O0lh! xcQDy0#lie4^T0b1ACG-(r~W$B^B(45U@*U(JU7SI#s;X3!PC{xWt~$(69A%_8>#>R literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..73c981285837f550453b25eda148c90f1b1c7a47 GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^5+KY7Bp6QcFoXgrrjj7PUx9{KW6@ll{Pre}Cj9mdKI;Vst09JY%g#Z8m literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..dc4ebe9f39a7bc76959e00f975a36880980654b4 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}t3r`ovkP61PmoIWMC3FG+?2Zo5J^ZOdkg}ZJ7n!(`d L>gTe~DWM4fRg5BO literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..52ccba0b2f500aab5cc7f89184de089425638fe7 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PUsElAj0bSzCLqp zSfb;PGdFyqYu2$dFl?Bu>@h(h;YxhjeQqHY4ONc`3m)8bpKNFb6tz|X5{^wQf7uz> YgL@iQo&P4j3uq>Tr>mdKI;Vst06PUJ-T(jq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3f652366df1d17852a763aa0634361a066ac41da GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJd0L(>jv*C{ z$r4=+KkWr%6#89v?yWzOaFF4p&I--?AC7Dj&pFL-3cuFC$iUQ8>8mdK II;Vst04|sy&Hw-a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..46020a7e04f2cd8f9945abd5eb75e7917c2e22d4 GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeK3?y%aJ*@^(YymzYu0R?HmZtAK52P4Ng8YIR z9G=}s19CJxT^vIy7?Tx*dz$_yI3)A^{rh?TG#Qo!w(Lo&92cfFZzy?D+$PV!;Kh90 TQvZJM5s*Qiu6{1-oD!MT$m`Z~Df*BafCZDwc@-#eM978G? zlNE$}n*Jv^B=i0K`+5E}8I}dM>`AH|7p66DD0xxbCeOg&#eCdS|9 + android:textSize="12sp"/> + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 61bc5e520..46676e200 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -26,6 +26,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df5b15c19..361f453c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ Background Popup + Add To Video download path Path to store downloaded videos in @@ -375,4 +376,5 @@ Create New Playlist Delete Playlist Name + Add To Playlist diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ee526ca41..1f79bbf3d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -41,6 +41,7 @@ @drawable/ic_play_arrow_black_24dp @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp + @drawable/ic_playlist_add_black_24dp @color/light_separator_color @color/light_contrast_background_color @@ -88,6 +89,7 @@ @drawable/ic_play_arrow_white_24dp @drawable/ic_whatshot_white_24dp @drawable/ic_channel_white_24dp + @drawable/ic_playlist_add_white_24dp @color/dark_separator_color @color/dark_contrast_background_color From 168ac91ab8c25d5b47b4229aeffa6a3ef3e3c4dc Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 18 Jan 2018 11:02:06 -0800 Subject: [PATCH 027/276] -Fixed toast exception on playlist creation. --- .../newpipe/fragments/local/PlaylistCreationDialog.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 843b84de6..c43ba25b8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -78,12 +78,13 @@ public class PlaylistCreationDialog extends DialogFragment { new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); final List streams = Collections.singletonList(new StreamEntity(streamInfo)); + final Toast successToast = Toast.makeText(getActivity(), + "Playlist " + name + " successfully created", + Toast.LENGTH_SHORT); playlistManager.createPlaylist(name, streams) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(longs -> Toast.makeText(getActivity(), - "Playlist " + name + " successfully created", - Toast.LENGTH_SHORT).show()); + .subscribe(longs -> successToast.show()); }); return dialogBuilder.create(); From 776dbc34f78ccab76979f3beffd4399c39cc6b4c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 21 Jan 2018 19:32:49 -0800 Subject: [PATCH 028/276] -Added bulk playlist creation and append. -Added UI to create playlist from service player activity. -Added state saving to playlist dialogs. -Removed access to history activity on service player activity. -Made StreamEntity serializable. --- .../org/schabi/newpipe/NewPipeDatabase.java | 2 +- .../schabi/newpipe/database/Migrations.java | 2 +- .../database/stream/model/StreamEntity.java | 11 ++- .../fragments/detail/VideoDetailFragment.java | 3 +- .../fragments/local/LocalPlaylistManager.java | 48 ++++++------ .../fragments/local/PlaylistAppendDialog.java | 64 ++++++++-------- .../local/PlaylistCreationDialog.java | 44 ++--------- .../fragments/local/PlaylistDialog.java | 73 +++++++++++++++++++ .../org/schabi/newpipe/player/BasePlayer.java | 1 + .../newpipe/player/ServicePlayerActivity.java | 13 +++- .../newpipe/playlist/PlayQueueItem.java | 16 +++- app/src/main/res/menu/menu_play_queue.xml | 6 +- 12 files changed, 182 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 9cd56fca4..7b33d0c10 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -19,7 +19,7 @@ public final class NewPipeDatabase { public static void init(Context context) { databaseInstance = Room - .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) + .databaseBuilder(context, AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_11_12) .fallbackToDestructiveMigration() .build(); 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 72b0d2126..9200a64b0 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -15,7 +15,7 @@ public class Migrations { /* * Unfortunately these queries must be hardcoded due to the possibility of * schema and names changing at a later date, thus invalidating the older migration - * scripts if names are not hardcoded. + * scripts if they are not hardcoded. * */ // Not much we can do about this, since room doesn't create tables before migration. diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index c7ef889b9..0b73e81e9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -9,15 +9,18 @@ import android.arch.persistence.room.PrimaryKey; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.Constants; +import java.io.Serializable; + import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; @Entity(tableName = STREAM_TABLE, indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)}) -public class StreamEntity { +public class StreamEntity implements Serializable { final public static String STREAM_TABLE = "streams"; final public static String STREAM_ID = "uid"; @@ -78,6 +81,12 @@ public class StreamEntity { info.uploader_name, info.duration); } + @Ignore + public StreamEntity(final PlayQueueItem item) { + this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(), + item.getThumbnailUrl(), item.getUploader(), item.getDuration()); + } + @Ignore public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { StreamInfoItem item = new StreamInfoItem( diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 7f8afdbe8..91299ac14 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -331,7 +331,8 @@ public class VideoDetailFragment extends BaseStateFragment implement break; case R.id.detail_controls_playlist_append: if (getFragmentManager() != null && currentInfo != null) { - PlaylistAppendDialog.newInstance(currentInfo).show(getFragmentManager(), TAG); + PlaylistAppendDialog.fromStreamInfo(currentInfo) + .show(getFragmentManager(), TAG); } break; case R.id.detail_uploader_root_layout: diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index bf7bc14c8..89d69d4b4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -33,34 +33,38 @@ public class LocalPlaylistManager { } public Maybe> createPlaylist(final String name, final List streams) { - // Disallow creation of empty playlists until user is able to select thumbnail + // Disallow creation of empty playlists if (streams.isEmpty()) return Maybe.empty(); final StreamEntity defaultStream = streams.get(0); - final PlaylistEntity newPlaylist = new PlaylistEntity(name, defaultStream.getThumbnailUrl()); + final PlaylistEntity newPlaylist = + new PlaylistEntity(name, defaultStream.getThumbnailUrl()); - return Maybe.fromCallable(() -> database.runInTransaction(() -> { - final long playlistId = playlistTable.insert(newPlaylist); - - List joinEntities = new ArrayList<>(streams.size()); - for (int index = 0; index < streams.size(); index++) { - // Upsert streams and get their ids - final long streamId = streamTable.upsert(streams.get(index)); - joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, index)); - } - - return playlistStreamTable.insertAll(joinEntities); - })).subscribeOn(Schedulers.io()); + return Maybe.fromCallable(() -> database.runInTransaction(() -> + upsertStreams(playlistTable.insert(newPlaylist), streams, 0)) + ).subscribeOn(Schedulers.io()); } - public Maybe appendToPlaylist(final long playlistId, final StreamEntity stream) { - final Maybe streamIdFuture = Maybe.fromCallable(() -> streamTable.upsert(stream)); - final Maybe joinIndexFuture = - playlistStreamTable.getMaximumIndexOf(playlistId).firstElement(); + public Maybe> appendToPlaylist(final long playlistId, + final List streams) { + return playlistStreamTable.getMaximumIndexOf(playlistId) + .firstElement() + .map(maxJoinIndex -> database.runInTransaction(() -> + upsertStreams(playlistId, streams, maxJoinIndex + 1)) + ).subscribeOn(Schedulers.io()); + } - return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) -> - playlistStreamTable.insert(new PlaylistStreamEntity(playlistId, - streamId, currentMaxJoinIndex + 1)) - ).subscribeOn(Schedulers.io()); + private List upsertStreams(final long playlistId, + final List streams, + final int indexOffset) { + + List joinEntities = new ArrayList<>(streams.size()); + for (int index = 0; index < streams.size(); index++) { + // Upsert streams and get their ids + final long streamId = streamTable.upsert(streams.get(index)); + joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, + index + indexOffset)); + } + return playlistStreamTable.insertAll(joinEntities); } public Completable updateJoin(final long playlistId, final List streamIds) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 6fad839f1..de854ae0c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -4,7 +4,6 @@ import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -19,34 +18,48 @@ import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; +import org.schabi.newpipe.playlist.PlayQueueItem; -import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; -public class PlaylistAppendDialog extends DialogFragment { +public final class PlaylistAppendDialog extends PlaylistDialog { private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); - private static final String INFO_KEY = "info_key"; - private StreamInfo streamInfo; - - private View newPlaylistButton; private RecyclerView playlistRecyclerView; private InfoListAdapter playlistAdapter; - public static PlaylistAppendDialog newInstance(final StreamInfo info) { + public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { PlaylistAppendDialog dialog = new PlaylistAppendDialog(); - dialog.setInfo(info); + dialog.setInfo(Collections.singletonList(new StreamEntity(info))); return dialog; } - private void setInfo(StreamInfo info) { - this.streamInfo = info; + public static PlaylistAppendDialog fromStreamInfoItems(final List items) { + PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + List entities = new ArrayList<>(items.size()); + for (final StreamInfoItem item : items) { + entities.add(new StreamEntity(item)); + } + dialog.setInfo(entities); + return dialog; + } + + public static PlaylistAppendDialog fromPlayQueueItems(final List items) { + PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + List entities = new ArrayList<>(items.size()); + for (final PlayQueueItem item : items) { + entities.add(new StreamEntity(item)); + } + dialog.setInfo(entities); + return dialog; } /*////////////////////////////////////////////////////////////////////////// @@ -60,14 +73,9 @@ public class PlaylistAppendDialog extends DialogFragment { playlistAdapter.useMiniItemVariants(true); } - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - Serializable serial = savedInstanceState.getSerializable(INFO_KEY); - if (serial instanceof StreamInfo) streamInfo = (StreamInfo) serial; - } - } + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -79,7 +87,7 @@ public class PlaylistAppendDialog extends DialogFragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - newPlaylistButton = view.findViewById(R.id.newPlaylist); + final View newPlaylistButton = view.findViewById(R.id.newPlaylist); playlistRecyclerView = view.findViewById(R.id.playlist_list); playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); playlistRecyclerView.setAdapter(playlistAdapter); @@ -92,12 +100,14 @@ public class PlaylistAppendDialog extends DialogFragment { playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(PlaylistInfoItem selectedItem) { - if (!(selectedItem instanceof LocalPlaylistInfoItem)) return; + if (!(selectedItem instanceof LocalPlaylistInfoItem) || getStreams() == null) + return; + final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); final Toast successToast = Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); - playlistManager.appendToPlaylist(playlistId, new StreamEntity(streamInfo)) + playlistManager.appendToPlaylist(playlistId, getStreams()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> successToast.show()); @@ -127,20 +137,14 @@ public class PlaylistAppendDialog extends DialogFragment { }); } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable(INFO_KEY, streamInfo); - } - /*////////////////////////////////////////////////////////////////////////// // Helper //////////////////////////////////////////////////////////////////////////*/ public void openCreatePlaylistDialog() { - if (streamInfo == null || getFragmentManager() == null) return; + if (getStreams() == null || getFragmentManager() == null) return; - PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); + PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG); getDialog().dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index c43ba25b8..386ac1819 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -5,63 +5,35 @@ import android.app.Dialog; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; import android.view.View; import android.widget.EditText; import android.widget.Toast; -import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import java.util.Collections; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; -public class PlaylistCreationDialog extends DialogFragment { +public final class PlaylistCreationDialog extends PlaylistDialog { private static final String TAG = PlaylistCreationDialog.class.getCanonicalName(); - private static final boolean DEBUG = MainActivity.DEBUG; - private static final String INFO_KEY = "info_key"; - - private StreamInfo streamInfo; - - public static PlaylistCreationDialog newInstance(final StreamInfo info) { + public static PlaylistCreationDialog newInstance(final List streams) { PlaylistCreationDialog dialog = new PlaylistCreationDialog(); - dialog.setInfo(info); + dialog.setInfo(streams); return dialog; } - private void setInfo(final StreamInfo info) { - this.streamInfo = info; - } - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle + // Dialog //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (streamInfo != null) { - outState.putSerializable(INFO_KEY, streamInfo); - } - } - @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - if (savedInstanceState != null && streamInfo == null) { - final Object infoCandidate = savedInstanceState.getSerializable(INFO_KEY); - if (infoCandidate != null && infoCandidate instanceof StreamInfo) { - streamInfo = (StreamInfo) infoCandidate; - } - } - - if (streamInfo == null) return super.onCreateDialog(savedInstanceState); + if (getStreams() == null) return super.onCreateDialog(savedInstanceState); View dialogView = View.inflate(getContext(), R.layout.dialog_create_playlist, null); @@ -76,13 +48,11 @@ public class PlaylistCreationDialog extends DialogFragment { final String name = nameInput.getText().toString(); final LocalPlaylistManager playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); - final List streams = - Collections.singletonList(new StreamEntity(streamInfo)); final Toast successToast = Toast.makeText(getActivity(), - "Playlist " + name + " successfully created", + "Playlist successfully created", Toast.LENGTH_SHORT); - playlistManager.createPlaylist(name, streams) + playlistManager.createPlaylist(name, getStreams()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(longs -> successToast.show()); }); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java new file mode 100644 index 000000000..010ba0181 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java @@ -0,0 +1,73 @@ +package org.schabi.newpipe.fragments.local; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; + +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.util.StateSaver; + +import java.util.List; +import java.util.Queue; + +public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead { + + private List streamEntities; + + private StateSaver.SavedState savedState; + + protected void setInfo(final List entities) { + this.streamEntities = entities; + } + + protected List getStreams() { + return streamEntities; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + savedState = StateSaver.tryToRestore(savedInstanceState, this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + StateSaver.onDestroy(savedState); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public String generateSuffix() { + final int size = streamEntities == null ? 0 : streamEntities.size(); + return "." + size + ".list"; + } + + @Override + public void writeTo(Queue objectsToSave) { + objectsToSave.add(streamEntities); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + streamEntities = (List) savedObjects.poll(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (getActivity() != null) { + savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(), + savedState, outState, this); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 3cf169ecd..a481b3335 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -675,6 +675,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen simpleExoPlayer.seekTo(currentSourceIndex, startPos); } + // TODO: update exoplayer to 2.6.x in order to register view count on repeated streams databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 4165dc087..6e0f5c1d7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Player; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; @@ -149,8 +150,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity case android.R.id.home: finish(); return true; - case R.id.action_history: - NavigationHelper.openHistory(this); + case R.id.action_append_playlist: + appendToPlaylist(); return true; case R.id.action_settings: NavigationHelper.openSettings(this); @@ -185,6 +186,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity null ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } + + private void appendToPlaylist() { + if (this.player != null && this.player.getPlayQueue() != null) { + PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams()) + .show(getSupportFragmentManager(), getTag()); + } + } + //////////////////////////////////////////////////////////////////////////// // Service Connection //////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java index 9b14e8f03..f8e7b8655 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java @@ -5,6 +5,7 @@ import android.support.annotation.Nullable; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.util.ExtractorHelper; import java.io.Serializable; @@ -23,6 +24,7 @@ public class PlayQueueItem implements Serializable { final private long duration; final private String thumbnailUrl; final private String uploader; + final private StreamType streamType; private long recoveryPosition; private Throwable error; @@ -30,22 +32,26 @@ public class PlayQueueItem implements Serializable { private transient Single stream; PlayQueueItem(@NonNull final StreamInfo info) { - this(info.getName(), info.getUrl(), info.getServiceId(), info.duration, info.thumbnail_url, info.uploader_name); + this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(), + info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType()); this.stream = Single.just(info); } PlayQueueItem(@NonNull final StreamInfoItem item) { - this(item.getName(), item.getUrl(), item.getServiceId(), item.duration, item.thumbnail_url, item.uploader_name); + this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(), + item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); } private PlayQueueItem(final String name, final String url, final int serviceId, - final long duration, final String thumbnailUrl, final String uploader) { + final long duration, final String thumbnailUrl, final String uploader, + final StreamType streamType) { this.title = name; this.url = url; this.serviceId = serviceId; this.duration = duration; this.thumbnailUrl = thumbnailUrl; this.uploader = uploader; + this.streamType = streamType; this.recoveryPosition = RECOVERY_UNSET; } @@ -78,6 +84,10 @@ public class PlayQueueItem implements Serializable { return uploader; } + public StreamType getStreamType() { + return streamType; + } + public long getRecoveryPosition() { return recoveryPosition; } diff --git a/app/src/main/res/menu/menu_play_queue.xml b/app/src/main/res/menu/menu_play_queue.xml index 671d46329..fb64cb9fa 100644 --- a/app/src/main/res/menu/menu_play_queue.xml +++ b/app/src/main/res/menu/menu_play_queue.xml @@ -1,11 +1,11 @@ + tools:context=".player.BackgroundPlayerActivity"> - Date: Mon, 22 Jan 2018 14:13:11 -0800 Subject: [PATCH 029/276] -Improved bulk stream upsert into playlist performance by 5x. -Added custom info item type for plain stream entity. --- .../database/stream/dao/StreamDAO.java | 45 ++++++++++++++----- .../database/stream/model/StreamEntity.java | 7 +-- .../local/LocalPlaylistFragment.java | 2 +- .../fragments/local/LocalPlaylistManager.java | 9 ++-- .../stored/LocalPlaylistInfoItem.java | 2 +- .../stored/StreamEntityInfoItem.java | 18 ++++++++ .../stored/StreamStatisticsInfoItem.java | 12 +---- 7 files changed, 64 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index f7807ef42..ee246db1a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.database.stream.dao; import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; import android.arch.persistence.room.Transaction; @@ -12,6 +14,7 @@ import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; @@ -31,27 +34,47 @@ public abstract class StreamDAO implements BasicDAO { public abstract Flowable> listByService(int serviceId); @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + - STREAM_URL + " LIKE :url AND " + + STREAM_URL + " = :url AND " + STREAM_SERVICE_ID + " = :serviceId") public abstract Flowable> getStream(long serviceId, String url); - @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + - STREAM_URL + " LIKE :url AND " + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract void silentInsertAllInternal(final List streams); + + @Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " + + STREAM_URL + " = :url AND " + STREAM_SERVICE_ID + " = :serviceId") - abstract List getStreamInternal(long serviceId, String url); + abstract Long getStreamIdInternal(long serviceId, String url); @Transaction public long upsert(StreamEntity stream) { - final List streams = getStreamInternal(stream.getServiceId(), stream.getUrl()); + final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); - final long uid; - if (streams.isEmpty()) { - uid = insert(stream); + if (streamIdCandidate == null) { + return insert(stream); } else { - uid = streams.get(0).getUid(); - stream.setUid(uid); + stream.setUid(streamIdCandidate); update(stream); + return streamIdCandidate; } - return uid; + } + + @Transaction + public List upsertAll(List streams) { + silentInsertAllInternal(streams); + + final List streamIds = new ArrayList<>(streams.size()); + for (StreamEntity stream : streams) { + final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); + if (streamId == null) { + throw new IllegalStateException("StreamID cannot be null just after insertion."); + } + + streamIds.add(streamId); + stream.setUid(streamId); + } + + update(streams); + return streamIds; } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index 0b73e81e9..eb078a03c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -9,6 +9,7 @@ import android.arch.persistence.room.PrimaryKey; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.info_list.stored.StreamEntityInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.Constants; @@ -88,9 +89,9 @@ public class StreamEntity implements Serializable { } @Ignore - public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { - StreamInfoItem item = new StreamInfoItem( - getServiceId(), getUrl(), getTitle(), getStreamType()); + public StreamEntityInfoItem toStreamEntityInfoItem() throws IllegalArgumentException { + StreamEntityInfoItem item = new StreamEntityInfoItem(getUid(), getServiceId(), + getUrl(), getTitle(), getStreamType()); item.setThumbnailUrl(getThumbnailUrl()); item.setUploaderName(getUploader()); item.setDuration(getDuration()); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 44ecfb924..802532272 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -288,7 +288,7 @@ public class LocalPlaylistFragment extends BaseListFragment, private List getStreamItems(final List streams) { List items = new ArrayList<>(streams.size()); for (final StreamEntity stream : streams) { - items.add(stream.toStreamInfoItem()); + items.add(stream.toStreamEntityInfoItem()); } return items; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 89d69d4b4..5633e104d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -58,10 +58,9 @@ public class LocalPlaylistManager { final int indexOffset) { List joinEntities = new ArrayList<>(streams.size()); - for (int index = 0; index < streams.size(); index++) { - // Upsert streams and get their ids - final long streamId = streamTable.upsert(streams.get(index)); - joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, + final List streamIds = streamTable.upsertAll(streams); + for (int index = 0; index < streamIds.size(); index++) { + joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(index), index + indexOffset)); } return playlistStreamTable.insertAll(joinEntities); @@ -76,7 +75,7 @@ public class LocalPlaylistManager { return Completable.fromRunnable(() -> database.runInTransaction(() -> { playlistStreamTable.deleteBatch(playlistId); playlistStreamTable.insertAll(joinEntities); - })); + })).subscribeOn(Schedulers.io()); } public Flowable> getPlaylists() { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java index 3ac5fabb7..b0afe1948 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java @@ -5,7 +5,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; import static org.schabi.newpipe.util.Constants.NO_URL; -public class LocalPlaylistInfoItem extends PlaylistInfoItem { +public final class LocalPlaylistInfoItem extends PlaylistInfoItem { private final long playlistId; public LocalPlaylistInfoItem(final long playlistId, final String name) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java new file mode 100644 index 000000000..a54135211 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java @@ -0,0 +1,18 @@ +package org.schabi.newpipe.info_list.stored; + +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; + +public class StreamEntityInfoItem extends StreamInfoItem { + protected final long streamId; + + public StreamEntityInfoItem(final long streamId, final int serviceId, + final String url, final String name, final StreamType type) { + super(serviceId, url, name, type); + this.streamId = streamId; + } + + public long getStreamId() { + return streamId; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java index 76984d363..6659b551a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java @@ -1,24 +1,16 @@ package org.schabi.newpipe.info_list.stored; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; -public class StreamStatisticsInfoItem extends StreamInfoItem { - private final long streamId; - +public final class StreamStatisticsInfoItem extends StreamEntityInfoItem { private Date latestAccessDate; private long watchCount; public StreamStatisticsInfoItem(final long streamId, final int serviceId, final String url, final String name, final StreamType type) { - super(serviceId, url, name, type); - this.streamId = streamId; - } - - public long getStreamId() { - return streamId; + super(streamId, serviceId, url, name, type); } public Date getLatestAccessDate() { From 81f481833c89ad2e86b95a139bc77007a9ecb9c7 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 22 Jan 2018 14:21:00 -0800 Subject: [PATCH 030/276] -Added icon for bookmark pager. --- .../schabi/newpipe/fragments/MainFragment.java | 5 +++-- .../res/drawable-hdpi/ic_bookmark_black_24dp.png | Bin 0 -> 180 bytes .../res/drawable-hdpi/ic_bookmark_white_24dp.png | Bin 0 -> 185 bytes .../res/drawable-mdpi/ic_bookmark_black_24dp.png | Bin 0 -> 137 bytes .../res/drawable-mdpi/ic_bookmark_white_24dp.png | Bin 0 -> 139 bytes .../res/drawable-xhdpi/ic_bookmark_black_24dp.png | Bin 0 -> 204 bytes .../res/drawable-xhdpi/ic_bookmark_white_24dp.png | Bin 0 -> 213 bytes .../drawable-xxhdpi/ic_bookmark_black_24dp.png | Bin 0 -> 261 bytes .../drawable-xxhdpi/ic_bookmark_white_24dp.png | Bin 0 -> 273 bytes .../drawable-xxxhdpi/ic_bookmark_black_24dp.png | Bin 0 -> 332 bytes .../drawable-xxxhdpi/ic_bookmark_white_24dp.png | Bin 0 -> 351 bytes app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/styles.xml | 2 ++ 13 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_bookmark_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_bookmark_white_24dp.png diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index e76b97086..fc4f9a323 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -85,14 +85,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel); int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot); + int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark); if (isSubscriptionsPageOnlySelected()) { tabLayout.getTabAt(0).setIcon(channelIcon); - tabLayout.getTabAt(1).setText(R.string.tab_bookmarks); + tabLayout.getTabAt(1).setIcon(bookmarkIcon); } else { tabLayout.getTabAt(0).setIcon(whatsHotIcon); tabLayout.getTabAt(1).setIcon(channelIcon); - tabLayout.getTabAt(2).setText(R.string.tab_bookmarks); + tabLayout.getTabAt(2).setIcon(bookmarkIcon); } } diff --git a/app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad39da3adb4e7edea43312e46bac72af4a97395 GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8wWo_?NCo5D^BV;Z83?#se9sW& zP-?M&`$|K$V2t6p*mkWz-Y%9#4-P+d{)d0h-ak{Ml&?5RAk|;#^bE_4Hd$q@OFh*K zrkx3J@;x)b;*0zV#devZ6`P9x=PaMs&ZVDM!_AQ@cPc05$Jzopr0DpEqQUCw| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..9de15c51a92bbc29536fc7a8e34da51f1b6961de GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8gQtsQh{y4_*EaGtCXxw1 zJlv$4!kF$T**4E|&JoEK`&v7DIc1K0vU?gE=foPj+|<4QurOEp;fvR2wk!!*Sl|(T zYJ#})J|0#>5s@Gd#a5AD?5A9Grg{A1oBPJ+{1E|*+Y6hgJ~Wk$v|JLPmeg(i-BtTS ikD%?Fw#fbFYh(9%S-#0z*fJ0376wmOKbLh*2~7a_ZbD%I literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0a10c249467ff61ec41efe8951720d964cf9b8e1 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+i08bakkP60Ri8~4t9;zO$*ZCmY zY#;Wu;OUGHs%5)Iaz?{r|?s z@c(}|W*=B5!xEQty@9*PJwv*lZbnu4RXIwb+Xpg(_C> zuBdKQG}`7|c7iKr!rhKFC*N5-Xkg}-DLBwraDb8hpJ;|gt(q#vkvvDKNDNO*IwoXI*-BA)z4*}Q$iB} Dg$qwX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..872349cca6ffc150af47fddd05a5cd9648619c1f GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DIi4<#ArXh)Uf9Tc$Uww3&^)Ep zfYG3F`bPtU9&fG$ZvRPo3JW%}#O(4AJh91FX;%5|n&%$pC6=ajnX<2YQuh4FvM1lQ z&rK=QRJaj**--w)>V{~$4=b2Xu+L*+;dr3x(7+&|;Ba7nL&x!?k4IiM_UXR;PzWCs&hKm{}Ung#;dGK1}q&RP1!jxG{&BN`jM9sN0LV@mN N@O1TaS?83{1OP}SQ~Ce^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2189be346cf4bb7b0335b4e58e1c140790d1bd2d GIT binary patch literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw&Um^whEy=Vy?BuGkb_9e!~c(> znikbAnK?yak!*(HN>`4qMZQn8IFcM<{o`zG%=Szbn2|5_BT+zb;yWhqtUQE?^-f{N*#pR)!`v7Kfji}^^59SkVhFjUHx3vIVCg!0OO@*WB>pF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3faff90bb2b05245359ab9bc9ac54d7e7838f369 GIT binary patch literal 273 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawZg{#lhGg7(d(n}rDL|w(QT?V+ z*RA&1g`9$0o1=QNIhSZ;3HvoG2(8*5DR47*=X4i~|2KaB_~0@z$ZO>`L1XpjO-rS% zr}5ZY2=i5xCT0D%uC}ojy6s@RC;EniboqsAY*xFQ*O^_oE8D{RMZiO$kwwU3!Mx^q zOq?nc9GEz-6bnFEK<<pF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2b90acd7446b6737f60905b2f51679e8626ababe GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z$onL;uuoF_~!iDz@rW_3?EGm zUNTfUu(%p)vPD%e9a<1CpO9?mqt$V2@8idOJZ!ByejDfKm23Y3g+IWy{qyf+mo zSWw08_2Q~`EHev-fP%w;=<5uOOhD#`d!`NzAZASoFHjuFoDWsv@JnA9J9H`$wr0 zZy3HcXmp(|G2Fn)!sA#jpO9_1M0>@t(jPOJxtvd=PO>&Pc|RxnzVQAh`+roY0l_g# z+i12;(l;(`PBDDD%=Z0`^ptaM?_NA(DzOQ;sC?I^A$rxT=L~ZUgRY)1k}vR0HUHV5 zu2p@GeNVtmJNXUEx6~ZCVB*lgz{te1$Bb7%0mRJrWoF?3G7nT=28nYBe7Fcz(gT%o zC}muWsi>^BNep8S44c-;+IzJ$YFi{=0KM{fM7-jHC@eP3T9gi}Gnx8UFc zErx>`P26mu{2lJX{Hx>)Doq|Pc*S_xU6^kbmq5Q-dcec(kJQ4;w e1!%j*4u4672{qE|*LeX0j=|H_&t;ucLK6V3Jc9`U literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 46676e200..e770cf102 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -26,6 +26,7 @@ + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1f79bbf3d..bcbc759d2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -41,6 +41,7 @@ @drawable/ic_play_arrow_black_24dp @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp + @drawable/ic_bookmark_black_24dp @drawable/ic_playlist_add_black_24dp @color/light_separator_color @@ -89,6 +90,7 @@ @drawable/ic_play_arrow_white_24dp @drawable/ic_whatshot_white_24dp @drawable/ic_channel_white_24dp + @drawable/ic_bookmark_white_24dp @drawable/ic_playlist_add_white_24dp @color/dark_separator_color From f0829f9ef37e61385d84c69c377a5d93bded0683 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 25 Jan 2018 22:24:59 -0800 Subject: [PATCH 031/276] -Added support for changing local playlist name and thumbnail url. -Added query to remove stream table orphans. -Added query for retrieving flattened watch history records. -Added holder for local playlist stream info items. -Refactored info item on select listener as on touch gesture. --- .../database/stream/StreamHistoryEntry.java | 47 ++++++++ .../database/stream/dao/StreamDAO.java | 24 +++++ .../database/stream/dao/StreamHistoryDAO.java | 7 ++ .../fragments/detail/VideoDetailFragment.java | 3 +- .../fragments/list/BaseListFragment.java | 19 +--- .../fragments/local/BookmarkFragment.java | 6 +- .../local/LocalPlaylistFragment.java | 8 +- .../fragments/local/LocalPlaylistManager.java | 28 ++++- .../fragments/local/PlaylistAppendDialog.java | 7 +- .../local/PlaylistCreationDialog.java | 3 +- .../local/StatisticsPlaylistFragment.java | 4 +- .../subscription/SubscriptionFragment.java | 21 ++-- .../newpipe/info_list/InfoItemBuilder.java | 23 ++-- .../newpipe/info_list/InfoListAdapter.java | 13 ++- .../newpipe/info_list/OnInfoItemGesture.java | 18 ++++ .../holder/StreamPlaylistInfoItemHolder.java | 102 ++++++++++++++++++ ..._playlist.xml => dialog_playlist_name.xml} | 1 - .../res/layout/list_stream_playlist_item.xml | 86 +++++++++++++++ app/src/main/res/values/strings.xml | 1 + 19 files changed, 353 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java rename app/src/main/res/layout/{dialog_create_playlist.xml => dialog_playlist_name.xml} (96%) create mode 100644 app/src/main/res/layout/list_stream_playlist_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java new file mode 100644 index 000000000..3df641372 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.database.stream; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.extractor.stream.StreamType; + +import java.util.Date; + +public class StreamHistoryEntry { + @ColumnInfo(name = StreamEntity.STREAM_ID) + final public long uid; + @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) + final public int serviceId; + @ColumnInfo(name = StreamEntity.STREAM_URL) + final public String url; + @ColumnInfo(name = StreamEntity.STREAM_TITLE) + final public String title; + @ColumnInfo(name = StreamEntity.STREAM_TYPE) + final public StreamType streamType; + @ColumnInfo(name = StreamEntity.STREAM_DURATION) + final public long duration; + @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) + final public String uploader; + @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) + final public long streamId; + @ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE) + final public Date accessDate; + + public StreamHistoryEntry(long uid, int serviceId, String url, String title, + StreamType streamType, long duration, String uploader, + String thumbnailUrl, long streamId, Date accessDate) { + this.uid = uid; + this.serviceId = serviceId; + this.url = url; + this.title = title; + this.streamType = streamType; + this.duration = duration; + this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl; + this.streamId = streamId; + this.accessDate = accessDate; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index ee246db1a..a4955d835 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -7,17 +7,23 @@ import android.arch.persistence.room.Query; import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import java.util.ArrayList; import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; +import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao public abstract class StreamDAO implements BasicDAO { @@ -77,4 +83,22 @@ public abstract class StreamDAO implements BasicDAO { update(streams); return streamIds; } + + @Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID + + " NOT IN " + + "(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE + + + " LEFT JOIN " + STREAM_HISTORY_TABLE + + " ON " + STREAM_ID + " = " + + StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID + + + " LEFT JOIN " + STREAM_STATE_TABLE + + " ON " + STREAM_ID + " = " + + StreamStateEntity.STREAM_STATE_TABLE + "." + StreamStateEntity.JOIN_STREAM_ID + + + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + STREAM_ID + " = " + + PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID + + ")") + public abstract int deleteOrphans(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java index 527d151ea..81ee9d912 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -6,6 +6,7 @@ import android.arch.persistence.room.Query; import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.stream.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; @@ -36,6 +37,12 @@ public abstract class StreamHistoryDAO implements BasicDAO throw new UnsupportedOperationException(); } + @Query("SELECT * FROM " + STREAM_TABLE + + " INNER JOIN " + STREAM_HISTORY_TABLE + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + public abstract Flowable> getHistory(); + @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") public abstract int deleteStreamHistory(final long streamId); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 91299ac14..05550a0a5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -62,6 +62,7 @@ import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.helper.PlayerHelper; @@ -471,7 +472,7 @@ public class VideoDetailFragment extends BaseStateFragment implement @Override protected void initListeners() { super.initListeners(); - infoItemBuilder.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoItemBuilder.setOnStreamSelectedListener(new OnInfoItemGesture() { @Override public void selected(StreamInfoItem selectedItem) { selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index a09a472a5..9e4fe89ab 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -3,19 +3,15 @@ package org.schabi.newpipe.fragments.list; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; -import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.ActionBar; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; -import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.View; -import android.widget.TextView; -import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; @@ -24,12 +20,11 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.StateSaver; import java.util.List; @@ -140,7 +135,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { @Override public void selected(StreamInfoItem selectedItem) { onItemSelected(selectedItem); @@ -155,7 +150,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } }); - infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnChannelSelectedListener(new OnInfoItemGesture() { @Override public void selected(ChannelInfoItem selectedItem) { onItemSelected(selectedItem); @@ -163,12 +158,9 @@ public abstract class BaseListFragment extends BaseStateFragment implem useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } - - @Override - public void held(ChannelInfoItem selectedItem) {} }); - infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { onItemSelected(selectedItem); @@ -176,9 +168,6 @@ public abstract class BaseListFragment extends BaseStateFragment implem useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } - - @Override - public void held(PlaylistInfoItem selectedItem) {} }); itemsList.clearOnScrollListeners(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java index ecbd416ee..769365dd8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java @@ -21,9 +21,9 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; @@ -33,10 +33,8 @@ import java.util.Collections; import java.util.List; import icepick.State; -import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -143,7 +141,7 @@ public class BookmarkFragment extends BaseStateFragment() { + infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 802532272..7ba5db7e1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -21,8 +21,8 @@ import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; @@ -141,7 +141,7 @@ public class LocalPlaylistFragment extends BaseListFragment, protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { @Override public void selected(StreamInfoItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement @@ -219,7 +219,7 @@ public class LocalPlaylistFragment extends BaseListFragment, super.startLoading(forceLoad); resetFragment(); - playlistManager.getPlaylist(playlistId) + playlistManager.getPlaylistStreams(playlistId) .observeOn(AndroidSchedulers.mainThread()) .subscribe(getPlaylistObserver()); } @@ -317,7 +317,7 @@ public class LocalPlaylistFragment extends BaseListFragment, if (super.onError(exception)) return true; onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, - "none", "Subscriptions", R.string.general_error); + "none", "Local Playlist", R.string.general_error); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 5633e104d..4bc161c04 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.fragments.local; +import android.support.annotation.Nullable; + import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; @@ -82,7 +84,7 @@ public class LocalPlaylistManager { return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); } - public Flowable> getPlaylist(final long playlistId) { + public Flowable> getPlaylistStreams(final long playlistId) { return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); } @@ -90,4 +92,28 @@ public class LocalPlaylistManager { return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId)) .subscribeOn(Schedulers.io()); } + + public Maybe renamePlaylist(final long playlistId, final String name) { + return modifyPlaylist(playlistId, name, null); + } + + public Maybe changePlaylistThumbnail(final long playlistId, + final String thumbnailUrl) { + return modifyPlaylist(playlistId, null, thumbnailUrl); + } + + private Maybe modifyPlaylist(final long playlistId, + @Nullable final String name, + @Nullable final String thumbnailUrl) { + return playlistTable.getPlaylist(playlistId) + .firstElement() + .filter(playlistEntities -> !playlistEntities.isEmpty()) + .map(playlistEntities -> { + PlaylistEntity playlist = playlistEntities.get(0); + if (name != null) playlist.setName(name); + if (thumbnailUrl != null) playlist.setThumbnailUrl(thumbnailUrl); + return playlistTable.update(playlist); + }).subscribeOn(Schedulers.io()); + } + } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index de854ae0c..6ed357e36 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -19,8 +19,8 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; @@ -97,7 +97,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); - playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + playlistAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { if (!(selectedItem instanceof LocalPlaylistInfoItem) || getStreams() == null) @@ -113,9 +113,6 @@ public final class PlaylistAppendDialog extends PlaylistDialog { getDialog().dismiss(); } - - @Override - public void held(PlaylistInfoItem selectedItem) {} }); playlistManager.getPlaylists() diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 386ac1819..791e90fa2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -35,8 +35,7 @@ public final class PlaylistCreationDialog extends PlaylistDialog { public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { if (getStreams() == null) return super.onCreateDialog(savedInstanceState); - View dialogView = View.inflate(getContext(), - R.layout.dialog_create_playlist, null); + View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); EditText nameInput = dialogView.findViewById(R.id.playlist_name); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java index 8db1f8780..c2181ca8d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java @@ -19,8 +19,8 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; @@ -127,7 +127,7 @@ public abstract class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { @Override public void selected(StreamInfoItem selectedItem) { NavigationHelper.openVideoDetailFragment(getFragmentManager(), diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java index 662f617bb..8db5d5f00 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java @@ -16,8 +16,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; @@ -125,24 +125,17 @@ public class SubscriptionFragment extends BaseStateFragment() { + infoListAdapter.setOnChannelSelectedListener(new OnInfoItemGesture() { @Override public void selected(ChannelInfoItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); - - } - - @Override - public void held(ChannelInfoItem selectedItem) {} - }); - - headerRootLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()); + NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), + selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); } }); + + headerRootLayout.setOnClickListener(view -> + NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager())); } private void resetFragment() { 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 c81235623..cdad31674 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 @@ -43,17 +43,12 @@ import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; public class InfoItemBuilder { private static final String TAG = InfoItemBuilder.class.toString(); - public interface OnInfoItemSelectedListener { - void selected(T selectedItem); - void held(T selectedItem); - } - private final Context context; private ImageLoader imageLoader = ImageLoader.getInstance(); - private OnInfoItemSelectedListener onStreamSelectedListener; - private OnInfoItemSelectedListener onChannelSelectedListener; - private OnInfoItemSelectedListener onPlaylistSelectedListener; + private OnInfoItemGesture onStreamSelectedListener; + private OnInfoItemGesture onChannelSelectedListener; + private OnInfoItemGesture onPlaylistSelectedListener; public InfoItemBuilder(Context context) { this.context = context; @@ -91,27 +86,27 @@ public class InfoItemBuilder { return imageLoader; } - public OnInfoItemSelectedListener getOnStreamSelectedListener() { + public OnInfoItemGesture getOnStreamSelectedListener() { return onStreamSelectedListener; } - public void setOnStreamSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnStreamSelectedListener(OnInfoItemGesture listener) { this.onStreamSelectedListener = listener; } - public OnInfoItemSelectedListener getOnChannelSelectedListener() { + public OnInfoItemGesture getOnChannelSelectedListener() { return onChannelSelectedListener; } - public void setOnChannelSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnChannelSelectedListener(OnInfoItemGesture listener) { this.onChannelSelectedListener = listener; } - public OnInfoItemSelectedListener getOnPlaylistSelectedListener() { + public OnInfoItemGesture getOnPlaylistSelectedListener() { return onPlaylistSelectedListener; } - public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnPlaylistSelectedListener(OnInfoItemGesture listener) { this.onPlaylistSelectedListener = listener; } 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 5494eae23..1dc4442c7 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 @@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder.OnInfoItemSelectedListener; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; @@ -56,6 +55,10 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; private boolean useMiniVariant = false; @@ -77,15 +80,15 @@ public class InfoListAdapter extends RecyclerView.Adapter(); } - public void setOnStreamSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnStreamSelectedListener(OnInfoItemGesture listener) { infoItemBuilder.setOnStreamSelectedListener(listener); } - public void setOnChannelSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnChannelSelectedListener(OnInfoItemGesture listener) { infoItemBuilder.setOnChannelSelectedListener(listener); } - public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener listener) { + public void setOnPlaylistSelectedListener(OnInfoItemGesture listener) { infoItemBuilder.setOnPlaylistSelectedListener(listener); } @@ -202,7 +205,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + + public abstract void selected(T selectedItem); + + public void held(T selectedItem) { + // Optional gesture + } + + public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { + // Optional gesture + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java new file mode 100644 index 000000000..8261d4760 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java @@ -0,0 +1,102 @@ +package org.schabi.newpipe.info_list.holder; + +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.Localization; + +public class StreamPlaylistInfoItemHolder extends InfoItemHolder { + + public final ImageView itemThumbnailView; + public final TextView itemVideoTitleView; + public final TextView itemUploaderView; + public final TextView itemDurationView; + public final View itemHandleView; + + StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemDurationView = itemView.findViewById(R.id.itemDurationView); + itemHandleView = itemView.findViewById(R.id.itemHandle); + } + + public StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + if (!(infoItem instanceof StreamInfoItem)) return; + final StreamInfoItem item = (StreamInfoItem) infoItem; + + itemVideoTitleView.setText(item.getName()); + itemUploaderView.setText(item.uploader_name); + + if (item.duration > 0) { + itemDurationView.setText(Localization.getDurationString(item.duration)); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + R.color.duration_background_color)); + itemDurationView.setVisibility(View.VISIBLE); + } else { + itemDurationView.setVisibility(View.GONE); + } + + // Default thumbnail is shown on error, while loading and if the url is empty + itemBuilder.getImageLoader().displayImage(item.thumbnail_url, itemThumbnailView, + StreamPlaylistInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().selected(item); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().held(item); + } + return true; + }); + + itemThumbnailView.setOnTouchListener(getOnTouchListener(item)); + itemHandleView.setOnTouchListener(getOnTouchListener(item)); + } + + private View.OnTouchListener getOnTouchListener(final StreamInfoItem item) { + return (view, motionEvent) -> { + view.performClick(); + if (itemBuilder != null && + motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + itemBuilder.getOnStreamSelectedListener() + .drag(item, StreamPlaylistInfoItemHolder.this); + } + return false; + }; + } + + /** + * Display options for stream thumbnails + */ + private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnFail(R.drawable.dummy_thumbnail) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnLoading(R.drawable.dummy_thumbnail) + .build(); +} diff --git a/app/src/main/res/layout/dialog_create_playlist.xml b/app/src/main/res/layout/dialog_playlist_name.xml similarity index 96% rename from app/src/main/res/layout/dialog_create_playlist.xml rename to app/src/main/res/layout/dialog_playlist_name.xml index b42d3101f..2dfab228b 100644 --- a/app/src/main/res/layout/dialog_create_playlist.xml +++ b/app/src/main/res/layout/dialog_playlist_name.xml @@ -1,6 +1,5 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 361f453c4..161cf1735 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -188,6 +188,7 @@ No results @string/no_videos Nothing Here But Crickets + Drag to reorder Cannot create download directory \'%1$s\' Created download directory \'%1$s\' From 388ec3e3d3ac9754acae04271cd39f91223387a3 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 26 Jan 2018 21:34:17 -0800 Subject: [PATCH 032/276] -Added history record manager as single entry for all database history transactions. -Merged stream record manager into history record manager. -Removed subject-based history database actions. -Merged normalized history table into watch history fragment. -Modified history fragments to use long click for delete actions. -Refactored DAO operations from search fragment to record manager. -Added index to search history table on search string. -Fix baseplayer round repeat not detected by discontinuity. --- .../java/org/schabi/newpipe/MainActivity.java | 98 +-------- .../schabi/newpipe/database/AppDatabase.java | 4 +- .../schabi/newpipe/database/Migrations.java | 1 + .../history/dao/SearchHistoryDAO.java | 2 + .../dao/StreamHistoryDAO.java | 13 +- .../history/model/SearchHistoryEntry.java | 6 +- .../model/StreamHistoryEntity.java | 10 +- .../model}/StreamHistoryEntry.java | 7 +- .../stream/StreamStatisticsEntry.java | 3 +- .../database/stream/dao/StreamDAO.java | 4 +- .../fragments/detail/VideoDetailFragment.java | 17 +- .../fragments/list/search/SearchFragment.java | 189 ++++++++---------- .../local/StatisticsPlaylistFragment.java | 8 +- .../fragments/local/StreamRecordManager.java | 44 ---- .../newpipe/history/HistoryActivity.java | 23 +-- .../newpipe/history/HistoryEntryAdapter.java | 45 ++--- .../newpipe/history/HistoryFragment.java | 176 ++++++---------- .../newpipe/history/HistoryRecordManager.java | 147 ++++++++++++++ .../history/SearchHistoryFragment.java | 63 +++++- .../history/WatchedHistoryFragment.java | 85 +++++--- .../org/schabi/newpipe/player/BasePlayer.java | 12 +- app/src/main/res/values/strings.xml | 4 + 22 files changed, 476 insertions(+), 485 deletions(-) rename app/src/main/java/org/schabi/newpipe/database/{stream => history}/dao/StreamHistoryDAO.java (80%) rename app/src/main/java/org/schabi/newpipe/database/{stream => history}/model/StreamHistoryEntity.java (81%) rename app/src/main/java/org/schabi/newpipe/database/{stream => history/model}/StreamHistoryEntry.java (90%) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 9e8f3fa76..9a1ecd07a 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -26,8 +26,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; @@ -42,40 +40,21 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; -import org.schabi.newpipe.database.history.model.HistoryEntry; -import org.schabi.newpipe.database.history.model.SearchHistoryEntry; -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; -import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; -import java.util.Date; - -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; - -public class MainActivity extends AppCompatActivity implements HistoryListener { +public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); - private SharedPreferences sharedPreferences; private ActionBarDrawerToggle toggle = null; /*////////////////////////////////////////////////////////////////////////// @@ -86,7 +65,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { protected void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); super.onCreate(savedInstanceState); @@ -98,7 +76,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { setSupportActionBar(findViewById(R.id.toolbar)); setupDrawer(); - initHistory(); } private void setupDrawer() { @@ -149,8 +126,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { if (!isChangingConfigurations()) { StateSaver.clearStateFiles(); } - - disposeHistory(); } @Override @@ -357,75 +332,4 @@ public class MainActivity extends AppCompatActivity implements HistoryListener { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } } - - /*////////////////////////////////////////////////////////////////////////// - // History - //////////////////////////////////////////////////////////////////////////*/ - - private WatchHistoryDAO watchHistoryDAO; - private SearchHistoryDAO searchHistoryDAO; - private PublishSubject historyEntrySubject; - private Disposable disposable; - - private void initHistory() { - final AppDatabase database = NewPipeDatabase.getInstance(); - watchHistoryDAO = database.watchHistoryDAO(); - searchHistoryDAO = database.searchHistoryDAO(); - historyEntrySubject = PublishSubject.create(); - disposable = historyEntrySubject - .observeOn(Schedulers.io()) - .subscribe(getHistoryEntryConsumer()); - } - - private void disposeHistory() { - if (disposable != null) disposable.dispose(); - watchHistoryDAO = null; - searchHistoryDAO = null; - } - - @NonNull - private Consumer getHistoryEntryConsumer() { - return new Consumer() { - @Override - public void accept(HistoryEntry historyEntry) throws Exception { - //noinspection unchecked - HistoryDAO historyDAO = (HistoryDAO) - (historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO); - - HistoryEntry latestEntry = historyDAO.getLatestEntry(); - if (historyEntry.hasEqualValues(latestEntry)) { - latestEntry.setCreationDate(historyEntry.getCreationDate()); - historyDAO.update(latestEntry); - } else { - historyDAO.insert(historyEntry); - } - } - }; - } - - private void addWatchHistoryEntry(StreamInfo streamInfo) { - if (sharedPreferences.getBoolean(getString(R.string.enable_watch_history_key), true)) { - WatchHistoryEntry entry = new WatchHistoryEntry(streamInfo); - historyEntrySubject.onNext(entry); - } - } - - @Override - public void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream) { - addWatchHistoryEntry(streamInfo); - } - - @Override - public void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream) { - addWatchHistoryEntry(streamInfo); - } - - @Override - public void onSearch(int serviceId, String query) { - // Add search history entry - if (sharedPreferences.getBoolean(getString(R.string.enable_search_history_key), true)) { - SearchHistoryEntry searchHistoryEntry = new SearchHistoryEntry(new Date(), serviceId, query); - historyEntrySubject.onNext(searchHistoryEntry); - } - } } 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 d5a9164dc..7097dd4a7 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -13,10 +13,10 @@ import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; 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 9200a64b0..b977e43e9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -20,6 +20,7 @@ public class Migrations { // Not much we can do about this, since room doesn't create tables before migration. // It's either this or blasting the entire database anew. + database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index 70799d971..257c1ec3d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.database.history.dao; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.support.annotation.Nullable; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; @@ -22,6 +23,7 @@ public interface SearchHistoryDAO extends HistoryDAO { @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") @Override + @Nullable SearchHistoryEntry getLatestEntry(); @Query("DELETE FROM " + TABLE_NAME) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java similarity index 80% rename from app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java rename to app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 81ee9d912..64003910e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -1,14 +1,13 @@ -package org.schabi.newpipe.database.stream.dao; +package org.schabi.newpipe.database.history.dao; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; -import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.stream.StreamHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import java.util.List; @@ -18,9 +17,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LA import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Dao public abstract class StreamHistoryDAO implements BasicDAO { diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java index d18974089..bba3f4295 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java @@ -3,10 +3,14 @@ package org.schabi.newpipe.database.history.model; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.Ignore; +import android.arch.persistence.room.Index; import java.util.Date; -@Entity(tableName = SearchHistoryEntry.TABLE_NAME) +import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH; + +@Entity(tableName = SearchHistoryEntry.TABLE_NAME, + indices = {@Index(value = SEARCH)}) public class SearchHistoryEntry extends HistoryEntry { public static final String TABLE_NAME = "search_history"; diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java similarity index 81% rename from app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java rename to app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java index d937a29ed..b238af0a9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.database.stream.model; +package org.schabi.newpipe.database.history.model; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; @@ -6,12 +6,14 @@ import android.arch.persistence.room.ForeignKey; import android.arch.persistence.room.Index; import android.support.annotation.NonNull; +import org.schabi.newpipe.database.stream.model.StreamEntity; + import java.util.Date; import static android.arch.persistence.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; @Entity(tableName = STREAM_HISTORY_TABLE, primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java similarity index 90% rename from app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java rename to app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java index 3df641372..cdc9cc40a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java @@ -1,9 +1,8 @@ -package org.schabi.newpipe.database.stream; +package org.schabi.newpipe.database.history.model; import android.arch.persistence.room.ColumnInfo; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; @@ -44,4 +43,8 @@ public class StreamHistoryEntry { this.streamId = streamId; this.accessDate = accessDate; } + + public StreamHistoryEntity toStreamHistoryEntity() { + return new StreamHistoryEntity(streamId, accessDate); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java index 722cff5cd..1c2a7028e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -2,9 +2,8 @@ package org.schabi.newpipe.database.stream; import android.arch.persistence.room.ColumnInfo; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index a4955d835..b699e0b6b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -9,7 +9,7 @@ import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import java.util.ArrayList; @@ -22,7 +22,7 @@ import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; -import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 05550a0a5..b134bc98d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -44,6 +44,7 @@ import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; @@ -60,6 +61,7 @@ import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.history.HistoryListener; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.OnInfoItemGesture; @@ -649,9 +651,6 @@ public class VideoDetailFragment extends BaseStateFragment implement public void onActionSelected(int selectedStreamId) { try { NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http"))); - if(activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(info, null); - } } catch (Exception e) { if(DEBUG) Log.i(TAG, "Failed to start kore", e); showInstallKoreDialog(activity); @@ -805,10 +804,6 @@ public class VideoDetailFragment extends BaseStateFragment implement private void openBackgroundPlayer(final boolean append) { AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onAudioPlayed(currentInfo, audioStream); - } - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); @@ -825,10 +820,6 @@ public class VideoDetailFragment extends BaseStateFragment implement return; } - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream()); - } - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue); @@ -844,10 +835,6 @@ public class VideoDetailFragment extends BaseStateFragment implement private void openVideoPlayer() { VideoStream selectedVideoStream = getSelectedVideoStream(); - if (activity instanceof HistoryListener) { - ((HistoryListener) activity).onVideoPlayed(currentInfo, selectedVideoStream); - } - if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream); } else { 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 d6ed2a313..0638c06e7 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 @@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list.search; import android.app.Activity; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -30,10 +29,8 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; -import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; @@ -44,7 +41,7 @@ import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.history.HistoryListener; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.AnimationUtils; @@ -64,16 +61,11 @@ import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.Flowable; -import io.reactivex.Notification; import io.reactivex.Observable; -import io.reactivex.ObservableSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.functions.Predicate; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; @@ -121,7 +113,7 @@ public class SearchFragment extends BaseListFragment suggestionPublisher + .onNext(searchEditText.getText().toString()), + + throwable -> showSnackBarError(throwable, + UserAction.SOMETHING_ELSE, "none", + "Deleting item failed", R.string.general_error) + ); + new AlertDialog.Builder(activity) .setTitle(item.query) .setMessage(R.string.delete_item_search_history) .setCancelable(true) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - disposables.add(Observable - .fromCallable(new Callable() { - @Override - public Integer call() throws Exception { - return searchHistoryDAO.deleteAllWhereQuery(item.query); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Integer howManyDeleted) throws Exception { - suggestionPublisher.onNext(searchEditText.getText().toString()); - } - }, new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error); - } - })); - } - }).show(); + .setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete)) + .show(); } @Override @@ -589,83 +569,67 @@ public class SearchFragment extends BaseListFragment observable = suggestionPublisher .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .startWith(searchQuery != null ? searchQuery : "") - .filter(new Predicate() { - @Override - public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception { - return isSuggestionsEnabled; - } - }); + .filter(query -> isSuggestionsEnabled); suggestionDisposable = observable - .switchMap(new Function>>>() { - @Override - public ObservableSource>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception { - final Flowable> flowable = query.length() > 0 - ? searchHistoryDAO.getSimilarEntries(query, 3) - : searchHistoryDAO.getUniqueEntries(25); - final Observable> local = flowable.toObservable() - .map(new Function, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List searchHistoryEntries) throws Exception { - List result = new ArrayList<>(); - for (SearchHistoryEntry entry : searchHistoryEntries) - result.add(new SuggestionItem(true, entry.getSearch())); - return result; - } - }); + .switchMap(query -> { + final Flowable> flowable = historyRecordManager + .getRelatedSearches(query, 3, 25); + final Observable> local = flowable.toObservable() + .map(searchHistoryEntries -> { + List result = new ArrayList<>(); + for (SearchHistoryEntry entry : searchHistoryEntries) + result.add(new SuggestionItem(true, entry.getSearch())); + return result; + }); - if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { - // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION - return local.materialize(); + if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { + // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION + return local.materialize(); + } + + final Observable> network = ExtractorHelper + .suggestionsFor(serviceId, query, contentCountry) + .toObservable() + .map(strings -> { + List result = new ArrayList<>(); + for (String entry : strings) { + result.add(new SuggestionItem(false, entry)); + } + return result; + }); + + return Observable.zip(local, network, (localResult, networkResult) -> { + List result = new ArrayList<>(); + if (localResult.size() > 0) result.addAll(localResult); + + // Remove duplicates + final Iterator iterator = networkResult.iterator(); + while (iterator.hasNext() && localResult.size() > 0) { + final SuggestionItem next = iterator.next(); + for (SuggestionItem item : localResult) { + if (item.query.equals(next.query)) { + iterator.remove(); + break; + } + } } - final Observable> network = ExtractorHelper.suggestionsFor(serviceId, query, contentCountry).toObservable() - .map(new Function, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List strings) throws Exception { - List result = new ArrayList<>(); - for (String entry : strings) result.add(new SuggestionItem(false, entry)); - return result; - } - }); - - return Observable.zip(local, network, new BiFunction, List, List>() { - @Override - public List apply(@io.reactivex.annotations.NonNull List localResult, @io.reactivex.annotations.NonNull List networkResult) throws Exception { - List result = new ArrayList<>(); - if (localResult.size() > 0) result.addAll(localResult); - - // Remove duplicates - final Iterator iterator = networkResult.iterator(); - while (iterator.hasNext() && localResult.size() > 0) { - final SuggestionItem next = iterator.next(); - for (SuggestionItem item : localResult) { - if (item.query.equals(next.query)) { - iterator.remove(); - break; - } - } - } - - if (networkResult.size() > 0) result.addAll(networkResult); - return result; - } - }).materialize(); - } + if (networkResult.size() > 0) result.addAll(networkResult); + return result; + }).materialize(); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer>>() { - @Override - public void accept(@io.reactivex.annotations.NonNull Notification> listNotification) throws Exception { - if (listNotification.isOnNext()) { - handleSuggestions(listNotification.getValue()); - } else if (listNotification.isOnError()) { - Throwable error = listNotification.getError(); - if (!ExtractorHelper.hasAssignableCauseThrowable(error, - IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) { - onSuggestionError(error); - } + .subscribe(listNotification -> { + if (listNotification.isOnNext()) { + handleSuggestions(listNotification.getValue()); + } else if (listNotification.isOnError()) { + Throwable error = listNotification.getError(); + if (!ExtractorHelper.hasAssignableCauseThrowable(error, + IOException.class, SocketException.class, + InterruptedException.class, InterruptedIOException.class)) { + onSuggestionError(error); } } }); @@ -718,11 +682,14 @@ public class SearchFragment extends BaseListFragment {}, + error -> showSnackBarError(error, UserAction.SEARCHED, + NewPipe.getNameOfService(serviceId), query, 0) + ); + suggestionPublisher.onNext(query); startLoading(false); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java index c2181ca8d..6eddc3a5c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java @@ -13,12 +13,12 @@ import android.view.ViewGroup; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; @@ -49,7 +49,7 @@ public abstract class StatisticsPlaylistFragment /* Used for independent events */ private Subscription databaseSubscription; - private StreamRecordManager recordManager; + private HistoryRecordManager recordManager; /////////////////////////////////////////////////////////////////////////// // Abstracts @@ -68,7 +68,7 @@ public abstract class StatisticsPlaylistFragment @Override public void onAttach(Context context) { super.onAttach(context); - recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); + recordManager = new HistoryRecordManager(context); } @Override @@ -205,7 +205,7 @@ public abstract class StatisticsPlaylistFragment super.startLoading(forceLoad); resetFragment(); - recordManager.getStatistics() + recordManager.getStreamStatistics() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getHistoryObserver()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java deleted file mode 100644 index 993ed58da..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; -import org.schabi.newpipe.extractor.stream.StreamInfo; - -import java.util.Date; -import java.util.List; - -import io.reactivex.Flowable; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; - -public class StreamRecordManager { - - private final AppDatabase database; - private final StreamDAO streamTable; - private final StreamHistoryDAO historyTable; - - public StreamRecordManager(final AppDatabase db) { - database = db; - streamTable = db.streamDAO(); - historyTable = db.streamHistoryDAO(); - } - - public Single onViewed(final StreamInfo info) { - return Single.fromCallable(() -> database.runInTransaction(() -> { - final long streamId = streamTable.upsert(new StreamEntity(info)); - return historyTable.insert(new StreamHistoryEntity(streamId, new Date())); - })).subscribeOn(Schedulers.io()); - } - - public int removeHistory(final long streamId) { - return historyTable.deleteStreamHistory(streamId); - } - - public Flowable> getStatistics() { - return historyTable.getStatistics(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java index 8d8e4ef16..30589a22c 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java @@ -9,6 +9,7 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; @@ -50,8 +51,10 @@ public class HistoryActivity extends AppCompatActivity { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle(R.string.title_activity_history); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(R.string.title_activity_history); + } // Create the adapter that will return a fragment for each of the three // primary sections of the activity. mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); @@ -66,17 +69,11 @@ public class HistoryActivity extends AppCompatActivity { final FloatingActionButton fab = findViewById(R.id.fab); RxView.clicks(fab) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Object o) { - int currentItem = mViewPager.getCurrentItem(); - HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem); - if(fragment != null) { - fragment.onHistoryCleared(); - } else { - Log.w(TAG, "Couldn't find current fragment"); - } - } + .subscribe(ignored -> { + int currentItem = mViewPager.getCurrentItem(); + HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter + .instantiateItem(mViewPager, currentItem); + fragment.onHistoryCleared(); }); } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java index d56469a7e..f61e8eb7d 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java @@ -4,9 +4,8 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; -import android.view.View; -import org.schabi.newpipe.database.history.model.HistoryEntry; +import org.schabi.newpipe.util.Localization; import java.text.DateFormat; import java.util.ArrayList; @@ -19,7 +18,7 @@ import java.util.Date; * @param the type of the entries * @param the type of the view holder */ -public abstract class HistoryEntryAdapter extends RecyclerView.Adapter { +public abstract class HistoryEntryAdapter extends RecyclerView.Adapter { private final ArrayList mEntries; private final DateFormat mDateFormat; @@ -29,9 +28,8 @@ public abstract class HistoryEntryAdapter(); - mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext()); - - setHasStableIds(true); + mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, + Localization.getPreferredLocale(context)); } public void setEntries(@NonNull Collection historyEntries) { @@ -53,11 +51,6 @@ public abstract class HistoryEntryAdapter historyItemClickListener = onHistoryItemClickListener; - if(historyItemClickListener != null) { - historyItemClickListener.onHistoryItemClick(entry); - } + holder.itemView.setOnClickListener(v -> { + if(onHistoryItemClickListener != null) { + onHistoryItemClickListener.onHistoryItemClick(entry); } }); + + holder.itemView.setOnLongClickListener(view -> { + if (onHistoryItemClickListener != null) { + onHistoryItemClickListener.onHistoryItemLongClick(entry); + return true; + } + return false; + }); + onBindViewHolder(holder, entry, position); } @@ -94,13 +92,8 @@ public abstract class HistoryEntryAdapter { - void onHistoryItemClick(E historyItem); + public interface OnHistoryItemClickListener { + void onHistoryItemClick(E item); + void onHistoryItemLongClick(E item); } } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index c64689775..462c12e61 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -2,7 +2,6 @@ package org.schabi.newpipe.history; import android.content.SharedPreferences; -import android.graphics.Color; import android.os.Bundle; import android.os.Parcelable; import android.preference.PreferenceManager; @@ -12,34 +11,31 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.model.HistoryEntry; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import icepick.State; -import io.reactivex.Observer; +import io.reactivex.Flowable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; +import io.reactivex.disposables.CompositeDisposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public abstract class HistoryFragment extends BaseFragment +public abstract class HistoryFragment extends BaseFragment implements HistoryEntryAdapter.OnHistoryItemClickListener { private SharedPreferences mSharedPreferences; @@ -54,12 +50,11 @@ public abstract class HistoryFragment extends BaseFragme Parcelable mRecyclerViewState; private RecyclerView mRecyclerView; private HistoryEntryAdapter mHistoryAdapter; - private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback; - // private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; - private HistoryDAO mHistoryDataSource; - private PublishSubject> mHistoryEntryDeleteSubject; - private PublishSubject> mHistoryEntryInsertSubject; + private Subscription historySubscription; + + protected HistoryRecordManager historyRecordManager; + protected CompositeDisposable disposables; @StringRes abstract int getEnabledConfigKey(); @@ -77,88 +72,47 @@ public abstract class HistoryFragment extends BaseFragme // Register history enabled listener mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); - mHistoryDataSource = createHistoryDAO(); - - mHistoryEntryDeleteSubject = PublishSubject.create(); - mHistoryEntryDeleteSubject - .observeOn(Schedulers.io()) - .subscribe(new Consumer>() { - @Override - public void accept(Collection historyEntries) throws Exception { - mHistoryDataSource.delete(historyEntries); - } - }); - - mHistoryEntryInsertSubject = PublishSubject.create(); - mHistoryEntryInsertSubject - .observeOn(Schedulers.io()) - .subscribe(new Consumer>() { - @Override - public void accept(Collection historyEntries) throws Exception { - mHistoryDataSource.insertAll(historyEntries); - } - }); - - - } - - protected void historyItemSwipeCallback(int swipeDirection) { - mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, swipeDirection) { - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { - if (mHistoryAdapter != null) { - final E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition()); - mHistoryEntryDeleteSubject.onNext(Collections.singletonList(historyEntry)); - - View view = getActivity().findViewById(R.id.main_content); - if (view == null) view = mRecyclerView.getRootView(); - - Snackbar.make(view, R.string.item_deleted, 5 * 1000) - .setActionTextColor(Color.WHITE) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - mHistoryEntryInsertSubject.onNext(Collections.singletonList(historyEntry)); - } - }).show(); - } - } - }; + historyRecordManager = new HistoryRecordManager(getContext()); + disposables = new CompositeDisposable(); } @NonNull protected abstract HistoryEntryAdapter createAdapter(); + protected abstract Single> insert(final Collection entries); + + protected abstract Single delete(final Collection entries); + + @NonNull + protected abstract Flowable> getAll(); + @Override public void onResume() { super.onResume(); - mHistoryDataSource.getAll() - .toObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getHistoryListConsumer()); - boolean newEnabled = isHistoryEnabled(); + + getAll().observeOn(AndroidSchedulers.mainThread()).subscribe(getHistorySubscriber()); + + final boolean newEnabled = isHistoryEnabled(); if (newEnabled != mHistoryIsEnabled) { onHistoryIsEnabledChanged(newEnabled); } } @NonNull - private Observer> getHistoryListConsumer() { - return new Observer>() { + private Subscriber> getHistorySubscriber() { + return new Subscriber>() { @Override - public void onSubscribe(@NonNull Disposable d) { + public void onSubscribe(Subscription s) { + if (historySubscription != null) historySubscription.cancel(); + historySubscription = s; + historySubscription.request(1); } @Override - public void onNext(@NonNull List historyEntries) { - if (!historyEntries.isEmpty()) { - mHistoryAdapter.setEntries(historyEntries); + public void onNext(List entries) { + if (!entries.isEmpty()) { + mHistoryAdapter.setEntries(entries); animateView(mEmptyHistoryView, false, 200); if (mRecyclerViewState != null) { @@ -169,11 +123,13 @@ public abstract class HistoryFragment extends BaseFragme mHistoryAdapter.clear(); showEmptyHistory(); } + + if (historySubscription != null) historySubscription.request(1); } @Override - public void onError(@NonNull Throwable e) { - // TODO: error handling like in (see e.g. subscription fragment) + public void onError(Throwable t) { + } @Override @@ -192,30 +148,33 @@ public abstract class HistoryFragment extends BaseFragme */ @MainThread public void onHistoryCleared() { - final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState(); - final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); - mHistoryEntryDeleteSubject.onNext(itemsToDelete); + if (getContext() == null) return; + + new AlertDialog.Builder(getContext()) + .setTitle(R.string.delete_all) + .setMessage(R.string.delete_all_history_prompt) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.delete, (dialog, i) -> clearHistory()) + .show(); + } + + protected void makeSnackbar(@StringRes final int text) { + if (getActivity() == null) return; View view = getActivity().findViewById(R.id.main_content); if (view == null) view = mRecyclerView.getRootView(); + Snackbar.make(view, text, Snackbar.LENGTH_LONG).show(); + } - if (!itemsToDelete.isEmpty()) { - Snackbar.make(view, R.string.history_cleared, 5 * 1000) - .setActionTextColor(Color.WHITE) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - mRecyclerViewState = stateBeforeClear; - mHistoryEntryInsertSubject.onNext(itemsToDelete); - } - }).show(); - } else { - Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show(); - } + private void clearHistory() { + final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); + disposables.add(delete(itemsToDelete).observeOn(AndroidSchedulers.mainThread()) + .subscribe()); + makeSnackbar(R.string.history_cleared); mHistoryAdapter.clear(); showEmptyHistory(); - } private void showEmptyHistory() { @@ -227,18 +186,18 @@ public abstract class HistoryFragment extends BaseFragme @Nullable @CallSuper @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_history, container, false); mRecyclerView = rootView.findViewById(R.id.history_view); - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), + LinearLayoutManager.VERTICAL, false); mRecyclerView.setLayoutManager(layoutManager); mHistoryAdapter = createAdapter(); mHistoryAdapter.setOnHistoryItemClickListener(this); mRecyclerView.setAdapter(mHistoryAdapter); - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mHistoryItemSwipeCallback); - itemTouchHelper.attachToRecyclerView(mRecyclerView); mDisabledView = rootView.findViewById(R.id.history_disabled_view); mEmptyHistoryView = rootView.findViewById(R.id.history_empty); @@ -260,7 +219,7 @@ public abstract class HistoryFragment extends BaseFragme mSharedPreferences = null; mHistoryIsEnabledChangeListener = null; mHistoryIsEnabledKey = null; - mHistoryDataSource = null; + if (disposables != null) disposables.dispose(); } @Override @@ -290,15 +249,8 @@ public abstract class HistoryFragment extends BaseFragme } } - /** - * Creates a new history DAO - * - * @return the history DAO - */ - @NonNull - protected abstract HistoryDAO createHistoryDAO(); - - private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { + private class HistoryIsEnabledChangeListener + implements SharedPreferences.OnSharedPreferenceChangeListener { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(mHistoryIsEnabledKey)) { diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java new file mode 100644 index 000000000..1a5fe0525 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -0,0 +1,147 @@ +package org.schabi.newpipe.history; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; +import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.dao.StreamDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +public class HistoryRecordManager { + + private final AppDatabase database; + private final StreamDAO streamTable; + private final StreamHistoryDAO streamHistoryTable; + private final SearchHistoryDAO searchHistoryTable; + private final SharedPreferences sharedPreferences; + private final String searchHistoryKey; + private final String streamHistoryKey; + + public HistoryRecordManager(final Context context) { + database = NewPipeDatabase.getInstance(context); + streamTable = database.streamDAO(); + streamHistoryTable = database.streamHistoryDAO(); + searchHistoryTable = database.searchHistoryDAO(); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + searchHistoryKey = context.getString(R.string.enable_search_history_key); + streamHistoryKey = context.getString(R.string.enable_watch_history_key); + } + + public Maybe onViewed(final StreamInfo info) { + if (!isStreamHistoryEnabled()) return Maybe.empty(); + + final Date currentTime = new Date(); + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + })).subscribeOn(Schedulers.io()); + } + + public Single deleteStreamHistory(final long streamId) { + return Single.fromCallable(() -> streamHistoryTable.deleteStreamHistory(streamId)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getStreamHistory() { + return streamHistoryTable.getHistory().subscribeOn(Schedulers.io()); + } + + public Flowable> getStreamStatistics() { + return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); + } + + public Single> insertStreamHistory(final Collection entries) { + List entities = new ArrayList<>(entries.size()); + for (final StreamHistoryEntry entry : entries) { + entities.add(entry.toStreamHistoryEntity()); + } + return Single.fromCallable(() -> streamHistoryTable.insertAll(entities)) + .subscribeOn(Schedulers.io()); + } + + public Single deleteStreamHistory(final Collection entries) { + List entities = new ArrayList<>(entries.size()); + for (final StreamHistoryEntry entry : entries) { + entities.add(entry.toStreamHistoryEntity()); + } + return Single.fromCallable(() -> streamHistoryTable.delete(entities)) + .subscribeOn(Schedulers.io()); + } + + private boolean isStreamHistoryEnabled() { + return sharedPreferences.getBoolean(streamHistoryKey, false); + } + + /////////////////////////////////////////////////////// + // Search History + /////////////////////////////////////////////////////// + + public Single> insertSearches(final Collection entries) { + return Single.fromCallable(() -> searchHistoryTable.insertAll(entries)) + .subscribeOn(Schedulers.io()); + } + + public Single deleteSearches(final Collection entries) { + return Single.fromCallable(() -> searchHistoryTable.delete(entries)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getSearchHistory() { + return searchHistoryTable.getAll(); + } + + public Maybe onSearched(final int serviceId, final String search) { + if (!isSearchHistoryEnabled()) return Maybe.empty(); + + final Date currentTime = new Date(); + final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search); + + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry(); + if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) { + latestEntry.setCreationDate(currentTime); + return (long) searchHistoryTable.update(latestEntry); + } else { + return searchHistoryTable.insert(newEntry); + } + })).subscribeOn(Schedulers.io()); + } + + public Single deleteSearchHistory(final String search) { + return Single.fromCallable(() -> searchHistoryTable.deleteAllWhereQuery(search)) + .subscribeOn(Schedulers.io()); + } + + public Flowable> getRelatedSearches(final String query, + final int similarQueryLimit, + final int uniqueQueryLimit) { + return query.length() > 0 + ? searchHistoryTable.getSimilarEntries(query, similarQueryLimit) + : searchHistoryTable.getUniqueEntries(uniqueQueryLimit); + } + + private boolean isSearchHistoryEnabled() { + return sharedPreferences.getBoolean(searchHistoryKey, false); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java index 91e2cecff..a8bba0573 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -5,22 +5,27 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.util.NavigationHelper; -public class SearchHistoryFragment extends HistoryFragment { +import java.util.Collection; +import java.util.Collections; +import java.util.List; - private static int allowedSwipeToDeleteDirections = ItemTouchHelper.RIGHT; +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; + +public class SearchHistoryFragment extends HistoryFragment { @NonNull public static SearchHistoryFragment newInstance() { @@ -30,7 +35,6 @@ public class SearchHistoryFragment extends HistoryFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - historyItemSwipeCallback(allowedSwipeToDeleteDirections); } @NonNull @@ -39,21 +43,58 @@ public class SearchHistoryFragment extends HistoryFragment { return new SearchHistoryAdapter(getContext()); } + @Override + protected Single> insert(Collection entries) { + return historyRecordManager.insertSearches(entries); + } + + @Override + protected Single delete(Collection entries) { + return historyRecordManager.deleteSearches(entries); + } + + @NonNull + @Override + protected Flowable> getAll() { + return historyRecordManager.getSearchHistory(); + } + @StringRes @Override int getEnabledConfigKey() { return R.string.enable_search_history_key; } - @NonNull @Override - protected HistoryDAO createHistoryDAO() { - return NewPipeDatabase.getInstance().searchHistoryDAO(); + public void onHistoryItemClick(final SearchHistoryEntry historyItem) { + NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), + historyItem.getSearch()); } @Override - public void onHistoryItemClick(SearchHistoryEntry historyItem) { - NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch()); + public void onHistoryItemLongClick(final SearchHistoryEntry item) { + if (activity == null) return; + + new AlertDialog.Builder(activity) + .setTitle(item.getSearch()) + .setMessage(R.string.delete_item_search_history) + .setCancelable(true) + .setNeutralButton(R.string.cancel, null) + .setPositiveButton(R.string.delete_one, (dialog, i) -> { + final Single onDelete = historyRecordManager + .deleteSearches(Collections.singleton(item)) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDelete.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .setNegativeButton(R.string.delete_all, (dialog, i) -> { + final Single onDeleteAll = historyRecordManager + .deleteSearchHistory(item.getSearch()) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDeleteAll.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .show(); } private static class ViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java index d898bf353..026d5ee16 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -6,8 +6,8 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -16,18 +16,22 @@ import android.widget.TextView; import com.nostra13.universalimageloader.core.ImageLoader; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.history.dao.HistoryDAO; -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import java.util.Collection; +import java.util.Collections; +import java.util.List; -public class WatchedHistoryFragment extends HistoryFragment { +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; - private static int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT; + +public class WatchedHistoryFragment extends HistoryFragment { @NonNull public static WatchedHistoryFragment newInstance() { @@ -37,7 +41,6 @@ public class WatchedHistoryFragment extends HistoryFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - historyItemSwipeCallback(allowedSwipeToDeleteDirections); } @StringRes @@ -48,27 +51,59 @@ public class WatchedHistoryFragment extends HistoryFragment { @NonNull @Override - protected WatchedHistoryAdapter createAdapter() { - return new WatchedHistoryAdapter(getContext()); + protected StreamHistoryAdapter createAdapter() { + return new StreamHistoryAdapter(getContext()); + } + + @Override + protected Single> insert(Collection entries) { + return historyRecordManager.insertStreamHistory(entries); + } + + @Override + protected Single delete(Collection entries) { + return historyRecordManager.deleteStreamHistory(entries); } @NonNull @Override - protected HistoryDAO createHistoryDAO() { - return NewPipeDatabase.getInstance().watchHistoryDAO(); + protected Flowable> getAll() { + return historyRecordManager.getStreamHistory(); } @Override - public void onHistoryItemClick(WatchHistoryEntry historyItem) { - NavigationHelper.openVideoDetail(getContext(), - historyItem.getServiceId(), - historyItem.getUrl(), - historyItem.getTitle()); + public void onHistoryItemClick(StreamHistoryEntry historyItem) { + NavigationHelper.openVideoDetail(getContext(), historyItem.serviceId, historyItem.url, + historyItem.title); } - private static class WatchedHistoryAdapter extends HistoryEntryAdapter { + @Override + public void onHistoryItemLongClick(StreamHistoryEntry item) { + new AlertDialog.Builder(activity) + .setTitle(item.title) + .setMessage(R.string.delete_stream_history_prompt) + .setCancelable(true) + .setNeutralButton(R.string.cancel, null) + .setPositiveButton(R.string.delete_one, (dialog, i) -> { + final Single onDelete = historyRecordManager + .deleteStreamHistory(Collections.singleton(item)) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDelete.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .setNegativeButton(R.string.delete_all, (dialog, i) -> { + final Single onDeleteAll = historyRecordManager + .deleteStreamHistory(item.streamId) + .observeOn(AndroidSchedulers.mainThread()); + disposables.add(onDeleteAll.subscribe()); + makeSnackbar(R.string.item_deleted); + }) + .show(); + } - public WatchedHistoryAdapter(Context context) { + private static class StreamHistoryAdapter extends HistoryEntryAdapter { + + StreamHistoryAdapter(Context context) { super(context); } @@ -87,13 +122,13 @@ public class WatchedHistoryFragment extends HistoryFragment { } @Override - void onBindViewHolder(ViewHolder holder, WatchHistoryEntry entry, int position) { - holder.date.setText(getFormattedDate(entry.getCreationDate())); - holder.streamTitle.setText(entry.getTitle()); - holder.uploader.setText(entry.getUploader()); - holder.duration.setText(Localization.getDurationString(entry.getDuration())); - ImageLoader.getInstance() - .displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) { + holder.date.setText(getFormattedDate(entry.accessDate)); + holder.streamTitle.setText(entry.title); + holder.uploader.setText(entry.uploader); + holder.duration.setText(Localization.getDurationString(entry.duration)); + ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, + StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index a481b3335..8260adc6e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -61,10 +61,9 @@ import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.fragments.local.StreamRecordManager; +import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; @@ -150,7 +149,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; - protected StreamRecordManager recordManager; + protected HistoryRecordManager recordManager; //////////////////////////////////////////////////////////////////////////*/ @@ -176,9 +175,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void initPlayer() { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); - if (recordManager == null) { - recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context)); - } + if (recordManager == null) recordManager = new HistoryRecordManager(context); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); databaseUpdateReactor = new CompositeDisposable(); @@ -614,7 +611,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // If the user selects a new track, then the discontinuity occurs after the index is changed. // Therefore, the only source that causes a discrepancy would be gapless transition, // which can only offset the current track by +1. - if (newWindowIndex == playQueue.getIndex() + 1) { + if (newWindowIndex == playQueue.getIndex() + 1 || + (newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) { playQueue.offsetIndex(+1); } playbackManager.load(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 161cf1735..fcfe5c4a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,6 +229,8 @@ Play Create Delete + Delete One + Delete All Checksum @@ -307,6 +309,8 @@ History cleared Item deleted Do you want to delete this item from search history? + Do you want to delete this item from watch history? + Are you sure you want to delete all items from history? Watch History Most Played From 17d77aa31ffd3be0d9a6d31f66d8a47d42180f13 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 26 Jan 2018 21:45:48 -0800 Subject: [PATCH 033/276] -Removed watch history table. -Added migration for dropping watch history table. --- .../java/org/schabi/newpipe/database/AppDatabase.java | 10 +++------- .../java/org/schabi/newpipe/database/Migrations.java | 2 ++ .../newpipe/database/history/dao/StreamHistoryDAO.java | 10 +++++++++- 3 files changed, 14 insertions(+), 8 deletions(-) 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 7097dd4a7..086e1bed0 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -5,18 +5,16 @@ import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.TypeConverters; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; @@ -26,7 +24,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; @TypeConverters({Converters.class}) @Database( entities = { - SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class, + SubscriptionEntity.class, SearchHistoryEntry.class, StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, PlaylistEntity.class, PlaylistStreamEntity.class }, @@ -39,8 +37,6 @@ public abstract class AppDatabase extends RoomDatabase { public abstract SubscriptionDAO subscriptionDAO(); - public abstract WatchHistoryDAO watchHistoryDAO(); - public abstract SearchHistoryDAO searchHistoryDAO(); public abstract StreamDAO streamDAO(); 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 b977e43e9..825ec5fd5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -51,6 +51,8 @@ public class Migrations { "ON watch_history.service_id == streams.service_id " + "AND watch_history.url == streams.url " + "ORDER BY creation_date DESC"); + + database.execSQL("DROP TABLE IF EXISTS watch_history"); } }; } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 64003910e..fe19d362e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.database.history.dao; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.support.annotation.Nullable; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; @@ -22,7 +23,14 @@ import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STRE import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Dao -public abstract class StreamHistoryDAO implements BasicDAO { +public abstract class StreamHistoryDAO implements HistoryDAO { + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + + " WHERE " + STREAM_ACCESS_DATE + " = " + + "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")") + @Override + @Nullable + public abstract StreamHistoryEntity getLatestEntry(); + @Override @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) public abstract Flowable> getAll(); From 84c5d274169b853f821c152ce93b41ef64b53b07 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 27 Jan 2018 22:14:38 -0800 Subject: [PATCH 034/276] -Revamped local items to display more information such as service name, etc. -Enabled reordering, renaming, removing of items on playlist fragment. -Enabled removal of dangling streams entries when history is cleared. -Changed playlist append menu item to icon on service player activity. -Added adapter and builder for local items, removed dependency on infoitem and existing infolist for database entry items. -Removed watch history entity and DAO. -Extracted info item selected listener to remove adding boilerplate code when long click functionality is optional. -Fixed query returning no record on left join when right table is empty. --- .../schabi/newpipe/database/LocalItem.java | 11 + .../database/history/dao/WatchHistoryDAO.java | 37 --- .../history/model/WatchHistoryEntry.java | 109 -------- .../playlist/PlaylistMetadataEntry.java | 8 +- .../playlist/PlaylistStreamEntry.java | 60 +++++ .../playlist/dao/PlaylistStreamDAO.java | 19 +- .../stream/StreamStatisticsEntry.java | 17 +- .../database/stream/model/StreamEntity.java | 11 - .../newpipe/fragments/MainFragment.java | 2 +- .../local/BaseLocalListFragment.java | 184 +++++++++++++ .../fragments/local/HeaderFooterHolder.java | 13 + .../fragments/local/LocalItemBuilder.java | 56 ++++ .../fragments/local/LocalItemListAdapter.java | 243 ++++++++++++++++++ .../local/LocalPlaylistFragment.java | 221 +++++++++++----- .../fragments/local/LocalPlaylistManager.java | 3 +- .../fragments/local/MostPlayedFragment.java | 35 --- .../fragments/local/OnCustomItemGesture.java | 19 ++ .../fragments/local/WatchHistoryFragment.java | 36 --- .../{ => bookmark}/BookmarkFragment.java | 92 +++---- .../local/bookmark/MostPlayedFragment.java | 22 ++ .../StatisticsPlaylistFragment.java | 78 +++--- .../local/bookmark/WatchHistoryFragment.java | 21 ++ .../local/holder/LocalItemHolder.java | 56 ++++ .../local/holder/LocalPlaylistItemHolder.java | 74 ++++++ .../LocalPlaylistStreamItemHolder.java} | 53 ++-- .../LocalStatisticStreamItemHolder.java | 119 +++++++++ .../newpipe/history/HistoryFragment.java | 11 +- .../newpipe/history/HistoryRecordManager.java | 8 + .../newpipe/info_list/InfoListAdapter.java | 4 - .../newpipe/info_list/OnInfoItemGesture.java | 6 - .../stored/StreamEntityInfoItem.java | 18 -- .../stored/StreamStatisticsInfoItem.java | 31 --- .../org/schabi/newpipe/util/Localization.java | 27 ++ .../schabi/newpipe/util/NavigationHelper.java | 4 +- .../res/layout/list_stream_playlist_item.xml | 2 +- .../main/res/layout/local_playlist_header.xml | 9 +- app/src/main/res/menu/menu_play_queue.xml | 8 +- app/src/main/res/values/strings.xml | 3 + 38 files changed, 1224 insertions(+), 506 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/LocalItem.java delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => bookmark}/BookmarkFragment.java (74%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => bookmark}/StatisticsPlaylistFragment.java (80%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java rename app/src/main/java/org/schabi/newpipe/{info_list/holder/StreamPlaylistInfoItemHolder.java => fragments/local/holder/LocalPlaylistStreamItemHolder.java} (58%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java new file mode 100644 index 000000000..95d0d9213 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java @@ -0,0 +1,11 @@ +package org.schabi.newpipe.database; + +public interface LocalItem { + enum LocalItemType { + PLAYLIST_ITEM, + PLAYLIST_STREAM_ITEM, + STATISTIC_STREAM_ITEM + } + + LocalItemType getLocalItemType(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java deleted file mode 100644 index a01d8e46d..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import android.arch.persistence.room.Dao; -import android.arch.persistence.room.Query; - -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; - -import java.util.List; - -import io.reactivex.Flowable; - -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.CREATION_DATE; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.ID; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.SERVICE_ID; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.TABLE_NAME; - -@Dao -public interface WatchHistoryDAO extends HistoryDAO { - - String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; - - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") - @Override - WatchHistoryEntry getLatestEntry(); - - @Query("DELETE FROM " + TABLE_NAME) - @Override - int deleteAll(); - - @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) - @Override - Flowable> getAll(); - - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) - @Override - Flowable> listByService(int serviceId); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java deleted file mode 100644 index bfd84d377..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.schabi.newpipe.database.history.model; - -import android.arch.persistence.room.ColumnInfo; -import android.arch.persistence.room.Entity; -import android.arch.persistence.room.Ignore; - -import org.schabi.newpipe.extractor.stream.StreamInfo; - -import java.util.Date; - -@Entity(tableName = WatchHistoryEntry.TABLE_NAME) -public class WatchHistoryEntry extends HistoryEntry { - - public static final String TABLE_NAME = "watch_history"; - public static final String TITLE = "title"; - public static final String URL = "url"; - public static final String STREAM_ID = "stream_id"; - public static final String THUMBNAIL_URL = "thumbnail_url"; - public static final String UPLOADER = "uploader"; - public static final String DURATION = "duration"; - - @ColumnInfo(name = TITLE) - private String title; - - @ColumnInfo(name = URL) - private String url; - - @ColumnInfo(name = STREAM_ID) - private String streamId; - - @ColumnInfo(name = THUMBNAIL_URL) - private String thumbnailURL; - - @ColumnInfo(name = UPLOADER) - private String uploader; - - @ColumnInfo(name = DURATION) - private long duration; - - public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long duration) { - super(creationDate, serviceId); - this.title = title; - this.url = url; - this.streamId = streamId; - this.thumbnailURL = thumbnailURL; - this.uploader = uploader; - this.duration = duration; - } - - public WatchHistoryEntry(StreamInfo streamInfo) { - this(new Date(), streamInfo.getServiceId(), streamInfo.getName(), streamInfo.getUrl(), - streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, streamInfo.duration); - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getStreamId() { - return streamId; - } - - public void setStreamId(String streamId) { - this.streamId = streamId; - } - - public String getThumbnailURL() { - return thumbnailURL; - } - - public void setThumbnailURL(String thumbnailURL) { - this.thumbnailURL = thumbnailURL; - } - - public String getUploader() { - return uploader; - } - - public void setUploader(String uploader) { - this.uploader = uploader; - } - - public long getDuration() { - return duration; - } - - public void setDuration(int duration) { - this.duration = duration; - } - - @Ignore - @Override - public boolean hasEqualValues(HistoryEntry otherEntry) { - return otherEntry instanceof WatchHistoryEntry && super.hasEqualValues(otherEntry) - && getUrl().equals(((WatchHistoryEntry) otherEntry).getUrl()); - } -} 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 53ae3d48a..2daea298b 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,13 +2,14 @@ package org.schabi.newpipe.database.playlist; import android.arch.persistence.room.ColumnInfo; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; 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; -public class PlaylistMetadataEntry { +public class PlaylistMetadataEntry implements LocalItem { final public static String PLAYLIST_STREAM_COUNT = "streamCount"; @ColumnInfo(name = PLAYLIST_ID) @@ -33,4 +34,9 @@ public class PlaylistMetadataEntry { storedPlaylistInfoItem.setStreamCount(streamCount); return storedPlaylistInfoItem; } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.PLAYLIST_ITEM; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java new file mode 100644 index 000000000..b6ecfe1f0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java @@ -0,0 +1,60 @@ +package org.schabi.newpipe.database.playlist; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; + +public class PlaylistStreamEntry implements LocalItem { + @ColumnInfo(name = StreamEntity.STREAM_ID) + final public long uid; + @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) + final public int serviceId; + @ColumnInfo(name = StreamEntity.STREAM_URL) + final public String url; + @ColumnInfo(name = StreamEntity.STREAM_TITLE) + final public String title; + @ColumnInfo(name = StreamEntity.STREAM_TYPE) + final public StreamType streamType; + @ColumnInfo(name = StreamEntity.STREAM_DURATION) + final public long duration; + @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) + final public String uploader; + @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) + final public long streamId; + @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) + final public int joinIndex; + + public PlaylistStreamEntry(long uid, int serviceId, String url, String title, + StreamType streamType, long duration, String uploader, + String thumbnailUrl, long streamId, int joinIndex) { + this.uid = uid; + this.serviceId = serviceId; + this.url = url; + this.title = title; + this.streamType = streamType; + this.duration = duration; + this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl; + this.streamId = streamId; + this.joinIndex = joinIndex; + } + + public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { + StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); + item.setThumbnailUrl(thumbnailUrl); + item.setUploaderName(uploader); + item.setDuration(duration); + return item; + } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.PLAYLIST_STREAM_ITEM; + } +} 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 b9f325aa2..2d645e793 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 @@ -6,6 +6,7 @@ import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -37,17 +38,13 @@ public abstract class PlaylistStreamDAO implements BasicDAO getMaximumIndexOf(final long playlistId); @Transaction - @Query("SELECT " + STREAM_ID + ", " + STREAM_SERVICE_ID + ", " + STREAM_URL + ", " + - STREAM_TITLE + ", " + STREAM_TYPE + ", " + STREAM_UPLOADER + ", " + - STREAM_DURATION + ", " + STREAM_THUMBNAIL_URL + - - " FROM " + STREAM_TABLE + " INNER JOIN " + + @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + // get ids of streams of the given playlist "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE " @@ -56,14 +53,16 @@ public abstract class PlaylistStreamDAO implements BasicDAO> getOrderedStreamsOf(long playlistId); + public abstract Flowable> getOrderedStreamsOf(long playlistId); @Transaction @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + - PLAYLIST_THUMBNAIL_URL + ", COUNT(*) AS " + PLAYLIST_STREAM_COUNT + + PLAYLIST_THUMBNAIL_URL + ", " + + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + - " FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + - " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + PLAYLIST_STREAM_JOIN_TABLE + "." + JOIN_PLAYLIST_ID + + " FROM " + PLAYLIST_TABLE + + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + " GROUP BY " + JOIN_PLAYLIST_ID) public abstract Flowable> getPlaylistMetadata(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java index 1c2a7028e..6909f3397 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -2,14 +2,15 @@ package org.schabi.newpipe.database.stream; import android.arch.persistence.room.ColumnInfo; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import java.util.Date; -public class StreamStatisticsEntry { +public class StreamStatisticsEntry implements LocalItem { final public static String STREAM_LATEST_DATE = "latestAccess"; final public static String STREAM_WATCH_COUNT = "watchCount"; @@ -53,14 +54,16 @@ public class StreamStatisticsEntry { this.watchCount = watchCount; } - public StreamStatisticsInfoItem toStreamStatisticsInfoItem() { - StreamStatisticsInfoItem item = - new StreamStatisticsInfoItem(uid, serviceId, url, title, streamType); + public StreamInfoItem toStreamInfoItem() { + StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); item.setDuration(duration); item.setUploaderName(uploader); item.setThumbnailUrl(thumbnailUrl); - item.setLatestAccessDate(latestAccessDate); - item.setWatchCount(watchCount); return item; } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.STATISTIC_STREAM_ITEM; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index eb078a03c..2fddaa1bb 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -9,7 +9,6 @@ import android.arch.persistence.room.PrimaryKey; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.stored.StreamEntityInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.Constants; @@ -88,16 +87,6 @@ public class StreamEntity implements Serializable { item.getThumbnailUrl(), item.getUploader(), item.getDuration()); } - @Ignore - public StreamEntityInfoItem toStreamEntityInfoItem() throws IllegalArgumentException { - StreamEntityInfoItem item = new StreamEntityInfoItem(getUid(), getServiceId(), - getUrl(), getTitle(), getStreamType()); - item.setThumbnailUrl(getThumbnailUrl()); - item.setUploaderName(getUploader()); - item.setDuration(getDuration()); - return item; - } - public long getUid() { return uid; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index fc4f9a323..fc4913114 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -29,7 +29,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskList; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; -import org.schabi.newpipe.fragments.local.BookmarkFragment; +import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment; import org.schabi.newpipe.fragments.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java new file mode 100644 index 000000000..afc67aa68 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java @@ -0,0 +1,184 @@ +package org.schabi.newpipe.fragments.local; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.list.ListViewContract; +import org.schabi.newpipe.util.StateSaver; + +import java.util.List; +import java.util.Queue; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public abstract class BaseLocalListFragment extends BaseStateFragment + implements ListViewContract, StateSaver.WriteRead { + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + protected LocalItemListAdapter itemListAdapter; + protected RecyclerView itemsList; + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + itemListAdapter = new LocalItemListAdapter(activity); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onDestroy() { + super.onDestroy(); + StateSaver.onDestroy(savedState); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + protected StateSaver.SavedState savedState; + + @Override + public String generateSuffix() { + // Naive solution, but it's good for now (the items don't change) + return "." + itemListAdapter.getItemsList().size() + ".list"; + } + + @Override + public void writeTo(Queue objectsToSave) { + objectsToSave.add(itemListAdapter.getItemsList()); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + itemListAdapter.getItemsList().clear(); + itemListAdapter.getItemsList().addAll((List) savedObjects.poll()); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle bundle) { + super.onRestoreInstanceState(bundle); + savedState = StateSaver.tryToRestore(bundle, this); + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + protected View getListHeader() { + return null; + } + + protected View getListFooter() { + return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); + } + + protected RecyclerView.LayoutManager getListLayoutManager() { + return new LinearLayoutManager(activity); + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + itemsList = rootView.findViewById(R.id.items_list); + itemsList.setLayoutManager(getListLayoutManager()); + + itemListAdapter.setFooter(getListFooter()); + itemListAdapter.setHeader(getListHeader()); + + itemsList.setAdapter(itemListAdapter); + } + + @Override + protected void initListeners() { + super.initListeners(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + super.onCreateOptionsMenu(menu, inflater); + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + if(useAsFrontPage) { + supportActionBar.setDisplayHomeAsUpEnabled(false); + } else { + supportActionBar.setDisplayHomeAsUpEnabled(true); + } + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + // animateView(itemsList, false, 400); + } + + @Override + public void hideLoading() { + super.hideLoading(); + animateView(itemsList, true, 300); + } + + @Override + public void showError(String message, boolean showRetryButton) { + super.showError(message, showRetryButton); + showListFooter(false); + animateView(itemsList, false, 200); + } + + @Override + public void showEmptyState() { + super.showEmptyState(); + showListFooter(false); + } + + @Override + public void showListFooter(final boolean show) { + itemsList.post(() -> itemListAdapter.showFooter(show)); + } + + @Override + public void handleNextItems(N result) { + isLoading.set(false); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java new file mode 100644 index 000000000..3c0830751 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java @@ -0,0 +1,13 @@ +package org.schabi.newpipe.fragments.local; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class HeaderFooterHolder extends RecyclerView.ViewHolder { + public View view; + + public HeaderFooterHolder(View v) { + super(v); + view = v; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java new file mode 100644 index 000000000..d31c85712 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -0,0 +1,56 @@ +package org.schabi.newpipe.fragments.local; + +import android.content.Context; + +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.database.LocalItem; + +/* + * Created by Christian Schabesberger on 26.09.16. + *

+ * Copyright (C) Christian Schabesberger 2016 + * InfoItemBuilder.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 LocalItemBuilder { + private static final String TAG = LocalItemBuilder.class.toString(); + + private final Context context; + private ImageLoader imageLoader = ImageLoader.getInstance(); + + private OnCustomItemGesture onSelectedListener; + + public LocalItemBuilder(Context context) { + this.context = context; + } + + public Context getContext() { + return context; + } + + public ImageLoader getImageLoader() { + return imageLoader; + } + + public OnCustomItemGesture getOnItemSelectedListener() { + return onSelectedListener; + } + + public void setOnItemSelectedListener(OnCustomItemGesture listener) { + this.onSelectedListener = listener; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java new file mode 100644 index 000000000..9c621a55c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -0,0 +1,243 @@ +package org.schabi.newpipe.fragments.local; + +import android.app.Activity; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.local.holder.LocalItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder; +import org.schabi.newpipe.util.Localization; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/* + * Created by Christian Schabesberger on 01.08.16. + * + * Copyright (C) Christian Schabesberger 2016 + * InfoListAdapter.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 LocalItemListAdapter extends RecyclerView.Adapter { + + private static final String TAG = LocalItemListAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final int HEADER_TYPE = 0; + private static final int FOOTER_TYPE = 1; + + private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000; + private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001; + private static final int PLAYLIST_HOLDER_TYPE = 0x2000; + + private final LocalItemBuilder localItemBuilder; + private final ArrayList localItems; + private final DateFormat dateFormat; + + private boolean showFooter = false; + private View header = null; + private View footer = null; + + + public LocalItemListAdapter(Activity activity) { + localItemBuilder = new LocalItemBuilder(activity); + localItems = new ArrayList<>(); + dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, + Localization.getPreferredLocale(activity)); + } + + public void setSelectedListener(OnCustomItemGesture listener) { + localItemBuilder.setOnItemSelectedListener(listener); + } + + public void addInfoItemList(List data) { + if (data != null) { + if (DEBUG) { + Log.d(TAG, "addInfoItemList() before > localItems.size() = " + + localItems.size() + ", data.size() = " + data.size()); + } + + int offsetStart = sizeConsideringHeader(); + localItems.addAll(data); + + if (DEBUG) { + Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } + + notifyItemRangeInserted(offsetStart, data.size()); + + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeader(); + notifyItemMoved(offsetStart, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + + " to " + footerNow); + } + } + } + + public void addInfoItem(LocalItem data) { + addInfoItemList(Collections.singletonList(data)); + } + + public void removeItemAt(final int infoListPosition) { + if (infoListPosition < 0 || infoListPosition >= localItems.size()) return; + + localItems.remove(infoListPosition); + + notifyItemRemoved(infoListPosition + (header != null ? 1 : 0)); + } + + public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) { + final int actualFrom = offsetWithoutHeader(fromAdapterPosition); + final int actualTo = offsetWithoutHeader(toAdapterPosition); + + if (actualFrom < 0 || actualTo < 0) return false; + if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false; + + localItems.add(actualTo, localItems.remove(actualFrom)); + notifyItemMoved(fromAdapterPosition, toAdapterPosition); + return true; + } + + public void clearStreamItemList() { + if (localItems.isEmpty()) { + return; + } + localItems.clear(); + notifyDataSetChanged(); + } + + public void setHeader(View header) { + boolean changed = header != this.header; + this.header = header; + if (changed) notifyDataSetChanged(); + } + + public void setFooter(View view) { + this.footer = view; + } + + public void showFooter(boolean show) { + if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]"); + if (show == showFooter) return; + + showFooter = show; + if (show) notifyItemInserted(sizeConsideringHeader()); + else notifyItemRemoved(sizeConsideringHeader()); + } + + private int offsetWithoutHeader(final int offset) { + return offset - (header != null ? 1 : 0); + } + + private int sizeConsideringHeader() { + return localItems.size() + (header != null ? 1 : 0); + } + + public ArrayList getItemsList() { + return localItems; + } + + @Override + public int getItemCount() { + int count = localItems.size(); + if (header != null) count++; + if (footer != null && showFooter) count++; + + if (DEBUG) { + Log.d(TAG, "getItemCount() called, count = " + count + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } + return count; + } + + @Override + public int getItemViewType(int position) { + if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]"); + + if (header != null && position == 0) { + return HEADER_TYPE; + } else if (header != null) { + position--; + } + if (footer != null && position == localItems.size() && showFooter) { + return FOOTER_TYPE; + } + final LocalItem item = localItems.get(position); + + switch (item.getLocalItemType()) { + case PLAYLIST_ITEM: return PLAYLIST_HOLDER_TYPE; + case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE; + case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE; + default: + Log.e(TAG, "No holder type has been considered for item: [" + + item.getLocalItemType() + "]"); + return -1; + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { + if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + + parent + "], type = [" + type + "]"); + switch (type) { + case HEADER_TYPE: + return new HeaderFooterHolder(header); + case FOOTER_TYPE: + return new HeaderFooterHolder(footer); + case PLAYLIST_HOLDER_TYPE: + return new LocalPlaylistItemHolder(localItemBuilder, parent); + case STREAM_PLAYLIST_HOLDER_TYPE: + return new LocalPlaylistStreamItemHolder(localItemBuilder, parent); + case STREAM_STATISTICS_HOLDER_TYPE: + return new LocalStatisticStreamItemHolder(localItemBuilder, parent); + default: + Log.e(TAG, "No view type has been considered for holder: [" + type + "]"); + return null; + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + + holder.getClass().getSimpleName() + "], position = [" + position + "]"); + + if (holder instanceof LocalItemHolder) { + // If header isn't null, offset the items by -1 + if (header != null) position--; + + ((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat); + } else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) { + ((HeaderFooterHolder) holder).view = header; + } else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader() + && footer != null && showFooter) { + ((HeaderFooterHolder) holder).view = footer; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 7ba5db7e1..2c4af25d9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -7,25 +7,29 @@ import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import java.util.ArrayList; @@ -37,7 +41,7 @@ import io.reactivex.disposables.CompositeDisposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class LocalPlaylistFragment extends BaseListFragment, Void> { +public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { private View headerRootLayout; private TextView headerTitleView; @@ -49,12 +53,14 @@ public class LocalPlaylistFragment extends BaseListFragment, private View headerBackgroundButton; @State - protected long playlistId; + protected Long playlistId; @State protected String name; @State protected Parcelable itemsListState; + private ItemTouchHelper itemTouchHelper; + /* Used for independent events */ private CompositeDisposable disposables = new CompositeDisposable(); private Subscription databaseSubscription; @@ -86,6 +92,9 @@ public class LocalPlaylistFragment extends BaseListFragment, @Override public void onPause() { super.onPause(); + + saveJoin(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); } @@ -115,8 +124,6 @@ public class LocalPlaylistFragment extends BaseListFragment, @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - infoListAdapter.useMiniItemVariants(true); - setFragmentTitle(name); } @@ -141,44 +148,61 @@ public class LocalPlaylistFragment extends BaseListFragment, protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(itemsList); + + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(StreamInfoItem selectedItem) { - // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openVideoDetailFragment(getFragmentManager(), - selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + public void selected(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistStreamEntry) { + final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem; + NavigationHelper.openVideoDetailFragment(getFragmentManager(), + item.serviceId, item.url, item.title); + } } @Override - public void held(StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistStreamEntry) { + showStreamDialog((PlaylistStreamEntry) selectedItem); + } + } + + @Override + public void drag(LocalItem selectedItem, RecyclerView.ViewHolder viewHolder) { + if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); } }); - + headerTitleView.setOnClickListener(view -> createRenameDialog()); } - @Override - protected void showStreamDialog(final StreamInfoItem item) { + protected void showStreamDialog(final PlaylistStreamEntry item) { final Context context = getContext(); final Activity activity = getActivity(); if (context == null || context.getResources() == null || getActivity() == null) return; + final StreamInfoItem infoItem = item.toStreamInfoItem(); + final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_popup), context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), + "Set as Thumbnail", + context.getResources().getString(R.string.delete) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, + new SinglePlayQueue(infoItem)); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new + SinglePlayQueue(infoItem)); break; case 2: NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); @@ -189,18 +213,56 @@ public class LocalPlaylistFragment extends BaseListFragment, case 4: NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); break; + case 5: + changeThumbnailUrl(item.thumbnailUrl); + break; + case 6: + itemListAdapter.removeItemAt(index); + setVideoCount(itemListAdapter.getItemsList().size()); + break; default: break; } }; - new InfoItemDialog(getActivity(), item, commands, actions).show(); + new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.ACTION_STATE_IDLE) { + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() || + itemListAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + return itemListAdapter.swapItems(sourceIndex, targetIndex); + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} + }; } private void resetFragment() { if (disposables != null) disposables.clear(); if (databaseSubscription != null) databaseSubscription.cancel(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -224,8 +286,8 @@ public class LocalPlaylistFragment extends BaseListFragment, .subscribe(getPlaylistObserver()); } - private Subscriber> getPlaylistObserver() { - return new Subscriber>() { + private Subscriber> getPlaylistObserver() { + return new Subscriber>() { @Override public void onSubscribe(Subscription s) { showLoading(); @@ -236,7 +298,7 @@ public class LocalPlaylistFragment extends BaseListFragment, } @Override - public void onNext(List streams) { + public void onNext(List streams) { handleResult(streams); if (databaseSubscription != null) databaseSubscription.request(1); } @@ -253,9 +315,9 @@ public class LocalPlaylistFragment extends BaseListFragment, } @Override - public void handleResult(@NonNull List result) { + public void handleResult(@NonNull List result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); @@ -265,15 +327,14 @@ public class LocalPlaylistFragment extends BaseListFragment, animateView(headerRootLayout, true, 100); animateView(itemsList, true, 300); - infoListAdapter.addInfoItemList(getStreamItems(result)); + itemListAdapter.addInfoItemList(result); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; } + setVideoCount(itemListAdapter.getItemsList().size()); playlistControl.setVisibility(View.VISIBLE); - headerStreamCount.setText( - getResources().getQuantityString(R.plurals.videos, result.size(), result.size())); headerPlayAllButton.setOnClickListener(view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); @@ -284,29 +345,6 @@ public class LocalPlaylistFragment extends BaseListFragment, hideLoading(); } - - private List getStreamItems(final List streams) { - List items = new ArrayList<>(streams.size()); - for (final StreamEntity stream : streams) { - items.add(stream.toStreamEntityInfoItem()); - } - return items; - } - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void loadMoreItems() { - // Do nothing - } - - @Override - protected boolean hasMoreItems() { - return false; - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @@ -325,13 +363,13 @@ public class LocalPlaylistFragment extends BaseListFragment, // Utils //////////////////////////////////////////////////////////////////////////*/ - protected void setInitialData(long playlistId, String name) { + private void setInitialData(long playlistId, String name) { this.playlistId = playlistId; this.name = !TextUtils.isEmpty(name) ? name : ""; } - protected void setFragmentTitle(final String title) { - if (activity.getSupportActionBar() != null) { + private void setFragmentTitle(final String title) { + if (activity != null && activity.getSupportActionBar() != null) { activity.getSupportActionBar().setTitle(title); } if (headerTitleView != null) { @@ -339,17 +377,80 @@ public class LocalPlaylistFragment extends BaseListFragment, } } + private void setVideoCount(final long count) { + if (activity != null && headerStreamCount != null) { + headerStreamCount.setText(Localization.localizeStreamCount(activity, count)); + } + } + private PlayQueue getPlayQueue() { return getPlayQueue(0); } private PlayQueue getPlayQueue(final int index) { - final List infoItems = infoListAdapter.getItemsList(); + final List infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); - for (final InfoItem item : infoItems) { - if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + for (final LocalItem item : infoItems) { + if (item instanceof PlaylistStreamEntry) { + streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem()); + } } return new SinglePlayQueue(streamInfoItems, index); } + + private void createRenameDialog() { + if (playlistId == null || name == null || getContext() == null) return; + + final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); + EditText nameEdit = dialogView.findViewById(R.id.playlist_name); + nameEdit.setText(name); + nameEdit.setSelection(nameEdit.getText().length()); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.rename_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> { + name = nameEdit.getText().toString(); + setFragmentTitle(name); + + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + final Toast successToast = Toast.makeText(getActivity(), + "Playlist renamed", + Toast.LENGTH_SHORT); + + playlistManager.renamePlaylist(playlistId, name) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> successToast.show()); + }); + + dialogBuilder.show(); + } + + private void changeThumbnailUrl(final String thumbnailUrl) { + final Toast successToast = Toast.makeText(getActivity(), + "Playlist thumbnail changed", + Toast.LENGTH_SHORT); + + playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignore -> successToast.show()); + } + + private void saveJoin() { + final List items = itemListAdapter.getItemsList(); + List streamIds = new ArrayList<>(items.size()); + for (final LocalItem item : items) { + if (item instanceof PlaylistStreamEntry) { + streamIds.add(((PlaylistStreamEntry) item).streamId); + } + } + + playlistManager.updateJoin(playlistId, streamIds) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 4bc161c04..c266f5365 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -4,6 +4,7 @@ import android.support.annotation.Nullable; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; @@ -84,7 +85,7 @@ public class LocalPlaylistManager { return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); } - public Flowable> getPlaylistStreams(final long playlistId) { + public Flowable> getPlaylistStreams(final long playlistId) { return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java deleted file mode 100644 index 7862cf2f4..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class MostPlayedFragment extends StatisticsPlaylistFragment { - @Override - protected String getName() { - return getString(R.string.title_most_played); - } - - @Override - protected List processResult(List results) { - Collections.sort(results, (left, right) -> - ((Long) right.watchCount).compareTo(left.watchCount)); - - List items = new ArrayList<>(results.size()); - for (final StreamStatisticsEntry stream : results) { - items.add(stream.toStreamStatisticsInfoItem()); - } - return items; - } - - @Override - protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) { - final int watchCount = (int) infoItem.getWatchCount(); - return getResources().getQuantityString(R.plurals.views, watchCount, watchCount); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java b/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java new file mode 100644 index 000000000..0b65c595a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java @@ -0,0 +1,19 @@ +package org.schabi.newpipe.fragments.local; + +import android.support.v7.widget.RecyclerView; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.extractor.InfoItem; + +public abstract class OnCustomItemGesture { + + public abstract void selected(T selectedItem); + + public void held(T selectedItem) { + // Optional gesture + } + + public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { + // Optional gesture + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java deleted file mode 100644 index 2a4b8cfb0..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import android.text.format.DateFormat; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class WatchHistoryFragment extends StatisticsPlaylistFragment { - @Override - protected String getName() { - return getString(R.string.title_watch_history); - } - - @Override - protected List processResult(List results) { - Collections.sort(results, (left, right) -> - right.latestAccessDate.compareTo(left.latestAccessDate)); - - List items = new ArrayList<>(results.size()); - for (final StreamStatisticsEntry stream : results) { - items.add(stream.toStreamStatisticsInfoItem()); - } - return items; - } - - @Override - protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) { - return DateFormat.getLongDateFormat(getContext()).format(infoItem.getLatestAccessDate()); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java similarity index 74% rename from app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 769365dd8..01b433052 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -1,7 +1,7 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; +import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; @@ -17,18 +17,15 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; -import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; +import org.schabi.newpipe.fragments.local.LocalItemListAdapter; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; +import org.schabi.newpipe.fragments.local.OnCustomItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -42,7 +39,7 @@ public class BookmarkFragment extends BaseStateFragment() { + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(PlaylistInfoItem selectedItem) { + public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement - if (selectedItem instanceof LocalPlaylistInfoItem && getParentFragment() != null) { - final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); - + if (selectedItem instanceof PlaylistMetadataEntry && getParentFragment() != null) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); NavigationHelper.openLocalPlaylistFragment( - getParentFragment().getFragmentManager(), - playlistId, - selectedItem.getName() - ); + getParentFragment().getFragmentManager(), entry.uid, entry.name); } } @Override - public void held(PlaylistInfoItem selectedItem) { - if (selectedItem instanceof LocalPlaylistInfoItem) { - showPlaylistDialog((LocalPlaylistInfoItem) selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistMetadataEntry) { + showDeleteDialog((PlaylistMetadataEntry) selectedItem); } } }); @@ -177,36 +168,25 @@ public class BookmarkFragment extends BaseStateFragment { - switch (i) { - case 0: + private void showDeleteDialog(final PlaylistMetadataEntry item) { + new AlertDialog.Builder(activity) + .setTitle(item.name) + .setMessage(R.string.delete_playlist_prompt) + .setCancelable(true) + .setPositiveButton(R.string.delete, (dialog, i) -> { final Toast deleteSuccessful = Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT); - disposables.add(localPlaylistManager.deletePlaylist(item.getPlaylistId()) + disposables.add(localPlaylistManager.deletePlaylist(item.uid) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> deleteSuccessful.show())); - break; - default: - break; - } - }; - - final String videoCount = getResources().getQuantityString(R.plurals.videos, - (int) item.getStreamCount(), (int) item.getStreamCount()); - new InfoItemDialog(getActivity(), commands, actions, item.getName(), videoCount).show(); + }) + .setNegativeButton(R.string.cancel, null) + .show(); } private void resetFragment() { if (disposables != null) disposables.clear(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -254,12 +234,12 @@ public class BookmarkFragment extends BaseStateFragment result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); } else { - infoListAdapter.addInfoItemList(infoItemsOf(result)); + itemListAdapter.addInfoItemList(infoItemsOf(result)); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; @@ -269,13 +249,9 @@ public class BookmarkFragment extends BaseStateFragment infoItemsOf(List playlists) { - List playlistInfoItems = new ArrayList<>(playlists.size()); - for (final PlaylistMetadataEntry playlist : playlists) { - playlistInfoItems.add(playlist.toStoredPlaylistInfoItem()); - } - Collections.sort(playlistInfoItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); - return playlistInfoItems; + private List infoItemsOf(List playlists) { + Collections.sort(playlists, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); + return playlists; } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java new file mode 100644 index 000000000..ed0d903a8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java @@ -0,0 +1,22 @@ +package org.schabi.newpipe.fragments.local.bookmark; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; + +import java.util.Collections; +import java.util.List; + +public class MostPlayedFragment extends StatisticsPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_most_played); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + ((Long) right.watchCount).compareTo(left.watchCount)); + return results; + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java similarity index 80% rename from app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 6eddc3a5c..5c2959d9c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; import android.app.Activity; import android.content.Context; @@ -14,14 +14,13 @@ import android.view.ViewGroup; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.fragments.local.BaseLocalListFragment; +import org.schabi.newpipe.fragments.local.OnCustomItemGesture; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.OnInfoItemGesture; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; @@ -36,7 +35,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; public abstract class StatisticsPlaylistFragment - extends BaseListFragment, Void> { + extends BaseLocalListFragment, Void> { private View headerRootLayout; private View playlistControl; @@ -57,9 +56,7 @@ public abstract class StatisticsPlaylistFragment protected abstract String getName(); - protected abstract List processResult(final List results); - - protected abstract String getAdditionalDetail(final StreamStatisticsInfoItem infoItem); + protected abstract List processResult(final List results); /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle @@ -106,8 +103,6 @@ public abstract class StatisticsPlaylistFragment @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - infoListAdapter.useMiniItemVariants(true); - setFragmentTitle(getName()); } @@ -127,27 +122,31 @@ public abstract class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(StreamInfoItem selectedItem) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), - selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + public void selected(LocalItem selectedItem) { + if (selectedItem instanceof StreamStatisticsEntry) { + final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; + NavigationHelper.openVideoDetailFragment(getFragmentManager(), + item.serviceId, item.url, item.title); + } } @Override - public void held(StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof StreamStatisticsEntry) { + showStreamDialog((StreamStatisticsEntry) selectedItem); + } } }); } - @Override - protected void showStreamDialog(final StreamInfoItem item) { + private void showStreamDialog(final StreamStatisticsEntry item) { final Context context = getContext(); final Activity activity = getActivity(); - if (context == null || context.getResources() == null - || getActivity() == null || !(item instanceof StreamStatisticsInfoItem)) return; + if (context == null || context.getResources() == null || getActivity() == null) return; + final StreamInfoItem infoItem = item.toStreamInfoItem(); final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), @@ -158,13 +157,13 @@ public abstract class StatisticsPlaylistFragment }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem)); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem)); break; case 2: NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); @@ -180,13 +179,12 @@ public abstract class StatisticsPlaylistFragment } }; - final String detail = getAdditionalDetail((StreamStatisticsInfoItem) item); - new InfoItemDialog(getActivity(), commands, actions, item.getName(), detail).show(); + new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); } private void resetFragment() { if (databaseSubscription != null) databaseSubscription.cancel(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -241,7 +239,7 @@ public abstract class StatisticsPlaylistFragment @Override public void handleResult(@NonNull List result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); @@ -251,7 +249,7 @@ public abstract class StatisticsPlaylistFragment animateView(headerRootLayout, true, 100); animateView(itemsList, true, 300); - infoListAdapter.addInfoItemList(processResult(result)); + itemListAdapter.addInfoItemList(processResult(result)); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; @@ -267,20 +265,6 @@ public abstract class StatisticsPlaylistFragment hideLoading(); } - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void loadMoreItems() { - // Do nothing - } - - @Override - protected boolean hasMoreItems() { - return false; - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @@ -310,10 +294,12 @@ public abstract class StatisticsPlaylistFragment } private PlayQueue getPlayQueue(final int index) { - final List infoItems = infoListAdapter.getItemsList(); + final List infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); - for (final InfoItem item : infoItems) { - if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + for (final LocalItem item : infoItems) { + if (item instanceof StreamStatisticsEntry) { + streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem()); + } } return new SinglePlayQueue(streamInfoItems, index); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java new file mode 100644 index 000000000..853029ae6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java @@ -0,0 +1,21 @@ +package org.schabi.newpipe.fragments.local.bookmark; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; + +import java.util.Collections; +import java.util.List; + +public class WatchHistoryFragment extends StatisticsPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_watch_history); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + right.latestAccessDate.compareTo(left.latestAccessDate)); + return results; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java new file mode 100644 index 000000000..64dc84472 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java @@ -0,0 +1,56 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; + +import java.text.DateFormat; + +/* + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2016 + * InfoItemHolder.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 abstract class LocalItemHolder extends RecyclerView.ViewHolder { + protected final LocalItemBuilder itemBuilder; + + public LocalItemHolder(LocalItemBuilder itemBuilder, int layoutId, ViewGroup parent) { + super(LayoutInflater.from(itemBuilder.getContext()) + .inflate(layoutId, parent, false)); + this.itemBuilder = itemBuilder; + } + + public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); + + /*////////////////////////////////////////////////////////////////////////// + // ImageLoaderOptions + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Base display options + */ + public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = + new DisplayImageOptions.Builder() + .cacheInMemory(true) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java new file mode 100644 index 000000000..d04fc123a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java @@ -0,0 +1,74 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; + +import java.text.DateFormat; + +public class LocalPlaylistItemHolder extends LocalItemHolder { + public final ImageView itemThumbnailView; + public final TextView itemStreamCountView; + public final TextView itemTitleView; + public final TextView itemUploaderView; + + public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, + int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemTitleView = itemView.findViewById(R.id.itemTitleView); + itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + } + + public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistMetadataEntry)) return; + final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; + + itemTitleView.setText(item.name); + itemStreamCountView.setText(String.valueOf(item.streamCount)); + itemUploaderView.setVisibility(View.INVISIBLE); + + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); + } + return true; + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java similarity index 58% rename from app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java index 8261d4760..712db8f8a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,7 +1,6 @@ -package org.schabi.newpipe.info_list.holder; +package org.schabi.newpipe.fragments.local.holder; import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -11,40 +10,44 @@ import android.widget.TextView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.util.Localization; -public class StreamPlaylistInfoItemHolder extends InfoItemHolder { +import java.text.DateFormat; + +public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public final ImageView itemThumbnailView; public final TextView itemVideoTitleView; - public final TextView itemUploaderView; + public final TextView itemAdditionalDetailsView; public final TextView itemDurationView; public final View itemHandleView; - StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); - itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemHandleView = itemView.findViewById(R.id.itemHandle); } - public StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_stream_playlist_item, parent); } @Override - public void updateFromItem(final InfoItem infoItem) { - if (!(infoItem instanceof StreamInfoItem)) return; - final StreamInfoItem item = (StreamInfoItem) infoItem; + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistStreamEntry)) return; + final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - itemVideoTitleView.setText(item.getName()); - itemUploaderView.setText(item.uploader_name); + itemVideoTitleView.setText(item.title); + itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader, + NewPipe.getNameOfService(item.serviceId))); if (item.duration > 0) { itemDurationView.setText(Localization.getDurationString(item.duration)); @@ -56,19 +59,19 @@ public class StreamPlaylistInfoItemHolder extends InfoItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.getImageLoader().displayImage(item.thumbnail_url, itemThumbnailView, - StreamPlaylistInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + LocalPlaylistStreamItemHolder.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { - if (itemBuilder.getOnStreamSelectedListener() != null) { - itemBuilder.getOnStreamSelectedListener().selected(item); + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); } }); itemView.setLongClickable(true); itemView.setOnLongClickListener(view -> { - if (itemBuilder.getOnStreamSelectedListener() != null) { - itemBuilder.getOnStreamSelectedListener().held(item); + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); } return true; }); @@ -77,13 +80,13 @@ public class StreamPlaylistInfoItemHolder extends InfoItemHolder { itemHandleView.setOnTouchListener(getOnTouchListener(item)); } - private View.OnTouchListener getOnTouchListener(final StreamInfoItem item) { + private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) { return (view, motionEvent) -> { view.performClick(); if (itemBuilder != null && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - itemBuilder.getOnStreamSelectedListener() - .drag(item, StreamPlaylistInfoItemHolder.this); + itemBuilder.getOnItemSelectedListener().drag(item, + LocalPlaylistStreamItemHolder.this); } return false; }; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java new file mode 100644 index 000000000..bce6bab76 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java @@ -0,0 +1,119 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.support.v4.content.ContextCompat; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.Localization; + +import java.text.DateFormat; + +/* + * Created by Christian Schabesberger on 01.08.16. + *

+ * Copyright (C) Christian Schabesberger 2016 + * StreamInfoItemHolder.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 LocalStatisticStreamItemHolder extends LocalItemHolder { + + public final ImageView itemThumbnailView; + public final TextView itemVideoTitleView; + public final TextView itemUploaderView; + public final TextView itemDurationView; + public final TextView itemAdditionalDetails; + + LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemDurationView = itemView.findViewById(R.id.itemDurationView); + itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); + } + + public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_stream_item, parent); + } + + private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, + final DateFormat dateFormat) { + final String watchCount = Localization.shortViewCount(itemBuilder.getContext(), + entry.watchCount); + final String uploadDate = dateFormat.format(entry.latestAccessDate); + final String serviceName = NewPipe.getNameOfService(entry.serviceId); + return Localization.concatenateStrings(watchCount, uploadDate, serviceName); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof StreamStatisticsEntry)) return; + final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; + + itemVideoTitleView.setText(item.title); + itemUploaderView.setText(item.uploader); + + if (item.duration > 0) { + itemDurationView.setText(Localization.getDurationString(item.duration)); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + R.color.duration_background_color)); + itemDurationView.setVisibility(View.VISIBLE); + } else { + itemDurationView.setVisibility(View.GONE); + } + + itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); + + // Default thumbnail is shown on error, while loading and if the url is empty + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); + } + return true; + }); + } + + /** + * Display options for stream thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnFail(R.drawable.dummy_thumbnail) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnLoading(R.drawable.dummy_thumbnail) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index 462c12e61..5369c657c 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -32,6 +32,7 @@ import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -169,8 +170,14 @@ public abstract class HistoryFragment extends BaseFragment private void clearHistory() { final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); - disposables.add(delete(itemsToDelete).observeOn(AndroidSchedulers.mainThread()) - .subscribe()); + + final Disposable deletion = delete(itemsToDelete) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + final Disposable cleanUp = historyRecordManager.removeOrphanedRecords() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + disposables.addAll(deletion, cleanUp); makeSnackbar(R.string.history_cleared); mHistoryAdapter.clear(); diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java index 1a5fe0525..9d9b74b30 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -48,6 +48,14 @@ public class HistoryRecordManager { streamHistoryKey = context.getString(R.string.enable_watch_history_key); } + public Single removeOrphanedRecords() { + return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); + } + + /////////////////////////////////////////////////////// + // Watch History + /////////////////////////////////////////////////////// + public Maybe onViewed(final StreamInfo info) { if (!isStreamHistoryEnabled()) return Maybe.empty(); 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 1dc4442c7..dbf5d7556 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 @@ -55,10 +55,6 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; private boolean useMiniVariant = false; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java b/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java index 3e6fe2213..84634c1d9 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.info_list; -import android.support.v7.widget.RecyclerView; - import org.schabi.newpipe.extractor.InfoItem; public abstract class OnInfoItemGesture { @@ -11,8 +9,4 @@ public abstract class OnInfoItemGesture { public void held(T selectedItem) { // Optional gesture } - - public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { - // Optional gesture - } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java deleted file mode 100644 index a54135211..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.schabi.newpipe.info_list.stored; - -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; - -public class StreamEntityInfoItem extends StreamInfoItem { - protected final long streamId; - - public StreamEntityInfoItem(final long streamId, final int serviceId, - final String url, final String name, final StreamType type) { - super(serviceId, url, name, type); - this.streamId = streamId; - } - - public long getStreamId() { - return streamId; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java deleted file mode 100644 index 6659b551a..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.schabi.newpipe.info_list.stored; - -import org.schabi.newpipe.extractor.stream.StreamType; - -import java.util.Date; - -public final class StreamStatisticsInfoItem extends StreamEntityInfoItem { - private Date latestAccessDate; - private long watchCount; - - public StreamStatisticsInfoItem(final long streamId, final int serviceId, - final String url, final String name, final StreamType type) { - super(streamId, serviceId, url, name, type); - } - - public Date getLatestAccessDate() { - return latestAccessDate; - } - - public void setLatestAccessDate(Date latestAccessDate) { - this.latestAccessDate = latestAccessDate; - } - - public long getWatchCount() { - return watchCount; - } - - public void setWatchCount(long watchCount) { - this.watchCount = watchCount; - } -} 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 43ebc1677..c1e5c9ed4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.annotation.PluralsRes; import android.support.annotation.StringRes; import android.text.TextUtils; @@ -14,7 +15,9 @@ import java.text.DateFormat; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Locale; /* @@ -39,9 +42,33 @@ import java.util.Locale; public class Localization { + public final static String DOT_SEPARATOR = " • "; + private Localization() { } + @NonNull + public static String concatenateStrings(final String... strings) { + return concatenateStrings(Arrays.asList(strings)); + } + + @NonNull + public static String concatenateStrings(final List strings) { + if (strings.isEmpty()) return ""; + + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(strings.get(0)); + + for (int i = 1; i < strings.size(); i++) { + final String string = strings.get(i); + if (!TextUtils.isEmpty(string)) { + stringBuilder.append(DOT_SEPARATOR).append(strings.get(i)); + } + } + + return stringBuilder.toString(); + } + public static Locale getPreferredLocale(Context context) { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 7ffbf07ed..3acfb6683 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -35,8 +35,8 @@ import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; -import org.schabi.newpipe.fragments.local.MostPlayedFragment; -import org.schabi.newpipe.fragments.local.WatchHistoryFragment; +import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; +import org.schabi.newpipe.fragments.local.bookmark.WatchHistoryFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; diff --git a/app/src/main/res/layout/list_stream_playlist_item.xml b/app/src/main/res/layout/list_stream_playlist_item.xml index 3a5b1b8e6..193b3fea4 100644 --- a/app/src/main/res/layout/list_stream_playlist_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_item.xml @@ -70,7 +70,7 @@ tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique..."/> + tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..." /> - + android:visible="true" + app:showAsAction="ifRoom"/> Delete One Delete All Checksum + Dismiss New mission @@ -380,6 +381,8 @@ Create New Playlist Delete Playlist + Rename Playlist Name Add To Playlist + Do you want to delete this playlist? From d31eeac49e1aef25b0ca506fe64cc32db813a54a Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 28 Jan 2018 18:26:19 -0800 Subject: [PATCH 035/276] -Condensed repeating entries on stream history. -Changed search history to show service name and stream history to show repeat count. -Removed history entry abstract and unused info items. --- .../schabi/newpipe/database/Migrations.java | 6 +- .../history/dao/SearchHistoryDAO.java | 5 +- .../history/dao/StreamHistoryDAO.java | 3 +- .../database/history/model/HistoryEntry.java | 60 ------------------- .../history/model/SearchHistoryEntry.java | 50 ++++++++++++++-- .../history/model/StreamHistoryEntity.java | 21 ++++++- .../history/model/StreamHistoryEntry.java | 13 +++- .../playlist/PlaylistMetadataEntry.java | 8 --- .../fragments/local/LocalItemBuilder.java | 6 +- .../fragments/local/LocalItemListAdapter.java | 2 +- .../local/LocalPlaylistFragment.java | 2 +- ...emGesture.java => OnLocalItemGesture.java} | 2 +- .../fragments/local/PlaylistAppendDialog.java | 26 +++----- .../local/bookmark/BookmarkFragment.java | 4 +- .../bookmark/StatisticsPlaylistFragment.java | 4 +- .../newpipe/history/HistoryEntryAdapter.java | 7 +++ .../newpipe/history/HistoryRecordManager.java | 11 +++- .../history/SearchHistoryFragment.java | 12 +++- .../history/WatchedHistoryFragment.java | 15 ++++- .../stored/LocalPlaylistInfoItem.java | 20 ------- .../org/schabi/newpipe/util/Constants.java | 1 - .../main/res/layout/item_search_history.xml | 2 +- 22 files changed, 140 insertions(+), 140 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/model/HistoryEntry.java rename app/src/main/java/org/schabi/newpipe/fragments/local/{OnCustomItemGesture.java => OnLocalItemGesture.java} (86%) delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java 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 825ec5fd5..fdfd04a84 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -23,7 +23,7 @@ public class Migrations { database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`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 )"); database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)"); database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`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 )"); database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)"); @@ -45,8 +45,8 @@ public class Migrations { // Once the streams have PKs, join them with the normalized history table // and populate it with the remaining data from watch history - database.execSQL("INSERT INTO stream_history (stream_id, access_date)" + - "SELECT uid, creation_date " + + database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" + + "SELECT uid, creation_date, 1 " + "FROM watch_history INNER JOIN streams " + "ON watch_history.service_id == streams.service_id " + "AND watch_history.url == streams.url " + diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index 257c1ec3d..b0a3c3a3c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -4,6 +4,7 @@ import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; import android.support.annotation.Nullable; +import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import java.util.List; @@ -21,8 +22,8 @@ public interface SearchHistoryDAO extends HistoryDAO { String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") - @Override + @Query("SELECT * FROM " + TABLE_NAME + + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") @Nullable SearchHistoryEntry getLatestEntry(); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index fe19d362e..fd7a1b96f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -14,6 +14,7 @@ import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; @@ -59,7 +60,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO onSelectedListener; + private OnLocalItemGesture onSelectedListener; public LocalItemBuilder(Context context) { this.context = context; @@ -46,11 +46,11 @@ public class LocalItemBuilder { return imageLoader; } - public OnCustomItemGesture getOnItemSelectedListener() { + public OnLocalItemGesture getOnItemSelectedListener() { return onSelectedListener; } - public void setOnItemSelectedListener(OnCustomItemGesture listener) { + public void setOnItemSelectedListener(OnLocalItemGesture listener) { this.onSelectedListener = listener; } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 9c621a55c..af1a0f666 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -66,7 +66,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter listener) { + public void setSelectedListener(OnLocalItemGesture listener) { localItemBuilder.setOnItemSelectedListener(listener); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 2c4af25d9..d54a4b4ae 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -151,7 +151,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { + itemListAdapter.setSelectedListener(new OnLocalItemGesture() { @Override public void selected(LocalItem selectedItem) { if (selectedItem instanceof PlaylistStreamEntry) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java b/app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java similarity index 86% rename from app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java index 0b65c595a..5cede4c67 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java @@ -5,7 +5,7 @@ import android.support.v7.widget.RecyclerView; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.extractor.InfoItem; -public abstract class OnCustomItemGesture { +public abstract class OnLocalItemGesture { public abstract void selected(T selectedItem); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 6ed357e36..302a37002 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -13,15 +13,11 @@ import android.widget.Toast; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; -import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; import java.util.ArrayList; @@ -34,7 +30,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); private RecyclerView playlistRecyclerView; - private InfoListAdapter playlistAdapter; + private LocalItemListAdapter playlistAdapter; public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { PlaylistAppendDialog dialog = new PlaylistAppendDialog(); @@ -69,8 +65,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { @Override public void onAttach(Context context) { super.onAttach(context); - playlistAdapter = new InfoListAdapter(getActivity()); - playlistAdapter.useMiniItemVariants(true); + playlistAdapter = new LocalItemListAdapter(getActivity()); } /*////////////////////////////////////////////////////////////////////////// @@ -97,13 +92,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog { newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); - playlistAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { + playlistAdapter.setSelectedListener(new OnLocalItemGesture() { @Override - public void selected(PlaylistInfoItem selectedItem) { - if (!(selectedItem instanceof LocalPlaylistInfoItem) || getStreams() == null) + public void selected(LocalItem selectedItem) { + if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) return; - final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); + final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid; final Toast successToast = Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); @@ -123,13 +118,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog { return; } - List playlistInfoItems = new ArrayList<>(metadataEntries.size()); - for (final PlaylistMetadataEntry metadataEntry : metadataEntries) { - playlistInfoItems.add(metadataEntry.toStoredPlaylistInfoItem()); - } - playlistAdapter.clearStreamItemList(); - playlistAdapter.addInfoItemList(playlistInfoItems); + playlistAdapter.addInfoItemList(metadataEntries); playlistRecyclerView.setVisibility(View.VISIBLE); }); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 01b433052..0bd0fa00f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -22,7 +22,7 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.local.LocalItemListAdapter; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; -import org.schabi.newpipe.fragments.local.OnCustomItemGesture; +import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; @@ -136,7 +136,7 @@ public class BookmarkFragment extends BaseStateFragment() { + itemListAdapter.setSelectedListener(new OnLocalItemGesture() { @Override public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 5c2959d9c..cb2d671cc 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -18,7 +18,7 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.local.BaseLocalListFragment; -import org.schabi.newpipe.fragments.local.OnCustomItemGesture; +import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; @@ -122,7 +122,7 @@ public abstract class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - itemListAdapter.setSelectedListener(new OnCustomItemGesture() { + itemListAdapter.setSelectedListener(new OnLocalItemGesture() { @Override public void selected(LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java index f61e8eb7d..4170a1139 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.history; import android.content.Context; +import android.content.res.Resources; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; @@ -22,11 +23,13 @@ public abstract class HistoryEntryAdapter private final ArrayList mEntries; private final DateFormat mDateFormat; + private final Context mContext; private OnHistoryItemClickListener onHistoryItemClickListener = null; public HistoryEntryAdapter(Context context) { super(); + mContext = context; mEntries = new ArrayList<>(); mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, Localization.getPreferredLocale(context)); @@ -51,6 +54,10 @@ public abstract class HistoryEntryAdapter return mDateFormat.format(date); } + protected String getFormattedViewString(final long viewCount) { + return Localization.shortViewCount(mContext, viewCount); + } + @Override public int getItemCount() { return mEntries.size(); diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java index 9d9b74b30..839b8c89b 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -62,7 +62,16 @@ public class HistoryRecordManager { final Date currentTime = new Date(); return Maybe.fromCallable(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); - return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(); + + if (latestEntry != null && latestEntry.getStreamUid() == streamId) { + streamHistoryTable.delete(latestEntry); + latestEntry.setAccessDate(currentTime); + latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); + return streamHistoryTable.insert(latestEntry); + } else { + return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + } })).subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java index a8bba0573..e40a79368 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -14,6 +14,8 @@ import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import java.util.Collection; @@ -99,12 +101,12 @@ public class SearchHistoryFragment extends HistoryFragment { private static class ViewHolder extends RecyclerView.ViewHolder { private final TextView search; - private final TextView time; + private final TextView info; public ViewHolder(View itemView) { super(itemView); search = itemView.findViewById(R.id.search); - time = itemView.findViewById(R.id.time); + info = itemView.findViewById(R.id.info); } } @@ -125,7 +127,11 @@ public class SearchHistoryFragment extends HistoryFragment { @Override void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) { holder.search.setText(entry.getSearch()); - holder.time.setText(getFormattedDate(entry.getCreationDate())); + + final String info = Localization.concatenateStrings( + getFormattedDate(entry.getCreationDate()), + NewPipe.getNameOfService(entry.getServiceId())); + holder.info.setText(info); } } } diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java index 026d5ee16..7913c9a28 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -123,7 +123,16 @@ public class WatchedHistoryFragment extends HistoryFragment @Override void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) { - holder.date.setText(getFormattedDate(entry.accessDate)); + final String formattedDate = getFormattedDate(entry.accessDate); + final String info; + if (entry.repeatCount > 1) { + info = Localization.concatenateStrings(formattedDate, + getFormattedViewString(entry.repeatCount)); + } else { + info = formattedDate; + } + + holder.info.setText(info); holder.streamTitle.setText(entry.title); holder.uploader.setText(entry.uploader); holder.duration.setText(Localization.getDurationString(entry.duration)); @@ -133,7 +142,7 @@ public class WatchedHistoryFragment extends HistoryFragment } private static class ViewHolder extends RecyclerView.ViewHolder { - private final TextView date; + private final TextView info; private final TextView streamTitle; private final ImageView thumbnailView; private final TextView uploader; @@ -142,7 +151,7 @@ public class WatchedHistoryFragment extends HistoryFragment public ViewHolder(View itemView) { super(itemView); thumbnailView = itemView.findViewById(R.id.itemThumbnailView); - date = itemView.findViewById(R.id.itemAdditionalDetails); + info = itemView.findViewById(R.id.itemAdditionalDetails); streamTitle = itemView.findViewById(R.id.itemVideoTitleView); uploader = itemView.findViewById(R.id.itemUploaderView); duration = itemView.findViewById(R.id.itemDurationView); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java deleted file mode 100644 index b0afe1948..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.schabi.newpipe.info_list.stored; - -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; - -import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; -import static org.schabi.newpipe.util.Constants.NO_URL; - -public final class LocalPlaylistInfoItem extends PlaylistInfoItem { - private final long playlistId; - - public LocalPlaylistInfoItem(final long playlistId, final String name) { - super(NO_SERVICE_ID, NO_URL, name); - - this.playlistId = playlistId; - } - - public long getPlaylistId() { - return playlistId; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java index 4238424d9..a6aec96e2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -12,5 +12,4 @@ public class Constants { public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; public static final int NO_SERVICE_ID = -1; - public static final String NO_URL = ""; } diff --git a/app/src/main/res/layout/item_search_history.xml b/app/src/main/res/layout/item_search_history.xml index a89882c0c..2c40ca1d1 100644 --- a/app/src/main/res/layout/item_search_history.xml +++ b/app/src/main/res/layout/item_search_history.xml @@ -13,7 +13,7 @@ android:paddingTop="8dp"> From 9b4a07de345eb76c8ecc1ab944614affc48227e4 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 28 Jan 2018 23:01:06 -0800 Subject: [PATCH 036/276] -Redone control panel in video detail fragment. -Added playlist append menu item to channel and playlist fragments. -Added debouncing to local playlist fragment to allow saving join when list is reordered or item is deleted. -Extracted hardcoded strings. --- .../fragments/detail/VideoDetailFragment.java | 1 + .../fragments/list/BaseListFragment.java | 18 ++ .../list/channel/ChannelFragment.java | 19 +- .../list/playlist/PlaylistFragment.java | 19 +- .../fragments/local/LocalItemListAdapter.java | 20 +- .../local/LocalPlaylistFragment.java | 46 ++- .../fragments/local/PlaylistAppendDialog.java | 4 +- .../local/PlaylistCreationDialog.java | 2 +- .../local/bookmark/BookmarkFragment.java | 4 +- .../bookmark/StatisticsPlaylistFragment.java | 2 +- app/src/main/res/layout/dialog_playlists.xml | 2 +- .../main/res/layout/fragment_video_detail.xml | 288 +++++++++--------- app/src/main/res/menu/menu_channel.xml | 8 + app/src/main/res/menu/menu_playlist.xml | 11 +- app/src/main/res/values/strings.xml | 7 + 15 files changed, 279 insertions(+), 172 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index b134bc98d..6907f3266 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1031,6 +1031,7 @@ public class VideoDetailFragment extends BaseStateFragment implement if (!TextUtils.isEmpty(info.getUploaderName())) { uploaderTextView.setText(info.getUploaderName()); uploaderTextView.setVisibility(View.VISIBLE); + uploaderTextView.setSelected(true); } else { uploaderTextView.setVisibility(View.GONE); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 9e4fe89ab..82b45c76e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -20,6 +20,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.OnInfoItemGesture; @@ -27,6 +28,7 @@ import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.StateSaver; +import java.util.ArrayList; import java.util.List; import java.util.Queue; @@ -283,4 +285,20 @@ public abstract class BaseListFragment extends BaseStateFragment implem public void handleNextItems(N result) { isLoading.set(false); } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected void appendToPlaylist(final android.support.v4.app.FragmentManager manager, + final String tag) { + if (infoListAdapter == null) return; + List streams = new ArrayList<>(); + for (final InfoItem item : infoListAdapter.getItemsList()) { + if (item instanceof StreamInfoItem) { + streams.add((StreamInfoItem) item); + } + } + PlaylistAppendDialog.fromStreamInfoItems(streams).show(manager, tag); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 1b24a5dce..641b26299 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -84,6 +84,7 @@ public class ChannelFragment extends BaseListInfoFragment { private LinearLayout headerBackgroundButton; private MenuItem menuRssButton; + private MenuItem playlistAppendButton; public static ChannelFragment getInstance(int serviceId, String url, String name) { ChannelFragment instance = new ChannelFragment(); @@ -194,17 +195,20 @@ public class ChannelFragment extends BaseListInfoFragment { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); - if(useAsFrontPage) { + if(useAsFrontPage && supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(false); } else { inflater.inflate(R.menu.menu_channel, menu); - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + + "], inflater = [" + inflater + "]"); menuRssButton = menu.findItem(R.id.menu_item_rss); + playlistAppendButton = menu.findItem(R.id.menu_append_playlist); + if (currentInfo != null) { menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); + playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty()); } - } } @@ -225,10 +229,12 @@ public class ChannelFragment extends BaseListInfoFragment { case R.id.menu_item_openInBrowser: openUrlInBrowser(url); break; - case R.id.menu_item_share: { + case R.id.menu_item_share: shareUrl(name, url); break; - } + case R.id.menu_append_playlist: + appendToPlaylist(getFragmentManager(), TAG); + break; default: return super.onOptionsItemSelected(item); } @@ -428,6 +434,9 @@ public class ChannelFragment extends BaseListInfoFragment { } else headerSubscribersTextView.setVisibility(View.GONE); if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); + if (playlistAppendButton != null) playlistAppendButton + .setVisible(!currentInfo.getRelatedStreams().isEmpty()); + playlistCtrl.setVisibility(View.VISIBLE); if (!result.errors.isEmpty()) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 52eeb337c..15255618b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -54,6 +54,8 @@ public class PlaylistFragment extends BaseListInfoFragment { private View headerPopupButton; private View headerBackgroundButton; + private MenuItem playlistAppendButton; + public static PlaylistFragment getInstance(int serviceId, String url, String name) { PlaylistFragment instance = new PlaylistFragment(); instance.setInitialData(serviceId, url, name); @@ -141,9 +143,15 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + + "], inflater = [" + inflater + "]"); super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_playlist, menu); + + playlistAppendButton = menu.findItem(R.id.menu_append_playlist); + if (currentInfo != null) { + playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty()); + } } /*////////////////////////////////////////////////////////////////////////// @@ -166,10 +174,12 @@ public class PlaylistFragment extends BaseListInfoFragment { case R.id.menu_item_openInBrowser: openUrlInBrowser(url); break; - case R.id.menu_item_share: { + case R.id.menu_item_share: shareUrl(name, url); break; - } + case R.id.menu_append_playlist: + appendToPlaylist(getFragmentManager(), TAG); + break; default: return super.onOptionsItemSelected(item); } @@ -215,6 +225,9 @@ public class PlaylistFragment extends BaseListInfoFragment { imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count)); + if (playlistAppendButton != null) playlistAppendButton + .setVisible(!currentInfo.getRelatedStreams().isEmpty()); + if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index af1a0f666..0e012aad7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -58,7 +58,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); @@ -99,21 +98,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size()) return; - - localItems.remove(infoListPosition); - - notifyItemRemoved(infoListPosition + (header != null ? 1 : 0)); + localItems.remove(index); + notifyItemRemoved(index + (header != null ? 1 : 0)); } public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) { - final int actualFrom = offsetWithoutHeader(fromAdapterPosition); - final int actualTo = offsetWithoutHeader(toAdapterPosition); + final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition); + final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition); if (actualFrom < 0 || actualTo < 0) return false; if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false; @@ -150,7 +144,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter, Void> { + private static final long SAVE_DEBOUNCE_MILLIS = 1000; + private View headerRootLayout; private TextView headerTitleView; private TextView headerStreamCount; @@ -66,6 +71,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment debouncedSaveSignal; + private Disposable debouncedSaver; + public static LocalPlaylistFragment getInstance(long playlistId, String name) { LocalPlaylistFragment instance = new LocalPlaylistFragment(); instance.setInitialData(playlistId, name); @@ -89,11 +97,22 @@ public class LocalPlaylistFragment extends BaseLocalListFragment successToast.show()); } + private void saveDebounced() { + debouncedSaveSignal.onNext(System.currentTimeMillis()); + } + + private Disposable getDebouncedSaver() { + return debouncedSaveSignal + .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .doOnDispose(this::saveJoin) + .subscribe(ignored -> saveJoin()); + } + private void saveJoin() { final List items = itemListAdapter.getItemsList(); List streamIds = new ArrayList<>(items.size()); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 302a37002..d4b6bd964 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -99,8 +99,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog { return; final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid; - final Toast successToast = - Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); + final Toast successToast = Toast.makeText(getContext(), + R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); playlistManager.appendToPlaylist(playlistId, getStreams()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 791e90fa2..670ae9819 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -48,7 +48,7 @@ public final class PlaylistCreationDialog extends PlaylistDialog { final LocalPlaylistManager playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); final Toast successToast = Toast.makeText(getActivity(), - "Playlist successfully created", + R.string.playlist_creation_success, Toast.LENGTH_SHORT); playlistManager.createPlaylist(name, getStreams()) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 0bd0fa00f..ce80bcf0d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -174,8 +174,8 @@ public class BookmarkFragment extends BaseStateFragment { - final Toast deleteSuccessful = - Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT); + final Toast deleteSuccessful = Toast.makeText(getContext(), + R.string.playlist_delete_success, Toast.LENGTH_SHORT); disposables.add(localPlaylistManager.deletePlaylist(item.uid) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> deleteSuccessful.show())); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index cb2d671cc..1a872f382 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -275,7 +275,7 @@ public abstract class StatisticsPlaylistFragment if (super.onError(exception)) return true; onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, - "none", "History", R.string.general_error); + "none", "History Statistics", R.string.general_error); return true; } diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml index 5abe91a8e..8c639fff6 100644 --- a/app/src/main/res/layout/dialog_playlists.xml +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -28,7 +28,7 @@ android:layout_height="50dp" android:layout_toRightOf="@+id/newPlaylistIcon" android:gravity="left|center" - android:text="Create New Playlist" + android:text="@string/create_playlist" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="15sp" android:textStyle="bold" diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index cf555ffa5..3861a380d 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -159,112 +159,175 @@ android:baselineAligned="false" android:orientation="horizontal"> + + + + + + + + + + - + android:layout_height="match_parent" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:paddingLeft="6dp" + android:paddingRight="6dp"> + - + - + - + - + - + + + + + - - - - - - - - - - - + + diff --git a/app/src/main/res/menu/menu_playlist.xml b/app/src/main/res/menu/menu_playlist.xml index f125c3fc7..a12fb2f49 100644 --- a/app/src/main/res/menu/menu_playlist.xml +++ b/app/src/main/res/menu/menu_playlist.xml @@ -1,6 +1,7 @@

+ xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3604f953..1e6d3e641 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -384,5 +384,12 @@ Rename Playlist Name Add To Playlist + Set as Playlist Thumbnail + Do you want to delete this playlist? + Playlist successfully created + Added to playlist + Playlist thumbnail changed + Playlist renamed + Playlist deleted From d3160eed9d17703a70d0278f120aac40e2f49f29 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 29 Jan 2018 14:08:26 -0800 Subject: [PATCH 037/276] -Added state saving for streams on skip and player exception events. -Added query for loading saved stream states. -Modified orphan record removal to no longer consider stream table records. --- .../database/stream/dao/StreamDAO.java | 4 -- .../database/stream/dao/StreamStateDAO.java | 15 +++++++ .../newpipe/history/HistoryFragment.java | 2 +- .../newpipe/history/HistoryRecordManager.java | 43 +++++++++++++++---- .../org/schabi/newpipe/player/BasePlayer.java | 24 +++++++++++ 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java index b699e0b6b..63f9e5940 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -92,10 +92,6 @@ public abstract class StreamDAO implements BasicDAO { " ON " + STREAM_ID + " = " + StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID + - " LEFT JOIN " + STREAM_STATE_TABLE + - " ON " + STREAM_ID + " = " + - StreamStateEntity.STREAM_STATE_TABLE + "." + StreamStateEntity.JOIN_STREAM_ID + - " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + " ON " + STREAM_ID + " = " + PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID + diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java index f89f2f7ef..1c06f4df9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java @@ -1,7 +1,10 @@ package org.schabi.newpipe.database.stream.dao; import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.stream.model.StreamStateEntity; @@ -28,6 +31,18 @@ public abstract class StreamStateDAO implements BasicDAO { throw new UnsupportedOperationException(); } + @Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") + public abstract Flowable> getState(final long streamId); + @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") public abstract int deleteState(final long streamId); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract void silentInsertInternal(final StreamStateEntity streamState); + + @Transaction + public long upsert(StreamStateEntity stream) { + silentInsertInternal(stream); + return update(stream); + } } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index 5369c657c..ac5cf4cc3 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -156,7 +156,7 @@ public abstract class HistoryFragment extends BaseFragment .setMessage(R.string.delete_all_history_prompt) .setCancelable(true) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.delete, (dialog, i) -> clearHistory()) + .setPositiveButton(R.string.delete_all, (dialog, i) -> clearHistory()) .show(); } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java index 839b8c89b..9d3ffaffe 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -3,23 +3,25 @@ package org.schabi.newpipe.history; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.model.StreamHistoryEntry; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; +import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.List; @@ -34,6 +36,7 @@ public class HistoryRecordManager { private final StreamDAO streamTable; private final StreamHistoryDAO streamHistoryTable; private final SearchHistoryDAO searchHistoryTable; + private final StreamStateDAO streamStateTable; private final SharedPreferences sharedPreferences; private final String searchHistoryKey; private final String streamHistoryKey; @@ -43,15 +46,12 @@ public class HistoryRecordManager { streamTable = database.streamDAO(); streamHistoryTable = database.streamHistoryDAO(); searchHistoryTable = database.searchHistoryDAO(); + streamStateTable = database.streamStateDAO(); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); searchHistoryKey = context.getString(R.string.enable_search_history_key); streamHistoryKey = context.getString(R.string.enable_watch_history_key); } - public Single removeOrphanedRecords() { - return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); - } - /////////////////////////////////////////////////////// // Watch History /////////////////////////////////////////////////////// @@ -161,4 +161,31 @@ public class HistoryRecordManager { private boolean isSearchHistoryEnabled() { return sharedPreferences.getBoolean(searchHistoryKey, false); } + + /////////////////////////////////////////////////////// + // Stream State History + /////////////////////////////////////////////////////// + + @SuppressWarnings("unused") + public Maybe loadStreamState(final StreamInfo info) { + return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info))) + .flatMap(streamId -> streamStateTable.getState(streamId).firstElement()) + .flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0))) + .subscribeOn(Schedulers.io()); + } + + public Maybe saveStreamState(@NonNull final StreamInfo info, final long progressTime) { + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime)); + })).subscribeOn(Schedulers.io()); + } + + /////////////////////////////////////////////////////// + // Utility + /////////////////////////////////////////////////////// + + public Single removeOrphanedRecords() { + return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 8260adc6e..369b15509 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -581,6 +581,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen errorToast = null; } + savePlaybackState(); + switch (error.type) { case ExoPlaybackException.TYPE_SOURCE: if (simpleExoPlayer.getCurrentPosition() < @@ -758,6 +760,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (simpleExoPlayer == null || playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); + savePlaybackState(); + /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. * Also restart the track if the current track is the first in a queue.*/ if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { @@ -772,6 +776,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayNext() called"); + savePlaybackState(); + playQueue.offsetIndex(+1); } @@ -833,6 +839,24 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen ); } + protected void savePlaybackState(final StreamInfo info, final long progress) { + if (context == null || info == null || databaseUpdateReactor == null) return; + final Disposable stateSaver = recordManager.saveStreamState(info, progress) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorComplete() + .subscribe(); + databaseUpdateReactor.add(stateSaver); + } + + private void savePlaybackState() { + if (simpleExoPlayer == null || currentInfo == null) return; + + if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD && + simpleExoPlayer.getCurrentPosition() < + simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { + savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); + } + } /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ From 6f9deea873d9504eb06182b12aef6f3f22cf2e39 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 29 Jan 2018 18:06:48 -0800 Subject: [PATCH 038/276] -Fixed memory leak due to image loader overusing memory cache. -Added disk cache for local item loading. --- app/src/main/java/org/schabi/newpipe/App.java | 5 ++++- .../newpipe/fragments/local/LocalItemBuilder.java | 9 +++++++-- .../newpipe/fragments/local/LocalItemListAdapter.java | 8 ++++---- .../fragments/local/LocalPlaylistFragment.java | 2 +- .../newpipe/fragments/local/PlaylistAppendDialog.java | 2 +- .../fragments/local/bookmark/BookmarkFragment.java | 2 +- .../local/bookmark/StatisticsPlaylistFragment.java | 2 +- .../fragments/local/holder/LocalItemHolder.java | 7 +++++++ .../local/holder/LocalPlaylistItemHolder.java | 3 +-- .../local/holder/LocalPlaylistStreamItemHolder.java | 5 +++-- .../local/holder/LocalStatisticStreamItemHolder.java | 11 +++-------- 11 files changed, 33 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 49f73853b..c182bfcfe 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -10,6 +10,7 @@ import android.content.Intent; import android.os.Build; import android.util.Log; +import com.nostra13.universalimageloader.cache.memory.impl.WeakMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; @@ -80,7 +81,9 @@ public class App extends Application { initNotificationChannel(); // Initialize image loader - ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build(); + ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this) + .memoryCache(new WeakMemoryCache()) + .build(); ImageLoader.getInstance().init(config); configureRxJavaErrorHandler(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java index ca200cc8a..128daf435 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -1,8 +1,12 @@ package org.schabi.newpipe.fragments.local; import android.content.Context; +import android.graphics.Bitmap; +import android.widget.ImageView; +import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.database.LocalItem; @@ -42,8 +46,9 @@ public class LocalItemBuilder { return context; } - public ImageLoader getImageLoader() { - return imageLoader; + public void displayImage(final String url, final ImageView view, + final DisplayImageOptions options) { + imageLoader.displayImage(url, view, options); } public OnLocalItemGesture getOnItemSelectedListener() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 0e012aad7..35112a6a5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -69,10 +69,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { + public void addItems(List data) { if (data != null) { if (DEBUG) { - Log.d(TAG, "addInfoItemList() before > localItems.size() = " + + Log.d(TAG, "addItems() before > localItems.size() = " + localItems.size() + ", data.size() = " + data.size()); } @@ -80,7 +80,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter offsetStart = " + offsetStart + + Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + ", localItems.size() = " + localItems.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); @@ -92,7 +92,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter { if (itemBuilder.getOnItemSelectedListener() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java index 712db8f8a..4fe577aaf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments.local.holder; +import android.graphics.Bitmap; import android.support.v4.content.ContextCompat; import android.view.MotionEvent; import android.view.View; @@ -8,6 +9,7 @@ import android.widget.ImageView; import android.widget.TextView; import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.assist.ImageScaleType; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; @@ -59,8 +61,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, - LocalPlaylistStreamItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java index bce6bab76..cd0630b37 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java @@ -45,8 +45,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { public final TextView itemDurationView; public final TextView itemAdditionalDetails; - LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { - super(infoItemBuilder, layoutId, parent); + public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + super(infoItemBuilder, R.layout.list_stream_item, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); @@ -55,10 +55,6 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); } - public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { - this(infoItemBuilder, R.layout.list_stream_item, parent); - } - private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, final DateFormat dateFormat) { final String watchCount = Localization.shortViewCount(itemBuilder.getContext(), @@ -88,8 +84,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, - DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { From 62814f083ec7ca2eb0179fd812703b5259014016 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 29 Jan 2018 20:42:52 -0800 Subject: [PATCH 039/276] -Fixed memory leak in playlist append dialog due to rogue flowables. -Changed image loader memory cache to use limited LRU. --- app/src/main/java/org/schabi/newpipe/App.java | 13 +++++--- .../fragments/local/PlaylistAppendDialog.java | 33 +++++++++++++++---- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index c182bfcfe..2ae21137f 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -10,6 +10,8 @@ import android.content.Intent; import android.os.Build; import android.util.Log; +import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; +import com.nostra13.universalimageloader.cache.memory.impl.LruMemoryCache; import com.nostra13.universalimageloader.cache.memory.impl.WeakMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; @@ -81,10 +83,7 @@ public class App extends Application { initNotificationChannel(); // Initialize image loader - ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this) - .memoryCache(new WeakMemoryCache()) - .build(); - ImageLoader.getInstance().init(config); + ImageLoader.getInstance().init(getImageLoaderConfigurations(10)); configureRxJavaErrorHandler(); } @@ -122,6 +121,12 @@ public class App extends Application { }); } + private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb) { + return new ImageLoaderConfiguration.Builder(this) + .memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024)) + .build(); + } + private void initACRA() { try { final ACRAConfiguration acraConfig = new ConfigurationBuilder(this) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 6aca5ae70..7145d91d7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments.local; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; @@ -25,6 +26,7 @@ import java.util.Collections; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; public final class PlaylistAppendDialog extends PlaylistDialog { private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); @@ -32,6 +34,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog { private RecyclerView playlistRecyclerView; private LocalItemListAdapter playlistAdapter; + private Disposable playlistReactor; + public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { PlaylistAppendDialog dialog = new PlaylistAppendDialog(); dialog.setInfo(Collections.singletonList(new StreamEntity(info))); @@ -68,6 +72,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog { playlistAdapter = new LocalItemListAdapter(getActivity()); } + @Override + public void onDestroy() { + super.onDestroy(); + if (playlistReactor != null) playlistReactor.dispose(); + playlistReactor = null; + playlistRecyclerView = null; + playlistAdapter = null; + } + /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -99,18 +112,20 @@ public final class PlaylistAppendDialog extends PlaylistDialog { return; final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid; - final Toast successToast = Toast.makeText(getContext(), - R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); + @SuppressLint("ShowToast") + final Toast successToast = Toast.makeText(getContext(), R.string.playlist_add_stream_success, + Toast.LENGTH_SHORT); playlistManager.appendToPlaylist(playlistId, getStreams()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> successToast.show()); + .doOnDispose(successToast::show) + .subscribe(ignored -> {}); getDialog().dismiss(); } }); - playlistManager.getPlaylists() + playlistReactor = playlistManager.getPlaylists() .observeOn(AndroidSchedulers.mainThread()) .subscribe(metadataEntries -> { if (metadataEntries.isEmpty()) { @@ -118,9 +133,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog { return; } - playlistAdapter.clearStreamItemList(); - playlistAdapter.addItems(metadataEntries); - playlistRecyclerView.setVisibility(View.VISIBLE); + if (playlistAdapter != null) { + playlistAdapter.clearStreamItemList(); + playlistAdapter.addItems(metadataEntries); + } + if (playlistRecyclerView != null) { + playlistRecyclerView.setVisibility(View.VISIBLE); + } }); } From 75a58d6381290453ffe8cb72e6c11054e4734bff Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 30 Jan 2018 08:06:12 -0800 Subject: [PATCH 040/276] -Fixed memory leak on rogue observable in history fragment. -Removed stream id from playlist stream join table since only foreign constraint is needed. -Added bar to playlist control UI. -Modified local playlist fragment to no longer save when out of focus. --- .../schabi/newpipe/database/Migrations.java | 2 +- .../playlist/model/PlaylistStreamEntity.java | 2 +- .../local/LocalPlaylistFragment.java | 1 - .../newpipe/history/HistoryFragment.java | 7 +- app/src/main/res/layout/playlist_control.xml | 165 ++++++++++-------- .../main/res/layout/subscription_header.xml | 3 +- 6 files changed, 101 insertions(+), 79 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 fdfd04a84..c6b472f7f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -28,7 +28,7 @@ public class Migrations { database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`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 )"); database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `stream_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)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`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)"); database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)"); database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)"); diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java index 3d71f7e70..a5b2e8248 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java @@ -14,7 +14,7 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JO import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; @Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE, - primaryKeys = {JOIN_PLAYLIST_ID, JOIN_STREAM_ID, JOIN_INDEX}, + primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX}, indices = { @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true), @Index(value = {JOIN_STREAM_ID}) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index e9d24357d..6528b8923 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -469,7 +469,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment saveJoin()); } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index ac5cf4cc3..3fa8076f3 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -222,11 +222,16 @@ public abstract class HistoryFragment extends BaseFragment @Override public void onDestroy() { super.onDestroy(); + + if (disposables != null) disposables.dispose(); + if (historySubscription != null) historySubscription.cancel(); + mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); mSharedPreferences = null; mHistoryIsEnabledChangeListener = null; mHistoryIsEnabledKey = null; - if (disposables != null) disposables.dispose(); + historySubscription = null; + disposables = null; } @Override diff --git a/app/src/main/res/layout/playlist_control.xml b/app/src/main/res/layout/playlist_control.xml index 821158bba..01632f9fc 100644 --- a/app/src/main/res/layout/playlist_control.xml +++ b/app/src/main/res/layout/playlist_control.xml @@ -1,83 +1,100 @@ - + android:visibility="invisible" + tools:visibility="visible"> - + android:layout_height="@dimen/playlist_ctrl_height"> + + + + + + + + + + + + + + + - - - - - - - - - - - - \ No newline at end of file + android:layout_height="1dp" + android:layout_below="@+id/playlist_panel" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:background="?attr/separator_color"/> + diff --git a/app/src/main/res/layout/subscription_header.xml b/app/src/main/res/layout/subscription_header.xml index 432be08b7..84a5a6216 100644 --- a/app/src/main/res/layout/subscription_header.xml +++ b/app/src/main/res/layout/subscription_header.xml @@ -6,7 +6,8 @@ android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:background="?attr/selectableItemBackground" - android:clickable="true"> + android:clickable="true" + android:focusable="true"> Date: Tue, 30 Jan 2018 16:01:11 -0800 Subject: [PATCH 041/276] -Modified BaseLocalItemFragment to no longer cache items when going into background. -Refactored and restructured all LocalItem related fragments and dialogs. -Added error logging to unmonitored single-use observables. -Modified playlist metadata query to return by alphabetical order. -Removed sending toast when playlist is renamed or deleted as it is obvious. -Removed unused code in main fragment. --- .../playlist/dao/PlaylistStreamDAO.java | 3 +- .../newpipe/fragments/MainFragment.java | 4 - .../local/BaseLocalListFragment.java | 122 +++-- .../local/LocalPlaylistFragment.java | 484 +++++++++--------- .../fragments/local/PlaylistAppendDialog.java | 109 ++-- .../local/bookmark/BookmarkFragment.java | 214 ++++---- .../local/bookmark/MostPlayedFragment.java | 2 +- .../bookmark/StatisticsPlaylistFragment.java | 265 +++++----- .../local/bookmark/WatchHistoryFragment.java | 2 +- .../newpipe/history/HistoryFragment.java | 14 +- .../history/SearchHistoryFragment.java | 24 +- .../history/WatchedHistoryFragment.java | 22 +- .../org/schabi/newpipe/player/BasePlayer.java | 11 +- .../main/res/layout/local_playlist_header.xml | 13 +- app/src/main/res/values/settings_keys.xml | 2 - app/src/main/res/values/strings.xml | 3 +- 16 files changed, 647 insertions(+), 647 deletions(-) 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 2d645e793..dd2994d29 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 @@ -63,6 +63,7 @@ public abstract class PlaylistStreamDAO implements BasicDAO> getPlaylistMetadata(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index fc4913114..4512e316f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -229,10 +229,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); fragment.useAsFrontPage(true); return fragment; - } else if (setMainPage.equals(getString(R.string.bookmark_page_key))) { - final BookmarkFragment fragment = new BookmarkFragment(); - fragment.useAsFrontPage(true); - return fragment; } else { return new BlankFragment(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java index afc67aa68..7db54db6c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java @@ -1,8 +1,7 @@ package org.schabi.newpipe.fragments.local; -import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; import android.support.v7.app.ActionBar; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -12,86 +11,44 @@ import android.view.MenuInflater; import android.view.View; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.ListViewContract; -import org.schabi.newpipe.util.StateSaver; - -import java.util.List; -import java.util.Queue; import static org.schabi.newpipe.util.AnimationUtils.animateView; +/** + * This fragment is design to be used with persistent data such as + * {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained + * in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle. + * + * This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is + * called and is memory efficient when in backstack. + * */ public abstract class BaseLocalListFragment extends BaseStateFragment - implements ListViewContract, StateSaver.WriteRead { + implements ListViewContract { /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ + protected View headerRootView; + protected View footerRootView; + protected LocalItemListAdapter itemListAdapter; protected RecyclerView itemsList; /*////////////////////////////////////////////////////////////////////////// - // LifeCycle + // Lifecycle - Creation //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onAttach(Context context) { - super.onAttach(context); - itemListAdapter = new LocalItemListAdapter(activity); - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } - @Override - public void onDestroy() { - super.onDestroy(); - StateSaver.onDestroy(savedState); - } - /*////////////////////////////////////////////////////////////////////////// - // State Saving - //////////////////////////////////////////////////////////////////////////*/ - - protected StateSaver.SavedState savedState; - - @Override - public String generateSuffix() { - // Naive solution, but it's good for now (the items don't change) - return "." + itemListAdapter.getItemsList().size() + ".list"; - } - - @Override - public void writeTo(Queue objectsToSave) { - objectsToSave.add(itemListAdapter.getItemsList()); - } - - @Override - @SuppressWarnings("unchecked") - public void readFrom(@NonNull Queue savedObjects) throws Exception { - itemListAdapter.getItemsList().clear(); - itemListAdapter.getItemsList().addAll((List) savedObjects.poll()); - } - - @Override - public void onSaveInstanceState(Bundle bundle) { - super.onSaveInstanceState(bundle); - savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this); - } - - @Override - protected void onRestoreInstanceState(@NonNull Bundle bundle) { - super.onRestoreInstanceState(bundle); - savedState = StateSaver.tryToRestore(bundle, this); - } - - /*////////////////////////////////////////////////////////////////////////// - // Init + // Lifecycle - View //////////////////////////////////////////////////////////////////////////*/ protected View getListHeader() { @@ -113,8 +70,9 @@ public abstract class BaseLocalListFragment extends BaseStateFragment itemsList = rootView.findViewById(R.id.items_list); itemsList.setLayoutManager(getListLayoutManager()); - itemListAdapter.setFooter(getListFooter()); - itemListAdapter.setHeader(getListHeader()); + itemListAdapter = new LocalItemListAdapter(activity); + itemListAdapter.setHeader(headerRootView = getListHeader()); + itemListAdapter.setFooter(footerRootView = getListFooter()); itemsList.setAdapter(itemListAdapter); } @@ -125,12 +83,13 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } /*////////////////////////////////////////////////////////////////////////// - // Menu + // Lifecycle - Menu //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + + "], inflater = [" + inflater + "]"); super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { @@ -143,27 +102,48 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } } + /*////////////////////////////////////////////////////////////////////////// + // Lifecycle - Destruction + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onDestroyView() { + super.onDestroyView(); + itemsList = null; + itemListAdapter = null; + } + /*////////////////////////////////////////////////////////////////////////// // Contract //////////////////////////////////////////////////////////////////////////*/ + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + } + @Override public void showLoading() { super.showLoading(); - // animateView(itemsList, false, 400); + animateView(itemsList, false, 200); + if (headerRootView != null) animateView(headerRootView, false, 200); } @Override public void hideLoading() { super.hideLoading(); - animateView(itemsList, true, 300); + animateView(itemsList, true, 200); + if (headerRootView != null) animateView(headerRootView, true, 200); } @Override public void showError(String message, boolean showRetryButton) { super.showError(message, showRetryButton); showListFooter(false); + animateView(itemsList, false, 200); + if (headerRootView != null) animateView(headerRootView, false, 200); } @Override @@ -181,4 +161,18 @@ public abstract class BaseLocalListFragment extends BaseStateFragment public void handleNextItems(N result) { isLoading.set(false); } + + /*////////////////////////////////////////////////////////////////////////// + // Error handling + //////////////////////////////////////////////////////////////////////////*/ + + protected void resetFragment() { + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); + } + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + return super.onError(exception); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 6528b8923..7ec24337a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -11,6 +11,7 @@ import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -38,7 +39,6 @@ import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.subjects.PublishSubject; @@ -51,8 +51,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment debouncedSaveSignal; private Disposable debouncedSaver; @@ -81,13 +79,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment createRenameDialog()); + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(itemsList); @@ -192,9 +164,236 @@ public class LocalPlaylistFragment extends BaseLocalListFragment createRenameDialog()); } + /////////////////////////////////////////////////////////////////////////// + // Fragment Lifecycle - Loading + /////////////////////////////////////////////////////////////////////////// + + @Override + public void showLoading() { + super.showLoading(); + animateView(headerRootLayout, false, 200); + animateView(playlistControl, false, 200); + } + + @Override + public void hideLoading() { + super.hideLoading(); + animateView(headerRootLayout, true, 200); + animateView(playlistControl, true, 200); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + + if (debouncedSaver != null) debouncedSaver.dispose(); + debouncedSaver = getDebouncedSaver(); + + playlistManager.getPlaylistStreams(playlistId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistObserver()); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Lifecycle - Destruction + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (databaseSubscription != null) databaseSubscription.cancel(); + if (debouncedSaver != null) debouncedSaver.dispose(); + + databaseSubscription = null; + debouncedSaver = null; + itemTouchHelper = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (debouncedSaveSignal != null) debouncedSaveSignal.onComplete(); + + debouncedSaveSignal = null; + playlistManager = null; + } + + /////////////////////////////////////////////////////////////////////////// + // Playlist Stream Loader + /////////////////////////////////////////////////////////////////////////// + + private Subscriber> getPlaylistObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List streams) { + // Do not allow saving while the result is being updated + if (debouncedSaver != null) debouncedSaver.dispose(); + handleResult(streams); + debouncedSaver = getDebouncedSaver(); + + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + LocalPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() {} + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + itemListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + return; + } + + itemListAdapter.addItems(result); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + setVideoCount(itemListAdapter.getItemsList().size()); + + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + + hideLoading(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void resetFragment() { + super.resetFragment(); + if (databaseSubscription != null) databaseSubscription.cancel(); + } + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "Local Playlist", R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Playlist Metadata/Streams Manipulation + //////////////////////////////////////////////////////////////////////////*/ + + private void createRenameDialog() { + if (playlistId == null || name == null || getContext() == null) return; + + final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); + EditText nameEdit = dialogView.findViewById(R.id.playlist_name); + nameEdit.setText(name); + nameEdit.setSelection(nameEdit.getText().length()); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.rename_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> + changePlaylistName(nameEdit.getText().toString()) + ); + + dialogBuilder.show(); + } + + private void changePlaylistName(final String name) { + this.name = name; + setTitle(name); + + Log.e(TAG, "Updating playlist id=[" + playlistId + + "] with new name=[" + name + "] items"); + + playlistManager.renamePlaylist(playlistId, name) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> {/*Do nothing on success*/}, this::onError); + } + + private void changeThumbnailUrl(final String thumbnailUrl) { + final Toast successToast = Toast.makeText(getActivity(), + R.string.playlist_thumbnail_change_success, + Toast.LENGTH_SHORT); + + Log.e(TAG, "Updating playlist id=[" + playlistId + + "] with new thumbnail url=[" + thumbnailUrl + "]"); + + playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignore -> successToast.show(), this::onError); + } + + private void deleteItem(final PlaylistStreamEntry item) { + itemListAdapter.removeItem(item); + setVideoCount(itemListAdapter.getItemsList().size()); + saveDebounced(); + } + + private void saveDebounced() { + debouncedSaveSignal.onNext(System.currentTimeMillis()); + } + + private Disposable getDebouncedSaver() { + return debouncedSaveSignal + .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> saveJoin()); + } + + private void saveJoin() { + final List items = itemListAdapter.getItemsList(); + List streamIds = new ArrayList<>(items.size()); + for (final LocalItem item : items) { + if (item instanceof PlaylistStreamEntry) { + streamIds.add(((PlaylistStreamEntry) item).streamId); + } + } + + Log.e(TAG, "Updating playlist id=[" + playlistId + + "] with [" + streamIds.size() + "] items"); + + playlistManager.updateJoin(playlistId, streamIds) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> {/*Do nothing on success*/}, this::onError); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + protected void showStreamDialog(final PlaylistStreamEntry item) { final Context context = getContext(); final Activity activity = getActivity(); @@ -236,9 +435,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment> getPlaylistObserver() { - return new Subscriber>() { - @Override - public void onSubscribe(Subscription s) { - showLoading(); - - if (databaseSubscription != null) databaseSubscription.cancel(); - databaseSubscription = s; - databaseSubscription.request(1); - } - - @Override - public void onNext(List streams) { - handleResult(streams); - if (databaseSubscription != null) databaseSubscription.request(1); - } - - @Override - public void onError(Throwable exception) { - LocalPlaylistFragment.this.onError(exception); - } - - @Override - public void onComplete() { - } - }; - } - - @Override - public void handleResult(@NonNull List result) { - super.handleResult(result); - itemListAdapter.clearStreamItemList(); - - if (result.isEmpty()) { - showEmptyState(); - return; - } - - animateView(headerRootLayout, true, 100); - animateView(itemsList, true, 300); - - itemListAdapter.addItems(result); - if (itemsListState != null) { - itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); - itemsListState = null; - } - setVideoCount(itemListAdapter.getItemsList().size()); - - playlistControl.setVisibility(View.VISIBLE); - - headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); - headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); - headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); - hideLoading(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - @Override - protected boolean onError(Throwable exception) { - resetFragment(); - if (super.onError(exception)) return true; - - onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, - "none", "Local Playlist", R.string.general_error); - return true; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - private void setInitialData(long playlistId, String name) { this.playlistId = playlistId; this.name = !TextUtils.isEmpty(name) ? name : ""; } - private void setFragmentTitle(final String title) { - if (activity != null && activity.getSupportActionBar() != null) { - activity.getSupportActionBar().setTitle(title); - } - if (headerTitleView != null) { - headerTitleView.setText(title); - } - } - private void setVideoCount(final long count) { if (activity != null && headerStreamCount != null) { headerStreamCount.setText(Localization.localizeStreamCount(activity, count)); @@ -419,71 +503,5 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { - name = nameEdit.getText().toString(); - setFragmentTitle(name); - - final LocalPlaylistManager playlistManager = - new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); - final Toast successToast = Toast.makeText(getActivity(), - R.string.playlist_rename_success, - Toast.LENGTH_SHORT); - - playlistManager.renamePlaylist(playlistId, name) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(longs -> successToast.show()); - }); - - dialogBuilder.show(); - } - - private void changeThumbnailUrl(final String thumbnailUrl) { - final Toast successToast = Toast.makeText(getActivity(), - R.string.playlist_thumbnail_change_success, - Toast.LENGTH_SHORT); - - playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignore -> successToast.show()); - } - - private void saveDebounced() { - debouncedSaveSignal.onNext(System.currentTimeMillis()); - } - - private Disposable getDebouncedSaver() { - return debouncedSaveSignal - .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> saveJoin()); - } - - private void saveJoin() { - final List items = itemListAdapter.getItemsList(); - List streamIds = new ArrayList<>(items.size()); - for (final LocalItem item : items) { - if (item instanceof PlaylistStreamEntry) { - streamIds.add(((PlaylistStreamEntry) item).streamId); - } - } - - playlistManager.updateJoin(playlistId, streamIds) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); - } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index 7145d91d7..b01dcabeb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.fragments.local; import android.annotation.SuppressLint; -import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -25,6 +24,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import javax.annotation.Nonnull; + import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -63,26 +64,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { } /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onAttach(Context context) { - super.onAttach(context); - playlistAdapter = new LocalItemListAdapter(getActivity()); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (playlistReactor != null) playlistReactor.dispose(); - playlistReactor = null; - playlistRecyclerView = null; - playlistAdapter = null; - } - - /*////////////////////////////////////////////////////////////////////////// - // Views + // LifeCycle - Creation //////////////////////////////////////////////////////////////////////////*/ @Override @@ -95,52 +77,44 @@ public final class PlaylistAppendDialog extends PlaylistDialog { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - final View newPlaylistButton = view.findViewById(R.id.newPlaylist); - playlistRecyclerView = view.findViewById(R.id.playlist_list); - playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - playlistRecyclerView.setAdapter(playlistAdapter); - final LocalPlaylistManager playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); - newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); - + playlistAdapter = new LocalItemListAdapter(getActivity()); playlistAdapter.setSelectedListener(new OnLocalItemGesture() { @Override public void selected(LocalItem selectedItem) { if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) return; - - final long playlistId = ((PlaylistMetadataEntry) selectedItem).uid; - @SuppressLint("ShowToast") - final Toast successToast = Toast.makeText(getContext(), R.string.playlist_add_stream_success, - Toast.LENGTH_SHORT); - - playlistManager.appendToPlaylist(playlistId, getStreams()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnDispose(successToast::show) - .subscribe(ignored -> {}); - - getDialog().dismiss(); + onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, + getStreams()); } }); + playlistRecyclerView = view.findViewById(R.id.playlist_list); + playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + playlistRecyclerView.setAdapter(playlistAdapter); + + final View newPlaylistButton = view.findViewById(R.id.newPlaylist); + newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); + playlistReactor = playlistManager.getPlaylists() .observeOn(AndroidSchedulers.mainThread()) - .subscribe(metadataEntries -> { - if (metadataEntries.isEmpty()) { - openCreatePlaylistDialog(); - return; - } + .subscribe(this::onPlaylistsReceived); + } - if (playlistAdapter != null) { - playlistAdapter.clearStreamItemList(); - playlistAdapter.addItems(metadataEntries); - } - if (playlistRecyclerView != null) { - playlistRecyclerView.setVisibility(View.VISIBLE); - } - }); + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle - Destruction + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (playlistReactor != null) playlistReactor.dispose(); + + playlistReactor = null; + playlistRecyclerView = null; + playlistAdapter = null; } /*////////////////////////////////////////////////////////////////////////// @@ -153,4 +127,33 @@ public final class PlaylistAppendDialog extends PlaylistDialog { PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG); getDialog().dismiss(); } + + private void onPlaylistsReceived(@NonNull final List playlists) { + if (playlists.isEmpty()) { + openCreatePlaylistDialog(); + return; + } + + if (playlistAdapter != null && playlistRecyclerView != null) { + playlistAdapter.clearStreamItemList(); + playlistAdapter.addItems(playlists); + playlistRecyclerView.setVisibility(View.VISIBLE); + } + } + + private void onPlaylistSelected(@NonNull LocalPlaylistManager manager, + @NonNull PlaylistMetadataEntry playlist, + @Nonnull List streams) { + if (getStreams() == null) return; + + @SuppressLint("ShowToast") + final Toast successToast = Toast.makeText(getContext(), + R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); + + manager.appendToPlaylist(playlist.uid, streams) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> successToast.show()); + + getDialog().dismiss(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 613e77cfe..393e128f7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -1,17 +1,14 @@ package org.schabi.newpipe.fragments.local.bookmark; import android.app.AlertDialog; -import android.content.Context; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -19,29 +16,25 @@ import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.fragments.local.LocalItemListAdapter; +import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; -import java.util.Collections; import java.util.List; import icepick.State; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; -import static org.schabi.newpipe.util.AnimationUtils.animateView; +public final class BookmarkFragment + extends BaseLocalListFragment, Void> { -public class BookmarkFragment extends BaseStateFragment> { private View watchHistoryButton; private View mostWatchedButton; - private LocalItemListAdapter itemListAdapter; - private RecyclerView itemsList; - @State protected Parcelable itemsListState; @@ -50,23 +43,14 @@ public class BookmarkFragment extends BaseStateFragment { - final Toast deleteSuccessful = Toast.makeText(getContext(), - R.string.playlist_delete_success, Toast.LENGTH_SHORT); - disposables.add(localPlaylistManager.deletePlaylist(item.uid) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> deleteSuccessful.show())); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - } - - private void resetFragment() { - if (disposables != null) disposables.clear(); - if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); - } - /////////////////////////////////////////////////////////////////////////// - // Subscriptions Loader + // Fragment LifeCycle - Loading /////////////////////////////////////////////////////////////////////////// @Override public void startLoading(boolean forceLoad) { super.startLoading(forceLoad); - resetFragment(); - localPlaylistManager.getPlaylists() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getSubscriptionSubscriber()); } + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle - Destruction + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (disposables != null) disposables.clear(); + if (databaseSubscription != null) databaseSubscription.cancel(); + + databaseSubscription = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposables != null) disposables.dispose(); + + disposables = null; + localPlaylistManager = null; + itemsListState = null; + } + + /////////////////////////////////////////////////////////////////////////// + // Subscriptions Loader + /////////////////////////////////////////////////////////////////////////// + private Subscriber> getSubscriptionSubscriber() { return new Subscriber>() { @Override @@ -238,55 +207,58 @@ public class BookmarkFragment extends BaseStateFragment infoItemsOf(List playlists) { - Collections.sort(playlists, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); - return playlists; - } - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void showLoading() { - super.showLoading(); - animateView(itemsList, false, 100); - } - - @Override - public void hideLoading() { - super.hideLoading(); - animateView(itemsList, true, 200); - } - - @Override - public void showEmptyState() { - super.showEmptyState(); - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @Override protected boolean onError(Throwable exception) { - resetFragment(); if (super.onError(exception)) return true; onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Bookmark", R.string.general_error); return true; } + + @Override + protected void resetFragment() { + super.resetFragment(); + if (disposables != null) disposables.clear(); + } + + /////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////// + + private void showDeleteDialog(final PlaylistMetadataEntry item) { + new AlertDialog.Builder(activity) + .setTitle(item.name) + .setMessage(R.string.delete_playlist_prompt) + .setCancelable(true) + .setPositiveButton(R.string.delete, (dialog, i) -> + disposables.add(deletePlaylist(item.uid)) + ) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + private Disposable deletePlaylist(final long playlistId) { + return localPlaylistManager.deletePlaylist(playlistId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> {/*Do nothing on success*/}, + throwable -> Log.e(TAG, "Playlist deletion failed, id=[" + + playlistId + "]") + ); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java index ed0d903a8..cba9e9c64 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java @@ -6,7 +6,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import java.util.Collections; import java.util.List; -public class MostPlayedFragment extends StatisticsPlaylistFragment { +public final class MostPlayedFragment extends StatisticsPlaylistFragment { @Override protected String getName() { return getString(R.string.title_most_played); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 78038c3de..4843034eb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -32,13 +32,9 @@ import java.util.List; import icepick.State; import io.reactivex.android.schedulers.AndroidSchedulers; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - public abstract class StatisticsPlaylistFragment extends BaseLocalListFragment, Void> { - private View headerRootLayout; - private View playlistControl; private View headerPlayAllButton; private View headerPopupButton; private View headerBackgroundButton; @@ -59,13 +55,13 @@ public abstract class StatisticsPlaylistFragment protected abstract List processResult(final List results); /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle + // Fragment LifeCycle - Creation /////////////////////////////////////////////////////////////////////////// @Override - public void onAttach(Context context) { - super.onAttach(context); - recordManager = new HistoryRecordManager(context); + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + recordManager = new HistoryRecordManager(getContext()); } @Override @@ -75,46 +71,23 @@ public abstract class StatisticsPlaylistFragment return inflater.inflate(R.layout.fragment_playlist, container, false); } - @Override - public void onPause() { - super.onPause(); - itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); - } - - @Override - public void onDestroyView() { - if (databaseSubscription != null) databaseSubscription.cancel(); - super.onDestroyView(); - } - - @Override - public void onDestroy() { - if (databaseSubscription != null) databaseSubscription.cancel(); - databaseSubscription = null; - recordManager = null; - - super.onDestroy(); - } - /////////////////////////////////////////////////////////////////////////// - // Fragment Views + // Fragment LifeCycle - Views /////////////////////////////////////////////////////////////////////////// @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - setFragmentTitle(getName()); + setTitle(getName()); } @Override protected View getListHeader() { - headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control, + final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control, itemsList, false); - playlistControl = headerRootLayout.findViewById(R.id.playlist_control); headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); - return headerRootLayout; } @@ -139,9 +112,124 @@ public abstract class StatisticsPlaylistFragment } } }); - } + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle - Loading + /////////////////////////////////////////////////////////////////////////// + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + recordManager.getStreamStatistics() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getHistoryObserver()); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle - Destruction + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + recordManager = null; + itemsListState = null; + } + + /////////////////////////////////////////////////////////////////////////// + // Statistics Loader + /////////////////////////////////////////////////////////////////////////// + + private Subscriber> getHistoryObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List streams) { + handleResult(streams); + if (databaseSubscription != null) databaseSubscription.request(1); + } + + @Override + public void onError(Throwable exception) { + StatisticsPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + itemListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + return; + } + + itemListAdapter.addItems(processResult(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + + hideLoading(); + } + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void resetFragment() { + super.resetFragment(); + if (databaseSubscription != null) databaseSubscription.cancel(); + } + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, + "none", "History Statistics", R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + private void showStreamDialog(final StreamStatisticsEntry item) { final Context context = getContext(); final Activity activity = getActivity(); @@ -182,113 +270,6 @@ public abstract class StatisticsPlaylistFragment new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); } - private void resetFragment() { - if (databaseSubscription != null) databaseSubscription.cancel(); - if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); - } - - /////////////////////////////////////////////////////////////////////////// - // Loader - /////////////////////////////////////////////////////////////////////////// - - @Override - public void showLoading() { - super.showLoading(); - animateView(headerRootLayout, false, 200); - animateView(itemsList, false, 100); - } - - @Override - public void startLoading(boolean forceLoad) { - super.startLoading(forceLoad); - resetFragment(); - - recordManager.getStreamStatistics() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getHistoryObserver()); - } - - private Subscriber> getHistoryObserver() { - return new Subscriber>() { - @Override - public void onSubscribe(Subscription s) { - showLoading(); - - if (databaseSubscription != null) databaseSubscription.cancel(); - databaseSubscription = s; - databaseSubscription.request(1); - } - - @Override - public void onNext(List streams) { - handleResult(streams); - if (databaseSubscription != null) databaseSubscription.request(1); - } - - @Override - public void onError(Throwable exception) { - StatisticsPlaylistFragment.this.onError(exception); - } - - @Override - public void onComplete() { - } - }; - } - - @Override - public void handleResult(@NonNull List result) { - super.handleResult(result); - itemListAdapter.clearStreamItemList(); - - if (result.isEmpty()) { - showEmptyState(); - return; - } - - animateView(headerRootLayout, true, 100); - animateView(itemsList, true, 300); - - itemListAdapter.addItems(processResult(result)); - if (itemsListState != null) { - itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); - itemsListState = null; - } - - playlistControl.setVisibility(View.VISIBLE); - headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); - headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); - headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); - hideLoading(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - @Override - protected boolean onError(Throwable exception) { - resetFragment(); - if (super.onError(exception)) return true; - - onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, - "none", "History Statistics", R.string.general_error); - return true; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - protected void setFragmentTitle(final String title) { - if (activity.getSupportActionBar() != null) { - activity.getSupportActionBar().setTitle(title); - } - } - private PlayQueue getPlayQueue() { return getPlayQueue(0); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java index 853029ae6..84126ad4b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java @@ -6,7 +6,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import java.util.Collections; import java.util.List; -public class WatchHistoryFragment extends StatisticsPlaylistFragment { +public final class WatchHistoryFragment extends StatisticsPlaylistFragment { @Override protected String getName() { return getString(R.string.title_watch_history); diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index 3fa8076f3..14bd93c57 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -14,6 +14,7 @@ import android.support.design.widget.Snackbar; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -173,10 +174,19 @@ public abstract class HistoryFragment extends BaseFragment final Disposable deletion = delete(itemsToDelete) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); + .subscribe( + ignored -> Log.d(TAG, "Clear history deleted [" + + itemsToDelete.size() + "] items."), + error -> Log.e(TAG, "Clear history delete step failed", error) + ); + final Disposable cleanUp = historyRecordManager.removeOrphanedRecords() .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); + .subscribe( + ignored -> Log.d(TAG, "Clear history deleted orphaned stream records"), + error -> Log.e(TAG, "Clear history remove orphaned records failed", error) + ); + disposables.addAll(deletion, cleanUp); makeSnackbar(R.string.history_cleared); diff --git a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java index e40a79368..25098fac8 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -7,6 +7,7 @@ import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -83,17 +84,25 @@ public class SearchHistoryFragment extends HistoryFragment { .setCancelable(true) .setNeutralButton(R.string.cancel, null) .setPositiveButton(R.string.delete_one, (dialog, i) -> { - final Single onDelete = historyRecordManager + final Disposable onDelete = historyRecordManager .deleteSearches(Collections.singleton(item)) - .observeOn(AndroidSchedulers.mainThread()); - disposables.add(onDelete.subscribe()); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + ignored -> {/*successful*/}, + error -> Log.e(TAG, "Search history Delete One failed:", error) + ); + disposables.add(onDelete); makeSnackbar(R.string.item_deleted); }) .setNegativeButton(R.string.delete_all, (dialog, i) -> { - final Single onDeleteAll = historyRecordManager + final Disposable onDeleteAll = historyRecordManager .deleteSearchHistory(item.getSearch()) - .observeOn(AndroidSchedulers.mainThread()); - disposables.add(onDeleteAll.subscribe()); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + ignored -> {/*successful*/}, + error -> Log.e(TAG, "Search history Delete All failed:", error) + ); + disposables.add(onDeleteAll); makeSnackbar(R.string.item_deleted); }) .show(); @@ -112,8 +121,7 @@ public class SearchHistoryFragment extends HistoryFragment { protected class SearchHistoryAdapter extends HistoryEntryAdapter { - - public SearchHistoryAdapter(Context context) { + SearchHistoryAdapter(Context context) { super(context); } diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java index 7913c9a28..0fabc594a 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -8,6 +8,7 @@ import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -29,6 +30,7 @@ import java.util.List; import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; public class WatchedHistoryFragment extends HistoryFragment { @@ -85,17 +87,25 @@ public class WatchedHistoryFragment extends HistoryFragment .setCancelable(true) .setNeutralButton(R.string.cancel, null) .setPositiveButton(R.string.delete_one, (dialog, i) -> { - final Single onDelete = historyRecordManager + final Disposable onDelete = historyRecordManager .deleteStreamHistory(Collections.singleton(item)) - .observeOn(AndroidSchedulers.mainThread()); - disposables.add(onDelete.subscribe()); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + ignored -> {/*successful*/}, + error -> Log.e(TAG, "Watch history Delete One failed:", error) + ); + disposables.add(onDelete); makeSnackbar(R.string.item_deleted); }) .setNegativeButton(R.string.delete_all, (dialog, i) -> { - final Single onDeleteAll = historyRecordManager + final Disposable onDeleteAll = historyRecordManager .deleteStreamHistory(item.streamId) - .observeOn(AndroidSchedulers.mainThread()); - disposables.add(onDeleteAll.subscribe()); + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + ignored -> {/*successful*/}, + error -> Log.e(TAG, "Watch history Delete All failed:", error) + ); + disposables.add(onDeleteAll); makeSnackbar(R.string.item_deleted); }) .show(); diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 369b15509..7558f1375 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -676,7 +676,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } // TODO: update exoplayer to 2.6.x in order to register view count on repeated streams - databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); + databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() + .subscribe( + ignored -> {/* successful */}, + error -> Log.e(TAG, "Player onViewed() failure: ", error) + )); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } @@ -844,7 +848,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) .onErrorComplete() - .subscribe(); + .subscribe( + ignored -> {/* successful */}, + error -> Log.e(TAG, "savePlaybackState() failure: ", error) + ); databaseUpdateReactor.add(stateSaver); } diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml index 4ba611681..4d686c515 100644 --- a/app/src/main/res/layout/local_playlist_header.xml +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -11,9 +11,12 @@ android:id="@+id/playlist_title_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginLeft="8dp" - android:layout_marginRight="8dp" - android:layout_marginTop="6dp" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:paddingLeft="12dp" + android:paddingRight="12dp" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_toLeftOf="@id/playlist_stream_count" android:layout_toStartOf="@id/playlist_stream_count" android:background="?attr/selectableItemBackground" @@ -26,7 +29,7 @@ android:singleLine="true" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="@dimen/playlist_detail_title_text_size" - tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..." /> + tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..."/> subscription_page_key kiosk_page channel_page - bookmark_page @string/blank_page_key @string/kiosk_page_key @string/feed_page_key @string/subscription_page_key @string/channel_page_key - @string/bookmark_page_key main_page_selected_service main_page_selected_channel_name diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e6d3e641..577d85ce5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -390,6 +390,5 @@ Playlist successfully created Added to playlist Playlist thumbnail changed - Playlist renamed - Playlist deleted + Failed to delete playlist From 1ff8b5fb9f5afaa01ed7e15df9c104a520a5e394 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 30 Jan 2018 16:21:50 -0800 Subject: [PATCH 042/276] -Refactored info item and local item click gestures into the same OnClickGesture. --- .../fragments/detail/VideoDetailFragment.java | 7 ++----- .../fragments/list/BaseListFragment.java | 8 ++++---- .../fragments/local/LocalItemBuilder.java | 7 ++++--- .../fragments/local/LocalItemListAdapter.java | 3 ++- .../local/LocalPlaylistFragment.java | 3 ++- .../fragments/local/PlaylistAppendDialog.java | 3 ++- .../local/bookmark/BookmarkFragment.java | 4 ++-- .../bookmark/StatisticsPlaylistFragment.java | 4 ++-- .../subscription/SubscriptionFragment.java | 4 ++-- .../newpipe/info_list/InfoItemBuilder.java | 19 ++++++++++--------- .../newpipe/info_list/InfoListAdapter.java | 7 ++++--- .../newpipe/info_list/OnInfoItemGesture.java | 12 ------------ .../OnClickGesture.java} | 7 ++----- 13 files changed, 38 insertions(+), 50 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java rename app/src/main/java/org/schabi/newpipe/{fragments/local/OnLocalItemGesture.java => util/OnClickGesture.java} (58%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 6907f3266..923feeba0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -44,7 +44,6 @@ import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; @@ -60,11 +59,8 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; -import org.schabi.newpipe.history.HistoryListener; -import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.helper.PlayerHelper; @@ -79,6 +75,7 @@ import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -474,7 +471,7 @@ public class VideoDetailFragment extends BaseStateFragment implement @Override protected void initListeners() { super.initListeners(); - infoItemBuilder.setOnStreamSelectedListener(new OnInfoItemGesture() { + infoItemBuilder.setOnStreamSelectedListener(new OnClickGesture() { @Override public void selected(StreamInfoItem selectedItem) { selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 82b45c76e..aad6e95fa 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -23,9 +23,9 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; import java.util.ArrayList; @@ -137,7 +137,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { + infoListAdapter.setOnStreamSelectedListener(new OnClickGesture() { @Override public void selected(StreamInfoItem selectedItem) { onItemSelected(selectedItem); @@ -152,7 +152,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } }); - infoListAdapter.setOnChannelSelectedListener(new OnInfoItemGesture() { + infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { @Override public void selected(ChannelInfoItem selectedItem) { onItemSelected(selectedItem); @@ -162,7 +162,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } }); - infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture() { + infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() { @Override public void selected(PlaylistInfoItem selectedItem) { onItemSelected(selectedItem); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java index 128daf435..4794def97 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -9,6 +9,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.util.OnClickGesture; /* * Created by Christian Schabesberger on 26.09.16. @@ -36,7 +37,7 @@ public class LocalItemBuilder { private final Context context; private ImageLoader imageLoader = ImageLoader.getInstance(); - private OnLocalItemGesture onSelectedListener; + private OnClickGesture onSelectedListener; public LocalItemBuilder(Context context) { this.context = context; @@ -51,11 +52,11 @@ public class LocalItemBuilder { imageLoader.displayImage(url, view, options); } - public OnLocalItemGesture getOnItemSelectedListener() { + public OnClickGesture getOnItemSelectedListener() { return onSelectedListener; } - public void setOnItemSelectedListener(OnLocalItemGesture listener) { + public void setOnItemSelectedListener(OnClickGesture listener) { this.onSelectedListener = listener; } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 35112a6a5..807599678 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -12,6 +12,7 @@ import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; @@ -65,7 +66,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter listener) { + public void setSelectedListener(OnClickGesture listener) { localItemBuilder.setOnItemSelectedListener(listener); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 7ec24337a..7d73b14b9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -32,6 +32,7 @@ import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; @@ -142,7 +143,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { + itemListAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { if (selectedItem instanceof PlaylistStreamEntry) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index b01dcabeb..6d18e9155 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -19,6 +19,7 @@ import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.Collections; @@ -81,7 +82,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog { new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); playlistAdapter = new LocalItemListAdapter(getActivity()); - playlistAdapter.setSelectedListener(new OnLocalItemGesture() { + playlistAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 393e128f7..5c863590f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -18,9 +18,9 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; -import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import java.util.List; @@ -95,7 +95,7 @@ public final class BookmarkFragment protected void initListeners() { super.initListeners(); - itemListAdapter.setSelectedListener(new OnLocalItemGesture() { + itemListAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 4843034eb..d4e888c30 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -18,13 +18,13 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.local.BaseLocalListFragment; -import org.schabi.newpipe.fragments.local.OnLocalItemGesture; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; @@ -95,7 +95,7 @@ public abstract class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - itemListAdapter.setSelectedListener(new OnLocalItemGesture() { + itemListAdapter.setSelectedListener(new OnClickGesture() { @Override public void selected(LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java index 8db5d5f00..a91cca908 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java @@ -17,9 +17,9 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.Collections; @@ -125,7 +125,7 @@ public class SubscriptionFragment extends BaseStateFragment() { + infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { @Override public void selected(ChannelInfoItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement 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 cdad31674..218895983 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 @@ -19,6 +19,7 @@ import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; +import org.schabi.newpipe.util.OnClickGesture; /* * Created by Christian Schabesberger on 26.09.16. @@ -46,9 +47,9 @@ public class InfoItemBuilder { private final Context context; private ImageLoader imageLoader = ImageLoader.getInstance(); - private OnInfoItemGesture onStreamSelectedListener; - private OnInfoItemGesture onChannelSelectedListener; - private OnInfoItemGesture onPlaylistSelectedListener; + private OnClickGesture onStreamSelectedListener; + private OnClickGesture onChannelSelectedListener; + private OnClickGesture onPlaylistSelectedListener; public InfoItemBuilder(Context context) { this.context = context; @@ -86,27 +87,27 @@ public class InfoItemBuilder { return imageLoader; } - public OnInfoItemGesture getOnStreamSelectedListener() { + public OnClickGesture getOnStreamSelectedListener() { return onStreamSelectedListener; } - public void setOnStreamSelectedListener(OnInfoItemGesture listener) { + public void setOnStreamSelectedListener(OnClickGesture listener) { this.onStreamSelectedListener = listener; } - public OnInfoItemGesture getOnChannelSelectedListener() { + public OnClickGesture getOnChannelSelectedListener() { return onChannelSelectedListener; } - public void setOnChannelSelectedListener(OnInfoItemGesture listener) { + public void setOnChannelSelectedListener(OnClickGesture listener) { this.onChannelSelectedListener = listener; } - public OnInfoItemGesture getOnPlaylistSelectedListener() { + public OnClickGesture getOnPlaylistSelectedListener() { return onPlaylistSelectedListener; } - public void setOnPlaylistSelectedListener(OnInfoItemGesture listener) { + public void setOnPlaylistSelectedListener(OnClickGesture listener) { this.onPlaylistSelectedListener = listener; } 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 dbf5d7556..4b9914397 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 @@ -17,6 +17,7 @@ import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; +import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; @@ -76,15 +77,15 @@ public class InfoListAdapter extends RecyclerView.Adapter(); } - public void setOnStreamSelectedListener(OnInfoItemGesture listener) { + public void setOnStreamSelectedListener(OnClickGesture listener) { infoItemBuilder.setOnStreamSelectedListener(listener); } - public void setOnChannelSelectedListener(OnInfoItemGesture listener) { + public void setOnChannelSelectedListener(OnClickGesture listener) { infoItemBuilder.setOnChannelSelectedListener(listener); } - public void setOnPlaylistSelectedListener(OnInfoItemGesture listener) { + public void setOnPlaylistSelectedListener(OnClickGesture listener) { infoItemBuilder.setOnPlaylistSelectedListener(listener); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java b/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java deleted file mode 100644 index 84634c1d9..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.schabi.newpipe.info_list; - -import org.schabi.newpipe.extractor.InfoItem; - -public abstract class OnInfoItemGesture { - - public abstract void selected(T selectedItem); - - public void held(T selectedItem) { - // Optional gesture - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java b/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java similarity index 58% rename from app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java rename to app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java index 5cede4c67..01416b279 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/OnLocalItemGesture.java +++ b/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java @@ -1,11 +1,8 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.util; import android.support.v7.widget.RecyclerView; -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.extractor.InfoItem; - -public abstract class OnLocalItemGesture { +public abstract class OnClickGesture { public abstract void selected(T selectedItem); From 53a1833e26e949e3ec541f40a549c3b8ca3d3247 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 30 Jan 2018 18:17:27 -0800 Subject: [PATCH 043/276] -Increased save join debounce time to 2 seconds. -Added add to playlist option for videos available as base list items. -Moved video count to second row on local playlist header. -Removed bottom line on playlist control UI. --- .../fragments/list/BaseListFragment.java | 33 ++-- .../local/LocalPlaylistFragment.java | 2 +- .../main/res/layout/local_playlist_header.xml | 17 +- app/src/main/res/layout/playlist_control.xml | 157 ++++++++---------- 4 files changed, 100 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index aad6e95fa..8bc68a4c7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -29,6 +29,7 @@ import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Queue; @@ -194,22 +195,26 @@ public abstract class BaseListFragment extends BaseStateFragment implem final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), - context.getResources().getString(R.string.enqueue_on_popup) + context.getResources().getString(R.string.enqueue_on_popup), + context.getResources().getString(R.string.append_playlist) }; - final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - switch (i) { - case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); - break; - case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); - break; - default: - break; - } + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + if (getFragmentManager() != null) { + PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) + .show(getFragmentManager(), TAG); + } + break; + default: + break; } }; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 7d73b14b9..0a6f9158e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -47,7 +47,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { - private static final long SAVE_DEBOUNCE_MILLIS = 1000; + private static final long SAVE_DEBOUNCE_MILLIS = 2000; private View headerRootLayout; private TextView headerTitleView; diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml index 4d686c515..ab5dd4440 100644 --- a/app/src/main/res/layout/local_playlist_header.xml +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -5,20 +5,17 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:padding="6dp" android:background="?attr/contrast_background_color"> - - - - - - - - - - - - - - - - + android:layout_height="@dimen/playlist_ctrl_height" + android:layout_weight="1" + android:gravity="center" + android:clickable="true" + android:focusable="true" + android:background="?attr/selectableItemBackground"> + - + + - + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center" + android:clickable="true" + android:focusable="true" + android:background="?attr/selectableItemBackground" + android:id="@+id/playlist_ctrl_play_all_button"> + + + + + + + + + From 268762166af3a74fe4a9f9d53fa89f0729c5591c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 30 Jan 2018 19:39:41 -0800 Subject: [PATCH 044/276] -Added save on exit to local playlist fragment. -Improved drag reordering experience by setting minimum velocity. -Increased save debounce to 10 seconds. --- .../local/LocalPlaylistFragment.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 0a6f9158e..e84ee41ff 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -47,7 +47,9 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { - private static final long SAVE_DEBOUNCE_MILLIS = 2000; + // 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 = 15; private View headerRootLayout; private TextView headerTitleView; @@ -205,6 +207,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment saveJoin()); + .subscribe(ignored -> saveImmediate()); } - private void saveJoin() { + private void saveImmediate() { final List items = itemListAdapter.getItemsList(); List streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { @@ -449,6 +452,17 @@ public class LocalPlaylistFragment extends BaseLocalListFragment Date: Wed, 31 Jan 2018 11:51:47 -0800 Subject: [PATCH 045/276] -Fixed database updates cause outdated record to overwrite reordered local playlist when fragment is active. -Fixed save on exit causes empty list being saved after orientation changes on older devices. -Fixed NPE on animating garbage collected views on local item fragments. -Reduced drag speed from 15 to 12 items per second. --- .../playlist/dao/PlaylistStreamDAO.java | 4 +- .../local/BaseLocalListFragment.java | 6 +- .../local/LocalPlaylistFragment.java | 156 +++++++++++------- 3 files changed, 100 insertions(+), 66 deletions(-) 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 dd2994d29..8bf1ea696 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 @@ -47,8 +47,8 @@ public abstract class PlaylistStreamDAO implements BasicDAO saveImmediate()); + .subscribe(ignored -> saveImmediate(), this::onError); } private void saveImmediate() { + // 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; + } + final List items = itemListAdapter.getItemsList(); List streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { @@ -386,12 +416,60 @@ public class LocalPlaylistFragment extends BaseLocalListFragment {/*Do nothing on success*/}, this::onError); + .subscribe( + () -> { if (isModified != null) isModified.set(false); }, + this::onError + ); + } + + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.ACTION_STATE_IDLE) { + @Override + public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, + int viewSizeOutOfBounds, int totalSize, + 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(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() || + itemListAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); + if (isSwapped) saveChanges(); + return isSwapped; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} + }; } /*////////////////////////////////////////////////////////////////////////// @@ -449,50 +527,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment Date: Sat, 3 Feb 2018 09:36:40 -0800 Subject: [PATCH 046/276] -Fixed NPE issues when button views are clicked on local playlist and statistics playlist fragments are out of focus. -Added disk cache size limit for image loader. -Fixed button names for playlist rename dialog. --- app/src/main/java/org/schabi/newpipe/App.java | 8 ++++---- .../fragments/local/LocalPlaylistFragment.java | 11 ++++++++++- .../local/bookmark/StatisticsPlaylistFragment.java | 7 +++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 2ae21137f..3a22bf511 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -11,8 +11,6 @@ import android.os.Build; import android.util.Log; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; -import com.nostra13.universalimageloader.cache.memory.impl.LruMemoryCache; -import com.nostra13.universalimageloader.cache.memory.impl.WeakMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; @@ -83,7 +81,7 @@ public class App extends Application { initNotificationChannel(); // Initialize image loader - ImageLoader.getInstance().init(getImageLoaderConfigurations(10)); + ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); configureRxJavaErrorHandler(); } @@ -121,9 +119,11 @@ public class App extends Application { }); } - private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb) { + private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb, + final int diskCacheSizeMb) { return new ImageLoaderConfiguration.Builder(this) .memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024)) + .diskCacheSize(diskCacheSizeMb * 1024 * 1024) .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index 058dc43b2..ea7242055 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -35,6 +35,7 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -289,6 +290,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment result) { super.handleResult(result); + if (itemListAdapter == null) return; + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { @@ -349,7 +352,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment + .setPositiveButton(R.string.rename, (dialogInterface, i) -> changePlaylistName(nameEdit.getText().toString()) ); @@ -382,6 +385,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); for (final LocalItem item : infoItems) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index d4e888c30..ec2dda0a4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -27,6 +27,7 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import icepick.State; @@ -185,6 +186,8 @@ public abstract class StatisticsPlaylistFragment @Override public void handleResult(@NonNull List result) { super.handleResult(result); + if (itemListAdapter == null) return; + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { @@ -275,6 +278,10 @@ public abstract class StatisticsPlaylistFragment } private PlayQueue getPlayQueue(final int index) { + if (itemListAdapter == null) { + return new SinglePlayQueue(Collections.emptyList(), 0); + } + final List infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); for (final LocalItem item : infoItems) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 577d85ce5..05db15da1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -233,6 +233,7 @@ Delete All Checksum Dismiss + Rename New mission From c0a75f5b98606091070ac6a37d82658db5b06afc Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Mon, 5 Feb 2018 21:32:23 -0800 Subject: [PATCH 047/276] -Added ability to save playlist as remote playlist link rather than storing it in database. -Added LeakCanary as part of debug build. -Modified bookmark list to show both remote and local playlists. -Removed ability to save channel items as local playlist, in favor of subscribe. --- app/build.gradle | 2 + .../java/org/schabi/newpipe/DebugApp.java | 8 + app/src/main/java/org/schabi/newpipe/App.java | 3 - .../schabi/newpipe/database/AppDatabase.java | 6 +- .../schabi/newpipe/database/LocalItem.java | 6 +- .../database/playlist/PlaylistLocalItem.java | 7 + .../playlist/PlaylistMetadataEntry.java | 11 +- .../playlist/dao/PlaylistRemoteDAO.java | 60 +++++ .../playlist/model/PlaylistRemoteEntity.java | 138 ++++++++++++ .../fragments/list/BaseListFragment.java | 16 -- .../list/channel/ChannelFragment.java | 12 - .../list/playlist/PlaylistFragment.java | 205 +++++++++++++----- .../fragments/local/LocalItemListAdapter.java | 15 +- .../local/RemotePlaylistManager.java | 48 ++++ .../local/bookmark/BookmarkFragment.java | 93 +++++--- .../local/holder/LocalPlaylistItemHolder.java | 43 +--- .../local/holder/PlaylistItemHolder.java | 62 ++++++ .../holder/RemotePlaylistItemHolder.java | 33 +++ .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 163 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 159 bytes .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 122 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 124 bytes .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 163 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 163 bytes .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 236 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 236 bytes .../ic_playlist_add_check_black_24dp.png | Bin 0 -> 283 bytes .../ic_playlist_add_check_white_24dp.png | Bin 0 -> 289 bytes .../main/res/layout/fragment_video_detail.xml | 2 +- app/src/main/res/menu/menu_channel.xml | 8 - app/src/main/res/menu/menu_play_queue.xml | 2 +- app/src/main/res/menu/menu_playlist.xml | 16 +- app/src/main/res/values/attrs.xml | 3 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 6 +- 35 files changed, 625 insertions(+), 183 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png diff --git a/app/build.gradle b/app/build.gradle index 86d6542e0..748bbb9c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,4 +89,6 @@ dependencies { implementation 'frankiesardo:icepick:3.2.0' annotationProcessor 'frankiesardo:icepick-processor:3.2.0' + + debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' } diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index 1a507b4e5..fbf414738 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -4,6 +4,7 @@ import android.content.Context; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; +import com.squareup.leakcanary.LeakCanary; public class DebugApp extends App { private static final String TAG = DebugApp.class.toString(); @@ -18,6 +19,13 @@ public class DebugApp extends App { public void onCreate() { super.onCreate(); + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + LeakCanary.install(this); + initStetho(); } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 3a22bf511..79221db7f 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -1,12 +1,9 @@ package org.schabi.newpipe; -import android.app.AlarmManager; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; -import android.content.Intent; import android.os.Build; import android.util.Log; 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 086e1bed0..145a77c70 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -9,8 +9,10 @@ import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; +import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.dao.StreamDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; @@ -26,7 +28,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; entities = { SubscriptionEntity.class, SearchHistoryEntry.class, StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, - PlaylistEntity.class, PlaylistStreamEntity.class + PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class }, version = DB_VER_12_0, exportSchema = false @@ -48,4 +50,6 @@ public abstract class AppDatabase extends RoomDatabase { public abstract PlaylistDAO playlistDAO(); public abstract PlaylistStreamDAO playlistStreamDAO(); + + public abstract PlaylistRemoteDAO playlistRemoteDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java index 95d0d9213..e121739ab 100644 --- a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java @@ -2,9 +2,11 @@ package org.schabi.newpipe.database; public interface LocalItem { enum LocalItemType { - PLAYLIST_ITEM, + PLAYLIST_LOCAL_ITEM, + PLAYLIST_REMOTE_ITEM, + PLAYLIST_STREAM_ITEM, - STATISTIC_STREAM_ITEM + STATISTIC_STREAM_ITEM, } LocalItemType getLocalItemType(); 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 new file mode 100644 index 000000000..fd99f84a1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.database.playlist; + +import org.schabi.newpipe.database.LocalItem; + +public interface PlaylistLocalItem extends LocalItem { + String getOrderingName(); +} 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 205c5108d..6d9fc2213 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,13 +2,11 @@ package org.schabi.newpipe.database.playlist; import android.arch.persistence.room.ColumnInfo; -import org.schabi.newpipe.database.LocalItem; - 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; -public class PlaylistMetadataEntry implements LocalItem { +public class PlaylistMetadataEntry implements PlaylistLocalItem { final public static String PLAYLIST_STREAM_COUNT = "streamCount"; @ColumnInfo(name = PLAYLIST_ID) @@ -29,6 +27,11 @@ public class PlaylistMetadataEntry implements LocalItem { @Override public LocalItemType getLocalItemType() { - return LocalItemType.PLAYLIST_ITEM; + return LocalItemType.PLAYLIST_LOCAL_ITEM; + } + + @Override + public String getOrderingName() { + return name; } } 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 new file mode 100644 index 000000000..82d767b07 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java @@ -0,0 +1,60 @@ +package org.schabi.newpipe.database.playlist.dao; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; + +import org.schabi.newpipe.database.BasicDAO; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; + +import java.util.List; + +import io.reactivex.Flowable; + +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; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; + +@Dao +public abstract class PlaylistRemoteDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE) + public abstract int deleteAll(); + + @Override + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + + " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + public abstract Flowable> listByService(int serviceId); + + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + + REMOTE_PLAYLIST_URL + " = :url AND " + + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + public abstract Flowable> getPlaylist(long serviceId, String url); + + @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE + + " WHERE " + + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + abstract Long getPlaylistIdInternal(long serviceId, String url); + + @Transaction + public long upsert(PlaylistRemoteEntity playlist) { + final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl()); + + if (playlistId == null) { + return insert(playlist); + } else { + playlist.setUid(playlistId); + update(playlist); + return playlistId; + } + } + + @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE + + " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId") + public abstract int deletePlaylist(final long playlistId); +} 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 new file mode 100644 index 000000000..5e3db62a9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -0,0 +1,138 @@ +package org.schabi.newpipe.database.playlist.model; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Ignore; +import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.util.Constants; + +import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME; +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; +import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; + +@Entity(tableName = REMOTE_PLAYLIST_TABLE, + indices = { + @Index(value = {REMOTE_PLAYLIST_NAME}), + @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true) + }) +public class PlaylistRemoteEntity implements PlaylistLocalItem { + final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists"; + final public static String REMOTE_PLAYLIST_ID = "uid"; + final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id"; + final public static String REMOTE_PLAYLIST_NAME = "name"; + final public static String REMOTE_PLAYLIST_URL = "url"; + final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; + final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; + final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = REMOTE_PLAYLIST_ID) + private long uid = 0; + + @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID) + private int serviceId = Constants.NO_SERVICE_ID; + + @ColumnInfo(name = REMOTE_PLAYLIST_NAME) + private String name; + + @ColumnInfo(name = REMOTE_PLAYLIST_URL) + private String url; + + @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL) + private String thumbnailUrl; + + @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) + private String uploader; + + @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) + private Long streamCount; + + public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl, + String uploader, Long streamCount) { + this.serviceId = serviceId; + this.name = name; + this.url = url; + this.thumbnailUrl = thumbnailUrl; + this.uploader = uploader; + this.streamCount = streamCount; + } + + @Ignore + public PlaylistRemoteEntity(final PlaylistInfo info) { + this(info.getServiceId(), info.getName(), info.getUrl(), info.getThumbnailUrl(), + info.getUploaderName(), info.getStreamCount()); + } + + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + + public int getServiceId() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId = serviceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUploader() { + return uploader; + } + + public void setUploader(String uploader) { + this.uploader = uploader; + } + + public Long getStreamCount() { + return streamCount; + } + + public void setStreamCount(Long streamCount) { + this.streamCount = streamCount; + } + + @Override + public LocalItemType getLocalItemType() { + return PLAYLIST_REMOTE_ITEM; + } + + @Override + public String getOrderingName() { + return name; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 8bc68a4c7..1a0a836c5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -290,20 +290,4 @@ public abstract class BaseListFragment extends BaseStateFragment implem public void handleNextItems(N result) { isLoading.set(false); } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - protected void appendToPlaylist(final android.support.v4.app.FragmentManager manager, - final String tag) { - if (infoListAdapter == null) return; - List streams = new ArrayList<>(); - for (final InfoItem item : infoListAdapter.getItemsList()) { - if (item instanceof StreamInfoItem) { - streams.add((StreamInfoItem) item); - } - } - PlaylistAppendDialog.fromStreamInfoItems(streams).show(manager, tag); - } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 641b26299..a7f513de9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -84,7 +84,6 @@ public class ChannelFragment extends BaseListInfoFragment { private LinearLayout headerBackgroundButton; private MenuItem menuRssButton; - private MenuItem playlistAppendButton; public static ChannelFragment getInstance(int serviceId, String url, String name) { ChannelFragment instance = new ChannelFragment(); @@ -203,12 +202,6 @@ public class ChannelFragment extends BaseListInfoFragment { if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); menuRssButton = menu.findItem(R.id.menu_item_rss); - playlistAppendButton = menu.findItem(R.id.menu_append_playlist); - - if (currentInfo != null) { - menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); - playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty()); - } } } @@ -232,9 +225,6 @@ public class ChannelFragment extends BaseListInfoFragment { case R.id.menu_item_share: shareUrl(name, url); break; - case R.id.menu_append_playlist: - appendToPlaylist(getFragmentManager(), TAG); - break; default: return super.onOptionsItemSelected(item); } @@ -434,8 +424,6 @@ public class ChannelFragment extends BaseListInfoFragment { } else headerSubscribersTextView.setVisibility(View.GONE); if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); - if (playlistAppendButton != null) playlistAppendButton - .setVisible(!currentInfo.getRelatedStreams().isEmpty()); playlistCtrl.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 15255618b..39c88f8d3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -17,13 +17,18 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.fragments.local.RemotePlaylistManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlaylistPlayQueue; @@ -32,12 +37,21 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import java.util.List; + import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class PlaylistFragment extends BaseListInfoFragment { + private CompositeDisposable disposables; + private Subscription bookmarkReactor; + + private RemotePlaylistManager remotePlaylistManager; + private PlaylistRemoteEntity playlistEntity; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -54,7 +68,8 @@ public class PlaylistFragment extends BaseListInfoFragment { private View headerPopupButton; private View headerBackgroundButton; - private MenuItem playlistAppendButton; + private MenuItem playlistBookmarkButton; + private MenuItem playlistUnbookmarkButton; public static PlaylistFragment getInstance(int serviceId, String url, String name) { PlaylistFragment instance = new PlaylistFragment(); @@ -67,7 +82,15 @@ public class PlaylistFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + disposables = new CompositeDisposable(); + remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_playlist, container, false); } @@ -96,6 +119,11 @@ public class PlaylistFragment extends BaseListInfoFragment { super.initViews(rootView, savedInstanceState); infoListAdapter.useMiniItemVariants(true); + + remotePlaylistManager.getPlaylist(serviceId, url) + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistBookmarkSubscriber()); } @Override @@ -112,29 +140,26 @@ public class PlaylistFragment extends BaseListInfoFragment { context.getResources().getString(R.string.start_here_on_popup), }; - final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); - switch (i) { - case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); - break; - case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); - break; - case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); - break; - case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); - break; - case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); - break; - default: - break; - } + final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { + final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 2: + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + break; + case 3: + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + break; + case 4: + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + break; + default: + break; } }; @@ -148,10 +173,28 @@ public class PlaylistFragment extends BaseListInfoFragment { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_playlist, menu); - playlistAppendButton = menu.findItem(R.id.menu_append_playlist); - if (currentInfo != null) { - playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty()); - } + playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark); + playlistUnbookmarkButton = menu.findItem(R.id.menu_item_unbookmark); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (disposables != null) disposables.clear(); + if (bookmarkReactor != null) bookmarkReactor.cancel(); + + bookmarkReactor = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (disposables != null) disposables.dispose(); + + disposables = null; + remotePlaylistManager = null; + playlistEntity = null; } /*////////////////////////////////////////////////////////////////////////// @@ -177,8 +220,11 @@ public class PlaylistFragment extends BaseListInfoFragment { case R.id.menu_item_share: shareUrl(name, url); break; - case R.id.menu_append_playlist: - appendToPlaylist(getFragmentManager(), TAG); + case R.id.menu_item_bookmark: + bookmarkPlaylist(); + break; + case R.id.menu_item_unbookmark: + unbookmarkPlaylist(); break; default: return super.onOptionsItemSelected(item); @@ -211,12 +257,11 @@ public class PlaylistFragment extends BaseListInfoFragment { if (!TextUtils.isEmpty(result.getUploaderName())) { headerUploaderName.setText(result.getUploaderName()); if (!TextUtils.isEmpty(result.getUploaderUrl())) { - headerUploaderLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - NavigationHelper.openChannelFragment(getFragmentManager(), result.getServiceId(), result.getUploaderUrl(), result.getUploaderName()); - } - }); + headerUploaderLayout.setOnClickListener(v -> + NavigationHelper.openChannelFragment(getFragmentManager(), + result.getServiceId(), result.getUploaderUrl(), + result.getUploaderName()) + ); } } @@ -225,31 +270,20 @@ public class PlaylistFragment extends BaseListInfoFragment { imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count)); - if (playlistAppendButton != null) playlistAppendButton - .setVisible(!currentInfo.getRelatedStreams().isEmpty()); - if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } - headerPlayAllButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnMainPlayer(activity, getPlayQueue()); - } - }); - headerPopupButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()); - } - }); - headerBackgroundButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()); - } - }); + remotePlaylistManager.onUpdate(result) + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(integer -> {/* Do nothing*/}, this::onError); + + headerPlayAllButton.setOnClickListener(view -> + NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener(view -> + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener(view -> + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); } private PlayQueue getPlayQueue() { @@ -293,9 +327,64 @@ public class PlaylistFragment extends BaseListInfoFragment { // Utils //////////////////////////////////////////////////////////////////////////*/ + private Subscriber> getPlaylistBookmarkSubscriber() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + if (bookmarkReactor != null) bookmarkReactor.cancel(); + bookmarkReactor = s; + bookmarkReactor.request(1); + } + + @Override + public void onNext(List playlist) { + if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return; + + playlistBookmarkButton.setVisible(playlist.isEmpty()); + playlistUnbookmarkButton.setVisible(!playlist.isEmpty()); + playlistEntity = playlist.isEmpty() ? null : playlist.get(0); + + if (bookmarkReactor != null) bookmarkReactor.request(1); + } + + @Override + public void onError(Throwable t) { + PlaylistFragment.this.onError(t); + } + + @Override + public void onComplete() { + + } + }; + } + @Override public void setTitle(String title) { super.setTitle(title); headerTitleView.setText(title); } + + private void bookmarkPlaylist() { + if (remotePlaylistManager == null || currentInfo == null) return; + + playlistBookmarkButton.setVisible(false); + playlistUnbookmarkButton.setVisible(false); + + remotePlaylistManager.onBookmark(currentInfo) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> {/* Do nothing */}, this::onError); + } + + private void unbookmarkPlaylist() { + if (remotePlaylistManager == null || playlistEntity == null) return; + + playlistBookmarkButton.setVisible(false); + playlistUnbookmarkButton.setVisible(false); + + remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) + .observeOn(AndroidSchedulers.mainThread()) + .doFinally(() -> playlistEntity = null) + .subscribe(ignored -> {/* Do nothing */}, this::onError); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 807599678..0ccc13446 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -11,12 +11,12 @@ import org.schabi.newpipe.fragments.local.holder.LocalItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder; import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder; +import org.schabi.newpipe.fragments.local.holder.RemotePlaylistItemHolder; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /* @@ -49,8 +49,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; private final DateFormat dateFormat; @@ -187,7 +188,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter> getPlaylists() { + return playlistRemoteTable.getAll().subscribeOn(Schedulers.io()); + } + + public Flowable> getPlaylist(final int serviceId, final String url) { + return playlistRemoteTable.getPlaylist(serviceId, url).subscribeOn(Schedulers.io()); + } + + public Single deletePlaylist(final long playlistId) { + return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId)) + .subscribeOn(Schedulers.io()); + } + + public Single onBookmark(final PlaylistInfo playlistInfo) { + return Single.fromCallable(() -> { + final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); + return playlistRemoteTable.upsert(playlist); + }).subscribeOn(Schedulers.io()); + } + + public Single onUpdate(final PlaylistInfo playlistInfo) { + return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo))) + .subscribeOn(Schedulers.io()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 5c863590f..51bd312b0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -5,7 +5,7 @@ import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; +import android.support.v4.app.FragmentManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -14,23 +14,30 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; +import org.schabi.newpipe.fragments.local.RemotePlaylistManager; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import icepick.State; +import io.reactivex.Flowable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; public final class BookmarkFragment - extends BaseLocalListFragment, Void> { + extends BaseLocalListFragment, Void> { private View watchHistoryButton; private View mostWatchedButton; @@ -41,6 +48,7 @@ public final class BookmarkFragment private Subscription databaseSubscription; private CompositeDisposable disposables = new CompositeDisposable(); private LocalPlaylistManager localPlaylistManager; + private RemotePlaylistManager remotePlaylistManager; /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Creation @@ -49,7 +57,9 @@ public final class BookmarkFragment @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + final AppDatabase database = NewPipeDatabase.getInstance(getContext()); + localPlaylistManager = new LocalPlaylistManager(database); + remotePlaylistManager = new RemotePlaylistManager(database); disposables = new CompositeDisposable(); } @@ -99,17 +109,28 @@ public final class BookmarkFragment @Override public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement - if (selectedItem instanceof PlaylistMetadataEntry && getParentFragment() != null) { + if (getParentFragment() == null) return; + final FragmentManager fragmentManager = getParentFragment().getFragmentManager(); + + if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - NavigationHelper.openLocalPlaylistFragment( - getParentFragment().getFragmentManager(), entry.uid, entry.name); + NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid, + entry.name); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(), + entry.getUrl(), entry.getName()); } } @Override public void held(LocalItem selectedItem) { if (selectedItem instanceof PlaylistMetadataEntry) { - showDeleteDialog((PlaylistMetadataEntry) selectedItem); + showLocalDeleteDialog((PlaylistMetadataEntry) selectedItem); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem); } } }); @@ -134,9 +155,14 @@ public final class BookmarkFragment @Override public void startLoading(boolean forceLoad) { super.startLoading(forceLoad); - localPlaylistManager.getPlaylists() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscriptionSubscriber()); + + Flowable.combineLatest( + localPlaylistManager.getPlaylists(), + remotePlaylistManager.getPlaylists(), + BookmarkFragment::merge + ).onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistsSubscriber()); } /////////////////////////////////////////////////////////////////////////// @@ -165,6 +191,7 @@ public final class BookmarkFragment disposables = null; localPlaylistManager = null; + remotePlaylistManager = null; itemsListState = null; } @@ -172,8 +199,8 @@ public final class BookmarkFragment // Subscriptions Loader /////////////////////////////////////////////////////////////////////////// - private Subscriber> getSubscriptionSubscriber() { - return new Subscriber>() { + private Subscriber> getPlaylistsSubscriber() { + return new Subscriber>() { @Override public void onSubscribe(Subscription s) { showLoading(); @@ -183,7 +210,7 @@ public final class BookmarkFragment } @Override - public void onNext(List subscriptions) { + public void onNext(List subscriptions) { handleResult(subscriptions); if (databaseSubscription != null) databaseSubscription.request(1); } @@ -200,7 +227,7 @@ public final class BookmarkFragment } @Override - public void handleResult(@NonNull List result) { + public void handleResult(@NonNull List result) { super.handleResult(result); itemListAdapter.clearStreamItemList(); @@ -240,25 +267,41 @@ public final class BookmarkFragment // Utils /////////////////////////////////////////////////////////////////////////// - private void showDeleteDialog(final PlaylistMetadataEntry item) { + private void showLocalDeleteDialog(final PlaylistMetadataEntry item) { + showDeleteDialog(item.name, localPlaylistManager.deletePlaylist(item.uid)); + } + + private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { + showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); + } + + private void showDeleteDialog(final String name, final Single deleteReactor) { + if (activity == null || disposables == null) return; + new AlertDialog.Builder(activity) - .setTitle(item.name) + .setTitle(name) .setMessage(R.string.delete_playlist_prompt) .setCancelable(true) .setPositiveButton(R.string.delete, (dialog, i) -> - disposables.add(deletePlaylist(item.uid)) + disposables.add(deleteReactor + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> {/*Do nothing on success*/}, this::onError)) ) .setNegativeButton(R.string.cancel, null) .show(); } - private Disposable deletePlaylist(final long playlistId) { - return localPlaylistManager.deletePlaylist(playlistId) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> {/*Do nothing on success*/}, - throwable -> Log.e(TAG, "Playlist deletion failed, id=[" - + playlistId + "]") - ); + private static List merge(final List localPlaylists, + final List remotePlaylists) { + List items = new ArrayList<>( + localPlaylists.size() + remotePlaylists.size()); + items.addAll(localPlaylists); + items.addAll(remotePlaylists); + + Collections.sort(items, (left, right) -> + left.getOrderingName().compareToIgnoreCase(right.getOrderingName())); + + return items; } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java index cbc1d07aa..1fbea6cc4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java @@ -14,24 +14,10 @@ import org.schabi.newpipe.fragments.local.LocalItemBuilder; import java.text.DateFormat; -public class LocalPlaylistItemHolder extends LocalItemHolder { - public final ImageView itemThumbnailView; - public final TextView itemStreamCountView; - public final TextView itemTitleView; - public final TextView itemUploaderView; - - public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, - int layoutId, ViewGroup parent) { - super(infoItemBuilder, layoutId, parent); - - itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); - itemTitleView = itemView.findViewById(R.id.itemTitleView); - itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); - itemUploaderView = itemView.findViewById(R.id.itemUploaderView); - } +public class LocalPlaylistItemHolder extends PlaylistItemHolder { public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + super(infoItemBuilder, parent); } @Override @@ -45,29 +31,6 @@ public class LocalPlaylistItemHolder extends LocalItemHolder { itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); - itemView.setOnClickListener(view -> { - if (itemBuilder.getOnItemSelectedListener() != null) { - itemBuilder.getOnItemSelectedListener().selected(item); - } - }); - - itemView.setLongClickable(true); - itemView.setOnLongClickListener(view -> { - if (itemBuilder.getOnItemSelectedListener() != null) { - itemBuilder.getOnItemSelectedListener().held(item); - } - return true; - }); + super.updateFromItem(localItem, dateFormat); } - - /** - * Display options for playlist thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) - .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) - .showImageOnFail(R.drawable.dummy_thumbnail_playlist) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java new file mode 100644 index 000000000..bab76ddcb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java @@ -0,0 +1,62 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; + +import java.text.DateFormat; + +public abstract class PlaylistItemHolder extends LocalItemHolder { + public final ImageView itemThumbnailView; + public final TextView itemStreamCountView; + public final TextView itemTitleView; + public final TextView itemUploaderView; + + public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, + int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemTitleView = itemView.findViewById(R.id.itemTitleView); + itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + } + + public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(localItem); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(localItem); + } + return true; + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java new file mode 100644 index 000000000..0f7b00e6d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java @@ -0,0 +1,33 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.view.ViewGroup; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.Localization; + +import java.text.DateFormat; + +public class RemotePlaylistItemHolder extends PlaylistItemHolder { + public RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + super(infoItemBuilder, parent); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistRemoteEntity)) return; + final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; + + itemTitleView.setText(item.getName()); + itemStreamCountView.setText(String.valueOf(item.getStreamCount())); + itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), + NewPipe.getNameOfService(item.getServiceId()))); + + itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, + DISPLAY_THUMBNAIL_OPTIONS); + + super.updateFromItem(localItem, dateFormat); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..92448842b50fc2c58ba7785c14447e69a0e51303 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8mZytjNCo5Di>!=?4Fp;rsy|)g z6QnX{i)ZH~$CAQ}43wdkdu9 zIA`fiS`h8VD0KLji>&0=iiJ5dbW;!KXn3c28|ox`r#TyTt^CS-z39@>vuhYP0&QjR MboFyt=akR{04w@A{{R30 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bd23b9c481abe4fdafac75b22f43e01ffd4f0621 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8nx~6nNCo5Di|&j^40u=rodu_~ zrf@WE3pkT%#UgxxC8Dzb1+t*q%76?zVy4kHv*A`Z?Vk zE(aV`QC{E^evoDHA$6^D8d~Q#yo_f>o%OkLWX*CL+hyyy9$nb`;^6dkia;9~JYD@< J);T3K0RYYNJH-G1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..416490774d7064fd48305d40e9124de6ea0d2512 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1M^6{WkP60R1*x8<|2Gs*O0_MN zm+>feWxmkBc4>;TNkXX?vp|x@@wEpTm$59ITI UbyV1%1{%iT>FVdQ&MBb@0Gt;jp#T5? literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0e35fe739f9ea8e3fe8f1365a79bfbcfd328e8f0 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1XHOT$kP60R1+ku{{}v1bP0l+XkKFxDtK literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..24855e94f1f97621c1dc74ba1e24d80106190f4a GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DEKe85kP61PQ;rKBF%W4D`L(*{ z`onWQlZ^jZ-|%vEc@p+VdN~(!PX#|u%3S7?{}?U9_WSl-Eqa&fY0qDsoEQ9kWJ=y0{*^3b`R{7wqur4|KXspAa%(MHucSQn0MJ$j MPgg&ebxsLQ0Ig_0g8%>k literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a94c5d035a7b8fd290e821336729a300b8c14b75 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DEKe85kP61PQ&<@p964C*f0@~H zHhkVEoKVoG?#Q92%)u*DwdCe=xJC{Ou%kl_%AYpq3t zfm2{SzX2Od?Q;dz+H?-b`{GU)<_j>X%U%7M&v^dd?5pSfIc9!Z!(taW>yZ}FRt8U3 KKbLh*2~7YA6fmm* literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ac03e19abfc714e97ba924b57fe4770076412b88 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawHha1_hEy=Vy?T6Ri-Ca4Lp5Wj z!_N0^F=VAL`e>oE+DYM$mGmFOk99B>FDHexj6m?B_BcpoL z(rZ(}E60LW_3xDnmsd9hGhVW`e`WDip;j>6C-dCRl2-zK6Y7eV*)53N7E`qHt?KfB zoSG}A1y7&!ZU)aBmdxK@zQ`<5zveF^eDZ+cq}j@E>-_pkxQ{!0DqM2*+(a{L_gimd jGJB7oP*hSf`oezVmbc!XhskC@=QDV^`njxgN@xNA0d!#h literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..290088718fb6f86b6efbd14b59a6f5b204140477 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawHha1_hEy=Vy?Q;d)j@>$qI;^_H=VWYVN=Z2sx$0t6xc;;bA33QCXknORoMyCu(hNhPz&wYYE2lxG&2=TA=V zDQ04M{^skoB`>4TMLe3r@bk>_&vP~_-0z6=ooO8ZtJLDS2W#J~Xosw)x^`3Ate$TY zExP1+?&e8uW9`O9YNzp7H#O$LiuHmFFy%1RjfcIQC8K kne4xJ8-u!ppx{Mmr`EGqYws>B13I6<)78&qol`;+0EvQPkpKVy literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..068c596a3476e67afd160d396e36b493743c2b47 GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg9(lSrhEy=Vz3R%;93XJ)!NYqN zb9U=(b_qUveA}e`hq|>rC#e9@%M)>Y1r5v*KtjcH-VqSH;WT6Jmismf_S)_4)t~b7 z4~xzG`3=uM3!kW|W17IvXK=sffx_*Qu4QSib`}c$feQ2GC)l^?b&3A``KID;!Z;1E}_b4uhM*J16#%u^)DMuQY7Y{_g}Gm|8oC%49`9b zA6sz&WO#q(%bGan=bvZspKw>_nLm5Jl-2y}7k=)Ub?*7q61(-wK3AH~J-<3BX1YGe a@#-e|w{E5=|GEwIErX}4pUXO@geCxg0eV3I literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..767d066de4a1a5067ead23b6ef0276f34d58a24a GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgUU<4VhEy=Vy~>_-I6=TA@%25u ztnc$SY`MTzw@K&Yf!}@x=cjGq2Py=D1sM{IECL4Y3``6R4}{JuG%zr6Tv%so;jsJE zzY_`1rrTE6-Q4^fOgTS4&u%dPXV$Bji}AmH{QS)Luj}0T_v|-*KD}^lM*W(r=0AUC zbJp$KR?l%EuWie-^Xxx7fvGiIRE5Ac^oF^bLDVb%unh^MQc%Q~loCIB>Wg(m<2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 3861a380d..2d39d3d70 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -314,7 +314,7 @@ android:clickable="true" android:focusable="true" android:contentDescription="@string/append_playlist" - android:drawableTop="?attr/playlist_add" + android:drawableTop="?attr/ic_playlist_add" android:gravity="center" android:paddingBottom="6dp" android:paddingTop="6dp" diff --git a/app/src/main/res/menu/menu_channel.xml b/app/src/main/res/menu/menu_channel.xml index 79a0fd5c9..cc6a9ed71 100644 --- a/app/src/main/res/menu/menu_channel.xml +++ b/app/src/main/res/menu/menu_channel.xml @@ -22,12 +22,4 @@ android:icon="?attr/share" android:title="@string/share" app:showAsAction="ifRoom"/> - - diff --git a/app/src/main/res/menu/menu_play_queue.xml b/app/src/main/res/menu/menu_play_queue.xml index 31e2ebe72..6261b8c18 100644 --- a/app/src/main/res/menu/menu_play_queue.xml +++ b/app/src/main/res/menu/menu_play_queue.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/menu/menu_playlist.xml b/app/src/main/res/menu/menu_playlist.xml index a12fb2f49..e0e7ebe18 100644 --- a/app/src/main/res/menu/menu_playlist.xml +++ b/app/src/main/res/menu/menu_playlist.xml @@ -15,10 +15,18 @@ app:showAsAction="ifRoom"/> + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index e770cf102..794365a3d 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -27,7 +27,8 @@ - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05db15da1..032e56b22 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -387,6 +387,9 @@ Add To Playlist Set as Playlist Thumbnail + Bookmark Playlist + Remove Bookmark + Do you want to delete this playlist? Playlist successfully created Added to playlist diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index bcbc759d2..b16958ae6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -42,7 +42,8 @@ @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp @drawable/ic_bookmark_black_24dp - @drawable/ic_playlist_add_black_24dp + @drawable/ic_playlist_add_black_24dp + @drawable/ic_playlist_add_check_black_24dp @color/light_separator_color @color/light_contrast_background_color @@ -91,7 +92,8 @@ @drawable/ic_whatshot_white_24dp @drawable/ic_channel_white_24dp @drawable/ic_bookmark_white_24dp - @drawable/ic_playlist_add_white_24dp + @drawable/ic_playlist_add_white_24dp + @drawable/ic_playlist_add_check_white_24dp @color/dark_separator_color @color/dark_contrast_background_color From 7ab41e0c3aefd33317ecf50d128bcc8b6e4d12bf Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 7 Feb 2018 14:20:16 -0800 Subject: [PATCH 048/276] -Added listener unregistration to local item adapters to release dependency and avoid memory leak. -Added listener unregistration on all listeners using contexts in local item related fragments. --- .../newpipe/fragments/local/LocalItemListAdapter.java | 4 ++++ .../newpipe/fragments/local/LocalPlaylistFragment.java | 5 +++++ .../newpipe/fragments/local/PlaylistAppendDialog.java | 1 + .../newpipe/fragments/local/bookmark/BookmarkFragment.java | 3 +++ .../local/bookmark/StatisticsPlaylistFragment.java | 6 ++++++ .../local/holder/LocalPlaylistStreamItemHolder.java | 2 +- 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java index 0ccc13446..d36f56733 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -71,6 +71,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { if (data != null) { if (DEBUG) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java index ea7242055..abc12fb14 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -230,6 +230,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { view.performClick(); - if (itemBuilder != null && + if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { itemBuilder.getOnItemSelectedListener().drag(item, LocalPlaylistStreamItemHolder.this); From 6020dc2b2d999a950995df9239205f73dd955dd4 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 7 Feb 2018 14:37:05 -0800 Subject: [PATCH 049/276] -Renamed "watch history" fragment under bookmark to "last played". -Renamed "watched history" fragment under history to "watch history". --- .../local/bookmark/BookmarkFragment.java | 18 +++++++-------- ...yFragment.java => LastPlayedFragment.java} | 4 ++-- .../newpipe/history/HistoryActivity.java | 5 +---- ...ragment.java => WatchHistoryFragment.java} | 6 ++--- .../schabi/newpipe/util/NavigationHelper.java | 6 ++--- app/src/main/res/layout/bookmark_header.xml | 22 +++++++++---------- app/src/main/res/values/strings.xml | 2 +- 7 files changed, 30 insertions(+), 33 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/{WatchHistoryFragment.java => LastPlayedFragment.java} (79%) rename app/src/main/java/org/schabi/newpipe/history/{WatchedHistoryFragment.java => WatchHistoryFragment.java} (97%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 93b0dea29..2aa648fa9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -39,8 +39,8 @@ import io.reactivex.disposables.CompositeDisposable; public final class BookmarkFragment extends BaseLocalListFragment, Void> { - private View watchHistoryButton; - private View mostWatchedButton; + private View lastPlayedButton; + private View mostPlayedButton; @State protected Parcelable itemsListState; @@ -96,8 +96,8 @@ public final class BookmarkFragment protected View getListHeader() { final View headerRootLayout = activity.getLayoutInflater() .inflate(R.layout.bookmark_header, itemsList, false); - watchHistoryButton = headerRootLayout.findViewById(R.id.watchHistory); - mostWatchedButton = headerRootLayout.findViewById(R.id.mostWatched); + lastPlayedButton = headerRootLayout.findViewById(R.id.lastPlayed); + mostPlayedButton = headerRootLayout.findViewById(R.id.mostPlayed); return headerRootLayout; } @@ -135,13 +135,13 @@ public final class BookmarkFragment } }); - watchHistoryButton.setOnClickListener(view -> { + lastPlayedButton.setOnClickListener(view -> { if (getParentFragment() != null) { - NavigationHelper.openWatchHistoryFragment(getParentFragment().getFragmentManager()); + NavigationHelper.openLastPlayedFragment(getParentFragment().getFragmentManager()); } }); - mostWatchedButton.setOnClickListener(view -> { + mostPlayedButton.setOnClickListener(view -> { if (getParentFragment() != null) { NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager()); } @@ -178,8 +178,8 @@ public final class BookmarkFragment @Override public void onDestroyView() { super.onDestroyView(); - if (mostWatchedButton != null) mostWatchedButton.setOnClickListener(null); - if (watchHistoryButton != null) watchHistoryButton.setOnClickListener(null); + if (mostPlayedButton != null) mostPlayedButton.setOnClickListener(null); + if (lastPlayedButton != null) lastPlayedButton.setOnClickListener(null); if (disposables != null) disposables.clear(); if (databaseSubscription != null) databaseSubscription.cancel(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LastPlayedFragment.java similarity index 79% rename from app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LastPlayedFragment.java index 84126ad4b..a5b62c63e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LastPlayedFragment.java @@ -6,10 +6,10 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import java.util.Collections; import java.util.List; -public final class WatchHistoryFragment extends StatisticsPlaylistFragment { +public final class LastPlayedFragment extends StatisticsPlaylistFragment { @Override protected String getName() { - return getString(R.string.title_watch_history); + return getString(R.string.title_last_played); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java index 30589a22c..267d07065 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java @@ -9,10 +9,8 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; -import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -23,7 +21,6 @@ import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.ThemeHelper; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.functions.Consumer; public class HistoryActivity extends AppCompatActivity { @@ -116,7 +113,7 @@ public class HistoryActivity extends AppCompatActivity { fragment = SearchHistoryFragment.newInstance(); break; case 1: - fragment = WatchedHistoryFragment.newInstance(); + fragment = WatchHistoryFragment.newInstance(); break; default: throw new IllegalArgumentException("position: " + position); diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java rename to app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java index 0fabc594a..4830ed33b 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java @@ -33,11 +33,11 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; -public class WatchedHistoryFragment extends HistoryFragment { +public class WatchHistoryFragment extends HistoryFragment { @NonNull - public static WatchedHistoryFragment newInstance() { - return new WatchedHistoryFragment(); + public static WatchHistoryFragment newInstance() { + return new WatchHistoryFragment(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 3acfb6683..42cf135e3 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -36,7 +36,7 @@ import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; -import org.schabi.newpipe.fragments.local.bookmark.WatchHistoryFragment; +import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -335,10 +335,10 @@ public class NavigationHelper { .commit(); } - public static void openWatchHistoryFragment(FragmentManager fragmentManager) { + public static void openLastPlayedFragment(FragmentManager fragmentManager) { fragmentManager.beginTransaction() .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) - .replace(R.id.fragment_holder, new WatchHistoryFragment()) + .replace(R.id.fragment_holder, new LastPlayedFragment()) .addToBackStack(null) .commit(); } diff --git a/app/src/main/res/layout/bookmark_header.xml b/app/src/main/res/layout/bookmark_header.xml index b087a5157..8ca5c1228 100644 --- a/app/src/main/res/layout/bookmark_header.xml +++ b/app/src/main/res/layout/bookmark_header.xml @@ -8,14 +8,14 @@ android:background="?attr/selectableItemBackground"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 032e56b22..21579d3e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -313,7 +313,7 @@ Do you want to delete this item from search history? Do you want to delete this item from watch history? Are you sure you want to delete all items from history? - Watch History + Last Played Most Played From c3941d5becccb5c2e960f9ed0c861fe36612ba93 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 7 Feb 2018 17:33:29 -0800 Subject: [PATCH 050/276] -Added remote playlist table creation to migrations. --- app/src/main/java/org/schabi/newpipe/database/Migrations.java | 3 +++ 1 file changed, 3 insertions(+) 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 c6b472f7f..239fc02bb 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -31,6 +31,9 @@ public class Migrations { database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`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)"); database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)"); database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"); + 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`)"); // Populate streams table with existing entries in watch history // Latest data first, thus ignoring older entries with the same indices From a55ee32058e1648ca81ca0720fd94f6fb1263a10 Mon Sep 17 00:00:00 2001 From: ScratchBuild Date: Thu, 8 Feb 2018 07:12:00 +0000 Subject: [PATCH 051/276] Translated using Weblate (Japanese) Currently translated at 78.9% (218 of 276 strings) --- app/src/main/res/values-ja/strings.xml | 44 ++++++++++++++++---------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 140c434a3..1079128bc 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -20,9 +20,9 @@ Kodi で再生 Koreが見つかりません。Kore を入手しますか? \"Kodi で再生\" オプションを表示 - Kodi メディアセンター経由で動画を再生するための設定を表示します + Kodi メディアセンター経由で動画を再生するための設定を表示します 音楽 - 基本の音楽形式 + デフォルトの音楽形式 WebM形式 m4a形式 保存 @@ -38,7 +38,7 @@ 低評価 高評価 外部プレイヤーを使用する - 外部プレイヤーを使用する + 外部プレイヤーを使用する バックグラウンドで再生中 再生 @@ -55,18 +55,18 @@ 音楽を保存する場所 音楽を保存する場所 - 音楽ファイルをダウンロードする場所を入力して下さい。 + 音楽ファイルをダウンロードする場所を入力して下さい。 保存場所 \'%1$s\' を作成できません 保存場所 \'%1$s\' を作成しました エラー - 全てのサムネイルを読み込むことができません + 全てのサムネイルを読み込むことができません 動画のURL署名を復号できませんでした - Webサイトを解析できませんでした - コンテンツがありません + Webサイトを解析できませんでした + コンテンツが利用できません GEMA によって阻止されました - 保存メニューを設定できませんでした + 保存メニューを設定できませんでした 生放送には対応していません @@ -77,9 +77,9 @@ この動画には、年齢制限があります。設定から制限を解除して下さい。 Webサイトを完全には解析できませんでした - ストリームを取得できませんでした + 動画を取得できませんでした 申し訳ありません。発生すべきではありませんでした。 - メールで不具合を報告 + メールで不具合を報告 複数の不具合が発生しました 報告 情報: @@ -93,7 +93,7 @@ 再試行 ストレージへのアクセスが拒否されました 自動再生 - NewPipeが他のアプリケーションから呼び出された際に、自動的に動画を再生します。 + NewPipeが他のアプリから呼び出された際に、自動的に動画を再生します。 不具合を報告 利用者報告 @@ -109,11 +109,11 @@ 新しいミッション OK - ファイル名 + ファイル名 同時接続数 エラー - サーバーが対応していません - ファイルが既に存在します + サーバーが対応していません + ファイルが既に存在します URL の形式が正しくないか、通信が利用できません NewPipeの保存中 詳細はタップ @@ -127,7 +127,7 @@ 画像を読み込みできません - アプリ/UI がクラッシュしました + アプリ/UI がクラッシュしました 何:\\n提案:\\nコンテンツ言語:\\nサービス:\\nGMT 時間:\\nパッケージ:\\nバージョン:\\nOS バージョン:\\nグローバル IP 範囲: reCAPTCHA reCAPTCHA の要求 @@ -229,7 +229,7 @@ NewPipeの通知 [不明] - ストリームの再生に失敗しました + 動画の再生に失敗しました 回復不能な再生エラーが発生しました 何も見つかりませんでした チャンネル登録なし @@ -279,4 +279,16 @@ メイン再生に変更 動画プレイヤーが見つかりません (VLCをインストールして再生できます) + デフォルトのコンテンツの国 + サービス + 常に + 一度だけ + + データベースのインポート + データベースのエクスポート + 既存の履歴と購読リストは上書きされます + 履歴や購読リスト、プレイリストをエクスポートします。 + 再生エラーからの回復中 + 外部プレーヤーは、これらのタイプのリンクをサポートしていません + 無効なURL From b01ae33d1eab6a93a2e367d99d7d65be919b0629 Mon Sep 17 00:00:00 2001 From: Rintaro matsuo Date: Thu, 8 Feb 2018 07:12:44 +0000 Subject: [PATCH 052/276] Translated using Weblate (Japanese) Currently translated at 78.9% (218 of 276 strings) --- app/src/main/res/values-ja/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1079128bc..a2a6197a6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -116,7 +116,7 @@ ファイルが既に存在します URL の形式が正しくないか、通信が利用できません NewPipeの保存中 - 詳細はタップ + タップして詳細を表示 お待ちください… クリップボードにコピーしました 利用可能な保存場所を選択して下さい From 0630423c8e41042521702a2b087391a1a1265bdc Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 10:13:29 -0800 Subject: [PATCH 053/276] -Fixed bookmark fragment in main pager not showing hamburger menu. --- .../local/BaseLocalListFragment.java | 29 +++++++++++++------ .../local/bookmark/BookmarkFragment.java | 9 ++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java index c0c4362eb..53786d2ac 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java @@ -86,19 +86,30 @@ public abstract class BaseLocalListFragment extends BaseStateFragment // Lifecycle - Menu //////////////////////////////////////////////////////////////////////////*/ + /** Determines if the fragment is part of the main fragment view pager. + * If so, then this method must be overriden to return true + * in order to show the hamburger menu. */ + protected boolean isPartOfFrontPager() { + return false; + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); - super.onCreateOptionsMenu(menu, inflater); - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayShowTitleEnabled(true); - if(useAsFrontPage) { - supportActionBar.setDisplayHomeAsUpEnabled(false); - } else { - supportActionBar.setDisplayHomeAsUpEnabled(true); - } + + final ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar == null) return; + + supportActionBar.setDisplayShowTitleEnabled(true); + + // Show up arrow icon if the fragment is not used as front page or part of the front pager + if (!useAsFrontPage && !isPartOfFrontPager()) { + // If set true, an up arrow icon will be displayed. + // If set false, no icon will be shown. + // If unset, show hamburger menu + supportActionBar.setDisplayHomeAsUpEnabled(true); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 2aa648fa9..a2e00429b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -148,6 +148,15 @@ public final class BookmarkFragment }); } + /*////////////////////////////////////////////////////////////////////////// + // Fragment Lifecycle - Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected boolean isPartOfFrontPager() { + return true; + } + /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Loading /////////////////////////////////////////////////////////////////////////// From 490b250db687f2847e4b4371149c9786b03f627c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 11:53:08 -0800 Subject: [PATCH 054/276] -Removed Leak Canary dependency. -Fixed local playlist header margins. --- app/build.gradle | 2 -- app/src/debug/java/org/schabi/newpipe/DebugApp.java | 9 --------- app/src/main/res/layout/local_playlist_header.xml | 9 +++++---- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 748bbb9c6..86d6542e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,6 +89,4 @@ dependencies { implementation 'frankiesardo:icepick:3.2.0' annotationProcessor 'frankiesardo:icepick-processor:3.2.0' - - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' } diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index fbf414738..4d37094ba 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -4,7 +4,6 @@ import android.content.Context; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; -import com.squareup.leakcanary.LeakCanary; public class DebugApp extends App { private static final String TAG = DebugApp.class.toString(); @@ -18,14 +17,6 @@ public class DebugApp extends App { @Override public void onCreate() { super.onCreate(); - - if (LeakCanary.isInAnalyzerProcess(this)) { - // This process is dedicated to LeakCanary for heap analysis. - // You should not init your app in this process. - return; - } - LeakCanary.install(this); - initStetho(); } diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml index ab5dd4440..420da04ee 100644 --- a/app/src/main/res/layout/local_playlist_header.xml +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -5,13 +5,15 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="6dp" + android:paddingTop="6dp" android:background="?attr/contrast_background_color"> Date: Thu, 8 Feb 2018 15:58:48 -0800 Subject: [PATCH 055/276] -Fixed playlist bookmark button not showing out when activity / playlist fragment is created by external share. --- .../fragments/list/playlist/PlaylistFragment.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 39c88f8d3..9b57e7181 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -111,6 +111,7 @@ public class PlaylistFragment extends BaseListInfoFragment { headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); + return headerRootLayout; } @@ -175,6 +176,8 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark); playlistUnbookmarkButton = menu.findItem(R.id.menu_item_unbookmark); + + updateBookmarkButtonsVisibility(); } @Override @@ -338,11 +341,8 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void onNext(List playlist) { - if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return; - - playlistBookmarkButton.setVisible(playlist.isEmpty()); - playlistUnbookmarkButton.setVisible(!playlist.isEmpty()); playlistEntity = playlist.isEmpty() ? null : playlist.get(0); + updateBookmarkButtonsVisibility(); if (bookmarkReactor != null) bookmarkReactor.request(1); } @@ -387,4 +387,11 @@ public class PlaylistFragment extends BaseListInfoFragment { .doFinally(() -> playlistEntity = null) .subscribe(ignored -> {/* Do nothing */}, this::onError); } + + private void updateBookmarkButtonsVisibility() { + if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return; + + playlistBookmarkButton.setVisible(playlistEntity == null); + playlistUnbookmarkButton.setVisible(playlistEntity != null); + } } From 10700007d519b21bb6f3bd5f597bf6762c3074dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 9 Feb 2018 01:51:07 +0000 Subject: [PATCH 056/276] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-nb-rNO/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 95c4d63e2..d8dc4756a 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -324,4 +324,14 @@ Henter informasjon… Forespurt innhold innlastes - +Importer database + Eksporter database + Vil overstyre din nåværende historikk og abonnementsliste + Eksporter historikk, abonnementer og spillelister. + Eksport fullført + Import fullført + Ingen gyldig ZIP-fil + ADVARSEL: Kunne ikke importere alle filer. + Dette vil overskrive ditt nåværende oppsett. + + From 1b1dd6ef883a4c909c72d263df08973fcfe96514 Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Fri, 9 Feb 2018 01:47:57 +0000 Subject: [PATCH 057/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 96.0% (265 of 276 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index b088fd4bd..767046126 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -304,4 +304,5 @@ 在背景這裡開始 在懸浮視窗這裡開始 + 維持在佇列 From e0d21627bb4097e843855d7855cebe43efe0b198 Mon Sep 17 00:00:00 2001 From: ScratchBuild Date: Thu, 8 Feb 2018 07:27:17 +0000 Subject: [PATCH 058/276] Translated using Weblate (Japanese) Currently translated at 85.5% (236 of 276 strings) --- app/src/main/res/values-ja/strings.xml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a2a6197a6..00cca771f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -291,4 +291,25 @@ 再生エラーからの回復中 外部プレーヤーは、これらのタイプのリンクをサポートしていません 無効なURL - + エクスポートが完了しました + インポートが完了しました + 有効なZipファイルがありません + 警告: すべてのファイルをインポートできませんでした。 + これにより、現在の設定が上書きされます。 + + バックグラウンド再生 + ここから再生を開始 + ここからバックグランド再生を開始 + ドロワーを開く + ドロワーを閉じる + 優先プレーヤーで開く + 優先プレーヤー + + 動画プレーヤー + バックグラウンドプレーヤー + ポップアッププレーヤー + 常に尋ねる + + 情報を取得しています… + 要求したコンテンツを読み込んでいます + From 43ab0283d934babeb9f097a9c0e8a5fbfe5cfb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 9 Feb 2018 02:53:59 +0100 Subject: [PATCH 059/276] ZIP, Warning: --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d05d088d..b23e1db93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -323,8 +323,8 @@ Select a kiosk Export complete Import complete - No valid Zip file - WARNING: Could not import all files. + No valid ZIP file + Warning: Could not import all files. This will override your current setup. From 7b19dadbf5ebbe7c9d1e1ed69632ef6ff1a7932c Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Fri, 9 Feb 2018 02:10:02 +0000 Subject: [PATCH 060/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 767046126..aa5fec548 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -305,4 +305,17 @@ 在懸浮視窗這裡開始 維持在佇列 - + NewPipe 由志願者所開發,他們花費了空閒時間將獲得的最佳體驗帶給您。現在是時候回過頭來,讓我們的開發人員可以能夠在享受一杯 java 的同時,使 NewPipe 更加的美好! + 打開抽屜 + 關閉抽屜 + 開啟優先的播放器 + 優先播放機 + + 影片播放 + 背景播放 + 懸浮視窗播放 + 總是詢問 + + 正在取得資訊… + 正在載入要求的內容 + From d0808ce1590b6d43f15ef1984a1a55807d3c73c5 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 18:48:36 -0800 Subject: [PATCH 061/276] -Fixed playlist creation icon in playlist append dialog. -Fixed bookmarking disposable not part of playlist fragment lifecycle. -Rearranged local fragment directory structure. --- .../newpipe/fragments/detail/VideoDetailFragment.java | 2 +- .../schabi/newpipe/fragments/list/BaseListFragment.java | 3 +-- .../newpipe/fragments/list/playlist/PlaylistFragment.java | 7 +++++-- .../local/{ => bookmark}/BaseLocalListFragment.java | 3 ++- .../newpipe/fragments/local/bookmark/BookmarkFragment.java | 1 - .../local/{ => bookmark}/LocalPlaylistFragment.java | 3 ++- .../local/bookmark/StatisticsPlaylistFragment.java | 1 - .../fragments/local/{ => dialog}/PlaylistAppendDialog.java | 4 +++- .../local/{ => dialog}/PlaylistCreationDialog.java | 3 ++- .../fragments/local/{ => dialog}/PlaylistDialog.java | 2 +- .../org/schabi/newpipe/player/ServicePlayerActivity.java | 2 +- .../java/org/schabi/newpipe/util/NavigationHelper.java | 2 +- app/src/main/res/layout/dialog_playlists.xml | 2 +- 13 files changed, 20 insertions(+), 15 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => bookmark}/BaseLocalListFragment.java (98%) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => bookmark}/LocalPlaylistFragment.java (99%) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => dialog}/PlaylistAppendDialog.java (97%) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => dialog}/PlaylistCreationDialog.java (95%) rename app/src/main/java/org/schabi/newpipe/fragments/local/{ => dialog}/PlaylistDialog.java (97%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 923feeba0..89f35c306 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -58,7 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; +import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.player.MainVideoPlayer; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 1a0a836c5..8c9945149 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -20,7 +20,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; +import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.playlist.SinglePlayQueue; @@ -28,7 +28,6 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Queue; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 9b57e7181..15c9d4b38 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -42,6 +42,7 @@ import java.util.List; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -371,9 +372,10 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistBookmarkButton.setVisible(false); playlistUnbookmarkButton.setVisible(false); - remotePlaylistManager.onBookmark(currentInfo) + final Disposable disposable = remotePlaylistManager.onBookmark(currentInfo) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> {/* Do nothing */}, this::onError); + disposables.add(disposable); } private void unbookmarkPlaylist() { @@ -382,10 +384,11 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistBookmarkButton.setVisible(false); playlistUnbookmarkButton.setVisible(false); - remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) + final Disposable disposable = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) .observeOn(AndroidSchedulers.mainThread()) .doFinally(() -> playlistEntity = null) .subscribe(ignored -> {/* Do nothing */}, this::onError); + disposables.add(disposable); } private void updateBookmarkButtonsVisibility() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java index 53786d2ac..261f28d7c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -13,6 +13,7 @@ import android.view.View; import org.schabi.newpipe.R; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.ListViewContract; +import org.schabi.newpipe.fragments.local.LocalItemListAdapter; import static org.schabi.newpipe.util.AnimationUtils.animateView; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index a2e00429b..4166f462b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -19,7 +19,6 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import org.schabi.newpipe.fragments.local.RemotePlaylistManager; import org.schabi.newpipe.report.UserAction; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java similarity index 99% rename from app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java index abc12fb14..a3a78e46e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; import android.app.Activity; import android.content.Context; @@ -26,6 +26,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 543fc03eb..d9bbc68c8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -17,7 +17,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.local.BaseLocalListFragment; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.playlist.PlayQueue; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistAppendDialog.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistAppendDialog.java index 034411e4a..40637e149 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistAppendDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.dialog; import android.annotation.SuppressLint; import android.os.Bundle; @@ -18,6 +18,8 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.local.LocalItemListAdapter; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.OnClickGesture; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistCreationDialog.java similarity index 95% rename from app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistCreationDialog.java index 670ae9819..f721e7701 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistCreationDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.dialog; import android.app.AlertDialog; import android.app.Dialog; @@ -12,6 +12,7 @@ import android.widget.Toast; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; import java.util.List; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistDialog.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistDialog.java index 010ba0181..a632988c4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/dialog/PlaylistDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.dialog; import android.os.Bundle; import android.support.annotation.NonNull; diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 6e0f5c1d7..5518357a8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.Player; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.fragments.local.PlaylistAppendDialog; +import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 42cf135e3..4c7b32954 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -34,7 +34,7 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; -import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; +import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment; import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; import org.schabi.newpipe.history.HistoryActivity; diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml index 8c639fff6..c08aa315e 100644 --- a/app/src/main/res/layout/dialog_playlists.xml +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -19,7 +19,7 @@ android:layout_centerVertical="true" android:layout_marginLeft="12dp" android:layout_marginRight="12dp" - android:src="?attr/palette" + android:src="?attr/ic_playlist_add" tools:ignore="ContentDescription,RtlHardcoded"/> Date: Thu, 8 Feb 2018 19:53:04 -0800 Subject: [PATCH 062/276] -Merged bookmark buttons on playlist fragment into one. -Fixed bookmark button flickering on visibility toggling. -Removed toolbar up button control from local fragments, delegating functionality back to main fragment. -Updated extractor to latest. --- app/build.gradle | 2 +- .../list/playlist/PlaylistFragment.java | 72 ++++++++++--------- .../local/bookmark/BaseLocalListFragment.java | 15 ---- .../local/bookmark/BookmarkFragment.java | 9 --- app/src/main/res/menu/menu_playlist.xml | 12 +--- 5 files changed, 43 insertions(+), 67 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 86d6542e0..273616f91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:7fd21ec08581d' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:4fb49d54b5' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 15c9d4b38..2c0b94c69 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -36,13 +36,16 @@ import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ThemeHelper; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -50,6 +53,7 @@ public class PlaylistFragment extends BaseListInfoFragment { private CompositeDisposable disposables; private Subscription bookmarkReactor; + private AtomicBoolean isBookmarkButtonReady; private RemotePlaylistManager remotePlaylistManager; private PlaylistRemoteEntity playlistEntity; @@ -70,7 +74,6 @@ public class PlaylistFragment extends BaseListInfoFragment { private View headerBackgroundButton; private MenuItem playlistBookmarkButton; - private MenuItem playlistUnbookmarkButton; public static PlaylistFragment getInstance(int serviceId, String url, String name) { PlaylistFragment instance = new PlaylistFragment(); @@ -86,6 +89,7 @@ public class PlaylistFragment extends BaseListInfoFragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); disposables = new CompositeDisposable(); + isBookmarkButtonReady = new AtomicBoolean(false); remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext())); } @@ -176,14 +180,14 @@ public class PlaylistFragment extends BaseListInfoFragment { inflater.inflate(R.menu.menu_playlist, menu); playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark); - playlistUnbookmarkButton = menu.findItem(R.id.menu_item_unbookmark); - - updateBookmarkButtonsVisibility(); + updateBookmarkButtons(); } @Override public void onDestroyView() { super.onDestroyView(); + if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false); + if (disposables != null) disposables.clear(); if (bookmarkReactor != null) bookmarkReactor.cancel(); @@ -199,6 +203,7 @@ public class PlaylistFragment extends BaseListInfoFragment { disposables = null; remotePlaylistManager = null; playlistEntity = null; + isBookmarkButtonReady = null; } /*////////////////////////////////////////////////////////////////////////// @@ -225,10 +230,7 @@ public class PlaylistFragment extends BaseListInfoFragment { shareUrl(name, url); break; case R.id.menu_item_bookmark: - bookmarkPlaylist(); - break; - case R.id.menu_item_unbookmark: - unbookmarkPlaylist(); + onBookmarkClicked(); break; default: return super.onOptionsItemSelected(item); @@ -343,7 +345,9 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void onNext(List playlist) { playlistEntity = playlist.isEmpty() ? null : playlist.get(0); - updateBookmarkButtonsVisibility(); + + updateBookmarkButtons(); + isBookmarkButtonReady.set(true); if (bookmarkReactor != null) bookmarkReactor.request(1); } @@ -366,35 +370,39 @@ public class PlaylistFragment extends BaseListInfoFragment { headerTitleView.setText(title); } - private void bookmarkPlaylist() { - if (remotePlaylistManager == null || currentInfo == null) return; + private void onBookmarkClicked() { + if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() || + remotePlaylistManager == null) + return; - playlistBookmarkButton.setVisible(false); - playlistUnbookmarkButton.setVisible(false); + final Disposable action; - final Disposable disposable = remotePlaylistManager.onBookmark(currentInfo) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> {/* Do nothing */}, this::onError); - disposables.add(disposable); + if (currentInfo != null && playlistEntity == null) { + action = remotePlaylistManager.onBookmark(currentInfo) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> {/* Do nothing */}, this::onError); + } else if (playlistEntity != null) { + action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) + .observeOn(AndroidSchedulers.mainThread()) + .doFinally(() -> playlistEntity = null) + .subscribe(ignored -> {/* Do nothing */}, this::onError); + } else { + action = Disposables.empty(); + } + + disposables.add(action); } - private void unbookmarkPlaylist() { - if (remotePlaylistManager == null || playlistEntity == null) return; + private void updateBookmarkButtons() { + if (playlistBookmarkButton == null || activity == null) return; - playlistBookmarkButton.setVisible(false); - playlistUnbookmarkButton.setVisible(false); + final int iconAttr = playlistEntity == null ? + R.attr.ic_playlist_add : R.attr.ic_playlist_check; - final Disposable disposable = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) - .observeOn(AndroidSchedulers.mainThread()) - .doFinally(() -> playlistEntity = null) - .subscribe(ignored -> {/* Do nothing */}, this::onError); - disposables.add(disposable); - } + final int titleRes = playlistEntity == null ? + R.string.bookmark_playlist : R.string.unbookmark_playlist; - private void updateBookmarkButtonsVisibility() { - if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return; - - playlistBookmarkButton.setVisible(playlistEntity == null); - playlistUnbookmarkButton.setVisible(playlistEntity != null); + playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); + playlistBookmarkButton.setTitle(titleRes); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java index 261f28d7c..d2c4e1b14 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java @@ -87,13 +87,6 @@ public abstract class BaseLocalListFragment extends BaseStateFragment // Lifecycle - Menu //////////////////////////////////////////////////////////////////////////*/ - /** Determines if the fragment is part of the main fragment view pager. - * If so, then this method must be overriden to return true - * in order to show the hamburger menu. */ - protected boolean isPartOfFrontPager() { - return false; - } - @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); @@ -104,14 +97,6 @@ public abstract class BaseLocalListFragment extends BaseStateFragment if (supportActionBar == null) return; supportActionBar.setDisplayShowTitleEnabled(true); - - // Show up arrow icon if the fragment is not used as front page or part of the front pager - if (!useAsFrontPage && !isPartOfFrontPager()) { - // If set true, an up arrow icon will be displayed. - // If set false, no icon will be shown. - // If unset, show hamburger menu - supportActionBar.setDisplayHomeAsUpEnabled(true); - } } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 4166f462b..21aceade8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -147,15 +147,6 @@ public final class BookmarkFragment }); } - /*////////////////////////////////////////////////////////////////////////// - // Fragment Lifecycle - Menu - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected boolean isPartOfFrontPager() { - return true; - } - /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Loading /////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/res/menu/menu_playlist.xml b/app/src/main/res/menu/menu_playlist.xml index e0e7ebe18..4583ff719 100644 --- a/app/src/main/res/menu/menu_playlist.xml +++ b/app/src/main/res/menu/menu_playlist.xml @@ -18,15 +18,7 @@ android:id="@+id/menu_item_bookmark" android:icon="?attr/ic_playlist_add" android:title="@string/bookmark_playlist" - android:visible="false" - app:showAsAction="always" - tools:visible="true"/> - - \ No newline at end of file From 7f3982d153dbfb68de85c47610ffe28445dbe334 Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Feb 2018 13:30:08 +0000 Subject: [PATCH 063/276] Translated using Weblate (French) Currently translated at 97.1% (268 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e7a459c07..7e39f2207 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -328,4 +328,5 @@ Obtention des infos… Le contenu demandé est en chargement + Importer les données d\'utilisateur From 668e2da01b36075297b30bc83cf43bd1adfe0530 Mon Sep 17 00:00:00 2001 From: M1ck Date: Fri, 9 Feb 2018 13:30:30 +0000 Subject: [PATCH 064/276] Translated using Weblate (French) Currently translated at 97.1% (268 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7e39f2207..dbaddde91 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -328,5 +328,5 @@ Obtention des infos… Le contenu demandé est en chargement - Importer les données d\'utilisateur + Importer les données From 817fa57bfe05f3aa310c2c1cc537dc06c84d2758 Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Feb 2018 13:30:44 +0000 Subject: [PATCH 065/276] Translated using Weblate (French) Currently translated at 97.4% (269 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index dbaddde91..707574a9c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -329,4 +329,5 @@ Obtention des infos… Le contenu demandé est en chargement Importer les données + Exporter les données d\'utilisateur From 44fc8d80e040f40d153ece004265ef1596bd96a4 Mon Sep 17 00:00:00 2001 From: M1ck Date: Fri, 9 Feb 2018 13:30:56 +0000 Subject: [PATCH 066/276] Translated using Weblate (French) Currently translated at 97.4% (269 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 707574a9c..6c652450d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -329,5 +329,5 @@ Obtention des infos… Le contenu demandé est en chargement Importer les données - Exporter les données d\'utilisateur + Exporter les données From 5aa9b6cb1276cabe7a1e3021802b9130b2baa923 Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Feb 2018 13:32:47 +0000 Subject: [PATCH 067/276] Translated using Weblate (French) Currently translated at 98.5% (272 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6c652450d..219c4fcd8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -330,4 +330,7 @@ Le contenu demandé est en chargement Importer les données Exporter les données + Cela effacera vos données actuelles + Exporter votre historique, vos abonnements et vos listes de lecture + "Exportation terminée " From 50cdadc4a2f30a0863ad52eb99d59db4e8efdf56 Mon Sep 17 00:00:00 2001 From: M1ck Date: Fri, 9 Feb 2018 13:33:07 +0000 Subject: [PATCH 068/276] Translated using Weblate (French) Currently translated at 98.5% (272 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 219c4fcd8..d3fdaa459 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -332,5 +332,5 @@ Exporter les données Cela effacera vos données actuelles Exporter votre historique, vos abonnements et vos listes de lecture - "Exportation terminée " + Exportation terminée From d50d4254c56fe9ab22150676a0d2dcb78402eb2a Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Feb 2018 13:33:23 +0000 Subject: [PATCH 069/276] Translated using Weblate (French) Currently translated at 98.9% (273 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d3fdaa459..23e9453b8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -333,4 +333,5 @@ Cela effacera vos données actuelles Exporter votre historique, vos abonnements et vos listes de lecture Exportation terminée + Importation terminée From a3426f92ace707f909320e3408018295a55fc6cb Mon Sep 17 00:00:00 2001 From: M1ck Date: Fri, 9 Feb 2018 13:37:44 +0000 Subject: [PATCH 070/276] Translated using Weblate (French) Currently translated at 99.6% (275 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 23e9453b8..60cd287be 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -334,4 +334,6 @@ Exporter votre historique, vos abonnements et vos listes de lecture Exportation terminée Importation terminée + Aucun fichier ZIP valide + Avertissement : Impossible d\'importer tous les fichiers. From aa0196b9d09d85969050cc9f4afc323f1852d254 Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Feb 2018 13:38:10 +0000 Subject: [PATCH 071/276] Translated using Weblate (French) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 60cd287be..c9bb68c52 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -336,4 +336,6 @@ Importation terminée Aucun fichier ZIP valide Avertissement : Impossible d\'importer tous les fichiers. + Cela écrasera vos réglages actuels + From 08fdef4870b992a0acbbdde65ece592f0e6e60e4 Mon Sep 17 00:00:00 2001 From: E T Date: Fri, 9 Feb 2018 16:32:51 +0000 Subject: [PATCH 072/276] Translated using Weblate (Turkish) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-tr/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 43f07e947..c19e13e29 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -326,4 +326,14 @@ Bilgi alınıyor… İstenen içerik yükleniyor - +Veri tabanını içe aktar + Veri tabanını dışa aktar + Şimdiki geçmişinizi ve aboneliklerinizi geçersiz kılacak + Geçmişi, abonelikleri ve oynatma listelerini dışa aktar. + Dışa aktarım bitti + İçe aktarım bitti + Geçerli Zip dosyası yok + UYARI: Tüm dosyalar içe aktarılamadı. + Bu şimdiki kurulumunuzu geçersiz kılacak. + + From 8fb7d64f790323bd61f4fdf7b32ba2ace38f0c3b Mon Sep 17 00:00:00 2001 From: M1ck Date: Fri, 9 Feb 2018 13:39:56 +0000 Subject: [PATCH 073/276] Translated using Weblate (French) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c9bb68c52..fa9692a6c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -336,6 +336,6 @@ Importation terminée Aucun fichier ZIP valide Avertissement : Impossible d\'importer tous les fichiers. - Cela écrasera vos réglages actuels + Cela effacera vos paramètres actuels From 39e1f9cb7660256b6605c87e98171ed0c3702b29 Mon Sep 17 00:00:00 2001 From: Eduardo Caron Date: Fri, 9 Feb 2018 14:25:30 +0000 Subject: [PATCH 074/276] Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.2% (274 of 276 strings) --- app/src/main/res/values-pt-rBR/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b207ae3ac..e971c46c0 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -310,4 +310,14 @@ abrir em modo popup Obtendo informações… O conteúdo solicitado está carregando - +Importar base de dados + Exportar base de dados + Isso irá sobrescrever seu histórico e inscrições + Exportar histórico, inscrições e listas de reprodução. + Exportação completa + Importação completa + Não há nenhum arquivo Zip válido + Aviso: Não foi possível importar todos arquivos. + Isso irá sobrescrever suas configurações atuais. + + From 6d27aea9f277f8600b31c62d03ccb025e40949c9 Mon Sep 17 00:00:00 2001 From: Enol P Date: Sat, 10 Feb 2018 15:43:11 +0000 Subject: [PATCH 075/276] Translated using Weblate (Asturian) Currently translated at 100.0% (276 of 276 strings) --- app/src/main/res/values-b+ast/strings.xml | 40 ++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml index 6c361babb..5c3aab961 100644 --- a/app/src/main/res/values-b+ast/strings.xml +++ b/app/src/main/res/values-b+ast/strings.xml @@ -295,4 +295,42 @@ Devolver favor Sitiu web Pa consiguir información y les anuncies caberes tocante a NewPipe visita\'l nuesu sitiu web. - + Nun s\'alcontró un reproductor de fluxos (pues instalar VLC pa reproducilu) + País predetermináu de conteníu + Serviciu + Siempres + Namái un vegada + + Alternar orientación + Camudar a segundu planu + Camudar a ventanu + Camudar a principal + + Importar base de datos + Esportar base de datos + Va sobrescribise l\'historial y soscripciones actuales + Esporta l\'hisotorial, soscripciones y llistaos de reproducción. + Los reproductories esternos nun sofites estes tribes d\'enllaces + URL non válida + Nun s\'alcontraron fluxos de videu + Nun s\'alcontraron fluxos de videu + + Esportación completada + Importación completada + Ficheru ZIP non válidu + ALVERTENCIA: Nun pudieron importase tolos ficheros. + Esto va sobrescribir la configuración actual. + + Abrir caxón + Zarrar caxón + Abrir col reproductor preferíu + Reproductor preferíu + + Reproductor de videu + Reproductor en segundu planu + Reproductor en ventanu + Entrugar siempres + + Consiguiendo información… + Ta cargando\'l conteníu solicitáu + From cb41afb11fd5fb5d7de9b5c5f581da150b4a672a Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 10 Feb 2018 17:20:56 -0800 Subject: [PATCH 076/276] -Fixed Soundcloud playlist bookmark button not working when entered from search page. -Fixed NPE when playlist fragment is destroyed while renaming. -Fixed remote playlist thumbnail to use uploader avatar when thumbnail url is unavailable. -Added dispose on exit to all database requests in local playlist fragment. --- .../playlist/model/PlaylistRemoteEntity.java | 3 +- .../list/playlist/PlaylistFragment.java | 10 ++--- .../local/RemotePlaylistManager.java | 5 ++- .../local/bookmark/LocalPlaylistFragment.java | 41 ++++++++++++++----- 4 files changed, 40 insertions(+), 19 deletions(-) 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 5e3db62a9..486350fc9 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 @@ -66,7 +66,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { @Ignore public PlaylistRemoteEntity(final PlaylistInfo info) { - this(info.getServiceId(), info.getName(), info.getUrl(), info.getThumbnailUrl(), + this(info.getServiceId(), info.getName(), info.getUrl(), + info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(), info.getUploaderName(), info.getStreamCount()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 2c0b94c69..db382ef5d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -125,11 +125,6 @@ public class PlaylistFragment extends BaseListInfoFragment { super.initViews(rootView, savedInstanceState); infoListAdapter.useMiniItemVariants(true); - - remotePlaylistManager.getPlaylist(serviceId, url) - .onBackpressureLatest() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getPlaylistBookmarkSubscriber()); } @Override @@ -280,6 +275,11 @@ public class PlaylistFragment extends BaseListInfoFragment { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } + remotePlaylistManager.getPlaylist(result) + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistBookmarkSubscriber()); + remotePlaylistManager.onUpdate(result) .subscribeOn(AndroidSchedulers.mainThread()) .subscribe(integer -> {/* Do nothing*/}, this::onError); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java index 3012f3d73..1e9be5638 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/RemotePlaylistManager.java @@ -25,8 +25,9 @@ public class RemotePlaylistManager { return playlistRemoteTable.getAll().subscribeOn(Schedulers.io()); } - public Flowable> getPlaylist(final int serviceId, final String url) { - return playlistRemoteTable.getPlaylist(serviceId, url).subscribeOn(Schedulers.io()); + public Flowable> getPlaylist(final PlaylistInfo info) { + return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl()) + .subscribeOn(Schedulers.io()); } public Single deletePlaylist(final long playlistId) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java index a3a78e46e..20eee38fc 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/LocalPlaylistFragment.java @@ -43,7 +43,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -76,7 +78,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment debouncedSaveSignal; - private Disposable debouncedSaver; + private CompositeDisposable disposables; /* Has the playlist been fully loaded from db */ private AtomicBoolean isLoadingComplete; @@ -99,6 +101,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment - changePlaylistName(nameEdit.getText().toString()) - ); + .setPositiveButton(R.string.rename, (dialogInterface, i) -> { + changePlaylistName(nameEdit.getText().toString()); + }); dialogBuilder.show(); } private void changePlaylistName(final String name) { + if (playlistManager == null) return; + this.name = name; setTitle(name); Log.d(TAG, "Updating playlist id=[" + playlistId + "] with new name=[" + name + "] items"); - playlistManager.renamePlaylist(playlistId, name) + final Disposable disposable = playlistManager.renamePlaylist(playlistId, name) .observeOn(AndroidSchedulers.mainThread()) .subscribe(longs -> {/*Do nothing on success*/}, this::onError); + disposables.add(disposable); } private void changeThumbnailUrl(final String thumbnailUrl) { + if (playlistManager == null) return; + final Toast successToast = Toast.makeText(getActivity(), R.string.playlist_thumbnail_change_success, Toast.LENGTH_SHORT); @@ -385,9 +395,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment successToast.show(), this::onError); + disposables.add(disposable); } private void deleteItem(final PlaylistStreamEntry item) { @@ -399,11 +411,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { if (isModified != null) isModified.set(false); }, this::onError ); + disposables.add(disposable); } From 5773152ed35a87ca10df7bc39215a966d0fc2e02 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 3 Feb 2018 14:39:03 -0800 Subject: [PATCH 077/276] -Added subtitles loading and display. -Added subtitles switching button to popup and main players. -Added aspect ratio switching button to popup pand main players. --- .../newpipe/player/PopupVideoPlayer.java | 3 +- .../schabi/newpipe/player/VideoPlayer.java | 164 ++++++++++++++++-- .../newpipe/player/helper/PlayerHelper.java | 45 +++++ .../main/res/layout/activity_main_player.xml | 40 ++++- app/src/main/res/layout/player_popup.xml | 32 ++++ 5 files changed, 272 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index c3803f0d5..e8ccba7d0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -49,6 +49,7 @@ import android.widget.RemoteViews; import android.widget.SeekBar; import android.widget.TextView; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -642,7 +643,7 @@ public final class PopupVideoPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ /*package-private*/ void enableVideoRenderer(final boolean enable) { - final int videoRendererIndex = getVideoRendererIndex(); + final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); if (trackSelector != null && videoRendererIndex != -1) { trackSelector.setRendererDisabled(videoRendererIndex, !enable); } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 5399ff047..48d10a4ad 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -29,12 +29,14 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; +import android.net.Uri; import android.os.Build; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.Log; +import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; @@ -46,17 +48,24 @@ import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.Subtitles; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; @@ -64,6 +73,9 @@ import org.schabi.newpipe.util.ListHelper; import java.util.ArrayList; import java.util.List; +import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; +import static com.google.android.exoplayer2.C.TIME_UNSET; +import static com.google.android.exoplayer2.C.TRACK_TYPE_TEXT; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -95,6 +107,8 @@ public abstract class VideoPlayer extends BasePlayer protected String playbackQuality; + private List availableCaptions; + protected boolean wasPlaying = false; /*////////////////////////////////////////////////////////////////////////// @@ -123,6 +137,11 @@ public abstract class VideoPlayer extends BasePlayer private View topControlsRoot; private TextView qualityTextView; + private SubtitleView subtitleView; + + private TextView resizeView; + private TextView captionTextView; + private ValueAnimator controlViewAnimator; private Handler controlsVisibilityHandler = new Handler(); @@ -133,6 +152,9 @@ public abstract class VideoPlayer extends BasePlayer private int playbackSpeedPopupMenuGroupId = 79; private PopupMenu playbackSpeedPopupMenu; + private int captionPopupMenuGroupId = 89; + private PopupMenu captionPopupMenu; + /////////////////////////////////////////////////////////////////////////// public VideoPlayer(String debugTag, Context context) { @@ -163,6 +185,12 @@ public abstract class VideoPlayer extends BasePlayer this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.topControlsRoot = rootView.findViewById(R.id.topControls); this.qualityTextView = rootView.findViewById(R.id.qualityTextView); + this.subtitleView = rootView.findViewById(R.id.subtitleView); + + this.resizeView = rootView.findViewById(R.id.resizeTextView); + resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode())); + + this.captionTextView = rootView.findViewById(R.id.captionTextView); //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); @@ -172,9 +200,13 @@ public abstract class VideoPlayer extends BasePlayer this.qualityPopupMenu = new PopupMenu(context, qualityTextView); this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView); + this.captionPopupMenu = new PopupMenu(context, captionTextView); - ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); + ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)) + .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); + subtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + PlayerHelper.getCaptionSizePx(context)); } @Override @@ -183,14 +215,22 @@ public abstract class VideoPlayer extends BasePlayer playbackSeekBar.setOnSeekBarChangeListener(this); playbackSpeedTextView.setOnClickListener(this); qualityTextView.setOnClickListener(this); + captionTextView.setOnClickListener(this); + resizeView.setOnClickListener(this); } @Override public void initPlayer() { super.initPlayer(); + + // Setup video view simpleExoPlayer.setVideoSurfaceView(surfaceView); simpleExoPlayer.addVideoListener(this); + // Setup subtitle view + simpleExoPlayer.addTextOutput(cues -> subtitleView.onCues(cues)); + + // Setup audio session with onboard equalizer if (Build.VERSION.SDK_INT >= 21) { trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)); } @@ -236,6 +276,39 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedPopupMenu.setOnDismissListener(this); } + private void buildCaptionMenu() { + if (captionPopupMenu == null) return; + captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); + + if (availableCaptions == null || trackSelector == null) return; + MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, + 0, Menu.NONE, "Caption Off"); + captionOffItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); + if (trackSelector != null && textRendererIndex != -1) { + trackSelector.setRendererDisabled(textRendererIndex, true); + } + return true; + }); + + for (int i = 0; i < availableCaptions.size(); i++) { + final Subtitles subtitles = availableCaptions.get(i); + final String captionLanguage = PlayerHelper.captionLanguageOf(subtitles); + MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, + i + 1, Menu.NONE, captionLanguage); + captionItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); + if (trackSelector != null && textRendererIndex != -1) { + trackSelector.setRendererDisabled(textRendererIndex, false); + trackSelector.setParameters(trackSelector.getParameters() + .withPreferredTextLanguage(captionLanguage)); + } + return true; + }); + } + //captionPopupMenu.setOnMenuItemClickListener(this); + captionPopupMenu.setOnDismissListener(this); + } /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @@ -249,9 +322,11 @@ public abstract class VideoPlayer extends BasePlayer super.sync(item, info); qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); + captionTextView.setVisibility(View.GONE); if (info != null) { - final List videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); + final List videos = ListHelper.getSortedStreamVideosList(context, + info.video_streams, info.video_only_streams, false); availableStreams = new ArrayList<>(videos); if (playbackQuality == null) { selectedStreamIndex = getDefaultResolutionIndex(videos); @@ -261,8 +336,12 @@ public abstract class VideoPlayer extends BasePlayer buildQualityMenu(); buildPlaybackSpeedMenu(); + buildCaptionMenu(); qualityTextView.setVisibility(View.VISIBLE); playbackSpeedTextView.setVisibility(View.VISIBLE); + + availableCaptions = info.getSubtitles(); + if (!availableCaptions.isEmpty()) captionTextView.setVisibility(View.VISIBLE); } } @@ -280,13 +359,39 @@ public abstract class VideoPlayer extends BasePlayer if (index < 0 || index >= videos.size()) return null; final VideoStream video = videos.get(index); - final MediaSource streamSource = buildMediaSource(video.getUrl(), MediaFormat.getSuffixById(video.format)); - final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); - if (!video.isVideoOnly || audio == null) return streamSource; + List mediaSources = new ArrayList<>(); + // Create video stream source + final MediaSource streamSource = buildMediaSource(video.getUrl(), + MediaFormat.getSuffixById(video.getFormatId())); + mediaSources.add(streamSource); - // Merge with audio stream in case if video does not contain audio - final MediaSource audioSource = buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.format)); - return new MergingMediaSource(streamSource, audioSource); + // Create optional audio stream source + final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); + if (video.isVideoOnly && audio != null) { + // Merge with audio stream in case if video does not contain audio + final MediaSource audioSource = buildMediaSource(audio.getUrl(), + MediaFormat.getSuffixById(audio.getFormatId())); + mediaSources.add(audioSource); + } + + // Create subtitle sources + for (final Subtitles subtitle : info.getSubtitles()) { + final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); + if (mimeType == null) continue; + + final Format textFormat = Format.createTextSampleFormat(null, mimeType, + SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(subtitle)); + final MediaSource textSource = new SingleSampleMediaSource( + Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET); + mediaSources.add(textSource); + } + + if (mediaSources.size() == 1) { + return mediaSources.get(0); + } else { + return new MergingMediaSource(mediaSources.toArray( + new MediaSource[mediaSources.size()])); + } } /*////////////////////////////////////////////////////////////////////////// @@ -364,6 +469,20 @@ public abstract class VideoPlayer extends BasePlayer // ExoPlayer Video Listener //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + super.onTracksChanged(trackGroups, trackSelections); + if (trackSelector == null || captionTextView == null) return; + + if (trackSelector.getRendererDisabled(getRendererIndex(C.TRACK_TYPE_TEXT)) || + trackSelector.getParameters().preferredTextLanguage == null) { + captionTextView.setText("No Caption"); + } else { + final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; + captionTextView.setText(preferredLanguage); + } + } + @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { if (DEBUG) { @@ -453,6 +572,10 @@ public abstract class VideoPlayer extends BasePlayer onQualitySelectorClicked(); } else if (v.getId() == playbackSpeedTextView.getId()) { onPlaybackSpeedClicked(); + } else if (v.getId() == resizeView.getId()) { + onResizeClicked(); + } else if (v.getId() == captionTextView.getId()) { + onCaptionClicked(); } } @@ -516,6 +639,27 @@ public abstract class VideoPlayer extends BasePlayer showControls(300); } + private void onCaptionClicked() { + if (DEBUG) Log.d(TAG, "onCaptionClicked() called"); + captionPopupMenu.show(); + isSomePopupMenuVisible = true; + showControls(300); + } + + protected void onResizeClicked() { + if (aspectRatioFrameLayout != null && context != null) { + final int currentResizeMode = aspectRatioFrameLayout.getResizeMode(); + final int newResizeMode; + if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_ZOOM) { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + } else { + newResizeMode = currentResizeMode + 1; + } + + aspectRatioFrameLayout.setResizeMode(newResizeMode); + resizeView.setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + } + } /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ @@ -557,11 +701,11 @@ public abstract class VideoPlayer extends BasePlayer // Utils //////////////////////////////////////////////////////////////////////////*/ - public int getVideoRendererIndex() { + public int getRendererIndex(final int trackIndex) { if (simpleExoPlayer == null) return -1; for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { - if (simpleExoPlayer.getRendererType(t) == C.TRACK_TYPE_VIDEO) { + if (simpleExoPlayer.getRendererType(t) == trackIndex) { return t; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 40063ba40..75ea3bdc6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -4,8 +4,14 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.util.DisplayMetrics; + +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.Subtitles; +import org.schabi.newpipe.extractor.stream.SubtitlesFormat; import java.text.DecimalFormat; import java.text.NumberFormat; @@ -14,6 +20,12 @@ import java.util.Locale; import javax.annotation.Nonnull; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH; +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + public class PlayerHelper { private PlayerHelper() {} @@ -46,6 +58,39 @@ public class PlayerHelper { return pitchFormatter.format(pitch); } + public static String mimeTypesOf(final SubtitlesFormat format) { + switch (format) { + case VTT: return MimeTypes.TEXT_VTT; + case TTML: return MimeTypes.APPLICATION_TTML; + default: throw new IllegalArgumentException("Unrecognized mime type: " + format.name()); + } + } + + @NonNull + public static String captionLanguageOf(@NonNull final Subtitles subtitles) { + final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); + return displayName + (subtitles.isAutoGenerated() ? " (auto-generated)" : ""); + } + + public static String resizeTypeOf(@NonNull final Context context, + @AspectRatioFrameLayout.ResizeMode final int resizeMode) { + switch (resizeMode) { + case RESIZE_MODE_FIT: return "FIT"; + case RESIZE_MODE_FILL: return "FILL"; + case RESIZE_MODE_FIXED_HEIGHT: return "HEIGHT"; + case RESIZE_MODE_FIXED_WIDTH: return "WIDTH"; + case RESIZE_MODE_ZOOM: return "ZOOM"; + default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); + } + } + + public static float getCaptionSizePx(@NonNull final Context context) { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + // todo: expose size control to users + return (float) minimumLength / 20f; + } + public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { return isResumeAfterAudioFocusGain(context, false); } diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 36222f5bc..785505df9 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -30,6 +30,12 @@ + + + + - + @@ -97,6 +101,34 @@ android:src="@drawable/ic_fullscreen_white" tools:ignore="ContentDescription,RtlHardcoded"/> + + + From 6485327b9711f73687b12cb39eb67a991e5fc7f8 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 6 Feb 2018 15:07:49 -0800 Subject: [PATCH 078/276] -Replace main player dropdown menu with expand/collapse custom UI. --- .../newpipe/player/MainVideoPlayer.java | 77 +++++++-------- .../main/res/layout/activity_main_player.xml | 94 ++++++++++++++++--- 2 files changed, 114 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 8081dcad7..a23290486 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -61,7 +61,6 @@ import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.PopupMenuIconHacker; import org.schabi.newpipe.util.ThemeHelper; import java.util.List; @@ -194,7 +193,6 @@ public final class MainVideoPlayer extends Activity { super.onConfigurationChanged(newConfig); if (playerImpl.isSomePopupMenuVisible()) { - playerImpl.moreOptionsPopupMenu.dismiss(); playerImpl.getQualityPopupMenu().dismiss(); playerImpl.getPlaybackSpeedPopupMenu().dismiss(); } @@ -301,8 +299,11 @@ public final class MainVideoPlayer extends Activity { private boolean queueVisible; private ImageButton moreOptionsButton; - public int moreOptionsPopupMenuGroupId = 89; - public PopupMenu moreOptionsPopupMenu; + private ImageButton toggleOrientationButton; + private ImageButton switchPopupButton; + private ImageButton switchBackgroundButton; + + private View secondaryControls; VideoPlayerImpl(final Context context) { super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); @@ -322,9 +323,12 @@ public final class MainVideoPlayer extends Activity { this.playPauseButton = rootView.findViewById(R.id.playPauseButton); this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton); this.playNextButton = rootView.findViewById(R.id.playNextButton); + this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton); - this.moreOptionsPopupMenu = new PopupMenu(context, moreOptionsButton); - buildMoreOptionsMenu(); + this.secondaryControls = rootView.findViewById(R.id.secondaryControls); + this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation); + this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); + this.switchPopupButton = rootView.findViewById(R.id.switchPopup); titleTextView.setSelected(true); channelTextView.setSelected(true); @@ -348,7 +352,11 @@ public final class MainVideoPlayer extends Activity { playPauseButton.setOnClickListener(this); playPreviousButton.setOnClickListener(this); playNextButton.setOnClickListener(this); + moreOptionsButton.setOnClickListener(this); + toggleOrientationButton.setOnClickListener(this); + switchBackgroundButton.setOnClickListener(this); + switchPopupButton.setOnClickListener(this); } /*////////////////////////////////////////////////////////////////////////// @@ -464,6 +472,16 @@ public final class MainVideoPlayer extends Activity { return; } else if (v.getId() == moreOptionsButton.getId()) { onMoreOptionsClicked(); + + } else if (v.getId() == toggleOrientationButton.getId()) { + onScreenRotationClicked(); + + } else if (v.getId() == switchPopupButton.getId()) { + onFullScreenButtonClicked(); + + } else if (v.getId() == switchBackgroundButton.getId()) { + onPlayBackgroundButtonClicked(); + } if (getCurrentState() != STATE_COMPLETED) { @@ -497,8 +515,15 @@ public final class MainVideoPlayer extends Activity { private void onMoreOptionsClicked() { if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); - moreOptionsPopupMenu.show(); - isSomePopupMenuVisible = true; + if (secondaryControls.getVisibility() == View.VISIBLE) { + moreOptionsButton.setImageDrawable(getResources().getDrawable( + R.drawable.ic_expand_more_white_24dp)); + animateView(secondaryControls, false, 200); + } else { + moreOptionsButton.setImageDrawable(getResources().getDrawable( + R.drawable.ic_expand_less_white_24dp)); + animateView(secondaryControls, true, 200); + } showControls(300); } @@ -637,42 +662,6 @@ public final class MainVideoPlayer extends Activity { setShuffleButton(shuffleButton, playQueue.isShuffled()); } - private void buildMoreOptionsMenu() { - this.moreOptionsPopupMenu.getMenuInflater().inflate(R.menu.menu_videooptions, - moreOptionsPopupMenu.getMenu()); - - moreOptionsPopupMenu.setOnMenuItemClickListener(menuItem -> { - switch (menuItem.getItemId()) { - case R.id.toggleOrientation: - onScreenRotationClicked(); - break; - case R.id.switchPopup: - onFullScreenButtonClicked(); - break; - case R.id.switchBackground: - onPlayBackgroundButtonClicked(); - break; - } - return false; - }); - - try { - PopupMenuIconHacker.setShowPopupIcon(moreOptionsPopupMenu); - } catch (Exception e) { - e.printStackTrace(); - } - - // fix icon theme - if(ThemeHelper.isLightThemeSelected(MainVideoPlayer.this)) { - moreOptionsPopupMenu.getMenu() - .findItem(R.id.toggleOrientation) - .setIcon(R.drawable.ic_screen_rotation_black_24dp); - moreOptionsPopupMenu.getMenu() - .findItem(R.id.switchPopup) - .setIcon(R.drawable.ic_fullscreen_exit_black_24dp); - } - } - private void buildQueue() { queueLayout = findViewById(R.id.playQueuePanel); diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 785505df9..577a6a8f5 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -145,10 +145,10 @@ android:layout_alignParentTop="true" android:background="@drawable/player_top_controls_bg" android:gravity="top" - android:paddingBottom="70dp" - android:paddingLeft="2dp" - android:paddingRight="10dp" android:paddingTop="10dp" + android:paddingBottom="10dp" + android:paddingLeft="5dp" + android:paddingRight="5dp" tools:ignore="RtlHardcoded"> @@ -232,12 +234,12 @@ android:layout_marginLeft="2dp" android:layout_marginRight="2dp" android:layout_toLeftOf="@+id/moreOptionsButton" - android:background="#00ffffff" android:clickable="true" android:focusable="true" android:padding="5dp" android:scaleType="fitXY" android:src="@drawable/list" + android:background="?attr/selectableItemBackground" tools:ignore="ContentDescription,RtlHardcoded"/> + + + + + + + + + + tools:visibility="visible" /> + tools:visibility="visible" /> + tools:visibility="visible" /> From 880676d67028bc23ad8d8df85be4532e807588e1 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 7 Feb 2018 13:11:19 -0800 Subject: [PATCH 079/276] -Modified popup video player to show extra options only when screen is large enough. -Modified available resize options for different player modes. -Fixed caption menu not working on popup player. -Extracted hardcoded strings. -Added button effects to both main and popup players. --- .../newpipe/player/MainVideoPlayer.java | 29 ++++++++ .../newpipe/player/PopupVideoPlayer.java | 34 ++++++++- .../schabi/newpipe/player/VideoPlayer.java | 51 +++++++------ .../newpipe/player/helper/PlayerHelper.java | 16 +---- .../main/res/layout/activity_main_player.xml | 7 +- app/src/main/res/layout/player_popup.xml | 72 +++++++++++-------- app/src/main/res/values/strings.xml | 8 +++ 7 files changed, 142 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index a23290486..49ce06a9b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -35,7 +35,9 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; +import android.util.DisplayMetrics; import android.util.Log; +import android.util.TypedValue; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; @@ -48,6 +50,7 @@ import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -334,6 +337,8 @@ public final class MainVideoPlayer extends Activity { channelTextView.setSelected(true); getRootView().setKeepScreenOn(true); + getSubtitleView().setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + getCaptionSizePx(context)); } @Override @@ -547,6 +552,24 @@ public final class MainVideoPlayer extends Activity { if (isPlaying()) hideControls(300, 0); } + @Override + protected void onResizeClicked() { + if (getAspectRatioFrameLayout() != null && context != null) { + final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); + final int newResizeMode; + if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FIT) { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; + } else if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + } else { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + } + + getAspectRatioFrameLayout().setResizeMode(newResizeMode); + getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + } + } + @Override protected int getDefaultResolutionIndex(final List sortedVideos) { return ListHelper.getDefaultResolutionIndex(context, sortedVideos); @@ -745,6 +768,12 @@ public final class MainVideoPlayer extends Activity { }; } + private float getCaptionSizePx(@NonNull Context context) { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + // todo: expose size control to users + return (float) minimumLength / 20f; + } /////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index e8ccba7d0..bf6d6f445 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -52,6 +52,7 @@ import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; @@ -89,6 +90,8 @@ public final class PopupVideoPlayer extends Service { private static final String POPUP_SAVED_X = "popup_saved_x"; private static final String POPUP_SAVED_Y = "popup_saved_y"; + private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; + private WindowManager windowManager; private WindowManager.LayoutParams windowLayoutParams; private GestureDetector gestureDetector; @@ -359,10 +362,12 @@ public final class PopupVideoPlayer extends Service { /////////////////////////////////////////////////////////////////////////// - protected class VideoPlayerImpl extends VideoPlayer { + protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener { private TextView resizingIndicator; private ImageButton fullScreenButton; + private View extraOptionsView; + @Override public void handleIntent(Intent intent) { super.handleIntent(intent); @@ -381,6 +386,17 @@ public final class PopupVideoPlayer extends Service { resizingIndicator = rootView.findViewById(R.id.resizing_indicator); fullScreenButton = rootView.findViewById(R.id.fullScreenButton); fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); + + extraOptionsView = rootView.findViewById(R.id.extraOptionsView); + rootView.addOnLayoutChangeListener(this); + } + + @Override + public void onLayoutChange(final View view, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + float widthDp = Math.abs(right - left) / getResources().getDisplayMetrics().density; + final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE; + extraOptionsView.setVisibility(visibility); } @Override @@ -439,6 +455,22 @@ public final class PopupVideoPlayer extends Service { if (isPlaying()) hideControls(500, 0); } + @Override + protected void onResizeClicked() { + if (getAspectRatioFrameLayout() != null && context != null) { + final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); + final int newResizeMode; + if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + } else { + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; + } + + getAspectRatioFrameLayout().setResizeMode(newResizeMode); + getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + } + } + @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 48d10a4ad..344175030 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -36,7 +36,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.Log; -import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; @@ -75,7 +74,6 @@ import java.util.List; import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; import static com.google.android.exoplayer2.C.TIME_UNSET; -import static com.google.android.exoplayer2.C.TRACK_TYPE_TEXT; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -204,9 +202,6 @@ public abstract class VideoPlayer extends BasePlayer ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)) .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); - - subtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - PlayerHelper.getCaptionSizePx(context)); } @Override @@ -281,8 +276,10 @@ public abstract class VideoPlayer extends BasePlayer captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); if (availableCaptions == null || trackSelector == null) return; + + // Add option for turning off caption MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, - 0, Menu.NONE, "Caption Off"); + 0, Menu.NONE, R.string.caption_none); captionOffItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); if (trackSelector != null && textRendererIndex != -1) { @@ -291,6 +288,7 @@ public abstract class VideoPlayer extends BasePlayer return true; }); + // Add all available captions for (int i = 0; i < availableCaptions.size(); i++) { final Subtitles subtitles = availableCaptions.get(i); final String captionLanguage = PlayerHelper.captionLanguageOf(subtitles); @@ -306,7 +304,6 @@ public abstract class VideoPlayer extends BasePlayer return true; }); } - //captionPopupMenu.setOnMenuItemClickListener(this); captionPopupMenu.setOnDismissListener(this); } /*////////////////////////////////////////////////////////////////////////// @@ -334,14 +331,14 @@ public abstract class VideoPlayer extends BasePlayer selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); } + availableCaptions = info.getSubtitles(); + buildQualityMenu(); buildPlaybackSpeedMenu(); buildCaptionMenu(); qualityTextView.setVisibility(View.VISIBLE); playbackSpeedTextView.setVisibility(View.VISIBLE); - - availableCaptions = info.getSubtitles(); - if (!availableCaptions.isEmpty()) captionTextView.setVisibility(View.VISIBLE); + captionTextView.setVisibility(availableCaptions.isEmpty() ? View.GONE : View.VISIBLE); } } @@ -472,11 +469,13 @@ public abstract class VideoPlayer extends BasePlayer @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { super.onTracksChanged(trackGroups, trackSelections); - if (trackSelector == null || captionTextView == null) return; + if (captionTextView == null) return; - if (trackSelector.getRendererDisabled(getRendererIndex(C.TRACK_TYPE_TEXT)) || + if (trackSelector == null) { + captionTextView.setVisibility(View.GONE); + } else if (trackSelector.getRendererDisabled(getRendererIndex(C.TRACK_TYPE_TEXT)) || trackSelector.getParameters().preferredTextLanguage == null) { - captionTextView.setText("No Caption"); + captionTextView.setText(R.string.caption_none); } else { final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; captionTextView.setText(preferredLanguage); @@ -646,20 +645,7 @@ public abstract class VideoPlayer extends BasePlayer showControls(300); } - protected void onResizeClicked() { - if (aspectRatioFrameLayout != null && context != null) { - final int currentResizeMode = aspectRatioFrameLayout.getResizeMode(); - final int newResizeMode; - if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_ZOOM) { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - } else { - newResizeMode = currentResizeMode + 1; - } - - aspectRatioFrameLayout.setResizeMode(newResizeMode); - resizeView.setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); - } - } + protected abstract void onResizeClicked(); /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ @@ -899,4 +885,15 @@ public abstract class VideoPlayer extends BasePlayer return currentDisplaySeek; } + public SubtitleView getSubtitleView() { + return subtitleView; + } + + public TextView getResizeView() { + return resizeView; + } + + public TextView getCaptionTextView() { + return captionTextView; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 75ea3bdc6..476838f13 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.NonNull; -import android.util.DisplayMetrics; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.util.MimeTypes; @@ -75,22 +74,13 @@ public class PlayerHelper { public static String resizeTypeOf(@NonNull final Context context, @AspectRatioFrameLayout.ResizeMode final int resizeMode) { switch (resizeMode) { - case RESIZE_MODE_FIT: return "FIT"; - case RESIZE_MODE_FILL: return "FILL"; - case RESIZE_MODE_FIXED_HEIGHT: return "HEIGHT"; - case RESIZE_MODE_FIXED_WIDTH: return "WIDTH"; - case RESIZE_MODE_ZOOM: return "ZOOM"; + case RESIZE_MODE_FIT: return context.getResources().getString(R.string.resize_fit); + case RESIZE_MODE_FILL: return context.getResources().getString(R.string.resize_fill); + case RESIZE_MODE_ZOOM: return context.getResources().getString(R.string.resize_zoom); default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); } } - public static float getCaptionSizePx(@NonNull final Context context) { - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); - // todo: expose size control to users - return (float) minimumLength / 20f; - } - public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { return isResumeAfterAudioFocusGain(context, false); } diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 577a6a8f5..80c67974f 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -286,12 +286,13 @@ @@ -83,52 +84,61 @@ android:layout_height="30dp" android:layout_toRightOf="@+id/qualityTextView" android:gravity="center" - android:padding="6dp" + android:padding="5dp" android:textColor="@android:color/white" android:textStyle="bold" + android:background="?attr/selectableItemBackground" tools:ignore="RelativeOverlap,RtlHardcoded,RtlSymmetry" tools:text="1.75x"/> + + + + + + + - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 767bffa10..b1bd5c3ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -395,4 +395,12 @@ Added to playlist Playlist thumbnail changed Failed to delete playlist + + + No Caption + + FIT + FILL + ZOOM + From f506fc0478cbed1df0387880c9b1513d0f233169 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 9 Feb 2018 12:43:24 -0800 Subject: [PATCH 080/276] -Moved caption extraction and menu building into exoplayer track changing callback. -Updated extractor dependency. --- .../newpipe/player/PopupVideoPlayer.java | 2 +- .../schabi/newpipe/player/VideoPlayer.java | 89 +++++++++++++------ 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index bf6d6f445..9b5413977 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -676,7 +676,7 @@ public final class PopupVideoPlayer extends Service { /*package-private*/ void enableVideoRenderer(final boolean enable) { final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); - if (trackSelector != null && videoRendererIndex != -1) { + if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setRendererDisabled(videoRendererIndex, !enable); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 344175030..7f41c028f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -53,6 +53,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; @@ -98,6 +99,7 @@ public abstract class VideoPlayer extends BasePlayer // Player //////////////////////////////////////////////////////////////////////////*/ + protected static final int RENDERER_UNAVAILABLE = -1; public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds private ArrayList availableStreams; @@ -105,8 +107,6 @@ public abstract class VideoPlayer extends BasePlayer protected String playbackQuality; - private List availableCaptions; - protected boolean wasPlaying = false; /*////////////////////////////////////////////////////////////////////////// @@ -271,35 +271,32 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedPopupMenu.setOnDismissListener(this); } - private void buildCaptionMenu() { + private void buildCaptionMenu(final List availableLanguages) { if (captionPopupMenu == null) return; captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); - if (availableCaptions == null || trackSelector == null) return; - // Add option for turning off caption MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, 0, Menu.NONE, R.string.caption_none); captionOffItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (trackSelector != null && textRendererIndex != -1) { + if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setRendererDisabled(textRendererIndex, true); } return true; }); // Add all available captions - for (int i = 0; i < availableCaptions.size(); i++) { - final Subtitles subtitles = availableCaptions.get(i); - final String captionLanguage = PlayerHelper.captionLanguageOf(subtitles); + for (int i = 0; i < availableLanguages.size(); i++) { + final String captionLanguage = availableLanguages.get(i); MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, i + 1, Menu.NONE, captionLanguage); captionItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (trackSelector != null && textRendererIndex != -1) { - trackSelector.setRendererDisabled(textRendererIndex, false); + if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setParameters(trackSelector.getParameters() .withPreferredTextLanguage(captionLanguage)); + trackSelector.setRendererDisabled(textRendererIndex, false); } return true; }); @@ -319,7 +316,6 @@ public abstract class VideoPlayer extends BasePlayer super.sync(item, info); qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); - captionTextView.setVisibility(View.GONE); if (info != null) { final List videos = ListHelper.getSortedStreamVideosList(context, @@ -331,14 +327,10 @@ public abstract class VideoPlayer extends BasePlayer selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); } - availableCaptions = info.getSubtitles(); - buildQualityMenu(); buildPlaybackSpeedMenu(); - buildCaptionMenu(); qualityTextView.setVisibility(View.VISIBLE); playbackSpeedTextView.setVisibility(View.VISIBLE); - captionTextView.setVisibility(availableCaptions.isEmpty() ? View.GONE : View.VISIBLE); } } @@ -469,17 +461,7 @@ public abstract class VideoPlayer extends BasePlayer @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { super.onTracksChanged(trackGroups, trackSelections); - if (captionTextView == null) return; - - if (trackSelector == null) { - captionTextView.setVisibility(View.GONE); - } else if (trackSelector.getRendererDisabled(getRendererIndex(C.TRACK_TYPE_TEXT)) || - trackSelector.getParameters().preferredTextLanguage == null) { - captionTextView.setText(R.string.caption_none); - } else { - final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; - captionTextView.setText(preferredLanguage); - } + onTextTrackUpdate(); } @Override @@ -495,6 +477,55 @@ public abstract class VideoPlayer extends BasePlayer animateView(surfaceForeground, false, 100); } + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Track Updates + //////////////////////////////////////////////////////////////////////////*/ + + private void onTextTrackUpdate() { + final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); + + if (captionTextView == null) return; + if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null || + textRenderer == RENDERER_UNAVAILABLE) { + captionTextView.setVisibility(View.GONE); + return; + } + + final TrackGroupArray textTracks = trackSelector.getCurrentMappedTrackInfo() + .getTrackGroups(textRenderer); + + // Extract all loaded languages + List availableLanguages = new ArrayList<>(textTracks.length); + for (int i = 0; i < textTracks.length; i++) { + final TrackGroup textTrack = textTracks.get(i); + if (textTrack.length > 0 && textTrack.getFormat(0) != null) { + availableLanguages.add(textTrack.getFormat(0).language); + } + } + + // Normalize mismatching language strings + final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; + // Because ExoPlayer normalizes the preferred language string but not the text track + // language strings, some preferred language string will have the language name in lowercase + String formattedPreferredLanguage = null; + if (preferredLanguage != null) { + for (final String language : availableLanguages) { + if (language.compareToIgnoreCase(preferredLanguage) == 0) + formattedPreferredLanguage = language; + } + } + + // Build UI + buildCaptionMenu(availableLanguages); + if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null || + !availableLanguages.contains(formattedPreferredLanguage)) { + captionTextView.setText(R.string.caption_none); + } else { + captionTextView.setText(formattedPreferredLanguage); + } + captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); + } + /*////////////////////////////////////////////////////////////////////////// // General Player //////////////////////////////////////////////////////////////////////////*/ @@ -688,7 +719,7 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ public int getRendererIndex(final int trackIndex) { - if (simpleExoPlayer == null) return -1; + if (simpleExoPlayer == null) return RENDERER_UNAVAILABLE; for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { if (simpleExoPlayer.getRendererType(t) == trackIndex) { @@ -696,7 +727,7 @@ public abstract class VideoPlayer extends BasePlayer } } - return -1; + return RENDERER_UNAVAILABLE; } public boolean isControlsVisible() { From 59f85838950b5dae0c7e154efed817ab14ed2cb2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Fri, 9 Feb 2018 13:26:03 -0800 Subject: [PATCH 081/276] -Added settings for managing caption font size. --- .../newpipe/player/MainVideoPlayer.java | 27 +++++++++++++------ .../newpipe/player/PopupVideoPlayer.java | 13 +++++++++ .../schabi/newpipe/player/VideoPlayer.java | 9 +++++++ app/src/main/res/values/settings_keys.xml | 19 +++++++++++++ app/src/main/res/values/strings.xml | 5 ++++ app/src/main/res/xml/appearance_settings.xml | 8 ++++++ 6 files changed, 73 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 49ce06a9b..5e19e8173 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -51,6 +51,7 @@ import android.widget.Toast; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -337,8 +338,24 @@ public final class MainVideoPlayer extends Activity { channelTextView.setSelected(true); getRootView().setKeepScreenOn(true); - getSubtitleView().setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - getCaptionSizePx(context)); + } + + @Override + protected void setupSubtitleView(@NonNull SubtitleView view, + @NonNull String captionSizeKey) { + final float captionRatio; + if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { + captionRatio = 22f; + } else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { + captionRatio = 18f; + } else { + captionRatio = 20f; + } + + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + (float) minimumLength / captionRatio); } @Override @@ -768,12 +785,6 @@ public final class MainVideoPlayer extends Activity { }; } - private float getCaptionSizePx(@NonNull Context context) { - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); - // todo: expose size control to users - return (float) minimumLength / 20f; - } /////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 9b5413977..bda2b7aa5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -53,6 +53,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; @@ -391,6 +392,18 @@ public final class PopupVideoPlayer extends Service { rootView.addOnLayoutChangeListener(this); } + @Override + protected void setupSubtitleView(@NonNull SubtitleView view, + @NonNull String captionSizeKey) { + float captionRatio = SubtitleView.DEFAULT_TEXT_SIZE_FRACTION; + if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { + captionRatio *= 0.9; + } else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { + captionRatio *= 1.1; + } + view.setFractionalTextSize(captionRatio); + } + @Override public void onLayoutChange(final View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 7f41c028f..a2f5d22ae 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -32,6 +32,7 @@ import android.graphics.PorterDuff; import android.net.Uri; import android.os.Build; import android.os.Handler; +import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; @@ -183,7 +184,12 @@ public abstract class VideoPlayer extends BasePlayer this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.topControlsRoot = rootView.findViewById(R.id.topControls); this.qualityTextView = rootView.findViewById(R.id.qualityTextView); + this.subtitleView = rootView.findViewById(R.id.subtitleView); + final String captionSizeKey = PreferenceManager.getDefaultSharedPreferences(context) + .getString(context.getString(R.string.caption_size_key), + context.getString(R.string.caption_size_default)); + setupSubtitleView(subtitleView, captionSizeKey); this.resizeView = rootView.findViewById(R.id.resizeTextView); resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode())); @@ -204,6 +210,9 @@ public abstract class VideoPlayer extends BasePlayer .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); } + protected abstract void setupSubtitleView(@NonNull SubtitleView view, + @NonNull String captionSizeKey); + @Override public void initListeners() { super.initListeners(); diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..047f03f8b 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -100,6 +100,25 @@ @string/black_theme_title + + caption_size_key + @string/normal_caption_size_key + + smaller_caption_size + normal_caption_size + larger_caption_size + + + @string/smaller_caption_font_size + @string/normal_caption_font_size + @string/larger_caption_font_size + + + @string/smaller_caption_size_key + @string/normal_caption_size_key + @string/larger_caption_size_key + + show_search_suggestions show_play_with_kodi diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b1bd5c3ec..7ceec994b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,4 +403,9 @@ FILL ZOOM + Caption Font Size + Smaller Font + Normal Font + Larger Font + diff --git a/app/src/main/res/xml/appearance_settings.xml b/app/src/main/res/xml/appearance_settings.xml index 58b08a284..3ffbd9d81 100644 --- a/app/src/main/res/xml/appearance_settings.xml +++ b/app/src/main/res/xml/appearance_settings.xml @@ -21,4 +21,12 @@ android:key="@string/show_hold_to_append_key" android:title="@string/show_hold_to_append_title" android:summary="@string/show_hold_to_append_summary"/> + + From f09b04dce0c87b7dc4d3039e02101b8e96b460db Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 10 Feb 2018 19:33:48 -0800 Subject: [PATCH 082/276] -Code clean up on resize switching. --- .../newpipe/player/MainVideoPlayer.java | 32 ++++++++----------- .../newpipe/player/PopupVideoPlayer.java | 17 +++------- .../schabi/newpipe/player/VideoPlayer.java | 15 +++++++-- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 5e19e8173..d48994d0f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -343,19 +343,19 @@ public final class MainVideoPlayer extends Activity { @Override protected void setupSubtitleView(@NonNull SubtitleView view, @NonNull String captionSizeKey) { - final float captionRatio; + final float captionRatioInverse; if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) { - captionRatio = 22f; + captionRatioInverse = 22f; } else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) { - captionRatio = 18f; + captionRatioInverse = 18f; } else { - captionRatio = 20f; + captionRatioInverse = 20f; } final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - (float) minimumLength / captionRatio); + (float) minimumLength / captionRatioInverse); } @Override @@ -570,20 +570,14 @@ public final class MainVideoPlayer extends Activity { } @Override - protected void onResizeClicked() { - if (getAspectRatioFrameLayout() != null && context != null) { - final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); - final int newResizeMode; - if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FIT) { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; - } else if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; - } else { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - } - - getAspectRatioFrameLayout().setResizeMode(newResizeMode); - getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + protected int nextResizeMode(int currentResizeMode) { + switch (currentResizeMode) { + case AspectRatioFrameLayout.RESIZE_MODE_FIT: + return AspectRatioFrameLayout.RESIZE_MODE_FILL; + case AspectRatioFrameLayout.RESIZE_MODE_FILL: + return AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + default: + return AspectRatioFrameLayout.RESIZE_MODE_FIT; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index bda2b7aa5..f4e7a0d6a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -469,18 +469,11 @@ public final class PopupVideoPlayer extends Service { } @Override - protected void onResizeClicked() { - if (getAspectRatioFrameLayout() != null && context != null) { - final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); - final int newResizeMode; - if (currentResizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - } else { - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; - } - - getAspectRatioFrameLayout().setResizeMode(newResizeMode); - getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + protected int nextResizeMode(int resizeMode) { + if (resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { + return AspectRatioFrameLayout.RESIZE_MODE_FIT; + } else { + return AspectRatioFrameLayout.RESIZE_MODE_FILL; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index a2f5d22ae..a0bc7223f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -519,8 +519,10 @@ public abstract class VideoPlayer extends BasePlayer String formattedPreferredLanguage = null; if (preferredLanguage != null) { for (final String language : availableLanguages) { - if (language.compareToIgnoreCase(preferredLanguage) == 0) + if (language.compareToIgnoreCase(preferredLanguage) == 0) { formattedPreferredLanguage = language; + break; + } } } @@ -685,7 +687,16 @@ public abstract class VideoPlayer extends BasePlayer showControls(300); } - protected abstract void onResizeClicked(); + private void onResizeClicked() { + if (getAspectRatioFrameLayout() != null && context != null) { + final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); + final int newResizeMode = nextResizeMode(currentResizeMode); + getAspectRatioFrameLayout().setResizeMode(newResizeMode); + getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode)); + } + } + + protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode); /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ From 622d698ff87e8afde15cb59d0187a0f94296b1dd Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 8 Feb 2018 12:46:54 -0800 Subject: [PATCH 083/276] -Added LeakCanary to debug build for memory detection on activities and fragments. -Added LeakCanary no-op lib to release and beta builds. --- app/build.gradle | 4 ++++ .../java/org/schabi/newpipe/DebugApp.java | 11 +++++++++++ app/src/main/java/org/schabi/newpipe/App.java | 18 ++++++++++++++++++ .../java/org/schabi/newpipe/BaseFragment.java | 8 ++++++++ 4 files changed, 41 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 273616f91..d3084c2d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,4 +89,8 @@ dependencies { implementation 'frankiesardo:icepick:3.2.0' annotationProcessor 'frankiesardo:icepick-processor:3.2.0' + + debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' + betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' + releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' } diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index 4d37094ba..dba4179c2 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -4,6 +4,10 @@ import android.content.Context; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; + +import java.util.concurrent.TimeUnit; public class DebugApp extends App { private static final String TAG = DebugApp.class.toString(); @@ -41,4 +45,11 @@ public class DebugApp extends App { // Initialize Stetho with the Initializer Stetho.initialize(initializer); } + + @Override + protected RefWatcher installLeakCanary() { + return LeakCanary.refWatcher(this) + .watchDelay(5, TimeUnit.SECONDS) + .buildAndInstall(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 79221db7f..e171c7726 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -10,6 +10,8 @@ import android.util.Log; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; import org.acra.ACRA; import org.acra.config.ACRAConfiguration; @@ -54,6 +56,7 @@ import io.reactivex.plugins.RxJavaPlugins; public class App extends Application { protected static final String TAG = App.class.toString(); + private RefWatcher refWatcher; @SuppressWarnings("unchecked") private static final Class[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; @@ -69,6 +72,13 @@ public class App extends Application { public void onCreate() { super.onCreate(); + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + refWatcher = installLeakCanary(); + // Initialize settings first because others inits can use its values SettingsActivity.initSettings(this); @@ -157,4 +167,12 @@ public class App extends Application { mNotificationManager.createNotificationChannel(mChannel); } + public static RefWatcher getRefWatcher(Context context) { + final App application = (App) context.getApplicationContext(); + return application.refWatcher; + } + + protected RefWatcher installLeakCanary() { + return RefWatcher.DISABLED; + } } diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 6cd79e2c9..383e0dc4f 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -13,6 +13,7 @@ import android.view.View; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; +import com.squareup.leakcanary.RefWatcher; import icepick.Icepick; @@ -67,6 +68,13 @@ public abstract class BaseFragment extends Fragment { protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { } + @Override + public void onDestroy() { + super.onDestroy(); + RefWatcher refWatcher = App.getRefWatcher(getActivity()); + refWatcher.watch(this); + } + /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ From 829059ea0101a0ec1346a785d9d82f4231d29974 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 10 Feb 2018 11:07:17 -0800 Subject: [PATCH 084/276] -Added toggle for enabling leak canary heap dump. --- .../java/org/schabi/newpipe/DebugApp.java | 34 ++++++++++++++++++- app/src/main/java/org/schabi/newpipe/App.java | 2 ++ .../java/org/schabi/newpipe/BaseFragment.java | 3 +- .../java/org/schabi/newpipe/MainActivity.java | 24 +++++++++++++ app/src/main/res/menu/debug_menu.xml | 12 +++++++ app/src/main/res/values/settings_keys.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ 7 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/menu/debug_menu.xml diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index dba4179c2..ba1fd90cc 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -1,12 +1,20 @@ package org.schabi.newpipe; import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; +import com.squareup.leakcanary.AndroidHeapDumper; +import com.squareup.leakcanary.DefaultLeakDirectoryProvider; +import com.squareup.leakcanary.HeapDumper; import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.LeakDirectoryProvider; import com.squareup.leakcanary.RefWatcher; +import java.io.File; import java.util.concurrent.TimeUnit; public class DebugApp extends App { @@ -49,7 +57,31 @@ public class DebugApp extends App { @Override protected RefWatcher installLeakCanary() { return LeakCanary.refWatcher(this) - .watchDelay(5, TimeUnit.SECONDS) + .heapDumper(new ToggleableHeapDumper(this)) + // give each object 10 seconds to be gc'ed, before leak canary gets nosy on it + .watchDelay(10, TimeUnit.SECONDS) .buildAndInstall(); } + + public static class ToggleableHeapDumper implements HeapDumper { + private final HeapDumper dumper; + private final SharedPreferences preferences; + private final String dumpingAllowanceKey; + + ToggleableHeapDumper(@NonNull final Context context) { + LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context); + this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider); + this.preferences = PreferenceManager.getDefaultSharedPreferences(context); + this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key); + } + + private boolean isDumpingAllowed() { + return preferences.getBoolean(dumpingAllowanceKey, false); + } + + @Override + public File dumpHeap() { + return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index e171c7726..b15a38aae 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -5,6 +5,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; +import android.support.annotation.Nullable; import android.util.Log; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; @@ -167,6 +168,7 @@ public class App extends Application { mNotificationManager.createNotificationChannel(mChannel); } + @Nullable public static RefWatcher getRefWatcher(Context context) { final App application = (App) context.getApplicationContext(); return application.refWatcher; diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 383e0dc4f..d3e4a4b28 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -71,8 +71,9 @@ public abstract class BaseFragment extends Fragment { @Override public void onDestroy() { super.onDestroy(); + RefWatcher refWatcher = App.getRefWatcher(getActivity()); - refWatcher.watch(this); + if (refWatcher != null) refWatcher.watch(this); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 9a1ecd07a..329bd4165 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -211,6 +211,12 @@ public class MainActivity extends AppCompatActivity { } } + private void onHeapDumpToggled(@NonNull MenuItem item) { + final boolean newToggleState = !item.isChecked(); + sharedPreferences.edit().putBoolean(getString(R.string.allow_heap_dumping_key), + newToggleState).apply(); + item.setChecked(newToggleState); + } /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @@ -232,6 +238,10 @@ public class MainActivity extends AppCompatActivity { inflater.inflate(R.menu.main_menu, menu); } + if (DEBUG) { + getMenuInflater().inflate(R.menu.debug_menu, menu); + } + ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); @@ -242,6 +252,17 @@ public class MainActivity extends AppCompatActivity { return true; } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem heapDumpToggle = menu.findItem(R.id.action_toggle_heap_dump); + if (heapDumpToggle != null) { + final boolean isToggled = sharedPreferences.getBoolean( + getString(R.string.allow_heap_dumping_key), false); + heapDumpToggle.setChecked(isToggled); + } + return super.onPrepareOptionsMenu(menu); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); @@ -262,6 +283,9 @@ public class MainActivity extends AppCompatActivity { case R.id.action_history: NavigationHelper.openHistory(this); return true; + case R.id.action_toggle_heap_dump: + onHeapDumpToggled(item); + return true; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/res/menu/debug_menu.xml b/app/src/main/res/menu/debug_menu.xml new file mode 100644 index 000000000..448f9cf23 --- /dev/null +++ b/app/src/main/res/menu/debug_menu.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..5934fdee6 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -83,6 +83,9 @@ last_orientation_landscape_key + + allow_heap_dumping_key + theme light_theme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 767bffa10..fbdd22564 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -395,4 +395,7 @@ Added to playlist Playlist thumbnail changed Failed to delete playlist + + + Leak Canary From e7d148336b2c88c09835775c6e5e0e9d55c20a43 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 10 Feb 2018 12:24:23 -0800 Subject: [PATCH 085/276] -Changed leak canary toggle text to "monitor leaks". -Added toast when enabling/disabling heap dumping. --- .../java/org/schabi/newpipe/MainActivity.java | 19 +++++++++++++++---- app/src/main/res/values/strings.xml | 4 +++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 329bd4165..b54da2ad0 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -20,6 +20,7 @@ package org.schabi.newpipe; +import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -39,6 +40,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.Toast; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.fragments.BackPressable; @@ -211,11 +213,20 @@ public class MainActivity extends AppCompatActivity { } } + @SuppressLint("ShowToast") private void onHeapDumpToggled(@NonNull MenuItem item) { - final boolean newToggleState = !item.isChecked(); - sharedPreferences.edit().putBoolean(getString(R.string.allow_heap_dumping_key), - newToggleState).apply(); - item.setChecked(newToggleState); + final boolean isHeapDumpEnabled = !item.isChecked(); + + sharedPreferences.edit().putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply(); + item.setChecked(isHeapDumpEnabled); + + final String heapDumpNotice; + if (isHeapDumpEnabled) { + heapDumpNotice = getString(R.string.enable_leak_canary_notice); + } else { + heapDumpNotice = getString(R.string.disable_leak_canary_notice); + } + Toast.makeText(getApplicationContext(), heapDumpNotice, Toast.LENGTH_SHORT).show(); } /*////////////////////////////////////////////////////////////////////////// // Menu diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fbdd22564..b1478fed4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -397,5 +397,7 @@ Failed to delete playlist - Leak Canary + Monitor Leaks + Memory leak monitoring enabled, app may become unresponsive when heap dumping + Memory leak monitoring disabled From 263a816c3b9ad1de439a16d1a70964ece6bf74e2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sun, 11 Feb 2018 11:40:08 -0800 Subject: [PATCH 086/276] -Fixed preferences fetching. --- app/src/main/java/org/schabi/newpipe/MainActivity.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index b54da2ad0..ea6715f16 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -27,6 +27,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; @@ -217,7 +218,8 @@ public class MainActivity extends AppCompatActivity { private void onHeapDumpToggled(@NonNull MenuItem item) { final boolean isHeapDumpEnabled = !item.isChecked(); - sharedPreferences.edit().putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply(); + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply(); item.setChecked(isHeapDumpEnabled); final String heapDumpNotice; @@ -267,8 +269,8 @@ public class MainActivity extends AppCompatActivity { public boolean onPrepareOptionsMenu(Menu menu) { MenuItem heapDumpToggle = menu.findItem(R.id.action_toggle_heap_dump); if (heapDumpToggle != null) { - final boolean isToggled = sharedPreferences.getBoolean( - getString(R.string.allow_heap_dumping_key), false); + final boolean isToggled = PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(getString(R.string.allow_heap_dumping_key), false); heapDumpToggle.setChecked(isToggled); } return super.onPrepareOptionsMenu(menu); From 2773f5fbc8318e37c6753a659d0a10ecb14b38a8 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Sun, 11 Feb 2018 21:34:32 +0100 Subject: [PATCH 087/276] move download menu item into detail controls menu --- .../fragments/detail/ActionBarHandler.java | 15 +- .../fragments/detail/VideoDetailFragment.java | 162 +++++++----------- .../main/res/layout/fragment_video_detail.xml | 18 ++ app/src/main/res/menu/video_detail_menu.xml | 6 - app/src/main/res/values/strings.xml | 1 + 5 files changed, 86 insertions(+), 116 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java index 27bffca2d..d928166ab 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java @@ -53,7 +53,6 @@ class ActionBarHandler { // those are edited directly. Typically VideoDetailFragment will implement those callbacks. private OnActionListener onShareListener; private OnActionListener onOpenInBrowserListener; - private OnActionListener onDownloadListener; private OnActionListener onPlayWithKodiListener; // Triggered when a stream related action is triggered. @@ -117,11 +116,6 @@ class ActionBarHandler { } return true; } - case R.id.menu_item_download: - if (onDownloadListener != null) { - onDownloadListener.onActionSelected(selectedVideoStream); - } - return true; case R.id.action_play_with_kodi: if (onPlayWithKodiListener != null) { onPlayWithKodiListener.onActionSelected(selectedVideoStream); @@ -145,19 +139,12 @@ class ActionBarHandler { onOpenInBrowserListener = listener; } - public void setOnDownloadListener(OnActionListener listener) { - onDownloadListener = listener; - } - public void setOnPlayWithKodiListener(OnActionListener listener) { onPlayWithKodiListener = listener; } - public void showDownloadAction(boolean visible) { - menu.findItem(R.id.menu_item_download).setVisible(visible); - } - public void showPlayWithKodiAction(boolean visible) { menu.findItem(R.id.action_play_with_kodi).setVisible(visible); } + } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 89f35c306..daf422516 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -147,6 +147,7 @@ public class VideoDetailFragment extends BaseStateFragment implement private TextView detailControlsBackground; private TextView detailControlsPopup; private TextView detailControlsAddToPlaylist; + private TextView detailControlsDownload; private TextView appendControlsDetail; private LinearLayout videoDescriptionRootLayout; @@ -335,6 +336,24 @@ public class VideoDetailFragment extends BaseStateFragment implement .show(getFragmentManager(), TAG); } break; + case R.id.detail_controls_download: + if (!PermissionHelper.checkStoragePermissions(activity)) { + return; + } + + try { + DownloadDialog downloadDialog = + DownloadDialog.newInstance(currentInfo, + sortedStreamVideosList, + actionBarHandler.getSelectedVideoStream()); + downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); + } catch (Exception e) { + Toast.makeText(activity, + R.string.could_not_setup_download_menu, + Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } + break; case R.id.detail_uploader_root_layout: if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { Log.w(TAG, "Can't open channel because we got no channel URL"); @@ -438,6 +457,7 @@ public class VideoDetailFragment extends BaseStateFragment implement detailControlsBackground = rootView.findViewById(R.id.detail_controls_background); detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append); + detailControlsDownload = rootView.findViewById(R.id.detail_controls_download); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); @@ -489,6 +509,7 @@ public class VideoDetailFragment extends BaseStateFragment implement detailControlsBackground.setOnClickListener(this); detailControlsPopup.setOnClickListener(this); detailControlsAddToPlaylist.setOnClickListener(this); + detailControlsDownload.setOnClickListener(this); relatedStreamExpandButton.setOnClickListener(this); detailControlsBackground.setLongClickable(true); @@ -508,19 +529,16 @@ public class VideoDetailFragment extends BaseStateFragment implement context.getResources().getString(R.string.enqueue_on_popup) }; - final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - switch (i) { - case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); - break; - case 1: - NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); - break; - default: - break; - } + final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { + switch (i) { + case 0: + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + break; + case 1: + NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); + break; + default: + break; } }; @@ -528,21 +546,19 @@ public class VideoDetailFragment extends BaseStateFragment implement } private View.OnTouchListener getOnControlsTouchListener() { - return new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; + return (View view, MotionEvent motionEvent) -> { + view.performClick(); + if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; - if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { - animateView(appendControlsDetail, true, 250, 0, new Runnable() { - @Override - public void run() { - animateView(appendControlsDetail, false, 1500, 1000); - } - }); - } - return false; + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + animateView(appendControlsDetail, true, 250, 0, new Runnable() { + @Override + public void run() { + animateView(appendControlsDetail, false, 1500, 1000); + } + }); } + return false; }; } @@ -615,18 +631,9 @@ public class VideoDetailFragment extends BaseStateFragment implement private static void showInstallKoreDialog(final Context context) { final AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setMessage(R.string.kore_not_found) - .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - NavigationHelper.installKore(context); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }); + .setPositiveButton(R.string.install, (DialogInterface dialog, int which) -> + NavigationHelper.installKore(context)) + .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {}); builder.create().show(); } @@ -636,41 +643,19 @@ public class VideoDetailFragment extends BaseStateFragment implement actionBarHandler.setupStreamList(sortedStreamVideosList, spinnerToolbar); actionBarHandler.setOnShareListener(selectedStreamId -> shareUrl(info.name, info.url)); - actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() { - @Override - public void onActionSelected(int selectedStreamId) { - openUrlInBrowser(info.getUrl()); + actionBarHandler.setOnOpenInBrowserListener((int selectedStreamId)-> + openUrlInBrowser(info.getUrl())); + + actionBarHandler.setOnPlayWithKodiListener((int selectedStreamId) -> { + try { + NavigationHelper.playWithKore(activity, Uri.parse( + info.getUrl().replace("https", "http"))); + } catch (Exception e) { + if(DEBUG) Log.i(TAG, "Failed to start kore", e); + showInstallKoreDialog(activity); } }); - actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() { - @Override - public void onActionSelected(int selectedStreamId) { - try { - NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http"))); - } catch (Exception e) { - if(DEBUG) Log.i(TAG, "Failed to start kore", e); - showInstallKoreDialog(activity); - } - } - }); - - actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { - @Override - public void onActionSelected(int selectedStreamId) { - if (!PermissionHelper.checkStoragePermissions(activity)) { - return; - } - - try { - DownloadDialog downloadDialog = DownloadDialog.newInstance(info, sortedStreamVideosList, selectedStreamId); - downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); - } catch (Exception e) { - Toast.makeText(activity, R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show(); - e.printStackTrace(); - } - } - }); } /*////////////////////////////////////////////////////////////////////////// @@ -777,20 +762,14 @@ public class VideoDetailFragment extends BaseStateFragment implement currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(@NonNull StreamInfo result) throws Exception { - isLoading.set(false); - currentInfo = result; - showContentWithAnimation(120, 0, 0); - handleResult(result); - } - }, new Consumer() { - @Override - public void accept(@NonNull Throwable throwable) throws Exception { - isLoading.set(false); - onError(throwable); - } + .subscribe((@NonNull StreamInfo result) -> { + isLoading.set(false); + currentInfo = result; + showContentWithAnimation(120, 0, 0); + handleResult(result); + }, (@NonNull Throwable throwable) -> { + isLoading.set(false); + onError(throwable); }); } @@ -884,9 +863,7 @@ public class VideoDetailFragment extends BaseStateFragment implement } disposables.add(Single.just(descriptionHtml) - .map(new Function() { - @Override - public Spanned apply(@io.reactivex.annotations.NonNull String description) throws Exception { + .map((@io.reactivex.annotations.NonNull String description) -> { Spanned parsedDescription; if (Build.VERSION.SDK_INT >= 24) { parsedDescription = Html.fromHtml(description, 0); @@ -895,16 +872,12 @@ public class VideoDetailFragment extends BaseStateFragment implement parsedDescription = Html.fromHtml(description); } return parsedDescription; - } }) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(@io.reactivex.annotations.NonNull Spanned spanned) throws Exception { + .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { videoDescriptionView.setText(spanned); videoDescriptionView.setVisibility(View.VISIBLE); - } })); } @@ -1131,14 +1104,11 @@ public class VideoDetailFragment extends BaseStateFragment implement } public void onBlockedByGemaError() { - thumbnailBackgroundButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { + thumbnailBackgroundButton.setOnClickListener((View v) -> { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setData(Uri.parse(getString(R.string.c3s_url))); startActivity(intent); - } }); showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 2d39d3d70..d47a2215b 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -354,6 +354,24 @@ android:paddingTop="6dp" android:text="@string/controls_popup_title" android:textSize="12sp"/> + + + - - Open in popup mode Share Download + Download stream file. Search Settings Did you mean: %1$s ? From ba0be665ae9b68b4d43fe8f41cb9d07f91cf943e Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 12 Feb 2018 00:43:12 +0100 Subject: [PATCH 088/276] fixed issues from prevoius merge --- .../newpipe/fragments/detail/VideoDetailFragment.java | 9 ++------- app/src/main/res/layout/fragment_video_detail.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index daf422516..206f6edd8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -547,16 +547,11 @@ public class VideoDetailFragment extends BaseStateFragment implement private View.OnTouchListener getOnControlsTouchListener() { return (View view, MotionEvent motionEvent) -> { - view.performClick(); if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { - animateView(appendControlsDetail, true, 250, 0, new Runnable() { - @Override - public void run() { - animateView(appendControlsDetail, false, 1500, 1000); - } - }); + animateView(appendControlsDetail, true, 250, 0, () -> + animateView(appendControlsDetail, false, 1500, 1000)); } return false; }; diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index d47a2215b..330fb34da 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -364,7 +364,7 @@ android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:contentDescription="@string/controls_download_desg" + android:contentDescription="@string/controls_download_desc" android:drawableTop="?attr/download" android:gravity="center" android:paddingBottom="6dp" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c04cb802..ea8f0fce8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ Open in popup mode Share Download - Download stream file. + Download stream file. Search Settings Did you mean: %1$s ? From 738e2ac3443fa256cbdb7c452cf28c93bf9dfdd5 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 12 Feb 2018 19:44:35 +0100 Subject: [PATCH 089/276] merge RouterActivity and RouterVideoActivity --- app/src/main/AndroidManifest.xml | 113 ++--- .../org/schabi/newpipe/RouterActivity.java | 460 ++++++++++++++++-- .../schabi/newpipe/RouterPlayerActivity.java | 413 ---------------- .../ic_info_outline_black_24dp.png | Bin 0 -> 487 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 485 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 323 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 320 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 640 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 655 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 940 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 953 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 1256 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 1279 bytes app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 2 + 17 files changed, 465 insertions(+), 526 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc3dc62e6..f286ee76c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -122,8 +122,12 @@ + + @@ -169,6 +173,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -176,17 +215,8 @@ - - - - - - - - - - + @@ -195,68 +225,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 8aaa248dd..0586a86be 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -1,51 +1,80 @@ package org.schabi.newpipe; +import android.app.IntentService; +import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; +import android.os.PersistableBundle; +import android.preference.PreferenceManager; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.Toast; +import org.schabi.newpipe.extractor.Info; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.StreamingService.LinkType; +import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.playlist.ChannelPlayQueue; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlaylistPlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ThemeHelper; +import java.io.Serializable; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import icepick.Icepick; import icepick.State; import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; -/* - * Copyright (C) Christian Schabesberger 2017 - * RouterActivity.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 . - */ +import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; /** - * This Acitivty is designed to route share/open intents to the specified service, and - * to the part of the service which can handle the url. + * Get the url from the intent and open it in the chosen preferred player */ public class RouterActivity extends AppCompatActivity { @State + protected int currentServiceId = -1; + private StreamingService currentService; + @State + protected LinkType currentLinkType; + @State + protected int selectedRadioPosition = -1; + protected int selectedPreviously = -1; + protected String currentUrl; protected CompositeDisposable disposables = new CompositeDisposable(); @@ -62,6 +91,10 @@ public class RouterActivity extends AppCompatActivity { finish(); } } + + setTheme(ThemeHelper.isLightThemeSelected(this) + ? R.style.RouterActivityThemeLight + : R.style.RouterActivityThemeDark); } @Override @@ -73,25 +106,43 @@ public class RouterActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); + handleUrl(currentUrl); } - protected void handleUrl(String url) { - disposables.add(Observable - .fromCallable(() -> NavigationHelper.getIntentByLink(this, url)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(intent -> { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); + @Override + protected void onDestroy() { + super.onDestroy(); - finish(); - }, this::handleError) - ); + disposables.clear(); } - protected void handleError(Throwable error) { + private void handleUrl(String url) { + disposables.add(Observable + .fromCallable(() -> { + if (currentServiceId == -1) { + currentService = NewPipe.getServiceByUrl(url); + currentServiceId = currentService.getServiceId(); + currentLinkType = currentService.getLinkTypeByUrl(url); + currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); + } else { + currentService = NewPipe.getService(currentServiceId); + } + + return currentLinkType != LinkType.NONE; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result) { + onSuccess(); + } else { + onError(); + } + }, this::handleError)); + } + + private void handleError(Throwable error) { error.printStackTrace(); if (error instanceof ExtractionException) { @@ -103,11 +154,339 @@ public class RouterActivity extends AppCompatActivity { finish(); } - @Override - protected void onDestroy() { - super.onDestroy(); + private void onError() { + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + finish(); + } - disposables.clear(); + protected void onSuccess() { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + + if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { + Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) + if (currentService == ServiceList.SoundCloud.getService()) { + handleChoice(getString(R.string.background_player_key)); + return; + } + + final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); + final String alwaysAskKey = getString(R.string.always_ask_player_key); + + if (playerChoiceKey.equals(alwaysAskKey)) { + showDialog(); + } else { + handleChoice(playerChoiceKey); + } + } + + private void showDialog() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, + ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); + + LayoutInflater inflater = LayoutInflater.from(themeWrapper); + final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); + final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); + + final AdapterChoiceItem[] choices = { + new AdapterChoiceItem(getString(R.string.info_screen_key), getString(R.string.info_screen), + resolveResourceIdFromAttr(themeWrapper, R.attr.info)), + new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.play)), + new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), + new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) + }; + + final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { + final int indexOfChild = radioGroup.indexOfChild( + radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); + final AdapterChoiceItem choice = choices[indexOfChild]; + + handleChoice(choice.key); + + if (which == DialogInterface.BUTTON_POSITIVE) { + preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); + } + }; + + final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) + .setTitle(R.string.preferred_player_share_menu_title) + .setView(radioGroup) + .setCancelable(true) + .setNegativeButton(R.string.just_once, dialogButtonsClickListener) + .setPositiveButton(R.string.always, dialogButtonsClickListener) + .setOnDismissListener((dialog) -> finish()) + .create(); + + alertDialog.setOnShowListener(dialog -> { + setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); + }); + + radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); + final View.OnClickListener radioButtonsClickListener = v -> { + final int indexOfChild = radioGroup.indexOfChild(v); + if (indexOfChild == -1) return; + + selectedPreviously = selectedRadioPosition; + selectedRadioPosition = indexOfChild; + + if (selectedPreviously == selectedRadioPosition) { + handleChoice(choices[selectedRadioPosition].key); + } + }; + + int id = 12345; + for (AdapterChoiceItem item : choices) { + final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + radioButton.setText(item.description); + radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); + radioButton.setChecked(false); + radioButton.setId(id++); + radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + radioButton.setOnClickListener(radioButtonsClickListener); + radioGroup.addView(radioButton); + } + + if (selectedRadioPosition == -1) { + final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); + if (!TextUtils.isEmpty(lastSelectedPlayer)) { + for (int i = 0; i < choices.length; i++) { + AdapterChoiceItem c = choices[i]; + if (lastSelectedPlayer.equals(c.key)) { + selectedRadioPosition = i; + break; + } + } + } + } + + selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); + if (selectedRadioPosition != -1) { + ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); + } + selectedPreviously = selectedRadioPosition; + + alertDialog.show(); + } + + private void setDialogButtonsState(AlertDialog dialog, boolean state) { + final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (negativeButton == null || positiveButton == null) return; + + negativeButton.setEnabled(state); + positiveButton.setEnabled(state); + } + + private void handleChoice(final String playerChoiceKey) { + if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); + } + + if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { + PermissionHelper.showPopupEnablementToast(this); + finish(); + return; + } + + // stop and bypass FetcherService if InfoScreen was selected since + // StreamDetailFragment can fetch data itself + if(playerChoiceKey.equals(getString(R.string.info_screen_key))) { + disposables.add(Observable + .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(intent -> { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + + finish(); + }, this::handleError) + ); + return; + } + + final Intent intent = new Intent(this, FetcherService.class); + intent.putExtra(FetcherService.KEY_CHOICE, + new Choice(currentService.getServiceId(), + currentLinkType, + currentUrl, + playerChoiceKey)); + startService(intent); + + finish(); + } + + private static class AdapterChoiceItem { + final String description, key; + @DrawableRes + final int icon; + + AdapterChoiceItem(String key, String description, int icon) { + this.description = description; + this.key = key; + this.icon = icon; + } + } + + private static class Choice implements Serializable { + final int serviceId; + final String url, playerChoice; + final LinkType linkType; + + Choice(int serviceId, LinkType linkType, String url, String playerChoice) { + this.serviceId = serviceId; + this.linkType = linkType; + this.url = url; + this.playerChoice = playerChoice; + } + + @Override + public String toString() { + return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Service Fetcher + //////////////////////////////////////////////////////////////////////////*/ + + public static class FetcherService extends IntentService { + + private static final int ID = 456; + public static final String KEY_CHOICE = "key_choice"; + private Disposable fetcher; + + public FetcherService() { + super(FetcherService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + startForeground(ID, createNotification().build()); + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + if (intent == null) return; + + final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); + if (!(serializable instanceof Choice)) return; + Choice playerChoice = (Choice) serializable; + handleChoice(playerChoice); + } + + public void handleChoice(Choice choice) { + Single single = null; + UserAction userAction = UserAction.SOMETHING_ELSE; + + switch (choice.linkType) { + case STREAM: + single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_STREAM; + break; + case CHANNEL: + single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_CHANNEL; + break; + case PLAYLIST: + single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_PLAYLIST; + break; + } + + + if (single != null) { + final UserAction finalUserAction = userAction; + final Consumer resultHandler = getResultHandler(choice); + fetcher = single + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + resultHandler.accept(info); + if (fetcher != null) fetcher.dispose(); + }, throwable -> ExtractorHelper.handleGeneralException(this, + choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); + } + } + + public Consumer getResultHandler(Choice choice) { + return info -> { + final String videoPlayerKey = getString(R.string.video_player_key); + final String backgroundPlayerKey = getString(R.string.background_player_key); + final String popupPlayerKey = getString(R.string.popup_player_key); + + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); + + PlayQueue playQueue; + String playerChoice = choice.playerChoice; + + if (info instanceof StreamInfo) { + if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { + NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { + NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { + NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); + + } else { + playQueue = new SinglePlayQueue((StreamInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); + } + } + } + + if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { + playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.playOnBackgroundPlayer(this, playQueue); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.playOnPopupPlayer(this, playQueue); + } + } + }; + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopForeground(true); + if (fetcher != null) fetcher.dispose(); + } + + private NotificationCompat.Builder createNotification() { + return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) + .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); + } } /*////////////////////////////////////////////////////////////////////////// @@ -119,9 +498,9 @@ public class RouterActivity extends AppCompatActivity { * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for * more details. */ - protected final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; + private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; - protected String getUrl(Intent intent) { + private String getUrl(Intent intent) { // first gather data and find service String videoUrl = null; if (intent.getData() != null) { @@ -137,7 +516,7 @@ public class RouterActivity extends AppCompatActivity { return videoUrl; } - protected String removeHeadingGibberish(final String input) { + private String removeHeadingGibberish(final String input) { int start = 0; for (int i = input.indexOf("://") - 1; i >= 0; i--) { if (!input.substring(i, i + 1).matches("\\p{L}")) { @@ -148,7 +527,7 @@ public class RouterActivity extends AppCompatActivity { return input.substring(start, input.length()); } - protected String trim(final String input) { + private String trim(final String input) { if (input == null || input.length() < 1) { return input; } else { @@ -188,5 +567,4 @@ public class RouterActivity extends AppCompatActivity { } return result.toArray(new String[result.size()]); } - } diff --git a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java deleted file mode 100644 index 7196e413d..000000000 --- a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java +++ /dev/null @@ -1,413 +0,0 @@ -package org.schabi.newpipe; - -import android.app.IntentService; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.PersistableBundle; -import android.preference.PreferenceManager; -import android.support.annotation.DrawableRes; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.support.v7.app.AlertDialog; -import android.text.TextUtils; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.Toast; - -import org.schabi.newpipe.extractor.Info; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.StreamingService.LinkType; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.playlist.ChannelPlayQueue; -import org.schabi.newpipe.playlist.PlayQueue; -import org.schabi.newpipe.playlist.PlaylistPlayQueue; -import org.schabi.newpipe.playlist.SinglePlayQueue; -import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ThemeHelper; - -import java.io.Serializable; -import java.util.Arrays; - -import icepick.State; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; - -/** - * Get the url from the intent and open it in the chosen preferred player - */ -public class RouterPlayerActivity extends RouterActivity { - - @State - protected int currentServiceId = -1; - private StreamingService currentService; - @State - protected LinkType currentLinkType; - @State - protected int selectedRadioPosition = -1; - protected int selectedPreviously = -1; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { - super.onCreate(savedInstanceState, persistentState); - setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); - } - - @Override - protected void handleUrl(String url) { - disposables.add(Observable - .fromCallable(() -> { - if (currentServiceId == -1) { - currentService = NewPipe.getServiceByUrl(url); - currentServiceId = currentService.getServiceId(); - currentLinkType = currentService.getLinkTypeByUrl(url); - currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); - } else { - currentService = NewPipe.getService(currentServiceId); - } - - return currentLinkType != LinkType.NONE; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - onSuccess(); - } else { - onError(); - } - }, this::handleError)); - } - - protected void onError() { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); - finish(); - } - - protected void onSuccess() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - - if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { - Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); - finish(); - return; - } - - // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) - if (currentService == ServiceList.SoundCloud.getService()) { - handleChoice(getString(R.string.background_player_key)); - return; - } - - final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); - final String alwaysAskKey = getString(R.string.always_ask_player_key); - - if (playerChoiceKey.equals(alwaysAskKey)) { - showDialog(); - } else { - handleChoice(playerChoiceKey); - } - } - - private void showDialog() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, - ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); - - LayoutInflater inflater = LayoutInflater.from(themeWrapper); - final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); - final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); - - final AdapterChoiceItem[] choices = { - new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.play)), - new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), - new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) - }; - - final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { - final int indexOfChild = radioGroup.indexOfChild(radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); - final AdapterChoiceItem choice = choices[indexOfChild]; - - handleChoice(choice.key); - - if (which == DialogInterface.BUTTON_POSITIVE) { - preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); - } - }; - - final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) - .setTitle(R.string.preferred_player_share_menu_title) - .setView(radioGroup) - .setCancelable(true) - .setNegativeButton(R.string.just_once, dialogButtonsClickListener) - .setPositiveButton(R.string.always, dialogButtonsClickListener) - .setOnDismissListener((dialog) -> finish()) - .create(); - - alertDialog.setOnShowListener(dialog -> { - setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); - }); - - radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); - final View.OnClickListener radioButtonsClickListener = v -> { - final int indexOfChild = radioGroup.indexOfChild(v); - if (indexOfChild == -1) return; - - selectedPreviously = selectedRadioPosition; - selectedRadioPosition = indexOfChild; - - if (selectedPreviously == selectedRadioPosition) { - handleChoice(choices[selectedRadioPosition].key); - } - }; - - int id = 12345; - for (AdapterChoiceItem item : choices) { - final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); - radioButton.setText(item.description); - radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); - radioButton.setChecked(false); - radioButton.setId(id++); - radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - radioButton.setOnClickListener(radioButtonsClickListener); - radioGroup.addView(radioButton); - } - - if (selectedRadioPosition == -1) { - final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); - if (!TextUtils.isEmpty(lastSelectedPlayer)) { - for (int i = 0; i < choices.length; i++) { - AdapterChoiceItem c = choices[i]; - if (lastSelectedPlayer.equals(c.key)) { - selectedRadioPosition = i; - break; - } - } - } - } - - selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); - if (selectedRadioPosition != -1) { - ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); - } - selectedPreviously = selectedRadioPosition; - - alertDialog.show(); - } - - private void setDialogButtonsState(AlertDialog dialog, boolean state) { - final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); - final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - if (negativeButton == null || positiveButton == null) return; - - negativeButton.setEnabled(state); - positiveButton.setEnabled(state); - } - - private void handleChoice(final String playerChoiceKey) { - if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); - } - - if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { - PermissionHelper.showPopupEnablementToast(this); - finish(); - return; - } - - final Intent intent = new Intent(this, FetcherService.class); - intent.putExtra(FetcherService.KEY_CHOICE, new Choice(currentService.getServiceId(), currentLinkType, currentUrl, playerChoiceKey)); - startService(intent); - - finish(); - } - - private static class AdapterChoiceItem { - final String description, key; - @DrawableRes - final int icon; - - AdapterChoiceItem(String key, String description, int icon) { - this.description = description; - this.key = key; - this.icon = icon; - } - } - - private static class Choice implements Serializable { - final int serviceId; - final String url, playerChoice; - final LinkType linkType; - - Choice(int serviceId, LinkType linkType, String url, String playerChoice) { - this.serviceId = serviceId; - this.linkType = linkType; - this.url = url; - this.playerChoice = playerChoice; - } - - @Override - public String toString() { - return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Service Fetcher - //////////////////////////////////////////////////////////////////////////*/ - - public static class FetcherService extends IntentService { - - private static final int ID = 456; - public static final String KEY_CHOICE = "key_choice"; - private Disposable fetcher; - - public FetcherService() { - super(FetcherService.class.getSimpleName()); - } - - @Override - public void onCreate() { - super.onCreate(); - startForeground(ID, createNotification().build()); - } - - @Override - protected void onHandleIntent(@Nullable Intent intent) { - if (intent == null) return; - - final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); - if (!(serializable instanceof Choice)) return; - Choice playerChoice = (Choice) serializable; - handleChoice(playerChoice); - } - - public void handleChoice(Choice choice) { - Single single = null; - UserAction userAction = UserAction.SOMETHING_ELSE; - - switch (choice.linkType) { - case STREAM: - single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_STREAM; - break; - case CHANNEL: - single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_CHANNEL; - break; - case PLAYLIST: - single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_PLAYLIST; - break; - } - - - if (single != null) { - final UserAction finalUserAction = userAction; - final Consumer resultHandler = getResultHandler(choice); - fetcher = single - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(info -> { - resultHandler.accept(info); - if (fetcher != null) fetcher.dispose(); - }, throwable -> ExtractorHelper.handleGeneralException(this, - choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); - } - } - - public Consumer getResultHandler(Choice choice) { - return info -> { - final String videoPlayerKey = getString(R.string.video_player_key); - final String backgroundPlayerKey = getString(R.string.background_player_key); - final String popupPlayerKey = getString(R.string.popup_player_key); - - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); - - PlayQueue playQueue; - String playerChoice = choice.playerChoice; - - if (info instanceof StreamInfo) { - if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { - NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { - NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { - NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); - - } else { - playQueue = new SinglePlayQueue((StreamInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); - } - } - } - - if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { - playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.playOnBackgroundPlayer(this, playQueue); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.playOnPopupPlayer(this, playQueue); - } - } - }; - } - - @Override - public void onDestroy() { - super.onDestroy(); - stopForeground(true); - if (fetcher != null) fetcher.dispose(); - } - - private NotificationCompat.Builder createNotification() { - return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) - .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); - } - } -} diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5ab06e19d61515cf3a7395db43a81bb30a8203 GIT binary patch literal 487 zcmVS-P=?%O3jv@aJa6HGW<0ceyC!`rEoc~?j{U=`ic|b z9N*QPsMv*+MQv@1i8kR%)UIh1`=XoCqqIYBOKErvea{GWZ!y>;D~RpT`=%tUi9cM8 zjMJd-5IZXpx*;AQpQG66NVp4e3Hh{Q=T<@);yl@>u@F125nkF002ovPDHLkV1kFY-~<2w literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c7b1113cfef22bcec86ead7ae67be12326276cab GIT binary patch literal 485 zcmVW+#V#f>U=9~3v zy5k#|%`o-bRp5gvNWBjKU}@RX_o(#cOX<;tO*2A$>f4JBeW-BBCp2Wv4dAi~>H>ZU-{cI? zse=}k;97{XqEJ|gdQ)z}NLv+h2nN-m6riYd=e>Y~D%A1;c7!A-*ac%VF|-AqWJ2?3 zQF6~&s5ci5fY}sT-yed|rRg0)~VyRe(RxP5| ziKSu?+iDTDUGbuL5L2~?+LgFdoQk=|C}Pxj7RQRtyXcJ~MwepHsvL@iJ?s4SCH8$Y z6AQgYt#Ro~-1uQ)CSvZwj!tLSm1hyxCe}LeCL$stVz6(6)`@$smX==JIoA4R{{X3C VfQx`Hg^K_H002ovPDHLkV1oM>kN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..353e064951788a29a64eef439284fd97e904f374 GIT binary patch literal 320 zcmV-G0l)rT_h)IX%8`!xx#?O($dW=BI;pgAmAMoJ&FVmpak^_M&10n8om%>Hp0CB z>RbH6BfRjxN9M~)&M4Ia$ETJBLQ^<<<3g$He5hLhidW{F&l zq78;P`5|=ls>YxRGDkGO4)Ha@UK+s{*1}DYDDe@&3c1kJ7>fuhJobWT>>{`#?O7?v zaJd6ic&m6Kxyn*Ci7xy1O7D6yV<~Ad#ZoSbb%oCVPWUuVr%YQDrkfL?gZZ)JBOfu SH&bZ<0000o#88fEO5na|OlO{3G z*yIPEVO{WsIjnKnVT>2DMzkql!ApMPMO=L<#3I9oI}LubOO+CNvScYxqQ)LKcQkDr zB9h}Xp02N~GG)+JIYyNnkc)PDZOnzu*#; zB8R%~1(i4Rl0>e>Eb#WLtxfOV(#vvo;8MrfX8# zOJwj-6bRC^RW#U^^>`Uk%GqUlp=9>%y-#&$qZ(w=qV2=#%SNueJg~}R_Q4VL}5`+@9~aM z91c`Iph*TZkfp^?^`Dp^0y)$Ij}vN48)8+LhbiruA|j3sO47|fb;`I*GKou>Ivq)^ z4u=V1QQ;ad;<{u73-W9;3T-}Qiy5qW$_D4g=4Y%kOJdS2(WFC22NX4g$Gh;r2I3UU zC}4AjSPu(ixIwI-i~@=>;)?%*fd#|^BPhd-@r0NU2I`1i6z1DU)PjK~qBKyTf@lN- zCy2s8fg++640I7Wl<`GYIEcGoz(-^U3JfE>zYypm@_+0U8C+z9*+p(5%4UK1zv(Js zZ=k^ch-xrUL_9KT9C*VMq7V#ZxJHzW0~N$&*uzfo7172v3S>D)tf9gJQ}~FgQJ{|K z@irKcxri5LQU~VfAvRG_fjnJAhY3`QO>%*_Wjs2dSVB8ak(i`~wix%~<)U?%MJ3oA z7ie3kxPZkW+6y&o6w%94r-ydHP_ma_Nr*Gbj78j570v p!{d$?4XPC6k!Bt+*lWOGzX87wDYF(rDzN|n002ovPDHLkV1ir08p{9x literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3847a9fe74774d3593dd694cff48ccca13cdec5a GIT binary patch literal 940 zcmV;d15^BoP)mdcfcOtJM6uO{+z8D@UxkKMBq1+Buz@6?P#P0jG1VJo zr4$sRhK5k2Z=fXRN~6nW@y^{1KD~XI=}gYdoFjX&pWp3sm>a@9yM-m0@&}6isAEB(Qj;>qRst`p9~=6q zAu7LO=u+TQ_K7Dd@dg8*_ZUX>5!&WBSSti4^<9Bi7`TY2+y<+KU=~psWnX%OzQeO# z$Y%%reeR=-{wJ-1^+Iq9eT6F#>jwG=^_&zcppV__#9BjN7F^M%z)NCy=&SoFTsf2ppC<<**2_ZI5@SJo9Db@pv9Dwel>@Q2Y|A^pR+ zXm)h0I4$&8t(hXy6*cz3S)t(J`aO{jqc{F(SB1>4AMjd@E;uWs|KhZ2{;};ky2AQ2 z)rxDxD$`NJH=PukQuA$!?4zc}-&3$&Xn>Du{t5dULsafqFEod!oWp?rpGSuiRtueB z&+LQNE+8tK3|cF6gtuz;3+FNP08y#Yj|~U-15sJR*io8@$}eQs#}cB_phN-_NT5>I zKj|$a=v*bC1q7WMgQRsN@Unzi63g)%L1&Ybq;ZC~2s(dqkQ990An0_M#V3gY=Ga5f z`5TXvN})=^4!0;^!e^R~NT^Pc418W7q3m*(v-Aa=K4XNz?fnc_RXp{J*(OZ^8ac7(2N>$@cY O0000+=u1!Si^=c~o6pU56D8MW!jS z$u@O7JnC$-Nr`E0Z~~QViq1F4UDojk)8rj4KiUtv${KCrYEfp?UeGn(lf*h#%mwiZO?K>;SqC6p{w@`EooS#EOw+c}UBbluHuSO9kD3{Dexd zGgQ&NCewM)0=iXlsD$fh4c%;~LATKTP7f-@j@01>+c-g20r3E3BuF*`h>_@^ zHN+KB7e-Jm@Z>j$y%;(Bn$f*7_b_YN7RCYJVeH7P%ol+c!LiAkL*xA)X9%M!t}A%e=*t&auG}M zUL7k|gPtKKgM)4$K3fdZU#`Cd2c5t}oVOJe{qaNg4l!>nXc19Cg$22YJsc}RIU0!D zQ6KVKhxdR-j0Rnxtv}Fl zg03K-O24t7lYB&c=W=+E>@fn$hQ5wD$_oT6qv8ggqJe-vjQr`djDR|Y=pflR=t-%+ z(kr36+3A;_^XSU-C(b#^3v@H6PJ=SMLRaN%qMQqSME8wcL$0HkK4xj7 z`v)iS)q*bxcDYYB<~wVOFW}3`r@4H|@fyE97P!cfkP~%@MH=|M#7TQSm0^y9)p&}_ z7{lc0-Cv$z#&CH?jf4HhF*6r<<1|RC&P|h7oP+#Da^G2|VEz(#l(!^NVZ`1w`3S4D zh|A+Sm+k)}L4iBGp-Gr;RJbh%++L9EIM5D%liv*A40h bb%Xu~a~|hNXxfgr00000NkvXXu0mjfV)nVh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c1e2a03a4bc540419ce1165d5dedd8d5345fdec2 GIT binary patch literal 1256 zcmVP)W$LQv#I8j8T_{yf~=m$~;IXzu5?eh%l+bI*O5LnIQ3L?S-g zNEKIT<0XBJGshx}%rQL6n2^)65BytPpur(aAq3mzkx5L*!vp z0a@Ola>x)hq%a~$J)clM5drmH23z#q<|xU<0`*qKPGo7Si~n z%wqBU&0hSlj{%E?RDuuU7MJcXB=Jj%b`&x_e(Xp;ExUAT@Y|(jm(qjWZ(B;_Uor&Y z6!R#Nv*chaGsx+GPf(5^TbQOC&z>U}Plh0!MlPf-JUW9CsUc{WQ6klNkmN0L=W$Hb zI`=E``6_{H*O2>uGL!}iknh%)aH)U^vHJHyC=I|j;!5~HKCWcN1R5Z;2B2BgU%-W! zczBp#L#PcDi)Yo>;Taa!Y@Ptv;<}7l4r)8NeQGlwNF& zYb9ky8`z|cGKQ@=tt4)=frR*F&DNq;5;NLBOdDlk)i+CZzQKZh$R zK8n3)UuX^N7uDk=aj8xWGC(Sn1_~Jx)mdCiFf4{?52b-lQTv8C9#o@5E)leAD3OzR z)Fn^Gaf%@QAdedG1kbj}b8XCXgrFSb3rcL@n*sbhFEP*Qfd2Kr_;5}_cz_aF;&K3M ze+ebhM9$XHgOZ`06n-hB6D318YwImEMS(?`<%O>vfHEvXL&vNpLe#!|zDLp9b&_k9T z*x5;~c-Pf0-68_APCn*}yr+}vWZ1(d62yp+WD|SHaD&H;+SWI+mO!0l8Wm6`sU`&F zbfE&s6E+i?YKBohW{8u7EWu?ypnS+US>hOx&jq=;y2>EuNMckT2YAG1l*`QUkZ)PT z3X$L_jr@gjkUknXOq>;|goD)5%rkl!W0nOLm}QJ!o^qco2Ph^Ii9{k1-~0zqnGDmx SqDLYC0000 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3a82cab3b4f2cfe77333cee6121d867353753d5e GIT binary patch literal 1279 zcmVXA;DPbB};p_0dfAo z$vAy9#+6S9#ELYJ@(Lqa#%C_802gIqArYp#f(wN zJ5D-2x}I^0EQMtFA?SeRyu{vHx_o|^)~`kO7MeJbsRD=Aw#N4d<{5Bs2vT z(7+gW9I~nSN2#E6lJC{kK0$L^4urow?@d3Y+FGmL|;O)mE-^F4Bc9HL2 zQm7ENoloT3SxmqprfnYoS5!#bXp=BSqyK>0=yIe`A#Mj2x&gld?d0n~g}ejgYx5J3 z2Egy^UyUJ3s{D( z);XXRX&hy6FOb$b2OLD|GaAs3)ae{>38~9yz)_?g=YYFNn~esfkZw5#3?r>T`I=7o z+A5@n&H)oh4MqdzBfWAC_=prY8jwJmaSq5K#f%2TkiLBX0pHn2D2X)lKlmdaMq2s3 zeU08h+F~>yg>=(7pcm8C~mULv(P2h^i$HWskl&kK!W z1ZkhKfKETJl!_jtL1O{;k-FUlY(=tYHWIKD-3E67NoJ5v83{OpG)>%HfT9m6OI73n zbC^cDf^r+M9^H}115Tj(#eF~-qezp~L>f@f6jFv+?gJDZ=*~qN(2H(2%1b~cZ;&k3 zL>90X-2_Q*0g5hkkEx6#pqf#1X_VK11mozgMH0}DZj8A107V=5_M<}HVe)PBd(>b6 zoyB%ki2Dr--3^rAfQ4kyePSajq;2Lix=9xJ4^Xs&O~Q_l{`EhTudUbvPZ!B&aUg`x zzD2%ssA2=k8N$vrDp3KgnttpIGQ0QyMI9OJJZ23lfURW|J0sKtb?mZ<=h(66p%zud z>bZoyXEX+R@O5c{1jurnYLt)7;RI9Id&-iaj~+Mi1PVr+=0}fjTW6S}kP#ZgIQ(7D z5MMJynq@Bjr=gSke9a&=VI99OrxzzL=;tUYRxzIhF=8Z{&ni+J<2o;Ka+cZQUVzxi z6k(a9Es85BjSLWm8!U+K5>OlC1Zk9QQD4JKaDdkYVS+SqGZ*11*vk`&kzqGU(^nB& zxXOFHn&t{WGwWNq44PmQXZZ&=hUsR5_|TXQ5UXPyo%C>vhrD8jFMMH!S3Kk<=SkB- pEh@+ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..8f33f9297 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -155,6 +155,7 @@ @string/always_ask_player_key preferred_player_last_selected + info_screen video_player background_player popup_player diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea8f0fce8..0e9f6e7ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Channel unsubscribed Unable to change subscription Unable to update subscription + Info Screen Main Subscriptions diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b16958ae6..dcf8f9268 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,6 +18,7 @@ @drawable/ic_thumb_up_black_24dp @drawable/ic_thumb_down_black_24dp + @drawable/ic_info_outline_black_24dp @drawable/ic_headset_black_24dp @drawable/ic_delete_sweep_white_24dp @drawable/ic_file_download_black_24dp @@ -69,6 +70,7 @@ @drawable/ic_thumb_up_white_24dp @drawable/ic_thumb_down_white_24dp @drawable/ic_headset_white_24dp + @drawable/ic_info_outline_white_24dp @drawable/ic_delete_sweep_black_24dp @drawable/ic_file_download_white_24dp @drawable/ic_share_white_24dp From cbfe91f36f626fad343d4475f7aefd67e8f28901 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 12 Feb 2018 19:44:35 +0100 Subject: [PATCH 090/276] merge RouterActivity and RouterVideoActivity change share title fixed compatiblity issue rename info_screen to show_info --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 113 ++--- .../org/schabi/newpipe/RouterActivity.java | 460 ++++++++++++++++-- .../schabi/newpipe/RouterPlayerActivity.java | 413 ---------------- .../newpipe/fragments/MainFragment.java | 2 +- .../settings/ContentSettingsFragment.java | 4 +- .../newpipe/settings/SelectKioskFragment.java | 4 +- .../schabi/newpipe/util/ServiceHelper.java | 8 +- .../org/schabi/newpipe/util/ThemeHelper.java | 2 +- .../ic_info_outline_black_24dp.png | Bin 0 -> 487 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 485 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 323 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 320 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 640 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 655 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 940 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 953 bytes .../ic_info_outline_black_24dp.png | Bin 0 -> 1256 bytes .../ic_info_outline_white_24dp.png | Bin 0 -> 1279 bytes app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 3 +- app/src/main/res/values/styles.xml | 2 + app/src/main/res/xml/video_audio_settings.xml | 6 - 24 files changed, 477 insertions(+), 544 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png diff --git a/app/build.gradle b/app/build.gradle index 273616f91..efc3b69f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:4fb49d54b5' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:e51bc58a856dcf3' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc3dc62e6..e15d9abf8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -122,8 +122,12 @@ + + @@ -169,6 +173,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -176,17 +215,8 @@ - - - - - - - - - - + @@ -195,68 +225,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 8aaa248dd..c32b924b0 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -1,51 +1,80 @@ package org.schabi.newpipe; +import android.app.IntentService; +import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; +import android.os.PersistableBundle; +import android.preference.PreferenceManager; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.Toast; +import org.schabi.newpipe.extractor.Info; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.StreamingService.LinkType; +import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.playlist.ChannelPlayQueue; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlaylistPlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ThemeHelper; +import java.io.Serializable; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import icepick.Icepick; import icepick.State; import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; -/* - * Copyright (C) Christian Schabesberger 2017 - * RouterActivity.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 . - */ +import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; /** - * This Acitivty is designed to route share/open intents to the specified service, and - * to the part of the service which can handle the url. + * Get the url from the intent and open it in the chosen preferred player */ public class RouterActivity extends AppCompatActivity { @State + protected int currentServiceId = -1; + private StreamingService currentService; + @State + protected LinkType currentLinkType; + @State + protected int selectedRadioPosition = -1; + protected int selectedPreviously = -1; + protected String currentUrl; protected CompositeDisposable disposables = new CompositeDisposable(); @@ -62,6 +91,10 @@ public class RouterActivity extends AppCompatActivity { finish(); } } + + setTheme(ThemeHelper.isLightThemeSelected(this) + ? R.style.RouterActivityThemeLight + : R.style.RouterActivityThemeDark); } @Override @@ -73,25 +106,43 @@ public class RouterActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); + handleUrl(currentUrl); } - protected void handleUrl(String url) { - disposables.add(Observable - .fromCallable(() -> NavigationHelper.getIntentByLink(this, url)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(intent -> { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); + @Override + protected void onDestroy() { + super.onDestroy(); - finish(); - }, this::handleError) - ); + disposables.clear(); } - protected void handleError(Throwable error) { + private void handleUrl(String url) { + disposables.add(Observable + .fromCallable(() -> { + if (currentServiceId == -1) { + currentService = NewPipe.getServiceByUrl(url); + currentServiceId = currentService.getServiceId(); + currentLinkType = currentService.getLinkTypeByUrl(url); + currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); + } else { + currentService = NewPipe.getService(currentServiceId); + } + + return currentLinkType != LinkType.NONE; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result) { + onSuccess(); + } else { + onError(); + } + }, this::handleError)); + } + + private void handleError(Throwable error) { error.printStackTrace(); if (error instanceof ExtractionException) { @@ -103,11 +154,339 @@ public class RouterActivity extends AppCompatActivity { finish(); } - @Override - protected void onDestroy() { - super.onDestroy(); + private void onError() { + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + finish(); + } - disposables.clear(); + protected void onSuccess() { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + + if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { + Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) + if (currentService == ServiceList.SoundCloud) { + handleChoice(getString(R.string.background_player_key)); + return; + } + + final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); + final String alwaysAskKey = getString(R.string.always_ask_player_key); + + if (playerChoiceKey.equals(alwaysAskKey)) { + showDialog(); + } else { + handleChoice(playerChoiceKey); + } + } + + private void showDialog() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, + ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); + + LayoutInflater inflater = LayoutInflater.from(themeWrapper); + final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); + final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); + + final AdapterChoiceItem[] choices = { + new AdapterChoiceItem(getString(R.string.info_screen_key), getString(R.string.show_info), + resolveResourceIdFromAttr(themeWrapper, R.attr.info)), + new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.play)), + new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), + new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) + }; + + final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { + final int indexOfChild = radioGroup.indexOfChild( + radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); + final AdapterChoiceItem choice = choices[indexOfChild]; + + handleChoice(choice.key); + + if (which == DialogInterface.BUTTON_POSITIVE) { + preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); + } + }; + + final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) + .setTitle(R.string.preferred_player_share_menu_title) + .setView(radioGroup) + .setCancelable(true) + .setNegativeButton(R.string.just_once, dialogButtonsClickListener) + .setPositiveButton(R.string.always, dialogButtonsClickListener) + .setOnDismissListener((dialog) -> finish()) + .create(); + + alertDialog.setOnShowListener(dialog -> { + setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); + }); + + radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); + final View.OnClickListener radioButtonsClickListener = v -> { + final int indexOfChild = radioGroup.indexOfChild(v); + if (indexOfChild == -1) return; + + selectedPreviously = selectedRadioPosition; + selectedRadioPosition = indexOfChild; + + if (selectedPreviously == selectedRadioPosition) { + handleChoice(choices[selectedRadioPosition].key); + } + }; + + int id = 12345; + for (AdapterChoiceItem item : choices) { + final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + radioButton.setText(item.description); + radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); + radioButton.setChecked(false); + radioButton.setId(id++); + radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + radioButton.setOnClickListener(radioButtonsClickListener); + radioGroup.addView(radioButton); + } + + if (selectedRadioPosition == -1) { + final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); + if (!TextUtils.isEmpty(lastSelectedPlayer)) { + for (int i = 0; i < choices.length; i++) { + AdapterChoiceItem c = choices[i]; + if (lastSelectedPlayer.equals(c.key)) { + selectedRadioPosition = i; + break; + } + } + } + } + + selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); + if (selectedRadioPosition != -1) { + ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); + } + selectedPreviously = selectedRadioPosition; + + alertDialog.show(); + } + + private void setDialogButtonsState(AlertDialog dialog, boolean state) { + final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (negativeButton == null || positiveButton == null) return; + + negativeButton.setEnabled(state); + positiveButton.setEnabled(state); + } + + private void handleChoice(final String playerChoiceKey) { + if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); + } + + if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { + PermissionHelper.showPopupEnablementToast(this); + finish(); + return; + } + + // stop and bypass FetcherService if InfoScreen was selected since + // StreamDetailFragment can fetch data itself + if(playerChoiceKey.equals(getString(R.string.info_screen_key))) { + disposables.add(Observable + .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(intent -> { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + + finish(); + }, this::handleError) + ); + return; + } + + final Intent intent = new Intent(this, FetcherService.class); + intent.putExtra(FetcherService.KEY_CHOICE, + new Choice(currentService.getServiceId(), + currentLinkType, + currentUrl, + playerChoiceKey)); + startService(intent); + + finish(); + } + + private static class AdapterChoiceItem { + final String description, key; + @DrawableRes + final int icon; + + AdapterChoiceItem(String key, String description, int icon) { + this.description = description; + this.key = key; + this.icon = icon; + } + } + + private static class Choice implements Serializable { + final int serviceId; + final String url, playerChoice; + final LinkType linkType; + + Choice(int serviceId, LinkType linkType, String url, String playerChoice) { + this.serviceId = serviceId; + this.linkType = linkType; + this.url = url; + this.playerChoice = playerChoice; + } + + @Override + public String toString() { + return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Service Fetcher + //////////////////////////////////////////////////////////////////////////*/ + + public static class FetcherService extends IntentService { + + private static final int ID = 456; + public static final String KEY_CHOICE = "key_choice"; + private Disposable fetcher; + + public FetcherService() { + super(FetcherService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + startForeground(ID, createNotification().build()); + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + if (intent == null) return; + + final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); + if (!(serializable instanceof Choice)) return; + Choice playerChoice = (Choice) serializable; + handleChoice(playerChoice); + } + + public void handleChoice(Choice choice) { + Single single = null; + UserAction userAction = UserAction.SOMETHING_ELSE; + + switch (choice.linkType) { + case STREAM: + single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_STREAM; + break; + case CHANNEL: + single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_CHANNEL; + break; + case PLAYLIST: + single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_PLAYLIST; + break; + } + + + if (single != null) { + final UserAction finalUserAction = userAction; + final Consumer resultHandler = getResultHandler(choice); + fetcher = single + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + resultHandler.accept(info); + if (fetcher != null) fetcher.dispose(); + }, throwable -> ExtractorHelper.handleGeneralException(this, + choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); + } + } + + public Consumer getResultHandler(Choice choice) { + return info -> { + final String videoPlayerKey = getString(R.string.video_player_key); + final String backgroundPlayerKey = getString(R.string.background_player_key); + final String popupPlayerKey = getString(R.string.popup_player_key); + + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); + + PlayQueue playQueue; + String playerChoice = choice.playerChoice; + + if (info instanceof StreamInfo) { + if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { + NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { + NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { + NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); + + } else { + playQueue = new SinglePlayQueue((StreamInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); + } + } + } + + if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { + playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.playOnBackgroundPlayer(this, playQueue); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.playOnPopupPlayer(this, playQueue); + } + } + }; + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopForeground(true); + if (fetcher != null) fetcher.dispose(); + } + + private NotificationCompat.Builder createNotification() { + return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) + .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); + } } /*////////////////////////////////////////////////////////////////////////// @@ -119,9 +498,9 @@ public class RouterActivity extends AppCompatActivity { * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for * more details. */ - protected final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; + private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; - protected String getUrl(Intent intent) { + private String getUrl(Intent intent) { // first gather data and find service String videoUrl = null; if (intent.getData() != null) { @@ -137,7 +516,7 @@ public class RouterActivity extends AppCompatActivity { return videoUrl; } - protected String removeHeadingGibberish(final String input) { + private String removeHeadingGibberish(final String input) { int start = 0; for (int i = input.indexOf("://") - 1; i >= 0; i--) { if (!input.substring(i, i + 1).matches("\\p{L}")) { @@ -148,7 +527,7 @@ public class RouterActivity extends AppCompatActivity { return input.substring(start, input.length()); } - protected String trim(final String input) { + private String trim(final String input) { if (input == null || input.length() < 1) { return input; } else { @@ -188,5 +567,4 @@ public class RouterActivity extends AppCompatActivity { } return result.toArray(new String[result.size()]); } - } diff --git a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java deleted file mode 100644 index 7196e413d..000000000 --- a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java +++ /dev/null @@ -1,413 +0,0 @@ -package org.schabi.newpipe; - -import android.app.IntentService; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.PersistableBundle; -import android.preference.PreferenceManager; -import android.support.annotation.DrawableRes; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.support.v7.app.AlertDialog; -import android.text.TextUtils; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.Toast; - -import org.schabi.newpipe.extractor.Info; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.StreamingService.LinkType; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.playlist.ChannelPlayQueue; -import org.schabi.newpipe.playlist.PlayQueue; -import org.schabi.newpipe.playlist.PlaylistPlayQueue; -import org.schabi.newpipe.playlist.SinglePlayQueue; -import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ThemeHelper; - -import java.io.Serializable; -import java.util.Arrays; - -import icepick.State; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; - -/** - * Get the url from the intent and open it in the chosen preferred player - */ -public class RouterPlayerActivity extends RouterActivity { - - @State - protected int currentServiceId = -1; - private StreamingService currentService; - @State - protected LinkType currentLinkType; - @State - protected int selectedRadioPosition = -1; - protected int selectedPreviously = -1; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { - super.onCreate(savedInstanceState, persistentState); - setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); - } - - @Override - protected void handleUrl(String url) { - disposables.add(Observable - .fromCallable(() -> { - if (currentServiceId == -1) { - currentService = NewPipe.getServiceByUrl(url); - currentServiceId = currentService.getServiceId(); - currentLinkType = currentService.getLinkTypeByUrl(url); - currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); - } else { - currentService = NewPipe.getService(currentServiceId); - } - - return currentLinkType != LinkType.NONE; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - onSuccess(); - } else { - onError(); - } - }, this::handleError)); - } - - protected void onError() { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); - finish(); - } - - protected void onSuccess() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - - if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { - Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); - finish(); - return; - } - - // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) - if (currentService == ServiceList.SoundCloud.getService()) { - handleChoice(getString(R.string.background_player_key)); - return; - } - - final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); - final String alwaysAskKey = getString(R.string.always_ask_player_key); - - if (playerChoiceKey.equals(alwaysAskKey)) { - showDialog(); - } else { - handleChoice(playerChoiceKey); - } - } - - private void showDialog() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, - ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); - - LayoutInflater inflater = LayoutInflater.from(themeWrapper); - final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); - final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); - - final AdapterChoiceItem[] choices = { - new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.play)), - new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), - new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), - resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) - }; - - final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { - final int indexOfChild = radioGroup.indexOfChild(radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); - final AdapterChoiceItem choice = choices[indexOfChild]; - - handleChoice(choice.key); - - if (which == DialogInterface.BUTTON_POSITIVE) { - preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); - } - }; - - final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) - .setTitle(R.string.preferred_player_share_menu_title) - .setView(radioGroup) - .setCancelable(true) - .setNegativeButton(R.string.just_once, dialogButtonsClickListener) - .setPositiveButton(R.string.always, dialogButtonsClickListener) - .setOnDismissListener((dialog) -> finish()) - .create(); - - alertDialog.setOnShowListener(dialog -> { - setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); - }); - - radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); - final View.OnClickListener radioButtonsClickListener = v -> { - final int indexOfChild = radioGroup.indexOfChild(v); - if (indexOfChild == -1) return; - - selectedPreviously = selectedRadioPosition; - selectedRadioPosition = indexOfChild; - - if (selectedPreviously == selectedRadioPosition) { - handleChoice(choices[selectedRadioPosition].key); - } - }; - - int id = 12345; - for (AdapterChoiceItem item : choices) { - final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); - radioButton.setText(item.description); - radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); - radioButton.setChecked(false); - radioButton.setId(id++); - radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - radioButton.setOnClickListener(radioButtonsClickListener); - radioGroup.addView(radioButton); - } - - if (selectedRadioPosition == -1) { - final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); - if (!TextUtils.isEmpty(lastSelectedPlayer)) { - for (int i = 0; i < choices.length; i++) { - AdapterChoiceItem c = choices[i]; - if (lastSelectedPlayer.equals(c.key)) { - selectedRadioPosition = i; - break; - } - } - } - } - - selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); - if (selectedRadioPosition != -1) { - ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); - } - selectedPreviously = selectedRadioPosition; - - alertDialog.show(); - } - - private void setDialogButtonsState(AlertDialog dialog, boolean state) { - final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); - final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - if (negativeButton == null || positiveButton == null) return; - - negativeButton.setEnabled(state); - positiveButton.setEnabled(state); - } - - private void handleChoice(final String playerChoiceKey) { - if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); - } - - if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { - PermissionHelper.showPopupEnablementToast(this); - finish(); - return; - } - - final Intent intent = new Intent(this, FetcherService.class); - intent.putExtra(FetcherService.KEY_CHOICE, new Choice(currentService.getServiceId(), currentLinkType, currentUrl, playerChoiceKey)); - startService(intent); - - finish(); - } - - private static class AdapterChoiceItem { - final String description, key; - @DrawableRes - final int icon; - - AdapterChoiceItem(String key, String description, int icon) { - this.description = description; - this.key = key; - this.icon = icon; - } - } - - private static class Choice implements Serializable { - final int serviceId; - final String url, playerChoice; - final LinkType linkType; - - Choice(int serviceId, LinkType linkType, String url, String playerChoice) { - this.serviceId = serviceId; - this.linkType = linkType; - this.url = url; - this.playerChoice = playerChoice; - } - - @Override - public String toString() { - return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Service Fetcher - //////////////////////////////////////////////////////////////////////////*/ - - public static class FetcherService extends IntentService { - - private static final int ID = 456; - public static final String KEY_CHOICE = "key_choice"; - private Disposable fetcher; - - public FetcherService() { - super(FetcherService.class.getSimpleName()); - } - - @Override - public void onCreate() { - super.onCreate(); - startForeground(ID, createNotification().build()); - } - - @Override - protected void onHandleIntent(@Nullable Intent intent) { - if (intent == null) return; - - final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); - if (!(serializable instanceof Choice)) return; - Choice playerChoice = (Choice) serializable; - handleChoice(playerChoice); - } - - public void handleChoice(Choice choice) { - Single single = null; - UserAction userAction = UserAction.SOMETHING_ELSE; - - switch (choice.linkType) { - case STREAM: - single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_STREAM; - break; - case CHANNEL: - single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_CHANNEL; - break; - case PLAYLIST: - single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_PLAYLIST; - break; - } - - - if (single != null) { - final UserAction finalUserAction = userAction; - final Consumer resultHandler = getResultHandler(choice); - fetcher = single - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(info -> { - resultHandler.accept(info); - if (fetcher != null) fetcher.dispose(); - }, throwable -> ExtractorHelper.handleGeneralException(this, - choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); - } - } - - public Consumer getResultHandler(Choice choice) { - return info -> { - final String videoPlayerKey = getString(R.string.video_player_key); - final String backgroundPlayerKey = getString(R.string.background_player_key); - final String popupPlayerKey = getString(R.string.popup_player_key); - - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); - - PlayQueue playQueue; - String playerChoice = choice.playerChoice; - - if (info instanceof StreamInfo) { - if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { - NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { - NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { - NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); - - } else { - playQueue = new SinglePlayQueue((StreamInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); - } - } - } - - if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { - playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.playOnBackgroundPlayer(this, playQueue); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.playOnPopupPlayer(this, playQueue); - } - } - }; - } - - @Override - public void onDestroy() { - super.onDestroy(); - stopForeground(true); - if (fetcher != null) fetcher.dispose(); - } - - private NotificationCompat.Builder createNotification() { - return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) - .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 4512e316f..abc150e7d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -47,7 +47,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte // Constants //////////////////////////////////////////////////////////////////////////*/ - private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getId(); + private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getServiceId(); private static final String FALLBACK_CHANNEL_URL = "https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ"; private static final String FALLBACK_CHANNEL_NAME = "Music"; private static final String FALLBACK_KIOSK_ID = "Trending"; 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 4161f96c1..855594503 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -73,7 +73,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { .putString(getString(R.string.main_page_selectd_kiosk_id), kioskId).apply(); String serviceName = ""; try { - serviceName = NewPipe.getService(service_id).getServiceInfo().name; + serviceName = NewPipe.getService(service_id).getServiceInfo().getName(); } catch (ExtractionException e) { onError(e); } @@ -245,7 +245,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { String summary = String.format(getString(R.string.service_kiosk_string), - service.getServiceInfo().name, + service.getServiceInfo().getName(), kioskName); mainPagePref.setSummary(summary); diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index 167b6f31b..5ab1ed1f2 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -122,11 +122,11 @@ public class SelectKioskFragment extends DialogFragment { for(StreamingService service : NewPipe.getServices()) { //TODO: Multi-service support - if (service.getServiceId() != ServiceList.YouTube.getId()) continue; + if (service.getServiceId() != ServiceList.YouTube.getServiceId()) continue; for(String kioskId : service.getKioskList().getAvailableKiosks()) { String name = String.format(getString(R.string.service_kiosk_string), - service.getServiceInfo().name, + service.getServiceInfo().getName(), KioskTranslator.getTranslatedKioskName(kioskId, getContext())); kioskList.add(new Entry( ServiceHelper.getIcon(service.getServiceId()), diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index ce1491ba4..55c6e68f2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -12,7 +12,7 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; public class ServiceHelper { - private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube.getService(); + private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; @DrawableRes public static int getIcon(int serviceId) { @@ -45,9 +45,9 @@ public class ServiceHelper { public static void setSelectedServiceId(Context context, int serviceId) { String serviceName; try { - serviceName = NewPipe.getService(serviceId).getServiceInfo().name; + serviceName = NewPipe.getService(serviceId).getServiceInfo().getName(); } catch (ExtractionException e) { - serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().name; + serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName(); } setSelectedServicePreferences(context, serviceName); @@ -55,7 +55,7 @@ public class ServiceHelper { public static void setSelectedServiceId(Context context, String serviceName) { int serviceId = NewPipe.getIdOfService(serviceName); - if (serviceId == -1) serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().name; + if (serviceId == -1) serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName(); setSelectedServicePreferences(context, serviceName); } diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index b0e00465a..824ac4a9d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -73,7 +73,7 @@ public class ThemeHelper { else if (selectedTheme.equals(blackTheme)) themeName = "BlackTheme"; else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme"; - themeName += "." + service.getServiceInfo().name; + themeName += "." + service.getServiceInfo().getName(); int resourceId = context.getResources().getIdentifier(themeName, "style", context.getPackageName()); if (resourceId > 0) { diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5ab06e19d61515cf3a7395db43a81bb30a8203 GIT binary patch literal 487 zcmVS-P=?%O3jv@aJa6HGW<0ceyC!`rEoc~?j{U=`ic|b z9N*QPsMv*+MQv@1i8kR%)UIh1`=XoCqqIYBOKErvea{GWZ!y>;D~RpT`=%tUi9cM8 zjMJd-5IZXpx*;AQpQG66NVp4e3Hh{Q=T<@);yl@>u@F125nkF002ovPDHLkV1kFY-~<2w literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c7b1113cfef22bcec86ead7ae67be12326276cab GIT binary patch literal 485 zcmVW+#V#f>U=9~3v zy5k#|%`o-bRp5gvNWBjKU}@RX_o(#cOX<;tO*2A$>f4JBeW-BBCp2Wv4dAi~>H>ZU-{cI? zse=}k;97{XqEJ|gdQ)z}NLv+h2nN-m6riYd=e>Y~D%A1;c7!A-*ac%VF|-AqWJ2?3 zQF6~&s5ci5fY}sT-yed|rRg0)~VyRe(RxP5| ziKSu?+iDTDUGbuL5L2~?+LgFdoQk=|C}Pxj7RQRtyXcJ~MwepHsvL@iJ?s4SCH8$Y z6AQgYt#Ro~-1uQ)CSvZwj!tLSm1hyxCe}LeCL$stVz6(6)`@$smX==JIoA4R{{X3C VfQx`Hg^K_H002ovPDHLkV1oM>kN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..353e064951788a29a64eef439284fd97e904f374 GIT binary patch literal 320 zcmV-G0l)rT_h)IX%8`!xx#?O($dW=BI;pgAmAMoJ&FVmpak^_M&10n8om%>Hp0CB z>RbH6BfRjxN9M~)&M4Ia$ETJBLQ^<<<3g$He5hLhidW{F&l zq78;P`5|=ls>YxRGDkGO4)Ha@UK+s{*1}DYDDe@&3c1kJ7>fuhJobWT>>{`#?O7?v zaJd6ic&m6Kxyn*Ci7xy1O7D6yV<~Ad#ZoSbb%oCVPWUuVr%YQDrkfL?gZZ)JBOfu SH&bZ<0000o#88fEO5na|OlO{3G z*yIPEVO{WsIjnKnVT>2DMzkql!ApMPMO=L<#3I9oI}LubOO+CNvScYxqQ)LKcQkDr zB9h}Xp02N~GG)+JIYyNnkc)PDZOnzu*#; zB8R%~1(i4Rl0>e>Eb#WLtxfOV(#vvo;8MrfX8# zOJwj-6bRC^RW#U^^>`Uk%GqUlp=9>%y-#&$qZ(w=qV2=#%SNueJg~}R_Q4VL}5`+@9~aM z91c`Iph*TZkfp^?^`Dp^0y)$Ij}vN48)8+LhbiruA|j3sO47|fb;`I*GKou>Ivq)^ z4u=V1QQ;ad;<{u73-W9;3T-}Qiy5qW$_D4g=4Y%kOJdS2(WFC22NX4g$Gh;r2I3UU zC}4AjSPu(ixIwI-i~@=>;)?%*fd#|^BPhd-@r0NU2I`1i6z1DU)PjK~qBKyTf@lN- zCy2s8fg++640I7Wl<`GYIEcGoz(-^U3JfE>zYypm@_+0U8C+z9*+p(5%4UK1zv(Js zZ=k^ch-xrUL_9KT9C*VMq7V#ZxJHzW0~N$&*uzfo7172v3S>D)tf9gJQ}~FgQJ{|K z@irKcxri5LQU~VfAvRG_fjnJAhY3`QO>%*_Wjs2dSVB8ak(i`~wix%~<)U?%MJ3oA z7ie3kxPZkW+6y&o6w%94r-ydHP_ma_Nr*Gbj78j570v p!{d$?4XPC6k!Bt+*lWOGzX87wDYF(rDzN|n002ovPDHLkV1ir08p{9x literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3847a9fe74774d3593dd694cff48ccca13cdec5a GIT binary patch literal 940 zcmV;d15^BoP)mdcfcOtJM6uO{+z8D@UxkKMBq1+Buz@6?P#P0jG1VJo zr4$sRhK5k2Z=fXRN~6nW@y^{1KD~XI=}gYdoFjX&pWp3sm>a@9yM-m0@&}6isAEB(Qj;>qRst`p9~=6q zAu7LO=u+TQ_K7Dd@dg8*_ZUX>5!&WBSSti4^<9Bi7`TY2+y<+KU=~psWnX%OzQeO# z$Y%%reeR=-{wJ-1^+Iq9eT6F#>jwG=^_&zcppV__#9BjN7F^M%z)NCy=&SoFTsf2ppC<<**2_ZI5@SJo9Db@pv9Dwel>@Q2Y|A^pR+ zXm)h0I4$&8t(hXy6*cz3S)t(J`aO{jqc{F(SB1>4AMjd@E;uWs|KhZ2{;};ky2AQ2 z)rxDxD$`NJH=PukQuA$!?4zc}-&3$&Xn>Du{t5dULsafqFEod!oWp?rpGSuiRtueB z&+LQNE+8tK3|cF6gtuz;3+FNP08y#Yj|~U-15sJR*io8@$}eQs#}cB_phN-_NT5>I zKj|$a=v*bC1q7WMgQRsN@Unzi63g)%L1&Ybq;ZC~2s(dqkQ990An0_M#V3gY=Ga5f z`5TXvN})=^4!0;^!e^R~NT^Pc418W7q3m*(v-Aa=K4XNz?fnc_RXp{J*(OZ^8ac7(2N>$@cY O0000+=u1!Si^=c~o6pU56D8MW!jS z$u@O7JnC$-Nr`E0Z~~QViq1F4UDojk)8rj4KiUtv${KCrYEfp?UeGn(lf*h#%mwiZO?K>;SqC6p{w@`EooS#EOw+c}UBbluHuSO9kD3{Dexd zGgQ&NCewM)0=iXlsD$fh4c%;~LATKTP7f-@j@01>+c-g20r3E3BuF*`h>_@^ zHN+KB7e-Jm@Z>j$y%;(Bn$f*7_b_YN7RCYJVeH7P%ol+c!LiAkL*xA)X9%M!t}A%e=*t&auG}M zUL7k|gPtKKgM)4$K3fdZU#`Cd2c5t}oVOJe{qaNg4l!>nXc19Cg$22YJsc}RIU0!D zQ6KVKhxdR-j0Rnxtv}Fl zg03K-O24t7lYB&c=W=+E>@fn$hQ5wD$_oT6qv8ggqJe-vjQr`djDR|Y=pflR=t-%+ z(kr36+3A;_^XSU-C(b#^3v@H6PJ=SMLRaN%qMQqSME8wcL$0HkK4xj7 z`v)iS)q*bxcDYYB<~wVOFW}3`r@4H|@fyE97P!cfkP~%@MH=|M#7TQSm0^y9)p&}_ z7{lc0-Cv$z#&CH?jf4HhF*6r<<1|RC&P|h7oP+#Da^G2|VEz(#l(!^NVZ`1w`3S4D zh|A+Sm+k)}L4iBGp-Gr;RJbh%++L9EIM5D%liv*A40h bb%Xu~a~|hNXxfgr00000NkvXXu0mjfV)nVh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c1e2a03a4bc540419ce1165d5dedd8d5345fdec2 GIT binary patch literal 1256 zcmVP)W$LQv#I8j8T_{yf~=m$~;IXzu5?eh%l+bI*O5LnIQ3L?S-g zNEKIT<0XBJGshx}%rQL6n2^)65BytPpur(aAq3mzkx5L*!vp z0a@Ola>x)hq%a~$J)clM5drmH23z#q<|xU<0`*qKPGo7Si~n z%wqBU&0hSlj{%E?RDuuU7MJcXB=Jj%b`&x_e(Xp;ExUAT@Y|(jm(qjWZ(B;_Uor&Y z6!R#Nv*chaGsx+GPf(5^TbQOC&z>U}Plh0!MlPf-JUW9CsUc{WQ6klNkmN0L=W$Hb zI`=E``6_{H*O2>uGL!}iknh%)aH)U^vHJHyC=I|j;!5~HKCWcN1R5Z;2B2BgU%-W! zczBp#L#PcDi)Yo>;Taa!Y@Ptv;<}7l4r)8NeQGlwNF& zYb9ky8`z|cGKQ@=tt4)=frR*F&DNq;5;NLBOdDlk)i+CZzQKZh$R zK8n3)UuX^N7uDk=aj8xWGC(Sn1_~Jx)mdCiFf4{?52b-lQTv8C9#o@5E)leAD3OzR z)Fn^Gaf%@QAdedG1kbj}b8XCXgrFSb3rcL@n*sbhFEP*Qfd2Kr_;5}_cz_aF;&K3M ze+ebhM9$XHgOZ`06n-hB6D318YwImEMS(?`<%O>vfHEvXL&vNpLe#!|zDLp9b&_k9T z*x5;~c-Pf0-68_APCn*}yr+}vWZ1(d62yp+WD|SHaD&H;+SWI+mO!0l8Wm6`sU`&F zbfE&s6E+i?YKBohW{8u7EWu?ypnS+US>hOx&jq=;y2>EuNMckT2YAG1l*`QUkZ)PT z3X$L_jr@gjkUknXOq>;|goD)5%rkl!W0nOLm}QJ!o^qco2Ph^Ii9{k1-~0zqnGDmx SqDLYC0000 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3a82cab3b4f2cfe77333cee6121d867353753d5e GIT binary patch literal 1279 zcmVXA;DPbB};p_0dfAo z$vAy9#+6S9#ELYJ@(Lqa#%C_802gIqArYp#f(wN zJ5D-2x}I^0EQMtFA?SeRyu{vHx_o|^)~`kO7MeJbsRD=Aw#N4d<{5Bs2vT z(7+gW9I~nSN2#E6lJC{kK0$L^4urow?@d3Y+FGmL|;O)mE-^F4Bc9HL2 zQm7ENoloT3SxmqprfnYoS5!#bXp=BSqyK>0=yIe`A#Mj2x&gld?d0n~g}ejgYx5J3 z2Egy^UyUJ3s{D( z);XXRX&hy6FOb$b2OLD|GaAs3)ae{>38~9yz)_?g=YYFNn~esfkZw5#3?r>T`I=7o z+A5@n&H)oh4MqdzBfWAC_=prY8jwJmaSq5K#f%2TkiLBX0pHn2D2X)lKlmdaMq2s3 zeU08h+F~>yg>=(7pcm8C~mULv(P2h^i$HWskl&kK!W z1ZkhKfKETJl!_jtL1O{;k-FUlY(=tYHWIKD-3E67NoJ5v83{OpG)>%HfT9m6OI73n zbC^cDf^r+M9^H}115Tj(#eF~-qezp~L>f@f6jFv+?gJDZ=*~qN(2H(2%1b~cZ;&k3 zL>90X-2_Q*0g5hkkEx6#pqf#1X_VK11mozgMH0}DZj8A107V=5_M<}HVe)PBd(>b6 zoyB%ki2Dr--3^rAfQ4kyePSajq;2Lix=9xJ4^Xs&O~Q_l{`EhTudUbvPZ!B&aUg`x zzD2%ssA2=k8N$vrDp3KgnttpIGQ0QyMI9OJJZ23lfURW|J0sKtb?mZ<=h(66p%zud z>bZoyXEX+R@O5c{1jurnYLt)7;RI9Id&-iaj~+Mi1PVr+=0}fjTW6S}kP#ZgIQ(7D z5MMJynq@Bjr=gSke9a&=VI99OrxzzL=;tUYRxzIhF=8Z{&ni+J<2o;Ka+cZQUVzxi z6k(a9Es85BjSLWm8!U+K5>OlC1Zk9QQD4JKaDdkYVS+SqGZ*11*vk`&kzqGU(^nB& zxXOFHn&t{WGwWNq44PmQXZZ&=hUsR5_|TXQ5UXPyo%C>vhrD8jFMMH!S3Kk<=SkB- pEh@+ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..8f33f9297 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -155,6 +155,7 @@ @string/always_ask_player_key preferred_player_last_selected + info_screen video_player background_player popup_player diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea8f0fce8..caf792186 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Channel unsubscribed Unable to change subscription Unable to update subscription + Show info Main Subscriptions @@ -368,7 +369,7 @@ - @string/preferred_player_settings_title + NewPipe Open with preferred player Preferred player diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b16958ae6..dcf8f9268 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,6 +18,7 @@ @drawable/ic_thumb_up_black_24dp @drawable/ic_thumb_down_black_24dp + @drawable/ic_info_outline_black_24dp @drawable/ic_headset_black_24dp @drawable/ic_delete_sweep_white_24dp @drawable/ic_file_download_black_24dp @@ -69,6 +70,7 @@ @drawable/ic_thumb_up_white_24dp @drawable/ic_thumb_down_white_24dp @drawable/ic_headset_white_24dp + @drawable/ic_info_outline_white_24dp @drawable/ic_delete_sweep_black_24dp @drawable/ic_file_download_white_24dp @drawable/ic_share_white_24dp diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index ceacfb142..b32bff6f8 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -74,12 +74,6 @@ android:layout="@layout/settings_category_header_layout" android:title="@string/settings_category_player_behavior_title"> - - Date: Wed, 14 Feb 2018 19:33:43 +0100 Subject: [PATCH 091/276] fixed preferred_player inconsistancy --- .../org/schabi/newpipe/RouterActivity.java | 26 +++++++++++-------- app/src/main/res/values/settings_keys.xml | 22 +++++++++------- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/video_audio_settings.xml | 8 +++--- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index c32b924b0..ad79c40b4 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -5,7 +5,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.os.PersistableBundle; import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; @@ -13,7 +12,6 @@ import android.support.v4.app.NotificationCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; -import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; @@ -176,8 +174,10 @@ public class RouterActivity extends AppCompatActivity { return; } - final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); - final String alwaysAskKey = getString(R.string.always_ask_player_key); + final String playerChoiceKey = preferences.getString( + getString(R.string.preferred_open_action_key), + getString(R.string.preferred_open_action_default)); + final String alwaysAskKey = getString(R.string.always_ask_open_action_key); if (playerChoiceKey.equals(alwaysAskKey)) { showDialog(); @@ -196,7 +196,7 @@ public class RouterActivity extends AppCompatActivity { final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); final AdapterChoiceItem[] choices = { - new AdapterChoiceItem(getString(R.string.info_screen_key), getString(R.string.show_info), + new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info), resolveResourceIdFromAttr(themeWrapper, R.attr.info)), new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), resolveResourceIdFromAttr(themeWrapper, R.attr.play)), @@ -214,7 +214,7 @@ public class RouterActivity extends AppCompatActivity { handleChoice(choice.key); if (which == DialogInterface.BUTTON_POSITIVE) { - preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); + preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply(); } }; @@ -257,7 +257,7 @@ public class RouterActivity extends AppCompatActivity { } if (selectedRadioPosition == -1) { - final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null); + final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null); if (!TextUtils.isEmpty(lastSelectedPlayer)) { for (int i = 0; i < choices.length; i++) { AdapterChoiceItem c = choices[i]; @@ -288,12 +288,16 @@ public class RouterActivity extends AppCompatActivity { } private void handleChoice(final String playerChoiceKey) { - if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { + if (Arrays.asList(getResources() + .getStringArray(R.array.preferred_open_action_values_list)) + .contains(playerChoiceKey)) { PreferenceManager.getDefaultSharedPreferences(this).edit() - .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); + .putString(getString(R.string.preferred_open_action_last_selected_key), + playerChoiceKey).apply(); } - if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { + if (playerChoiceKey.equals(getString(R.string.popup_player_key)) + && !PermissionHelper.isPopupEnabled(this)) { PermissionHelper.showPopupEnablementToast(this); finish(); return; @@ -301,7 +305,7 @@ public class RouterActivity extends AppCompatActivity { // stop and bypass FetcherService if InfoScreen was selected since // StreamDetailFragment can fetch data itself - if(playerChoiceKey.equals(getString(R.string.info_screen_key))) { + if(playerChoiceKey.equals(getString(R.string.show_info_key))) { disposables.add(Observable .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 8f33f9297..1ac8e68cd 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -150,28 +150,30 @@ @string/charset_most_special_characters_value - - preferred_player_key - @string/always_ask_player_key - preferred_player_last_selected + + preferred_open_action_key + @string/always_ask_open_action_key + preferred_open_action_last_selected - info_screen + show_info video_player background_player popup_player - always_ask_player + always_ask_player - + + @string/show_info @string/video_player @string/background_player @string/popup_player - @string/always_ask_player + @string/always_ask_open_action - + + @string/show_info_key @string/video_player_key @string/background_player_key @string/popup_player_key - @string/always_ask_player_key + @string/always_ask_open_action_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index caf792186..23453c89d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -376,7 +376,7 @@ Video player Background player Popup player - Always ask + Always ask Getting info… "The requested content is loading" diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index b32bff6f8..7551834a2 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -75,10 +75,10 @@ android:title="@string/settings_category_player_behavior_title"> From 42a2bc8a9a8aa5e6368d31e123271c53c234535e Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 16 Feb 2018 11:31:25 +0100 Subject: [PATCH 092/276] clean DetailFragment code --- .../fragments/detail/VideoDetailFragment.java | 220 ++++++++++++------ 1 file changed, 151 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 206f6edd8..a4c1bacbf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -95,7 +95,12 @@ import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class VideoDetailFragment extends BaseStateFragment implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener, View.OnLongClickListener { +public class VideoDetailFragment + extends BaseStateFragment + implements BackPressable, + SharedPreferences.OnSharedPreferenceChangeListener, + View.OnClickListener, + View.OnLongClickListener { public static final String AUTO_PLAY = "auto_play"; // Amount of videos to show on start @@ -186,8 +191,10 @@ public class VideoDetailFragment extends BaseStateFragment implement super.onCreate(savedInstanceState); setHasOptionsMenu(true); - showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_next_video_key), true); - PreferenceManager.getDefaultSharedPreferences(activity).registerOnSharedPreferenceChangeListener(this); + showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(getString(R.string.show_next_video_key), true); + PreferenceManager.getDefaultSharedPreferences(activity) + .registerOnSharedPreferenceChangeListener(this); } @Override @@ -211,7 +218,10 @@ public class VideoDetailFragment extends BaseStateFragment implement if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBarHandler(currentInfo); } - if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 && actionBarHandler != null) actionBarHandler.updateItemsVisibility(); + if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 + && actionBarHandler != null) { + actionBarHandler.updateItemsVisibility(); + } updateFlags = 0; } @@ -224,7 +234,8 @@ public class VideoDetailFragment extends BaseStateFragment implement @Override public void onDestroy() { super.onDestroy(); - PreferenceManager.getDefaultSharedPreferences(activity).unregisterOnSharedPreferenceChangeListener(this); + PreferenceManager.getDefaultSharedPreferences(activity) + .unregisterOnSharedPreferenceChangeListener(this); if (currentWorker != null) currentWorker.dispose(); if (disposables != null) disposables.clear(); @@ -285,7 +296,8 @@ public class VideoDetailFragment extends BaseStateFragment implement // Check if the next video label and video is visible, // if it is, include the two elements in the next check int nextCount = currentInfo != null && currentInfo.getNextVideo() != null ? 2 : 0; - if (relatedStreamsView != null && relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) { + if (relatedStreamsView != null + && relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) { outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true); } @@ -417,8 +429,10 @@ public class VideoDetailFragment extends BaseStateFragment implement int initialCount = INITIAL_RELATED_VIDEOS + nextCount; if (relatedStreamsView.getChildCount() > initialCount) { - relatedStreamsView.removeViews(initialCount, relatedStreamsView.getChildCount() - (initialCount)); - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); + relatedStreamsView.removeViews(initialCount, + relatedStreamsView.getChildCount() - (initialCount)); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable( + activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); return; } @@ -428,7 +442,9 @@ public class VideoDetailFragment extends BaseStateFragment implement //Log.d(TAG, "i = " + i); relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item)); } - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse))); + relatedStreamExpandButton.setImageDrawable( + ContextCompat.getDrawable(activity, + ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse))); } /*////////////////////////////////////////////////////////////////////////// @@ -547,7 +563,10 @@ public class VideoDetailFragment extends BaseStateFragment implement private View.OnTouchListener getOnControlsTouchListener() { return (View view, MotionEvent motionEvent) -> { - if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false; + if (!PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(getString(R.string.show_hold_to_append_key), true)) { + return false; + } if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { animateView(appendControlsDetail, true, 250, 0, () -> @@ -560,14 +579,25 @@ public class VideoDetailFragment extends BaseStateFragment implement private void initThumbnailViews(StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); if (!TextUtils.isEmpty(info.getThumbnailUrl())) { - imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView, DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() { + imageLoader.displayImage( + info.getThumbnailUrl(), + thumbnailImageView, + DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - ErrorActivity.reportError(activity, failReason.getCause(), null, activity.findViewById(android.R.id.content), ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, NewPipe.getNameOfService(currentInfo.getServiceId()), imageUri, R.string.could_not_load_thumbnails)); + ErrorActivity.reportError( + activity, + failReason.getCause(), + null, + activity.findViewById(android.R.id.content), + ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, + NewPipe.getNameOfService(currentInfo.getServiceId()), + imageUri, + R.string.could_not_load_thumbnails)); } }); } - + if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS); } @@ -578,14 +608,17 @@ public class VideoDetailFragment extends BaseStateFragment implement if (info.getNextVideo() != null && showRelatedStreams) { nextStreamTitle.setVisibility(View.VISIBLE); - relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, info.getNextVideo())); + relatedStreamsView.addView( + infoItemBuilder.buildView(relatedStreamsView, info.getNextVideo())); relatedStreamsView.addView(getSeparatorView()); relatedStreamRootLayout.setVisibility(View.VISIBLE); } else nextStreamTitle.setVisibility(View.GONE); if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) { //long first = System.nanoTime(), each; - int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS ? INITIAL_RELATED_VIDEOS : info.getRelatedStreams().size(); + int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS + ? INITIAL_RELATED_VIDEOS + : info.getRelatedStreams().size(); for (int i = 0; i < to; i++) { InfoItem item = info.getRelatedStreams().get(i); //each = System.nanoTime(); @@ -597,7 +630,8 @@ public class VideoDetailFragment extends BaseStateFragment implement relatedStreamRootLayout.setVisibility(View.VISIBLE); relatedStreamExpandButton.setVisibility(View.VISIBLE); - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable( + activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); } else { if (info.getNextVideo() == null) relatedStreamRootLayout.setVisibility(View.GONE); relatedStreamExpandButton.setVisibility(View.GONE); @@ -620,7 +654,8 @@ public class VideoDetailFragment extends BaseStateFragment implement @Override public boolean onOptionsItemSelected(MenuItem item) { - return (!isLoading.get() && actionBarHandler.onItemSelected(item)) || super.onOptionsItemSelected(item); + return (!isLoading.get() && actionBarHandler.onItemSelected(item)) + || super.onOptionsItemSelected(item); } private static void showInstallKoreDialog(final Context context) { @@ -634,7 +669,8 @@ public class VideoDetailFragment extends BaseStateFragment implement private void setupActionBarHandler(final StreamInfo info) { if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); - sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false)); + sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList( + activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false)); actionBarHandler.setupStreamList(sortedStreamVideosList, spinnerToolbar); actionBarHandler.setOnShareListener(selectedStreamId -> shareUrl(info.name, info.url)); @@ -685,7 +721,10 @@ public class VideoDetailFragment extends BaseStateFragment implement public void setTitleToUrl(int serviceId, String videoUrl, String name) { if (name != null && !name.isEmpty()) { for (StackItem stackItem : stack) { - if (stack.peek().getServiceId() == serviceId && stackItem.getUrl().equals(videoUrl)) stackItem.setTitle(name); + if (stack.peek().getServiceId() == serviceId + && stackItem.getUrl().equals(videoUrl)) { + stackItem.setTitle(name); + } } } } @@ -727,18 +766,18 @@ public class VideoDetailFragment extends BaseStateFragment implement pushToStack(serviceId, url, name); showLoading(); - Log.d(TAG, "prepareAndHandleInfo() called parallaxScrollRootView.getScrollY(): " + parallaxScrollRootView.getScrollY()); + Log.d(TAG, "prepareAndHandleInfo() called parallaxScrollRootView.getScrollY(): " + + parallaxScrollRootView.getScrollY()); final boolean greaterThanThreshold = parallaxScrollRootView.getScrollY() > (int) (getResources().getDisplayMetrics().heightPixels * .1f); if (scrollToTop) parallaxScrollRootView.smoothScrollTo(0, 0); - animateView(contentRootLayoutHiding, false, greaterThanThreshold ? 250 : 0, 0, new Runnable() { - @Override - public void run() { - handleResult(info); - showContentWithAnimation(120, 0, .01f); - } - }); + animateView(contentRootLayoutHiding, + false, + greaterThanThreshold ? 250 : 0, 0, () -> { + handleResult(info); + showContentWithAnimation(120, 0, .01f); + }); } protected void prepareAndLoadInfo() { @@ -773,7 +812,8 @@ public class VideoDetailFragment extends BaseStateFragment implement //////////////////////////////////////////////////////////////////////////*/ private void openBackgroundPlayer(final boolean append) { - AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); + AudioStream audioStream = currentInfo.getAudioStreams() + .get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); @@ -781,7 +821,10 @@ public class VideoDetailFragment extends BaseStateFragment implement if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { openNormalBackgroundPlayer(append); } else { - NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), audioStream); + NavigationHelper.playOnExternalPlayer(activity, + currentInfo.getName(), + currentInfo.getUploaderName(), + audioStream); } } @@ -806,8 +849,12 @@ public class VideoDetailFragment extends BaseStateFragment implement private void openVideoPlayer() { VideoStream selectedVideoStream = getSelectedVideoStream(); - if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { - NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream); + if (PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { + NavigationHelper.playOnExternalPlayer(activity, + currentInfo.getName(), + currentInfo.getUploaderName(), + selectedVideoStream); } else { openNormalPlayer(selectedVideoStream); } @@ -828,7 +875,10 @@ public class VideoDetailFragment extends BaseStateFragment implement if (!useOldPlayer) { // ExoPlayer final PlayQueue playQueue = new SinglePlayQueue(currentInfo); - mIntent = NavigationHelper.getPlayerIntent(activity, MainVideoPlayer.class, playQueue, getSelectedVideoStream().getResolution()); + mIntent = NavigationHelper.getPlayerIntent(activity, + MainVideoPlayer.class, + playQueue, + getSelectedVideoStream().getResolution()); } else { // Internal Player mIntent = new Intent(activity, PlayVideoActivity.class) @@ -859,28 +909,31 @@ public class VideoDetailFragment extends BaseStateFragment implement disposables.add(Single.just(descriptionHtml) .map((@io.reactivex.annotations.NonNull String description) -> { - Spanned parsedDescription; - if (Build.VERSION.SDK_INT >= 24) { - parsedDescription = Html.fromHtml(description, 0); - } else { - //noinspection deprecation - parsedDescription = Html.fromHtml(description); - } - return parsedDescription; + Spanned parsedDescription; + if (Build.VERSION.SDK_INT >= 24) { + parsedDescription = Html.fromHtml(description, 0); + } else { + //noinspection deprecation + parsedDescription = Html.fromHtml(description); + } + return parsedDescription; }) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { - videoDescriptionView.setText(spanned); - videoDescriptionView.setVisibility(View.VISIBLE); + videoDescriptionView.setText(spanned); + videoDescriptionView.setVisibility(View.VISIBLE); })); } private View getSeparatorView() { View separator = new View(activity); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1); - int m8 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()); - int m5 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()); + LinearLayout.LayoutParams params = + new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1); + int m8 = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()); + int m5 = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()); params.setMargins(m8, m5, m8, m5); separator.setLayoutParams(params); @@ -894,13 +947,20 @@ public class VideoDetailFragment extends BaseStateFragment implement private void setHeightThumbnail() { final DisplayMetrics metrics = getResources().getDisplayMetrics(); boolean isPortrait = metrics.heightPixels > metrics.widthPixels; - int height = isPortrait ? (int) (metrics.widthPixels / (16.0f / 9.0f)) : (int) (metrics.heightPixels / 2f); - thumbnailImageView.setScaleType(isPortrait ? ImageView.ScaleType.CENTER_CROP : ImageView.ScaleType.FIT_CENTER); - thumbnailImageView.setLayoutParams(new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); + int height = isPortrait + ? (int) (metrics.widthPixels / (16.0f / 9.0f)) + : (int) (metrics.heightPixels / 2f); + thumbnailImageView.setScaleType(isPortrait + ? ImageView.ScaleType.CENTER_CROP + : ImageView.ScaleType.FIT_CENTER); + thumbnailImageView.setLayoutParams( + new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); thumbnailImageView.setMinimumHeight(height); } - private void showContentWithAnimation(long duration, long delay, @FloatRange(from = 0.0f, to = 1.0f) float translationPercent) { + private void showContentWithAnimation(long duration, + long delay, + @FloatRange(from = 0.0f, to = 1.0f) float translationPercent) { int translationY = (int) (getResources().getDisplayMetrics().heightPixels * (translationPercent > 0.0f ? translationPercent : .06f)); @@ -908,23 +968,38 @@ public class VideoDetailFragment extends BaseStateFragment implement contentRootLayoutHiding.setAlpha(0f); contentRootLayoutHiding.setTranslationY(translationY); contentRootLayoutHiding.setVisibility(View.VISIBLE); - contentRootLayoutHiding.animate().alpha(1f).translationY(0) - .setStartDelay(delay).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()).start(); + contentRootLayoutHiding.animate() + .alpha(1f) + .translationY(0) + .setStartDelay(delay) + .setDuration(duration) + .setInterpolator(new FastOutSlowInInterpolator()) + .start(); uploaderRootLayout.animate().setListener(null).cancel(); uploaderRootLayout.setAlpha(0f); uploaderRootLayout.setTranslationY(translationY); uploaderRootLayout.setVisibility(View.VISIBLE); - uploaderRootLayout.animate().alpha(1f).translationY(0) - .setStartDelay((long) (duration * .5f) + delay).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()).start(); + uploaderRootLayout.animate() + .alpha(1f) + .translationY(0) + .setStartDelay((long) (duration * .5f) + delay) + .setDuration(duration) + .setInterpolator(new FastOutSlowInInterpolator()) + .start(); if (showRelatedStreams) { relatedStreamRootLayout.animate().setListener(null).cancel(); relatedStreamRootLayout.setAlpha(0f); relatedStreamRootLayout.setTranslationY(translationY); relatedStreamRootLayout.setVisibility(View.VISIBLE); - relatedStreamRootLayout.animate().alpha(1f).translationY(0) - .setStartDelay((long) (duration * .8f) + delay).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()).start(); + relatedStreamRootLayout.animate() + .alpha(1f) + .translationY(0) + .setStartDelay((long) (duration * .8f) + delay) + .setDuration(duration) + .setInterpolator(new FastOutSlowInInterpolator()) + .start(); } } @@ -938,12 +1013,8 @@ public class VideoDetailFragment extends BaseStateFragment implement if (thumbnailImageView == null || activity == null) return; thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, imageResource)); - animateView(thumbnailImageView, false, 0, 0, new Runnable() { - @Override - public void run() { - animateView(thumbnailImageView, true, 500); - } - }); + animateView(thumbnailImageView, false, 0, 0, + () -> animateView(thumbnailImageView, true, 500)); } @Override @@ -1058,7 +1129,11 @@ public class VideoDetailFragment extends BaseStateFragment implement setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName()); if (!info.getErrors().isEmpty()) { - showSnackBarError(info.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(info.getServiceId()), info.getUrl(), 0); + showSnackBarError(info.getErrors(), + UserAction.REQUESTED_STREAM, + NewPipe.getNameOfService(info.getServiceId()), + info.getUrl(), + 0); } if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) { @@ -1090,9 +1165,16 @@ public class VideoDetailFragment extends BaseStateFragment implement } else if (exception instanceof ContentNotAvailableException) { showError(getString(R.string.content_not_available), false); } else { - int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : - exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId); + int errorId = exception instanceof YoutubeStreamExtractor.DecryptException + ? R.string.youtube_signature_decryption_error + : exception instanceof ParsingException + ? R.string.parsing_error + : R.string.general_error; + onUnrecoverableError(exception, + UserAction.REQUESTED_STREAM, + NewPipe.getNameOfService(serviceId), + url, + errorId); } return true; @@ -1100,10 +1182,10 @@ public class VideoDetailFragment extends BaseStateFragment implement public void onBlockedByGemaError() { thumbnailBackgroundButton.setOnClickListener((View v) -> { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(getString(R.string.c3s_url))); - startActivity(intent); + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(getString(R.string.c3s_url))); + startActivity(intent); }); showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); From b12f0490f38c692f3872c8f5f88334e40046de90 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 16 Feb 2018 12:18:15 +0100 Subject: [PATCH 093/276] remove ActionBarHandler --- .../fragments/detail/ActionBarHandler.java | 150 ------------------ .../fragments/detail/VideoDetailFragment.java | 102 +++++++++--- 2 files changed, 79 insertions(+), 173 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java deleted file mode 100644 index d928166ab..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.schabi.newpipe.fragments.detail; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.support.v7.app.AppCompatActivity; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.Spinner; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.util.ListHelper; - -import java.util.List; - -/* - * Created by Christian Schabesberger on 18.08.15. - *

- * Copyright (C) Christian Schabesberger 2015 - * DetailsMenuHandler.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 . - */ - - -@SuppressWarnings("WeakerAccess") -class ActionBarHandler { - private static final String TAG = "ActionBarHandler"; - - private AppCompatActivity activity; - private int selectedVideoStream = -1; - - private SharedPreferences defaultPreferences; - - private Menu menu; - - // Only callbacks are listed here, there are more actions which don't need a callback. - // those are edited directly. Typically VideoDetailFragment will implement those callbacks. - private OnActionListener onShareListener; - private OnActionListener onOpenInBrowserListener; - private OnActionListener onPlayWithKodiListener; - - // Triggered when a stream related action is triggered. - public interface OnActionListener { - void onActionSelected(int selectedStreamId); - } - - public ActionBarHandler(AppCompatActivity activity) { - this.activity = activity; - } - - public void setupStreamList(final List videoStreams, Spinner toolbarSpinner) { - if (activity == null) return; - - selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, videoStreams); - - boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.use_external_video_player_key), false); - toolbarSpinner.setAdapter(new SpinnerToolbarAdapter(activity, videoStreams, isExternalPlayerEnabled)); - toolbarSpinner.setSelection(selectedVideoStream); - toolbarSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - selectedVideoStream = position; - } - - @Override - public void onNothingSelected(AdapterView parent) { - } - }); - - } - - public void setupMenu(Menu menu, MenuInflater inflater) { - this.menu = menu; - - // CAUTION set item properties programmatically otherwise it would not be accepted by - // appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu); - - defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity); - inflater.inflate(R.menu.video_detail_menu, menu); - - updateItemsVisibility(); - } - - public void updateItemsVisibility(){ - showPlayWithKodiAction(defaultPreferences.getBoolean(activity.getString(R.string.show_play_with_kodi_key), false)); - } - - public boolean onItemSelected(MenuItem item) { - int id = item.getItemId(); - switch (id) { - case R.id.menu_item_share: { - if (onShareListener != null) { - onShareListener.onActionSelected(selectedVideoStream); - } - return true; - } - case R.id.menu_item_openInBrowser: { - if (onOpenInBrowserListener != null) { - onOpenInBrowserListener.onActionSelected(selectedVideoStream); - } - return true; - } - case R.id.action_play_with_kodi: - if (onPlayWithKodiListener != null) { - onPlayWithKodiListener.onActionSelected(selectedVideoStream); - } - return true; - default: - Log.e(TAG, "Menu Item not known"); - } - return false; - } - - public int getSelectedVideoStream() { - return selectedVideoStream; - } - - public void setOnShareListener(OnActionListener listener) { - onShareListener = listener; - } - - public void setOnOpenInBrowserListener(OnActionListener listener) { - onOpenInBrowserListener = listener; - } - - public void setOnPlayWithKodiListener(OnActionListener listener) { - onPlayWithKodiListener = listener; - } - - public void showPlayWithKodiAction(boolean visible) { - menu.findItem(R.id.action_play_with_kodi).setVisible(visible); - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index a4c1bacbf..94a2f8ec0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -31,6 +31,7 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; @@ -106,7 +107,6 @@ public class VideoDetailFragment // Amount of videos to show on start private static final int INITIAL_RELATED_VIDEOS = 8; - private ActionBarHandler actionBarHandler; private ArrayList sortedStreamVideosList; private InfoItemBuilder infoItemBuilder = null; @@ -131,9 +131,12 @@ public class VideoDetailFragment private Disposable currentWorker; private CompositeDisposable disposables = new CompositeDisposable(); + private int selectedVideoStream = -1; + /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ + private Menu menu; private Spinner spinnerToolbar; @@ -174,6 +177,7 @@ public class VideoDetailFragment private LinearLayout relatedStreamsView; private ImageButton relatedStreamExpandButton; + /*////////////////////////////////////////////////////////////////////////*/ public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String name) { @@ -215,12 +219,12 @@ public class VideoDetailFragment if (updateFlags != 0) { if (!isLoading.get() && currentInfo != null) { if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentInfo); - if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBarHandler(currentInfo); + if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo); } if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 - && actionBarHandler != null) { - actionBarHandler.updateItemsVisibility(); + && menu != null) { + updateMenuItemVisibility(); } updateFlags = 0; } @@ -357,7 +361,7 @@ public class VideoDetailFragment DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo, sortedStreamVideosList, - actionBarHandler.getSelectedVideoStream()); + selectedVideoStream); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); } catch (Exception e) { Toast.makeText(activity, @@ -499,7 +503,6 @@ public class VideoDetailFragment relatedStreamExpandButton = rootView.findViewById(R.id.detail_related_streams_expand); - actionBarHandler = new ActionBarHandler(activity); infoItemBuilder = new InfoItemBuilder(activity); setHeightThumbnail(); } @@ -644,7 +647,15 @@ public class VideoDetailFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - actionBarHandler.setupMenu(menu, inflater); + this.menu = menu; + + // CAUTION set item properties programmatically otherwise it would not be accepted by + // appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu); + + inflater.inflate(R.menu.video_detail_menu, menu); + + updateMenuItemVisibility(); + ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(true); @@ -652,10 +663,47 @@ public class VideoDetailFragment } } + private void updateMenuItemVisibility() { + + // show kodi if set in settings + menu.findItem(R.id.action_play_with_kodi).setVisible( + PreferenceManager.getDefaultSharedPreferences(activity).getBoolean( + activity.getString(R.string.show_play_with_kodi_key), false)); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { - return (!isLoading.get() && actionBarHandler.onItemSelected(item)) - || super.onOptionsItemSelected(item); + if(isLoading.get()) { + // if is still loading block menu + return true; + } + + int id = item.getItemId(); + switch (id) { + case R.id.menu_item_share: { + if(currentInfo != null) { + shareUrl(currentInfo.name, url); + } else { + shareUrl(url, url); + } + return true; + } + case R.id.menu_item_openInBrowser: { + openUrlInBrowser(url); + return true; + } + case R.id.action_play_with_kodi: + try { + NavigationHelper.playWithKore(activity, Uri.parse( + url.replace("https", "http"))); + } catch (Exception e) { + if(DEBUG) Log.i(TAG, "Failed to start kore", e); + showInstallKoreDialog(activity); + } + return true; + default: + return super.onOptionsItemSelected(item); + } } private static void showInstallKoreDialog(final Context context) { @@ -667,23 +715,31 @@ public class VideoDetailFragment builder.create().show(); } - private void setupActionBarHandler(final StreamInfo info) { + private void setupActionBarOnError(final String url) { + if (DEBUG) Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]"); + Log.e("-----", "missing code"); + } + + private void setupActionBar(final StreamInfo info) { if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList( activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false)); - actionBarHandler.setupStreamList(sortedStreamVideosList, spinnerToolbar); - actionBarHandler.setOnShareListener(selectedStreamId -> shareUrl(info.name, info.url)); - actionBarHandler.setOnOpenInBrowserListener((int selectedStreamId)-> - openUrlInBrowser(info.getUrl())); + selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, sortedStreamVideosList); - actionBarHandler.setOnPlayWithKodiListener((int selectedStreamId) -> { - try { - NavigationHelper.playWithKore(activity, Uri.parse( - info.getUrl().replace("https", "http"))); - } catch (Exception e) { - if(DEBUG) Log.i(TAG, "Failed to start kore", e); - showInstallKoreDialog(activity); + boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(activity.getString(R.string.use_external_video_player_key), false); + spinnerToolbar.setAdapter(new SpinnerToolbarAdapter(activity, sortedStreamVideosList, + isExternalPlayerEnabled)); + spinnerToolbar.setSelection(selectedVideoStream); + spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + selectedVideoStream = position; + } + + @Override + public void onNothingSelected(AdapterView parent) { } }); @@ -899,7 +955,7 @@ public class VideoDetailFragment } private VideoStream getSelectedVideoStream() { - return sortedStreamVideosList.get(actionBarHandler.getSelectedVideoStream()); + return sortedStreamVideosList.get(selectedVideoStream); } private void prepareDescription(final String descriptionHtml) { @@ -1119,7 +1175,7 @@ public class VideoDetailFragment prepareDescription(info.getDescription()); animateView(spinnerToolbar, true, 500); - setupActionBarHandler(info); + setupActionBar(info); initThumbnailViews(info); initRelatedVideos(info); if (wasRelatedStreamsExpanded) { From 00e65153f4ef067c70f42e6f8b5ddbd7a3ab213b Mon Sep 17 00:00:00 2001 From: TobiGr Date: Fri, 16 Feb 2018 13:13:40 +0100 Subject: [PATCH 094/276] Enable SoundCloud kiosks as main page fragment for debug and beta --- .../org/schabi/newpipe/settings/SelectKioskFragment.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index 5ab1ed1f2..00b618889 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -13,6 +13,7 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; @@ -45,6 +46,8 @@ import java.util.Vector; public class SelectKioskFragment extends DialogFragment { + private static final boolean DEBUG = MainActivity.DEBUG; + RecyclerView recyclerView = null; SelectKioskAdapter selectKioskAdapter = null; @@ -122,7 +125,7 @@ public class SelectKioskFragment extends DialogFragment { for(StreamingService service : NewPipe.getServices()) { //TODO: Multi-service support - if (service.getServiceId() != ServiceList.YouTube.getServiceId()) continue; + if (service.getServiceId() != ServiceList.YouTube.getServiceId() && !DEBUG) continue; for(String kioskId : service.getKioskList().getAvailableKiosks()) { String name = String.format(getString(R.string.service_kiosk_string), From 86f041b803792b23be664ae50aa5c97675acaa44 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 16 Feb 2018 14:45:52 +0100 Subject: [PATCH 095/276] add header/footer to drawer --- .../java/org/schabi/newpipe/MainActivity.java | 13 ++++ .../settings/MainSettingsFragment.java | 1 + .../drawable-hdpi/ic_settings_black_24dp.png | Bin 0 -> 453 bytes .../drawable-hdpi/ic_settings_white_24dp.png | Bin 0 -> 460 bytes .../drawable-mdpi/ic_settings_black_24dp.png | Bin 0 -> 322 bytes .../drawable-mdpi/ic_settings_white_24dp.png | Bin 0 -> 326 bytes .../drawable-xhdpi/ic_settings_black_24dp.png | Bin 0 -> 557 bytes .../drawable-xhdpi/ic_settings_white_24dp.png | Bin 0 -> 562 bytes .../ic_settings_black_24dp.png | Bin 0 -> 827 bytes .../ic_settings_white_24dp.png | Bin 0 -> 843 bytes .../ic_settings_black_24dp.png | Bin 0 -> 1073 bytes .../ic_settings_white_24dp.png | Bin 0 -> 1074 bytes app/src/main/res/layout/activity_main.xml | 9 +-- app/src/main/res/layout/drawer_laoyut.xml | 65 ++++++++++++++++++ .../main/res/layout/fragment_video_detail.xml | 8 +-- app/src/main/res/values/attrs.xml | 3 + app/src/main/res/values/styles.xml | 5 ++ 17 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_settings_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_settings_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png create mode 100644 app/src/main/res/layout/drawer_laoyut.xml diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index ea6715f16..cbd2bacaa 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -41,6 +41,8 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; import android.widget.Toast; import org.schabi.newpipe.extractor.StreamingService; @@ -79,6 +81,7 @@ public class MainActivity extends AppCompatActivity { setSupportActionBar(findViewById(R.id.toolbar)); setupDrawer(); + setupDrawerFooter(); } private void setupDrawer() { @@ -123,6 +126,16 @@ public class MainActivity extends AppCompatActivity { } } + private void setupDrawerFooter() { + ImageButton settings = findViewById(R.id.drawer_settings); + ImageButton downloads = findViewById(R.id.drawer_downloads); + ImageButton history = findViewById(R.id.drawer_history); + + settings.setOnClickListener(view -> NavigationHelper.openSettings(this) ); + downloads.setOnClickListener(view -> NavigationHelper.openDownloads(this)); + history.setOnClickListener(view -> NavigationHelper.openHistory(this)); + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 230f3b5ee..728da0ae5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.settings; import android.os.Bundle; +import android.support.v7.preference.Preference; import org.schabi.newpipe.R; diff --git a/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..acf1ddf85b3388b4fb02a683664d7599d96ebfe3 GIT binary patch literal 453 zcmV;$0XqJPP)bdAc8oEppaIOE(LXPbaD_uiXgfuI0y=Ui?$Rg z3N9|rijvq&CiQlchlhJevpDqnjrr%mb8?c{t6qzumxlIL6n!zIh8Gl(U}e3LlM0Ef;V2BeJSjZb=;SQ|z+u z4yaibL-bf>PG6*TZCbM}(t2i;JWOXDi<+RJrJZgbD4Fy&xy-!MTym*5Ce+-mj<=}R zs*bfH=Sq2RG}Riq#p042E*7&*ne}3EUJe(E*_zBou{bA(tHo?vL#bs|A+IvyeHm7t#M^wEtHOK?m*3tk?OFDx*kh6Lst!v$eu1NdssLB&DM32-Q z^h4~jZ>A+~7n2>4r=NU@Dq}tg9ji`Suql$(%vHn+eg}v!7dsOm%)+nCb00000NkvXXu0mjfkE75f literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..97ded33b5558a0efbd7812296a52325b4170076e GIT binary patch literal 460 zcmV;-0W=4>kIV21T<&3Kg>clSSXlU}7 zNsTbZb7CDT8exhj%*e(#KY#)6xy}u?uzckjH(4PAzEPEh1-w0q?BPAtVVvU+$MuUd zim^-?Z*&k19JY)eT?W{4>I$)hC8bSG`$%Zf$FisplYAzY(o}#3ePW-OP>dHiLTyE8 z5z6Vv#(lhunj$1*cz0EVJ9s@A*ucB3BFy1+WndHUmWuEIZ$P35H8Q+=vQXg_LqaV@ zm?z|TsTk9IA=am%09Qzftuv(%OIZ4}NVJa{^Q2f76=H@QTgC>PWY~7OsDpS*8Lt)N zEWbIfpG@m8p5YBC;_%wCP~|(2vdmRx>0#;8WR`dQ1-2NIjXDoGtr048h%M+pCV0dp z9SxU>omUy0Vn}G0s>)%9(6-8A4Tr6%Lb0000J%}fHzV<>iUk{yCv=|P(yFM9CIKrv6;vdj!^e)OQ=oM~3L z3Hov zYMc*TMS6^4Y*s#v8YK}AT(ig&XB4{J=afm7xZ$2C{CQWp)PoCx)G7YcXPY7T1DnU) UWG_v5V*mgE07*qoM6N<$f<$+V!T>O6 zS70QgNAU8v+(?*#6#o9*V@~mgC;x#Rf^DRK!rxsIH1Y+{1W$v50*~CV#54`Q2vW|N zVwqbWh7Wvdgwql&RGT?0V4Y7)9ZgCcW4f$baDbN(6Vt-m zvtW!jvUC`+K-kE98fAa!hNkla9V}f0{{|82kXi@9MRd@?$weK4A^ju7 zLRBcZi6~YV4Y?x0YCBk^ee93}@A5qN9`;=V`Fy9md5-r894+Q7Vwa zKGHbsTJ3CbB>k$}G!3du63TIxO+;yvI}8$@RV#hZwGsJa1 zG(UeLOQIW*%YP$A_S8JsifkZzY=a`Er-+B1U^vMR;-MW*pvdqV@zg66vq)lJDKSEJ zw`0sxb4<_B;v8FuS(#q`)kg&}+hPL6xOrb?GQ?%fExvFHL!Kq|$LU4WPhFXQ&M?@z zxR1mtVOeOIy~lo>;tz)rETgf>fK;}TNLegBvB^KiQ=u3Ot|-Y( z2Uu+mzU3;1m}L#YE?|a1uJMlWb*&qR^Q=+kG@*msHr|$gaEk##XL&}z6#u1a9Hidf vu2=2VHre2#z3N8+Q5wRd6(rV6I@-ivWBUX&LwAlh00000NkvXXu0mjfs0IG% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5caedc8e57497ba9d32c3a1798f7394bd8ca7cff GIT binary patch literal 562 zcmV-20?qx2P)lpy;7r2s24Mgk2IZK^^iBh`hXFsLp$Hi^-XdC|o$nN|w}JYolv9aiaA3)AHJ#jL^+Vnw!iASI;~WiKXmD0RMEHO!?-j&6;kHOKLX;?@ zWGE7DPJUcs54$WqYC|tM?Dm*a5EB$J%My_f5ptOQ;JnO8utv}pJu;w=5<;?v>&>8 zhF?Y&EaUf-F4>V}ouDGUG9XTwpfwUQW0DePIU@2QLIJZP6AEI68g@DQ)P^_(>}pKO zkE?_$vBVfLVvMs)nQ&JW#5%6Ll?i8P;6j~onehS_p38;=e>tq*49Si(?RM$JF6|Z- zhLe1y=@Yj&%{<$LDKkeuxA{!dCfy3hWwyzZQVXYeKoyfe+#{|QF7lc(Gdc+Jm}KP& zE12AuD=c7gL#~j*pF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3023ff8daa925ac79e863caf679d03c56c3afc93 GIT binary patch literal 827 zcmV-B1H}A^P)>`06 zl!7Kix5q+Fq6=w?Bq2zRX%{Q=l9q~JFAfaD-fMk(ql{OMJp*hZ8Le@FKzb36b`fRvPbJZ#ihbmi`-huQ`v`H+dnnz5 zIO;AGB7BE9YM!%LJ&ic*2`mcPKpgfvmoR!03D^-c43Z$hAhR4H0ecmL8#qMtu}v3c zWQvny2hqm?F??uNd0r4z>Rub{-SwiV_KcJlc$@G(Y7OnY{oYXaZ@4M{dXjJ$v z`$eh=sKvO?Cei3I?)A;6Md`CXq2t(8SWgZw6G7n$pCQ&JiDGgU(}=YRim_lq5{{Tt>$z%L(Sw8>(002ovPDHLk FV1g%Ge~AD9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..eabb0a2ba41bbe0e109263ee9b0d6ada40a6e792 GIT binary patch literal 843 zcmV-R1GM~!P)RPWZAHfP!)RVB2S(!`lt|Um(uYf2_cGfFjG~?QJ_SKM>Hu7Qz_%` zC^KY0j^p?%F-hrI#t?oEkx>e=^x$WZC9-20$8bAJMiyi_f!j|s$&C!#uq!e{4&*3d z*U4D<;$4cQ>yQUJNq3uf6oz@6$E?Ckd0-Y5%+4`a;aG`TuRL)UvsTrDjhJ=G6Wy3? zP%Zcjv%EY}z-*IhK|5x7d7^;XCe?zCn03h$-I#4qEm(P<#_c#+Sul>1xE*7f>{!k} z_~{|56pZ6*{0y;7>1g5}{!WmS0WF-w-(4mv4bw>oQKFMsszM7}DHGx$QauHt_cw57G zmNrf?VwFPCz(EGt%|eYrmJb-fVG|Z VF?y~eTbBR;002ovPDHLkV1i=jh>-vQ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..476d5c9780b6904004c7da0d86c1f02e4c37f892 GIT binary patch literal 1073 zcmV-11kU@3P)?ENDOXxEq##k{R3pG`V_=<^3NWfR4)j)KkE?kf% zU}CHb1trze6>4h;sTD;rtpyq(711_6TOXmFPeL**CLQm0&Kbrt$^HG8P44Zq_ne-d zIYT6GB9TZWvWW>Kq@TD8Z%rbKCJFFT3Q=|)UTQ^@t;I_ZBg!7bOS=$dFXN?m5M}%E z(wB&`ukq3!h_Yz{ypu!>x)$#=AqKVLoh^t#k5E8sIL>$MVhOq3&gY0hM`8YgceIr|YY&iqQ@`QK~6 zQos?!gFYj`%4WoqwqT*0?-5V>feMW6L_F#x3@zbr#H0S91|$0r&w3vNHzOXl4y8E9 z5Dz;}9F@lr$I={PD~(h#??P2Hv5k*OBaS_R%D0F^Gwfz5bE%<=S;V1HRE7}izM+=? zvXWuMx}&JnaTc-a1LhN^j9$d5v#dlZNx#*%UxG%$>Lq+a5(6c?YW(obN2{C>#Hd#& z!B{)f219oRCRP~?=PB;R(sE89s&->)k9y|05eEzCQO)~HF|~}eY5{Q-jsl(y{dFJ0 zLg+*>%Tom8#bC&-wqj#@$W=cjZ_b3=su3H_Ay*wEUqWZ2%FD7H{2O^AAiF$gQgUe1!CaxCt9AAy+*GP?%|S8#ZFe%*GOO> z#bLy_Ugb>^^dZI#kVL7TbBI;FloO_cKI?z#>rnXuv2KJ_AwQf^#JVG>3?mNBvWI0k zCeFQ%8(UwY@+jh1ntry^Op?+IC23&?1EdS}PEAbz5c&FESu5gU4Th($JgXZci}?%j zAcuGCUqC#n17l^3BOc^%YUO^!lkUMvz+uFL`muJC2Zudosl~w?NY31kB51DEg_A{d zo(4V7yWC2^CMFTbCfGp0ZS3d#oLeiEIC_%o8Q~cgkx|9lh(m9&kc<=^oYC)hXq9s6 z($m<(9mL4iY&qBmXe3*lJNfX^nst-}xU1(FC+XyRa$C!w;mW?1+!pg9Cm5oZ0(uZJ z=)Phh(s literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..507c5edd44bfb5efe41327d5cef511a108d35f13 GIT binary patch literal 1074 zcmV-21kL-2P)+t)}?IPi4`?n-BcG{I5ur- z(S>O`4876<#~6}k=`>Up4hWM~CUltm-)|Ri1Ni>m=Xw7B>2vXZK2lazR#sM4OxVB` zGWm@sMM^ z-;l({UGfbz*r=0l*o=*rbn~#1)BM5?7AY+r;d^YHV6D>PUbfT6IjR&Lx6#YE6o*+) zuBKoO-MHxHK}|s}>p4WgxIShmD&AzWe%_@-6DZ+pqS#BZCQ!mV^fOt5qM?`(rW@k` zYsq0Fe-hOnJV_2~IlvgxjZ&g0_?+}E6PL@R*DQCeAjPd$B{wAK#0SSo$OW75#b()K zHUs$LSLVnT+wsWmw*M;;$z=L|Dth}oRO&PU|Q78@DG)EG-;f#n348sTZV zVF~9kvs0GXg_$1ilRM_pLS`uz%MxXz$ZS8iD;g5KO+dIKazGp50^T5@IM7A7EpotC z!W~mwxJ0;WIiQwsy^0H?ge#H*l7zdexR6PK98gHOKye|H0y&_NaDn2&2;t_-0ZGDL zRb044xEeWN72z%_E_4yDUJlqsxMPX~IW&?Y+)+88op1pSBoqyIaEQ!Ol*$riq{ys= zxpK!+dNI=^OYFwX1(wJSoA?`3V^qijl?0d?Wut77&!^Zq%PbjU4n5fUgnXH!m=5f< zk}m_~a~ON=BxQz5hH%r$Y-PY44&!E!3Yp*}?#{7XA*iGWcbzi9S)vHoMVW4ZGIkRX z#g8(<%S4r;jjhy@q>w_AtYRDOq=;&>OpwD#d~}?I%ut7qs$`Bm_@-GlSja#4WRw!w z;XQoPAY06$A7At_L-u$UUpyrjB^DU#RVFRX2UE@R5j!x|C?D|xrk<0JScR#T@)5nxBvhE literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 7eafc6c69..b894c23aa 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,7 +1,6 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_laoyut.xml b/app/src/main/res/layout/drawer_laoyut.xml new file mode 100644 index 000000000..dcf29c42f --- /dev/null +++ b/app/src/main/res/layout/drawer_laoyut.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 330fb34da..fb9ee2890 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -310,7 +310,7 @@ android:layout_height="55dp" android:layout_gravity="center_vertical" android:layout_weight="1" - android:background="?attr/selectableItemBackground" + android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:focusable="true" android:contentDescription="@string/append_playlist" @@ -327,7 +327,7 @@ android:layout_height="55dp" android:layout_gravity="center_vertical" android:layout_weight="1" - android:background="?attr/selectableItemBackground" + android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:focusable="true" android:contentDescription="@string/play_audio" @@ -344,7 +344,7 @@ android:layout_height="55dp" android:layout_gravity="center_vertical" android:layout_weight="1" - android:background="?attr/selectableItemBackground" + android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:focusable="true" android:contentDescription="@string/open_in_popup_mode" @@ -361,7 +361,7 @@ android:layout_height="55dp" android:layout_gravity="center_vertical" android:layout_weight="1" - android:background="?attr/selectableItemBackground" + android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:focusable="true" android:contentDescription="@string/controls_download_desc" diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 31eda4fbc..c8e0907be 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -25,6 +25,7 @@ + @@ -39,4 +40,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index dcf8f9268..ad767835b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -15,6 +15,7 @@ @color/light_youtube_dark_color @color/light_youtube_accent_color @color/light_background_color + @color/light_background_color @drawable/ic_thumb_up_black_24dp @drawable/ic_thumb_down_black_24dp @@ -40,6 +41,7 @@ @drawable/ic_arrow_top_left_black_24dp @drawable/ic_more_vert_black_24dp @drawable/ic_play_arrow_black_24dp + @drawable/ic_settings_black_24dp @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp @drawable/ic_bookmark_black_24dp @@ -66,6 +68,7 @@ @color/dark_youtube_dark_color @color/dark_youtube_accent_color @color/dark_background_color + @color/dark_background_color @drawable/ic_thumb_up_white_24dp @drawable/ic_thumb_down_white_24dp @@ -91,6 +94,7 @@ @drawable/ic_arrow_top_left_white_24dp @drawable/ic_more_vert_white_24dp @drawable/ic_play_arrow_white_24dp + @drawable/ic_settings_white_24dp @drawable/ic_whatshot_white_24dp @drawable/ic_channel_white_24dp @drawable/ic_bookmark_white_24dp @@ -114,6 +118,7 @@ + + + + + - - - - - \ No newline at end of file From e8c5ae194df0b405acfc3b741e9290e8dc4f2aac Mon Sep 17 00:00:00 2001 From: Kotoba Murasaki Date: Thu, 8 Mar 2018 19:24:14 +0000 Subject: [PATCH 211/276] Translated using Weblate (Ukrainian) Currently translated at 100.0% (318 of 318 strings) --- app/src/main/res/values-uk/strings.xml | 265 +++++++++++++++++-------- 1 file changed, 184 insertions(+), 81 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6e377f7f1..7774f7a19 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2,17 +2,17 @@ %1$s переглядів Опубліковано %1$s - Потоковий програвач не знайдено. Бажаєте встановити VLC? - Встановити + Потоковий програвач не знайдено. Чи ви хочете встановити VLC? + Установити Скасувати - Відкрити в браузері - Поділитись + Відкрити у переглядачі + Поширити Завантажити - Пошук + Шукати Налаштування - Ви мали на увазі: %1$s ? + Чи ви мали на увазі: %1$s ? Поділитись з - Виберіть браузер + Оберіть переглядач обертання Використовувати зовнішній відео програвач Використовувати зовнішній аудіо програвач @@ -29,13 +29,13 @@ Автоматично відтворювати відео коли NewPipe викликано з іншого додатку. Типова роздільна здатність Відтворювати за допомогою Kodi - Додаток Kore не знайдено. Встановити? + Застосунок Kore не знайдено. Встановити? Показувати опцію \"Програвати за допомогою Kodi\" - Показувати опцію відтворення відео за допомогою Kodi media center. + Показувати опцію відтворення відео за допомогою Kodi media center Аудіо - Аудіо формат за замовчуванням + Типовий аудіо формат WebM — вільний формат - m4a — краща якість + M4A — ліпша якість Тема Темна Світла @@ -50,49 +50,49 @@ Інше Відтворення у фоновому режимі Відтворити - В доступі відмовлено + У доступі відмовлено Контент - Показувати контент з віковим обмеженням - Відео має вікові обмеження. Спершу активуйте опцію для відображення таких відео в налаштуваннях. + Показувати контент з віковими обмеженнями + Це відео має вікові обмеження. Спершу активуйте опцію для відображення таких відео у налаштуваннях. наживо Помилка Помилка мережі Не вдалося завантажити всі ескізи - Не вдалося розшифрувати підпис url відео. - Не вдалося проаналізувати веб-сайт. - Не вдалося повністю проаналізувати веб-сайт. - Контент не доступний. + Не вдалося розшифрувати підпис URL відео. + Не вдалося проаналізувати веб-сайт + Не вдалося повністю проаналізувати веб-сайт + Контент не доступний Заблоковано GEMA - Не вдалося налаштувати меню завантаження. - Це пряма трансляція. Це ще не підтримується. - Не вдалося отримати ніякий потік. - Шкода, цього не повинно було статися. - Звіт про помилку за допомогою пошти - На жаль сталася помилка. + Не вдалося налаштувати меню завантаження + Трансляції наживо ще не мають підтримування. + Не вдалося отримати жодний потік + Шкода, цього не мало статися. + Зазвітувати помилку за допомогою пошти + На жаль, сталася помилка. ЗВІТУВАТИ Інформація: Що сталося: - Натисніть Пошук для початку + Аби почати натисніть на \"шукати\" Чорна Завантаження Завантаження Звіт про помилку - Все + Усе Канал Так Пізніше Вимкнено Не вдалося завантажити зображення - Додаток/інтерфейс зазнав краху - Ваші коментарі (Англійською): + Застосунок/інтерфейс зазнав краху + Ваш коментар (англійською): Деталі: - Мініатюра попереднього перегляду відео - Мініатюра попереднього перегляду відео + Ескіз попереднього перегляду відео + Ескіз попереднього перегляду відео Використовувати Tor (Експериментально) Перенаправляти трафік через Tor для підвищення конфіденційності (трансляція відео ще не підтримується). Звітувати про помилку @@ -102,7 +102,7 @@ Відео Аудіо Повторити - Використовувати старий плеер + Використовувати старий програвач К М Б @@ -118,57 +118,57 @@ Потоки Помилка Сервер не підтримується - Файл вже існує + Файл уже існує NewPipe завантажує Натисніть для подробиць Будь ласка, зачекайте… - Скопійовано до буферу обміну + Скопійовано до сховку Будь ласка, оберіть теку для завантаження Потоковий програвач не знайдено (ви можете встановити VLC для відтворення) - Відкрити у спливаючому вікні + Відкрити у виринальному режимі Певні роздільності НЕ МАТИМУТЬ звуку якщо цей параметр увімкнено - NewPipe у спливаючому вікні + NewPipe у виринальному вікні Підписатися Ви підписалися Ви відписалися від каналу - Неможливо змінити підписку - Неможливо оновити підписку + Неможливо змінити підписання + Неможливо оновити підписання Головна - Підписки + Підписання Новинки - Фон - Спливаюче вікно + Тло + Виринальне вікно - Типова роздільна здатність спливаючого вікна + Типова роздільна здатність виринального вікна Не всі пристрої підтримують програвання 2K/4K відео Показувати більші роздільні здатності - Відео формат за замовчуванням - Пам\'ятати розмір спливаючого вікна та положення - Пам\'ятати останній розмір та позицію спливаючого вікна + Типовий відео формат + Пам\'ятати розмір виринального вікна та положення + Пам\'ятати останній розмір та позицію виринального вікна Керування жестами Використовувати жести для контролю яскравості та гучності програвача Шукати схожі Показувати схожі під час пошуку - Історія пошуку + Історія пошуків Зберігати пошукові запити локально Історія Вести облік перегляду відео Відновити фокус Продовжувати відтворення опісля переривання (наприклад телефонний дзвінок) Відображати вказівку Утримуйте для додачі - Країна контенту за замовчуванням + Усталена країна контенту Сервіс Програвач Поведінка Історія - Спливаюче вікно - Відворення у спливаючому вікні - Додано в чергу у фоні - Додано в чергу в спливаючому вікні + Виринальне вікно + Відворення у виринальному вікні + Додано до фонового програвання + Додано до чергу у виринальному вікні Плейлист Фільтрувати Оновити @@ -181,53 +181,53 @@ Тільки тепер NewPipe сповіщення - Сповіщення для фонового та спливаючого плеєра NewPipe + Сповіщення для фонового та виринального програвача NewPipe [Невідомо] - Перемкнутися у Фон - Перемкнутися у Спливаюче вікно - Перемкнутися у Головне + Перемкнутися до Тла + Перемкнутися до Виринального вікна + Перемкнутися до Головної Імпортувати базу Експортувати базу - Це перезапише наявну історію та підписки - Експортувати історію, підписки та плейлісти. + Це перепише наявну історію та підписання + Експортувати історію, підписання та плейлисти. Помилка при відворенні потоку Відновлююсь після помилки програвача - Помилкова лінка URL + Помилкова ланка URL Не знайдено відео потоків Не знайдено аудіо потоків Звіт користувача Без підписників Без відео - Помилкова лінка URL або Інтернет не доступний + Помилкова ланка URL або інтернет не є доступним Цей дозвіл має бути відкритим -\nу спливаючому вікні +\nу виринальному вікні «reCAPTCHA» Завантажити - Допустимі символи у іменах файлів - "Невірні символи будуть скоректовані цим " + Допустимі символи у назвах файлів + "Неправильні символи будуть скоректовані цим " Символ заміни Літери та цифри - Більше спеціальних символів + Більш специфічні символи - Про NewPipe + Щодо NewPipe Налаштування Про Неможливо завантажити ліцензію Ліцензії - Внесок - Переглянути на GitHub - Підтримати - Вебсайт + Допомогти + Переглянути на Ґітгаб + Донат + Веб-сайт Історія Історію стерто - Експорт завершено - Іморт завершено + Експортування доконано + Імпортування доконано Топ 50 Новинки Деталі @@ -241,23 +241,23 @@ Отримую інформацію… Контент завантажується -Завантажити файл стріму. +Завантажити стримінґовий файл. Показати інфо Закладки Додати до - Показувати підказку коли фонова чи спливаюча кнопка натиснути на сторінці відео деталей + Показувати підказку коли фонова чи виринальна кнопка натиснута на сторінці відео деталей Перемкнути орієнтацію Фатальна помилка програвача - Зовнішні програвачі не підтримують такі види посилань + Зовнішні програвачі не підтримують такі види ланок Що:\\nЗапит:\\nМова змісту:\\nСервіс:\\nЧас GMT:\\nПакунок:\\nВерсія:\\nВерсія ОС:\\nГлоб. діапазон IP : - Мініатюра аватару користувача + Ескіз аватару користувача Сподобалося Не сподобалося - Без результатно - Перетягніть для зміни порядку + Нічого не знайдено + Перетягніть для зміни шикування %s підписник @@ -267,20 +267,123 @@ %s відео - - + %s відео + %s відео Створити - Видалити один + Видалення одного Видалити всі Відхилити - Перейменувати + Змінити назву Сторонні ліцензії Перейти до сайту Про - Легкий YouTube фронтенд застосунок для Android. - Які б не були Ваші ідеї: переклад, дизайн, легкий чи глобальний рефакторінг - будь яка допомога вітається. Чим більше зроблено тим краще він стає! + Легкий YouTube фронт-енд застосунок для Android. + Які б не були Ваші ідеї: переклад, дизайн, легкий чи глобальний рефакторинґ - будь яка допомога вітається. Що більше зроблено, то ліпшим NewPipe стає! Моніторинг витоку памяті вимкнено +Усунення вад + Нічого нема + Старий убудований медіа-фреймворк програвач + + Перегляди відсутні + + %s перегляд + %s перегляди + %s переглядів + + + Нове завдання + reCAPTCHA челендж + запит на челендж reCAPTCHA + + © %1$s by %2$s under %3$s + Учасники + NewPipe розробляється добровольцями, які витрачають власний час заради вашого задоволення. Настала черга допомогти й вам, аби розробники мали можливість зробити NewPipe ще ліпшим, насолоджуючись філіжанкою джави! + Ваша допомога + Аби одержати більше інформації та прочитати останні новини стосовно NewPipe, завітайте до нашого веб-сайту. + Ліцензія NewPipe + Прочитати ліцензію + + + Історія пошуку + Переглянуто + Історію вимкнено + Історія + Історія відсутня + Елемент видалено + Чи ви хочете видалити цей елемент з історії пошуку? + Чи ви хочете видалити цей елемент з історії переглядів? + Чи ви певні що хочете видали всі елементи з історії? + Програвалося останнім + Програвалося найбільше + + Контент на головній сторінці + Порожня сторінка + Сторінка ятки + Сторінка підписання + Сторінка фіду + Сторінка каналу + Обрати канал + Ви не підписані до жодного з каналів + Обрати ятку + Недійсний ZIP-файл + Увага: не можливо здійснити імпортування всіх файлів. + Це перепише ваш чинний сетап. + + Ятка + Набуває популярності + Фоновий програвач + Виринальний програвач + Усунути + Затиснути, аби зняти з черги + Зняти з черги у фоновому програвачеві + Зняти з черги у виринальному програвачеві + Розпочати програвання звідси + Розпочати програвання звідси у фоновому програвачеві + Розпочати програвання звідси у виринальному програвачеві + + Відчинити шухляду + Зачинити шухляду + Ми тут матимемо щось незабаром ;D + + + Відкрити в улюбленому програвачеві + Завжди питати + + Створити новий плейлист + Видалити плейлиста + Змінити назву плейлиста + Назва + Додати до плейлиста + Установити як ескіз плейлиста + + Додати плейлист до закладок + Усунути закладку + + Чи бажаєте видалити цього плейлиста? + Плейлист було створено + Додано до плейлиста + Ескіз плейлиста було змінено + Плейлист не видалено + + Опис відсутній + + ЗАМОСТИТИ + ЗАПОВНИТИ + ЗРОБИТИ БЛИЖЧИМ + + Створено автоматичним шляхом + Розмір шрифту опису + Малий шрифт + Звичайний шрифт + Великий шрифт + + Увімкнути LeakCanary + Під час роботи LeakCanary застосунок може стати несприйнятливим під час гіп-дампінґу + + Зазвітувати Out-of-Lifecycle хиби + Примусове звітування про неможливість доставлення Rx винятків, яку відбуваються за межами фраґменту, або діяльності життєвого циклу після усунення + From 736ccbe37609d0910187b5a76fe2cf2240bd11ce Mon Sep 17 00:00:00 2001 From: Kotoba Murasaki Date: Thu, 8 Mar 2018 19:38:44 +0000 Subject: [PATCH 212/276] Translated using Weblate (Ukrainian) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-uk/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 7774f7a19..1669a075f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -386,4 +386,11 @@ Зазвітувати Out-of-Lifecycle хиби Примусове звітування про неможливість доставлення Rx винятків, яку відбуваються за межами фраґменту, або діяльності життєвого циклу після усунення - +Використовувати неточне шукання + Неточне шукання дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю + Автоматично додати до черги наступний стрим + Автоматично додавати пов\'язаний стрим, під час початку програвання останнього стриму. + НАЖИВО + СИНХРОНІЗАЦІЯ + + From 728a61756ac1290ff48e267b2c30c230254c567f Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Thu, 8 Mar 2018 20:25:10 +0000 Subject: [PATCH 213/276] Translated using Weblate (Dutch) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-nl/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ca0c81160..f09914384 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -400,4 +400,11 @@ te openen in pop-upmodus Ouf-of-Lifecycle-fouten melden Forceer het melden van niet-bezorgbare Rx-uitzonderingen die gebeuren buten fragment of activiteitscyclus - +Snelle, minder exact spoelen gebruiken + Minder exact spoelen laat de speler sneller posities zoeken met verminderde precisie + Volgende stream automatisch in wachtrij plaatsen + Automatisch een gerealteerde stream toekennen als het afspelen van de laatste stream strat in een niet-herhalende afspeelwachtlijst. + LIVE + SYNCHRONISEREN + + From fa5f5ce2512e557913974640c2655bd871c6bc0d Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Fri, 9 Mar 2018 01:58:01 +0000 Subject: [PATCH 214/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7defff348..f9f8eaf1f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -381,4 +381,11 @@ 報告週期不足錯誤 強制報告在處理完片段或活動週期外發生的無法傳遞的 Rx 異常 - +使用快速不精確的尋找 + 不精確的尋找,允許玩家以更低的精確更快找到位置 + 自動佇列下一個串流 + 在非重複播放佇列中的最後一個串流上開始播放時,自動附上相關串流。 + 直播 + 同步 + + From 2567f8eefb2ff6282e4a037dae39488d44c69c43 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Fri, 9 Mar 2018 08:21:03 +0000 Subject: [PATCH 215/276] Translated using Weblate (Hebrew) Currently translated at 75.3% (244 of 324 strings) --- app/src/main/res/values-he/strings.xml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 25aed9cd8..5e199e2b1 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -176,20 +176,26 @@ אין עוקבים - %s עוקב - %s עוקבים + מנוי אחד + שני מנויים + %s מנויים + %s מנויים אין תצוגות - תצוגה %s - %s תצוגות + צפייה אחת + שתי צפיות + %s צפיות + %s צפיות אין סרטונים - סרטון %s - %s סרטונים + סרטון אחד + שני סרטונים + %s סרטונים + %s סרטונים התחל From 24a06ea6f6166915272faa27dcbcc17497bf0dc2 Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Mar 2018 08:21:58 +0000 Subject: [PATCH 216/276] Translated using Weblate (Hebrew) Currently translated at 75.6% (245 of 324 strings) --- app/src/main/res/values-he/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 5e199e2b1..660f51b4a 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -291,4 +291,5 @@ התחל לנגן כאן התחל כאן בנגן הרקע התחל כאן בנגן הצץ - +הורד קובץ. + From 9476bd65275c1bcee68cc29e8bbc05b9ba322a88 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Fri, 9 Mar 2018 08:22:13 +0000 Subject: [PATCH 217/276] Translated using Weblate (Hebrew) Currently translated at 75.6% (245 of 324 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 660f51b4a..fcb1f363f 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -291,5 +291,5 @@ התחל לנגן כאן התחל כאן בנגן הרקע התחל כאן בנגן הצץ -הורד קובץ. +הורדת קובץ הזרמה. From d2e2622279de9cff49c23233395e438f6f07555c Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Mar 2018 08:22:36 +0000 Subject: [PATCH 218/276] Translated using Weblate (Hebrew) Currently translated at 75.9% (246 of 324 strings) --- app/src/main/res/values-he/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index fcb1f363f..e2547e755 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -292,4 +292,6 @@ התחל כאן בנגן הרקע התחל כאן בנגן הצץ הורדת קובץ הזרמה. + הצג מידע + From 41fdafac459ba04139ff152f4b5c4f7a4056073b Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Fri, 9 Mar 2018 08:23:14 +0000 Subject: [PATCH 219/276] Translated using Weblate (Hebrew) Currently translated at 76.8% (249 of 324 strings) --- app/src/main/res/values-he/strings.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index e2547e755..8fc73903e 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -292,6 +292,11 @@ התחל כאן בנגן הרקע התחל כאן בנגן הצץ הורדת קובץ הזרמה. - הצג מידע + הצגת מידע + סימניות + + הוספה אל + + מדינת תוכן כבררת מחדל From ec8d488249f9acb785470bc630da5cb1fde219fa Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Mar 2018 08:23:39 +0000 Subject: [PATCH 220/276] Translated using Weblate (Hebrew) Currently translated at 78.0% (253 of 324 strings) --- app/src/main/res/values-he/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 8fc73903e..4d3d440c9 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -299,4 +299,9 @@ הוספה אל מדינת תוכן כבררת מחדל + שירות + ניפוי שגיאות + תמיד + חד פעמי + From 04e90cc279331c00cc2b3844f9e28897c42284fd Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Fri, 9 Mar 2018 08:24:26 +0000 Subject: [PATCH 221/276] Translated using Weblate (Hebrew) Currently translated at 78.7% (255 of 324 strings) --- app/src/main/res/values-he/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 4d3d440c9..0a9e5cd20 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -304,4 +304,6 @@ תמיד חד פעמי + יבוא מסד נתונים + יצוא מסד נתונים From 7be5ec05213733470cba02d0cfd02f8d329b8c3d Mon Sep 17 00:00:00 2001 From: anonymous <> Date: Fri, 9 Mar 2018 08:24:59 +0000 Subject: [PATCH 222/276] Translated using Weblate (Hebrew) Currently translated at 79.3% (257 of 324 strings) --- app/src/main/res/values-he/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 0a9e5cd20..1a6886f35 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -306,4 +306,6 @@ יבוא מסד נתונים יצוא מסד נתונים + נגנים חיצוניים לא תומכים בסוגי קישורים אלה + כתובת לא תקינה From ee3248ea5d3db633a368faed738f142e983bf6c2 Mon Sep 17 00:00:00 2001 From: E T Date: Fri, 9 Mar 2018 11:28:47 +0000 Subject: [PATCH 223/276] Translated using Weblate (Turkish) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-tr/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1f1208326..9cecac692 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -395,4 +395,11 @@ Out-of-Lifecycle Hatalarını Bildir Parçanın dışında veya atımdan sonraki etkinlik yaşam döngüsünde meydana gelen ve teslim edilemeyen Rx beklentilerinin bildirimini zorla - +Hızlı isabetsiz konumlama kullan + İsabetsiz konumlama, oynatıcının konumları düşürülmüş duyarlıkla saptamasını sağlar + Sonraki akışı kendiliğinden kuyrukla + Yinelemeyen oynatma kuyruğundaki son akış başladığında ilişkili akışı kuyruğun sonuna kendiliğinden ekle. + CANLI + EŞZAMANLA + + From 3dab4c07cf7525392d7295725152130adc3f21c3 Mon Sep 17 00:00:00 2001 From: Kotoba Murasaki Date: Thu, 8 Mar 2018 19:41:49 +0000 Subject: [PATCH 224/276] Translated using Weblate (Ukrainian) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-uk/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1669a075f..5deac5622 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -26,7 +26,7 @@ Шлях для завантаження аудіо Шлях де будуть зберігатись завантажені аудіо файли Автоматично відтворювати - Автоматично відтворювати відео коли NewPipe викликано з іншого додатку. + Автоматично відтворювати відео коли NewPipe викликано з іншого застосунку Типова роздільна здатність Відтворювати за допомогою Kodi Застосунок Kore не знайдено. Встановити? @@ -59,7 +59,7 @@ Помилка Помилка мережі Не вдалося завантажити всі ескізи - Не вдалося розшифрувати підпис URL відео. + Не вдалося розшифрувати підпис URL відео Не вдалося проаналізувати веб-сайт Не вдалося повністю проаналізувати веб-сайт Контент не доступний From a4fe43a9645544cb22ddde922722524cb3b2da50 Mon Sep 17 00:00:00 2001 From: Emanuele Petriglia Date: Fri, 9 Mar 2018 15:44:17 +0000 Subject: [PATCH 225/276] Translated using Weblate (Italian) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-it/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f65281c27..5436b9883 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -405,4 +405,11 @@ Riporta gli errori \"fuori dal ciclo di vita\" Forza il riporto delle eccezioni Rx non consegnabili verificate fuori dal frammento o dall\'attività del \"ciclo di vita\" - +Usa una ricerca veloce ma poco precisa + La ricerca imprecisa permette al riproduttore di spostarti nelle posizioni più velocemente con una precisione ridotta + Metti in coda automaticamente il prossimo flusso + Aggiungi automaticamente un flusso correlato mentre il playback parte dall\'ultimo flusso in una cosa non ripetitiva. + IN DIRETTA + SINCRONIZZAZIONE + + From dcc510ff6c645dd0d4ef4f811bf0e0d618bb5a88 Mon Sep 17 00:00:00 2001 From: Eduardo Caron Date: Thu, 8 Mar 2018 22:51:28 +0000 Subject: [PATCH 226/276] Translated using Weblate (Portuguese (Brazil)) Currently translated at 98.7% (320 of 324 strings) --- app/src/main/res/values-pt-rBR/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 75e6399f0..909df3080 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -379,4 +379,11 @@ abrir em modo popup Reportar Erros fora do ciclo de vida Forçar o report de exceções Rx não entregáveis ocorrendo fora do fragmento ou ciclo de vida da atividade após o dispose - +Usar índice de indexação rápido porém não preciso + Usar índice de indexação inexato + Adicionar a próxima stream à fila automaticamente + Anexar automaticamente uma stream relacionada quando a reprodução iniciar na última stream em uma fila não repetitiva + Ao Vivo + Sincronizar + + From 65674f7fd47f604e1b06562786b8bec91d7c12e7 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Fri, 9 Mar 2018 08:25:47 +0000 Subject: [PATCH 227/276] Translated using Weblate (Hebrew) Currently translated at 79.6% (258 of 324 strings) --- app/src/main/res/values-he/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 1a6886f35..a2b1c6abc 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -307,5 +307,6 @@ יבוא מסד נתונים יצוא מסד נתונים נגנים חיצוניים לא תומכים בסוגי קישורים אלה - כתובת לא תקינה + כתובת שגויה + חי From ca47f566dc8723f58b265ee6f0fd5485fe5386c3 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 9 Mar 2018 12:52:22 +0000 Subject: [PATCH 228/276] Translated using Weblate (Ukrainian) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5deac5622..7399bad2b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -222,7 +222,7 @@ Ліцензії Допомогти Переглянути на Ґітгаб - Донат + Задонатуй Веб-сайт Історія Історію стерто From b811aec7737e5e5fcc41dafd20279e68e5d95cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Freddy=20Mor=C3=A1n=20Jr?= Date: Fri, 9 Mar 2018 17:19:51 +0000 Subject: [PATCH 229/276] Translated using Weblate (Spanish) Currently translated at 98.7% (320 of 324 strings) --- app/src/main/res/values-es/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3acabfe57..f65f4a6a8 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -404,4 +404,6 @@ abrir en modo popup Reportar errores fuera del ciclo de duración Forzar la notificación de excepciones no entregables de RX que ocurren fuera del fragmento o del ciclo de actividad después de disponer - +Usar búsqueda rápida inexacta + La búsqueda inexacta permite al reproductor buscar posiciones más rápido con menor precisión + From 0abf97e999f770d0a353466d2f56211b008cfd4e Mon Sep 17 00:00:00 2001 From: Pablo Hinojosa Date: Sat, 10 Mar 2018 15:07:49 +0000 Subject: [PATCH 230/276] Translated using Weblate (Spanish) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-es/strings.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f65f4a6a8..d381c30fc 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -311,7 +311,7 @@ abrir en modo popup Servicio Abrir cajón Cerrar cajón - No se ha encontrado ningún reproductor de streaming (puede instalar VLC para reproducirlo) + No se ha encontrado ningún reproductor de vídeo (puede instalar VLC para reproducirlo) Siempre Sólo una vez @@ -406,4 +406,9 @@ abrir en modo popup Usar búsqueda rápida inexacta La búsqueda inexacta permite al reproductor buscar posiciones más rápido con menor precisión + Auto-encolar la siguiente transmisión + "Automáticamente añadir un vídeo relacionado cuando el reproductor llegue al último vídeo en una lista de reproducción no repetible. " + DIRECTO + SINCRONIZAR + From 562f7e7e414ad5164df738eea6043cb8f37fad4d Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Sat, 10 Mar 2018 13:16:51 -0300 Subject: [PATCH 231/276] Add duration view to video detail fragment - Add "textAllCaps" to the mini stream layout - Closes #609 --- .../fragments/detail/VideoDetailFragment.java | 25 +++++++++++++------ .../main/res/layout/fragment_video_detail.xml | 25 +++++++++++++++++++ .../main/res/layout/list_stream_mini_item.xml | 1 + app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-he/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 12 files changed, 43 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 8e8179b9a..fc9a427eb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -16,7 +16,6 @@ import android.support.v4.content.ContextCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; -import android.support.v7.widget.Toolbar; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; @@ -92,8 +91,6 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -138,9 +135,9 @@ public class VideoDetailFragment /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ + private Menu menu; - private Toolbar toolbar; private Spinner spinnerToolbar; private ParallaxScrollView parallaxScrollRootView; @@ -160,6 +157,7 @@ public class VideoDetailFragment private TextView detailControlsAddToPlaylist; private TextView detailControlsDownload; private TextView appendControlsDetail; + private TextView detailDurationView; private LinearLayout videoDescriptionRootLayout; private TextView videoUploadDateView; @@ -461,9 +459,7 @@ public class VideoDetailFragment @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - - toolbar = activity.findViewById(R.id.toolbar); - spinnerToolbar = toolbar.findViewById(R.id.toolbar_spinner); + spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner); parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content); @@ -483,6 +479,7 @@ public class VideoDetailFragment detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append); detailControlsDownload = rootView.findViewById(R.id.detail_controls_download); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); + detailDurationView = rootView.findViewById(R.id.detail_duration_view); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); @@ -1098,6 +1095,7 @@ public class VideoDetailFragment animateView(contentRootLayoutHiding, false, 200); animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); + animateView(detailDurationView, false, 100); videoTitleTextView.setText(name != null ? name : ""); videoTitleTextView.setMaxLines(1); @@ -1168,6 +1166,18 @@ public class VideoDetailFragment thumbsDisabledTextView.setVisibility(View.GONE); } + if (info.getDuration() > 0) { + detailDurationView.setText(Localization.getDurationString(info.getDuration())); + detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.duration_background_color)); + animateView(detailDurationView, true, 100); + } else if (info.getStreamType() == StreamType.LIVE_STREAM) { + detailDurationView.setText(R.string.duration_live); + detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.live_duration_background_color)); + animateView(detailDurationView, true, 100); + } else { + detailDurationView.setVisibility(View.GONE); + } + videoTitleRoot.setClickable(true); videoTitleToggleArrow.setVisibility(View.VISIBLE); videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); @@ -1201,7 +1211,6 @@ public class VideoDetailFragment case AUDIO_LIVE_STREAM: detailControlsDownload.setVisibility(View.GONE); spinnerToolbar.setVisibility(View.GONE); - toolbar.setTitle(R.string.live); break; default: if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break; diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index fb9ee2890..cc14e1322 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -70,6 +70,31 @@ android:visibility="gone" tools:ignore="RtlHardcoded" tools:visibility="visible"/> + + diff --git a/app/src/main/res/layout/list_stream_mini_item.xml b/app/src/main/res/layout/list_stream_mini_item.xml index 22cd3887e..bffd13a6f 100644 --- a/app/src/main/res/layout/list_stream_mini_item.xml +++ b/app/src/main/res/layout/list_stream_mini_item.xml @@ -36,6 +36,7 @@ android:paddingLeft="@dimen/video_item_search_duration_horizontal_padding" android:paddingRight="@dimen/video_item_search_duration_horizontal_padding" android:paddingTop="@dimen/video_item_search_duration_vertical_padding" + android:textAllCaps="true" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="@color/duration_text_color" android:textSize="@dimen/video_item_search_duration_text_size" diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d381c30fc..e9d686d10 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -408,7 +408,6 @@ abrir en modo popup La búsqueda inexacta permite al reproductor buscar posiciones más rápido con menor precisión Auto-encolar la siguiente transmisión "Automáticamente añadir un vídeo relacionado cuando el reproductor llegue al último vídeo en una lista de reproducción no repetible. " - DIRECTO SINCRONIZAR diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index a2b1c6abc..cb69d6639 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -308,5 +308,4 @@ יצוא מסד נתונים נגנים חיצוניים לא תומכים בסוגי קישורים אלה כתובת שגויה - חי diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5436b9883..f7206893d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -409,7 +409,6 @@ La ricerca imprecisa permette al riproduttore di spostarti nelle posizioni più velocemente con una precisione ridotta Metti in coda automaticamente il prossimo flusso Aggiungi automaticamente un flusso correlato mentre il playback parte dall\'ultimo flusso in una cosa non ripetitiva. - IN DIRETTA SINCRONIZZAZIONE diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f09914384..4f8e5b0e5 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -404,7 +404,6 @@ te openen in pop-upmodus Minder exact spoelen laat de speler sneller posities zoeken met verminderde precisie Volgende stream automatisch in wachtrij plaatsen Automatisch een gerealteerde stream toekennen als het afspelen van de laatste stream strat in een niet-herhalende afspeelwachtlijst. - LIVE SYNCHRONISEREN diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 909df3080..96e6ccb70 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -383,7 +383,6 @@ abrir em modo popup Usar índice de indexação inexato Adicionar a próxima stream à fila automaticamente Anexar automaticamente uma stream relacionada quando a reprodução iniciar na última stream em uma fila não repetitiva - Ao Vivo Sincronizar diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 9cecac692..9fbb42e0b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -399,7 +399,6 @@ İsabetsiz konumlama, oynatıcının konumları düşürülmüş duyarlıkla saptamasını sağlar Sonraki akışı kendiliğinden kuyrukla Yinelemeyen oynatma kuyruğundaki son akış başladığında ilişkili akışı kuyruğun sonuna kendiliğinden ekle. - CANLI EŞZAMANLA diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 7399bad2b..0608d8150 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -390,7 +390,6 @@ Неточне шукання дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю Автоматично додати до черги наступний стрим Автоматично додавати пов\'язаний стрим, під час початку програвання останнього стриму. - НАЖИВО СИНХРОНІЗАЦІЯ diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f9f8eaf1f..a112b4830 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -385,7 +385,6 @@ 不精確的尋找,允許玩家以更低的精確更快找到位置 自動佇列下一個串流 在非重複播放佇列中的最後一個串流上開始播放時,自動附上相關串流。 - 直播 同步 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e168165e6..c97f12809 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,7 +113,6 @@ Show age restricted content Age Restricted Video. Allowing such material is possible from Settings. live - LIVE Downloads Downloads Error report From 9c9b6bc0d6f291d33a04334fd6b871684a50a2cb Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Sat, 10 Mar 2018 13:36:50 -0300 Subject: [PATCH 232/276] Change thumbnail's scale strategy - Closes #1054 --- .../schabi/newpipe/fragments/detail/VideoDetailFragment.java | 3 --- app/src/main/res/layout/fragment_video_detail.xml | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index fc9a427eb..4d935dbce 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1007,9 +1007,6 @@ public class VideoDetailFragment int height = isPortrait ? (int) (metrics.widthPixels / (16.0f / 9.0f)) : (int) (metrics.heightPixels / 2f); - thumbnailImageView.setScaleType(isPortrait - ? ImageView.ScaleType.CENTER_CROP - : ImageView.ScaleType.FIT_CENTER); thumbnailImageView.setLayoutParams( new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); thumbnailImageView.setMinimumHeight(height); diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index cc14e1322..7c6568b67 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -37,7 +37,7 @@ android:layout_height="wrap_content" android:background="@android:color/transparent" android:contentDescription="@string/detail_thumbnail_view_description" - android:scaleType="centerCrop" + android:scaleType="fitCenter" tools:ignore="RtlHardcoded" tools:layout_height="200dp" tools:src="@drawable/dummy_thumbnail"/> From 0a5bffe8262f4b13194ead25eb338831a4661f98 Mon Sep 17 00:00:00 2001 From: Emanuele Petriglia Date: Fri, 9 Mar 2018 15:51:26 +0000 Subject: [PATCH 233/276] Translated using Weblate (Italian) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-it/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5436b9883..4e59659e7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -15,7 +15,7 @@ Scegli il browser rotazione Percorso dei video scaricati - Percorso in cui memorizzare i video scaricati + Cartella in cui memorizzare i video scaricati Inserisci il percorso per i download Risoluzione predefinita Riproduci con Kodi @@ -45,7 +45,7 @@ Percorso degli audio scaricati Percorso dove salvare gli audio scaricati - Inserisci il percorso per i file audio + Inserisci la cartella per i file audio Tema Scuro @@ -76,7 +76,7 @@ Mostra contenuti vincolati all\'età Questo video è riservato ad un pubblico maggiorenne. Per accedervi, abilita \"Mostra video vincolati all\'età\" nelle impostazioni. - Tocca \"cerca\" per iniziare + Tocca cerca per iniziare Riproduzione automatica Riproduci i video automaticamente quando NewPipe viene aperto da un\'altra app in diretta @@ -161,7 +161,7 @@ Risoluzione predefinita per la modalità popup Mostra risoluzioni più alte - Solo alcuni dispositivi supportano la riproduzione di video in 2K e 4K + Solo alcuni dispositivi supportano la riproduzione video in 2K e 4K Formato video predefinito Ricorda grandezza e posizione del popup Ricorda l\'ultima grandezza e posizione del popup @@ -191,7 +191,7 @@ Informazioni Contributori Licenze - Un\'interfaccia leggera e gratuita per accedere a YouTube su Android. + Un\'interfaccia leggera e libera per accedere a YouTube su Android. Mostra su GitHub Licenza di NewPipe Un aiuto è sempre il benvenuto: nuove idee, design o traduzioni, piccoli cambi o modifiche radicali del codice rendono l\'applicazione sempre migliore! @@ -405,7 +405,7 @@ Riporta gli errori \"fuori dal ciclo di vita\" Forza il riporto delle eccezioni Rx non consegnabili verificate fuori dal frammento o dall\'attività del \"ciclo di vita\" -Usa una ricerca veloce ma poco precisa +Usa una ricerca rapida ma imprecisa La ricerca imprecisa permette al riproduttore di spostarti nelle posizioni più velocemente con una precisione ridotta Metti in coda automaticamente il prossimo flusso Aggiungi automaticamente un flusso correlato mentre il playback parte dall\'ultimo flusso in una cosa non ripetitiva. From a8c9edbc3f123b27c797a8dac94f8f193d274c0a Mon Sep 17 00:00:00 2001 From: Pablo Hinojosa Date: Sat, 10 Mar 2018 15:08:19 +0000 Subject: [PATCH 234/276] Translated using Weblate (Spanish) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-es/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d381c30fc..8df829600 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -407,7 +407,7 @@ abrir en modo popup Usar búsqueda rápida inexacta La búsqueda inexacta permite al reproductor buscar posiciones más rápido con menor precisión Auto-encolar la siguiente transmisión - "Automáticamente añadir un vídeo relacionado cuando el reproductor llegue al último vídeo en una lista de reproducción no repetible. " + Automáticamente añadir un vídeo relacionado cuando el reproductor llegue al último vídeo en una lista de reproducción no repetible. DIRECTO SINCRONIZAR From 5f764ab8f5e28ac2744f8cbff648087bcf70b767 Mon Sep 17 00:00:00 2001 From: alexandre patelli Date: Sat, 10 Mar 2018 18:25:20 +0100 Subject: [PATCH 235/276] Media Button Play/Pause, Previous and Next in Background Player --- app/src/main/AndroidManifest.xml | 6 +++ .../newpipe/player/BackgroundPlayer.java | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1be8c1f2c..18b3222a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,6 +43,12 @@ android:launchMode="singleTask" android:label="@string/title_activity_background_player"/> + + + + + + Date: Sun, 11 Mar 2018 15:09:11 +0100 Subject: [PATCH 236/276] Direct use of AudioManager from AudioReactor --- .../java/org/schabi/newpipe/player/BackgroundPlayer.java | 7 ++----- .../org/schabi/newpipe/player/helper/AudioReactor.java | 9 +++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 48249d876..bc28116cd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -28,7 +28,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; -import android.media.AudioManager; import android.os.Build; import android.os.IBinder; import android.support.annotation.IntRange; @@ -82,7 +81,6 @@ public final class BackgroundPlayer extends Service { private BasePlayerImpl basePlayerImpl; private LockManager lockManager; - private AudioManager mAudioManager; private ComponentName mReceiverComponent; /*////////////////////////////////////////////////////////////////////////// @@ -122,9 +120,8 @@ public final class BackgroundPlayer extends Service { mBinder = new PlayerServiceBinder(basePlayerImpl); shouldUpdateOnProgress = true; - mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mReceiverComponent = new ComponentName(this, MediaButtonReceiver.class); - mAudioManager.registerMediaButtonEventReceiver(mReceiverComponent); + basePlayerImpl.audioReactor.registerMediaButtonEventReceiver(mReceiverComponent); } @Override @@ -163,7 +160,7 @@ public final class BackgroundPlayer extends Service { basePlayerImpl = null; lockManager = null; - mAudioManager.unregisterMediaButtonEventReceiver(mReceiverComponent); + basePlayerImpl.audioReactor.unregisterMediaButtonEventReceiver(mReceiverComponent); stopForeground(true); stopSelf(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 2c85cfc34..0e6642eed 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.player.helper; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.media.AudioFocusRequest; @@ -86,6 +87,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } + public void registerMediaButtonEventReceiver(ComponentName componentName) { + audioManager.registerMediaButtonEventReceiver(componentName); + } + + public void unregisterMediaButtonEventReceiver(ComponentName componentName) { + audioManager.unregisterMediaButtonEventReceiver(componentName); + } + /*////////////////////////////////////////////////////////////////////////// // AudioFocus //////////////////////////////////////////////////////////////////////////*/ From 36457400e708dc284e2bbb219b472f8baf795aee Mon Sep 17 00:00:00 2001 From: alexandre patelli Date: Sun, 11 Mar 2018 19:23:00 +0100 Subject: [PATCH 237/276] Review Fixes --- .../java/org/schabi/newpipe/player/BackgroundPlayer.java | 7 +++++-- .../org/schabi/newpipe/player/helper/AudioReactor.java | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index bc28116cd..3e9a63886 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -152,6 +152,7 @@ public final class BackgroundPlayer extends Service { lockManager.releaseWifiAndCpu(); } if (basePlayerImpl != null) { + basePlayerImpl.audioReactor.unregisterMediaButtonEventReceiver(mReceiverComponent); basePlayerImpl.stopActivityBinding(); basePlayerImpl.destroy(); } @@ -160,8 +161,6 @@ public final class BackgroundPlayer extends Service { basePlayerImpl = null; lockManager = null; - basePlayerImpl.audioReactor.unregisterMediaButtonEventReceiver(mReceiverComponent); - stopForeground(true); stopSelf(); } @@ -594,6 +593,10 @@ public final class BackgroundPlayer extends Service { pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT); } else if (keycode == KeyEvent.KEYCODE_HEADSETHOOK) { pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT); + } else if (keycode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT); + } else if (keycode == KeyEvent.KEYCODE_MEDIA_REWIND) { + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); } if (pendingIntent != null) { try { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 0e6642eed..df30c3e79 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -88,10 +88,18 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au } public void registerMediaButtonEventReceiver(ComponentName componentName) { + if (android.os.Build.VERSION.SDK_INT > 27) { + Log.e(TAG, "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."); + return; + } audioManager.registerMediaButtonEventReceiver(componentName); } public void unregisterMediaButtonEventReceiver(ComponentName componentName) { + if (android.os.Build.VERSION.SDK_INT > 27) { + Log.e(TAG, "unregisterMediaButtonEventReceiver has been deprecated and maybe not supported anymore."); + return; + } audioManager.unregisterMediaButtonEventReceiver(componentName); } From 2a778383b1338445a4008e39118ca1da079f8764 Mon Sep 17 00:00:00 2001 From: Andreas Kleinert Date: Sat, 10 Mar 2018 18:59:44 +0000 Subject: [PATCH 238/276] Translated using Weblate (German) Currently translated at 93.2% (302 of 324 strings) --- app/src/main/res/values-de/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 871e31750..df414b781 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -375,4 +375,5 @@ Schriftgröße der Untertitel Abbrechen Normale Schriftgröße + Stream-Datei herunterladen From a67ff564d0d1ed3bdee6b174ec1fd99d215f91ce Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sun, 11 Mar 2018 12:24:36 +0000 Subject: [PATCH 239/276] Translated using Weblate (Arabic) Currently translated at 91.6% (297 of 324 strings) --- app/src/main/res/values-ar/strings.xml | 93 ++++++++++++++++++++------ 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ba26d6ed6..f437d7cbb 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -10,15 +10,15 @@ الإعجابات صور معاينة الفيديو الصورة المصغرة الشخصية - هل تقصد : %1$s ? + هل تقصد : %1$s ؟ تنزيل تنزيل أدخل مسار لتنزيل الملفات الصوتية. مسار حفظ التنزيلات الصوتية - مسار الصوتيات المحفوظة + مسار الصوتيات المُنزَّلة أدخل مسار التنزيل لملفات الفيديو مسار حفظ تنزيلات الفيديو في - مسار تحميل الفيديو + مسار الفيديوهات المُنزّلَة "لا يمكن إنشاء مجلد للتنزيلات في '%1$s'" "تم إنشاء مجلد تنزيلات في '%1$s'" تثبيت @@ -60,7 +60,7 @@ خطأ تعذرت عملية تحليل الموقع تعذر فك تشفير توقيع رابط الفيديو -انقر على البحث للبدء +أنقر على البحث للبدء إشتراك مشترك الرئيسية @@ -70,7 +70,7 @@ الخلفية التشغيل التلقائي - أسود + أسوَد التاريخ التاريخ المحتوى @@ -86,7 +86,7 @@ التأريخ إفتح في نافذة منبثقه عند تمكين هذا الخيار لن يكون هنالك صوت في بعض خيارات الجودة - وضع النافذة NewPipe المنبثقة + وضع نافذة NewPipe المنبثقة تم إلغاء اشتراك القناة تعذر تغيير في الاشتراك تعذر تحديث الاشتراك @@ -94,9 +94,9 @@ نافذة تشغيل الفيديو تلقائيا عند استدعاء NewPipe من تطبيق آخر - الجودة الافتراضية للنوافذ المنبثقة + الجودة الإفتراضية للنوافذ المنبثقة إظهار جودة أعلى - فقط بعض الأجهزة تدعم تشغيل فيديوهات 2K / 4K + بعض الأجهزة فقط تدعم تشغيل فيديوهات 2K / 4K تنسيق الفيديو الافتراضي تذكر حجم النافذة و وضعها تذكر آخر حجم ومكان النافذة @@ -117,14 +117,14 @@ التشغيل في الوضع المنبثق تم وضعه على قائمة الانتظار في مشغل الخلفية تم وضعه على قائمة الانتظار في مشغل النافذة المنبثقة - عرض المحتوى المقيد بحسب العمر - فيديو مقيد بحسب العمر. السماح بهذه المحتوى ممكن من الإعدادات. + عرض المحتوى المقيّد بحسب العُمر + فيديو مقيد بحسب العمر. السماح بهذا المحتوى ممكن عن طريق الإعدادات. مباشر تقرير الخطأ قائمة التشغيل نعم - فيما بعد - معطل + لاحقًا + مُعطل فلتر تحديث تنظيف @@ -241,7 +241,7 @@ شاهد تم تعطيل السجل التاريخ فارغ - تم تنظيف التاريخ + تم تنظيف التأريخ تم حذف العنصر هل تريد حذف هذا العنصر من سجل البحث؟ @@ -265,20 +265,20 @@ تحدي ريكابتشا اضغط للإدراج بقائمة الانتظار - صفر + لم تُشاهَد %s مشاهدة - اثنان - قليل - كثير + مشاهدتَين + %s مشاهدات + %s مشاهدات %s مشاهدات - صفر + لا فيديو %s فيديو - اثنان - قليل - كثير + فيديوهتان + %s فيديوهات + %s فيديوهات %s فيديوهات @@ -311,4 +311,55 @@ تصدير قاعدة البيانات "سيتجاوز سجل التاريخ والاشتراكات الحالية " تصدير سجل, الاشتراكات وقوائم التشغيل. + عرض المعلومات + + إضافة إلى + + تحليل + مٌباشِر + لم يتم العثور على أي بث + لم يتم العثور على أي بث صوتي + + إسحب للقيام بالترتيب + + إنشاء + إحذف واحدة + حذف الكل + إلغاء + إعادة التسمية + + تمت عملية التصدير + إكتَملَت عملية الإستيراد + تنبيه : تعذرت عملية استيراد كافة الملفات. + سوف نقوم بإدماج شيء جديد هنا قريبا ;D + + + قارئ الوسائط المُفضّل + + قارئ الفيديوهات + أُطلُب دائمًا + + عملية تحميل المعلومات جارية … + جارٍ تحميل المحتوى المطلوب + + إنشاء قائمة تشغيل جديدة + حذف قائمة التشغيل + "إعادة تسمية قائمة التشغيل " + التسمية + إضافة إلى قائمة تشغيل + هل تريد حذف قائمة التشغيل هذه ؟ + تم إنشاء قائمة التشغيل بنجاح + تمت إضافتها إلى قائمة التشغيل + فشِلت عملية حذف قائمة التشغيل + + ملئ + تقريب + + حجم خط التسمية + أصغر خط + خط عادي + خط ذو حجم كبير + + مُزامَنة + From e7a0b850df2332e83dcd0551bc66fa19e4390134 Mon Sep 17 00:00:00 2001 From: cozyplanes Date: Mon, 12 Mar 2018 14:50:29 +0000 Subject: [PATCH 240/276] Translated using Weblate (Korean) Currently translated at 100.0% (324 of 324 strings) --- app/src/main/res/values-ko/strings.xml | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4a0caee97..cd273af38 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -284,4 +284,109 @@ 여기서부터 재생 여기서부터 백그라운드에서 재생 여기서부터 팝업에 재생 +스트리밍 플레이어를 찾을 수 없습니다. VLC를 설치하면 플레이하실 수 있습니다 + 스트리밍 파일 다운로드하기. + 정보 보기 + + 북마크 + + 이곳에 추가 + + 정확하지는 않지만 빠른 탐색 + 정확하지 않은 탐색은 빠르게 위치로 탐색할 수 있지만 정확도는 떨어집니다 + 다음 스트림을 자동으로 재생열에 추가하기 + 전 스트림이 무한 반복 재생 큐가 아닐 때 관련된 스트림 자동 재생하기. + 기본 콘텐츠 국가 + 서비스 + 디버그 + 라이브 (LIVE) + 항상 + 한번만 + + 디바이스 방향 토글 + 백그라운드로 전환 + 팝업으로 전환 + 기본으로 전환 + + 데이터베이스 가져오기 + 데이터베이스 내보내기 + 현재 시청 기록 및 구독 목록을 덮어쓰기 됩니다 + 시청 기록, 구독 목록, 재생목록 내보내기. + 외부 플레이어는 이러한 종류의 링크를 지원하지 않습니다 + 잘못된 URL + 발견된 비디오 스트림 없음 + 발견된 오디오 스트림 없음 + + 드래그하여 재배열 + + 만들기 + 1개 삭제하기 + 모두 삭제하기 + 취소 + 이름 바꾸기 + + 로봇인지 확인합니다 + 이 항목을 시청 기록에서 삭제하시겠습니까? + 모든 항목을 시청 기록에서 삭제하시겠습니까? + 마지막으로 재생 + 가장 많이 재생 + + 내보내기 완료 + 가져오기 완료 + 유효한 ZIP 파일 없음 + 경고: 모든 파일 가져오기를 실패했습니다. + 이것은 현재 설정을 덮어쓸 것입니다. + + 드로어 열기 + 드로어 닫기 + 여기에 무언가가 추가될 거에요~ :D + + + 선호하는 플레이어로 열기 + 선호하는 플레이어 + + 비디오 플레이어 + 백그라운드 플레이어 + 팝업 플레이어 + 항상 묻기 + + 정보 가져오는 중… + 요청한 콘텐츠를 로딩 중입니다 + + 새로운 재생목록 만들기 + 재생목록 삭제 + 재생목록 이름 바꾸기 + 이름 + 재생목록에 추가 + 재생목록 썸네일로 설정 + + 재생목록 북마크하기 + 북마크 제거하기 + + 이 재생목록을 삭제하시겠습니까? + 재생목록 생성 성공 + 재생목록에 추가됨 + 재생목록 썸내일이 바뀜 + 재생목록 삭제 실패 + + 자막 없음 + + 꼭 맞게 하기 + 채우기 + 확대 + + 자동 생성됨 + 자막 폰트 크기 + 작은 폰트 + 보통 폰트 + 큰 폰트 + + 동기화 + + LeakCanary 할성화 + 메모리 누수 모니터링은 힙 덤핑시 앱이 불안정할 수 있습니다 + + Out-of-Lifecycle 에러 보고 + 프래그먼트 또는 버려진 액티비티 주기 밖에서 일어나는 전달할 수 없는 Rx 예외를 강제적으로 보고하기 + From a9fea9f6065550890268692d845fed5e62cc7443 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 12 Mar 2018 15:57:35 +0100 Subject: [PATCH 241/276] fix release crash because of setting sidebar header --- app/src/main/java/org/schabi/newpipe/MainActivity.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 573479ea7..1c62690c2 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -176,9 +176,11 @@ public class MainActivity extends AppCompatActivity { // when the user returns to MainActivity drawer.closeDrawer(Gravity.START, false); try { - String selectedServiceName = NewPipe.getService( - ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName(); - headerServiceView.setText(selectedServiceName); + if(BuildConfig.BUILD_TYPE != "release" ) { + String selectedServiceName = NewPipe.getService( + ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName(); + headerServiceView.setText(selectedServiceName); + } } catch (Exception e) { ErrorActivity.reportUiError(this, e); } From 6049a1f2f59ee623628e6eb3a8e0bcc2b49045eb Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 12 Mar 2018 16:18:03 +0100 Subject: [PATCH 242/276] fix playlist banner foo --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 814006051..88fe38183 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:b1130629bb' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:fce324d1bc74bc' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' From bdf044d264043539d55dab8647b4e161a8e934bf Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Mon, 12 Mar 2018 16:00:07 +0000 Subject: [PATCH 243/276] Translated using Weblate (Dutch) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-nl/strings.xml | 42 +++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4f8e5b0e5..07fa4d720 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -406,4 +406,44 @@ te openen in pop-upmodus Automatisch een gerealteerde stream toekennen als het afspelen van de laatste stream strat in een niet-herhalende afspeelwachtlijst. SYNCHRONISEREN - + Bestand + + Ongeldige map + Ongeldig bestand/Ongeldige inhoudsbron + Het bestand bestaat niet of u beschikt niet over voldoende machtiging om het te lezen/er naar te schrijven + De bestandsnaam mag niet leeg zijn + Er is een fout opgetreden: %1$s + + Importeren/Exporteren + Importeren + Importeren uit + Exporteren naar + + Bezig met importeren… + Bezig met exporteren… + + Bestand importeren + Vorige exportering + + Importeren van abonnementen is mislukt + Exporteren van abonnementen is mislukt + + Als u uw YouTube-abonnementen wilt importeren, dan heeft u het exportbestand nodig. Dit kan worden gedownload door het volgen van onderstaande stappen: +\n +\n1. Ga naar dit adres: %1$s +\n2. Log, indien nodig, in op uw account +\n3. De download met het exportbestand zou nu moeten starten + Als u uw SoundCloud-abonnementen wilt importeren, dan moet u uw profiel-URL of ID kennen. Als u hem kent, typ hem dan hieronder in. +\n +\nAls u hem niet kent, volg dan onderstaande stappen: +\n +\n1. Kies een webbrowser en schakel bureaubladmodus in (de website is niet beschikbaar voor mobiele apparaten) +\n2. Volg deze link: %1$s +\n3. Log, indien nodig, in op uw account +\n4. Kopieer de link van de pagina waar u op terechtkomt (dat is uw profiel-URL) + uwid, soundcloud.com/uwid + + Let op: deze actie kan veel MB\'s van uw netwerk gebruiken. +\n +\nWilt u doorgaan? + From 4aa2c1c2c21c088c644c1706e72385cb066d6cf2 Mon Sep 17 00:00:00 2001 From: cozyplanes Date: Mon, 12 Mar 2018 15:20:18 +0000 Subject: [PATCH 244/276] Translated using Weblate (Korean) Currently translated at 95.0% (326 of 343 strings) --- app/src/main/res/values-ko/strings.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index cd273af38..33b730e3e 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -78,7 +78,7 @@ 다운로드 메뉴를 설정할 수 없습니다 실시간 스트리밍 비디오는 아직 지원되지 않습니다. 어떠한 스트림도 가져올 수 없습니다 - 죄송합니다 + 죄송합니다. 오류가 발생했습니다. 이메일을 통해 오류 보고 죄송합니다. 오류가 발생했습니다. 보고 @@ -389,4 +389,8 @@ Out-of-Lifecycle 에러 보고 프래그먼트 또는 버려진 액티비티 주기 밖에서 일어나는 전달할 수 없는 Rx 예외를 강제적으로 보고하기 - +파일 + + 잘못된 디렉토리 + 잘못된 파일/콘덴츠 소스 + From 158f0aa2d971463d705312da1ffc9d75610d9081 Mon Sep 17 00:00:00 2001 From: Emanuele Petriglia Date: Mon, 12 Mar 2018 16:53:28 +0000 Subject: [PATCH 245/276] Translated using Weblate (Italian) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-it/strings.xml | 42 +++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9b294a9a7..8ad96dd3f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -411,4 +411,44 @@ Aggiungi automaticamente un flusso correlato mentre il playback parte dall\'ultimo flusso in una cosa non ripetitiva. SINCRONIZZAZIONE - + File + + Cartella invalida + Fonte del contenuto o file invalido + Il file non esiste o non si hanno i permessi sufficenti per leggerlo o scriverci + Il nome del file non può essere vuoto + Si è verificato un errore: %1$s + + Importa/Esporta + Importa + Importa da + Esporta a + + Importando… + Esportando… + + Importa file + Esportazione precedente + + L\'importazione delle iscrizioni è fallita + L\'esportazione delle iscrizioni è fallita + + Per importare le tue iscrizioni YouTube devi procurarti il file d\'esportazione, il quale può essere scaricato seguendo le seguenti istruzioni: +\n +\n1. Vai a questo URL: %2$s +\n2. Accedi al tuo account quando è richiedto +\n3. Un download dovrebbe essere partito (è il file d\'esportazione) + Per importare i tuoi seguiti di SoundCloud devi conoscere l\'URL del tuo profilo od il tuo ID. Se lo sai, ti basta scrivere uno dei due nell\'immisione in basso ed hai fatto. +\n +\nSe non lo sai, puoi seguire le seguenti istruzioni: +\n +\n1. Abilita la \"modalità desktop\" nel browser che usi (il sito non funziona nella modalità mobile) +\n2. Vai a questo URL: %2$s +\n3. Accedi al tuo account quando richiesto +\n4. Copia l\'URL a cui vieni indirizzato (è l\'URL del tuo profilo) + iltuoid, soundcloud.com/iltuoid + + Tieni in mente che questa operazione può richiedere un costo di connessione dati. +\n +\nVuoi continuare? + From 21e300b9f36efd37d13d9ae3ab850a77902c13ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Freddy=20Mor=C3=A1n=20Jr?= Date: Mon, 12 Mar 2018 20:18:00 +0000 Subject: [PATCH 246/276] Translated using Weblate (Spanish) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-es/strings.xml | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5499188a3..d00a6be85 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -410,4 +410,44 @@ abrir en modo popup Automáticamente añadir un vídeo relacionado cuando el reproductor llegue al último vídeo en una lista de reproducción no repetible. DIRECTO SINCRONIZAR +Archivo + + Directorio invalido + Fuente del archivo/contenido inválida + El archivo no existe o el permiso es insuficiente para leerlo o escribir en él + El nombre del archivo no puede estar vacío + Ocurrió un error: %1$s + + Importar/Exportar + Importar + Importar desde + Exportar a + + Importando… + Exportando… + + Importar archivo + Exportación anterior + + Importación de suscripciones fallida + Exportación de suscripciones fallida + + Para importar sus suscripciones de YouTube, necesitará el archivo de exportación, el cual puede ser descargado siguiendo estas instrucciones: +\n +\n1. Vaya a esta URL: %1$s +\n2. Ingrese a su cuenta cuando se le pida +\n3. Una descarga debería comenzar (ese es el archivo de exportación) + Para importar sus seguimientos de SoundCloud, debe conocer la URL o el ID de su perfil. Si es así, simplemente escriba cualquiera de ellos en la entrada de abajo y ya está listo para comenzar. +\n +\nSi no es así, puede seguir estos pasos: +\n +\n1. Active el \"modo escritorio\" en algún navegador (el sitio no está disponible para dispositivos móviles) +\n2. Vaya a esta URL: %1$s +\n3. Ingrese a su cuenta cuando se le pida +\n4. Copie la URL a la que fue redireccionado (esa es la URL de su perfil) + suID, soundcloud.com/suID + + Tenga en cuenta que esta operación puede ser costosa para la red. +\n +\n¿Desea continuar? From 00dee43a1e90ae0bd8246f187fa9a37697f0fb09 Mon Sep 17 00:00:00 2001 From: cozyplanes Date: Tue, 13 Mar 2018 11:23:54 +0000 Subject: [PATCH 247/276] Translated using Weblate (Korean) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-ko/strings.xml | 39 ++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 33b730e3e..60ee4e523 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -241,7 +241,7 @@ 번역, 디자인, 코딩 등 다양한 기여를 언제나 환영합니다. 향상에 참여해주세요! GitHub에서 보기 기부 - 뉴파이프는 자원봉사자들이 자발적으로 여가 시간을 활용해 개발하고 있습니다. 이제 이러한 노력에 보답할 시간입니다. + 뉴파이프는 자원봉사자들이 자발적으로 여가 시간을 활용해 개발하고 있습니다. 이제 이러한 노력에 보답할 시간입니다! 보답하기 웹사이트 뉴파이프에 관한 최신 및 상세 정보를 얻으려면 웹사이트를 방문하세요. @@ -393,4 +393,39 @@ 잘못된 디렉토리 잘못된 파일/콘덴츠 소스 - + 파일이 존재하지 않거나 읽기/쓰기 권환이 없습니다 + 파일 이름이 비어 있으면 안됩니다 + 오류 발생: %1$s + + 가져오기/내보내기 + 가져오기 + 이곳으로부터 가져오기 + 이곳으로 내보내기 + + 가져오는 중.… + 내보내는 중… + + 파일 가져오기 + 이전 내보내기 + + 구독 목록 가져오기 실패 + 구독 목록 내보내기 실패 + + YouTube 구독 목록을 가져오려면 내보내기 파일이 필요합니다. 다운로드 하려면 +\n1. 이곳으로 가세요: $1$s +\n2. 로그인이 필요하면 하세요 +\n3. 다운로드가 곧 시작 됩니다 (이 파일이 내보내기 파일 입니다) + SoundCloud 팔로잉 목록을 가져오려면 당신의 프로필 URL 및 ID를 알아야 합니다. 알고 있다면 아래에 있는 빈칸에 입력해 주세요. +\n +\n만약 모르신다면, 다음을 참고하세요: +\n +\n1. 모바일 환경이시면 브라우저 설정에서 데스크탑 모드를 활성화해주세요. Chrome 모바일에서는 오른쪽 ... 클릭시 아래쪽에 있습니다. +\n2. 이 주소로 가세요: %1$s +\n3. 로그인이 필요하면 하세요. +\n4. 리디렉트된 곳의 URL을 복사하세요. (이 URL이 당신의 프로필 URL 입니다) + 프로필ID, soundcloud.com/프로필ID + + 경고: 데이터 소모량이 늘어날 수 있습니다. +\n +\n진행하시겠습니까? + From b08728b64511ed3b3d7baa46389cbead3dd3a87a Mon Sep 17 00:00:00 2001 From: Eduardo Caron Date: Mon, 12 Mar 2018 17:30:41 +0000 Subject: [PATCH 248/276] Translated using Weblate (Portuguese (Brazil)) Currently translated at 98.8% (339 of 343 strings) --- app/src/main/res/values-pt-rBR/strings.xml | 42 +++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 96e6ccb70..ea22efa95 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -385,4 +385,44 @@ abrir em modo popup Anexar automaticamente uma stream relacionada quando a reprodução iniciar na última stream em uma fila não repetitiva Sincronizar - + Arquivo + + Diretório inválido + Origem do arquivo/conteúdo inválido + Arquivo não existe ou não há permissão para ler ou escrever nele + Nome do arquivo não pode ser vazio + Um erro ocorreu: %1$s + + Importar/Exportar + Importar + Importar de + Exportar para + + Importando… + Exportando… + + Importar arquivo + Exportação anteriore + + Importação de inscrições falhou + Exportação de inscrições falhou + + "Para importar inscrições do YouTube você vai precisar exportar o arquivo, o que pode ser baixado seguindo estas informações: +\n +\n1. Vá para este link: %1$s +\n2. Faça login na sua conta quando solicitado +\n3. O download deverá começar (isto é exportar arquivo)" + Para importar as contas que você segue no SoundCloud, você terá que saber o link ou id do seu perfil. Se você souber, basta escrever um deles no campo abaixo e estará tudo pronto. +\n +\nSe você não souber, você pode seguir estas etapas: +\n +\n1. Habilite \"modo desktop\" em algum navegador da internet ( o site não está disponível para dispositivos móveis) +\n2. Vá para esta url: %1$s +\n3. Faça login na sua conta quando solicitado +\n4. Copie o link no qual que você foi redirecionado (este é o link do seu perfil) + seuid, soundcloud.com/seuid + + Tenha em mente que esta operação poderá usar bastante a conexão com a internet. +\n +\nVocê deseja continuar? + From 24f2999669a8db7a92a69f1325836d160502ed90 Mon Sep 17 00:00:00 2001 From: alexandre patelli Date: Tue, 13 Mar 2018 22:57:59 +0100 Subject: [PATCH 249/276] Handling play/pause button from different headsets --- .../main/java/org/schabi/newpipe/player/BackgroundPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 3e9a63886..06b62f46f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -591,7 +591,7 @@ public final class BackgroundPlayer extends Service { pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT); } else if (keycode == KeyEvent.KEYCODE_MEDIA_PREVIOUS) { pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_HEADSETHOOK) { + } else if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PAUSE || keycode == KeyEvent.KEYCODE_MEDIA_PLAY) { pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT); } else if (keycode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT); From 7d047e612e2ff43bbead9c073ce1d9e4ae76b512 Mon Sep 17 00:00:00 2001 From: cozyplanes Date: Tue, 13 Mar 2018 11:24:50 +0000 Subject: [PATCH 250/276] Translated using Weblate (Korean) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-ko/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 60ee4e523..eaa97a2a5 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -215,7 +215,7 @@ 이 권한은 팝업 모드에서 \n열기 위해 필요합니다 - reCAPTCHA + 로봇인지 확인 (reCAPTCHA) reCAPTCHA Challenge 요청됨 다운로드 From 1ac7b2b8cbd44c0ffa5546a2522fee9f368e3d72 Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Tue, 13 Mar 2018 12:30:13 +0000 Subject: [PATCH 251/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 98.2% (337 of 343 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index a112b4830..f506d4949 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -387,4 +387,23 @@ 在非重複播放佇列中的最後一個串流上開始播放時,自動附上相關串流。 同步 + 檔案 + + 無效的目錄 + 無效的檔案/內容來源 + 檔案名稱不能留空 + 發生錯誤:%1$s + + 匯入/匯出 + 匯入 + 匯入來自 + 匯出到 + + 正在匯入… + 正在匯出… + + 匯入檔案 + 訂閱匯入失敗 + 訂閱匯出失敗 + From 2dbfc28d696a8f82d284fa5f7685c30c8b19a6ca Mon Sep 17 00:00:00 2001 From: Edwar Tikhonov Date: Wed, 14 Mar 2018 15:34:30 +0000 Subject: [PATCH 252/276] Translated using Weblate (Russian) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-ru/strings.xml | 154 ++++++++++++++++++++++--- 1 file changed, 136 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 58fb004c7..44ab1f984 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -58,7 +58,7 @@ Нажмите поиск, чтобы начать Подождите… Файл уже существует - Потоки + Темы OK Начать Пауза @@ -69,7 +69,7 @@ Ошибка Сервер не поддерживается NewPipe скачивает - Неправильный URL или Интернет не доступен + Неправильный URL или нет доступа к интернету Нажмите для деталей Скопировано в буфер обмена Выберите доступную папку для загрузки @@ -137,19 +137,19 @@ Обновить Очистить Использовать старый плеер - Жесты + Контроль жестов Всё Фильтр - Новая миссия + Новая цель Что:\\nЗапрос:\\nЯзык контента:\\nСервис:\\nВремя по Гринвичу:\\nПакет:\\nВерсия:\\nВерсия ОС:\\nГлобальный диапазон IP: Это разрешение нужно для -\nвоспроизведения видео в отдельном окне +\nвоспроизведения в окне reCAPTCHA Открыть в отдельном окне - Показывать подсказки во время поиска + Показывать подсказки в поиске Позже Отключено @@ -162,7 +162,7 @@  тыс. Разрешение в режиме всплывающего окна Запоминать последний размер и положение всплывающего окна - Живой поиск + Поисковые подсказки Лучшее разрешение Старый встроенный плеер на Mediaframework @@ -265,7 +265,7 @@ Выберите киоск Киоск - В тренде + Тренды Топ 50 Новое и горячее Добавлено в очередь в фоне @@ -275,22 +275,22 @@ Не удалось воспроизвести этот поток Подробности Настройки аудио - Пока нет подписок + Пока нет подписок на каналы Удалить Отписаться Подписка отменена - Подсказка о длинном нажатии - Отображать подсказку о длинном нажатии на кнопки \"В фоне\" и \"В окне\" для добавления в очередь + Показывать напоминание о длинном нажатии + Показывать подсказку при нажатии на иконку «В окне» или «В фоне» на странице сведений о видео [Неизвестно] Восстановление после ошибки проигрывателя - Воспроизведение в фоне - Воспроизведение в окне - Зажмите чтобы добавить в очередь - Добавить в очередь в фоне - Добавить в очередь в окне - Воспроизвести + В фоне + В окне + Зажмите, чтобы добавить в очередь + Добавить в очередь «В фоне» + Добавить в очередь «В окне» + Воспроизвести тут Воспроизвести в фоне Воспроизвести в окне Ни одного потокового проигрывателя не было найдено (вы можете установить VLC) @@ -326,5 +326,123 @@ Всегда спрашивать Получение информации… - Загрузка запрашиваемого контента + Загрузка запрошенного контента +Загрузка файла прямой трансляции. + Показать информацию + + Закладки + + Добавить к + + Использовать быстрый, но неточный поиск + Неточный поиск позволяет плееру искать позицию быстрее, но с пониженной точностью + Автоматическая очередь следующего стрима + Автоматически добавлять связанные потоки, когда воспроизведение начинается с последнего потока в неповторяющейся очереди воспроизведения. + Отладка + Файл + + Импорт данных + Экспорт данных + Ваша текущая история и подписки будут перезаписаны + Экспорт истории, подписок и плейлистов. + Неправильная директория + Неправильный файл/контент источника + Файл не существует или нет разрешения на его прочтение или запись + Имя файла не может быть пустым + Произошла ошибка: %1$s + + Перетащите, чтобы изменить порядок + + Создать + Удалить одно + Удалить всё + Отклонить + Переименовать + + Вы хотите удалить этот элемент из истории поиска? + Вы уверены, что хотите удалить все элементы из истории? + Последнее проигрывание + Наиболее проигрываемые + + Экспорт завершён + Импорт завершён + Нет верного Zip файла + Предупреждение: нет возможности импорта всех файлов. + Это перезапишет вашу текущую установку. + + Что-то будет тут, скоро ;D + + + Всегда спрашивать + + Создать новый плейлист + Удалить плейлист + Переименовать плейлист + Имя + Добавить в плейлист + Установить как иконку плейлиста + + Пометить плейлист + Удалить пометку + + Вы хотите удалить этот плейлист? + Плейлист успешно создан + Добавлено в плейлист + Иконка плейлиста изменена + Ошибка при удалении плейлиста + + Без подписи + + Уместить + Заполнить + Приближение + + Автоматически созданный + Размер шрифта подписи + Маленький шрифт + Обычный шрифт + Большой шрифт + + Синхронизировать + + Включить LeakCanary + Мониторинг утечки памяти может привести к зависанию приложения + + Ошибки отчёта вне очереди + Форсировать отчетность о недопустимых исключениях Rx, возникающих за пределами фрагмента или цикла деятельности, после размещения + + Импорт/Экспорт + Импорт + Импорт из + Экспорт в + + Импорт… + Экспорт… + + Импорт файла + Предыдущий экспорт + + Импорт подписок провален + Экспорт подписок провален + + Для импорта подписок из YouTube вам необходимо файл экспорта, которые можно загрузить в соответствии с этими инструкциями: +\n +\n1. Перейдите на: %1$s +\n2. Войдите в ваш аккаунт, если необходимо +\n3. Загрузка должна начаться (это файл экспорта) + "Для импорта ваших подписок из SoundCloud вы должны знать ссылку на ваш профиль или id. Если вы знаете, просто напишите это в поле ниже и будьте готовы начинать. +\n +\nЕсли вы не знаете, то проследуйте следующей инструкции: +\n +\n1. Включите \"режим рабочего стола\" в браузере (сайт недоступен на телефоне) +\n2. Пройдите на: %1$s +\n3. Войдите в аккаунт, если надо +\n4. Скопируйте адрес из адресной строки (это адрес вашего профиля) +\n +\n" + вашid, soundcloud.com/вашid + + Помните, что за выход в интернет может взиматься плата. +\n +\nВы хотите продолжить? From e49c4162e562703a4c4f670050bab0daea98d88e Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Wed, 14 Mar 2018 13:37:29 +0000 Subject: [PATCH 253/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 98.5% (338 of 343 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f506d4949..6f7ffcd26 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -406,4 +406,6 @@ 訂閱匯入失敗 訂閱匯出失敗 + 之前的匯出 + From d63c0a32ebffda5adf858a2a6fe4eba348d370c3 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Thu, 15 Mar 2018 14:19:45 +0000 Subject: [PATCH 254/276] Translated using Weblate (Hebrew) Currently translated at 75.8% (260 of 343 strings) --- app/src/main/res/values-he/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index cb69d6639..8cb0e4571 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -308,4 +308,8 @@ יצוא מסד נתונים נגנים חיצוניים לא תומכים בסוגי קישורים אלה כתובת שגויה + קובץ + + העברה לרקע + העברה לחלון צץ From 65726d75ccd4849ffd9dec9d81a6cb729d978177 Mon Sep 17 00:00:00 2001 From: Edwar Tikhonov Date: Wed, 14 Mar 2018 15:35:45 +0000 Subject: [PATCH 255/276] Translated using Weblate (Russian) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 44ab1f984..cd2737723 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -169,7 +169,7 @@ Запрос reCAPTCHA Запрошен ввод reCAPTCHA - Показывать более высокие разрешения + Показывать более высокое разрешение NewPipe в окне О NewPipe Настройки From 2d1bc6436a74433bcdf8305637dd2d00767e0b5c Mon Sep 17 00:00:00 2001 From: Olexandr Nesterenko Date: Wed, 14 Mar 2018 17:19:04 +0000 Subject: [PATCH 256/276] Translated using Weblate (Ukrainian) Currently translated at 98.8% (339 of 343 strings) --- app/src/main/res/values-uk/strings.xml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0608d8150..fa06ebd87 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -392,4 +392,26 @@ Автоматично додавати пов\'язаний стрим, під час початку програвання останнього стриму. СИНХРОНІЗАЦІЯ + Файл + + Неправильна тека + Неправильний файл/контент джерела + Файл не існує, або немає дозволу на його запис чи читання + Ім\'я файлу не повинно бути порожнім + Трапилась помилка: %1$s + + Імпорт/Експорт + Імпорт + Імпорт з + Експорт до + + Імпортування… + Експортування… + + Імпорт файлу + Попередній експорт + + Не вдалось імпортувати підписки + Не вдалося експортувати підписки + From 37ff4e9aeb1e115af708fea3a0aea3cd4dff885c Mon Sep 17 00:00:00 2001 From: E T Date: Fri, 16 Mar 2018 11:20:59 +0000 Subject: [PATCH 257/276] Translated using Weblate (Turkish) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-tr/strings.xml | 42 +++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 9fbb42e0b..a6e3c7a1b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -401,4 +401,44 @@ Yinelemeyen oynatma kuyruğundaki son akış başladığında ilişkili akışı kuyruğun sonuna kendiliğinden ekle. EŞZAMANLA - + Dosya + + Geçersiz dizin + Geçersiz dosya/içerik kaynağı + Dosya yok ya da okuma veya yazma izni yetersiz + Dosya adı boş olamaz + Hata oluştu: %1$s + + İçe/Dışa Aktar + İçe Aktar + Şuradan içe aktar + Şuna dışa aktar + + İçe aktarılıyor… + Dışa aktarılıyor… + + Dosyayı içe aktar + Önceki dışa aktarım + + Aboneliklerin içe aktarımı başarısız + Aboneliklerin dışa aktarımı başarısız + + YouTube aboneliklerinizi içe aktarmak için dışa aktarılmış dosya gerekiyor, dosya şu yönergeler izlenerek indirilebilir: +\n +\n1. Şu adrese gidin: %1$s +\n2. Sorulduğunda hesabınıza giriş yapın +\n3. İndirme başlamalı (bu dışa aktarılmış dosyadır) + SoundCloud takiplerinizi içe aktarmak için profil adresinizi veya kimliğinizi bilmelisiniz. Eğer biliyorsanız, ikisinden birini aşağıdaki giriye yazın ve işte hazırsınız. +\n +\nEğer bilmiyorsanız şu adımları izleyebilirsiniz: +\n +\n1. Herhangi bir tarayıcıda \"masaüstü kipi\"ni açın (site, mobil aygıtlar için uygun değildir) +\n2. Şu adrese gidin: %1$s +\n3. Sorulduğunda hesabınıza giriş yapın +\n4. Yönlendirildiğiniz adresi kopyalayın (bu sizin profil adresinizdir) + kimliginiz, soundcloud.com/kimliginiz + + Bu sürecin ağ masrafına nedenn olabileceğini unutmayın. +\n +\nDevam etmek istiyor musunuz? + From 579efa15c74652c490686707cd5e1f709f889d99 Mon Sep 17 00:00:00 2001 From: E T Date: Fri, 16 Mar 2018 11:21:22 +0000 Subject: [PATCH 258/276] Translated using Weblate (Turkish) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-tr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a6e3c7a1b..736bb12ae 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -438,7 +438,7 @@ \n4. Yönlendirildiğiniz adresi kopyalayın (bu sizin profil adresinizdir) kimliginiz, soundcloud.com/kimliginiz - Bu sürecin ağ masrafına nedenn olabileceğini unutmayın. + Bu sürecin ağ masrafına neden olabileceğini unutmayın. \n \nDevam etmek istiyor musunuz? From c092fc8e18f00997889f08d596c4ceeabf6ae31b Mon Sep 17 00:00:00 2001 From: AB Date: Sat, 17 Mar 2018 12:40:26 +0000 Subject: [PATCH 259/276] Translated using Weblate (Ukrainian) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-uk/strings.xml | 35 ++++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index fa06ebd87..ce1b835e5 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -384,7 +384,7 @@ Під час роботи LeakCanary застосунок може стати несприйнятливим під час гіп-дампінґу Зазвітувати Out-of-Lifecycle хиби - Примусове звітування про неможливість доставлення Rx винятків, яку відбуваються за межами фраґменту, або діяльності життєвого циклу після усунення + Примусове звітування про неможливість доставлення Rx винятків, які відбуваються за межами фраґменту або діяльності життєвого циклу після усунення Використовувати неточне шукання Неточне шукання дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю @@ -400,18 +400,37 @@ Ім\'я файлу не повинно бути порожнім Трапилась помилка: %1$s - Імпорт/Експорт - Імпорт - Імпорт з - Експорт до + Імпортування/Експортування + Імпортування + Імпортувати з + Експортувати до Імпортування… Експортування… - Імпорт файлу - Попередній експорт + Імпортування файлу + Попереднє експортування Не вдалось імпортувати підписки Не вдалося експортувати підписки - + Аби імпортувати ваші підписання Ютюб, вам буде потрібно експортувати файл, який можна буде завантажити наступним чином: +\n +\n1. Перейдіть за цією ланкою: %1$s +\n2. За запитом увійдіть до вашої обліківки +\n3. Завантаження має початися (експортований файл) + Для імпортування ваших підписок з SoundCloud, ви маєте знати url вашого профайлу або ID. Якщо ви знаєте їх, упишіть їх нижче та можна працювати. +\n +\nЯкщо ви не маєте їх, зробіть наступним чином: +\n +\n1. Увімкніть режимі \"desktop\" у будь-якому з переглядачів (сайт не має підтримки мобільних ґаджетів) +\n +\n2. Перейдіть за цією ланкою: %1$s +\n3. За запитом увійдіть до вашої обліківки +\n4. Скопіюйте url, до якого вас відішле (це й є url вашого профайлу) + yourid, soundcloud.com/yourid + + Майте на увазі: ця операція може потребувати багато трафіку. +\n +\nПродовжуватимете? + From 80593e774c059605478d4617de894f09cc3d5dc6 Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Sat, 17 Mar 2018 12:19:31 +0000 Subject: [PATCH 260/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 99.7% (342 of 343 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 6f7ffcd26..c32a6f272 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -408,4 +408,15 @@ 之前的匯出 - + 檔案不存在或沒有足夠的權限讀取或寫入 + 要匯入您的 YouTube 訂閱,您必須匯出檔案,可以按照以下說明進行下載: +\n +\n1. 轉到此網址:%1$s +\n2. 當被詢問時登入您的帳戶 +\n3. 下載應該開始 ( 這就是匯出的檔案 ) + yourid, soundcloud.com/yourid + + 請記住,此操作可能會造成網路昂貴花費。 +\n +\n您想繼續嗎? + From 1910e81ad945e29b3225b7d9869d008fb9dca668 Mon Sep 17 00:00:00 2001 From: AB Date: Sat, 17 Mar 2018 12:47:36 +0000 Subject: [PATCH 261/276] Translated using Weblate (Ukrainian) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-uk/strings.xml | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ce1b835e5..a032ec339 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -126,9 +126,9 @@ Будь ласка, оберіть теку для завантаження Потоковий програвач не знайдено (ви можете встановити VLC для відтворення) - Відкрити у виринальному режимі + Відкрити у віконному режимі Певні роздільності НЕ МАТИМУТЬ звуку якщо цей параметр увімкнено - NewPipe у виринальному вікні + NewPipe у віконному режимі Підписатися Ви підписалися Ви відписалися від каналу @@ -141,14 +141,14 @@ Новинки Тло - Виринальне вікно + У вікні - Типова роздільна здатність виринального вікна + Типова роздільна здатність вікна Не всі пристрої підтримують програвання 2K/4K відео Показувати більші роздільні здатності Типовий відео формат - Пам\'ятати розмір виринального вікна та положення - Пам\'ятати останній розмір та позицію виринального вікна + Пам\'ятати розмір та положення вікна + Пам\'ятати останній розмір та позицію вікна Керування жестами Використовувати жести для контролю яскравості та гучності програвача Шукати схожі @@ -165,10 +165,10 @@ Програвач Поведінка Історія - Виринальне вікно - Відворення у виринальному вікні + Вікно + Відворення у вікні Додано до фонового програвання - Додано до чергу у виринальному вікні + Додано до чергу у вікно Плейлист Фільтрувати Оновити @@ -181,12 +181,12 @@ Тільки тепер NewPipe сповіщення - Сповіщення для фонового та виринального програвача NewPipe + Сповіщення для фонового та віконного програвачів NewPipe [Невідомо] Перемкнутися до Тла - Перемкнутися до Виринального вікна + Перемкнутися до вікна Перемкнутися до Головної Імпортувати базу @@ -204,7 +204,7 @@ Без відео Помилкова ланка URL або інтернет не є доступним Цей дозвіл має бути відкритим -\nу виринальному вікні +\nу вікні «reCAPTCHA» Завантажити @@ -248,7 +248,7 @@ Додати до - Показувати підказку коли фонова чи виринальна кнопка натиснута на сторінці відео деталей + Показувати підказку коли натиснута кнопка фону або вікна, на сторінці інформації відео Перемкнути орієнтацію Фатальна помилка програвача Зовнішні програвачі не підтримують такі види ланок @@ -335,14 +335,14 @@ Ятка Набуває популярності Фоновий програвач - Виринальний програвач + Віконний програвач Усунути Затиснути, аби зняти з черги Зняти з черги у фоновому програвачеві - Зняти з черги у виринальному програвачеві + Зняти з черги у віконному програвачеві Розпочати програвання звідси Розпочати програвання звідси у фоновому програвачеві - Розпочати програвання звідси у виринальному програвачеві + Розпочати програвання у вікні звідси Відчинити шухляду Зачинити шухляду From 96a327af178d5bef5928caae63580a6e9029af64 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Sun, 18 Mar 2018 16:37:49 +0100 Subject: [PATCH 262/276] made frontend combatible to latest extractor refactorings --- app/build.gradle | 2 +- .../database/stream/model/StreamEntity.java | 8 ++-- .../newpipe/download/DownloadDialog.java | 2 +- .../fragments/detail/VideoDetailFragment.java | 11 +++-- .../fragments/list/BaseListInfoFragment.java | 20 ++++---- .../list/channel/ChannelFragment.java | 48 +++++++++---------- .../fragments/list/feed/FeedFragment.java | 6 +-- .../fragments/list/kiosk/KioskFragment.java | 4 +- .../list/playlist/PlaylistFragment.java | 18 +++++-- .../fragments/list/search/SearchFragment.java | 15 +++--- .../subscription/SubscriptionFragment.java | 8 +--- .../newpipe/info_list/InfoItemBuilder.java | 2 +- .../newpipe/info_list/InfoListAdapter.java | 2 +- .../holder/ChannelInfoItemHolder.java | 7 +-- .../holder/ChannelMiniInfoItemHolder.java | 18 +++---- .../holder/PlaylistMiniInfoItemHolder.java | 2 +- .../holder/StreamInfoItemHolder.java | 10 ++-- .../holder/StreamMiniInfoItemHolder.java | 20 ++++---- .../newpipe/player/BackgroundPlayer.java | 6 +-- .../org/schabi/newpipe/player/BasePlayer.java | 4 +- .../newpipe/player/ServicePlayerActivity.java | 2 +- .../schabi/newpipe/player/VideoPlayer.java | 6 +-- .../player/playback/MediaSourceManager.java | 6 +-- .../playlist/AbstractInfoPlayQueue.java | 22 ++++----- .../newpipe/playlist/ChannelPlayQueue.java | 5 +- .../newpipe/playlist/PlaylistPlayQueue.java | 5 +- .../schabi/newpipe/util/ExtractorHelper.java | 12 ++--- 27 files changed, 141 insertions(+), 130 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 88fe38183..9fa911e54 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:fce324d1bc74bc' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:f787b375e5fb6d' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index 2fddaa1bb..0a9a0bb66 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -71,14 +71,14 @@ public class StreamEntity implements Serializable { @Ignore public StreamEntity(final StreamInfoItem item) { - this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url, - item.uploader_name, item.duration); + this(item.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(), + item.getUploaderName(), item.getDuration()); } @Ignore public StreamEntity(final StreamInfo info) { - this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url, - info.uploader_name, info.duration); + this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(), + info.getUploaderName(), info.getDuration()); } @Ignore diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index b217b91b3..9bcd0bcb7 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -205,7 +205,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); switch (checkedId) { case R.id.audio_button: - setupAudioSpinner(currentInfo.audio_streams, streamsSpinner); + setupAudioSpinner(currentInfo.getAudioStreams(), streamsSpinner); break; case R.id.video_button: setupVideoSpinner(sortedStreamVideosList, streamsSpinner); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 4d935dbce..b3ca5f47f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -383,7 +383,8 @@ public class VideoDetailFragment } break; case R.id.detail_thumbnail_root_layout: - if (currentInfo.video_streams.isEmpty() && currentInfo.video_only_streams.isEmpty()) { + if (currentInfo.getVideoStreams().isEmpty() + && currentInfo.getVideoOnlyStreams().isEmpty()) { openBackgroundPlayer(false); } else { openVideoPlayer(); @@ -618,7 +619,8 @@ public class VideoDetailFragment relatedStreamRootLayout.setVisibility(View.VISIBLE); } else nextStreamTitle.setVisibility(View.GONE); - if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) { + if (info.getRelatedStreams() != null + && !info.getRelatedStreams().isEmpty() && showRelatedStreams) { //long first = System.nanoTime(), each; int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS ? INITIAL_RELATED_VIDEOS @@ -683,7 +685,7 @@ public class VideoDetailFragment switch (id) { case R.id.menu_item_share: { if(currentInfo != null) { - shareUrl(currentInfo.name, url); + shareUrl(currentInfo.getName(), url); } else { shareUrl(url, url); } @@ -1210,7 +1212,8 @@ public class VideoDetailFragment spinnerToolbar.setVisibility(View.GONE); break; default: - if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break; + if (!info.getVideoStreams().isEmpty() + || !info.getVideoOnlyStreams().isEmpty()) break; detailControlsBackground.setVisibility(View.GONE); detailControlsPopup.setVisibility(View.GONE); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index 34f190032..a132213bf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -20,7 +20,7 @@ import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; public abstract class BaseListInfoFragment - extends BaseListFragment { + extends BaseListFragment { @State protected int serviceId = Constants.NO_SERVICE_ID; @@ -117,7 +117,7 @@ public abstract class BaseListInfoFragment .subscribe((@NonNull I result) -> { isLoading.set(false); currentInfo = result; - currentNextPageUrl = result.next_streams_url; + currentNextPageUrl = result.getNextPageUrl(); handleResult(result); }, (@NonNull Throwable throwable) -> onError(throwable)); } @@ -126,7 +126,7 @@ public abstract class BaseListInfoFragment * Implement the logic to load more items
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper} */ - protected abstract Single loadMoreItemsLogic(); + protected abstract Single loadMoreItemsLogic(); protected void loadMoreItems() { isLoading.set(true); @@ -135,9 +135,9 @@ public abstract class BaseListInfoFragment currentWorker = loadMoreItemsLogic() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemPage InfoItemPage) -> { + .subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> { isLoading.set(false); - handleNextItems(InfoItemPage); + handleNextItems(InfoItemsPage); }, (@io.reactivex.annotations.NonNull Throwable throwable) -> { isLoading.set(false); onError(throwable); @@ -145,10 +145,10 @@ public abstract class BaseListInfoFragment } @Override - public void handleNextItems(ListExtractor.InfoItemPage result) { + public void handleNextItems(ListExtractor.InfoItemsPage result) { super.handleNextItems(result); - currentNextPageUrl = result.nextPageUrl; - infoListAdapter.addInfoItemList(result.infoItemList); + currentNextPageUrl = result.getNextPageUrl(); + infoListAdapter.addInfoItemList(result.getItems()); showListFooter(hasMoreItems()); } @@ -171,8 +171,8 @@ public abstract class BaseListInfoFragment setTitle(name); if (infoListAdapter.getItemsList().size() == 0) { - if (result.related_streams.size() > 0) { - infoListAdapter.addInfoItemList(result.related_streams); + if (result.getRelatedItems().size() > 0) { + infoListAdapter.addInfoItemList(result.getRelatedItems()); showListFooter(hasMoreItems()); } else { infoListAdapter.clearStreamItemList(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 3261e6dad..7783d8a98 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -27,10 +27,13 @@ import com.jakewharton.rxbinding2.view.RxView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.info_list.InfoItemDialog; @@ -44,6 +47,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -388,7 +392,7 @@ public class ChannelFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - protected Single loadMoreItemsLogic() { + protected Single loadMoreItemsLogic() { return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl); } @@ -415,8 +419,8 @@ public class ChannelFragment extends BaseListInfoFragment { super.handleResult(result); headerRootLayout.setVisibility(View.VISIBLE); - imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS); - imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS); + imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, DISPLAY_BANNER_OPTIONS); + imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, DISPLAY_AVATAR_OPTIONS); if (result.getSubscriberCount() != -1) { headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); @@ -427,8 +431,8 @@ public class ChannelFragment extends BaseListInfoFragment { playlistCtrl.setVisibility(View.VISIBLE); - if (!result.errors.isEmpty()) { - showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + if (!result.getErrors().isEmpty()) { + showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } if (disposables != null) disposables.clear(); @@ -436,24 +440,12 @@ public class ChannelFragment extends BaseListInfoFragment { updateSubscription(result); monitorSubscription(result); - headerPlayAllButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnMainPlayer(activity, getPlayQueue()); - } - }); - headerPopupButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()); - } - }); - headerBackgroundButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()); - } - }); + headerPlayAllButton.setOnClickListener( + view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + headerPopupButton.setOnClickListener( + view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + headerBackgroundButton.setOnClickListener( + view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); } private PlayQueue getPlayQueue() { @@ -461,17 +453,23 @@ public class ChannelFragment extends BaseListInfoFragment { } private PlayQueue getPlayQueue(final int index) { + final List streamItems = new ArrayList<>(); + for(InfoItem i : infoListAdapter.getItemsList()) { + if(i instanceof StreamInfoItem) { + streamItems.add((StreamInfoItem) i); + } + } return new ChannelPlayQueue( currentInfo.getServiceId(), currentInfo.getUrl(), currentInfo.getNextPageUrl(), - infoListAdapter.getItemsList(), + streamItems, index ); } @Override - public void handleNextItems(ListExtractor.InfoItemPage result) { + public void handleNextItems(ListExtractor.InfoItemsPage result) { super.handleNextItems(result); if (!result.getErrors().isEmpty()) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java index 57841cb87..dabfd9e1b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java @@ -297,12 +297,12 @@ public class FeedFragment extends BaseListFragment, Voi // Called only when response is non-empty @Override public void onSuccess(final ChannelInfo channelInfo) { - if (infoListAdapter == null || channelInfo.getRelatedStreams().isEmpty()) { + if (infoListAdapter == null || channelInfo.getRelatedItems().isEmpty()) { onDone(); return; } - final InfoItem item = channelInfo.getRelatedStreams().get(0); + final InfoItem item = channelInfo.getRelatedItems().get(0); // Keep requesting new items if the current one already exists boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item); if (!itemExists) { @@ -411,7 +411,7 @@ public class FeedFragment extends BaseListFragment, Voi private boolean doesItemExist(final List items, final InfoItem item) { for (final InfoItem existingItem : items) { - if (existingItem.info_type == item.info_type && + if (existingItem.getInfoType() == item.getInfoType() && existingItem.getServiceId() == item.getServiceId() && existingItem.getName().equals(item.getName()) && existingItem.getUrl().equals(item.getUrl())) return true; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java index 976bcced2..482f71cb4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java @@ -141,7 +141,7 @@ public class KioskFragment extends BaseListInfoFragment { } @Override - public Single loadMoreItemsLogic() { + public Single loadMoreItemsLogic() { String contentCountry = PreferenceManager .getDefaultSharedPreferences(activity) .getString(getString(R.string.content_country_key), @@ -174,7 +174,7 @@ public class KioskFragment extends BaseListInfoFragment { } @Override - public void handleNextItems(ListExtractor.InfoItemPage result) { + public void handleNextItems(ListExtractor.InfoItemsPage result) { super.handleNextItems(result); if (!result.getErrors().isEmpty()) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 324d3d7ef..9033560bd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -22,10 +22,12 @@ import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.local.RemotePlaylistManager; @@ -38,6 +40,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -206,7 +209,7 @@ public class PlaylistFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - protected Single loadMoreItemsLogic() { + protected Single loadMoreItemsLogic() { return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl); } @@ -269,7 +272,8 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistCtrl.setVisibility(View.VISIBLE); imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); - headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count)); + headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, + (int) result.getStreamCount(), (int) result.getStreamCount())); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); @@ -297,17 +301,23 @@ public class PlaylistFragment extends BaseListInfoFragment { } private PlayQueue getPlayQueue(final int index) { + final List infoItems = new ArrayList<>(); + for(InfoItem i : infoListAdapter.getItemsList()) { + if(i instanceof StreamInfoItem) { + infoItems.add((StreamInfoItem) i); + } + } return new PlaylistPlayQueue( currentInfo.getServiceId(), currentInfo.getUrl(), currentInfo.getNextPageUrl(), - infoListAdapter.getItemsList(), + infoItems, index ); } @Override - public void handleNextItems(ListExtractor.InfoItemPage result) { + public void handleNextItems(ListExtractor.InfoItemsPage result) { super.handleNextItems(result); if (!result.getErrors().isEmpty()) { 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 1ad31d06c..f7831e02d 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 @@ -71,7 +71,9 @@ import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class SearchFragment extends BaseListFragment implements BackPressable { +public class SearchFragment + extends BaseListFragment + implements BackPressable { /*////////////////////////////////////////////////////////////////////////// // Search @@ -759,12 +761,7 @@ public class SearchFragment extends BaseListFragment suggestions) { if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]"); suggestionsRecyclerView.smoothScrollToPosition(0); - suggestionsRecyclerView.post(new Runnable() { - @Override - public void run() { - suggestionListAdapter.setItems(suggestions); - } - }); + suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions)); if (errorPanelRoot.getVisibility() == View.VISIBLE) { hideLoading(); @@ -822,10 +819,10 @@ public class SearchFragment extends BaseListFragment items = new ArrayList<>(); for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem()); - Collections.sort(items, new Comparator() { - @Override - public int compare(InfoItem o1, InfoItem o2) { - return o1.name.compareToIgnoreCase(o2.name); - } - }); + Collections.sort(items, + (InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName())); return items; } 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 218895983..78867c81f 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 @@ -60,7 +60,7 @@ public class InfoItemBuilder { } public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) { - InfoItemHolder holder = holderFromInfoType(parent, infoItem.info_type, useMiniVariant); + InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant); holder.updateFromItem(infoItem); return holder.itemView; } 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 4b9914397..9b3405484 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 @@ -203,7 +203,7 @@ public class InfoListAdapter extends RecyclerView.Adapter= 0) { - String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), item.stream_count); + if (item.getStreamCount() >= 0) { + String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), + item.getStreamCount()); if (!details.isEmpty()) { details += " • " + formattedVideoAmount; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java index 48fb18517..211fa60cd 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java @@ -40,22 +40,22 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder { itemAdditionalDetailView.setText(getDetailLine(item)); itemBuilder.getImageLoader() - .displayImage(item.thumbnail_url, itemThumbnailView, ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + .displayImage(item.getThumbnailUrl(), + itemThumbnailView, + ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); - itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (itemBuilder.getOnChannelSelectedListener() != null) { - itemBuilder.getOnChannelSelectedListener().selected(item); - } + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnChannelSelectedListener() != null) { + itemBuilder.getOnChannelSelectedListener().selected(item); } }); } protected String getDetailLine(final ChannelInfoItem item) { String details = ""; - if (item.subscriber_count >= 0) { - details += Localization.shortSubscriberCount(itemBuilder.getContext(), item.subscriber_count); + if (item.getSubscriberCount() >= 0) { + details += Localization.shortSubscriberCount(itemBuilder.getContext(), + item.getSubscriberCount()); } return details; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index 50b551c61..30d84e1bd 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -40,7 +40,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { itemUploaderView.setText(item.getUploaderName()); itemBuilder.getImageLoader() - .displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + .displayImage(item.getThumbnailUrl(), itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnPlaylistSelectedListener() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java index 78954a2ee..0a7705427 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java @@ -51,14 +51,14 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder { private String getStreamInfoDetailLine(final StreamInfoItem infoItem) { String viewsAndDate = ""; - if (infoItem.view_count >= 0) { - viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.view_count); + if (infoItem.getViewCount() >= 0) { + viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount()); } - if (!TextUtils.isEmpty(infoItem.upload_date)) { + if (!TextUtils.isEmpty(infoItem.getUploadDate())) { if (viewsAndDate.isEmpty()) { - viewsAndDate = infoItem.upload_date; + viewsAndDate = infoItem.getUploadDate(); } else { - viewsAndDate += " • " + infoItem.upload_date; + viewsAndDate += " • " + infoItem.getUploadDate(); } } return viewsAndDate; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 594a85582..72c2830e1 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -41,15 +41,17 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { final StreamInfoItem item = (StreamInfoItem) infoItem; itemVideoTitleView.setText(item.getName()); - itemUploaderView.setText(item.uploader_name); + itemUploaderView.setText(item.getUploaderName()); - if (item.duration > 0) { - itemDurationView.setText(Localization.getDurationString(item.duration)); - itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); + if (item.getDuration() > 0) { + itemDurationView.setText(Localization.getDurationString(item.getDuration())); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - } else if (item.stream_type == StreamType.LIVE_STREAM) { + } else if (item.getStreamType() == StreamType.LIVE_STREAM) { itemDurationView.setText(R.string.duration_live); - itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color)); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + R.color.live_duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); } else { itemDurationView.setVisibility(View.GONE); @@ -57,7 +59,9 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { // Default thumbnail is shown on error, while loading and if the url is empty itemBuilder.getImageLoader() - .displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + .displayImage(item.getThumbnailUrl(), + itemThumbnailView, + StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnStreamSelectedListener() != null) { @@ -65,7 +69,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { } }); - switch (item.stream_type) { + switch (item.getStreamType()) { case AUDIO_STREAM: case VIDEO_STREAM: case LIVE_STREAM: diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 06b62f46f..61720c6b4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -407,10 +407,10 @@ public final class BackgroundPlayer extends Service { final MediaSource liveSource = super.sourceOf(item, info); if (liveSource != null) return liveSource; - final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams); - if (index < 0 || index >= info.audio_streams.size()) return null; + final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); + if (index < 0 || index >= info.getAudioStreams().size()) return null; - final AudioStream audio = info.audio_streams.get(index); + final AudioStream audio = info.getAudioStreams().get(index); return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), MediaFormat.getSuffixById(audio.getFormatId())); } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index d5ba7bb86..cee885e22 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -833,7 +833,7 @@ public abstract class BasePlayer implements // on metadata changed } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { - final long startPos = info != null ? info.start_position : C.TIME_UNSET; + final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; if (DEBUG) Log.d(TAG, "Rewinding to correct" + " window=[" + currentPlayQueueIndex + "]," + " at=[" + getTimeString((int)startPos) + "]," + @@ -950,7 +950,7 @@ public abstract class BasePlayer implements /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. * Also restart the track if the current track is the first in a queue.*/ if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { - final long startPos = currentInfo == null ? 0 : currentInfo.start_position; + final long startPos = currentInfo == null ? 0 : currentInfo.getStartPosition(); simpleExoPlayer.seekTo(startPos); } else { playQueue.offsetIndex(-1); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index d9c04b796..c68133094 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -579,7 +579,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity public void onMetadataUpdate(StreamInfo info) { if (info != null) { metadataTitle.setText(info.getName()); - metadataArtist.setText(info.uploader_name); + metadataArtist.setText(info.getUploaderName()); progressEndTime.setVisibility(View.GONE); progressLiveSync.setVisibility(View.GONE); diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index aa90b7b88..48b13654c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -354,10 +354,10 @@ public abstract class VideoPlayer extends BasePlayer break; case VIDEO_STREAM: - if (info.video_streams.size() + info.video_only_streams.size() == 0) break; + if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break; final List videos = ListHelper.getSortedStreamVideosList(context, - info.video_streams, info.video_only_streams, false); + info.getVideoStreams(), info.getVideoOnlyStreams(), false); availableStreams = new ArrayList<>(videos); if (playbackQuality == null) { selectedStreamIndex = getDefaultResolutionIndex(videos); @@ -388,7 +388,7 @@ public abstract class VideoPlayer extends BasePlayer // Create video stream source final List videos = ListHelper.getSortedStreamVideosList(context, - info.video_streams, info.video_only_streams, false); + info.getVideoStreams(), info.getVideoOnlyStreams(), false); final int index; if (videos.isEmpty()) { index = -1; diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index cb803dcd1..ea13a28e7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -417,9 +417,9 @@ public class MediaSourceManager { final Exception exception = new IllegalStateException( "Unable to resolve source from stream info." + " URL: " + stream.getUrl() + - ", audio count: " + streamInfo.audio_streams.size() + - ", video count: " + streamInfo.video_only_streams.size() + - streamInfo.video_streams.size()); + ", audio count: " + streamInfo.getAudioStreams().size() + + ", video count: " + streamInfo.getVideoOnlyStreams().size() + + streamInfo.getVideoStreams().size()); return new FailedMediaSource(stream, exception); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java index 6e63a3aaa..2b31cd340 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/AbstractInfoPlayQueue.java @@ -26,13 +26,13 @@ abstract class AbstractInfoPlayQueue ext transient Disposable fetchReactor; AbstractInfoPlayQueue(final U item) { - this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0); + this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0); } AbstractInfoPlayQueue(final int serviceId, final String url, final String nextPageUrl, - final List streams, + final List streams, final int index) { super(index, extractListItems(streams)); @@ -65,10 +65,10 @@ abstract class AbstractInfoPlayQueue ext @Override public void onSuccess(@NonNull T result) { isInitial = false; - if (!result.has_more_streams) isComplete = true; - nextUrl = result.next_streams_url; + if (!result.hasNextPage()) isComplete = true; + nextUrl = result.getNextPageUrl(); - append(extractListItems(result.related_streams)); + append(extractListItems(result.getRelatedItems())); fetchReactor.dispose(); fetchReactor = null; @@ -83,8 +83,8 @@ abstract class AbstractInfoPlayQueue ext }; } - SingleObserver getNextPageObserver() { - return new SingleObserver() { + SingleObserver getNextPageObserver() { + return new SingleObserver() { @Override public void onSubscribe(@NonNull Disposable d) { if (isComplete || isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) { @@ -95,11 +95,11 @@ abstract class AbstractInfoPlayQueue ext } @Override - public void onSuccess(@NonNull ListExtractor.InfoItemPage result) { + public void onSuccess(@NonNull ListExtractor.InfoItemsPage result) { if (!result.hasNextPage()) isComplete = true; - nextUrl = result.nextPageUrl; + nextUrl = result.getNextPageUrl(); - append(extractListItems(result.infoItemList)); + append(extractListItems(result.getItems())); fetchReactor.dispose(); fetchReactor = null; @@ -121,7 +121,7 @@ abstract class AbstractInfoPlayQueue ext fetchReactor = null; } - private static List extractListItems(final List infos) { + private static List extractListItems(final List infos) { List result = new ArrayList<>(); for (final InfoItem stream : infos) { if (stream instanceof StreamInfoItem) { diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java index a5ecad027..d37b84072 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.playlist; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.util.ExtractorHelper; import java.util.List; @@ -16,13 +17,13 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue streams, + final List streams, final int index) { super(serviceId, url, nextPageUrl, streams, index); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistPlayQueue.java index c9340afad..d9e1d2d2b 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlaylistPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlaylistPlayQueue.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.playlist; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.util.ExtractorHelper; import java.util.List; @@ -16,13 +17,13 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue streams, + final List streams, final int index) { super(serviceId, url, nextPageUrl, streams, index); } diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 12c3dc44c..1897589c6 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -29,7 +29,7 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.Info; -import org.schabi.newpipe.extractor.ListExtractor.InfoItemPage; +import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; @@ -78,7 +78,7 @@ public final class ExtractorHelper { ); } - public static Single getMoreSearchItems(final int serviceId, + public static Single getMoreSearchItems(final int serviceId, final String query, final int nextPageNumber, final String searchLanguage, @@ -86,7 +86,7 @@ public final class ExtractorHelper { checkServiceId(serviceId); return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter) .map((@NonNull SearchResult searchResult) -> - new InfoItemPage(searchResult.resultList, + new InfoItemsPage(searchResult.resultList, nextPageNumber + "", searchResult.errors)); } @@ -117,7 +117,7 @@ public final class ExtractorHelper { ChannelInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreChannelItems(final int serviceId, + public static Single getMoreChannelItems(final int serviceId, final String url, final String nextStreamsUrl) { checkServiceId(serviceId); @@ -133,7 +133,7 @@ public final class ExtractorHelper { PlaylistInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMorePlaylistItems(final int serviceId, + public static Single getMorePlaylistItems(final int serviceId, final String url, final String nextStreamsUrl) { checkServiceId(serviceId); @@ -149,7 +149,7 @@ public final class ExtractorHelper { KioskInfo.getInfo(NewPipe.getService(serviceId), url, contentCountry))); } - public static Single getMoreKioskItems(final int serviceId, + public static Single getMoreKioskItems(final int serviceId, final String url, final String nextStreamsUrl, final String contentCountry) { From a32273af914bfd82d5ba0e1f48aa8b6a6754ecb2 Mon Sep 17 00:00:00 2001 From: ezjerry liao Date: Mon, 19 Mar 2018 11:40:49 +0000 Subject: [PATCH 263/276] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (343 of 343 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c32a6f272..4517da74b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -419,4 +419,11 @@ 請記住,此操作可能會造成網路昂貴花費。 \n \n您想繼續嗎? - +要匯入您的 SoundCloud,您必須知道您的個人資料網址或 ID。 如果您這樣做,只需在下方的輸入中鍵入其中的任意一個,然後就可以開始了。 +\n +\n如果您不這樣做,您可以按照以下步驟操作: +\n1. 在一些瀏覽器中啟用「桌面模式」(該網站不適用於行動裝置) +\n2. 移至此網址:%1$s +\n3. 詢問時登入到您的帳號 +\n4. 複製網址您會被重新導向(這是您的個人資料網址) + From 12ce915e8ef3741759106edc57d6d20d07b0d7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 18 Mar 2018 13:50:16 +0000 Subject: [PATCH 264/276] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 93.2% (320 of 343 strings) --- app/src/main/res/values-nb-rNO/strings.xml | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index c214f5bd1..df5221bc4 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -382,4 +382,26 @@ Mindre skrift Normal skrift Større skrift - +Bruk raskt unøyaktig søk + Feilretting + Fil + + Ugyldig mappe + Ugyldig fil/innholdskilde + Filen finnes ikke eller så har du ikke tilgang til å lese eller skrive til den + Filnavn kan ikke være tomt + En feil inntraff: %1$s + + Auto-generert + Skru på LeakCanary + Importer + Importer fra + Eksporter til + + Importerer… + Eksporterer… + + Importer fil + Forrige eksport + + From 61b422502b4baab2c1a33693bfdcc0d0d7d2bad4 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 13 Mar 2018 20:25:22 -0700 Subject: [PATCH 265/276] -[#1060] Added toggle to disable thumbnail loading. -Added button to wipe metadata cache. -Added more paddings on player buttons. -Added new animations to main player secondary controls and play queue expand/collapse. -Refactored play queue item touch callback for use in all players. -Improved MediaSourceManager to better handle expired stream reloading. -[#1186] Changed live sync button text to "LIVE". -Removed MediaSourceManager loader coupling on main players. -Moved service dependent expiry resolution to ServiceHelper. -[#1186] Fixed livestream timeline updates causing negative time position. -[#1186] Fixed livestream not starting from live-edge. -Fixed main player system UI not retracting on playback start. --- .../org/schabi/newpipe/ImageDownloader.java | 23 +++- .../org/schabi/newpipe/player/BasePlayer.java | 81 ++++++++----- .../newpipe/player/MainVideoPlayer.java | 53 +++------ .../newpipe/player/ServicePlayerActivity.java | 40 +------ .../player/playback/MediaSourceManager.java | 111 ++++++++++-------- .../player/playback/PlaybackListener.java | 10 ++ .../playlist/PlayQueueItemTouchCallback.java | 52 ++++++++ .../settings/HistorySettingsFragment.java | 23 ++++ .../org/schabi/newpipe/util/InfoCache.java | 9 +- .../schabi/newpipe/util/ServiceHelper.java | 12 ++ .../activity_player_queue_control.xml | 6 +- .../main/res/layout/activity_main_player.xml | 16 ++- .../layout/activity_player_queue_control.xml | 6 +- app/src/main/res/layout/player_popup.xml | 6 +- app/src/main/res/values/settings_keys.xml | 4 + app/src/main/res/values/strings.xml | 21 ++-- app/src/main/res/xml/content_settings.xml | 6 + app/src/main/res/xml/history_settings.xml | 5 + 18 files changed, 305 insertions(+), 179 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java index 5ea067d00..8baabed6b 100644 --- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java +++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java @@ -1,25 +1,40 @@ package org.schabi.newpipe; import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import org.schabi.newpipe.extractor.NewPipe; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; public class ImageDownloader extends BaseImageDownloader { + private static final ByteArrayInputStream DUMMY_INPUT_STREAM = + new ByteArrayInputStream(new byte[]{}); + + private final SharedPreferences preferences; + private final String downloadThumbnailKey; + public ImageDownloader(Context context) { super(context); + this.preferences = PreferenceManager.getDefaultSharedPreferences(context); + this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key); } - public ImageDownloader(Context context, int connectTimeout, int readTimeout) { - super(context, connectTimeout, readTimeout); + private boolean isDownloadingThumbnail() { + return preferences.getBoolean(downloadThumbnailKey, true); } protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { - Downloader downloader = (Downloader) NewPipe.getDownloader(); - return downloader.stream(imageUri); + if (isDownloadingThumbnail()) { + final Downloader downloader = (Downloader) NewPipe.getDownloader(); + return downloader.stream(imageUri); + } else { + return DUMMY_INPUT_STREAM; + } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index cee885e22..5355e19ee 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -57,6 +57,7 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; @@ -244,6 +245,7 @@ public abstract class BasePlayer implements playQueue = queue; playQueue.init(); + if (playbackManager != null) playbackManager.dispose(); playbackManager = new MediaSourceManager(this, playQueue); if (playQueueAdapter != null) playQueueAdapter.dispose(); @@ -272,7 +274,6 @@ public abstract class BasePlayer implements public void destroy() { if (DEBUG) Log.d(TAG, "destroy() called"); destroyPlayer(); - clearThumbnailCache(); unregisterBroadcastReceiver(); trackSelector = null; @@ -314,11 +315,6 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "]"); } - - protected void clearThumbnailCache() { - ImageLoader.getInstance().clearMemoryCache(); - } - /*////////////////////////////////////////////////////////////////////////// // MediaSource Building //////////////////////////////////////////////////////////////////////////*/ @@ -448,7 +444,6 @@ public abstract class BasePlayer implements public void onPlaying() { if (DEBUG) Log.d(TAG, "onPlaying() called"); if (!isProgressLoopRunning()) startProgressLoop(); - if (!isCurrentWindowValid()) seekToDefault(); } public void onBuffering() {} @@ -522,11 +517,9 @@ public abstract class BasePlayer implements ); } - private Disposable getProgressReactor() { return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .filter(ignored -> isProgressLoopRunning()) .subscribe(ignored -> triggerProgressUpdate()); } @@ -541,16 +534,19 @@ public abstract class BasePlayer implements (manifest == null ? "no manifest" : "available manifest") + ", " + "timeline size = [" + timeline.getWindowCount() + "], " + "reason = [" + reason + "]"); + if (playQueue == null) return; switch (reason) { case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes - if (playQueue != null && playbackManager != null && - // ensures MediaSourceManager#update is complete - timeline.getWindowCount() == playQueue.size()) { - playbackManager.load(); + // ensures MediaSourceManager#update is complete + final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); + // Ensure dynamic/livestream timeline changes does not cause negative position + if (isPlaylistStable && !isCurrentWindowValid()) { + simpleExoPlayer.seekTo(/*clampToMillis=*/0); } + break; } } @@ -775,6 +771,16 @@ public abstract class BasePlayer implements // Playback Listener //////////////////////////////////////////////////////////////////////////*/ + @Override + public boolean isNearPlaybackEdge(final long timeToEndMillis) { + // If live, then not near playback edge + if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false; + + final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); + final long currentDurationMillis = simpleExoPlayer.getDuration(); + return currentDurationMillis - currentPositionMillis < timeToEndMillis; + } + @Override public void onPlaybackBlock() { if (simpleExoPlayer == null) return; @@ -796,7 +802,6 @@ public abstract class BasePlayer implements if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); simpleExoPlayer.prepare(mediaSource); - seekToDefault(); } @Override @@ -825,16 +830,24 @@ public abstract class BasePlayer implements if (simpleExoPlayer == null) return; final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { - Log.e(TAG, "Play Queue may be desynchronized: item " + + Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + "index=[" + currentPlayQueueIndex + "], " + "queue index=[" + playQueue.getIndex() + "]"); - // on metadata changed + // Check if bad seek position + } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex > currentPlaylistSize) || + currentPlaylistIndex < 0) { + Log.e(TAG, "Playback - Trying to seek to " + + "index=[" + currentPlayQueueIndex + "] with " + + "playlist length=[" + currentPlaylistSize + "]"); + + // If not playing correct stream, change window position } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; - if (DEBUG) Log.d(TAG, "Rewinding to correct" + + if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + " window=[" + currentPlayQueueIndex + "]," + " at=[" + getTimeString((int)startPos) + "]," + " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); @@ -858,6 +871,11 @@ public abstract class BasePlayer implements @Nullable @Override public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { + final StreamType streamType = info.getStreamType(); + if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { + return null; + } + if (!info.getHlsUrl().isEmpty()) { return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); } else if (!info.getDashMpdUrl().isEmpty()) { @@ -909,6 +927,9 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); if (playWhenReady) audioReactor.requestAudioFocus(); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + + // On live prepared + if (simpleExoPlayer.isCurrentWindowDynamic()) seekToDefault(); } public void onVideoPlayPause() { @@ -945,14 +966,15 @@ public abstract class BasePlayer implements if (simpleExoPlayer == null || playQueue == null) return; if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); - savePlaybackState(); - - /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. - * Also restart the track if the current track is the first in a queue.*/ - if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { - final long startPos = currentInfo == null ? 0 : currentInfo.getStartPosition(); - simpleExoPlayer.seekTo(startPos); + /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, + * restart current track. Also restart the track if the current track + * is the first in a queue.*/ + if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || + playQueue.getIndex() == 0) { + seekToDefault(); + playQueue.offsetIndex(0); } else { + savePlaybackState(); playQueue.offsetIndex(-1); } } @@ -962,7 +984,6 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "onPlayNext() called"); savePlaybackState(); - playQueue.offsetIndex(+1); } @@ -975,8 +996,9 @@ public abstract class BasePlayer implements if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { seekToDefault(); } else { - playQueue.setIndex(index); + savePlaybackState(); } + playQueue.setIndex(index); } public void seekBy(int milliSeconds) { @@ -1015,8 +1037,11 @@ public abstract class BasePlayer implements protected void reload() { if (playbackManager != null) { - playbackManager.reset(); - playbackManager.load(); + playbackManager.dispose(); + } + + if (playQueue != null) { + playbackManager = new MediaSourceManager(this, playQueue); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 4f27d1fee..dd7e0c71e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -62,6 +62,7 @@ import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemHolder; +import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -76,6 +77,8 @@ import java.util.UUID; import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; +import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; +import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; @@ -110,7 +113,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); setVolumeControlStream(AudioManager.STREAM_MUSIC); - changeSystemUi(); + hideSystemUi(); setContentView(R.layout.activity_main_player); playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); @@ -597,28 +600,27 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR updatePlaybackButtons(); getControlsRoot().setVisibility(View.INVISIBLE); - queueLayout.setVisibility(View.VISIBLE); + animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true, + DEFAULT_CONTROLS_DURATION); itemsList.scrollToPosition(playQueue.getIndex()); } private void onQueueClosed() { - queueLayout.setVisibility(View.GONE); + animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false, + DEFAULT_CONTROLS_DURATION); queueVisible = false; } private void onMoreOptionsClicked() { if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); - if (secondaryControls.getVisibility() == View.VISIBLE) { - moreOptionsButton.setImageDrawable(getResources().getDrawable( - R.drawable.ic_expand_more_white_24dp)); - animateView(secondaryControls, false, 200); - } else { - moreOptionsButton.setImageDrawable(getResources().getDrawable( - R.drawable.ic_expand_less_white_24dp)); - animateView(secondaryControls, true, 200); - } + final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; + + animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, + isMoreControlsVisible ? 0 : 180); + animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, + DEFAULT_CONTROLS_DURATION); showControls(DEFAULT_CONTROLS_DURATION); } @@ -696,7 +698,6 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR animatePlayButtons(true, 200); }); - changeSystemUi(); getRootView().setKeepScreenOn(true); } @@ -798,31 +799,11 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { + return new PlayQueueItemTouchCallback() { @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType()) { - return false; - } - - final int sourceIndex = source.getLayoutPosition(); - final int targetIndex = target.getLayoutPosition(); - playQueue.move(sourceIndex, targetIndex); - return true; + public void onMove(int sourceIndex, int targetIndex) { + if (playQueue != null) playQueue.move(sourceIndex, targetIndex); } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} }; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index c68133094..1c3ffe911 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -34,6 +34,7 @@ import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemHolder; +import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -61,9 +62,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; - private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10; - private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25; - private View rootView; private RecyclerView itemsList; @@ -398,43 +396,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { + return new PlayQueueItemTouchCallback() { @Override - public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, - int viewSizeOutOfBounds, int totalSize, - long msSinceStartScroll) { - final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, - viewSizeOutOfBounds, totalSize, msSinceStartScroll); - final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, - Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)); - return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); - } - - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, - RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType()) { - return false; - } - - final int sourceIndex = source.getLayoutPosition(); - final int targetIndex = target.getLayoutPosition(); + public void onMove(int sourceIndex, int targetIndex) { if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex); - return true; } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} }; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index ea13a28e7..50c069b40 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.MoveEvent; import org.schabi.newpipe.playlist.events.PlayQueueEvent; import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.ReorderEvent; +import org.schabi.newpipe.util.ServiceHelper; -import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -42,7 +42,7 @@ import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; public class MediaSourceManager { - @NonNull private final static String TAG = "MediaSourceManager"; + @NonNull private final String TAG = "MediaSourceManager@" + hashCode(); /** * Determines how many streams before and after the current stream should be loaded. @@ -60,17 +60,18 @@ public class MediaSourceManager { @NonNull private final PlayQueue playQueue; /** - * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing - * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure - * the {@link StreamInfo} used in subsequent playback is up-to-date. - *

- * Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to - * replace the expired one on whereupon {@link #loadImmediate()} is called. + * Determines the gap time between the playback position and the playback duration which + * the {@link #getEdgeIntervalSignal()} begins to request loading. * - * @see #loadImmediate() - * @see #isCorrectionNeeded(PlayQueueItem) + * @see #progressUpdateIntervalMillis * */ - private final long windowRefreshTimeMillis; + private final long playbackNearEndGapMillis; + /** + * Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between + * each request for loading, once {@link #playbackNearEndGapMillis} has reached. + * */ + private final long progressUpdateIntervalMillis; + @NonNull private final Observable nearEndIntervalSignal; /** * Process only the last load order when receiving a stream of load orders (lessens I/O). @@ -106,23 +107,31 @@ public class MediaSourceManager { public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this(listener, playQueue, - /*loadDebounceMillis=*/400L, - /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); + this(listener, playQueue, /*loadDebounceMillis=*/400L, + /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS), + /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS)); } private MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, final long loadDebounceMillis, - final long windowRefreshTimeMillis) { + final long playbackNearEndGapMillis, + final long progressUpdateIntervalMillis) { if (playQueue.getBroadcastReceiver() == null) { throw new IllegalArgumentException("Play Queue has not been initialized."); } + if (playbackNearEndGapMillis < progressUpdateIntervalMillis) { + throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis + + " ms] must be longer than update interval=[ " + progressUpdateIntervalMillis + + " ms] for them to be useful."); + } this.playbackListener = listener; this.playQueue = playQueue; - this.windowRefreshTimeMillis = windowRefreshTimeMillis; + this.playbackNearEndGapMillis = playbackNearEndGapMillis; + this.progressUpdateIntervalMillis = progressUpdateIntervalMillis; + this.nearEndIntervalSignal = getEdgeIntervalSignal(); this.loadDebounceMillis = loadDebounceMillis; this.debouncedSignal = PublishSubject.create(); @@ -161,28 +170,6 @@ public class MediaSourceManager { sources.releaseSource(); } - /** - * Loads the current playing stream and the streams within its windowSize bound. - * - * Unblocks the player once the item at the current index is loaded. - * */ - public void load() { - if (DEBUG) Log.d(TAG, "load() called."); - loadDebounced(); - } - - /** - * Blocks the player and repopulate the sources. - * - * Does not ensure the player is unblocked and should be done explicitly - * through {@link #load() load}. - * */ - public void reset() { - if (DEBUG) Log.d(TAG, "reset() called."); - - maybeBlock(); - populateSources(); - } /*////////////////////////////////////////////////////////////////////////// // Event Reactor //////////////////////////////////////////////////////////////////////////*/ @@ -219,11 +206,13 @@ public class MediaSourceManager { switch (event.type()) { case INIT: case ERROR: - reset(); - break; + maybeBlock(); case APPEND: populateSources(); break; + case SELECT: + maybeRenewCurrentIndex(); + break; case REMOVE: final RemoveEvent removeEvent = (RemoveEvent) event; remove(removeEvent.getRemoveIndex()); @@ -238,7 +227,6 @@ public class MediaSourceManager { final ReorderEvent reorderEvent = (ReorderEvent) event; move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); break; - case SELECT: case RECOVERY: default: break; @@ -347,8 +335,13 @@ public class MediaSourceManager { // MediaSource Loading //////////////////////////////////////////////////////////////////////////*/ + private Observable getEdgeIntervalSignal() { + return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) + .filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis)); + } + private Disposable getDebouncedLoader() { - return debouncedSignal + return debouncedSignal.mergeWith(nearEndIntervalSignal) .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(timestamp -> loadImmediate()); @@ -359,13 +352,14 @@ public class MediaSourceManager { } private void loadImmediate() { + if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); // The current item has higher priority final int currentIndex = playQueue.getIndex(); final PlayQueueItem currentItem = playQueue.getItem(currentIndex); if (currentItem == null) return; // Evict the items being loaded to free up memory - if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) { + if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) { loaderReactor.clear(); loadingItems.clear(); } @@ -377,7 +371,7 @@ public class MediaSourceManager { final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); final int rightLimit = currentIndex + WINDOW_SIZE + 1; final int rightBound = Math.min(playQueue.size(), rightLimit); - final List items = new ArrayList<>( + final Set items = new HashSet<>( playQueue.getStreams().subList(leftBound,rightBound)); // Do a round robin @@ -385,6 +379,7 @@ public class MediaSourceManager { if (excess >= 0) { items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); } + items.remove(currentItem); for (final PlayQueueItem item : items) { maybeLoadItem(item); @@ -405,9 +400,9 @@ public class MediaSourceManager { /* No exception handling since getLoadedMediaSource guarantees nonnull return */ .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); loaderReactor.add(loader); + } else { + maybeSynchronizePlayer(); } - - maybeSynchronizePlayer(); } private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { @@ -423,7 +418,8 @@ public class MediaSourceManager { return new FailedMediaSource(stream, exception); } - final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis; + final long expiration = System.currentTimeMillis() + + ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); return new LoadedMediaSource(source, stream, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); } @@ -467,6 +463,24 @@ public class MediaSourceManager { } } + /** + * Checks if the current playing index contains an expired {@link ManagedMediaSource}. + * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and + * {@link #loadImmediate()} is called to reload the current item. + * */ + private void maybeRenewCurrentIndex() { + final int currentIndex = playQueue.getIndex(); + if (sources.getSize() <= currentIndex) return; + + final ManagedMediaSource currentSource = + (ManagedMediaSource) sources.getMediaSource(currentIndex); + final PlayQueueItem currentItem = playQueue.getItem(); + if (!currentSource.canReplace(currentItem)) return; + + if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + + "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); + update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate); + } /*////////////////////////////////////////////////////////////////////////// // MediaSource Playlist Helpers //////////////////////////////////////////////////////////////////////////*/ @@ -476,6 +490,7 @@ public class MediaSourceManager { this.sources.releaseSource(); this.sources = new DynamicConcatenatingMediaSource(false, + // Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order new ShuffleOrder.UnshuffledShuffleOrder(0)); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index b37a269e2..34c7702bc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -11,6 +11,16 @@ import org.schabi.newpipe.playlist.PlayQueueItem; import java.util.List; public interface PlaybackListener { + + /** + * Called to check if the currently playing stream is close to the end of its playback. + * Implementation should return true when the current playback position is within + * timeToEndMillis or less until its playback completes or transitions. + * + * May be called at any time. + * */ + boolean isNearPlaybackEdge(final long timeToEndMillis); + /** * Called when the stream at the current queue index is not ready yet. * Signals to the listener to block the player from playing anything and notify the source diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java new file mode 100644 index 000000000..405dba11e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemTouchCallback.java @@ -0,0 +1,52 @@ +package org.schabi.newpipe.playlist; + +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; + +public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback { + private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10; + private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25; + + public PlayQueueItemTouchCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0); + } + + public abstract void onMove(final int sourceIndex, final int targetIndex); + + @Override + public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, + int viewSizeOutOfBounds, int totalSize, + long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, + viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, + Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)); + return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType()) { + return false; + } + + final int sourceIndex = source.getLayoutPosition(); + final int targetIndex = target.getLayoutPosition(); + onMove(sourceIndex, targetIndex); + return true; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java index e0836e06c..53e8d6fc4 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -1,12 +1,35 @@ package org.schabi.newpipe.settings; import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.preference.Preference; +import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.InfoCache; public class HistorySettingsFragment extends BasePreferenceFragment { + private String cacheWipeKey; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + cacheWipeKey = getString(R.string.metadata_cache_wipe_key); + } + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.history_settings); } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference.getKey().equals(cacheWipeKey)) { + InfoCache.getInstance().clearCache(); + Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice, + Toast.LENGTH_SHORT).show(); + } + + return super.onPreferenceTreeClick(preference); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java index 47c45e82a..ecc66bb40 100644 --- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -43,7 +43,6 @@ public final class InfoCache { * Trim the cache to this size */ private static final int TRIM_CACHE_TO = 30; - private static final int DEFAULT_TIMEOUT_HOURS = 4; private static final LruCache lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); @@ -66,13 +65,7 @@ public final class InfoCache { public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); - final long expirationMillis; - if (info.getServiceId() == SoundCloud.getServiceId()) { - expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES); - } else { - expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); - } - + final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId()); synchronized (lruCache) { final CacheData data = new CacheData(info, expirationMillis); lruCache.put(keyOf(serviceId, url), data); diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index 7d71750eb..9d71ae83a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -12,6 +12,10 @@ import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import java.util.concurrent.TimeUnit; + +import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; + public class ServiceHelper { private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; @@ -98,4 +102,12 @@ public class ServiceHelper { PreferenceManager.getDefaultSharedPreferences(context).edit(). putString(context.getString(R.string.current_service_key), serviceName).apply(); } + + public static long getCacheExpirationMillis(final int serviceId) { + if (serviceId == SoundCloud.getServiceId()) { + return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); + } else { + return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); + } + } } diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index c3480c547..11765f901 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -301,9 +301,13 @@ android:id="@+id/live_sync" android:layout_width="wrap_content" android:layout_height="match_parent" + android:paddingLeft="4dp" + android:paddingRight="4dp" android:gravity="center" - android:text="@string/live_sync" + android:text="@string/duration_live" + android:textAllCaps="true" android:textColor="?attr/colorAccent" + android:maxLength="4" android:background="?attr/selectableItemBackground" android:visibility="gone"/> diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index e7d337c17..8f608de3a 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -308,7 +308,7 @@ android:id="@+id/toggleOrientation" android:layout_width="30dp" android:layout_height="30dp" - android:layout_marginLeft="2dp" + android:layout_marginLeft="4dp" android:layout_marginRight="2dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" @@ -325,8 +325,8 @@ android:id="@+id/switchPopup" android:layout_width="30dp" android:layout_height="30dp" - android:layout_marginLeft="2dp" - android:layout_marginRight="2dp" + android:layout_marginLeft="4dp" + android:layout_marginRight="4dp" android:layout_toLeftOf="@id/toggleOrientation" android:layout_centerVertical="true" android:clickable="true" @@ -341,8 +341,8 @@ android:id="@+id/switchBackground" android:layout_width="30dp" android:layout_height="30dp" - android:layout_marginLeft="2dp" - android:layout_marginRight="2dp" + android:layout_marginLeft="4dp" + android:layout_marginRight="4dp" android:layout_toLeftOf="@id/switchPopup" android:layout_centerVertical="true" android:clickable="true" @@ -403,9 +403,13 @@ android:id="@+id/playbackLiveSync" android:layout_width="wrap_content" android:layout_height="match_parent" + android:paddingLeft="4dp" + android:paddingRight="4dp" android:gravity="center" - android:text="@string/live_sync" + android:text="@string/duration_live" + android:textAllCaps="true" android:textColor="@android:color/white" + android:maxLength="4" android:visibility="gone" android:background="?attr/selectableItemBackground" tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml index 639a8037c..7f649e382 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -151,9 +151,13 @@ android:id="@+id/live_sync" android:layout_width="wrap_content" android:layout_height="match_parent" + android:paddingLeft="4dp" + android:paddingRight="4dp" android:gravity="center" - android:text="@string/live_sync" + android:text="@string/duration_live" + android:textAllCaps="true" android:textColor="?attr/colorAccent" + android:maxLength="4" android:background="?attr/selectableItemBackground" android:visibility="gone"/> diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml index 9bbd72fec..0c3ea77df 100644 --- a/app/src/main/res/layout/player_popup.xml +++ b/app/src/main/res/layout/player_popup.xml @@ -195,9 +195,13 @@ android:id="@+id/playbackLiveSync" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingLeft="4dp" + android:paddingRight="4dp" android:gravity="center_vertical" - android:text="@string/live_sync" + android:text="@string/duration_live" + android:textAllCaps="true" android:textColor="@android:color/white" + android:maxLength="4" android:visibility="gone" android:background="?attr/selectableItemBackground" tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index a897aa185..68d75737a 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -160,6 +160,10 @@ import_data export_data + download_thumbnail_key + + cache_wipe_key + file_rename file_replacement_character diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c97f12809..e1a353807 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,6 +74,11 @@ Remember last size and position of popup Use fast inexact seek Inexact seek allows the player to seek to positions faster with reduced precision + Load thumbnails + Disable to stop all non-cached thumbnail from loading and save on data and memory usage + Wipe cached metadata + Remove all cached webpage data + Metadata cache wiped Auto-queue next stream Automatically append a related stream when playback starts on the last stream in a non-repeating play queue. Player gesture controls @@ -89,7 +94,7 @@ Download Next video Show next and similar videos - Show Hold to Append Tip + Show hold to append tip Show tip when background or popup button is pressed on video details page URL not supported Default content country @@ -98,7 +103,7 @@ Player Behavior Video & Audio - History + History & Cache Popup Appearance Other @@ -418,18 +423,16 @@ ZOOM Auto-generated - Caption Font Size - Smaller Font - Normal Font - Larger Font - - SYNC + Caption font size + Smaller font + Normal font + Larger font Enable LeakCanary Memory leak monitoring may cause app to become unresponsive when heap dumping - Report Out-of-Lifecycle Errors + Report Out-of-lifecycle errors Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index c8c1efb12..2ce8bf9e6 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -37,6 +37,12 @@ android:summary="@string/auto_queue_summary" android:title="@string/auto_queue_title"/> + + + + From a5f9927459f48cdd4f57a648dd1247a85f978efb Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 13 Mar 2018 20:48:26 -0700 Subject: [PATCH 266/276] -Fixed main player animations not working on first call. --- app/src/main/res/layout/activity_main_player.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 8f608de3a..c581c3203 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -52,7 +52,7 @@ android:id="@+id/playQueuePanel" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="gone" + android:visibility="invisible" android:background="?attr/queue_background_color" tools:visibility="visible"> @@ -254,7 +254,7 @@ android:focusable="true" android:scaleType="fitXY" android:src="@drawable/ic_expand_more_white_24dp" - android:background="?attr/selectableItemBackground" + android:background="?attr/selectableItemBackgroundBorderless" tools:ignore="ContentDescription,RtlHardcoded"/> @@ -266,7 +266,7 @@ android:gravity="top" android:paddingLeft="5dp" android:paddingRight="5dp" - android:visibility="gone" + android:visibility="invisible" tools:ignore="RtlHardcoded" tools:visibility="visible"> From 2fa9aa04f4f6025b2b38306680a6b99d0ab999b4 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Tue, 13 Mar 2018 21:45:44 -0700 Subject: [PATCH 267/276] -Bump support library and multidex version. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9fa911e54..3529a37b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,7 +48,7 @@ android { } ext { - supportLibVersion = '27.0.2' + supportLibVersion = '27.1.0' } dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { @@ -77,7 +77,7 @@ dependencies { debugImplementation 'com.facebook.stetho:stetho:1.5.0' debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' - debugImplementation 'com.android.support:multidex:1.0.2' + debugImplementation 'com.android.support:multidex:1.0.3' implementation 'io.reactivex.rxjava2:rxjava:2.1.7' implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' From 0258726f0a4ffed04b7255389c104b3b93d6e31c Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 15 Mar 2018 20:07:20 -0700 Subject: [PATCH 268/276] -Changed thumbnail toggle in disabled mode to load dark dummy image. -Changed play queue items to display service names. -Fixed Soundcloud playlist not fitting thumbnail. -Refactored image display options to follow uniform behavior. -Refactoring and style changes on audio reactor and media button receiver. --- .../java/org/schabi/newpipe/BaseFragment.java | 33 --------- .../org/schabi/newpipe/ImageDownloader.java | 22 +++--- .../fragments/detail/VideoDetailFragment.java | 7 +- .../list/channel/ChannelFragment.java | 7 +- .../list/playlist/PlaylistFragment.java | 4 +- .../fragments/local/LocalItemBuilder.java | 2 - .../local/holder/LocalItemHolder.java | 21 ------ .../local/holder/LocalPlaylistItemHolder.java | 9 +-- .../holder/LocalPlaylistStreamItemHolder.java | 19 +---- .../LocalStatisticStreamItemHolder.java | 17 +---- .../local/holder/PlaylistItemHolder.java | 13 ---- .../holder/RemotePlaylistItemHolder.java | 3 +- .../newpipe/history/WatchHistoryFragment.java | 3 +- .../holder/ChannelMiniInfoItemHolder.java | 17 +---- .../info_list/holder/InfoItemHolder.java | 14 ---- .../holder/PlaylistMiniInfoItemHolder.java | 17 +---- .../holder/StreamMiniInfoItemHolder.java | 16 +---- .../newpipe/player/BackgroundPlayer.java | 70 +++++++++++-------- .../org/schabi/newpipe/player/BasePlayer.java | 10 +-- .../newpipe/player/helper/AudioReactor.java | 44 ++++++------ .../playlist/PlayQueueItemBuilder.java | 41 ++--------- .../settings/ContentSettingsFragment.java | 25 +++++++ .../newpipe/util/ImageDisplayConstants.java | 58 +++++++++++++++ .../main/res/layout/list_playlist_item.xml | 2 +- app/src/main/res/values/strings.xml | 3 +- 25 files changed, 206 insertions(+), 271 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 8967d8cb0..ce4318427 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -8,9 +8,7 @@ import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; -import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; import com.squareup.leakcanary.RefWatcher; import icepick.Icepick; @@ -94,35 +92,4 @@ public abstract class BaseFragment extends Fragment { activity.getSupportActionBar().setTitle(title); } } - - /*////////////////////////////////////////////////////////////////////////// - // DisplayImageOptions default configurations - //////////////////////////////////////////////////////////////////////////*/ - - public static final DisplayImageOptions BASE_OPTIONS = - new DisplayImageOptions.Builder().cacheInMemory(true).build(); - - public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_OPTIONS) - .showImageOnLoading(R.drawable.buddy) - .showImageForEmptyUri(R.drawable.buddy) - .showImageOnFail(R.drawable.buddy) - .build(); - - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_OPTIONS) - .displayer(new FadeInBitmapDisplayer(250)) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnFail(R.drawable.dummy_thumbnail) - .build(); - - public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_OPTIONS) - .showImageOnLoading(R.drawable.channel_banner) - .showImageForEmptyUri(R.drawable.channel_banner) - .showImageOnFail(R.drawable.channel_banner) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java index 8baabed6b..eb5e92e88 100644 --- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java +++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java @@ -1,26 +1,26 @@ package org.schabi.newpipe; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; import android.preference.PreferenceManager; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import org.schabi.newpipe.extractor.NewPipe; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; public class ImageDownloader extends BaseImageDownloader { - private static final ByteArrayInputStream DUMMY_INPUT_STREAM = - new ByteArrayInputStream(new byte[]{}); - + private final Resources resources; private final SharedPreferences preferences; private final String downloadThumbnailKey; public ImageDownloader(Context context) { super(context); + this.resources = context.getResources(); this.preferences = PreferenceManager.getDefaultSharedPreferences(context); this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key); } @@ -29,12 +29,18 @@ public class ImageDownloader extends BaseImageDownloader { return preferences.getBoolean(downloadThumbnailKey, true); } - protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { + @SuppressLint("ResourceType") + @Override + public InputStream getStream(String imageUri, Object extra) throws IOException { if (isDownloadingThumbnail()) { - final Downloader downloader = (Downloader) NewPipe.getDownloader(); - return downloader.stream(imageUri); + return super.getStream(imageUri, extra); } else { - return DUMMY_INPUT_STREAM; + return resources.openRawResource(R.drawable.dummy_thumbnail_dark); } } + + protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { + final Downloader downloader = (Downloader) NewPipe.getDownloader(); + return downloader.stream(imageUri); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index b3ca5f47f..2a95125df 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -73,6 +73,7 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; @@ -587,7 +588,8 @@ public class VideoDetailFragment imageLoader.displayImage( info.getThumbnailUrl(), thumbnailImageView, - DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() { + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, + new SimpleImageLoadingListener() { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { ErrorActivity.reportError( @@ -604,7 +606,8 @@ public class VideoDetailFragment } if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { - imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS); + imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 7783d8a98..dbc61961e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -44,6 +44,7 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.subscription.SubscriptionService; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; @@ -419,8 +420,10 @@ public class ChannelFragment extends BaseListInfoFragment { super.handleResult(result); headerRootLayout.setVisibility(View.VISIBLE); - imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, DISPLAY_BANNER_OPTIONS); - imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, DISPLAY_AVATAR_OPTIONS); + imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, + ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); + imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); if (result.getSubscriberCount() != -1) { headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 9033560bd..3bcf9d322 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -37,6 +37,7 @@ import org.schabi.newpipe.playlist.PlaylistPlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -271,7 +272,8 @@ public class PlaylistFragment extends BaseListInfoFragment { playlistCtrl.setVisibility(View.VISIBLE); - imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); + imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.getStreamCount(), (int) result.getStreamCount())); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java index 4794def97..5dc6c17a4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -1,12 +1,10 @@ package org.schabi.newpipe.fragments.local; import android.content.Context; -import android.graphics.Bitmap; import android.widget.ImageView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.util.OnClickGesture; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java index e4087d8a8..2dffdbfdb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java @@ -1,14 +1,8 @@ package org.schabi.newpipe.fragments.local.holder; -import android.graphics.Bitmap; -import android.support.annotation.DimenRes; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; -import android.widget.ImageView; - -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.fragments.local.LocalItemBuilder; @@ -45,19 +39,4 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder { } public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); - - /*////////////////////////////////////////////////////////////////////////// - // ImageLoaderOptions - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Base display options - */ - public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = - new DisplayImageOptions.Builder() - .cacheInMemory(true) - .cacheOnDisk(true) - .bitmapConfig(Bitmap.Config.RGB_565) - .resetViewBeforeLoading(false) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java index 1fbea6cc4..d9eb7caa5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java @@ -2,15 +2,11 @@ package org.schabi.newpipe.fragments.local.holder; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - -import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import java.text.DateFormat; @@ -29,7 +25,8 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { itemStreamCountView.setText(String.valueOf(item.streamCount)); itemUploaderView.setVisibility(View.INVISIBLE); - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); super.updateFromItem(localItem, dateFormat); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java index 0696f5f61..5f9555d9f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.fragments.local.holder; -import android.graphics.Bitmap; import android.support.v4.content.ContextCompat; import android.view.MotionEvent; import android.view.View; @@ -8,14 +7,12 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.assist.ImageScaleType; - import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; @@ -61,7 +58,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { @@ -92,15 +90,4 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { return false; }; } - - /** - * Display options for stream thumbnails - */ - private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java index cd0630b37..199158672 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java @@ -6,13 +6,12 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; @@ -84,7 +83,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { @@ -100,15 +100,4 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { return true; }); } - - /** - * Display options for stream thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java index bab76ddcb..57bc2a3cb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/PlaylistItemHolder.java @@ -4,8 +4,6 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.fragments.local.LocalItemBuilder; @@ -48,15 +46,4 @@ public abstract class PlaylistItemHolder extends LocalItemHolder { return true; }); } - - /** - * Display options for playlist thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) - .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) - .showImageOnFail(R.drawable.dummy_thumbnail_playlist) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java index 0f7b00e6d..871138464 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/RemotePlaylistItemHolder.java @@ -6,6 +6,7 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; @@ -26,7 +27,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { NewPipe.getNameOfService(item.getServiceId()))); itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, - DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); super.updateFromItem(localItem, dateFormat); } diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java index 4830ed33b..4fe2b701d 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchHistoryFragment.java @@ -20,6 +20,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.R; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; @@ -147,7 +148,7 @@ public class WatchHistoryFragment extends HistoryFragment { holder.uploader.setText(entry.uploader); holder.duration.setText(Localization.getDurationString(entry.duration)); ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, - StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java index 211fa60cd..643886da8 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java @@ -1,15 +1,13 @@ package org.schabi.newpipe.info_list.holder; -import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import de.hdodenhof.circleimageview.CircleImageView; @@ -42,7 +40,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder { itemBuilder.getImageLoader() .displayImage(item.getThumbnailUrl(), itemThumbnailView, - ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnChannelSelectedListener() != null) { @@ -59,15 +57,4 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder { } return details; } - - /** - * Display options for channel thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.buddy_channel_item) - .showImageForEmptyUri(R.drawable.buddy_channel_item) - .showImageOnFail(R.drawable.buddy_channel_item) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java index fb5aa2b7c..ebb5b4114 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java @@ -4,8 +4,6 @@ import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; @@ -38,16 +36,4 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder { } public abstract void updateFromItem(final InfoItem infoItem); - - /*////////////////////////////////////////////////////////////////////////// - // ImageLoaderOptions - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Base display options - */ - public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = - new DisplayImageOptions.Builder() - .cacheInMemory(true) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index 30d84e1bd..b6bd2f389 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -4,12 +4,11 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; public class PlaylistMiniInfoItemHolder extends InfoItemHolder { public final ImageView itemThumbnailView; @@ -40,7 +39,8 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { itemUploaderView.setText(item.getUploaderName()); itemBuilder.getImageLoader() - .displayImage(item.getThumbnailUrl(), itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + .displayImage(item.getThumbnailUrl(), itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnPlaylistSelectedListener() != null) { @@ -56,15 +56,4 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { return true; }); } - - /** - * Display options for playlist thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) - .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) - .showImageOnFail(R.drawable.dummy_thumbnail_playlist) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 72c2830e1..048b907af 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -6,13 +6,12 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; - import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; public class StreamMiniInfoItemHolder extends InfoItemHolder { @@ -61,7 +60,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { itemBuilder.getImageLoader() .displayImage(item.getThumbnailUrl(), itemThumbnailView, - StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnStreamSelectedListener() != null) { @@ -98,15 +97,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { itemView.setLongClickable(false); itemView.setOnLongClickListener(null); } - - /** - * Display options for stream thumbnails - */ - public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 61720c6b4..83ed54cf5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -121,7 +121,7 @@ public final class BackgroundPlayer extends Service { shouldUpdateOnProgress = true; mReceiverComponent = new ComponentName(this, MediaButtonReceiver.class); - basePlayerImpl.audioReactor.registerMediaButtonEventReceiver(mReceiverComponent); + basePlayerImpl.getAudioReactor().registerMediaButtonEventReceiver(mReceiverComponent); } @Override @@ -152,7 +152,7 @@ public final class BackgroundPlayer extends Service { lockManager.releaseWifiAndCpu(); } if (basePlayerImpl != null) { - basePlayerImpl.audioReactor.unregisterMediaButtonEventReceiver(mReceiverComponent); + basePlayerImpl.getAudioReactor().unregisterMediaButtonEventReceiver(mReceiverComponent); basePlayerImpl.stopActivityBinding(); basePlayerImpl.destroy(); } @@ -575,38 +575,46 @@ public final class BackgroundPlayer extends Service { } public static class MediaButtonReceiver extends BroadcastReceiver { - - public MediaButtonReceiver() { - super(); - } - @Override public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { - KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - if (event.getAction() == KeyEvent.ACTION_UP) { - int keycode = event.getKeyCode(); - PendingIntent pendingIntent = null; - if (keycode == KeyEvent.KEYCODE_MEDIA_NEXT) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_MEDIA_PREVIOUS) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PAUSE || keycode == KeyEvent.KEYCODE_MEDIA_PLAY) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT); - } else if (keycode == KeyEvent.KEYCODE_MEDIA_REWIND) { - pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); - } - if (pendingIntent != null) { - try { - pendingIntent.send(); - } catch (Exception e) { - Log.e(TAG, "Error Sending intent MediaButtonReceiver", e); - } - } + if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) return; + final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (event.getAction() != KeyEvent.ACTION_UP) return; + final int keycode = event.getKeyCode(); - } + final PendingIntent pendingIntent; + switch (keycode) { + case KeyEvent.KEYCODE_MEDIA_NEXT: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT); + break; + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT); + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT); + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, + new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); + break; + default: + pendingIntent = null; + } + + if (pendingIntent == null) return; + try { + pendingIntent.send(); + } catch (Exception e) { + Log.e(TAG, "Error Sending intent MediaButtonReceiver", e); } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 5355e19ee..5ec61b058 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -838,9 +838,9 @@ public abstract class BasePlayer implements "queue index=[" + playQueue.getIndex() + "]"); // Check if bad seek position - } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex > currentPlaylistSize) || - currentPlaylistIndex < 0) { - Log.e(TAG, "Playback - Trying to seek to " + + } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) || + currentPlayQueueIndex < 0) { + Log.e(TAG, "Playback - Trying to seek to invalid " + "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); @@ -848,9 +848,9 @@ public abstract class BasePlayer implements } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + - " window=[" + currentPlayQueueIndex + "]," + + " index=[" + currentPlayQueueIndex + "]," + " at=[" + getTimeString((int)startPos) + "]," + - " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); + " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index df30c3e79..c1896599f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -22,6 +22,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au private static final String TAG = "AudioFocusReactor"; + private static final boolean SHOULD_BUILD_FOCUS_REQUEST = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + + private static final boolean CAN_USE_MEDIA_BUTTONS = + Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1; + private static final String MEDIA_BUTTON_DEPRECATED_ERROR = + "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."; + private static final int DUCK_DURATION = 1500; private static final float DUCK_AUDIO_TO = .2f; @@ -38,9 +46,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au this.player = player; this.context = context; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - player.setAudioDebugListener(this); + player.addAudioDebugListener(this); - if (shouldBuildFocusRequest()) { + if (SHOULD_BUILD_FOCUS_REQUEST) { request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) .setAcceptsDelayedFocusGain(true) .setWillPauseWhenDucked(true) @@ -56,7 +64,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au //////////////////////////////////////////////////////////////////////////*/ public void requestAudioFocus() { - if (shouldBuildFocusRequest()) { + if (SHOULD_BUILD_FOCUS_REQUEST) { audioManager.requestAudioFocus(request); } else { audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE); @@ -64,7 +72,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au } public void abandonAudioFocus() { - if (shouldBuildFocusRequest()) { + if (SHOULD_BUILD_FOCUS_REQUEST) { audioManager.abandonAudioFocusRequest(request); } else { audioManager.abandonAudioFocus(this); @@ -83,24 +91,20 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au audioManager.setStreamVolume(STREAM_TYPE, volume, 0); } - private boolean shouldBuildFocusRequest() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - } - public void registerMediaButtonEventReceiver(ComponentName componentName) { - if (android.os.Build.VERSION.SDK_INT > 27) { - Log.e(TAG, "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."); - return; + if (CAN_USE_MEDIA_BUTTONS) { + audioManager.registerMediaButtonEventReceiver(componentName); + } else { + Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); } - audioManager.registerMediaButtonEventReceiver(componentName); } public void unregisterMediaButtonEventReceiver(ComponentName componentName) { - if (android.os.Build.VERSION.SDK_INT > 27) { - Log.e(TAG, "unregisterMediaButtonEventReceiver has been deprecated and maybe not supported anymore."); - return; + if (CAN_USE_MEDIA_BUTTONS) { + audioManager.unregisterMediaButtonEventReceiver(componentName); + } else { + Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); } - audioManager.unregisterMediaButtonEventReceiver(componentName); } /*////////////////////////////////////////////////////////////////////////// @@ -165,12 +169,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au player.setVolume(to); } }); - valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - player.setVolume(((float) animation.getAnimatedValue())); - } - }); + valueAnimator.addUpdateListener(animation -> + player.setVolume(((float) animation.getAnimatedValue()))); valueAnimator.start(); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java index 73cdf1113..7042bea89 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java @@ -1,28 +1,22 @@ package org.schabi.newpipe.playlist; import android.content.Context; -import android.graphics.Bitmap; import android.text.TextUtils; import android.view.MotionEvent; import android.view.View; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.assist.ImageScaleType; -import com.nostra13.universalimageloader.core.process.BitmapProcessor; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; - public class PlayQueueItemBuilder { private static final String TAG = PlayQueueItemBuilder.class.toString(); - private final int thumbnailWidthPx; - private final int thumbnailHeightPx; - private final DisplayImageOptions imageOptions; - public interface OnSelectedListener { void selected(PlayQueueItem item, View view); void held(PlayQueueItem item, View view); @@ -31,11 +25,7 @@ public class PlayQueueItemBuilder { private OnSelectedListener onItemClickListener; - public PlayQueueItemBuilder(final Context context) { - thumbnailWidthPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_width); - thumbnailHeightPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_height); - imageOptions = buildImageOptions(thumbnailWidthPx, thumbnailHeightPx); - } + public PlayQueueItemBuilder(final Context context) {} public void setOnSelectedListener(OnSelectedListener listener) { this.onItemClickListener = listener; @@ -43,7 +33,8 @@ public class PlayQueueItemBuilder { public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) { if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); - if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader()); + holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(), + NewPipe.getNameOfService(item.getServiceId()))); if (item.getDuration() > 0) { holder.itemDurationView.setText(Localization.getDurationString(item.getDuration())); @@ -51,7 +42,8 @@ public class PlayQueueItemBuilder { holder.itemDurationView.setVisibility(View.GONE); } - ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions); + ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); holder.itemRoot.setOnClickListener(view -> { if (onItemClickListener != null) { @@ -81,23 +73,4 @@ public class PlayQueueItemBuilder { return false; }; } - - private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) { - final BitmapProcessor bitmapProcessor = bitmap -> { - final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false); - bitmap.recycle(); - return resizedBitmap; - }; - - return new DisplayImageOptions.Builder() - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .bitmapConfig(Bitmap.Config.RGB_565) // Users won't be able to see much anyways - .preProcessor(bitmapProcessor) - .imageScaleType(ImageScaleType.EXACTLY) - .cacheInMemory(true) - .cacheOnDisk(true) - .build(); - } } 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 26278ac75..f0ab3bc03 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -6,12 +6,14 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.preference.ListPreference; import android.support.v7.preference.Preference; import android.util.Log; import android.widget.Toast; import com.nononsenseapps.filepicker.Utils; +import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; @@ -47,6 +49,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment { private File newpipe_db; private File newpipe_db_journal; + private String thumbnailLoadToggleKey; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference.getKey().equals(thumbnailLoadToggleKey)) { + final ImageLoader imageLoader = ImageLoader.getInstance(); + imageLoader.stop(); + imageLoader.clearDiskCache(); + imageLoader.clearMemoryCache(); + imageLoader.resume(); + Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice, + Toast.LENGTH_SHORT).show(); + } + + return super.onPreferenceTreeClick(preference); + } + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { diff --git a/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java new file mode 100644 index 000000000..9ee8a1095 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java @@ -0,0 +1,58 @@ +package org.schabi.newpipe.util; + +import android.graphics.Bitmap; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.assist.ImageScaleType; +import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; + +import org.schabi.newpipe.R; + +public class ImageDisplayConstants { + private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250; + + /** + * Base display options + */ + private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = + new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(true) + .resetViewBeforeLoading(true) + .bitmapConfig(Bitmap.Config.RGB_565) + .imageScaleType(ImageScaleType.EXACTLY) + .displayer(new FadeInBitmapDisplayer(BITMAP_FADE_IN_DURATION_MILLIS)) + .build(); + + /*////////////////////////////////////////////////////////////////////////// + // DisplayImageOptions default configurations + //////////////////////////////////////////////////////////////////////////*/ + + public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageForEmptyUri(R.drawable.buddy) + .showImageOnFail(R.drawable.buddy) + .build(); + + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnFail(R.drawable.dummy_thumbnail) + .build(); + + public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageForEmptyUri(R.drawable.channel_banner) + .showImageOnFail(R.drawable.channel_banner) + .build(); + + public static final DisplayImageOptions DISPLAY_PLAYLIST_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/res/layout/list_playlist_item.xml b/app/src/main/res/layout/list_playlist_item.xml index 23f5224c5..57a3cbef9 100644 --- a/app/src/main/res/layout/list_playlist_item.xml +++ b/app/src/main/res/layout/list_playlist_item.xml @@ -19,7 +19,7 @@ android:layout_alignParentTop="true" android:layout_marginRight="@dimen/video_item_search_image_right_margin" android:contentDescription="@string/list_thumbnail_view_description" - android:scaleType="fitEnd" + android:scaleType="centerCrop" android:src="@drawable/dummy_thumbnail_playlist" tools:ignore="RtlHardcoded"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1a353807..cd280ff02 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,7 +75,8 @@ Use fast inexact seek Inexact seek allows the player to seek to positions faster with reduced precision Load thumbnails - Disable to stop all non-cached thumbnail from loading and save on data and memory usage + Disable to stop all thumbnails from loading and save on data and memory usage. Changing this will clear both in-memory and on-disk image cache. + Image cache wiped Wipe cached metadata Remove all cached webpage data Metadata cache wiped From 5a05cb96beeb73c7586af1f2c9c249b6213c5a18 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 15 Mar 2018 20:07:38 -0700 Subject: [PATCH 269/276] -Changed start position seek to occur after media source window has been prepared. -Fixed livestream not seeking to live when started from play queue. -Fixed media source manager synchronization to only occur after timeline change has completed. -Fixed auto queue not working when last item is replayed after the auto-queued item is removed. -Updated ExoPlayer to 2.7.1. --- app/build.gradle | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 139 +++++++++--------- .../newpipe/player/MainVideoPlayer.java | 1 + .../newpipe/player/PopupVideoPlayer.java | 2 +- .../newpipe/player/ServicePlayerActivity.java | 2 +- .../schabi/newpipe/player/VideoPlayer.java | 10 +- .../player/playback/MediaSourceManager.java | 15 +- 7 files changed, 92 insertions(+), 79 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3529a37b1..952bc3067 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.nononsenseapps:filepicker:4.2.1' - implementation 'com.google.android.exoplayer:exoplayer:2.7.0' + implementation 'com.google.android.exoplayer:exoplayer:2.7.1' debugImplementation 'com.facebook.stetho:stetho:1.5.0' debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 5ec61b058..de85a3704 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -149,7 +149,8 @@ public abstract class BasePlayer implements protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; - protected boolean isPrepared = false; + private boolean isPrepared = false; + private boolean isSynchronizing = false; protected Disposable progressUpdateReactor; protected CompositeDisposable databaseUpdateReactor; @@ -402,6 +403,7 @@ public abstract class BasePlayer implements // States Implementation //////////////////////////////////////////////////////////////////////////*/ + public static final int STATE_PREFLIGHT = -1; public static final int STATE_BLOCKED = 123; public static final int STATE_PLAYING = 124; public static final int STATE_BUFFERING = 125; @@ -409,7 +411,7 @@ public abstract class BasePlayer implements public static final int STATE_PAUSED_SEEK = 127; public static final int STATE_COMPLETED = 128; - protected int currentState = -1; + protected int currentState = STATE_PREFLIGHT; public void changeState(int state) { if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); @@ -540,11 +542,13 @@ public abstract class BasePlayer implements case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes - // ensures MediaSourceManager#update is complete + // Ensures MediaSourceManager#update is complete final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); // Ensure dynamic/livestream timeline changes does not cause negative position - if (isPlaylistStable && !isCurrentWindowValid()) { - simpleExoPlayer.seekTo(/*clampToMillis=*/0); + if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) { + if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " + + "clamping position to default time."); + seekTo(/*clampToTime=*/0); } break; } @@ -596,49 +600,55 @@ public abstract class BasePlayer implements } break; case Player.STATE_READY: //3 - maybeRecover(); + maybeCorrectSeekPosition(); if (!isPrepared) { isPrepared = true; onPrepared(playWhenReady); break; } - if (currentState == STATE_PAUSED_SEEK) break; changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); break; case Player.STATE_ENDED: // 4 - // Ensure the current window has actually ended - // since single windows that are still loading may produce an ended state - if (isCurrentWindowValid() && - simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { - changeState(STATE_COMPLETED); - isPrepared = false; - } + changeState(STATE_COMPLETED); + isPrepared = false; break; } } - private void maybeRecover() { + private void maybeCorrectSeekPosition() { + if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return; + final int currentSourceIndex = playQueue.getIndex(); final PlayQueueItem currentSourceItem = playQueue.getItem(); + if (currentSourceItem == null) return; - // Check if already playing correct window - final boolean isCurrentPeriodCorrect = + final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); + final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; + final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; - // Check if recovering - if (isCurrentPeriodCorrect && currentSourceItem != null) { - /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, - * rounding this position to the nearest second will help alleviate this.*/ - final long position = currentSourceItem.getRecoveryPosition(); - - /* Skip recovering if the recovery position is not set.*/ - if (position == PlayQueueItem.RECOVERY_UNSET) return; - - if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + - " at: " + getTimeString((int)position)); - simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition()); + if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) { + // Is recovering previous playback? + if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" + + "[" + getTimeString((int)recoveryPositionMillis) + "]"); + seekTo(recoveryPositionMillis); playQueue.unsetRecovery(currentSourceIndex); + isSynchronizing = false; + + } else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) { + if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); + // Is still synchronizing? + seekToDefault(); + + } else if (isSynchronizing && presetStartPositionMillis != 0L) { + if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + + "position=[" + presetStartPositionMillis + "]"); + // Has another start position? + seekTo(presetStartPositionMillis); + currentInfo.setStartPosition(0); } + + isSynchronizing = false; } /** @@ -810,11 +820,26 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + (info != null ? "available" : "null") + " info, " + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); + if (simpleExoPlayer == null || playQueue == null) return; + final boolean onPlaybackInitial = currentItem == null; final boolean hasPlayQueueItemChanged = currentItem != item; final boolean hasStreamInfoChanged = currentInfo != info; + + final int currentPlayQueueIndex = playQueue.indexOf(item); + final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); + + // when starting playback on the last item when not repeating, maybe auto queue + if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && + getRepeatMode() == Player.REPEAT_MODE_OFF && + PlayerHelper.isAutoQueueEnabled(context)) { + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); + if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + } + // If nothing to synchronize if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { - return; // Nothing to synchronize + return; } currentItem = item; @@ -824,13 +849,8 @@ public abstract class BasePlayer implements registerView(); initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); } - - final int currentPlayQueueIndex = playQueue.indexOf(item); onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); - if (simpleExoPlayer == null) return; - final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); - final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + @@ -844,22 +864,16 @@ public abstract class BasePlayer implements "index=[" + currentPlayQueueIndex + "] with " + "playlist length=[" + currentPlaylistSize + "]"); - // If not playing correct stream, change window position - } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { - final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; + // If not playing correct stream, change window position and sets flag + // for synchronizing once window position is corrected + // @see maybeCorrectSeekPosition() + } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial || + !isPlaying()) { if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + " index=[" + currentPlayQueueIndex + "]," + - " at=[" + getTimeString((int)startPos) + "]," + " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); - simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); - } - - // when starting playback on the last item when not repeating, maybe auto queue - if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && - getRepeatMode() == Player.REPEAT_MODE_OFF && - PlayerHelper.isAutoQueueEnabled(context)) { - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); - if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + isSynchronizing = true; + simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); } } @@ -927,9 +941,6 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); if (playWhenReady) audioReactor.requestAudioFocus(); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - - // On live prepared - if (simpleExoPlayer.isCurrentWindowDynamic()) seekToDefault(); } public void onVideoPlayPause() { @@ -1001,16 +1012,16 @@ public abstract class BasePlayer implements playQueue.setIndex(index); } - public void seekBy(int milliSeconds) { - if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); - if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || - ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) { - return; - } + public void seekTo(long positionMillis) { + if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); + if (simpleExoPlayer == null || positionMillis < 0 || + positionMillis > simpleExoPlayer.getDuration()) return; + simpleExoPlayer.seekTo(positionMillis); + } - int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); - if (progress < 0) progress = 0; - simpleExoPlayer.seekTo(progress); + public void seekBy(long offsetMillis) { + if (DEBUG) Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); + seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); } public boolean isCurrentWindowValid() { @@ -1094,10 +1105,6 @@ public abstract class BasePlayer implements return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); } - public boolean isCompleted() { - return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED; - } - public boolean isPlaying() { final int state = simpleExoPlayer.getPlaybackState(); return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) @@ -1148,8 +1155,8 @@ public abstract class BasePlayer implements return playQueueAdapter; } - public boolean isPlayerReady() { - return currentState == STATE_PLAYING || currentState == STATE_COMPLETED || currentState == STATE_PAUSED; + public boolean isPrepared() { + return isPrepared; } public boolean isProgressLoopRunning() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index dd7e0c71e..90a4a8c9f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -640,6 +640,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR public void onDismiss(PopupMenu menu) { super.onDismiss(menu); if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); + hideSystemUi(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 123fbfee3..64dc03da6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -716,7 +716,7 @@ public final class PopupVideoPlayer extends Service { public boolean onDoubleTap(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false; + if (playerImpl == null || !playerImpl.isPlaying()) return false; if (e.getX() > popupWidth / 2) { playerImpl.onFastForward(); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 1c3ffe911..50248891b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -509,7 +509,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity @Override public void onStopTrackingTouch(SeekBar seekBar) { - if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress()); + if (player != null) player.seekTo(seekBar.getProgress()); seekDisplay.setVisibility(View.GONE); seeking = false; } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 48b13654c..aa896bb69 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -425,7 +425,7 @@ public abstract class VideoPlayer extends BasePlayer // Create subtitle sources for (final Subtitles subtitle : info.getSubtitles()) { final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); - if (mimeType == null || context == null) continue; + if (mimeType == null) continue; final Format textFormat = Format.createTextSampleFormat(null, mimeType, SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); @@ -599,7 +599,7 @@ public abstract class VideoPlayer extends BasePlayer @Override public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { - if (!isPrepared) return; + if (!isPrepared()) return; if (duration != playbackSeekBar.getMax()) { playbackEndTime.setText(getTimeString(duration)); @@ -624,8 +624,6 @@ public abstract class VideoPlayer extends BasePlayer } protected void onFullScreenButtonClicked() { - if (!isPlayerReady()) return; - changeState(STATE_BLOCKED); } @@ -735,7 +733,7 @@ public abstract class VideoPlayer extends BasePlayer } private void onResizeClicked() { - if (getAspectRatioFrameLayout() != null && context != null) { + if (getAspectRatioFrameLayout() != null) { final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); final int newResizeMode = nextResizeMode(currentResizeMode); getAspectRatioFrameLayout().setResizeMode(newResizeMode); @@ -772,7 +770,7 @@ public abstract class VideoPlayer extends BasePlayer public void onStopTrackingTouch(SeekBar seekBar) { if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); - simpleExoPlayer.seekTo(seekBar.getProgress()); + seekTo(seekBar.getProgress()); if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 50c069b40..170668169 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -307,7 +307,7 @@ public class MediaSourceManager { if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); final PlayQueueItem currentItem = playQueue.getItem(); - if (isBlocked.get() || currentItem == null) return; + if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return; final Consumer onSuccess = info -> syncInternal(currentItem, info); final Consumer onError = throwable -> syncInternal(currentItem, null); @@ -400,8 +400,6 @@ public class MediaSourceManager { /* No exception handling since getLoadedMediaSource guarantees nonnull return */ .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); loaderReactor.add(loader); - } else { - maybeSynchronizePlayer(); } } @@ -467,6 +465,12 @@ public class MediaSourceManager { * Checks if the current playing index contains an expired {@link ManagedMediaSource}. * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and * {@link #loadImmediate()} is called to reload the current item. + *

+ * If not, then the media source at the current index is ready for playback, and + * {@link #maybeSynchronizePlayer()} is called. + *

+ * Under both cases, {@link #maybeSync()} will be called to ensure the listener + * is up-to-date. * */ private void maybeRenewCurrentIndex() { final int currentIndex = playQueue.getIndex(); @@ -475,7 +479,10 @@ public class MediaSourceManager { final ManagedMediaSource currentSource = (ManagedMediaSource) sources.getMediaSource(currentIndex); final PlayQueueItem currentItem = playQueue.getItem(); - if (!currentSource.canReplace(currentItem)) return; + if (!currentSource.canReplace(currentItem)) { + maybeSynchronizePlayer(); + return; + } if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); From bc7188c8a8c8036ed67a93aa2572f5de676721ce Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 15 Mar 2018 23:42:46 -0700 Subject: [PATCH 270/276] -Added media session implementation for all players. -Extracted version numbers in gradle dependencies. -Updated ExoPlayer to 2.7.1. -Updated RxJava to 2.1.10, RxAndroid to 2.0.2 and RxBinding to 2.1.1. -Removed deprecated implementation of media buttons. --- app/build.gradle | 36 +++--- app/src/main/AndroidManifest.xml | 6 - .../newpipe/player/BackgroundPlayer.java | 54 --------- .../org/schabi/newpipe/player/BasePlayer.java | 12 +- .../newpipe/player/helper/AudioReactor.java | 33 ++---- .../player/helper/MediaSessionManager.java | 38 ++++++ .../mediasession/DummyPlaybackPreparer.java | 45 +++++++ .../mediasession/MediaSessionCallback.java | 17 +++ .../mediasession/PlayQueueNavigator.java | 111 ++++++++++++++++++ .../PlayQueuePlaybackController.java | 31 +++++ .../playback/BasePlayerMediaSession.java | 77 ++++++++++++ 11 files changed, 358 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java diff --git a/app/build.gradle b/app/build.gradle index 952bc3067..9b2569a66 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,11 @@ android { ext { supportLibVersion = '27.1.0' + exoPlayerLibVersion = '2.7.1' + roomDbLibVersion = '1.0.0' + leakCanaryVersion = '1.5.4' + okHttpVersion = '1.5.0' + icepickVersion = '3.2.0' } dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { @@ -73,27 +78,28 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.nononsenseapps:filepicker:4.2.1' - implementation 'com.google.android.exoplayer:exoplayer:2.7.1' + implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion" + implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion" - debugImplementation 'com.facebook.stetho:stetho:1.5.0' - debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' + debugImplementation "com.facebook.stetho:stetho:$okHttpVersion" + debugImplementation "com.facebook.stetho:stetho-urlconnection:$okHttpVersion" debugImplementation 'com.android.support:multidex:1.0.3' - implementation 'io.reactivex.rxjava2:rxjava:2.1.7' - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' - implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' + implementation 'io.reactivex.rxjava2:rxjava:2.1.10' + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' - implementation 'android.arch.persistence.room:runtime:1.0.0' - implementation 'android.arch.persistence.room:rxjava2:1.0.0' - annotationProcessor 'android.arch.persistence.room:compiler:1.0.0' + implementation "android.arch.persistence.room:runtime:$roomDbLibVersion" + implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion" + annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion" - implementation 'frankiesardo:icepick:3.2.0' - annotationProcessor 'frankiesardo:icepick-processor:3.2.0' + implementation "frankiesardo:icepick:$icepickVersion" + annotationProcessor "frankiesardo:icepick-processor:$icepickVersion" - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' - betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' + debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" + betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" + releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" implementation 'com.squareup.okhttp3:okhttp:3.9.1' - debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0' + debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpVersion" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 18b3222a0..1be8c1f2c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,12 +43,6 @@ android:launchMode="singleTask" android:label="@string/title_activity_background_player"/> - - - - - - = Build.VERSION_CODES.O; - private static final boolean CAN_USE_MEDIA_BUTTONS = - Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1; - private static final String MEDIA_BUTTON_DEPRECATED_ERROR = - "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."; - private static final int DUCK_DURATION = 1500; private static final float DUCK_AUDIO_TO = .2f; @@ -42,7 +37,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au private final AudioFocusRequest request; - public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) { + public AudioReactor(@NonNull final Context context, + @NonNull final SimpleExoPlayer player) { this.player = player; this.context = context; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -59,6 +55,11 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au } } + public void dispose() { + abandonAudioFocus(); + player.removeAudioDebugListener(this); + } + /*////////////////////////////////////////////////////////////////////////// // Audio Manager //////////////////////////////////////////////////////////////////////////*/ @@ -91,22 +92,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au audioManager.setStreamVolume(STREAM_TYPE, volume, 0); } - public void registerMediaButtonEventReceiver(ComponentName componentName) { - if (CAN_USE_MEDIA_BUTTONS) { - audioManager.registerMediaButtonEventReceiver(componentName); - } else { - Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); - } - } - - public void unregisterMediaButtonEventReceiver(ComponentName componentName) { - if (CAN_USE_MEDIA_BUTTONS) { - audioManager.unregisterMediaButtonEventReceiver(componentName); - } else { - Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); - } - } - /*////////////////////////////////////////////////////////////////////////// // AudioFocus //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java new file mode 100644 index 000000000..8405e45fd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.player.helper; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.media.session.MediaSessionCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +import org.schabi.newpipe.player.mediasession.DummyPlaybackPreparer; +import org.schabi.newpipe.player.mediasession.MediaSessionCallback; +import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; +import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; + +public class MediaSessionManager { + private static final String TAG = "MediaSessionManager"; + + private final MediaSessionCompat mediaSession; + private final MediaSessionConnector sessionConnector; + + public MediaSessionManager(@NonNull final Context context, + @NonNull final Player player, + @NonNull final MediaSessionCallback callback) { + this.mediaSession = new MediaSessionCompat(context, TAG); + this.sessionConnector = new MediaSessionConnector(mediaSession, + new PlayQueuePlaybackController(callback)); + this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); + this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer()); + } + + public MediaSessionCompat getMediaSession() { + return mediaSession; + } + + public MediaSessionConnector getSessionConnector() { + return sessionConnector; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java new file mode 100644 index 000000000..431a90d8a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java @@ -0,0 +1,45 @@ +package org.schabi.newpipe.player.mediasession; + +import android.net.Uri; +import android.os.Bundle; +import android.os.ResultReceiver; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer { + @Override + public long getSupportedPrepareActions() { + return 0; + } + + @Override + public void onPrepare() { + + } + + @Override + public void onPrepareFromMediaId(String mediaId, Bundle extras) { + + } + + @Override + public void onPrepareFromSearch(String query, Bundle extras) { + + } + + @Override + public void onPrepareFromUri(Uri uri, Bundle extras) { + + } + + @Override + public String[] getCommands() { + return new String[0]; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java new file mode 100644 index 000000000..a1a57a87d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.player.mediasession; + +import android.support.v4.media.MediaDescriptionCompat; + +public interface MediaSessionCallback { + void onSkipToPrevious(); + void onSkipToNext(); + void onSkipToIndex(final int index); + + int getCurrentPlayingIndex(); + int getQueueSize(); + MediaDescriptionCompat getQueueMetadata(final int index); + + void onPlay(); + void onPause(); + void onSetShuffle(final boolean isShuffled); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java new file mode 100644 index 000000000..429c26fd9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -0,0 +1,111 @@ +package org.schabi.newpipe.player.mediasession; + +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.session.MediaSessionCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; +import com.google.android.exoplayer2.util.Util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; + + +public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator { + public static final int DEFAULT_MAX_QUEUE_SIZE = 10; + + private final MediaSessionCompat mediaSession; + private final MediaSessionCallback callback; + private final int maxQueueSize; + + private long activeQueueItemId; + + public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession, + @NonNull final MediaSessionCallback callback) { + this.mediaSession = mediaSession; + this.callback = callback; + this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + + this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + } + + @Override + public long getSupportedQueueNavigatorActions(@Nullable Player player) { + return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; + } + + @Override + public void onTimelineChanged(Player player) { + publishFloatingQueueWindow(); + } + + @Override + public void onCurrentWindowIndexChanged(Player player) { + if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID + || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { + publishFloatingQueueWindow(); + } else if (!player.getCurrentTimeline().isEmpty()) { + activeQueueItemId = player.getCurrentWindowIndex(); + } + } + + @Override + public long getActiveQueueItemId(@Nullable Player player) { + return callback.getCurrentPlayingIndex(); + } + + @Override + public void onSkipToPrevious(Player player) { + callback.onSkipToPrevious(); + } + + @Override + public void onSkipToQueueItem(Player player, long id) { + callback.onSkipToIndex((int) id); + } + + @Override + public void onSkipToNext(Player player) { + callback.onSkipToNext(); + } + + private void publishFloatingQueueWindow() { + if (callback.getQueueSize() == 0) { + mediaSession.setQueue(Collections.emptyList()); + activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + return; + } + + // Yes this is almost a copypasta, got a problem with that? =\ + int windowCount = callback.getQueueSize(); + int currentWindowIndex = callback.getCurrentPlayingIndex(); + int queueSize = Math.min(maxQueueSize, windowCount); + int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, + windowCount - queueSize); + + List queue = new ArrayList<>(); + for (int i = startIndex; i < startIndex + queueSize; i++) { + queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i)); + } + mediaSession.setQueue(queue); + activeQueueItemId = currentWindowIndex; + } + + @Override + public String[] getCommands() { + return new String[0]; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java new file mode 100644 index 000000000..2aa41bd63 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java @@ -0,0 +1,31 @@ +package org.schabi.newpipe.player.mediasession; + +import android.support.v4.media.session.PlaybackStateCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController; + +public class PlayQueuePlaybackController extends DefaultPlaybackController { + private final MediaSessionCallback callback; + + public PlayQueuePlaybackController(final MediaSessionCallback callback) { + super(); + this.callback = callback; + } + + @Override + public void onPlay(Player player) { + callback.onPlay(); + } + + @Override + public void onPause(Player player) { + callback.onPause(); + } + + @Override + public void onSetShuffleMode(Player player, int shuffleMode) { + callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL + || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java new file mode 100644 index 000000000..07504542c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java @@ -0,0 +1,77 @@ +package org.schabi.newpipe.player.playback; + +import android.net.Uri; +import android.support.v4.media.MediaDescriptionCompat; + +import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.mediasession.MediaSessionCallback; +import org.schabi.newpipe.playlist.PlayQueueItem; + +public class BasePlayerMediaSession implements MediaSessionCallback { + private BasePlayer player; + + public BasePlayerMediaSession(final BasePlayer player) { + this.player = player; + } + + @Override + public void onSkipToPrevious() { + player.onPlayPrevious(); + } + + @Override + public void onSkipToNext() { + player.onPlayNext(); + } + + @Override + public void onSkipToIndex(int index) { + if (player.getPlayQueue() == null) return; + player.onSelected(player.getPlayQueue().getItem(index)); + } + + @Override + public int getCurrentPlayingIndex() { + if (player.getPlayQueue() == null) return -1; + return player.getPlayQueue().getIndex(); + } + + @Override + public int getQueueSize() { + if (player.getPlayQueue() == null) return -1; + return player.getPlayQueue().size(); + } + + @Override + public MediaDescriptionCompat getQueueMetadata(int index) { + if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) { + return null; + } + + final PlayQueueItem item = player.getPlayQueue().getItem(index); + MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder() + .setMediaId(String.valueOf(index)) + .setTitle(item.getTitle()) + .setSubtitle(item.getUploader()); + + final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl()); + if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri); + + return descriptionBuilder.build(); + } + + @Override + public void onPlay() { + if (!player.isPlaying()) player.onVideoPlayPause(); + } + + @Override + public void onPause() { + if (player.isPlaying()) player.onVideoPlayPause(); + } + + @Override + public void onSetShuffle(boolean isShuffled) { + player.onShuffleModeEnabledChanged(isShuffled); + } +} From 5167fe078bb066677644393bc52e6117d1e83c89 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Sat, 17 Mar 2018 16:04:02 -0700 Subject: [PATCH 271/276] -Refactored synchronization checks out from MediaSourceManager to ManagedMediaSource. -Fixed null input causing potential NPE on PlayQueueItem. --- .../player/mediasource/FailedMediaSource.java | 8 ++++- .../player/mediasource/LoadedMediaSource.java | 10 ++++-- .../mediasource/ManagedMediaSource.java | 18 ++++++++++- .../mediasource/PlaceholderMediaSource.java | 8 ++++- .../player/playback/MediaSourceManager.java | 21 ++++--------- .../newpipe/playlist/PlayQueueItem.java | 31 ++++++++++--------- 6 files changed, 61 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index d07baf2a7..5f029cc50 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -72,7 +72,13 @@ public class FailedMediaSource implements ManagedMediaSource { public void releaseSource() {} @Override - public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { + public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, + final boolean isInterruptable) { return newIdentity != playQueueItem || canRetry(); } + + @Override + public boolean isStreamEqual(@NonNull PlayQueueItem stream) { + return playQueueItem == stream; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index f523667f9..fe7508ecc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -59,7 +59,13 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { - return newIdentity != stream || isExpired(); + public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, + final boolean isInterruptable) { + return newIdentity != stream || (isInterruptable && isExpired()); + } + + @Override + public boolean isStreamEqual(@NonNull PlayQueueItem stream) { + return this.stream == stream; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java index 3bb7ca429..46fd149bb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java @@ -7,5 +7,21 @@ import com.google.android.exoplayer2.source.MediaSource; import org.schabi.newpipe.playlist.PlayQueueItem; public interface ManagedMediaSource extends MediaSource { - boolean canReplace(@NonNull final PlayQueueItem newIdentity); + /** + * Determines whether or not this {@link ManagedMediaSource} can be replaced. + * + * @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if + * it is different from the existing stream in the + * {@link ManagedMediaSource}, then it should be replaced. + * @param isInterruptable specifies if this {@link ManagedMediaSource} potentially + * being played. + * */ + boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, + final boolean isInterruptable); + + /** + * Determines if the {@link PlayQueueItem} is the one the + * {@link ManagedMediaSource} encapsulates over. + * */ + boolean isStreamEqual(@NonNull final PlayQueueItem stream); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 0d3436a01..2c57f2f9c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -19,7 +19,13 @@ public class PlaceholderMediaSource implements ManagedMediaSource { @Override public void releaseSource() {} @Override - public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { + public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, + final boolean isInterruptable) { return true; } + + @Override + public boolean isStreamEqual(@NonNull PlayQueueItem stream) { + return false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 170668169..477358113 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -268,15 +268,10 @@ public class MediaSourceManager { private boolean isPlaybackReady() { if (sources.getSize() != playQueue.size()) return false; - final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); + final ManagedMediaSource mediaSource = + (ManagedMediaSource) sources.getMediaSource(playQueue.getIndex()); final PlayQueueItem playQueueItem = playQueue.getItem(); - - if (mediaSource instanceof LoadedMediaSource) { - return playQueueItem == ((LoadedMediaSource) mediaSource).getStream(); - } else if (mediaSource instanceof FailedMediaSource) { - return playQueueItem == ((FailedMediaSource) mediaSource).getStream(); - } - return false; + return mediaSource.isStreamEqual(playQueueItem); } private void maybeBlock() { @@ -453,12 +448,8 @@ public class MediaSourceManager { if (index == -1 || index >= sources.getSize()) return false; final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index); - - if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) { - return item != ((LoadedMediaSource) mediaSource).getStream(); - } else { - return mediaSource.canReplace(item); - } + return mediaSource.shouldBeReplacedWith(item, + /*mightBeInProgress=*/index != playQueue.getIndex()); } /** @@ -479,7 +470,7 @@ public class MediaSourceManager { final ManagedMediaSource currentSource = (ManagedMediaSource) sources.getMediaSource(currentIndex); final PlayQueueItem currentItem = playQueue.getItem(); - if (!currentSource.canReplace(currentItem)) { + if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) { maybeSynchronizePlayer(); return; } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java index 752dc223d..df4d19720 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java @@ -11,20 +11,19 @@ import org.schabi.newpipe.util.ExtractorHelper; import java.io.Serializable; import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; public class PlayQueueItem implements Serializable { - final public static long RECOVERY_UNSET = Long.MIN_VALUE; + public final static long RECOVERY_UNSET = Long.MIN_VALUE; + private final static String EMPTY_STRING = ""; - final private String title; - final private String url; + @NonNull final private String title; + @NonNull final private String url; final private int serviceId; final private long duration; - final private String thumbnailUrl; - final private String uploader; - final private StreamType streamType; + @NonNull final private String thumbnailUrl; + @NonNull final private String uploader; + @NonNull final private StreamType streamType; private long recoveryPosition; private Throwable error; @@ -42,15 +41,16 @@ public class PlayQueueItem implements Serializable { item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); } - private PlayQueueItem(final String name, final String url, final int serviceId, - final long duration, final String thumbnailUrl, final String uploader, - final StreamType streamType) { - this.title = name; - this.url = url; + private PlayQueueItem(@Nullable final String name, @Nullable final String url, + final int serviceId, final long duration, + @Nullable final String thumbnailUrl, @Nullable final String uploader, + @NonNull final StreamType streamType) { + this.title = name != null ? name : EMPTY_STRING; + this.url = url != null ? url : EMPTY_STRING; this.serviceId = serviceId; this.duration = duration; - this.thumbnailUrl = thumbnailUrl; - this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING; + this.uploader = uploader != null ? uploader : EMPTY_STRING; this.streamType = streamType; this.recoveryPosition = RECOVERY_UNSET; @@ -84,6 +84,7 @@ public class PlayQueueItem implements Serializable { return uploader; } + @NonNull public StreamType getStreamType() { return streamType; } From e885822a3484d5404fe6cce51cb0eb87cadccf4f Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 21 Mar 2018 00:11:54 -0700 Subject: [PATCH 272/276] -Added playback speed control dialog to allow full user control over player tempo and pitch parameters. -Changed tempo and pitch button in service player activity and tempo button in main video player to open speed control dialog. -Changed LIVE button to be no longer clickable when player position is at or beyond default position. -Changed main video player to use AppCompatActivity rather than Activity. -Fixed video player tempo button not updating when player speed parameters change. -Fixed player crashing on lower sdk versions due to no MediaButtonReceiver, added intent back to manifest. -Fixed inconsistent gradle library naming. -Fixed stetho dependencies incorrect version. --- app/build.gradle | 23 +- app/src/main/AndroidManifest.xml | 6 + .../org/schabi/newpipe/player/BasePlayer.java | 21 +- .../newpipe/player/MainVideoPlayer.java | 22 +- .../newpipe/player/ServicePlayerActivity.java | 66 +--- .../schabi/newpipe/player/VideoPlayer.java | 10 +- .../helper/PlaybackParameterDialog.java | 348 ++++++++++++++++++ .../res/layout/dialog_playback_parameter.xml | 313 ++++++++++++++++ app/src/main/res/values/strings.xml | 8 + 9 files changed, 755 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java create mode 100644 app/src/main/res/layout/dialog_playback_parameter.xml diff --git a/app/build.gradle b/app/build.gradle index 9b2569a66..5c434c30c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,9 +51,10 @@ ext { supportLibVersion = '27.1.0' exoPlayerLibVersion = '2.7.1' roomDbLibVersion = '1.0.0' - leakCanaryVersion = '1.5.4' - okHttpVersion = '1.5.0' - icepickVersion = '3.2.0' + leakCanaryLibVersion = '1.5.4' + okHttpLibVersion = '1.5.0' + icepickLibVersion = '3.2.0' + stethoLibVersion = '1.5.0' } dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { @@ -81,8 +82,8 @@ dependencies { implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion" implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion" - debugImplementation "com.facebook.stetho:stetho:$okHttpVersion" - debugImplementation "com.facebook.stetho:stetho-urlconnection:$okHttpVersion" + debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion" + debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion" debugImplementation 'com.android.support:multidex:1.0.3' implementation 'io.reactivex.rxjava2:rxjava:2.1.10' @@ -93,13 +94,13 @@ dependencies { implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion" annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion" - implementation "frankiesardo:icepick:$icepickVersion" - annotationProcessor "frankiesardo:icepick-processor:$icepickVersion" + implementation "frankiesardo:icepick:$icepickLibVersion" + annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion" - debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" - betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" - releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" + debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion" + betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" + releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" implementation 'com.squareup.okhttp3:okhttp:3.9.1' - debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpVersion" + debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1be8c1f2c..1edd67d24 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,12 @@ + + + + + + = currentTimeline.getWindowCount()) { + return false; + } + + Timeline.Window timelineWindow = new Timeline.Window(); + currentTimeline.getWindow(currentWindowIndex, timelineWindow); + return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); + } + public boolean isPlaying() { final int state = simpleExoPlayer.getPlaybackState(); return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 90a4a8c9f..cbc4b8230 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -19,7 +19,6 @@ package org.schabi.newpipe.player; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -33,6 +32,7 @@ import android.preference.PreferenceManager; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.util.DisplayMetrics; @@ -49,6 +49,7 @@ import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; @@ -57,6 +58,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; @@ -87,7 +89,8 @@ import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; * * @author mauriciocolli */ -public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead { +public final class MainVideoPlayer extends AppCompatActivity + implements StateSaver.WriteRead, PlaybackParameterDialog.Callback { private static final String TAG = ".MainVideoPlayer"; private static final boolean DEBUG = BasePlayer.DEBUG; @@ -340,6 +343,15 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR } } + //////////////////////////////////////////////////////////////////////////// + // Playback Parameters Listener + //////////////////////////////////////////////////////////////////////////// + + @Override + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { + if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch); + } + /////////////////////////////////////////////////////////////////////////// @SuppressWarnings({"unused", "WeakerAccess"}) @@ -630,6 +642,12 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR showControlsThenHide(); } + @Override + public void onPlaybackSpeedClicked() { + PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch()) + .show(getSupportFragmentManager(), TAG); + } + @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 50248891b..1f850944d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -31,6 +31,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemHolder; @@ -43,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; public abstract class ServicePlayerActivity extends AppCompatActivity - implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener { + implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, + View.OnClickListener, PlaybackParameterDialog.Callback { private boolean serviceBound; private ServiceConnection serviceConnection; @@ -57,8 +59,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; - private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61; - private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; @@ -85,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ProgressBar progressBar; private TextView playbackSpeedButton; - private PopupMenu playbackSpeedPopupMenu; private TextView playbackPitchButton; - private PopupMenu playbackPitchPopupMenu; //////////////////////////////////////////////////////////////////////////// // Abstracts @@ -317,45 +315,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity shuffleButton.setOnClickListener(this); playbackSpeedButton.setOnClickListener(this); playbackPitchButton.setOnClickListener(this); - - playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton); - playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton); - buildPlaybackSpeedMenu(); - buildPlaybackPitchMenu(); - } - - private void buildPlaybackSpeedMenu() { - if (playbackSpeedPopupMenu == null) return; - - playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID); - for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) { - final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i]; - final String formattedSpeed = formatSpeed(playbackSpeed); - final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed); - item.setOnMenuItemClickListener(menuItem -> { - if (player == null) return false; - - player.setPlaybackSpeed(playbackSpeed); - return true; - }); - } - } - - private void buildPlaybackPitchMenu() { - if (playbackPitchPopupMenu == null) return; - - playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID); - for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) { - final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i]; - final String formattedPitch = formatPitch(playbackPitch); - final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch); - item.setOnMenuItemClickListener(menuItem -> { - if (player == null) return false; - - player.setPlaybackPitch(playbackPitch); - return true; - }); - } } private void buildItemPopupMenu(final PlayQueueItem item, final View view) { @@ -474,10 +433,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity player.onShuffleClicked(); } else if (view.getId() == playbackSpeedButton.getId()) { - playbackSpeedPopupMenu.show(); + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), + player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); } else if (view.getId() == playbackPitchButton.getId()) { - playbackPitchPopupMenu.show(); + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), + player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); } else if (view.getId() == metadata.getId()) { scrollToSelected(); @@ -488,6 +449,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } } + //////////////////////////////////////////////////////////////////////////// + // Playback Parameters Listener + //////////////////////////////////////////////////////////////////////////// + + @Override + public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { + if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch); + } + //////////////////////////////////////////////////////////////////////////// // Seekbar Listener //////////////////////////////////////////////////////////////////////////// @@ -539,6 +509,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity progressSeekBar.setProgress(currentProgress); progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); } + + if (player != null) { + progressLiveSync.setClickable(!player.isLiveEdge()); + } } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index aa896bb69..b019ea91e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -49,6 +49,7 @@ import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; @@ -523,6 +524,12 @@ public abstract class VideoPlayer extends BasePlayer onTextTrackUpdate(); } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + super.onPlaybackParametersChanged(playbackParameters); + playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed)); + } + @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { if (DEBUG) { @@ -615,6 +622,7 @@ public abstract class VideoPlayer extends BasePlayer if (DEBUG && bufferPercent % 20 == 0) { //Limit log Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); } + playbackLiveSync.setClickable(!isLiveEdge()); } @Override @@ -718,7 +726,7 @@ public abstract class VideoPlayer extends BasePlayer wasPlaying = simpleExoPlayer.getPlayWhenReady(); } - private void onPlaybackSpeedClicked() { + public void onPlaybackSpeedClicked() { if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); playbackSpeedPopupMenu.show(); isSomePopupMenuVisible = true; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java new file mode 100644 index 000000000..8a0a8a86c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -0,0 +1,348 @@ +package org.schabi.newpipe.player.helper; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.schabi.newpipe.R; + +import static org.schabi.newpipe.player.BasePlayer.DEBUG; + +public class PlaybackParameterDialog extends DialogFragment { + private static final String TAG = "PlaybackParameterDialog"; + + public static final float MINIMUM_PLAYBACK_VALUE = 0.25f; + public static final float MAXIMUM_PLAYBACK_VALUE = 3.00f; + + public static final String STEP_UP_SIGN = "+"; + public static final String STEP_DOWN_SIGN = "-"; + public static final float PLAYBACK_STEP_VALUE = 0.05f; + + public static final float NIGHTCORE_TEMPO = 1.20f; + public static final float NIGHTCORE_PITCH_LOWER = 1.15f; + public static final float NIGHTCORE_PITCH_UPPER = 1.25f; + + public static final float DEFAULT_TEMPO = 1.00f; + public static final float DEFAULT_PITCH = 1.00f; + + private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; + private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; + + public interface Callback { + void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); + } + + private Callback callback; + + private float initialTempo = DEFAULT_TEMPO; + private float initialPitch = DEFAULT_PITCH; + + private SeekBar tempoSlider; + private TextView tempoMinimumText; + private TextView tempoMaximumText; + private TextView tempoCurrentText; + private TextView tempoStepDownText; + private TextView tempoStepUpText; + + private SeekBar pitchSlider; + private TextView pitchMinimumText; + private TextView pitchMaximumText; + private TextView pitchCurrentText; + private TextView pitchStepDownText; + private TextView pitchStepUpText; + + private CheckBox unhookingCheckbox; + + private TextView nightCorePresetText; + private TextView resetPresetText; + + public static PlaybackParameterDialog newInstance(final float playbackTempo, + final float playbackPitch) { + PlaybackParameterDialog dialog = new PlaybackParameterDialog(); + dialog.initialTempo = playbackTempo; + dialog.initialPitch = playbackPitch; + return dialog; + } + + /*////////////////////////////////////////////////////////////////////////// + // Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context != null && context instanceof Callback) { + callback = (Callback) context; + } else { + dismiss(); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + initialTempo = savedInstanceState.getFloat(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); + initialPitch = savedInstanceState.getFloat(INITIAL_PITCH_KEY, DEFAULT_PITCH); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putFloat(INITIAL_TEMPO_KEY, initialTempo); + outState.putFloat(INITIAL_PITCH_KEY, initialPitch); + } + + /*////////////////////////////////////////////////////////////////////////// + // Dialog + //////////////////////////////////////////////////////////////////////////*/ + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); + setupView(view); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) + .setTitle(R.string.playback_speed_control) + .setView(view) + .setCancelable(true) + .setNegativeButton(R.string.cancel, (dialogInterface, i) -> + setPlaybackParameters(initialTempo, initialPitch)) + .setPositiveButton(R.string.finish, (dialogInterface, i) -> + setPlaybackParameters(getCurrentTempo(), getCurrentPitch())); + + return dialogBuilder.create(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Dialog Builder + //////////////////////////////////////////////////////////////////////////*/ + + private void setupView(@NonNull View rootView) { + setupHookingControl(rootView); + setupTempoControl(rootView); + setupPitchControl(rootView); + setupPresetControl(rootView); + } + + private void setupTempoControl(@NonNull View rootView) { + tempoSlider = rootView.findViewById(R.id.tempoSeekbar); + tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText); + tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText); + tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText); + tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); + tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); + + tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); + tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); + tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); + + tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + tempoStepUpText.setOnClickListener(view -> + setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE)); + + tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + tempoStepDownText.setOnClickListener(view -> + setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE)); + + tempoSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); + tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialTempo)); + tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); + } + + private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() { + return new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final float currentTempo = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); + if (fromUser) { // this change is first in chain + setTempo(currentTempo); + } else { + setPlaybackParameters(currentTempo, getCurrentPitch()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + }; + } + + private void setupPitchControl(@NonNull View rootView) { + pitchSlider = rootView.findViewById(R.id.pitchSeekbar); + pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); + pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); + pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText); + pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); + pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); + + pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); + pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); + pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); + + pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + pitchStepUpText.setOnClickListener(view -> + setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE)); + + pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + pitchStepDownText.setOnClickListener(view -> + setPitch(getCurrentPitch() - PLAYBACK_STEP_VALUE)); + + pitchSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); + pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialPitch)); + pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); + } + + private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { + return new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final float currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); + if (fromUser) { // this change is first in chain + setPitch(currentPitch); + } else { + setPlaybackParameters(getCurrentTempo(), currentPitch); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + }; + } + + private void setupHookingControl(@NonNull View rootView) { + unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); + unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { + if (isChecked) return; + // When unchecked, slide back to the minimum of current tempo or pitch + final float minimum = Math.min(getCurrentPitch(), getCurrentTempo()); + setSliders(minimum); + }); + } + + private void setupPresetControl(@NonNull View rootView) { + nightCorePresetText = rootView.findViewById(R.id.presetNightcore); + nightCorePresetText.setOnClickListener(view -> { + final float randomPitch = NIGHTCORE_PITCH_LOWER + + (float) Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); + + setTempoSlider(NIGHTCORE_TEMPO); + setPitchSlider(randomPitch); + }); + + resetPresetText = rootView.findViewById(R.id.presetReset); + resetPresetText.setOnClickListener(view -> { + setTempoSlider(DEFAULT_TEMPO); + setPitchSlider(DEFAULT_PITCH); + }); + } + + /*////////////////////////////////////////////////////////////////////////// + // Helper + //////////////////////////////////////////////////////////////////////////*/ + + private void setTempo(final float newTempo) { + if (unhookingCheckbox == null) return; + if (!unhookingCheckbox.isChecked()) { + setSliders(newTempo); + } else { + setTempoSlider(newTempo); + } + } + + private void setPitch(final float newPitch) { + if (unhookingCheckbox == null) return; + if (!unhookingCheckbox.isChecked()) { + setSliders(newPitch); + } else { + setPitchSlider(newPitch); + } + } + + private void setSliders(final float newValue) { + setTempoSlider(newValue); + setPitchSlider(newValue); + } + + private void setTempoSlider(final float newTempo) { + if (tempoSlider == null) return; + // seekbar doesn't register progress if it is the same as the existing progress + tempoSlider.setProgress(Integer.MAX_VALUE); + tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newTempo)); + } + + private void setPitchSlider(final float newPitch) { + if (pitchSlider == null) return; + pitchSlider.setProgress(Integer.MAX_VALUE); + pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newPitch)); + } + + private void setPlaybackParameters(final float tempo, final float pitch) { + if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { + if (DEBUG) Log.d(TAG, "Setting playback parameters to " + + "tempo=[" + tempo + "], " + + "pitch=[" + pitch + "]"); + + tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); + pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); + callback.onPlaybackParameterChanged(tempo, pitch); + } + } + + private float getCurrentTempo() { + return tempoSlider == null ? initialTempo : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, + tempoSlider.getProgress()); + } + + private float getCurrentPitch() { + return pitchSlider == null ? initialPitch : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, + pitchSlider.getProgress()); + } + + /** + * Converts from zeroed float with a minimum offset to the nearest rounded slider + * equivalent integer + * */ + private static int getSliderEquivalent(final float minimumValue, final float floatValue) { + return Math.round((floatValue - minimumValue) * 100f); + } + + /** + * Converts from slider integer value to an equivalent float value with a given minimum offset + * */ + private static float getSliderEquivalent(final float minimumValue, final int intValue) { + return ((float) intValue) / 100f + minimumValue; + } + + private static String getStepUpPercentString(final float percent) { + return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); + } + + private static String getStepDownPercentString(final float percent) { + return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); + } +} diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml new file mode 100644 index 000000000..a8c6a5dcd --- /dev/null +++ b/app/src/main/res/layout/dialog_playback_parameter.xml @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd280ff02..effdeaaba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -456,4 +456,12 @@ yourid, soundcloud.com/yourid Keep in mind that this operation can be network expensive.\n\nDo you want to continue? + + + Playback Speed Control + Tempo + Pitch + Unhook (may cause distortion) + Nightcore + Default From 18d019c62ae18f9c42a19d98e9cfe7d0cb43fd4a Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 21 Mar 2018 20:08:33 -0700 Subject: [PATCH 273/276] -Added quadratic slider strategy implementation and tests. -Modified playback speed control to use quadratic sliders instead of linear. -Modified number formatters in player helper to use double instead of float. -Simplified slider behavior in playback parameter dialog. -Fixed potential NPE in base local fragment. --- .../local/bookmark/BaseLocalListFragment.java | 5 +- .../helper/PlaybackParameterDialog.java | 345 ++++++++++-------- .../newpipe/player/helper/PlayerHelper.java | 4 +- .../schabi/newpipe/util/SliderStrategy.java | 73 ++++ .../util/QuadraticSliderStrategyTest.java | 86 +++++ 5 files changed, 353 insertions(+), 160 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java create mode 100644 app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java index d2c4e1b14..eb366d97f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BaseLocalListFragment.java @@ -151,7 +151,10 @@ public abstract class BaseLocalListFragment extends BaseStateFragment @Override public void showListFooter(final boolean show) { - itemsList.post(() -> itemListAdapter.showFooter(show)); + if (itemsList == null) return; + itemsList.post(() -> { + if (itemListAdapter != null) itemListAdapter.showFooter(show); + }); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 8a0a8a86c..7c7d87791 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -14,59 +14,64 @@ import android.widget.SeekBar; import android.widget.TextView; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.SliderStrategy; import static org.schabi.newpipe.player.BasePlayer.DEBUG; public class PlaybackParameterDialog extends DialogFragment { - private static final String TAG = "PlaybackParameterDialog"; + @NonNull private static final String TAG = "PlaybackParameterDialog"; - public static final float MINIMUM_PLAYBACK_VALUE = 0.25f; - public static final float MAXIMUM_PLAYBACK_VALUE = 3.00f; + public static final double MINIMUM_PLAYBACK_VALUE = 0.25f; + public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; - public static final String STEP_UP_SIGN = "+"; - public static final String STEP_DOWN_SIGN = "-"; - public static final float PLAYBACK_STEP_VALUE = 0.05f; + public static final char STEP_UP_SIGN = '+'; + public static final char STEP_DOWN_SIGN = '-'; + public static final double PLAYBACK_STEP_VALUE = 0.05f; - public static final float NIGHTCORE_TEMPO = 1.20f; - public static final float NIGHTCORE_PITCH_LOWER = 1.15f; - public static final float NIGHTCORE_PITCH_UPPER = 1.25f; + public static final double NIGHTCORE_TEMPO = 1.20f; + public static final double NIGHTCORE_PITCH_LOWER = 1.15f; + public static final double NIGHTCORE_PITCH_UPPER = 1.25f; - public static final float DEFAULT_TEMPO = 1.00f; - public static final float DEFAULT_PITCH = 1.00f; + public static final double DEFAULT_TEMPO = 1.00f; + public static final double DEFAULT_PITCH = 1.00f; - private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; - private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; + @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; + @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; public interface Callback { void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); } - private Callback callback; + @Nullable private Callback callback; - private float initialTempo = DEFAULT_TEMPO; - private float initialPitch = DEFAULT_PITCH; + @NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic( + MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE, + /*centerAt=*/1.00f, /*sliderGranularity=*/10000); - private SeekBar tempoSlider; - private TextView tempoMinimumText; - private TextView tempoMaximumText; - private TextView tempoCurrentText; - private TextView tempoStepDownText; - private TextView tempoStepUpText; + private double initialTempo = DEFAULT_TEMPO; + private double initialPitch = DEFAULT_PITCH; - private SeekBar pitchSlider; - private TextView pitchMinimumText; - private TextView pitchMaximumText; - private TextView pitchCurrentText; - private TextView pitchStepDownText; - private TextView pitchStepUpText; + @Nullable private SeekBar tempoSlider; + @Nullable private TextView tempoMinimumText; + @Nullable private TextView tempoMaximumText; + @Nullable private TextView tempoCurrentText; + @Nullable private TextView tempoStepDownText; + @Nullable private TextView tempoStepUpText; - private CheckBox unhookingCheckbox; + @Nullable private SeekBar pitchSlider; + @Nullable private TextView pitchMinimumText; + @Nullable private TextView pitchMaximumText; + @Nullable private TextView pitchCurrentText; + @Nullable private TextView pitchStepDownText; + @Nullable private TextView pitchStepUpText; - private TextView nightCorePresetText; - private TextView resetPresetText; + @Nullable private CheckBox unhookingCheckbox; - public static PlaybackParameterDialog newInstance(final float playbackTempo, - final float playbackPitch) { + @Nullable private TextView nightCorePresetText; + @Nullable private TextView resetPresetText; + + public static PlaybackParameterDialog newInstance(final double playbackTempo, + final double playbackPitch) { PlaybackParameterDialog dialog = new PlaybackParameterDialog(); dialog.initialTempo = playbackTempo; dialog.initialPitch = playbackPitch; @@ -91,16 +96,16 @@ public class PlaybackParameterDialog extends DialogFragment { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { - initialTempo = savedInstanceState.getFloat(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); - initialPitch = savedInstanceState.getFloat(INITIAL_PITCH_KEY, DEFAULT_PITCH); + initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); + initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putFloat(INITIAL_TEMPO_KEY, initialTempo); - outState.putFloat(INITIAL_PITCH_KEY, initialPitch); + outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); + outState.putDouble(INITIAL_PITCH_KEY, initialPitch); } /*////////////////////////////////////////////////////////////////////////// @@ -111,7 +116,7 @@ public class PlaybackParameterDialog extends DialogFragment { @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); - setupView(view); + setupControlViews(view); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) .setTitle(R.string.playback_speed_control) @@ -120,16 +125,16 @@ public class PlaybackParameterDialog extends DialogFragment { .setNegativeButton(R.string.cancel, (dialogInterface, i) -> setPlaybackParameters(initialTempo, initialPitch)) .setPositiveButton(R.string.finish, (dialogInterface, i) -> - setPlaybackParameters(getCurrentTempo(), getCurrentPitch())); + setCurrentPlaybackParameters()); return dialogBuilder.create(); } /*////////////////////////////////////////////////////////////////////////// - // Dialog Builder + // Control Views //////////////////////////////////////////////////////////////////////////*/ - private void setupView(@NonNull View rootView) { + private void setupControlViews(@NonNull View rootView) { setupHookingControl(rootView); setupTempoControl(rootView); setupPitchControl(rootView); @@ -144,45 +149,34 @@ public class PlaybackParameterDialog extends DialogFragment { tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); - tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); - tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); - tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); + if (tempoCurrentText != null) + tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); + if (tempoMaximumText != null) + tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); + if (tempoMinimumText != null) + tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); - tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - tempoStepUpText.setOnClickListener(view -> - setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE)); + if (tempoStepUpText != null) { + tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + tempoStepUpText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } - tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - tempoStepDownText.setOnClickListener(view -> - setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE)); + if (tempoStepDownText != null) { + tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + tempoStepDownText.setOnClickListener(view -> { + onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } - tempoSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); - tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialTempo)); - tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); - } - - private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() { - return new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - final float currentTempo = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); - if (fromUser) { // this change is first in chain - setTempo(currentTempo); - } else { - setPlaybackParameters(currentTempo, getCurrentPitch()); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - // Do Nothing. - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - // Do Nothing. - } - }; + if (tempoSlider != null) { + tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); + tempoSlider.setProgress(strategy.progressOf(initialTempo)); + tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); + } } private void setupPitchControl(@NonNull View rootView) { @@ -193,32 +187,85 @@ public class PlaybackParameterDialog extends DialogFragment { pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); - pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); - pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); - pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); + if (pitchCurrentText != null) + pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); + if (pitchMaximumText != null) + pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); + if (pitchMinimumText != null) + pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); - pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); - pitchStepUpText.setOnClickListener(view -> - setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE)); + if (pitchStepUpText != null) { + pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); + pitchStepUpText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } - pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); - pitchStepDownText.setOnClickListener(view -> - setPitch(getCurrentPitch() - PLAYBACK_STEP_VALUE)); + if (pitchStepDownText != null) { + pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); + pitchStepDownText.setOnClickListener(view -> { + onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE); + setCurrentPlaybackParameters(); + }); + } - pitchSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); - pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialPitch)); - pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); + if (pitchSlider != null) { + pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); + pitchSlider.setProgress(strategy.progressOf(initialPitch)); + pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); + } } - private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { + private void setupHookingControl(@NonNull View rootView) { + unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); + if (unhookingCheckbox != null) { + unhookingCheckbox.setChecked(initialPitch != initialTempo); + unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { + if (isChecked) return; + // When unchecked, slide back to the minimum of current tempo or pitch + final double minimum = Math.min(getCurrentPitch(), getCurrentTempo()); + setSliders(minimum); + setCurrentPlaybackParameters(); + }); + } + } + + private void setupPresetControl(@NonNull View rootView) { + nightCorePresetText = rootView.findViewById(R.id.presetNightcore); + if (nightCorePresetText != null) { + nightCorePresetText.setOnClickListener(view -> { + final double randomPitch = NIGHTCORE_PITCH_LOWER + + Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); + + setTempoSlider(NIGHTCORE_TEMPO); + setPitchSlider(randomPitch); + setCurrentPlaybackParameters(); + }); + } + + resetPresetText = rootView.findViewById(R.id.presetReset); + if (resetPresetText != null) { + resetPresetText.setOnClickListener(view -> { + setTempoSlider(DEFAULT_TEMPO); + setPitchSlider(DEFAULT_PITCH); + setCurrentPlaybackParameters(); + }); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Sliders + //////////////////////////////////////////////////////////////////////////*/ + + private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() { return new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - final float currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); - if (fromUser) { // this change is first in chain - setPitch(currentPitch); - } else { - setPlaybackParameters(getCurrentTempo(), currentPitch); + final double currentTempo = strategy.valueOf(progress); + if (fromUser) { + onTempoSliderUpdated(currentTempo); + setCurrentPlaybackParameters(); } } @@ -234,38 +281,30 @@ public class PlaybackParameterDialog extends DialogFragment { }; } - private void setupHookingControl(@NonNull View rootView) { - unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); - unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { - if (isChecked) return; - // When unchecked, slide back to the minimum of current tempo or pitch - final float minimum = Math.min(getCurrentPitch(), getCurrentTempo()); - setSliders(minimum); - }); + private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { + return new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final double currentPitch = strategy.valueOf(progress); + if (fromUser) { // this change is first in chain + onPitchSliderUpdated(currentPitch); + setCurrentPlaybackParameters(); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do Nothing. + } + }; } - private void setupPresetControl(@NonNull View rootView) { - nightCorePresetText = rootView.findViewById(R.id.presetNightcore); - nightCorePresetText.setOnClickListener(view -> { - final float randomPitch = NIGHTCORE_PITCH_LOWER + - (float) Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); - - setTempoSlider(NIGHTCORE_TEMPO); - setPitchSlider(randomPitch); - }); - - resetPresetText = rootView.findViewById(R.id.presetReset); - resetPresetText.setOnClickListener(view -> { - setTempoSlider(DEFAULT_TEMPO); - setPitchSlider(DEFAULT_PITCH); - }); - } - - /*////////////////////////////////////////////////////////////////////////// - // Helper - //////////////////////////////////////////////////////////////////////////*/ - - private void setTempo(final float newTempo) { + private void onTempoSliderUpdated(final double newTempo) { if (unhookingCheckbox == null) return; if (!unhookingCheckbox.isChecked()) { setSliders(newTempo); @@ -274,7 +313,7 @@ public class PlaybackParameterDialog extends DialogFragment { } } - private void setPitch(final float newPitch) { + private void onPitchSliderUpdated(final double newPitch) { if (unhookingCheckbox == null) return; if (!unhookingCheckbox.isChecked()) { setSliders(newPitch); @@ -283,25 +322,30 @@ public class PlaybackParameterDialog extends DialogFragment { } } - private void setSliders(final float newValue) { + private void setSliders(final double newValue) { setTempoSlider(newValue); setPitchSlider(newValue); } - private void setTempoSlider(final float newTempo) { + private void setTempoSlider(final double newTempo) { if (tempoSlider == null) return; - // seekbar doesn't register progress if it is the same as the existing progress - tempoSlider.setProgress(Integer.MAX_VALUE); - tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newTempo)); + tempoSlider.setProgress(strategy.progressOf(newTempo)); } - private void setPitchSlider(final float newPitch) { + private void setPitchSlider(final double newPitch) { if (pitchSlider == null) return; - pitchSlider.setProgress(Integer.MAX_VALUE); - pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newPitch)); + pitchSlider.setProgress(strategy.progressOf(newPitch)); } - private void setPlaybackParameters(final float tempo, final float pitch) { + /*////////////////////////////////////////////////////////////////////////// + // Helper + //////////////////////////////////////////////////////////////////////////*/ + + private void setCurrentPlaybackParameters() { + setPlaybackParameters(getCurrentTempo(), getCurrentPitch()); + } + + private void setPlaybackParameters(final double tempo, final double pitch) { if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { if (DEBUG) Log.d(TAG, "Setting playback parameters to " + "tempo=[" + tempo + "], " + @@ -309,40 +353,27 @@ public class PlaybackParameterDialog extends DialogFragment { tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); - callback.onPlaybackParameterChanged(tempo, pitch); + callback.onPlaybackParameterChanged((float) tempo, (float) pitch); } } - private float getCurrentTempo() { - return tempoSlider == null ? initialTempo : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, + private double getCurrentTempo() { + return tempoSlider == null ? initialTempo : strategy.valueOf( tempoSlider.getProgress()); } - private float getCurrentPitch() { - return pitchSlider == null ? initialPitch : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, + private double getCurrentPitch() { + return pitchSlider == null ? initialPitch : strategy.valueOf( pitchSlider.getProgress()); } - /** - * Converts from zeroed float with a minimum offset to the nearest rounded slider - * equivalent integer - * */ - private static int getSliderEquivalent(final float minimumValue, final float floatValue) { - return Math.round((floatValue - minimumValue) * 100f); - } - - /** - * Converts from slider integer value to an equivalent float value with a given minimum offset - * */ - private static float getSliderEquivalent(final float minimumValue, final int intValue) { - return ((float) intValue) / 100f + minimumValue; - } - - private static String getStepUpPercentString(final float percent) { + @NonNull + private static String getStepUpPercentString(final double percent) { return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); } - private static String getStepDownPercentString(final float percent) { + @NonNull + private static String getStepDownPercentString(final double percent) { return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index b34cec724..63ac7e8a1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -60,11 +60,11 @@ public class PlayerHelper { : stringFormatter.format("%02d:%02d", minutes, seconds).toString(); } - public static String formatSpeed(float speed) { + public static String formatSpeed(double speed) { return speedFormatter.format(speed); } - public static String formatPitch(float pitch) { + public static String formatPitch(double pitch) { return pitchFormatter.format(pitch); } diff --git a/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java b/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java new file mode 100644 index 000000000..efec1abb0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java @@ -0,0 +1,73 @@ +package org.schabi.newpipe.util; + +public interface SliderStrategy { + /** + * Converts from zeroed double with a minimum offset to the nearest rounded slider + * equivalent integer + * */ + int progressOf(final double value); + + /** + * Converts from slider integer value to an equivalent double value with a given + * minimum offset + * */ + double valueOf(final int progress); + + // TODO: also implement linear strategy when needed + + final class Quadratic implements SliderStrategy { + private final double leftGap; + private final double rightGap; + private final double center; + + private final int centerProgress; + + /** + * Quadratic slider strategy that scales the value of a slider given how far the slider + * progress is from the center of the slider. The further away from the center, + * the faster the interpreted value changes, and vice versa. + * + * @param minimum the minimum value of the interpreted value of the slider. + * @param maximum the maximum value of the interpreted value of the slider. + * @param center center of the interpreted value between the minimum and maximum, which + * will be used as the center value on the slider progress. Doesn't need + * to be the average of the minimum and maximum values, but must be in + * between the two. + * @param maxProgress the maximum possible progress of the slider, this is the + * value that is shown for the UI and controls the granularity of + * the slider. Should be as large as possible to avoid floating + * point round-off error. Using odd number is recommended. + * */ + public Quadratic(double minimum, double maximum, double center, int maxProgress) { + if (center < minimum || center > maximum) { + throw new IllegalArgumentException("Center must be in between minimum and maximum"); + } + + this.leftGap = minimum - center; + this.rightGap = maximum - center; + this.center = center; + + this.centerProgress = maxProgress / 2; + } + + @Override + public int progressOf(double value) { + final double difference = value - center; + final double root = difference >= 0 ? + Math.sqrt(difference / rightGap) : + -Math.sqrt(Math.abs(difference / leftGap)); + final double offset = Math.round(root * centerProgress); + + return (int) (centerProgress + offset); + } + + @Override + public double valueOf(int progress) { + final int offset = progress - centerProgress; + final double square = Math.pow(((double) offset) / ((double) centerProgress), 2); + final double difference = square * (offset >= 0 ? rightGap : leftGap); + + return difference + center; + } + } +} diff --git a/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java b/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java new file mode 100644 index 000000000..8c8d52043 --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/util/QuadraticSliderStrategyTest.java @@ -0,0 +1,86 @@ +package org.schabi.newpipe.util; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class QuadraticSliderStrategyTest { + private final static int STEP = 100; + private final static float DELTA = 1f / (float) STEP; + + private final SliderStrategy.Quadratic standard = + new SliderStrategy.Quadratic(0f, 100f, 50f, STEP); + @Test + public void testLeftBound() throws Exception { + assertEquals(standard.progressOf(0), 0); + assertEquals(standard.valueOf(0), 0f, DELTA); + } + + @Test + public void testCenter() throws Exception { + assertEquals(standard.progressOf(50), 50); + assertEquals(standard.valueOf(50), 50f, DELTA); + } + + @Test + public void testRightBound() throws Exception { + assertEquals(standard.progressOf(100), 100); + assertEquals(standard.valueOf(100), 100f, DELTA); + } + + @Test + public void testLeftRegion() throws Exception { + final int leftProgress = standard.progressOf(25); + final double leftValue = standard.valueOf(25); + assertTrue(leftProgress > 0 && leftProgress < 50); + assertTrue(leftValue > 0f && leftValue < 50); + } + + @Test + public void testRightRegion() throws Exception { + final int leftProgress = standard.progressOf(75); + final double leftValue = standard.valueOf(75); + assertTrue(leftProgress > 50 && leftProgress < 100); + assertTrue(leftValue > 50f && leftValue < 100); + } + + @Test + public void testConversion() throws Exception { + assertEquals(standard.progressOf(standard.valueOf(0)), 0); + assertEquals(standard.progressOf(standard.valueOf(25)), 25); + assertEquals(standard.progressOf(standard.valueOf(50)), 50); + assertEquals(standard.progressOf(standard.valueOf(75)), 75); + assertEquals(standard.progressOf(standard.valueOf(100)), 100); + } + + @Test + public void testReverseConversion() throws Exception { + // Need a larger delta since step size / granularity is too small and causes + // floating point round-off errors during conversion + final float largeDelta = 1f; + + assertEquals(standard.valueOf(standard.progressOf(0)), 0f, largeDelta); + assertEquals(standard.valueOf(standard.progressOf(25)), 25f, largeDelta); + assertEquals(standard.valueOf(standard.progressOf(50)), 50f, largeDelta); + assertEquals(standard.valueOf(standard.progressOf(75)), 75f, largeDelta); + assertEquals(standard.valueOf(standard.progressOf(100)), 100f, largeDelta); + } + + @Test + public void testQuadraticPropertyLeftRegion() throws Exception { + final double differenceCloserToCenter = + Math.abs(standard.valueOf(40) - standard.valueOf(45)); + final double differenceFurtherFromCenter = + Math.abs(standard.valueOf(10) - standard.valueOf(15)); + assertTrue(differenceCloserToCenter < differenceFurtherFromCenter); + } + + @Test + public void testQuadraticPropertyRightRegion() throws Exception { + final double differenceCloserToCenter = + Math.abs(standard.valueOf(75) - standard.valueOf(70)); + final double differenceFurtherFromCenter = + Math.abs(standard.valueOf(95) - standard.valueOf(90)); + assertTrue(differenceCloserToCenter < differenceFurtherFromCenter); + } +} From 8b60397f068c2d99f0d92d796400c6f77c2e5cd6 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 22 Mar 2018 18:11:59 -0700 Subject: [PATCH 274/276] -Changed detail fragment thumbnail failure to produce a snackbar error rather than a full error activity. --- .../fragments/detail/VideoDetailFragment.java | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 2a95125df..74e561f99 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -43,6 +43,7 @@ import android.widget.Toast; import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import org.schabi.newpipe.R; @@ -582,27 +583,20 @@ public class VideoDetailFragment }; } - private void initThumbnailViews(StreamInfo info) { + private void initThumbnailViews(@NonNull StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); if (!TextUtils.isEmpty(info.getThumbnailUrl())) { - imageLoader.displayImage( - info.getThumbnailUrl(), - thumbnailImageView, - ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, - new SimpleImageLoadingListener() { + final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); + final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - ErrorActivity.reportError( - activity, - failReason.getCause(), - null, - activity.findViewById(android.R.id.content), - ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, - NewPipe.getNameOfService(currentInfo.getServiceId()), - imageUri, - R.string.could_not_load_thumbnails)); + showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE, + infoServiceName, imageUri, R.string.could_not_load_thumbnails); } - }); + }; + + imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); } if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { From 72eaff148cbb473c840e59f4dd339c3c6c73abf2 Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 22 Mar 2018 18:12:11 -0700 Subject: [PATCH 275/276] -Fixed main player paused video not abandoning audio focus after navigating away from activity during interruption, when resume on focus regain is enabled. -Separated onPause and onPlay functions from onPlayPause. -Renamed onVideoPlayPause to onPlayPause. --- .../newpipe/player/BackgroundPlayer.java | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 33 ++++++++++++++----- .../newpipe/player/MainVideoPlayer.java | 7 ++-- .../newpipe/player/PopupVideoPlayer.java | 4 +-- .../newpipe/player/ServicePlayerActivity.java | 2 +- .../playback/BasePlayerMediaSession.java | 4 +-- 6 files changed, 33 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index f799941bd..ac070fb44 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -486,7 +486,7 @@ public final class BackgroundPlayer extends Service { onClose(); break; case ACTION_PLAY_PAUSE: - onVideoPlayPause(); + onPlayPause(); break; case ACTION_REPEAT: onRepeatClicked(); diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index fe19030f9..cd1451d37 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -392,7 +392,7 @@ public abstract class BasePlayer implements if (intent == null || intent.getAction() == null) return; switch (intent.getAction()) { case AudioManager.ACTION_AUDIO_BECOMING_NOISY: - if (isPlaying()) onVideoPlayPause(); + onPause(); break; } } @@ -948,14 +948,11 @@ public abstract class BasePlayer implements changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); } - public void onVideoPlayPause() { - if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); + public void onPlay() { + if (DEBUG) Log.d(TAG, "onPlay() called"); + if (audioReactor == null || playQueue == null || simpleExoPlayer == null) return; - if (!isPlaying()) { - audioReactor.requestAudioFocus(); - } else { - audioReactor.abandonAudioFocus(); - } + audioReactor.requestAudioFocus(); if (getCurrentState() == STATE_COMPLETED) { if (playQueue.getIndex() == 0) { @@ -965,7 +962,25 @@ public abstract class BasePlayer implements } } - simpleExoPlayer.setPlayWhenReady(!isPlaying()); + simpleExoPlayer.setPlayWhenReady(true); + } + + public void onPause() { + if (DEBUG) Log.d(TAG, "onPause() called"); + if (audioReactor == null || simpleExoPlayer == null) return; + + audioReactor.abandonAudioFocus(); + simpleExoPlayer.setPlayWhenReady(false); + } + + public void onPlayPause() { + if (DEBUG) Log.d(TAG, "onPlayPause() called"); + + if (!isPlaying()) { + onPlay(); + } else { + onPause(); + } } public void onFastRewind() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index cbc4b8230..dbc34b11a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -49,7 +49,6 @@ import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; @@ -153,7 +152,7 @@ public final class MainVideoPlayer extends AppCompatActivity if (DEBUG) Log.d(TAG, "onResume() called"); if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() && !playerImpl.isPlaying()) { - playerImpl.onVideoPlayPause(); + playerImpl.onPlay(); } activityPaused = false; @@ -188,7 +187,7 @@ public final class MainVideoPlayer extends AppCompatActivity if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { playerImpl.wasPlaying = playerImpl.isPlaying(); - if (playerImpl.isPlaying()) playerImpl.onVideoPlayPause(); + playerImpl.onPause(); } activityPaused = true; } @@ -563,7 +562,7 @@ public final class MainVideoPlayer extends AppCompatActivity public void onClick(View v) { super.onClick(v); if (v.getId() == playPauseButton.getId()) { - onVideoPlayPause(); + onPlayPause(); } else if (v.getId() == playPreviousButton.getId()) { onPlayPrevious(); diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 64dc03da6..20860d9c5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -618,7 +618,7 @@ public final class PopupVideoPlayer extends Service { onClose(); break; case ACTION_PLAY_PAUSE: - onVideoPlayPause(); + onPlayPause(); break; case ACTION_REPEAT: onRepeatClicked(); @@ -731,7 +731,7 @@ public final class PopupVideoPlayer extends Service { public boolean onSingleTapConfirmed(MotionEvent e) { if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); if (playerImpl == null || playerImpl.getPlayer() == null) return false; - playerImpl.onVideoPlayPause(); + playerImpl.onPlayPause(); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 1f850944d..994aa60e8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -424,7 +424,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity player.onPlayPrevious(); } else if (view.getId() == playPauseButton.getId()) { - player.onVideoPlayPause(); + player.onPlayPause(); } else if (view.getId() == forwardButton.getId()) { player.onPlayNext(); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java index 07504542c..616879917 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java @@ -62,12 +62,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback { @Override public void onPlay() { - if (!player.isPlaying()) player.onVideoPlayPause(); + player.onPlay(); } @Override public void onPause() { - if (player.isPlaying()) player.onVideoPlayPause(); + player.onPause(); } @Override From 02f48ccc7f4e0338557a47bc24d0993e2b4a71ff Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 22 Mar 2018 18:44:03 -0700 Subject: [PATCH 276/276] -Removed duplicate dialog open instances in service player activity. --- .../newpipe/player/ServicePlayerActivity.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 994aa60e8..239c9c8d3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -433,12 +433,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity player.onShuffleClicked(); } else if (view.getId() == playbackSpeedButton.getId()) { - PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), - player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + openPlaybackParameterDialog(); } else if (view.getId() == playbackPitchButton.getId()) { - PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), - player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + openPlaybackParameterDialog(); } else if (view.getId() == metadata.getId()) { scrollToSelected(); @@ -450,9 +448,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } //////////////////////////////////////////////////////////////////////////// - // Playback Parameters Listener + // Playback Parameters //////////////////////////////////////////////////////////////////////////// + private void openPlaybackParameterDialog() { + if (player == null) return; + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), + player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); + } + @Override public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);