├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── r2-testapp ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── Samples │ │ ├── 1.cbz │ │ ├── 1.epub │ │ ├── 2.epub │ │ ├── 3.epub │ │ ├── 4.epub │ │ ├── 5.epub │ │ ├── 6.epub │ │ └── flatland.json │ └── configs │ │ └── config.properties │ ├── ic_launcher-web.png │ ├── java │ └── org │ │ └── readium │ │ └── r2 │ │ └── testapp │ │ ├── MainActivity.kt │ │ ├── R2App.kt │ │ ├── about │ │ └── AboutFragment.kt │ │ ├── bookshelf │ │ ├── BookRepository.kt │ │ ├── BookshelfAdapter.kt │ │ ├── BookshelfFragment.kt │ │ └── BookshelfViewModel.kt │ │ ├── catalogs │ │ ├── CatalogDetailFragment.kt │ │ ├── CatalogFeedListAdapter.kt │ │ ├── CatalogFeedListFragment.kt │ │ ├── CatalogFeedListViewModel.kt │ │ ├── CatalogFragment.kt │ │ ├── CatalogListAdapter.kt │ │ ├── CatalogRepository.kt │ │ └── CatalogViewModel.kt │ │ ├── db │ │ ├── BooksDao.kt │ │ ├── CatalogDao.kt │ │ └── Database.kt │ │ ├── domain │ │ └── model │ │ │ ├── Book.kt │ │ │ ├── Bookmark.kt │ │ │ ├── Catalog.kt │ │ │ └── Highlight.kt │ │ ├── drm │ │ ├── DrmManagementContract.kt │ │ ├── DrmManagementFragment.kt │ │ ├── DrmManagementViewModel.kt │ │ └── LcpManagementViewModel.kt │ │ ├── epub │ │ └── UserSettings.kt │ │ ├── opds │ │ ├── GridAutoFitLayoutManager.kt │ │ └── OPDSDownloader.kt │ │ ├── outline │ │ ├── BookmarksFragment.kt │ │ ├── HighlightsFragment.kt │ │ ├── NavigationFragment.kt │ │ ├── OutlineContract.kt │ │ └── OutlineFragment.kt │ │ ├── reader │ │ ├── AudioReaderFragment.kt │ │ ├── AudiobookService.kt │ │ ├── BaseReaderFragment.kt │ │ ├── EpubReaderFragment.kt │ │ ├── ImageReaderFragment.kt │ │ ├── PdfReaderFragment.kt │ │ ├── ReaderActivity.kt │ │ ├── ReaderContract.kt │ │ ├── ReaderViewModel.kt │ │ ├── VisualReaderActivity.kt │ │ └── VisualReaderFragment.kt │ │ ├── search │ │ ├── SearchFragment.kt │ │ ├── SearchPagingSource.kt │ │ └── SearchResultAdapter.kt │ │ ├── tts │ │ ├── ScreenReaderContract.kt │ │ ├── ScreenReaderEngine.kt │ │ └── ScreenReaderFragment.kt │ │ └── utils │ │ ├── ContentResolverUtil.kt │ │ ├── EventChannel.kt │ │ ├── FragmentFactory.kt │ │ ├── R2DispatcherActivity.kt │ │ ├── SectionDecoration.kt │ │ ├── SingleClickListener.kt │ │ ├── SystemUiManagement.kt │ │ └── extensions │ │ ├── Bitmap.kt │ │ ├── Context.kt │ │ ├── File.kt │ │ ├── InputStream.kt │ │ ├── Link.kt │ │ ├── Metadata.kt │ │ ├── URL.kt │ │ └── Uri.kt │ └── res │ ├── drawable │ ├── background_action_mode.xml │ ├── cnl.png │ ├── cover.png │ ├── ic_add_white_24dp.xml │ ├── ic_baseline_bookmark_24.xml │ ├── ic_baseline_enhanced_encryption_24.xml │ ├── ic_baseline_fast_forward_24.xml │ ├── ic_baseline_fast_rewind_24.xml │ ├── ic_baseline_headphones_24.xml │ ├── ic_baseline_pause_24.xml │ ├── ic_baseline_play_arrow_24.xml │ ├── ic_baseline_search_24.xml │ ├── ic_baseline_skip_next_24.xml │ ├── ic_baseline_skip_previous_24.xml │ ├── ic_dashboard_black_24dp.xml │ ├── ic_delete.xml │ ├── ic_fastforward_30.xml │ ├── ic_info_black_24dp.xml │ ├── ic_local_library_black_24dp.xml │ ├── ic_notch.xml │ ├── ic_outline_add_24.xml │ ├── ic_outline_format_align_justify_24.xml │ ├── ic_outline_format_align_left_24.xml │ ├── ic_outline_format_size_24.xml │ ├── ic_outline_light_mode_24.xml │ ├── ic_outline_menu_24.xml │ ├── ic_outline_remove_24.xml │ ├── ic_outline_wb_sunny_24.xml │ ├── ic_pen.xml │ ├── ic_rewind_30.xml │ ├── icon_font_decrease.png │ ├── icon_font_increase.png │ ├── icon_overflow.png │ ├── rbtn_selector.xml │ ├── rbtn_textcolor_selector.xml │ ├── repfr.png │ ├── selector_blue.xml │ ├── selector_green.xml │ ├── selector_purple.xml │ ├── selector_red.xml │ └── selector_yellow.xml │ ├── layout │ ├── activity_epub.xml │ ├── activity_main.xml │ ├── activity_reader.xml │ ├── add_catalog_dialog.xml │ ├── filter_row.xml │ ├── filter_window.xml │ ├── fragment_about.xml │ ├── fragment_audiobook.xml │ ├── fragment_bookshelf.xml │ ├── fragment_catalog.xml │ ├── fragment_catalog_detail.xml │ ├── fragment_catalog_feed_list.xml │ ├── fragment_drm_management.xml │ ├── fragment_listview.xml │ ├── fragment_outline.xml │ ├── fragment_reader.xml │ ├── fragment_screen_reader.xml │ ├── fragment_search.xml │ ├── item_recycle_book.xml │ ├── item_recycle_bookmark.xml │ ├── item_recycle_catalog.xml │ ├── item_recycle_catalog_list.xml │ ├── item_recycle_highlight.xml │ ├── item_recycle_navigation.xml │ ├── item_recycle_search.xml │ ├── item_spinner_days.xml │ ├── popup_delete.xml │ ├── popup_note.xml │ ├── popup_passphrase.xml │ ├── popup_window_user_settings.xml │ ├── section_header.xml │ ├── view_action_mode.xml │ └── view_action_mode_reverse.xml │ ├── menu │ ├── bottom_nav_menu.xml │ ├── menu_action_mode.xml │ ├── menu_bookmark.xml │ ├── menu_epub.xml │ ├── menu_filter.xml │ └── menu_reader.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── navigation │ └── navigation.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── refs.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── network_security_config.xml └── settings.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug Report 11 | 12 | 15 | 16 | ### What happened? 17 | 18 | 23 | 24 | ### Expected behavior 25 | 26 | 27 | 28 | ### How to reproduce? 29 | 30 | 40 | 41 | ### Environment 42 | 43 | 44 | 45 | #### Readium versions 46 | 47 | 48 | 49 | * `r2-shared-kotlin`: 50 | * `r2-streamer-kotlin`: 51 | * `r2-navigator-kotlin`: 52 | * `r2-opds-kotlin`: 53 | * `r2-lcp-kotlin`: 54 | 55 | #### Development environment 56 | 57 | * OS: 58 | * IDE: 59 | 60 | #### Testing device 61 | 62 | * Android version: 63 | * Model: 64 | * Is it an emulator? Yes or No 65 | 66 | ### Additional context 67 | 68 | * Are you willing to fix the problem and contribute a pull request? Yes or No 69 | 70 | 77 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '11' 21 | distribution: 'adopt' 22 | - name: Build 23 | run: ./gradlew clean build 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | *.aab 11 | r2-testapp/release 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Readium 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **:warning: ᴛʜɪs ʀᴇᴘᴏsɪᴛᴏʀʏ ɪs ᴅᴇᴘʀᴇᴄᴀᴛᴇᴅ :warning:** 2 | > 3 | > We moved all the `r2-*-kotlin` modules to a single repository: [`kotlin-toolkit`](https://github.com/readium/kotlin-toolkit). 4 | 5 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.5.31' 5 | 6 | repositories { 7 | google() 8 | mavenCentral() 9 | jcenter() 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.0.2' 13 | 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | 16 | // NOTE: Do not place your application dependencies here; they belong 17 | // in the individual module build.gradle files 18 | } 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | google() 24 | mavenCentral() 25 | jcenter() 26 | maven { url 'https://jitpack.io' } 27 | maven { url "https://s3.amazonaws.com/repo.commonsware.com" } 28 | maven { url "https://oss.sonatype.org/content/repositories/snapshots" } 29 | } 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | org.gradle.parallel=true 18 | org.gradle.configureondemand=true 19 | android.useAndroidX=true 20 | android.enableJetifier=true 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /r2-testapp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /r2-testapp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /r2-testapp/src/main/assets/Samples/1.cbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/assets/Samples/1.cbz -------------------------------------------------------------------------------- /r2-testapp/src/main/assets/Samples/1.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/assets/Samples/1.epub -------------------------------------------------------------------------------- /r2-testapp/src/main/assets/Samples/2.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/assets/Samples/2.epub -------------------------------------------------------------------------------- /r2-testapp/src/main/assets/Samples/3.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/assets/Samples/3.epub -------------------------------------------------------------------------------- /r2-testapp/src/main/assets/Samples/4.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/assets/Samples/4.epub -------------------------------------------------------------------------------- /r2-testapp/src/main/assets/Samples/5.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/assets/Samples/5.epub -------------------------------------------------------------------------------- /r2-testapp/src/main/assets/Samples/6.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/assets/Samples/6.epub -------------------------------------------------------------------------------- /r2-testapp/src/main/assets/configs/config.properties: -------------------------------------------------------------------------------- 1 | useExternalFileDir=false -------------------------------------------------------------------------------- /r2-testapp/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp 8 | 9 | import android.os.Bundle 10 | import androidx.appcompat.app.AppCompatActivity 11 | import androidx.lifecycle.ViewModelProvider 12 | import androidx.navigation.NavController 13 | import androidx.navigation.fragment.NavHostFragment 14 | import androidx.navigation.ui.AppBarConfiguration 15 | import androidx.navigation.ui.setupActionBarWithNavController 16 | import androidx.navigation.ui.setupWithNavController 17 | import com.google.android.material.bottomnavigation.BottomNavigationView 18 | import org.readium.r2.testapp.bookshelf.BookshelfViewModel 19 | 20 | class MainActivity : AppCompatActivity() { 21 | 22 | private lateinit var navController: NavController 23 | private lateinit var viewModel: BookshelfViewModel 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | setContentView(R.layout.activity_main) 28 | 29 | viewModel = 30 | ViewModelProvider(this).get(BookshelfViewModel::class.java) 31 | 32 | intent.data?.let { 33 | viewModel.importPublicationFromUri(it) 34 | } 35 | 36 | val navView: BottomNavigationView = findViewById(R.id.nav_view) 37 | val navHostFragment = 38 | supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment 39 | navController = navHostFragment.navController 40 | 41 | val appBarConfiguration = AppBarConfiguration( 42 | setOf( 43 | R.id.navigation_bookshelf, R.id.navigation_catalog_list, R.id.navigation_about 44 | ) 45 | ) 46 | setupActionBarWithNavController(navController, appBarConfiguration) 47 | navView.setupWithNavController(navController) 48 | } 49 | 50 | override fun onSupportNavigateUp(): Boolean { 51 | return navController.navigateUp() || super.onSupportNavigateUp() 52 | } 53 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/R2App.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-testapp-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann 4 | * 5 | * Copyright (c) 2018. European Digital Reading Lab. All rights reserved. 6 | * Licensed to the Readium Foundation under one or more contributor license agreements. 7 | * Use of this source code is governed by a BSD-style license which is detailed in the 8 | * LICENSE file present in the project repository where this source code is maintained. 9 | */ 10 | 11 | package org.readium.r2.testapp 12 | 13 | import android.annotation.SuppressLint 14 | import android.app.Application 15 | import android.content.ContentResolver 16 | import android.content.Context 17 | import org.readium.r2.shared.Injectable 18 | import org.readium.r2.streamer.server.Server 19 | import org.readium.r2.testapp.BuildConfig.DEBUG 20 | import timber.log.Timber 21 | import java.io.IOException 22 | import java.net.ServerSocket 23 | import java.util.* 24 | 25 | class R2App : Application() { 26 | 27 | override fun onCreate() { 28 | super.onCreate() 29 | if (DEBUG) Timber.plant(Timber.DebugTree()) 30 | val s = ServerSocket(if (DEBUG) 8080 else 0) 31 | s.close() 32 | server = Server(s.localPort, applicationContext) 33 | startServer() 34 | R2DIRECTORY = r2Directory 35 | } 36 | 37 | companion object { 38 | @SuppressLint("StaticFieldLeak") 39 | lateinit var server: Server 40 | private set 41 | 42 | lateinit var R2DIRECTORY: String 43 | private set 44 | 45 | var isServerStarted = false 46 | private set 47 | } 48 | 49 | override fun onTerminate() { 50 | super.onTerminate() 51 | stopServer() 52 | } 53 | 54 | private fun startServer() { 55 | if (!server.isAlive) { 56 | try { 57 | server.start() 58 | } catch (e: IOException) { 59 | // do nothing 60 | if (DEBUG) Timber.e(e) 61 | } 62 | if (server.isAlive) { 63 | // // Add your own resources here 64 | // server.loadCustomResource(assets.open("scripts/test.js"), "test.js") 65 | // server.loadCustomResource(assets.open("styles/test.css"), "test.css") 66 | // server.loadCustomFont(assets.open("fonts/test.otf"), applicationContext, "test.otf") 67 | 68 | isServerStarted = true 69 | } 70 | } 71 | } 72 | 73 | private fun stopServer() { 74 | if (server.isAlive) { 75 | server.stop() 76 | isServerStarted = false 77 | } 78 | } 79 | 80 | private val r2Directory: String 81 | get() { 82 | val properties = Properties() 83 | val inputStream = applicationContext.assets.open("configs/config.properties") 84 | properties.load(inputStream) 85 | val useExternalFileDir = 86 | properties.getProperty("useExternalFileDir", "false")!!.toBoolean() 87 | return if (useExternalFileDir) { 88 | applicationContext.getExternalFilesDir(null)?.path + "/" 89 | } else { 90 | applicationContext.filesDir?.path + "/" 91 | } 92 | } 93 | } 94 | 95 | val Context.resolver: ContentResolver 96 | get() = applicationContext.contentResolver 97 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/about/AboutFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.about 8 | 9 | import android.os.Bundle 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import androidx.fragment.app.Fragment 14 | import org.readium.r2.testapp.R 15 | 16 | class AboutFragment : Fragment() { 17 | 18 | override fun onCreateView( 19 | inflater: LayoutInflater, 20 | container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View? { 23 | return inflater.inflate(R.layout.fragment_about, container, false) 24 | } 25 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/bookshelf/BookRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.bookshelf 8 | 9 | import androidx.annotation.ColorInt 10 | import androidx.lifecycle.LiveData 11 | import kotlinx.coroutines.flow.Flow 12 | import org.joda.time.DateTime 13 | import org.readium.r2.shared.publication.Locator 14 | import org.readium.r2.shared.publication.Publication 15 | import org.readium.r2.shared.publication.indexOfFirstWithHref 16 | import org.readium.r2.shared.util.mediatype.MediaType 17 | import org.readium.r2.testapp.db.BooksDao 18 | import org.readium.r2.testapp.domain.model.Book 19 | import org.readium.r2.testapp.domain.model.Bookmark 20 | import org.readium.r2.testapp.domain.model.Highlight 21 | import org.readium.r2.testapp.utils.extensions.authorName 22 | import java.util.* 23 | import org.readium.r2.navigator.epub.Highlight as NavigatorHighlight 24 | 25 | class BookRepository(private val booksDao: BooksDao) { 26 | 27 | fun books(): LiveData> = booksDao.getAllBooks() 28 | 29 | suspend fun get(id: Long) = booksDao.get(id) 30 | 31 | suspend fun insertBook(href: String, mediaType: MediaType, publication: Publication): Long { 32 | val book = Book( 33 | creation = DateTime().toDate().time, 34 | title = publication.metadata.title, 35 | author = publication.metadata.authorName, 36 | href = href, 37 | identifier = publication.metadata.identifier ?: "", 38 | type = mediaType.toString(), 39 | progression = "{}" 40 | ) 41 | return booksDao.insertBook(book) 42 | } 43 | 44 | suspend fun deleteBook(id: Long) = booksDao.deleteBook(id) 45 | 46 | suspend fun saveProgression(locator: Locator, bookId: Long) = 47 | booksDao.saveProgression(locator.toJSON().toString(), bookId) 48 | 49 | suspend fun insertBookmark(bookId: Long, publication: Publication, locator: Locator): Long { 50 | val resource = publication.readingOrder.indexOfFirstWithHref(locator.href)!! 51 | val bookmark = Bookmark( 52 | creation = DateTime().toDate().time, 53 | bookId = bookId, 54 | publicationId = publication.metadata.identifier ?: publication.metadata.title, 55 | resourceIndex = resource.toLong(), 56 | resourceHref = locator.href, 57 | resourceType = locator.type, 58 | resourceTitle = locator.title.orEmpty(), 59 | location = locator.locations.toJSON().toString(), 60 | locatorText = Locator.Text().toJSON().toString() 61 | ) 62 | 63 | return booksDao.insertBookmark(bookmark) 64 | } 65 | 66 | fun bookmarksForBook(bookId: Long): LiveData> = 67 | booksDao.getBookmarksForBook(bookId) 68 | 69 | suspend fun deleteBookmark(bookmarkId: Long) = booksDao.deleteBookmark(bookmarkId) 70 | 71 | suspend fun highlightById(id: Long): Highlight? = 72 | booksDao.getHighlightById(id) 73 | 74 | fun highlightsForBook(bookId: Long): Flow> = 75 | booksDao.getHighlightsForBook(bookId) 76 | 77 | suspend fun addHighlight(bookId: Long, style: Highlight.Style, @ColorInt tint: Int, locator: Locator, annotation: String): Long = 78 | booksDao.insertHighlight(Highlight(bookId, style, tint, locator, annotation)) 79 | 80 | suspend fun deleteHighlight(id: Long) = booksDao.deleteHighlight(id) 81 | 82 | suspend fun updateHighlightAnnotation(id: Long, annotation: String) { 83 | booksDao.updateHighlightAnnotation(id, annotation) 84 | } 85 | 86 | suspend fun updateHighlightStyle(id: Long, style: Highlight.Style, @ColorInt tint: Int) { 87 | booksDao.updateHighlightStyle(id, style, tint) 88 | } 89 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.bookshelf 8 | 9 | import android.view.LayoutInflater 10 | import android.view.ViewGroup 11 | import androidx.databinding.DataBindingUtil 12 | import androidx.recyclerview.widget.DiffUtil 13 | import androidx.recyclerview.widget.ListAdapter 14 | import androidx.recyclerview.widget.RecyclerView 15 | import org.readium.r2.testapp.R 16 | import org.readium.r2.testapp.databinding.ItemRecycleBookBinding 17 | import org.readium.r2.testapp.domain.model.Book 18 | import org.readium.r2.testapp.utils.singleClick 19 | 20 | 21 | class BookshelfAdapter( 22 | private val onBookClick: (Book) -> Unit, 23 | private val onBookLongClick: (Book) -> Unit 24 | ) : ListAdapter(BookListDiff()) { 25 | 26 | override fun onCreateViewHolder( 27 | parent: ViewGroup, 28 | viewType: Int 29 | ): ViewHolder { 30 | return ViewHolder( 31 | DataBindingUtil.inflate( 32 | LayoutInflater.from(parent.context), 33 | R.layout.item_recycle_book, parent, false 34 | ) 35 | ) 36 | } 37 | 38 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 39 | 40 | val book = getItem(position) 41 | 42 | viewHolder.bind(book) 43 | } 44 | 45 | inner class ViewHolder(private val binding: ItemRecycleBookBinding) : 46 | RecyclerView.ViewHolder(binding.root) { 47 | 48 | fun bind(book: Book) { 49 | binding.book = book 50 | binding.root.singleClick { 51 | onBookClick(book) 52 | } 53 | binding.root.setOnLongClickListener { 54 | onBookLongClick(book) 55 | true 56 | } 57 | } 58 | } 59 | 60 | private class BookListDiff : DiffUtil.ItemCallback() { 61 | 62 | override fun areItemsTheSame( 63 | oldItem: Book, 64 | newItem: Book 65 | ): Boolean { 66 | return oldItem.id == newItem.id 67 | } 68 | 69 | override fun areContentsTheSame( 70 | oldItem: Book, 71 | newItem: Book 72 | ): Boolean { 73 | return oldItem.title == newItem.title 74 | && oldItem.href == newItem.href 75 | && oldItem.author == newItem.author 76 | && oldItem.identifier == newItem.identifier 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/catalogs/CatalogDetailFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.catalogs 8 | 9 | import android.os.Bundle 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import androidx.databinding.DataBindingUtil 14 | import androidx.fragment.app.Fragment 15 | import androidx.fragment.app.viewModels 16 | import com.google.android.material.snackbar.Snackbar 17 | import com.squareup.picasso.Picasso 18 | import org.readium.r2.shared.extensions.getPublicationOrNull 19 | import org.readium.r2.shared.publication.Publication 20 | import org.readium.r2.shared.publication.opds.images 21 | import org.readium.r2.testapp.MainActivity 22 | import org.readium.r2.testapp.R 23 | import org.readium.r2.testapp.databinding.FragmentCatalogDetailBinding 24 | 25 | 26 | class CatalogDetailFragment : Fragment() { 27 | 28 | private var publication: Publication? = null 29 | private val catalogViewModel: CatalogViewModel by viewModels() 30 | 31 | private var _binding: FragmentCatalogDetailBinding? = null 32 | private val binding get() = _binding!! 33 | 34 | override fun onCreateView( 35 | inflater: LayoutInflater, container: ViewGroup?, 36 | savedInstanceState: Bundle? 37 | ): View { 38 | _binding = DataBindingUtil.inflate( 39 | LayoutInflater.from(context), 40 | R.layout.fragment_catalog_detail, container, false 41 | ) 42 | catalogViewModel.detailChannel.receive(this) { handleEvent(it) } 43 | publication = arguments?.getPublicationOrNull() 44 | binding.publication = publication 45 | binding.viewModel = catalogViewModel 46 | return binding.root 47 | } 48 | 49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 50 | super.onViewCreated(view, savedInstanceState) 51 | (activity as MainActivity).supportActionBar?.title = publication?.metadata?.title 52 | 53 | Picasso.with(requireContext()).load(publication?.images?.first()?.href) 54 | .into(binding.catalogDetailCoverImage) 55 | 56 | binding.catalogDetailDownloadButton.setOnClickListener { 57 | publication?.let { it1 -> 58 | catalogViewModel.downloadPublication( 59 | it1 60 | ) 61 | } 62 | } 63 | } 64 | 65 | private fun handleEvent(event: CatalogViewModel.Event.DetailEvent) { 66 | val message = 67 | when (event) { 68 | is CatalogViewModel.Event.DetailEvent.ImportPublicationSuccess -> getString(R.string.import_publication_success) 69 | is CatalogViewModel.Event.DetailEvent.ImportPublicationFailed -> getString(R.string.unable_add_pub_database) 70 | } 71 | Snackbar.make( 72 | requireView(), 73 | message, 74 | Snackbar.LENGTH_LONG 75 | ).show() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.catalogs 8 | 9 | import android.view.LayoutInflater 10 | import android.view.ViewGroup 11 | import androidx.core.os.bundleOf 12 | import androidx.databinding.DataBindingUtil 13 | import androidx.navigation.Navigation 14 | import androidx.recyclerview.widget.DiffUtil 15 | import androidx.recyclerview.widget.ListAdapter 16 | import androidx.recyclerview.widget.RecyclerView 17 | import org.readium.r2.testapp.R 18 | import org.readium.r2.testapp.databinding.ItemRecycleCatalogListBinding 19 | import org.readium.r2.testapp.domain.model.Catalog 20 | 21 | class CatalogFeedListAdapter(private val onLongClick: (Catalog) -> Unit) : 22 | ListAdapter(CatalogListDiff()) { 23 | 24 | override fun onCreateViewHolder( 25 | parent: ViewGroup, 26 | viewType: Int 27 | ): ViewHolder { 28 | return ViewHolder( 29 | DataBindingUtil.inflate( 30 | LayoutInflater.from(parent.context), 31 | R.layout.item_recycle_catalog_list, parent, false 32 | ) 33 | ) 34 | } 35 | 36 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 37 | 38 | val catalog = getItem(position) 39 | 40 | viewHolder.bind(catalog) 41 | } 42 | 43 | inner class ViewHolder(private val binding: ItemRecycleCatalogListBinding) : 44 | RecyclerView.ViewHolder(binding.root) { 45 | 46 | fun bind(catalog: Catalog) { 47 | binding.catalog = catalog 48 | binding.catalogListButton.setOnClickListener { 49 | val bundle = bundleOf(CATALOGFEED to catalog) 50 | Navigation.findNavController(it) 51 | .navigate(R.id.action_navigation_catalog_list_to_navigation_catalog, bundle) 52 | } 53 | binding.catalogListButton.setOnLongClickListener { 54 | onLongClick(catalog) 55 | true 56 | } 57 | } 58 | } 59 | 60 | companion object { 61 | const val CATALOGFEED = "catalogFeed" 62 | } 63 | 64 | private class CatalogListDiff : DiffUtil.ItemCallback() { 65 | 66 | override fun areItemsTheSame( 67 | oldItem: Catalog, 68 | newItem: Catalog 69 | ): Boolean { 70 | return oldItem.id == newItem.id 71 | } 72 | 73 | override fun areContentsTheSame( 74 | oldItem: Catalog, 75 | newItem: Catalog 76 | ): Boolean { 77 | return oldItem.title == newItem.title 78 | && oldItem.href == newItem.href 79 | && oldItem.type == newItem.type 80 | } 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.catalogs 8 | 9 | import android.app.Application 10 | import androidx.lifecycle.AndroidViewModel 11 | import androidx.lifecycle.viewModelScope 12 | import kotlinx.coroutines.channels.Channel 13 | import kotlinx.coroutines.launch 14 | import org.json.JSONObject 15 | import org.readium.r2.opds.OPDS1Parser 16 | import org.readium.r2.opds.OPDS2Parser 17 | import org.readium.r2.shared.opds.ParseData 18 | import org.readium.r2.shared.util.Try 19 | import org.readium.r2.shared.util.http.DefaultHttpClient 20 | import org.readium.r2.shared.util.http.HttpRequest 21 | import org.readium.r2.shared.util.http.fetchWithDecoder 22 | import org.readium.r2.testapp.db.BookDatabase 23 | import org.readium.r2.testapp.domain.model.Catalog 24 | import org.readium.r2.testapp.utils.EventChannel 25 | import java.net.URL 26 | 27 | class CatalogFeedListViewModel(application: Application) : AndroidViewModel(application) { 28 | 29 | private val catalogDao = BookDatabase.getDatabase(application).catalogDao() 30 | private val repository = CatalogRepository(catalogDao) 31 | val eventChannel = EventChannel(Channel(Channel.BUFFERED), viewModelScope) 32 | 33 | val catalogs = repository.getCatalogsFromDatabase() 34 | 35 | fun insertCatalog(catalog: Catalog) = viewModelScope.launch { 36 | repository.insertCatalog(catalog) 37 | } 38 | 39 | fun deleteCatalog(id: Long) = viewModelScope.launch { 40 | repository.deleteCatalog(id) 41 | } 42 | 43 | fun parseCatalog(url: String, title: String) = viewModelScope.launch { 44 | val parseData = parseURL(URL(url)) 45 | parseData.onSuccess { data -> 46 | val catalog = Catalog( 47 | title = title, 48 | href = url, 49 | type = data.type 50 | ) 51 | insertCatalog(catalog) 52 | } 53 | parseData.onFailure { 54 | eventChannel.send(Event.FeedListEvent.CatalogParseFailed) 55 | } 56 | } 57 | 58 | private suspend fun parseURL(url: URL): Try { 59 | return DefaultHttpClient().fetchWithDecoder(HttpRequest(url.toString())) { 60 | val result = it.body 61 | if (isJson(result)) { 62 | OPDS2Parser.parse(result, url) 63 | } else { 64 | OPDS1Parser.parse(result, url) 65 | } 66 | } 67 | } 68 | 69 | private fun isJson(byteArray: ByteArray): Boolean { 70 | return try { 71 | JSONObject(String(byteArray)) 72 | true 73 | } catch (e: Exception) { 74 | false 75 | } 76 | } 77 | 78 | sealed class Event { 79 | 80 | sealed class FeedListEvent : Event() { 81 | 82 | object CatalogParseFailed : FeedListEvent() 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/catalogs/CatalogListAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.catalogs 8 | 9 | import android.os.Bundle 10 | import android.view.LayoutInflater 11 | import android.view.ViewGroup 12 | import androidx.databinding.DataBindingUtil 13 | import androidx.navigation.Navigation 14 | import androidx.recyclerview.widget.DiffUtil 15 | import androidx.recyclerview.widget.ListAdapter 16 | import androidx.recyclerview.widget.RecyclerView 17 | import com.squareup.picasso.Picasso 18 | import org.readium.r2.shared.extensions.putPublication 19 | import org.readium.r2.shared.publication.Publication 20 | import org.readium.r2.shared.publication.opds.images 21 | import org.readium.r2.testapp.R 22 | import org.readium.r2.testapp.databinding.ItemRecycleCatalogBinding 23 | 24 | class CatalogListAdapter : 25 | ListAdapter(PublicationListDiff()) { 26 | 27 | override fun onCreateViewHolder( 28 | parent: ViewGroup, 29 | viewType: Int 30 | ): ViewHolder { 31 | return ViewHolder( 32 | DataBindingUtil.inflate( 33 | LayoutInflater.from(parent.context), 34 | R.layout.item_recycle_catalog, parent, false 35 | ) 36 | ) 37 | } 38 | 39 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 40 | 41 | val publication = getItem(position) 42 | 43 | viewHolder.bind(publication) 44 | } 45 | 46 | inner class ViewHolder(private val binding: ItemRecycleCatalogBinding) : 47 | RecyclerView.ViewHolder(binding.root) { 48 | 49 | fun bind(publication: Publication) { 50 | binding.catalogListTitleText.text = publication.metadata.title 51 | 52 | publication.linkWithRel("http://opds-spec.org/image/thumbnail")?.let { link -> 53 | Picasso.with(binding.catalogListCoverImage.context).load(link.href) 54 | .into(binding.catalogListCoverImage) 55 | } ?: run { 56 | if (publication.images.isNotEmpty()) { 57 | Picasso.with(binding.catalogListCoverImage.context) 58 | .load(publication.images.first().href).into(binding.catalogListCoverImage) 59 | } 60 | } 61 | 62 | binding.root.setOnClickListener { 63 | val bundle = Bundle().apply { 64 | putPublication(publication) 65 | } 66 | Navigation.findNavController(it) 67 | .navigate(R.id.action_navigation_catalog_to_navigation_catalog_detail, bundle) 68 | } 69 | } 70 | } 71 | 72 | private class PublicationListDiff : DiffUtil.ItemCallback() { 73 | 74 | override fun areItemsTheSame( 75 | oldItem: Publication, 76 | newItem: Publication 77 | ): Boolean { 78 | return oldItem.metadata.identifier == newItem.metadata.identifier 79 | } 80 | 81 | override fun areContentsTheSame( 82 | oldItem: Publication, 83 | newItem: Publication 84 | ): Boolean { 85 | return oldItem.jsonManifest == newItem.jsonManifest 86 | } 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/catalogs/CatalogRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.catalogs 8 | 9 | 10 | import androidx.lifecycle.LiveData 11 | import org.readium.r2.testapp.db.CatalogDao 12 | import org.readium.r2.testapp.domain.model.Catalog 13 | 14 | class CatalogRepository(private val catalogDao: CatalogDao) { 15 | 16 | suspend fun insertCatalog(catalog: Catalog): Long { 17 | return catalogDao.insertCatalog(catalog) 18 | } 19 | 20 | fun getCatalogsFromDatabase(): LiveData> = catalogDao.getCatalogModels() 21 | 22 | suspend fun deleteCatalog(id: Long) = catalogDao.deleteCatalog(id) 23 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/db/BooksDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.db 8 | 9 | import androidx.annotation.ColorInt 10 | import androidx.lifecycle.LiveData 11 | import androidx.room.Dao 12 | import androidx.room.Insert 13 | import androidx.room.OnConflictStrategy 14 | import androidx.room.Query 15 | import kotlinx.coroutines.flow.Flow 16 | import org.readium.r2.testapp.domain.model.Book 17 | import org.readium.r2.testapp.domain.model.Bookmark 18 | import org.readium.r2.testapp.domain.model.Highlight 19 | 20 | 21 | @Dao 22 | interface BooksDao { 23 | 24 | /** 25 | * Inserts a book 26 | * @param book The book to insert 27 | * @return ID of the book that was added (primary key) 28 | */ 29 | @Insert(onConflict = OnConflictStrategy.REPLACE) 30 | suspend fun insertBook(book: Book): Long 31 | 32 | /** 33 | * Deletes a book 34 | * @param bookId The ID of the book 35 | */ 36 | @Query("DELETE FROM " + Book.TABLE_NAME + " WHERE " + Book.ID + " = :bookId") 37 | suspend fun deleteBook(bookId: Long) 38 | 39 | /** 40 | * Retrieve a book from its ID. 41 | */ 42 | @Query("SELECT * FROM " + Book.TABLE_NAME + " WHERE " + Book.ID + " = :id") 43 | suspend fun get(id: Long): Book? 44 | 45 | /** 46 | * Retrieve all books 47 | * @return List of books as LiveData 48 | */ 49 | @Query("SELECT * FROM " + Book.TABLE_NAME + " ORDER BY " + Book.CREATION_DATE + " desc") 50 | fun getAllBooks(): LiveData> 51 | 52 | /** 53 | * Retrieve all bookmarks for a specific book 54 | * @param bookId The ID of the book 55 | * @return List of bookmarks for the book as LiveData 56 | */ 57 | @Query("SELECT * FROM " + Bookmark.TABLE_NAME + " WHERE " + Bookmark.BOOK_ID + " = :bookId") 58 | fun getBookmarksForBook(bookId: Long): LiveData> 59 | 60 | /** 61 | * Retrieve all highlights for a specific book 62 | */ 63 | @Query("SELECT * FROM ${Highlight.TABLE_NAME} WHERE ${Highlight.BOOK_ID} = :bookId ORDER BY ${Highlight.TOTAL_PROGRESSION} ASC") 64 | fun getHighlightsForBook(bookId: Long): Flow> 65 | 66 | /** 67 | * Retrieves the highlight with the given ID. 68 | */ 69 | @Query("SELECT * FROM ${Highlight.TABLE_NAME} WHERE ${Highlight.ID} = :highlightId") 70 | suspend fun getHighlightById(highlightId: Long): Highlight? 71 | 72 | /** 73 | * Inserts a bookmark 74 | * @param bookmark The bookmark to insert 75 | * @return The ID of the bookmark that was added (primary key) 76 | */ 77 | @Insert(onConflict = OnConflictStrategy.IGNORE) 78 | suspend fun insertBookmark(bookmark: Bookmark): Long 79 | 80 | /** 81 | * Inserts a highlight 82 | * @param highlight The highlight to insert 83 | * @return The ID of the highlight that was added (primary key) 84 | */ 85 | @Insert(onConflict = OnConflictStrategy.REPLACE) 86 | suspend fun insertHighlight(highlight: Highlight): Long 87 | 88 | /** 89 | * Updates a highlight's annotation. 90 | */ 91 | @Query("UPDATE ${Highlight.TABLE_NAME} SET ${Highlight.ANNOTATION} = :annotation WHERE ${Highlight.ID} = :id") 92 | suspend fun updateHighlightAnnotation(id: Long, annotation: String) 93 | 94 | /** 95 | * Updates a highlight's tint and style. 96 | */ 97 | @Query("UPDATE ${Highlight.TABLE_NAME} SET ${Highlight.TINT} = :tint, ${Highlight.STYLE} = :style WHERE ${Highlight.ID} = :id") 98 | suspend fun updateHighlightStyle(id: Long, style: Highlight.Style, @ColorInt tint: Int) 99 | 100 | /** 101 | * Deletes a bookmark 102 | */ 103 | @Query("DELETE FROM " + Bookmark.TABLE_NAME + " WHERE " + Bookmark.ID + " = :id") 104 | suspend fun deleteBookmark(id: Long) 105 | 106 | /** 107 | * Deletes the highlight with given id. 108 | */ 109 | @Query("DELETE FROM ${Highlight.TABLE_NAME} WHERE ${Highlight.ID} = :id") 110 | suspend fun deleteHighlight(id: Long) 111 | 112 | /** 113 | * Saves book progression 114 | * @param locator Location of the book 115 | * @param id The book to update 116 | */ 117 | @Query("UPDATE " + Book.TABLE_NAME + " SET " + Book.PROGRESSION + " = :locator WHERE " + Book.ID + "= :id") 118 | suspend fun saveProgression(locator: String, id: Long) 119 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/db/CatalogDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.db 8 | 9 | import androidx.lifecycle.LiveData 10 | import androidx.room.Dao 11 | import androidx.room.Insert 12 | import androidx.room.OnConflictStrategy 13 | import androidx.room.Query 14 | import org.readium.r2.testapp.domain.model.Catalog 15 | 16 | @Dao 17 | interface CatalogDao { 18 | 19 | /** 20 | * Inserts an Catalog 21 | * @param catalog The Catalog model to insert 22 | * @return ID of the Catalog model that was added (primary key) 23 | */ 24 | @Insert(onConflict = OnConflictStrategy.REPLACE) 25 | suspend fun insertCatalog(catalog: Catalog): Long 26 | 27 | /** 28 | * Retrieve list of Catalog models based on Catalog model 29 | * @return List of Catalog models as LiveData 30 | */ 31 | @Query("SELECT * FROM " + Catalog.TABLE_NAME + " WHERE " + Catalog.TITLE + " = :title AND " + Catalog.HREF + " = :href AND " + Catalog.TYPE + " = :type") 32 | fun getCatalogModels(title: String, href: String, type: Int): LiveData> 33 | 34 | /** 35 | * Retrieve list of all Catalog models 36 | * @return List of Catalog models as LiveData 37 | */ 38 | @Query("SELECT * FROM " + Catalog.TABLE_NAME) 39 | fun getCatalogModels(): LiveData> 40 | 41 | /** 42 | * Deletes an Catalog model 43 | * @param id The id of the Catalog model to delete 44 | */ 45 | @Query("DELETE FROM " + Catalog.TABLE_NAME + " WHERE " + Catalog.ID + " = :id") 46 | suspend fun deleteCatalog(id: Long) 47 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/db/Database.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.db 8 | 9 | import android.content.Context 10 | import androidx.room.Database 11 | import androidx.room.Room 12 | import androidx.room.RoomDatabase 13 | import androidx.room.TypeConverters 14 | import org.readium.r2.testapp.domain.model.* 15 | 16 | @Database( 17 | entities = [Book::class, Bookmark::class, Highlight::class, Catalog::class], 18 | version = 1, 19 | exportSchema = false 20 | ) 21 | @TypeConverters(HighlightConverters::class) 22 | abstract class BookDatabase : RoomDatabase() { 23 | 24 | abstract fun booksDao(): BooksDao 25 | 26 | abstract fun catalogDao(): CatalogDao 27 | 28 | companion object { 29 | @Volatile 30 | private var INSTANCE: BookDatabase? = null 31 | 32 | fun getDatabase(context: Context): BookDatabase { 33 | val tempInstance = INSTANCE 34 | if (tempInstance != null) { 35 | return tempInstance 36 | } 37 | synchronized(this) { 38 | val instance = Room.databaseBuilder( 39 | context.applicationContext, 40 | BookDatabase::class.java, 41 | "books_database" 42 | ).build() 43 | INSTANCE = instance 44 | return instance 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/domain/model/Book.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.domain.model 8 | 9 | import android.net.Uri 10 | import android.os.Build 11 | import androidx.room.ColumnInfo 12 | import androidx.room.Entity 13 | import androidx.room.PrimaryKey 14 | import org.readium.r2.shared.util.mediatype.MediaType 15 | import java.net.URI 16 | import java.nio.file.Paths 17 | 18 | 19 | @Entity(tableName = Book.TABLE_NAME) 20 | data class Book( 21 | @PrimaryKey 22 | @ColumnInfo(name = ID) 23 | var id: Long? = null, 24 | @ColumnInfo(name = Bookmark.CREATION_DATE, defaultValue = "CURRENT_TIMESTAMP") 25 | val creation: Long? = null, 26 | @ColumnInfo(name = HREF) 27 | val href: String, 28 | @ColumnInfo(name = TITLE) 29 | val title: String, 30 | @ColumnInfo(name = AUTHOR) 31 | val author: String? = null, 32 | @ColumnInfo(name = IDENTIFIER) 33 | val identifier: String, 34 | @ColumnInfo(name = PROGRESSION) 35 | val progression: String? = null, 36 | @ColumnInfo(name = TYPE) 37 | val type: String 38 | ) { 39 | 40 | val fileName: String? 41 | get() { 42 | val url = URI(href) 43 | if (!url.scheme.isNullOrEmpty() && url.isAbsolute) { 44 | val uri = Uri.parse(href) 45 | return uri.lastPathSegment 46 | } 47 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 48 | val path = Paths.get(href) 49 | path.fileName.toString() 50 | } else { 51 | val uri = Uri.parse(href) 52 | uri.lastPathSegment 53 | } 54 | } 55 | 56 | val url: URI? 57 | get() { 58 | val url = URI(href) 59 | if (url.isAbsolute && url.scheme.isNullOrEmpty()) { 60 | return null 61 | } 62 | return url 63 | } 64 | 65 | suspend fun mediaType(): MediaType? = MediaType.of(type) 66 | 67 | companion object { 68 | 69 | const val TABLE_NAME = "books" 70 | const val ID = "id" 71 | const val CREATION_DATE = "creation_date" 72 | const val HREF = "href" 73 | const val TITLE = "title" 74 | const val AUTHOR = "author" 75 | const val IDENTIFIER = "identifier" 76 | const val PROGRESSION = "progression" 77 | const val TYPE = "type" 78 | } 79 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/domain/model/Bookmark.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.domain.model 8 | 9 | import androidx.room.ColumnInfo 10 | import androidx.room.Entity 11 | import androidx.room.Index 12 | import androidx.room.PrimaryKey 13 | import org.json.JSONObject 14 | import org.readium.r2.shared.publication.Locator 15 | 16 | @Entity( 17 | tableName = Bookmark.TABLE_NAME, indices = [Index( 18 | value = ["BOOK_ID", "LOCATION"], 19 | unique = true 20 | )] 21 | ) 22 | data class Bookmark( 23 | @PrimaryKey 24 | @ColumnInfo(name = ID) 25 | var id: Long? = null, 26 | @ColumnInfo(name = CREATION_DATE, defaultValue = "CURRENT_TIMESTAMP") 27 | var creation: Long? = null, 28 | @ColumnInfo(name = BOOK_ID) 29 | val bookId: Long, 30 | @ColumnInfo(name = PUBLICATION_ID) 31 | val publicationId: String, 32 | @ColumnInfo(name = RESOURCE_INDEX) 33 | val resourceIndex: Long, 34 | @ColumnInfo(name = RESOURCE_HREF) 35 | val resourceHref: String, 36 | @ColumnInfo(name = RESOURCE_TYPE) 37 | val resourceType: String, 38 | @ColumnInfo(name = RESOURCE_TITLE) 39 | val resourceTitle: String, 40 | @ColumnInfo(name = LOCATION) 41 | val location: String, 42 | @ColumnInfo(name = LOCATOR_TEXT) 43 | val locatorText: String 44 | ) { 45 | 46 | val locator 47 | get() = Locator( 48 | href = resourceHref, 49 | type = resourceType, 50 | title = resourceTitle, 51 | locations = Locator.Locations.fromJSON(JSONObject(location)), 52 | text = Locator.Text.fromJSON(JSONObject(locatorText)) 53 | ) 54 | 55 | companion object { 56 | 57 | const val TABLE_NAME = "BOOKMARKS" 58 | const val ID = "ID" 59 | const val CREATION_DATE = "CREATION_DATE" 60 | const val BOOK_ID = "BOOK_ID" 61 | const val PUBLICATION_ID = "PUBLICATION_ID" 62 | const val RESOURCE_INDEX = "RESOURCE_INDEX" 63 | const val RESOURCE_HREF = "RESOURCE_HREF" 64 | const val RESOURCE_TYPE = "RESOURCE_TYPE" 65 | const val RESOURCE_TITLE = "RESOURCE_TITLE" 66 | const val LOCATION = "LOCATION" 67 | const val LOCATOR_TEXT = "LOCATOR_TEXT" 68 | } 69 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/domain/model/Catalog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.domain.model 8 | 9 | import android.os.Parcelable 10 | import androidx.room.ColumnInfo 11 | import androidx.room.Entity 12 | import androidx.room.PrimaryKey 13 | import kotlinx.parcelize.Parcelize 14 | 15 | @Parcelize 16 | @Entity(tableName = Catalog.TABLE_NAME) 17 | data class Catalog( 18 | @PrimaryKey 19 | @ColumnInfo(name = ID) 20 | var id: Long? = null, 21 | @ColumnInfo(name = TITLE) 22 | var title: String, 23 | @ColumnInfo(name = HREF) 24 | var href: String, 25 | @ColumnInfo(name = TYPE) 26 | var type: Int 27 | ) : Parcelable { 28 | companion object { 29 | 30 | const val TABLE_NAME = "CATALOG" 31 | const val ID = "ID" 32 | const val TITLE = "TITLE" 33 | const val HREF = "HREF" 34 | const val TYPE = "TYPE" 35 | } 36 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/drm/DrmManagementContract.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.drm 8 | 9 | import android.os.Bundle 10 | 11 | object DrmManagementContract { 12 | 13 | private const val HAS_RETURNED_KEY = "hasReturned" 14 | 15 | val REQUEST_KEY: String = DrmManagementContract::class.java.name 16 | 17 | data class Result(val hasReturned: Boolean) 18 | 19 | fun createResult(hasReturned: Boolean): Bundle { 20 | return Bundle().apply { 21 | putBoolean(HAS_RETURNED_KEY, hasReturned) 22 | } 23 | } 24 | 25 | fun parseResult(result: Bundle): Result { 26 | val hasReturned = requireNotNull(result.getBoolean(HAS_RETURNED_KEY)) 27 | return Result(hasReturned) 28 | } 29 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.drm 8 | 9 | import androidx.fragment.app.Fragment 10 | import androidx.lifecycle.ViewModel 11 | import org.readium.r2.shared.util.Try 12 | import java.util.* 13 | 14 | abstract class DrmManagementViewModel : ViewModel() { 15 | 16 | abstract val type: String 17 | 18 | open val state: String? = null 19 | 20 | open val provider: String? = null 21 | 22 | open val issued: Date? = null 23 | 24 | open val updated: Date? = null 25 | 26 | open val start: Date? = null 27 | 28 | open val end: Date? = null 29 | 30 | open val copiesLeft: String = "unlimited" 31 | 32 | open val printsLeft: String = "unlimited" 33 | 34 | open val canRenewLoan: Boolean = false 35 | 36 | open suspend fun renewLoan(fragment: Fragment): Try = 37 | Try.failure(Exception("Renewing a loan is not supported")) 38 | 39 | open val canReturnPublication: Boolean = false 40 | 41 | open suspend fun returnPublication(): Try = 42 | Try.failure(Exception("Returning a publication is not supported")) 43 | } 44 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.drm 8 | 9 | import androidx.fragment.app.Fragment 10 | import androidx.lifecycle.ViewModel 11 | import androidx.lifecycle.ViewModelProvider 12 | import org.readium.r2.lcp.LcpLicense 13 | import org.readium.r2.lcp.MaterialRenewListener 14 | import org.readium.r2.shared.util.Try 15 | import java.util.* 16 | 17 | class LcpManagementViewModel( 18 | private val lcpLicense: LcpLicense, 19 | private val renewListener: LcpLicense.RenewListener, 20 | ) : DrmManagementViewModel() { 21 | 22 | class Factory( 23 | private val lcpLicense: LcpLicense, 24 | private val renewListener: LcpLicense.RenewListener, 25 | ) : ViewModelProvider.NewInstanceFactory() { 26 | 27 | override fun create(modelClass: Class): T = 28 | modelClass.getDeclaredConstructor(LcpLicense::class.java, LcpLicense.RenewListener::class.java) 29 | .newInstance(lcpLicense, renewListener) 30 | } 31 | 32 | override val type: String = "LCP" 33 | 34 | override val state: String? 35 | get() = lcpLicense.status?.status?.rawValue 36 | 37 | override val provider: String? 38 | get() = lcpLicense.license.provider 39 | 40 | override val issued: Date? 41 | get() = lcpLicense.license.issued 42 | 43 | override val updated: Date? 44 | get() = lcpLicense.license.updated 45 | 46 | override val start: Date? 47 | get() = lcpLicense.license.rights.start 48 | 49 | override val end: Date? 50 | get() = lcpLicense.license.rights.end 51 | 52 | override val copiesLeft: String = 53 | lcpLicense.charactersToCopyLeft 54 | ?.let { "$it characters" } 55 | ?: super.copiesLeft 56 | 57 | override val printsLeft: String = 58 | lcpLicense.pagesToPrintLeft 59 | ?.let { "$it pages" } 60 | ?: super.printsLeft 61 | 62 | override val canRenewLoan: Boolean 63 | get() = lcpLicense.canRenewLoan 64 | 65 | override suspend fun renewLoan(fragment: Fragment): Try { 66 | return lcpLicense.renewLoan(renewListener) 67 | } 68 | 69 | override val canReturnPublication: Boolean 70 | get() = lcpLicense.canReturnPublication 71 | 72 | override suspend fun returnPublication(): Try = 73 | lcpLicense.returnPublication() 74 | 75 | } 76 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/opds/GridAutoFitLayoutManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-testapp-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann 4 | * 5 | * Copyright (c) 2018. European Digital Reading Lab. All rights reserved. 6 | * Licensed to the Readium Foundation under one or more contributor license agreements. 7 | * Use of this source code is governed by a BSD-style license which is detailed in the 8 | * LICENSE file present in the project repository where this source code is maintained. 9 | */ 10 | 11 | package org.readium.r2.testapp.opds 12 | 13 | import android.content.Context 14 | import android.util.TypedValue 15 | import androidx.recyclerview.widget.GridLayoutManager 16 | import androidx.recyclerview.widget.LinearLayoutManager 17 | import androidx.recyclerview.widget.RecyclerView 18 | import kotlin.math.max 19 | 20 | class GridAutoFitLayoutManager : GridLayoutManager { 21 | private var mColumnWidth: Int = 0 22 | private var mColumnWidthChanged = true 23 | private var mWidthChanged = true 24 | private var mWidth: Int = 0 25 | 26 | constructor(context: Context, columnWidth: Int) : super(context, 1) { 27 | setColumnWidth(checkedColumnWidth(context, columnWidth)) 28 | }/* Initially set spanCount to 1, will be changed automatically later. */ 29 | 30 | constructor(context: Context, columnWidth: Int, orientation: Int, reverseLayout: Boolean) : super(context, 1, orientation, reverseLayout) { 31 | setColumnWidth(checkedColumnWidth(context, columnWidth)) 32 | }/* Initially set spanCount to 1, will be changed automatically later. */ 33 | 34 | private fun checkedColumnWidth(context: Context, columnWidth: Int): Int { 35 | var width = columnWidth 36 | width = if (width <= 0) { 37 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, sColumnWidth.toFloat(), 38 | context.resources.displayMetrics).toInt() 39 | } else { 40 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, width.toFloat(), 41 | context.resources.displayMetrics).toInt() 42 | } 43 | return width 44 | } 45 | 46 | private fun setColumnWidth(newColumnWidth: Int) { 47 | if (newColumnWidth > 0 && newColumnWidth != mColumnWidth) { 48 | mColumnWidth = newColumnWidth 49 | mColumnWidthChanged = true 50 | } 51 | } 52 | 53 | override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State) { 54 | val width = width 55 | val height = height 56 | 57 | if (width != mWidth) { 58 | mWidthChanged = true 59 | mWidth = width 60 | } 61 | 62 | if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0 || mWidthChanged) { 63 | val totalSpace: Int = if (orientation == LinearLayoutManager.VERTICAL) { 64 | width - paddingRight - paddingLeft 65 | } else { 66 | height - paddingTop - paddingBottom 67 | } 68 | val spanCount = max(1, totalSpace / mColumnWidth) 69 | setSpanCount(spanCount) 70 | mColumnWidthChanged = false 71 | mWidthChanged = false 72 | } 73 | super.onLayoutChildren(recycler, state) 74 | } 75 | 76 | companion object { 77 | private const val sColumnWidth = 200 // assume cell width of 200dp 78 | } 79 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/outline/OutlineContract.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.outline 8 | 9 | import android.os.Bundle 10 | import org.readium.r2.shared.publication.Locator 11 | 12 | object OutlineContract { 13 | 14 | private const val DESTINATION_KEY = "locator" 15 | 16 | val REQUEST_KEY: String = OutlineContract::class.java.name 17 | 18 | data class Result(val destination: Locator) 19 | 20 | fun createResult(locator: Locator): Bundle = 21 | Bundle().apply { putParcelable(DESTINATION_KEY, locator) } 22 | 23 | fun parseResult(result: Bundle): Result { 24 | val destination = requireNotNull(result.getParcelable(DESTINATION_KEY)) 25 | return Result(destination) 26 | } 27 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/outline/OutlineFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.outline 8 | 9 | import android.os.Bundle 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import androidx.fragment.app.Fragment 14 | import androidx.fragment.app.FragmentResultListener 15 | import androidx.fragment.app.setFragmentResult 16 | import androidx.lifecycle.ViewModelProvider 17 | import androidx.viewpager2.adapter.FragmentStateAdapter 18 | import com.google.android.material.tabs.TabLayoutMediator 19 | import org.readium.r2.shared.publication.Publication 20 | import org.readium.r2.shared.publication.epub.landmarks 21 | import org.readium.r2.shared.publication.epub.pageList 22 | import org.readium.r2.shared.publication.opds.images 23 | import org.readium.r2.testapp.R 24 | import org.readium.r2.testapp.databinding.FragmentOutlineBinding 25 | import org.readium.r2.testapp.reader.ReaderViewModel 26 | 27 | class OutlineFragment : Fragment() { 28 | 29 | lateinit var publication: Publication 30 | 31 | private var _binding: FragmentOutlineBinding? = null 32 | private val binding get() = _binding!! 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | 37 | ViewModelProvider(requireActivity()).get(ReaderViewModel::class.java).let { 38 | publication = it.publication 39 | } 40 | 41 | childFragmentManager.setFragmentResultListener( 42 | OutlineContract.REQUEST_KEY, 43 | this, 44 | FragmentResultListener { requestKey, bundle -> setFragmentResult(requestKey, bundle) } 45 | ) 46 | } 47 | 48 | override fun onCreateView( 49 | inflater: LayoutInflater, 50 | container: ViewGroup?, 51 | savedInstanceState: Bundle? 52 | ): View { 53 | _binding = FragmentOutlineBinding.inflate(inflater, container, false) 54 | return binding.root 55 | } 56 | 57 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 58 | super.onViewCreated(view, savedInstanceState) 59 | 60 | val outlines: List = when (publication.type) { 61 | Publication.TYPE.EPUB -> listOf(Outline.Contents, Outline.Bookmarks, Outline.Highlights, Outline.PageList, Outline.Landmarks) 62 | else -> listOf(Outline.Contents, Outline.Bookmarks) 63 | } 64 | 65 | binding.outlinePager.adapter = OutlineFragmentStateAdapter(this, publication, outlines) 66 | TabLayoutMediator(binding.outlineTabLayout, binding.outlinePager) { tab, idx -> tab.setText(outlines[idx].label) }.attach() 67 | } 68 | 69 | override fun onDestroyView() { 70 | _binding = null 71 | super.onDestroyView() 72 | } 73 | } 74 | 75 | private class OutlineFragmentStateAdapter(fragment: Fragment, val publication: Publication, val outlines: List) 76 | : FragmentStateAdapter(fragment) { 77 | 78 | override fun getItemCount(): Int { 79 | return outlines.size 80 | } 81 | 82 | override fun createFragment(position: Int): Fragment { 83 | return when (this.outlines[position]) { 84 | Outline.Bookmarks -> BookmarksFragment() 85 | Outline.Highlights -> HighlightsFragment() 86 | Outline.Landmarks -> createLandmarksFragment() 87 | Outline.Contents -> createContentsFragment() 88 | Outline.PageList -> createPageListFragment() 89 | } 90 | } 91 | 92 | private fun createContentsFragment() = 93 | NavigationFragment.newInstance(when { 94 | publication.tableOfContents.isNotEmpty() -> publication.tableOfContents 95 | publication.readingOrder.isNotEmpty() -> publication.readingOrder 96 | publication.images.isNotEmpty() -> publication.images 97 | else -> mutableListOf() 98 | }) 99 | 100 | private fun createPageListFragment() = 101 | NavigationFragment.newInstance(publication.pageList) 102 | 103 | private fun createLandmarksFragment() = 104 | NavigationFragment.newInstance(publication.landmarks) 105 | } 106 | 107 | private enum class Outline(val label: Int) { 108 | Contents(R.string.contents_tab_label), 109 | Bookmarks(R.string.bookmarks_tab_label), 110 | Highlights(R.string.highlights_tab_label), 111 | PageList(R.string.pagelist_tab_label), 112 | Landmarks(R.string.landmarks_tab_label) 113 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/reader/AudiobookService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.reader 8 | 9 | import android.app.PendingIntent 10 | import android.content.Intent 11 | import android.os.Build 12 | import kotlinx.coroutines.ExperimentalCoroutinesApi 13 | import kotlinx.coroutines.flow.collect 14 | import kotlinx.coroutines.flow.emptyFlow 15 | import kotlinx.coroutines.flow.flatMapLatest 16 | import kotlinx.coroutines.flow.map 17 | import kotlinx.coroutines.launch 18 | import org.readium.r2.navigator.ExperimentalAudiobook 19 | import org.readium.r2.navigator.media.MediaService 20 | import org.readium.r2.shared.publication.Publication 21 | import org.readium.r2.shared.publication.PublicationId 22 | import org.readium.r2.testapp.bookshelf.BookRepository 23 | import org.readium.r2.testapp.db.BookDatabase 24 | 25 | @OptIn(ExperimentalAudiobook::class, ExperimentalCoroutinesApi::class) 26 | class AudiobookService : MediaService() { 27 | 28 | private val books by lazy { 29 | BookRepository(BookDatabase.getDatabase(this).booksDao()) 30 | } 31 | 32 | override fun onCreate() { 33 | super.onCreate() 34 | 35 | // Save the current locator in the database. We can't do this in the [ReaderActivity] since 36 | // the playback can continue in the background without any [Activity]. 37 | launch { 38 | navigator 39 | .flatMapLatest { navigator -> 40 | navigator ?: return@flatMapLatest emptyFlow() 41 | 42 | navigator.currentLocator 43 | .map { Pair(navigator.publicationId, it) } 44 | } 45 | .collect { (pubId, locator) -> 46 | books.saveProgression(locator, pubId.toLong()) 47 | } 48 | } 49 | } 50 | 51 | override suspend fun onCreateNotificationIntent(publicationId: PublicationId, publication: Publication): PendingIntent? { 52 | val bookId = publicationId.toLong() 53 | val book = books.get(bookId) ?: return null 54 | 55 | var flags = PendingIntent.FLAG_UPDATE_CURRENT 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 57 | flags = flags or PendingIntent.FLAG_IMMUTABLE 58 | } 59 | 60 | val intent = ReaderContract().createIntent(this, ReaderContract.Input( 61 | mediaType = book.mediaType(), 62 | publication = publication, 63 | bookId = bookId, 64 | )) 65 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 66 | 67 | return PendingIntent.getActivity(this, 0, intent, flags) 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/reader/ImageReaderFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.reader 8 | 9 | import android.graphics.PointF 10 | import android.os.Bundle 11 | import android.view.LayoutInflater 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import androidx.fragment.app.commitNow 15 | import androidx.lifecycle.ViewModelProvider 16 | import org.readium.r2.navigator.Navigator 17 | import org.readium.r2.navigator.image.ImageNavigatorFragment 18 | import org.readium.r2.shared.publication.Publication 19 | import org.readium.r2.testapp.R 20 | import org.readium.r2.testapp.utils.toggleSystemUi 21 | 22 | class ImageReaderFragment : VisualReaderFragment(), ImageNavigatorFragment.Listener { 23 | 24 | override lateinit var model: ReaderViewModel 25 | override lateinit var navigator: Navigator 26 | private lateinit var publication: Publication 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | ViewModelProvider(requireActivity()).get(ReaderViewModel::class.java).let { 30 | model = it 31 | publication = it.publication 32 | } 33 | 34 | childFragmentManager.fragmentFactory = 35 | ImageNavigatorFragment.createFactory(publication, model.initialLocation, this) 36 | 37 | super.onCreate(savedInstanceState) 38 | } 39 | 40 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 41 | val view = super.onCreateView(inflater, container, savedInstanceState) 42 | if (savedInstanceState == null) { 43 | childFragmentManager.commitNow { 44 | add(R.id.fragment_reader_container, ImageNavigatorFragment::class.java, Bundle(), NAVIGATOR_FRAGMENT_TAG) 45 | } 46 | } 47 | navigator = childFragmentManager.findFragmentByTag(NAVIGATOR_FRAGMENT_TAG)!! as Navigator 48 | return view 49 | } 50 | 51 | override fun onTap(point: PointF): Boolean { 52 | val viewWidth = requireView().width 53 | val leftRange = 0.0..(0.2 * viewWidth) 54 | 55 | when { 56 | leftRange.contains(point.x) -> navigator.goBackward(animated = true) 57 | leftRange.contains(viewWidth - point.x) -> navigator.goForward(animated = true) 58 | else -> requireActivity().toggleSystemUi() 59 | } 60 | 61 | return true 62 | } 63 | 64 | companion object { 65 | 66 | const val NAVIGATOR_FRAGMENT_TAG = "navigator" 67 | } 68 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/reader/PdfReaderFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.reader 8 | 9 | import android.graphics.PointF 10 | import android.os.Bundle 11 | import android.view.LayoutInflater 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.widget.Toast 15 | import androidx.fragment.app.commitNow 16 | import androidx.lifecycle.ViewModelProvider 17 | import org.readium.r2.navigator.Navigator 18 | import org.readium.r2.navigator.pdf.PdfNavigatorFragment 19 | import org.readium.r2.shared.fetcher.Resource 20 | import org.readium.r2.shared.publication.Link 21 | import org.readium.r2.shared.publication.Publication 22 | import org.readium.r2.testapp.R 23 | import org.readium.r2.testapp.utils.toggleSystemUi 24 | 25 | class PdfReaderFragment : VisualReaderFragment(), PdfNavigatorFragment.Listener { 26 | 27 | override lateinit var model: ReaderViewModel 28 | override lateinit var navigator: Navigator 29 | private lateinit var publication: Publication 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | ViewModelProvider(requireActivity()).get(ReaderViewModel::class.java).let { 33 | model = it 34 | publication = it.publication 35 | } 36 | 37 | childFragmentManager.fragmentFactory = 38 | PdfNavigatorFragment.createFactory(publication, model.initialLocation, this) 39 | 40 | super.onCreate(savedInstanceState) 41 | } 42 | 43 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 44 | val view = super.onCreateView(inflater, container, savedInstanceState) 45 | if (savedInstanceState == null) { 46 | childFragmentManager.commitNow { 47 | add(R.id.fragment_reader_container, PdfNavigatorFragment::class.java, Bundle(), NAVIGATOR_FRAGMENT_TAG) 48 | } 49 | } 50 | navigator = childFragmentManager.findFragmentByTag(NAVIGATOR_FRAGMENT_TAG)!! as Navigator 51 | return view 52 | } 53 | 54 | override fun onResourceLoadFailed(link: Link, error: Resource.Exception) { 55 | val message = when (error) { 56 | is Resource.Exception.OutOfMemory -> "The PDF is too large to be rendered on this device" 57 | else -> "Failed to render this PDF" 58 | } 59 | Toast.makeText(requireActivity(), message, Toast.LENGTH_LONG).show() 60 | 61 | // There's nothing we can do to recover, so we quit the Activity. 62 | requireActivity().finish() 63 | } 64 | 65 | override fun onTap(point: PointF): Boolean { 66 | val viewWidth = requireView().width 67 | val leftRange = 0.0..(0.2 * viewWidth) 68 | 69 | when { 70 | leftRange.contains(point.x) -> navigator.goBackward() 71 | leftRange.contains(viewWidth - point.x) -> navigator.goForward() 72 | else -> requireActivity().toggleSystemUi() 73 | } 74 | 75 | return true 76 | } 77 | 78 | companion object { 79 | 80 | const val NAVIGATOR_FRAGMENT_TAG = "navigator" 81 | } 82 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/reader/ReaderContract.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-testapp-kotlin 3 | * Developers: Quentin Gliosca 4 | * 5 | * Copyright (c) 2020. European Digital Reading Lab. All rights reserved. 6 | * Licensed to the Readium Foundation under one or more contributor license agreements. 7 | * Use of this source code is governed by a BSD-style license which is detailed in the 8 | * LICENSE file present in the project repository where this source code is maintained. 9 | */ 10 | 11 | package org.readium.r2.testapp.reader 12 | 13 | import android.app.Activity 14 | import android.content.Context 15 | import android.content.Intent 16 | import androidx.activity.result.contract.ActivityResultContract 17 | import org.readium.r2.shared.extensions.destroyPublication 18 | import org.readium.r2.shared.extensions.getPublication 19 | import org.readium.r2.shared.extensions.putPublication 20 | import org.readium.r2.shared.publication.Locator 21 | import org.readium.r2.shared.publication.Publication 22 | import org.readium.r2.shared.util.mediatype.MediaType 23 | import java.io.File 24 | import java.net.URL 25 | 26 | class ReaderContract : ActivityResultContract() { 27 | 28 | data class Input( 29 | val mediaType: MediaType?, 30 | val publication: Publication, 31 | val bookId: Long, 32 | val initialLocator: Locator? = null, 33 | val baseUrl: URL? = null 34 | ) 35 | 36 | data class Output( 37 | val publication: Publication 38 | ) 39 | 40 | override fun createIntent(context: Context, input: Input): Intent { 41 | val intent = Intent( 42 | context, when (input.mediaType) { 43 | MediaType.ZAB, MediaType.READIUM_AUDIOBOOK, 44 | MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.LCP_PROTECTED_AUDIOBOOK -> 45 | ReaderActivity::class.java 46 | MediaType.EPUB, MediaType.READIUM_WEBPUB_MANIFEST, MediaType.READIUM_WEBPUB, 47 | MediaType.CBZ, MediaType.DIVINA, MediaType.DIVINA_MANIFEST, 48 | MediaType.PDF, MediaType.LCP_PROTECTED_PDF -> 49 | VisualReaderActivity::class.java 50 | else -> throw IllegalArgumentException("Unknown [mediaType]") 51 | } 52 | ) 53 | 54 | return intent.apply { 55 | putPublication(input.publication) 56 | putExtra("bookId", input.bookId) 57 | putExtra("baseUrl", input.baseUrl?.toString()) 58 | putExtra("locator", input.initialLocator) 59 | } 60 | } 61 | 62 | override fun parseResult(resultCode: Int, intent: Intent?): Output? { 63 | if (intent == null) 64 | return null 65 | 66 | intent.destroyPublication(null) 67 | return Output( 68 | publication = intent.getPublication(null), 69 | ) 70 | } 71 | 72 | companion object { 73 | 74 | fun parseIntent(activity: Activity): Input = with(activity) { 75 | Input( 76 | mediaType = null, 77 | publication = intent.getPublication(activity), 78 | bookId = intent.getLongExtra("bookId", -1), 79 | initialLocator = intent.getParcelableExtra("locator"), 80 | baseUrl = intent.getStringExtra("baseUrl")?.let { URL(it) } 81 | ) 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/reader/VisualReaderActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.reader 8 | 9 | import android.os.Bundle 10 | import android.view.View 11 | import android.view.WindowInsets 12 | import org.readium.r2.testapp.utils.clearPadding 13 | import org.readium.r2.testapp.utils.padSystemUi 14 | import org.readium.r2.testapp.utils.showSystemUi 15 | 16 | /** 17 | * Adds fullscreen support to the ReaderActivity 18 | */ 19 | open class VisualReaderActivity : ReaderActivity() { 20 | 21 | private val visualReaderFragment: VisualReaderFragment 22 | get() = readerFragment as VisualReaderFragment 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | 27 | // Without this, activity_reader_container receives the insets only once, 28 | // although we need a call every time the reader is hidden 29 | window.decorView.setOnApplyWindowInsetsListener { view, insets -> 30 | val newInsets = view.onApplyWindowInsets(insets) 31 | binding.activityContainer.dispatchApplyWindowInsets(newInsets) 32 | } 33 | 34 | binding.activityContainer.setOnApplyWindowInsetsListener { container, insets -> 35 | updateSystemUiPadding(container, insets) 36 | insets 37 | } 38 | 39 | supportFragmentManager.addOnBackStackChangedListener { 40 | updateSystemUiVisibility() 41 | } 42 | } 43 | 44 | override fun onStart() { 45 | super.onStart() 46 | updateSystemUiVisibility() 47 | } 48 | 49 | private fun updateSystemUiVisibility() { 50 | if (visualReaderFragment.isHidden) 51 | showSystemUi() 52 | else 53 | visualReaderFragment.updateSystemUiVisibility() 54 | 55 | // Seems to be required to adjust padding when transitioning from the outlines to the screen reader 56 | binding.activityContainer.requestApplyInsets() 57 | } 58 | 59 | private fun updateSystemUiPadding(container: View, insets: WindowInsets) { 60 | if (visualReaderFragment.isHidden) 61 | container.padSystemUi(insets, this) 62 | else 63 | container.clearPadding() 64 | } 65 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.reader 8 | 9 | import android.os.Bundle 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import android.view.WindowInsets 14 | import android.widget.FrameLayout 15 | import androidx.fragment.app.Fragment 16 | import org.readium.r2.navigator.DecorableNavigator 17 | import org.readium.r2.navigator.ExperimentalDecorator 18 | import org.readium.r2.testapp.R 19 | import org.readium.r2.testapp.databinding.FragmentReaderBinding 20 | import org.readium.r2.testapp.utils.clearPadding 21 | import org.readium.r2.testapp.utils.hideSystemUi 22 | import org.readium.r2.testapp.utils.padSystemUi 23 | import org.readium.r2.testapp.utils.showSystemUi 24 | 25 | /* 26 | * Adds fullscreen support to the BaseReaderFragment 27 | */ 28 | abstract class VisualReaderFragment : BaseReaderFragment() { 29 | 30 | private lateinit var navigatorFragment: Fragment 31 | 32 | private var _binding: FragmentReaderBinding? = null 33 | val binding get() = _binding!! 34 | 35 | override fun onCreateView( 36 | inflater: LayoutInflater, 37 | container: ViewGroup?, 38 | savedInstanceState: Bundle? 39 | ): View? { 40 | _binding = FragmentReaderBinding.inflate(inflater, container, false) 41 | return binding.root 42 | } 43 | 44 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 45 | super.onViewCreated(view, savedInstanceState) 46 | navigatorFragment = navigator as Fragment 47 | 48 | childFragmentManager.addOnBackStackChangedListener { 49 | updateSystemUiVisibility() 50 | } 51 | binding.fragmentReaderContainer.setOnApplyWindowInsetsListener { container, insets -> 52 | updateSystemUiPadding(container, insets) 53 | insets 54 | } 55 | } 56 | 57 | override fun onDestroyView() { 58 | _binding = null 59 | super.onDestroyView() 60 | } 61 | 62 | fun updateSystemUiVisibility() { 63 | if (navigatorFragment.isHidden) 64 | requireActivity().showSystemUi() 65 | else 66 | requireActivity().hideSystemUi() 67 | 68 | requireView().requestApplyInsets() 69 | } 70 | 71 | private fun updateSystemUiPadding(container: View, insets: WindowInsets) { 72 | if (navigatorFragment.isHidden) { 73 | container.padSystemUi(insets, requireActivity()) 74 | } else { 75 | container.clearPadding() 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/search/SearchFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.search 8 | 9 | import android.os.Bundle 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import androidx.core.view.isVisible 14 | import androidx.fragment.app.Fragment 15 | import androidx.fragment.app.activityViewModels 16 | import androidx.fragment.app.setFragmentResult 17 | import androidx.lifecycle.lifecycleScope 18 | import androidx.recyclerview.widget.DividerItemDecoration 19 | import androidx.recyclerview.widget.LinearLayoutManager 20 | import kotlinx.coroutines.flow.launchIn 21 | import kotlinx.coroutines.flow.onEach 22 | import org.readium.r2.shared.publication.Locator 23 | import org.readium.r2.testapp.R 24 | import org.readium.r2.testapp.databinding.FragmentSearchBinding 25 | import org.readium.r2.testapp.reader.ReaderViewModel 26 | import org.readium.r2.testapp.utils.SectionDecoration 27 | 28 | class SearchFragment : Fragment(R.layout.fragment_search) { 29 | 30 | private val viewModel: ReaderViewModel by activityViewModels() 31 | 32 | private var _binding: FragmentSearchBinding? = null 33 | private val binding get() = _binding!! 34 | 35 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 36 | super.onViewCreated(view, savedInstanceState) 37 | 38 | val viewScope = viewLifecycleOwner.lifecycleScope 39 | 40 | val searchAdapter = SearchResultAdapter(object : SearchResultAdapter.Listener { 41 | override fun onItemClicked(v: View, locator: Locator) { 42 | val result = Bundle().apply { 43 | putParcelable(SearchFragment::class.java.name, locator) 44 | } 45 | setFragmentResult(SearchFragment::class.java.name, result) 46 | } 47 | }) 48 | 49 | viewModel.searchResult 50 | .onEach { searchAdapter.submitData(it) } 51 | .launchIn(viewScope) 52 | 53 | viewModel.searchLocators 54 | .onEach { binding.noResultLabel.isVisible = it.isEmpty() } 55 | .launchIn(viewScope) 56 | 57 | viewModel.channel 58 | .receive(viewLifecycleOwner) { event -> 59 | when (event) { 60 | ReaderViewModel.Event.StartNewSearch -> 61 | binding.searchRecyclerView.scrollToPosition(0) 62 | else -> {} 63 | } 64 | } 65 | 66 | binding.searchRecyclerView.apply { 67 | adapter = searchAdapter 68 | layoutManager = LinearLayoutManager(activity) 69 | addItemDecoration(SectionDecoration(context, object : SectionDecoration.Listener { 70 | override fun isStartOfSection(itemPos: Int): Boolean = 71 | viewModel.searchLocators.value.run { 72 | when { 73 | itemPos == 0 -> true 74 | itemPos < 0 -> false 75 | itemPos >= size -> false 76 | else -> getOrNull(itemPos)?.title != getOrNull(itemPos-1)?.title 77 | } 78 | } 79 | 80 | override fun sectionTitle(itemPos: Int): String = 81 | viewModel.searchLocators.value.getOrNull(itemPos)?.title ?: "" 82 | })) 83 | addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) 84 | } 85 | } 86 | 87 | override fun onCreateView( 88 | inflater: LayoutInflater, 89 | container: ViewGroup?, 90 | savedInstanceState: Bundle? 91 | ): View { 92 | _binding = FragmentSearchBinding.inflate(inflater, container, false) 93 | return binding.root 94 | } 95 | 96 | override fun onDestroyView() { 97 | super.onDestroyView() 98 | _binding = null 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.search 8 | 9 | import androidx.paging.PagingSource 10 | import androidx.paging.PagingState 11 | import org.readium.r2.shared.Search 12 | import org.readium.r2.shared.publication.Locator 13 | import org.readium.r2.shared.publication.LocatorCollection 14 | import org.readium.r2.shared.publication.services.search.SearchIterator 15 | import org.readium.r2.shared.publication.services.search.SearchTry 16 | 17 | @OptIn(Search::class) 18 | class SearchPagingSource( 19 | private val listener: Listener? 20 | ) : PagingSource() { 21 | 22 | interface Listener { 23 | suspend fun next(): SearchTry 24 | } 25 | 26 | override val keyReuseSupported: Boolean get() = true 27 | 28 | override fun getRefreshKey(state: PagingState): Unit? = null 29 | 30 | override suspend fun load(params: LoadParams): LoadResult { 31 | listener ?: return LoadResult.Page(data = emptyList(), prevKey = null, nextKey = null) 32 | 33 | return try { 34 | val page = listener.next().getOrThrow() 35 | LoadResult.Page( 36 | data = page?.locators ?: emptyList(), 37 | prevKey = null, 38 | nextKey = if (page == null) null else Unit 39 | ) 40 | } catch (e: Exception) { 41 | LoadResult.Error(e) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/search/SearchResultAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.search 8 | 9 | import android.os.Build 10 | import android.text.Html 11 | import android.view.LayoutInflater 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import androidx.paging.PagingDataAdapter 15 | import androidx.recyclerview.widget.DiffUtil 16 | import androidx.recyclerview.widget.RecyclerView 17 | import org.readium.r2.shared.publication.Locator 18 | import org.readium.r2.testapp.databinding.ItemRecycleSearchBinding 19 | import org.readium.r2.testapp.utils.singleClick 20 | 21 | /** 22 | * This class is an adapter for Search results' recycler view. 23 | */ 24 | class SearchResultAdapter(private var listener: Listener) : 25 | PagingDataAdapter(ItemCallback()) { 26 | 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 28 | return ViewHolder( 29 | ItemRecycleSearchBinding.inflate( 30 | LayoutInflater.from(parent.context), parent, false 31 | ) 32 | ) 33 | } 34 | 35 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 36 | val locator = getItem(position) ?: return 37 | val html = 38 | "${locator.text.before}${locator.text.highlight}${locator.text.after}" 39 | holder.textView.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 40 | Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT) 41 | } else { 42 | @Suppress("DEPRECATION") 43 | Html.fromHtml(html) 44 | } 45 | 46 | holder.itemView.singleClick { v -> 47 | listener.onItemClicked(v, locator) 48 | } 49 | } 50 | 51 | inner class ViewHolder(val binding: ItemRecycleSearchBinding) : 52 | RecyclerView.ViewHolder(binding.root) { 53 | val textView = binding.text 54 | } 55 | 56 | interface Listener { 57 | fun onItemClicked(v: View, locator: Locator) 58 | } 59 | 60 | private class ItemCallback : DiffUtil.ItemCallback() { 61 | 62 | override fun areItemsTheSame(oldItem: Locator, newItem: Locator): Boolean = 63 | oldItem == newItem 64 | 65 | override fun areContentsTheSame(oldItem: Locator, newItem: Locator): Boolean = 66 | oldItem == newItem 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/tts/ScreenReaderContract.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.tts 8 | 9 | import android.os.Bundle 10 | import org.readium.r2.shared.publication.Locator 11 | 12 | object ScreenReaderContract { 13 | 14 | private const val LOCATOR_KEY = "locator" 15 | 16 | val REQUEST_KEY: String = ScreenReaderContract::class.java.name 17 | 18 | data class Arguments(val locator: Locator) 19 | 20 | fun createArguments(locator: Locator): Bundle = 21 | Bundle().apply { putParcelable(LOCATOR_KEY, locator) } 22 | 23 | fun parseArguments(result: Bundle): Arguments { 24 | val locator = requireNotNull(result.getParcelable(LOCATOR_KEY)) 25 | return Arguments(locator) 26 | } 27 | 28 | data class Result(val locator: Locator) 29 | 30 | fun createResult(locator: Locator): Bundle = 31 | Bundle().apply { putParcelable(LOCATOR_KEY, locator) } 32 | 33 | fun parseResult(result: Bundle): Result { 34 | val destination = requireNotNull(result.getParcelable(LOCATOR_KEY)) 35 | return Result(destination) 36 | } 37 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/EventChannel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | // See https://proandroiddev.com/android-singleliveevent-redux-with-kotlin-flow-b755c70bb055 8 | 9 | package org.readium.r2.testapp.utils 10 | 11 | import androidx.lifecycle.Lifecycle 12 | import androidx.lifecycle.LifecycleObserver 13 | import androidx.lifecycle.LifecycleOwner 14 | import androidx.lifecycle.OnLifecycleEvent 15 | import androidx.lifecycle.lifecycleScope 16 | import kotlinx.coroutines.CoroutineScope 17 | import kotlinx.coroutines.Job 18 | import kotlinx.coroutines.channels.Channel 19 | import kotlinx.coroutines.flow.Flow 20 | import kotlinx.coroutines.flow.collect 21 | import kotlinx.coroutines.flow.receiveAsFlow 22 | import kotlinx.coroutines.launch 23 | 24 | class EventChannel(private val channel: Channel, private val sendScope: CoroutineScope) { 25 | 26 | fun send(event: T) { 27 | sendScope.launch { 28 | channel.send(event) 29 | } 30 | } 31 | 32 | fun receive(lifecycleOwner: LifecycleOwner, callback: suspend (T) -> Unit) { 33 | val observer = FlowObserver(lifecycleOwner, channel.receiveAsFlow(), callback) 34 | lifecycleOwner.lifecycle.addObserver(observer) 35 | } 36 | } 37 | 38 | class FlowObserver ( 39 | private val lifecycleOwner: LifecycleOwner, 40 | private val flow: Flow, 41 | private val collector: suspend (T) -> Unit 42 | ) : LifecycleObserver { 43 | 44 | private var job: Job? = null 45 | 46 | @OnLifecycleEvent(Lifecycle.Event.ON_START) 47 | fun onStart() { 48 | if (job == null) { 49 | job = lifecycleOwner.lifecycleScope.launch { 50 | flow.collect { collector(it) } 51 | } 52 | } 53 | } 54 | 55 | @OnLifecycleEvent(Lifecycle.Event.ON_STOP) 56 | fun onStop() { 57 | job?.cancel() 58 | job = null 59 | } 60 | } 61 | 62 | 63 | inline fun Flow.observeWhenStarted( 64 | lifecycleOwner: LifecycleOwner, 65 | noinline collector: suspend (T) -> Unit 66 | ) { 67 | val observer = FlowObserver(lifecycleOwner, this, collector) 68 | lifecycleOwner.lifecycle.addObserver(observer) 69 | } 70 | 71 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/FragmentFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.utils 8 | 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.FragmentFactory 11 | import org.readium.r2.shared.extensions.tryOrNull 12 | 13 | /** 14 | * Creates a [FragmentFactory] for a single type of [Fragment] using the result of the given 15 | * [factory] closure. 16 | */ 17 | inline fun createFragmentFactory(crossinline factory: () -> T): FragmentFactory = object : FragmentFactory() { 18 | 19 | override fun instantiate(classLoader: ClassLoader, className: String): Fragment { 20 | return when (className) { 21 | T::class.java.name -> factory() 22 | else -> super.instantiate(classLoader, className) 23 | } 24 | } 25 | 26 | } 27 | 28 | /** 29 | * A [FragmentFactory] which will iterate over a provided list of [factories] until finding one 30 | * instantiating successfully the requested [Fragment]. 31 | * 32 | * ``` 33 | * supportFragmentManager.fragmentFactory = CompositeFragmentFactory( 34 | * EpubNavigatorFragment.createFactory(publication, baseUrl, initialLocator, this), 35 | * PdfNavigatorFragment.createFactory(publication, initialLocator, this) 36 | * ) 37 | * ``` 38 | */ 39 | class CompositeFragmentFactory(private val factories: List) : FragmentFactory() { 40 | 41 | constructor(vararg factories: FragmentFactory) : this(factories.toList()) 42 | 43 | override fun instantiate(classLoader: ClassLoader, className: String): Fragment { 44 | for (factory in factories) { 45 | tryOrNull { factory.instantiate(classLoader, className) } 46 | ?.let { return it } 47 | } 48 | 49 | return super.instantiate(classLoader, className) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/R2DispatcherActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-testapp-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann 4 | * 5 | * Copyright (c) 2018. European Digital Reading Lab. All rights reserved. 6 | * Licensed to the Readium Foundation under one or more contributor license agreements. 7 | * Use of this source code is governed by a BSD-style license which is detailed in the 8 | * LICENSE file present in the project repository where this source code is maintained. 9 | */ 10 | 11 | package org.readium.r2.testapp.utils 12 | 13 | import android.app.Activity 14 | import android.content.Intent 15 | import android.net.Uri 16 | import android.os.Bundle 17 | import org.readium.r2.testapp.MainActivity 18 | import timber.log.Timber 19 | 20 | class R2DispatcherActivity : Activity() { 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | dispatchIntent(intent) 25 | finish() 26 | } 27 | 28 | private fun dispatchIntent(intent: Intent) { 29 | val uri = uriFromIntent(intent) 30 | ?: run { 31 | Timber.d("Got an empty intent.") 32 | return 33 | } 34 | val newIntent = Intent(this, MainActivity::class.java).apply { 35 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 36 | data = uri 37 | } 38 | startActivity(newIntent) 39 | } 40 | 41 | private fun uriFromIntent(intent: Intent): Uri? = 42 | when (intent.action) { 43 | Intent.ACTION_SEND -> { 44 | if ("text/plain" == intent.type) { 45 | intent.getStringExtra(Intent.EXTRA_TEXT).let { Uri.parse(it) } 46 | } else { 47 | intent.getParcelableExtra(Intent.EXTRA_STREAM) 48 | } 49 | } 50 | else -> { 51 | intent.data 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/SectionDecoration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.utils 8 | 9 | import android.content.Context 10 | import android.graphics.Canvas 11 | import android.graphics.Rect 12 | import android.view.LayoutInflater 13 | import android.view.View 14 | import android.view.ViewGroup 15 | import android.widget.TextView 16 | import androidx.core.view.children 17 | import androidx.recyclerview.widget.RecyclerView 18 | import androidx.recyclerview.widget.RecyclerView.NO_POSITION 19 | import org.readium.r2.testapp.databinding.SectionHeaderBinding 20 | 21 | class SectionDecoration( 22 | private val context: Context, 23 | private val listener: Listener 24 | ) : RecyclerView.ItemDecoration() { 25 | 26 | interface Listener { 27 | fun isStartOfSection(itemPos: Int): Boolean 28 | fun sectionTitle(itemPos: Int): String 29 | } 30 | 31 | private lateinit var headerView: View 32 | private lateinit var sectionTitleView: TextView 33 | 34 | override fun getItemOffsets( 35 | outRect: Rect, 36 | view: View, 37 | parent: RecyclerView, 38 | state: RecyclerView.State 39 | ) { 40 | super.getItemOffsets(outRect, view, parent, state) 41 | val pos = parent.getChildAdapterPosition(view) 42 | initHeaderViewIfNeeded(parent) 43 | if (listener.sectionTitle(pos) != "" && listener.isStartOfSection(pos)) { 44 | sectionTitleView.text = listener.sectionTitle(pos) 45 | fixLayoutSize(headerView, parent) 46 | outRect.top = headerView.height 47 | } 48 | } 49 | 50 | override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 51 | super.onDrawOver(c, parent, state) 52 | initHeaderViewIfNeeded(parent) 53 | 54 | val children = parent.children.toList() 55 | children.forEach { child -> 56 | val pos = parent.getChildAdapterPosition(child) 57 | if (pos != NO_POSITION && listener.sectionTitle(pos) != "" && 58 | (listener.isStartOfSection(pos) || isTopChild(child, children))) { 59 | sectionTitleView.text = listener.sectionTitle(pos) 60 | fixLayoutSize(headerView, parent) 61 | drawHeader(c, child, headerView) 62 | } 63 | } 64 | } 65 | 66 | private fun initHeaderViewIfNeeded(parent: RecyclerView) { 67 | if (::headerView.isInitialized) return 68 | SectionHeaderBinding.inflate( 69 | LayoutInflater.from(context), 70 | parent, 71 | false 72 | ).apply { 73 | headerView = root 74 | sectionTitleView = header 75 | } 76 | } 77 | 78 | private fun fixLayoutSize(v: View, parent: ViewGroup) { 79 | val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) 80 | val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) 81 | val childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingStart + parent.paddingEnd, v.layoutParams.width) 82 | val childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, v.layoutParams.height) 83 | v.measure(childWidth, childHeight) 84 | v.layout(0, 0, v.measuredWidth, v.measuredHeight) 85 | } 86 | 87 | private fun drawHeader(c: Canvas, child: View, headerView: View) { 88 | c.run { 89 | save() 90 | translate(0F, maxOf(0, child.top - headerView.height).toFloat()) 91 | headerView.draw(this) 92 | restore() 93 | } 94 | } 95 | 96 | private fun isTopChild(child: View, children: List): Boolean { 97 | var tmp = child.top 98 | children.forEach { c -> 99 | tmp = minOf(c.top, tmp) 100 | } 101 | return child.top == tmp 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/SingleClickListener.kt: -------------------------------------------------------------------------------- 1 | package org.readium.r2.testapp.utils 2 | 3 | import android.view.View 4 | 5 | 6 | /** 7 | * Prevents from double clicks on a view, which could otherwise lead to unpredictable states. Useful 8 | * while transitioning to another activity for instance. 9 | */ 10 | class SingleClickListener(private val click: (v: View) -> Unit) : View.OnClickListener { 11 | 12 | companion object { 13 | private const val DOUBLE_CLICK_TIMEOUT = 2500 14 | } 15 | 16 | private var lastClick: Long = 0 17 | 18 | override fun onClick(v: View) { 19 | if (getLastClickTimeout() > DOUBLE_CLICK_TIMEOUT) { 20 | lastClick = System.currentTimeMillis() 21 | click(v) 22 | } 23 | } 24 | 25 | private fun getLastClickTimeout(): Long { 26 | return System.currentTimeMillis() - lastClick 27 | } 28 | } 29 | 30 | fun View.singleClick(l: (View) -> Unit) { 31 | setOnClickListener(SingleClickListener(l)) 32 | } 33 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/SystemUiManagement.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.utils 8 | 9 | import android.app.Activity 10 | import android.view.View 11 | import android.view.WindowInsets 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.core.view.WindowInsetsCompat 14 | 15 | // Using ViewCompat and WindowInsetsCompat does not work properly in all versions of Android 16 | @Suppress("DEPRECATION") 17 | /** Returns `true` if fullscreen or immersive mode is not set. */ 18 | private fun Activity.isSystemUiVisible(): Boolean { 19 | return this.window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0 20 | } 21 | 22 | // Using ViewCompat and WindowInsetsCompat does not work properly in all versions of Android 23 | @Suppress("DEPRECATION") 24 | /** Enable fullscreen or immersive mode. */ 25 | fun Activity.hideSystemUi() { 26 | this.window.decorView.systemUiVisibility = ( 27 | View.SYSTEM_UI_FLAG_IMMERSIVE 28 | or View.SYSTEM_UI_FLAG_LAYOUT_STABLE 29 | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 30 | or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 31 | or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 32 | or View.SYSTEM_UI_FLAG_FULLSCREEN 33 | ) 34 | } 35 | 36 | // Using ViewCompat and WindowInsetsCompat does not work properly in all versions of Android 37 | @Suppress("DEPRECATION") 38 | /** Disable fullscreen or immersive mode. */ 39 | fun Activity.showSystemUi() { 40 | this.window.decorView.systemUiVisibility = ( 41 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 42 | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 43 | or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 44 | ) 45 | } 46 | 47 | /** Toggle fullscreen or immersive mode. */ 48 | fun Activity.toggleSystemUi() { 49 | if (this.isSystemUiVisible()) { 50 | this.hideSystemUi() 51 | } else { 52 | this.showSystemUi() 53 | } 54 | } 55 | 56 | /** Set padding around view so that content doesn't overlap system UI */ 57 | fun View.padSystemUi(insets: WindowInsets, activity: Activity) = 58 | WindowInsetsCompat.toWindowInsetsCompat(insets, this) 59 | .getInsets(WindowInsetsCompat.Type.statusBars()).apply { 60 | setPadding( 61 | left, 62 | top + (activity as AppCompatActivity).supportActionBar!!.height, 63 | right, 64 | bottom 65 | ) 66 | } 67 | 68 | /** Clear padding around view */ 69 | fun View.clearPadding() = 70 | setPadding(0, 0, 0, 0) 71 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/extensions/Bitmap.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.utils.extensions 8 | 9 | import android.graphics.Bitmap 10 | import android.util.Base64 11 | import timber.log.Timber 12 | import java.io.ByteArrayOutputStream 13 | 14 | /** 15 | * Converts the receiver bitmap into a data URL ready to be used in HTML or CSS. 16 | * 17 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 18 | */ 19 | fun Bitmap.toDataUrl(): String? = 20 | try { 21 | val stream = ByteArrayOutputStream() 22 | compress(Bitmap.CompressFormat.PNG, 100, stream) 23 | .also { success -> if (!success) throw Exception("Can't compress image to PNG") } 24 | val b64 = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT) 25 | "data:image/png;base64,$b64" 26 | } catch (e: Exception) { 27 | Timber.e(e) 28 | null 29 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/extensions/Context.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-testapp-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann 4 | * 5 | * Copyright (c) 2018. European Digital Reading Lab. All rights reserved. 6 | * Licensed to the Readium Foundation under one or more contributor license agreements. 7 | * Use of this source code is governed by a BSD-style license which is detailed in the 8 | * LICENSE file present in the project repository where this source code is maintained. 9 | */ 10 | 11 | package org.readium.r2.testapp.utils.extensions 12 | 13 | import android.content.Context 14 | import androidx.annotation.ColorInt 15 | import androidx.annotation.ColorRes 16 | import androidx.core.content.ContextCompat 17 | 18 | 19 | /** 20 | * Extensions 21 | */ 22 | 23 | @ColorInt 24 | fun Context.color(@ColorRes id: Int): Int { 25 | return ContextCompat.getColor(this, id) 26 | } 27 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt: -------------------------------------------------------------------------------- 1 | /* Module: r2-testapp-kotlin 2 | * Developers: Quentin Gliosca, Aferdita Muriqi, Clément Baumann 3 | * 4 | * Copyright (c) 2020. European Digital Reading Lab. All rights reserved. 5 | * Licensed to the Readium Foundation under one or more contributor license agreements. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.testapp.utils.extensions 11 | 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.withContext 14 | import java.io.File 15 | import java.io.FileFilter 16 | import java.io.IOException 17 | 18 | suspend fun File.moveTo(target: File) = withContext(Dispatchers.IO) { 19 | if (!this@moveTo.renameTo(target)) 20 | throw IOException() 21 | } 22 | 23 | 24 | /** 25 | * As there are cases where [File.listFiles] returns null even though it is a directory, we return 26 | * an empty list instead. 27 | */ 28 | fun File.listFilesSafely(filter: FileFilter? = null): List { 29 | val array: Array? = if (filter == null) listFiles() else listFiles(filter) 30 | return array?.toList() ?: emptyList() 31 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/extensions/InputStream.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-testapp-kotlin 3 | * Developers: Aferdita Muriqi, Clément Baumann 4 | * 5 | * Copyright (c) 2018. European Digital Reading Lab. All rights reserved. 6 | * Licensed to the Readium Foundation under one or more contributor license agreements. 7 | * Use of this source code is governed by a BSD-style license which is detailed in the 8 | * LICENSE file present in the project repository where this source code is maintained. 9 | */ 10 | 11 | package org.readium.r2.testapp.utils.extensions 12 | 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.withContext 15 | import org.readium.r2.shared.extensions.tryOrNull 16 | import java.io.File 17 | import java.io.InputStream 18 | import java.util.* 19 | 20 | 21 | suspend fun InputStream.toFile(path: String) { 22 | withContext(Dispatchers.IO) { 23 | use { input -> 24 | File(path).outputStream().use { input.copyTo(it) } 25 | } 26 | } 27 | } 28 | 29 | suspend fun InputStream.copyToTempFile(dir: String): File? = tryOrNull { 30 | val filename = UUID.randomUUID().toString() 31 | File(dir + filename) 32 | .also { toFile(it.path) } 33 | } 34 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/extensions/Link.kt: -------------------------------------------------------------------------------- 1 | package org.readium.r2.testapp.utils.extensions 2 | 3 | import org.readium.r2.shared.publication.Link 4 | 5 | val Link.outlineTitle: String 6 | get() = title ?: href 7 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/extensions/Metadata.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Module: r2-testapp-kotlin 3 | * Developers: Mickaël Menu 4 | * 5 | * Copyright (c) 2020. Readium Foundation. All rights reserved. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.testapp.utils.extensions 11 | 12 | import org.readium.r2.shared.publication.Metadata 13 | 14 | val Metadata.authorName: String get() = 15 | authors.firstOrNull()?.name ?: "" 16 | -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/extensions/URL.kt: -------------------------------------------------------------------------------- 1 | /* Module: r2-testapp-kotlin 2 | * Developers: Quentin Gliosca 3 | * 4 | * Copyright (c) 2020. European Digital Reading Lab. All rights reserved. 5 | * Licensed to the Readium Foundation under one or more contributor license agreements. 6 | * Use of this source code is governed by a BSD-style license which is detailed in the 7 | * LICENSE file present in the project repository where this source code is maintained. 8 | */ 9 | 10 | package org.readium.r2.testapp.utils.extensions 11 | 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.withContext 14 | import org.readium.r2.shared.extensions.extension 15 | import org.readium.r2.shared.extensions.tryOr 16 | import org.readium.r2.shared.extensions.tryOrNull 17 | import java.io.File 18 | import java.io.FileOutputStream 19 | import java.net.URL 20 | import java.util.* 21 | 22 | suspend fun URL.download(path: String): File? = tryOr(null) { 23 | val file = File(path) 24 | withContext(Dispatchers.IO) { 25 | openStream().use { input -> 26 | FileOutputStream(file).use { output -> 27 | input.copyTo(output) 28 | } 29 | } 30 | } 31 | file 32 | } 33 | 34 | suspend fun URL.copyToTempFile(dir: String): File? = tryOrNull { 35 | val filename = UUID.randomUUID().toString() 36 | val path = "$dir$filename.$extension" 37 | download(path) 38 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/java/org/readium/r2/testapp/utils/extensions/Uri.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Readium Foundation. All rights reserved. 3 | * Use of this source code is governed by the BSD-style license 4 | * available in the top-level LICENSE file of the project. 5 | */ 6 | 7 | package org.readium.r2.testapp.utils.extensions 8 | 9 | import android.content.Context 10 | import android.net.Uri 11 | import org.readium.r2.shared.extensions.tryOrNull 12 | import org.readium.r2.shared.util.mediatype.MediaType 13 | import org.readium.r2.testapp.utils.ContentResolverUtil 14 | import java.io.File 15 | import java.util.* 16 | 17 | suspend fun Uri.copyToTempFile(context: Context, dir: String): File? = tryOrNull { 18 | val filename = UUID.randomUUID().toString() 19 | val mediaType = MediaType.ofUri(this, context.contentResolver) 20 | val path = "$dir$filename.${mediaType?.fileExtension ?: "tmp"}" 21 | ContentResolverUtil.getContentInputStream(context, this, path) 22 | return@tryOrNull File(path) 23 | } -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/background_action_mode.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/cnl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/res/drawable/cnl.png -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/res/drawable/cover.png -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_add_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_bookmark_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_enhanced_encryption_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_fast_forward_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_fast_rewind_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_headphones_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_pause_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_play_arrow_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_search_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_skip_next_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_baseline_skip_previous_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_dashboard_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_fastforward_30.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_info_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_local_library_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_notch.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_outline_add_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_outline_format_align_justify_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_outline_format_align_left_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_outline_format_size_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_outline_light_mode_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_outline_menu_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_outline_remove_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_outline_wb_sunny_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_pen.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/ic_rewind_30.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/icon_font_decrease.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/res/drawable/icon_font_decrease.png -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/icon_font_increase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/res/drawable/icon_font_increase.png -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/icon_overflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/res/drawable/icon_overflow.png -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/rbtn_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/rbtn_textcolor_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/repfr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readium/r2-testapp-kotlin/426af0d69eb543f9415a38744c8cf9f7c4389e96/r2-testapp/src/main/res/drawable/repfr.png -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/selector_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/selector_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/selector_purple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/selector_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/drawable/selector_yellow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/activity_epub.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 31 | 32 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/activity_reader.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/add_catalog_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/filter_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 25 | 26 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/filter_window.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/fragment_bookshelf.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 19 | 20 | 28 | 29 | 36 | 37 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/fragment_catalog.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 20 | 21 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /r2-testapp/src/main/res/layout/fragment_catalog_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 19 | 22 | 23 | 27 | 28 | 34 | 35 | 40 | 41 | 47 | 48 | 53 | 54 |