diff --git a/app/build.gradle b/app/build.gradle index f1c74fd5c..c30060c0c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -211,11 +211,6 @@ dependencies { //转换繁体 implementation 'com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.1.3' - //epub -// implementation('com.positiondev.epublib:epublib-core:3.1') { -// exclude group: 'org.slf4j' -// exclude group: 'xmlpull' -// } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9a149a441..85914dd9d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -171,6 +171,10 @@ + + <.*?>([^<]+)<\\\/span><\\\/a>\/g,'

$2<\/a><\/h4>

$1<\/p>\\n\\n')}\nelse{\nhtml=String(java.getString(\"@@class.markdown-body@html\",false)+java.getString(\"@@class.flex-justify-between.-1@html\",false));\nhref='https:\/\/github.com'+String(java.getString(\"@@class.flex-justify-between.-1@class.Box-body.0@a@href\",false))\nhtml+'

下载地址:<\/p>'+href+'<\/a>'\n}", @@ -19,7 +28,7 @@ "style": "" }, { - "customOrder": 1, + "customOrder": 3, "enableJs": true, "enabled": true, "singleUrl": true, diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index 6107bf03e..cb8557c7b 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -7,6 +7,7 @@ * 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右 * js添加一个并发访问的方法 java.ajaxAll(urlList: Array) 返回 Array * 优化目录并发访问 +* 添加自定义epublib,支持epub v3解析目录。by ag2s20150909 **2021/03/19** * 修复图片地址参数缺少的bug diff --git a/app/src/main/java/io/legado/app/help/LocalConfig.kt b/app/src/main/java/io/legado/app/help/LocalConfig.kt index f70007fa3..b2aa30234 100644 --- a/app/src/main/java/io/legado/app/help/LocalConfig.kt +++ b/app/src/main/java/io/legado/app/help/LocalConfig.kt @@ -10,6 +10,33 @@ object LocalConfig { private val localConfig = appCtx.getSharedPreferences("local", Context.MODE_PRIVATE) + val readHelpVersionIsLast: Boolean + get() = isLastVersion(1, "readHelpVersion", "firstRead") + + val backupHelpVersionIsLast: Boolean + get() = isLastVersion(1, "backupHelpVersion", "firstBackup") + + val readMenuHelpVersionIsLast: Boolean + get() = isLastVersion(1, "readMenuHelpVersion", "firstReadMenu") + + val bookSourcesHelpVersionIsLast: Boolean + get() = isLastVersion(1, "bookSourceHelpVersion", "firstOpenBookSources") + + val debugHelpVersionIsLast: Boolean + get() = isLastVersion(1, "debugHelpVersion") + + val ruleHelpVersionIsLast: Boolean + get() = isLastVersion(1, "ruleHelpVersion") + + val hasUpHttpTTS: Boolean + get() = !isLastVersion(3, "httpTtsVersion") + + val hasUpTxtTocRule: Boolean + get() = !isLastVersion(1, "txtTocRuleVersion") + + val hasUpRssSources: Boolean + get() = !isLastVersion(3, "rssSourceVersion") + var versionCode get() = localConfig.getLong(versionCodeKey, 0) set(value) { @@ -46,30 +73,4 @@ object LocalConfig { return true } - val readHelpVersionIsLast: Boolean - get() = isLastVersion(1, "readHelpVersion", "firstRead") - - val backupHelpVersionIsLast: Boolean - get() = isLastVersion(1, "backupHelpVersion", "firstBackup") - - val readMenuHelpVersionIsLast: Boolean - get() = isLastVersion(1, "readMenuHelpVersion", "firstReadMenu") - - val bookSourcesHelpVersionIsLast: Boolean - get() = isLastVersion(1, "bookSourceHelpVersion", "firstOpenBookSources") - - val debugHelpVersionIsLast: Boolean - get() = isLastVersion(1, "debugHelpVersion") - - val ruleHelpVersionIsLast: Boolean - get() = isLastVersion(1, "ruleHelpVersion") - - val hasUpHttpTTS: Boolean - get() = !isLastVersion(3, "httpTtsVersion") - - val hasUpTxtTocRule: Boolean - get() = !isLastVersion(1, "txtTocRuleVersion") - - val hasUpRssSources: Boolean - get() = !isLastVersion(1, "rssSourceVersion") } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/permission/ActivitySource.kt b/app/src/main/java/io/legado/app/help/permission/ActivitySource.kt new file mode 100644 index 000000000..e0618067a --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/ActivitySource.kt @@ -0,0 +1,20 @@ +package io.legado.app.help.permission + +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity + +import java.lang.ref.WeakReference + +internal class ActivitySource(activity: AppCompatActivity) : RequestSource { + + private val actRef: WeakReference = WeakReference(activity) + + override val context: Context? + get() = actRef.get() + + override fun startActivity(intent: Intent) { + actRef.get()?.startActivity(intent) + } + +} diff --git a/app/src/main/java/io/legado/app/help/permission/FragmentSource.kt b/app/src/main/java/io/legado/app/help/permission/FragmentSource.kt new file mode 100644 index 000000000..b66e11b98 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/FragmentSource.kt @@ -0,0 +1,19 @@ +package io.legado.app.help.permission + +import android.content.Context +import android.content.Intent +import androidx.fragment.app.Fragment + +import java.lang.ref.WeakReference + +internal class FragmentSource(fragment: Fragment) : RequestSource { + + private val fragRef: WeakReference = WeakReference(fragment) + + override val context: Context? + get() = fragRef.get()?.requireContext() + + override fun startActivity(intent: Intent) { + fragRef.get()?.startActivity(intent) + } +} diff --git a/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt new file mode 100644 index 000000000..4a51881b0 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt @@ -0,0 +1,7 @@ +package io.legado.app.help.permission + +interface OnPermissionsDeniedCallback { + + fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) + +} diff --git a/app/src/main/java/io/legado/app/help/permission/OnPermissionsGrantedCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnPermissionsGrantedCallback.kt new file mode 100644 index 000000000..59f6977d4 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/OnPermissionsGrantedCallback.kt @@ -0,0 +1,7 @@ +package io.legado.app.help.permission + +interface OnPermissionsGrantedCallback { + + fun onPermissionsGranted(requestCode: Int) + +} diff --git a/app/src/main/java/io/legado/app/help/permission/OnPermissionsResultCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnPermissionsResultCallback.kt new file mode 100644 index 000000000..3d7afa600 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/OnPermissionsResultCallback.kt @@ -0,0 +1,9 @@ +package io.legado.app.help.permission + +interface OnPermissionsResultCallback { + + fun onPermissionsGranted(requestCode: Int) + + fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/permission/OnRequestPermissionsResultCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnRequestPermissionsResultCallback.kt new file mode 100644 index 000000000..fde78ae82 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/OnRequestPermissionsResultCallback.kt @@ -0,0 +1,14 @@ +package io.legado.app.help.permission + +import android.content.Intent + +interface OnRequestPermissionsResultCallback { + + fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) +} diff --git a/app/src/main/java/io/legado/app/help/permission/PermissionActivity.kt b/app/src/main/java/io/legado/app/help/permission/PermissionActivity.kt new file mode 100644 index 000000000..095f7e164 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/PermissionActivity.kt @@ -0,0 +1,84 @@ +package io.legado.app.help.permission + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.view.KeyEvent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import io.legado.app.R +import io.legado.app.utils.toastOnUi + +class PermissionActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + when (intent.getIntExtra(KEY_INPUT_REQUEST_TYPE, Request.TYPE_REQUEST_PERMISSION)) { + Request.TYPE_REQUEST_PERMISSION//权限请求 + -> { + val requestCode = intent.getIntExtra(KEY_INPUT_PERMISSIONS_CODE, 1000) + val permissions = intent.getStringArrayExtra(KEY_INPUT_PERMISSIONS) + if (permissions != null) { + ActivityCompat.requestPermissions(this, permissions, requestCode) + } else { + finish() + } + } + Request.TYPE_REQUEST_SETTING//跳转到设置界面 + -> try { + val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + settingIntent.data = Uri.fromParts("package", packageName, null) + startActivityForResult(settingIntent, Request.TYPE_REQUEST_SETTING) + } catch (e: Exception) { + toastOnUi(R.string.tip_cannot_jump_setting_page) + finish() + } + + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + RequestPlugins.sRequestCallback?.onRequestPermissionsResult( + requestCode, + permissions, + grantResults + ) + finish() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + RequestPlugins.sRequestCallback?.onActivityResult(requestCode, resultCode, data) + finish() + } + + override fun startActivity(intent: Intent) { + super.startActivity(intent) + overridePendingTransition(0, 0) + } + + override fun finish() { + super.finish() + overridePendingTransition(0, 0) + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + return if (keyCode == KeyEvent.KEYCODE_BACK) { + true + } else super.onKeyDown(keyCode, event) + } + + companion object { + + const val KEY_INPUT_REQUEST_TYPE = "KEY_INPUT_REQUEST_TYPE" + const val KEY_INPUT_PERMISSIONS_CODE = "KEY_INPUT_PERMISSIONS_CODE" + const val KEY_INPUT_PERMISSIONS = "KEY_INPUT_PERMISSIONS" + } +} diff --git a/app/src/main/java/io/legado/app/constant/Permissions.kt b/app/src/main/java/io/legado/app/help/permission/Permissions.kt similarity index 98% rename from app/src/main/java/io/legado/app/constant/Permissions.kt rename to app/src/main/java/io/legado/app/help/permission/Permissions.kt index c78bfeb4c..978541f69 100644 --- a/app/src/main/java/io/legado/app/constant/Permissions.kt +++ b/app/src/main/java/io/legado/app/help/permission/Permissions.kt @@ -1,4 +1,4 @@ -package io.legado.app.constant +package io.legado.app.help.permission @Suppress("unused") object Permissions { diff --git a/app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt b/app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt new file mode 100644 index 000000000..6875b9e19 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt @@ -0,0 +1,94 @@ +package io.legado.app.help.permission + +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import java.util.* + +@Suppress("unused") +class PermissionsCompat private constructor() { + + private var request: Request? = null + + fun request() { + RequestManager.pushRequest(request) + } + + companion object { + // 检查权限, 如果已经拥有返回 true + fun check(activity: AppCompatActivity, vararg permissions: String): Boolean { + val request = Request(activity) + val pers = ArrayList() + pers.addAll(listOf(*permissions)) + val data = request.getDeniedPermissions(pers.toTypedArray()) + return data == null + } + } + + class Builder { + private val request: Request + + constructor(activity: AppCompatActivity) { + request = Request(activity) + } + + constructor(fragment: Fragment) { + request = Request(fragment) + } + + fun addPermissions(vararg permissions: String): Builder { + request.addPermissions(*permissions) + return this + } + + fun requestCode(requestCode: Int): Builder { + request.setRequestCode(requestCode) + return this + } + + fun onGranted(callback: (requestCode: Int) -> Unit): Builder { + request.setOnGrantedCallback(object : OnPermissionsGrantedCallback { + override fun onPermissionsGranted(requestCode: Int) { + callback(requestCode) + } + }) + return this + } + + fun onDenied(callback: (requestCode: Int, deniedPermissions: Array) -> Unit): Builder { + request.setOnDeniedCallback(object : OnPermissionsDeniedCallback { + override fun onPermissionsDenied( + requestCode: Int, + deniedPermissions: Array + ) { + callback(requestCode, deniedPermissions) + } + }) + return this + } + + fun rationale(rationale: CharSequence): Builder { + request.setRationale(rationale) + return this + } + + fun rationale(@StringRes resId: Int): Builder { + request.setRationale(resId) + return this + } + + fun build(): PermissionsCompat { + val compat = PermissionsCompat() + compat.request = request + return compat + } + + fun request(): PermissionsCompat { + val compat = build() + compat.request = request + compat.request() + return compat + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/permission/Request.kt b/app/src/main/java/io/legado/app/help/permission/Request.kt new file mode 100644 index 000000000..7a26f521f --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/Request.kt @@ -0,0 +1,204 @@ +package io.legado.app.help.permission + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import io.legado.app.R +import io.legado.app.utils.startActivity +import java.util.* + +internal class Request : OnRequestPermissionsResultCallback { + + internal val requestTime: Long + private var requestCode: Int = TYPE_REQUEST_PERMISSION + private var source: RequestSource? = null + private var permissions: ArrayList? = null + private var grantedCallback: OnPermissionsGrantedCallback? = null + private var deniedCallback: OnPermissionsDeniedCallback? = null + private var rationaleResId: Int = 0 + private var rationale: CharSequence? = null + + private var rationaleDialog: AlertDialog? = null + + private val deniedPermissions: Array? + get() { + return getDeniedPermissions(this.permissions?.toTypedArray()) + } + + constructor(activity: AppCompatActivity) { + source = ActivitySource(activity) + permissions = ArrayList() + requestTime = System.currentTimeMillis() + } + + constructor(fragment: Fragment) { + source = FragmentSource(fragment) + permissions = ArrayList() + requestTime = System.currentTimeMillis() + } + + fun addPermissions(vararg permissions: String) { + this.permissions?.addAll(listOf(*permissions)) + } + + fun setRequestCode(requestCode: Int) { + this.requestCode = requestCode + } + + fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) { + grantedCallback = callback + } + + fun setOnDeniedCallback(callback: OnPermissionsDeniedCallback) { + deniedCallback = callback + } + + fun setRationale(@StringRes resId: Int) { + rationaleResId = resId + rationale = null + } + + fun setRationale(rationale: CharSequence) { + this.rationale = rationale + rationaleResId = 0 + } + + fun start() { + RequestPlugins.setOnRequestPermissionsCallback(this) + + val deniedPermissions = deniedPermissions + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + if (deniedPermissions == null) { + onPermissionsGranted(requestCode) + } else { + val rationale = + if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale + if (rationale != null) { + showSettingDialog(rationale) { + onPermissionsDenied( + requestCode, + deniedPermissions + ) + } + } else { + onPermissionsDenied(requestCode, deniedPermissions) + } + } + } else { + if (deniedPermissions != null) { + source?.context?.startActivity { + putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_PERMISSION) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions) + } + } else { + onPermissionsGranted(requestCode) + } + } + } + + fun clear() { + grantedCallback = null + deniedCallback = null + } + + fun getDeniedPermissions(permissions: Array?): Array? { + if (permissions != null) { + val deniedPermissionList = ArrayList() + for (permission in permissions) { + if (source?.context?.let { + ContextCompat.checkSelfPermission( + it, + permission + ) + } != PackageManager.PERMISSION_GRANTED + ) { + deniedPermissionList.add(permission) + } + } + val size = deniedPermissionList.size + if (size > 0) { + return deniedPermissionList.toTypedArray() + } + } + return null + } + + private fun showSettingDialog(rationale: CharSequence, cancel: () -> Unit) { + rationaleDialog?.dismiss() + source?.context?.let { + runCatching { + rationaleDialog = AlertDialog.Builder(it) + .setTitle(R.string.dialog_title) + .setMessage(rationale) + .setPositiveButton(R.string.dialog_setting) { _, _ -> + it.startActivity { + putExtra( + PermissionActivity.KEY_INPUT_REQUEST_TYPE, + TYPE_REQUEST_SETTING + ) + } + } + .setNegativeButton(R.string.dialog_cancel) { _, _ -> cancel() } + .show() + } + } + } + + private fun onPermissionsGranted(requestCode: Int) { + try { + grantedCallback?.onPermissionsGranted(requestCode) + } catch (ignore: Exception) { + } + + RequestPlugins.sResultCallback?.onPermissionsGranted(requestCode) + } + + private fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) { + try { + deniedCallback?.onPermissionsDenied(requestCode, deniedPermissions) + } catch (ignore: Exception) { + } + + RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + val deniedPermissions = getDeniedPermissions(permissions) + if (deniedPermissions != null) { + val rationale = + if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale + if (rationale != null) { + showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } + } else { + onPermissionsDenied(requestCode, deniedPermissions) + } + } else { + onPermissionsGranted(requestCode) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + val deniedPermissions = deniedPermissions + if (deniedPermissions == null) { + onPermissionsGranted(this.requestCode) + } else { + onPermissionsDenied(this.requestCode, deniedPermissions) + } + } + + companion object { + const val TYPE_REQUEST_PERMISSION = 1 + const val TYPE_REQUEST_SETTING = 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/permission/RequestManager.kt b/app/src/main/java/io/legado/app/help/permission/RequestManager.kt new file mode 100644 index 000000000..eb28b9e6c --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/RequestManager.kt @@ -0,0 +1,69 @@ +package io.legado.app.help.permission + +import android.os.Handler +import android.os.Looper +import java.util.* + +internal object RequestManager : OnPermissionsResultCallback { + + private var requests: Stack? = null + private var request: Request? = null + + private val handler = Handler(Looper.getMainLooper()) + + private val requestRunnable = Runnable { + request?.start() + } + + private val isCurrentRequestInvalid: Boolean + get() = request?.let { System.currentTimeMillis() - it.requestTime > 5 * 1000L } ?: true + + init { + RequestPlugins.setOnPermissionsResultCallback(this) + } + + fun pushRequest(request: Request?) { + if (request == null) return + + if (requests == null) { + requests = Stack() + } + + requests?.let { + val index = it.indexOf(request) + if (index >= 0) { + val to = it.size - 1 + if (index != to) { + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + Collections.swap(requests, index, to) + } + } else { + it.push(request) + } + + if (!it.empty() && isCurrentRequestInvalid) { + this.request = it.pop() + handler.post(requestRunnable) + } + } + } + + private fun startNextRequest() { + request?.clear() + request = null + + requests?.let { + request = if (it.empty()) null else it.pop() + request?.let { handler.post(requestRunnable) } + } + } + + override fun onPermissionsGranted(requestCode: Int) { + startNextRequest() + } + + override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) { + startNextRequest() + } + +} diff --git a/app/src/main/java/io/legado/app/help/permission/RequestPlugins.kt b/app/src/main/java/io/legado/app/help/permission/RequestPlugins.kt new file mode 100644 index 000000000..16370193f --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/RequestPlugins.kt @@ -0,0 +1,20 @@ +package io.legado.app.help.permission + +internal object RequestPlugins { + + @Volatile + var sRequestCallback: OnRequestPermissionsResultCallback? = null + + @Volatile + var sResultCallback: OnPermissionsResultCallback? = null + + fun setOnRequestPermissionsCallback(callback: OnRequestPermissionsResultCallback) { + sRequestCallback = callback + } + + fun setOnPermissionsResultCallback(callback: OnPermissionsResultCallback) { + sResultCallback = callback + } + + +} diff --git a/app/src/main/java/io/legado/app/help/permission/RequestSource.kt b/app/src/main/java/io/legado/app/help/permission/RequestSource.kt new file mode 100644 index 000000000..a822ff5ad --- /dev/null +++ b/app/src/main/java/io/legado/app/help/permission/RequestSource.kt @@ -0,0 +1,12 @@ +package io.legado.app.help.permission + +import android.content.Context +import android.content.Intent + +interface RequestSource { + + val context: Context? + + fun startActivity(intent: Intent) + +} diff --git a/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt b/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt similarity index 54% rename from app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt rename to app/src/main/java/io/legado/app/model/localBook/EpubFile.kt index c64468264..d3d9af2f1 100644 --- a/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt +++ b/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt @@ -5,14 +5,12 @@ import android.graphics.BitmapFactory import android.net.Uri import android.text.TextUtils import io.legado.app.data.appDb +import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.EpubChapter import io.legado.app.utils.* - -import me.ag2s.epublib.domain.Book +import me.ag2s.epublib.domain.EpubBook import me.ag2s.epublib.domain.MediaTypes import me.ag2s.epublib.domain.Resources -import me.ag2s.epublib.domain.TOCReference import me.ag2s.epublib.epub.EpubReader import me.ag2s.epublib.util.ResourceUtil import org.jsoup.Jsoup @@ -26,15 +24,15 @@ import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipInputStream -class EPUBFile(var book: io.legado.app.data.entities.Book) { +class EpubFile(var book: Book) { companion object { - private var eFile: EPUBFile? = null + private var eFile: EpubFile? = null @Synchronized - private fun getEFile(book: io.legado.app.data.entities.Book): EPUBFile { + private fun getEFile(book: Book): EpubFile { if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) { - eFile = EPUBFile(book) + eFile = EpubFile(book) return eFile!! } eFile?.book = book @@ -42,30 +40,30 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) { } @Synchronized - fun getChapterList(book: io.legado.app.data.entities.Book): ArrayList { + fun getChapterList(book: Book): ArrayList { return getEFile(book).getChapterList() } @Synchronized - fun getContent(book: io.legado.app.data.entities.Book, chapter: BookChapter): String? { + fun getContent(book: Book, chapter: BookChapter): String? { return getEFile(book).getContent(chapter) } @Synchronized fun getImage( - book: io.legado.app.data.entities.Book, + book: Book, href: String ): InputStream? { return getEFile(book).getImage(href) } @Synchronized - fun upBookInfo(book: io.legado.app.data.entities.Book) { + fun upBookInfo(book: Book) { return getEFile(book).upBookInfo() } } - private var epubBook: Book? = null + private var epubBook: EpubBook? = null private var mCharset: Charset = Charset.defaultCharset() init { @@ -102,7 +100,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) { } /*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/ - private fun readEpub(input: InputStream?): Book? { + private fun readEpub(input: InputStream?): EpubBook? { if (input == null) return null try { val inZip = ZipInputStream(input) @@ -156,7 +154,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) { } /*选择去除正文中的H标签,部分书籍标题与阅读标题重复待优化*/ - var tag = io.legado.app.data.entities.Book.hTag + var tag = Book.hTag if (book.getDelTag(tag)) { body.getElementsByTag("h1")?.remove() body.getElementsByTag("h2")?.remove() @@ -168,7 +166,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) { } /*选择去除正文中的img标签,目前图片支持效果待优化*/ - tag = io.legado.app.data.entities.Book.imgTag + tag = Book.imgTag if (book.getDelTag(tag)) { body.getElementsByTag("img")?.remove() } @@ -177,7 +175,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) { elements.select("script").remove() elements.select("style").remove() /*选择去除正文中的ruby标签,目前注释支持效果待优化*/ - tag = io.legado.app.data.entities.Book.rubyTag + tag = Book.rubyTag var html = elements.outerHtml() if (book.getDelTag(tag)) { html = html.replace("\\s?([\\u4e00-\\u9fa5])\\s?.*?".toRegex(), "$1") @@ -212,156 +210,34 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) { private fun getChapterList(): ArrayList { val chapterList = ArrayList() - epubBook?.let { eBook -> - val refs = eBook.tableOfContents.tocReferences - if (refs == null || refs.isEmpty()) { - val spineReferences = eBook.spine.spineReferences - var i = 0 - val size = spineReferences.size - while (i < size) { - val resource = - spineReferences[i].resource - var title = resource.title - if (TextUtils.isEmpty(title)) { - try { - val doc = - Jsoup.parse(String(resource.data, mCharset)) - val elements = doc.getElementsByTag("title") - if (elements != null && elements.size > 0) { - title = elements[0].text() - } - } catch (e: IOException) { - e.printStackTrace() - } - } - val chapter = BookChapter() - chapter.index = i - chapter.bookUrl = book.bookUrl - chapter.url = resource.href - if (i == 0 && title.isEmpty()) { - chapter.title = "封面" - } else { - chapter.title = title - } - chapterList.add(chapter) - i++ - } - } else { - parseFirstPage(chapterList, refs) - parseMenu(chapterList, refs, 0) - for (i in chapterList.indices) { - chapterList[i].index = i - } - getChildChapter(chapterList) - } - } - book.latestChapterTitle = chapterList.lastOrNull()?.title - book.totalChapterNum = chapterList.size - return chapterList - } - - /*获取当前章节的子章节。部分书籍一个章节包含多个html文件,(一些精排书籍,每一章节正文前的标题、标题封面、引言等都会有独立html)*/ - /*需在读取常规章节列表后调用,遍历书籍全内容,根据href检索原不包含在章节内的html归属父章节*/ - private fun getChildChapter(chapterList: ArrayList) { - epubBook?.let { - val contents = it.contents - val chapters = ArrayList() - if (contents != null) { - var i = 0 - var j = 0 - var parentHref: String? = null - while (i < contents.size) { - val content = contents[i] - if (j < chapterList.size && content.href == chapterList[j].url) { - parentHref = content.href - j++ - } else if (!parentHref.isNullOrBlank() && content.mediaType.toString() - .contains("htm") - ) { - val epubChapter = EpubChapter() - epubChapter.bookUrl = book.bookUrl - epubChapter.href = content.href - epubChapter.parentHref = parentHref - chapters.add(epubChapter) + epubBook?.tableOfContents?.allUniqueResources?.forEachIndexed { index, resource -> + var title = resource.title + if (TextUtils.isEmpty(title)) { + try { + val doc = + Jsoup.parse(String(resource.data, mCharset)) + val elements = doc.getElementsByTag("title") + if (elements != null && elements.size > 0) { + title = elements[0].text() } - i++ + } catch (e: IOException) { + e.printStackTrace() } } - appDb.epubChapterDao.deleteByName(book.bookUrl) - if (chapters.size > 0) appDb.epubChapterDao.insert(*chapters.toTypedArray()) - } - } - - /*获取书籍起始页内容。部分书籍第一章之前存在封面,引言,扉页等内容*/ - /*tile获取不同书籍风格杂乱,格式化处理待优化*/ - private var durIndex = 0 - private fun parseFirstPage( - chapterList: ArrayList, - refs: List? - ) { - val contents = epubBook?.contents - if (epubBook == null || contents == null || refs == null) return - var i = 0 - durIndex = 0 - while (i < contents.size) { - val content = contents[i] - if (!content.mediaType.toString().contains("htm")) continue - /*检索到第一章href停止*/ - if (refs[0].completeHref == content.href) break val chapter = BookChapter() - var title = content.title - if (TextUtils.isEmpty(title)) { - val elements = Jsoup.parse( - String( - epubBook!!.resources.getByHref(content.href).data, - mCharset - ) - ).getElementsByTag("title") - title = - if (elements != null && elements.size > 0 && elements[0].text() - .isNotBlank() - ) elements[0].text() else "--卷首--" - } + chapter.index = index chapter.bookUrl = book.bookUrl - chapter.title = title - chapter.url = content.href - chapter.startFragmentId = - if (content.href.substringAfter("#") == content.href) null - else content.href.substringAfter("#") - if (durIndex > 0) { - val preIndex = durIndex - 1 - chapterList[preIndex].endFragmentId = chapter.startFragmentId + chapter.url = resource.href + if (index == 0 && title.isEmpty()) { + chapter.title = "封面" + } else { + chapter.title = title } chapterList.add(chapter) - durIndex++ - i++ - } - } - - private fun parseMenu( - chapterList: ArrayList, - refs: List?, - level: Int - ) { - refs?.forEach { ref -> - if (ref.resource != null) { - val chapter = BookChapter() - chapter.bookUrl = book.bookUrl - chapter.title = ref.title - chapter.url = ref.completeHref - chapter.startFragmentId = ref.fragmentId - if (durIndex > 0) { - val preIndex = durIndex - 1 - chapterList[preIndex].endFragmentId = chapter.startFragmentId - } - chapterList.add(chapter) - durIndex++ - } - if (ref.children != null && ref.children.isNotEmpty()) { - parseMenu(chapterList, ref.children, level + 1) - } } + book.latestChapterTitle = chapterList.lastOrNull()?.title + book.totalChapterNum = chapterList.size + return chapterList } - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index e65eaa7b1..19e293b2b 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -22,7 +22,7 @@ object LocalBook { fun getChapterList(book: Book): ArrayList { return if (book.isEpub()) { - EPUBFile.getChapterList(book) + EpubFile.getChapterList(book) } else { AnalyzeTxtFile().analyze(book) } @@ -30,7 +30,7 @@ object LocalBook { fun getContext(book: Book, chapter: BookChapter): String? { return if (book.isEpub()) { - EPUBFile.getContent(book, chapter) + EpubFile.getContent(book, chapter) } else { AnalyzeTxtFile.getContent(book, chapter) } @@ -83,7 +83,7 @@ object LocalBook { "${MD5Utils.md5Encode16(path)}.jpg" ) ) - if (book.isEpub()) EPUBFile.upBookInfo(book) + if (book.isEpub()) EpubFile.upBookInfo(book) appDb.bookDao.insert(book) return book } diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt index 4963fe860..4d16ba6bd 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt @@ -1,18 +1,19 @@ package io.legado.app.ui.book.info.edit import android.app.Activity +import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.documentfile.provider.DocumentFile import io.legado.app.R import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.Permissions import io.legado.app.data.entities.Book import io.legado.app.databinding.ActivityBookInfoEditBinding +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.utils.* import java.io.File @@ -21,9 +22,7 @@ class BookInfoEditActivity : VMBaseActivity(), ChangeCoverDialog.CallBack { - private val selectCover = registerForActivityResult(ActivityResultContracts.GetContent()) { - coverChangeTo(it) - } + private val resultSelectCover = 132 override val viewModel: BookInfoEditViewModel by viewModels() @@ -61,7 +60,7 @@ class BookInfoEditActivity : } } tvSelectCover.setOnClickListener { - selectCover.launch("image/*") + selectImage() } tvRefreshCover.setOnClickListener { viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString() @@ -97,6 +96,13 @@ class BookInfoEditActivity : } } + private fun selectImage() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "image/*" + startActivityForResult(intent, resultSelectCover) + } + override fun coverChangeTo(coverUrl: String) { viewModel.book?.customCoverUrl = coverUrl binding.tieCoverUrl.setText(coverUrl) @@ -117,18 +123,37 @@ class BookInfoEditActivity : } ?: toastOnUi("获取文件出错") } } else { - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - RealPathUtil.getPath(this, uri)?.let { path -> - val imgFile = File(path) - if (imgFile.exists()) { - var file = this.externalFilesDir - file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name) - file.writeBytes(imgFile.readBytes()) - coverChangeTo(file.absolutePath) + PermissionsCompat.Builder(this) + .addPermissions( + Permissions.READ_EXTERNAL_STORAGE, + Permissions.WRITE_EXTERNAL_STORAGE + ) + .rationale(R.string.bg_image_per) + .onGranted { + RealPathUtil.getPath(this, uri)?.let { path -> + val imgFile = File(path) + if (imgFile.exists()) { + var file = this.externalFilesDir + file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name) + file.writeBytes(imgFile.readBytes()) + coverChangeTo(file.absolutePath) + } } } - }.launch(Permissions.Group.STORAGE) + .request() } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + resultSelectCover -> { + if (resultCode == Activity.RESULT_OK) { + data?.data?.let { uri -> + coverChangeTo(uri) + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt index 9cb291e96..776d980c5 100644 --- a/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt @@ -8,7 +8,6 @@ import android.os.Bundle import android.provider.DocumentsContract import android.view.Menu import android.view.MenuItem -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.widget.PopupMenu import androidx.documentfile.provider.DocumentFile @@ -16,10 +15,11 @@ import androidx.lifecycle.LiveData import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.Permissions import io.legado.app.data.appDb import io.legado.app.databinding.ActivityImportBookBinding import io.legado.app.help.AppConfig +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.theme.backgroundColor import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.filepicker.FilePickerDialog @@ -148,12 +148,16 @@ class ImportBookActivity : VMBaseActivity { binding.tvEmptyMsg.visible() - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - rootDoc = null - subDocs.clear() - path = lastPath - upPath() - }.launch(Permissions.Group.STORAGE) + PermissionsCompat.Builder(this) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + rootDoc = null + subDocs.clear() + path = lastPath + upPath() + } + .request() } } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt index e9d8775de..7b4eee738 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt @@ -8,17 +8,17 @@ import android.graphics.Color import android.net.Uri import android.os.Bundle import android.view.* -import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile import com.jaredrummler.android.colorpicker.ColorPickerDialog import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.EventBus -import io.legado.app.constant.Permissions import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogReadBgTextBinding import io.legado.app.databinding.ItemBgImageBinding import io.legado.app.help.ReadBookConfig +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor @@ -40,6 +40,7 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack { } private val binding by viewBinding(DialogReadBgTextBinding::bind) + private val requestCodeBg = 123 private val requestCodeExport = 131 private val requestCodeImport = 132 private val configFileName = "readConfig.zip" @@ -190,9 +191,10 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack { } private fun selectImage() { - registerForActivityResult(ActivityResultContracts.GetContent()) { - setBgFromUri(it) - }.launch("image/*") + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "image/*" + startActivityForResult(intent, requestCodeBg) } @Suppress("BlockingMethodInNonBlockingContext") @@ -369,7 +371,13 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) when (requestCode) { + requestCodeBg -> if (resultCode == RESULT_OK) { + data?.data?.let { uri -> + setBgFromUri(uri) + } + } requestCodeImport -> if (resultCode == RESULT_OK) { data?.data?.let { uri -> importConfig(uri) @@ -399,13 +407,20 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack { } ?: toastOnUi("获取文件出错") } } else { - registerForActivityResult(ActivityResultContracts.RequestPermission()) { - RealPathUtil.getPath(requireContext(), uri)?.let { path -> - ReadBookConfig.durConfig.setCurBg(2, path) - ReadBookConfig.upBg() - postEvent(EventBus.UP_CONFIG, false) + PermissionsCompat.Builder(this) + .addPermissions( + Permissions.READ_EXTERNAL_STORAGE, + Permissions.WRITE_EXTERNAL_STORAGE + ) + .rationale(R.string.bg_image_per) + .onGranted { + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + ReadBookConfig.durConfig.setCurBg(2, path) + ReadBookConfig.upBg() + postEvent(EventBus.UP_CONFIG, false) + } } - }.launch(Permissions.READ_EXTERNAL_STORAGE) + .request() } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlMesh.java b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlMesh.java new file mode 100644 index 000000000..e416bea3a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlMesh.java @@ -0,0 +1,957 @@ +package io.legado.app.ui.book.read.page.delegate.curl; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.RectF; +import android.opengl.GLUtils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import javax.microedition.khronos.opengles.GL10; + +/** + * Class implementing actual curl/page rendering. + * + * @author harism + */ +public class CurlMesh { + + // Flag for rendering some lines used for developing. Shows + // curl position and one for the direction from the + // position given. Comes handy once playing around with different + // ways for following pointer. + private static final boolean DRAW_CURL_POSITION = false; + // Flag for drawing polygon outlines. Using this flag crashes on emulator + // due to reason unknown to me. Leaving it here anyway as seeing polygon + // outlines gives good insight how original rectangle is divided. + private static final boolean DRAW_POLYGON_OUTLINES = false; + // Flag for enabling shadow rendering. + private static final boolean DRAW_SHADOW = true; + // Flag for texture rendering. While this is likely something you + // don't want to do it's been used for development purposes as texture + // rendering is rather slow on emulator. + private static final boolean DRAW_TEXTURE = true; + + // Colors for shadow. Inner one is the color drawn next to surface where + // shadowed area starts and outer one is color shadow ends to. + private static final float[] SHADOW_INNER_COLOR = {0f, 0f, 0f, .5f}; + private static final float[] SHADOW_OUTER_COLOR = {0f, 0f, 0f, .0f}; + + // Let's avoid using 'new' as much as possible. Meaning we introduce arrays + // once here and reuse them on runtime. Doesn't really have very much effect + // but avoids some garbage collections from happening. + private Array mArrDropShadowVertices; + private Array mArrIntersections; + private Array mArrOutputVertices; + private Array mArrRotatedVertices; + private Array mArrScanLines; + private Array mArrSelfShadowVertices; + private Array mArrTempShadowVertices; + private Array mArrTempVertices; + + // Buffers for feeding rasterizer. + private FloatBuffer mBufColors; + private FloatBuffer mBufCurlPositionLines; + private FloatBuffer mBufShadowColors; + private FloatBuffer mBufShadowVertices; + private FloatBuffer mBufTexCoords; + private FloatBuffer mBufVertices; + + private int mCurlPositionLinesCount; + private int mDropShadowCount; + + // Boolean for 'flipping' texture sideways. + private boolean mFlipTexture = false; + // Maximum number of split lines used for creating a curl. + private int mMaxCurlSplits; + + // Bounding rectangle for this mesh. mRectagle[0] = top-left corner, + // mRectangle[1] = bottom-left, mRectangle[2] = top-right and mRectangle[3] + // bottom-right. + private final Vertex[] mRectangle = new Vertex[4]; + private int mSelfShadowCount; + + private boolean mTextureBack = false; + // Texture ids and other variables. + private int[] mTextureIds = null; + private final CurlPage mTexturePage = new CurlPage(); + private final RectF mTextureRectBack = new RectF(); + private final RectF mTextureRectFront = new RectF(); + + private int mVerticesCountBack; + private int mVerticesCountFront; + + /** + * Constructor for mesh object. + * + * @param maxCurlSplits Maximum number curl can be divided into. The bigger the value + * the smoother curl will be. With the cost of having more + * polygons for drawing. + */ + public CurlMesh(int maxCurlSplits) { + // There really is no use for 0 splits. + mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits; + + mArrScanLines = new Array(maxCurlSplits + 2); + mArrOutputVertices = new Array(7); + mArrRotatedVertices = new Array(4); + mArrIntersections = new Array(2); + mArrTempVertices = new Array(7 + 4); + for (int i = 0; i < 7 + 4; ++i) { + mArrTempVertices.add(new Vertex()); + } + + if (DRAW_SHADOW) { + mArrSelfShadowVertices = new Array( + (mMaxCurlSplits + 2) * 2); + mArrDropShadowVertices = new Array( + (mMaxCurlSplits + 2) * 2); + mArrTempShadowVertices = new Array( + (mMaxCurlSplits + 2) * 2); + for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) { + mArrTempShadowVertices.add(new ShadowVertex()); + } + } + + // Rectangle consists of 4 vertices. Index 0 = top-left, index 1 = + // bottom-left, index 2 = top-right and index 3 = bottom-right. + for (int i = 0; i < 4; ++i) { + mRectangle[i] = new Vertex(); + } + // Set up shadow penumbra direction to each vertex. We do fake 'self + // shadow' calculations based on this information. + mRectangle[0].mPenumbraX = mRectangle[1].mPenumbraX = mRectangle[1].mPenumbraY = mRectangle[3].mPenumbraY = -1; + mRectangle[0].mPenumbraY = mRectangle[2].mPenumbraX = mRectangle[2].mPenumbraY = mRectangle[3].mPenumbraX = 1; + + if (DRAW_CURL_POSITION) { + mCurlPositionLinesCount = 3; + ByteBuffer hvbb = ByteBuffer + .allocateDirect(mCurlPositionLinesCount * 2 * 2 * 4); + hvbb.order(ByteOrder.nativeOrder()); + mBufCurlPositionLines = hvbb.asFloatBuffer(); + mBufCurlPositionLines.position(0); + } + + // There are 4 vertices from bounding rect, max 2 from adding split line + // to two corners and curl consists of max mMaxCurlSplits lines each + // outputting 2 vertices. + int maxVerticesCount = 4 + 2 + (2 * mMaxCurlSplits); + ByteBuffer vbb = ByteBuffer.allocateDirect(maxVerticesCount * 3 * 4); + vbb.order(ByteOrder.nativeOrder()); + mBufVertices = vbb.asFloatBuffer(); + mBufVertices.position(0); + + if (DRAW_TEXTURE) { + ByteBuffer tbb = ByteBuffer + .allocateDirect(maxVerticesCount * 2 * 4); + tbb.order(ByteOrder.nativeOrder()); + mBufTexCoords = tbb.asFloatBuffer(); + mBufTexCoords.position(0); + } + + ByteBuffer cbb = ByteBuffer.allocateDirect(maxVerticesCount * 4 * 4); + cbb.order(ByteOrder.nativeOrder()); + mBufColors = cbb.asFloatBuffer(); + mBufColors.position(0); + + if (DRAW_SHADOW) { + int maxShadowVerticesCount = (mMaxCurlSplits + 2) * 2 * 2; + ByteBuffer scbb = ByteBuffer + .allocateDirect(maxShadowVerticesCount * 4 * 4); + scbb.order(ByteOrder.nativeOrder()); + mBufShadowColors = scbb.asFloatBuffer(); + mBufShadowColors.position(0); + + ByteBuffer sibb = ByteBuffer + .allocateDirect(maxShadowVerticesCount * 3 * 4); + sibb.order(ByteOrder.nativeOrder()); + mBufShadowVertices = sibb.asFloatBuffer(); + mBufShadowVertices.position(0); + + mDropShadowCount = mSelfShadowCount = 0; + } + } + + /** + * Adds vertex to buffers. + */ + private void addVertex(Vertex vertex) { + mBufVertices.put((float) vertex.mPosX); + mBufVertices.put((float) vertex.mPosY); + mBufVertices.put((float) vertex.mPosZ); + mBufColors.put(vertex.mColorFactor * Color.red(vertex.mColor) / 255f); + mBufColors.put(vertex.mColorFactor * Color.green(vertex.mColor) / 255f); + mBufColors.put(vertex.mColorFactor * Color.blue(vertex.mColor) / 255f); + mBufColors.put(Color.alpha(vertex.mColor) / 255f); + if (DRAW_TEXTURE) { + mBufTexCoords.put((float) vertex.mTexX); + mBufTexCoords.put((float) vertex.mTexY); + } + } + + /** + * Sets curl for this mesh. + * + * @param curlPos Position for curl 'center'. Can be any point on line collinear + * to curl. + * @param curlDir Curl direction, should be normalized. + * @param radius Radius of curl. + */ + public synchronized void curl(PointF curlPos, PointF curlDir, double radius) { + + // First add some 'helper' lines used for development. + if (DRAW_CURL_POSITION) { + mBufCurlPositionLines.position(0); + + mBufCurlPositionLines.put(curlPos.x); + mBufCurlPositionLines.put(curlPos.y - 1.0f); + mBufCurlPositionLines.put(curlPos.x); + mBufCurlPositionLines.put(curlPos.y + 1.0f); + mBufCurlPositionLines.put(curlPos.x - 1.0f); + mBufCurlPositionLines.put(curlPos.y); + mBufCurlPositionLines.put(curlPos.x + 1.0f); + mBufCurlPositionLines.put(curlPos.y); + + mBufCurlPositionLines.put(curlPos.x); + mBufCurlPositionLines.put(curlPos.y); + mBufCurlPositionLines.put(curlPos.x + curlDir.x * 2); + mBufCurlPositionLines.put(curlPos.y + curlDir.y * 2); + + mBufCurlPositionLines.position(0); + } + + // Actual 'curl' implementation starts here. + mBufVertices.position(0); + mBufColors.position(0); + if (DRAW_TEXTURE) { + mBufTexCoords.position(0); + } + + // Calculate curl angle from direction. + double curlAngle = Math.acos(curlDir.x); + curlAngle = curlDir.y > 0 ? -curlAngle : curlAngle; + + // Initiate rotated rectangle which's is translated to curlPos and + // rotated so that curl direction heads to right (1,0). Vertices are + // ordered in ascending order based on x -coordinate at the same time. + // And using y -coordinate in very rare case in which two vertices have + // same x -coordinate. + mArrTempVertices.addAll(mArrRotatedVertices); + mArrRotatedVertices.clear(); + for (int i = 0; i < 4; ++i) { + Vertex v = mArrTempVertices.remove(0); + v.set(mRectangle[i]); + v.translate(-curlPos.x, -curlPos.y); + v.rotateZ(-curlAngle); + int j = 0; + for (; j < mArrRotatedVertices.size(); ++j) { + Vertex v2 = mArrRotatedVertices.get(j); + if (v.mPosX > v2.mPosX) { + break; + } + if (v.mPosX == v2.mPosX && v.mPosY > v2.mPosY) { + break; + } + } + mArrRotatedVertices.add(j, v); + } + + // Rotated rectangle lines/vertex indices. We need to find bounding + // lines for rotated rectangle. After sorting vertices according to + // their x -coordinate we don't have to worry about vertices at indices + // 0 and 1. But due to inaccuracy it's possible vertex 3 is not the + // opposing corner from vertex 0. So we are calculating distance from + // vertex 0 to vertices 2 and 3 - and altering line indices if needed. + // Also vertices/lines are given in an order first one has x -coordinate + // at least the latter one. This property is used in getIntersections to + // see if there is an intersection. + int lines[][] = {{0, 1}, {0, 2}, {1, 3}, {2, 3}}; + { + // TODO: There really has to be more 'easier' way of doing this - + // not including extensive use of sqrt. + Vertex v0 = mArrRotatedVertices.get(0); + Vertex v2 = mArrRotatedVertices.get(2); + Vertex v3 = mArrRotatedVertices.get(3); + double dist2 = Math.sqrt((v0.mPosX - v2.mPosX) + * (v0.mPosX - v2.mPosX) + (v0.mPosY - v2.mPosY) + * (v0.mPosY - v2.mPosY)); + double dist3 = Math.sqrt((v0.mPosX - v3.mPosX) + * (v0.mPosX - v3.mPosX) + (v0.mPosY - v3.mPosY) + * (v0.mPosY - v3.mPosY)); + if (dist2 > dist3) { + lines[1][1] = 3; + lines[2][1] = 2; + } + } + + mVerticesCountFront = mVerticesCountBack = 0; + + if (DRAW_SHADOW) { + mArrTempShadowVertices.addAll(mArrDropShadowVertices); + mArrTempShadowVertices.addAll(mArrSelfShadowVertices); + mArrDropShadowVertices.clear(); + mArrSelfShadowVertices.clear(); + } + + // Length of 'curl' curve. + double curlLength = Math.PI * radius; + // Calculate scan lines. + // TODO: Revisit this code one day. There is room for optimization here. + mArrScanLines.clear(); + if (mMaxCurlSplits > 0) { + mArrScanLines.add((double) 0); + } + for (int i = 1; i < mMaxCurlSplits; ++i) { + mArrScanLines.add((-curlLength * i) / (mMaxCurlSplits - 1)); + } + // As mRotatedVertices is ordered regarding x -coordinate, adding + // this scan line produces scan area picking up vertices which are + // rotated completely. One could say 'until infinity'. + mArrScanLines.add(mArrRotatedVertices.get(3).mPosX - 1); + + // Start from right most vertex. Pretty much the same as first scan area + // is starting from 'infinity'. + double scanXmax = mArrRotatedVertices.get(0).mPosX + 1; + + for (int i = 0; i < mArrScanLines.size(); ++i) { + // Once we have scanXmin and scanXmax we have a scan area to start + // working with. + double scanXmin = mArrScanLines.get(i); + // First iterate 'original' rectangle vertices within scan area. + for (int j = 0; j < mArrRotatedVertices.size(); ++j) { + Vertex v = mArrRotatedVertices.get(j); + // Test if vertex lies within this scan area. + // TODO: Frankly speaking, can't remember why equality check was + // added to both ends. Guessing it was somehow related to case + // where radius=0f, which, given current implementation, could + // be handled much more effectively anyway. + if (v.mPosX >= scanXmin && v.mPosX <= scanXmax) { + // Pop out a vertex from temp vertices. + Vertex n = mArrTempVertices.remove(0); + n.set(v); + // This is done solely for triangulation reasons. Given a + // rotated rectangle it has max 2 vertices having + // intersection. + Array intersections = getIntersections( + mArrRotatedVertices, lines, n.mPosX); + // In a sense one could say we're adding vertices always in + // two, positioned at the ends of intersecting line. And for + // triangulation to work properly they are added based on y + // -coordinate. And this if-else is doing it for us. + if (intersections.size() == 1 + && intersections.get(0).mPosY > v.mPosY) { + // In case intersecting vertex is higher add it first. + mArrOutputVertices.addAll(intersections); + mArrOutputVertices.add(n); + } else if (intersections.size() <= 1) { + // Otherwise add original vertex first. + mArrOutputVertices.add(n); + mArrOutputVertices.addAll(intersections); + } else { + // There should never be more than 1 intersecting + // vertex. But if it happens as a fallback simply skip + // everything. + mArrTempVertices.add(n); + mArrTempVertices.addAll(intersections); + } + } + } + + // Search for scan line intersections. + Array intersections = getIntersections(mArrRotatedVertices, + lines, scanXmin); + + // We expect to get 0 or 2 vertices. In rare cases there's only one + // but in general given a scan line intersecting rectangle there + // should be 2 intersecting vertices. + if (intersections.size() == 2) { + // There were two intersections, add them based on y + // -coordinate, higher first, lower last. + Vertex v1 = intersections.get(0); + Vertex v2 = intersections.get(1); + if (v1.mPosY < v2.mPosY) { + mArrOutputVertices.add(v2); + mArrOutputVertices.add(v1); + } else { + mArrOutputVertices.addAll(intersections); + } + } else if (intersections.size() != 0) { + // This happens in a case in which there is a original vertex + // exactly at scan line or something went very much wrong if + // there are 3+ vertices. What ever the reason just return the + // vertices to temp vertices for later use. In former case it + // was handled already earlier once iterating through + // mRotatedVertices, in latter case it's better to avoid doing + // anything with them. + mArrTempVertices.addAll(intersections); + } + + // Add vertices found during this iteration to vertex etc buffers. + while (mArrOutputVertices.size() > 0) { + Vertex v = mArrOutputVertices.remove(0); + mArrTempVertices.add(v); + + // Local texture front-facing flag. + boolean textureFront; + + // Untouched vertices. + if (i == 0) { + textureFront = true; + mVerticesCountFront++; + } + // 'Completely' rotated vertices. + else if (i == mArrScanLines.size() - 1 || curlLength == 0) { + v.mPosX = -(curlLength + v.mPosX); + v.mPosZ = 2 * radius; + v.mPenumbraX = -v.mPenumbraX; + + textureFront = false; + mVerticesCountBack++; + } + // Vertex lies within 'curl'. + else { + // Even though it's not obvious from the if-else clause, + // here v.mPosX is between [-curlLength, 0]. And we can do + // calculations around a half cylinder. + double rotY = Math.PI * (v.mPosX / curlLength); + v.mPosX = radius * Math.sin(rotY); + v.mPosZ = radius - (radius * Math.cos(rotY)); + v.mPenumbraX *= Math.cos(rotY); + // Map color multiplier to [.1f, 1f] range. + v.mColorFactor = (float) (.1f + .9f * Math.sqrt(Math + .sin(rotY) + 1)); + + if (v.mPosZ >= radius) { + textureFront = false; + mVerticesCountBack++; + } else { + textureFront = true; + mVerticesCountFront++; + } + } + + // We use local textureFront for flipping backside texture + // locally. Plus additionally if mesh is in flip texture mode, + // we'll make the procedure "backwards". Also, until this point, + // texture coordinates are within [0, 1] range so we'll adjust + // them to final texture coordinates too. + if (textureFront != mFlipTexture) { + v.mTexX *= mTextureRectFront.right; + v.mTexY *= mTextureRectFront.bottom; + v.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT); + } else { + v.mTexX *= mTextureRectBack.right; + v.mTexY *= mTextureRectBack.bottom; + v.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK); + } + + // Move vertex back to 'world' coordinates. + v.rotateZ(curlAngle); + v.translate(curlPos.x, curlPos.y); + addVertex(v); + + // Drop shadow is cast 'behind' the curl. + if (DRAW_SHADOW && v.mPosZ > 0 && v.mPosZ <= radius) { + ShadowVertex sv = mArrTempShadowVertices.remove(0); + sv.mPosX = v.mPosX; + sv.mPosY = v.mPosY; + sv.mPosZ = v.mPosZ; + sv.mPenumbraX = (v.mPosZ / 2) * -curlDir.x; + sv.mPenumbraY = (v.mPosZ / 2) * -curlDir.y; + sv.mPenumbraColor = v.mPosZ / radius; + int idx = (mArrDropShadowVertices.size() + 1) / 2; + mArrDropShadowVertices.add(idx, sv); + } + // Self shadow is cast partly over mesh. + if (DRAW_SHADOW && v.mPosZ > radius) { + ShadowVertex sv = mArrTempShadowVertices.remove(0); + sv.mPosX = v.mPosX; + sv.mPosY = v.mPosY; + sv.mPosZ = v.mPosZ; + sv.mPenumbraX = ((v.mPosZ - radius) / 3) * v.mPenumbraX; + sv.mPenumbraY = ((v.mPosZ - radius) / 3) * v.mPenumbraY; + sv.mPenumbraColor = (v.mPosZ - radius) / (2 * radius); + int idx = (mArrSelfShadowVertices.size() + 1) / 2; + mArrSelfShadowVertices.add(idx, sv); + } + } + + // Switch scanXmin as scanXmax for next iteration. + scanXmax = scanXmin; + } + + mBufVertices.position(0); + mBufColors.position(0); + if (DRAW_TEXTURE) { + mBufTexCoords.position(0); + } + + // Add shadow Vertices. + if (DRAW_SHADOW) { + mBufShadowColors.position(0); + mBufShadowVertices.position(0); + mDropShadowCount = 0; + + for (int i = 0; i < mArrDropShadowVertices.size(); ++i) { + ShadowVertex sv = mArrDropShadowVertices.get(i); + mBufShadowVertices.put((float) sv.mPosX); + mBufShadowVertices.put((float) sv.mPosY); + mBufShadowVertices.put((float) sv.mPosZ); + mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX)); + mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY)); + mBufShadowVertices.put((float) sv.mPosZ); + for (int j = 0; j < 4; ++j) { + double color = SHADOW_OUTER_COLOR[j] + + (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j]) + * sv.mPenumbraColor; + mBufShadowColors.put((float) color); + } + mBufShadowColors.put(SHADOW_OUTER_COLOR); + mDropShadowCount += 2; + } + mSelfShadowCount = 0; + for (int i = 0; i < mArrSelfShadowVertices.size(); ++i) { + ShadowVertex sv = mArrSelfShadowVertices.get(i); + mBufShadowVertices.put((float) sv.mPosX); + mBufShadowVertices.put((float) sv.mPosY); + mBufShadowVertices.put((float) sv.mPosZ); + mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX)); + mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY)); + mBufShadowVertices.put((float) sv.mPosZ); + for (int j = 0; j < 4; ++j) { + double color = SHADOW_OUTER_COLOR[j] + + (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j]) + * sv.mPenumbraColor; + mBufShadowColors.put((float) color); + } + mBufShadowColors.put(SHADOW_OUTER_COLOR); + mSelfShadowCount += 2; + } + mBufShadowColors.position(0); + mBufShadowVertices.position(0); + } + } + + /** + * Calculates intersections for given scan line. + */ + private Array getIntersections(Array vertices, + int[][] lineIndices, double scanX) { + mArrIntersections.clear(); + // Iterate through rectangle lines each re-presented as a pair of + // vertices. + for (int j = 0; j < lineIndices.length; j++) { + Vertex v1 = vertices.get(lineIndices[j][0]); + Vertex v2 = vertices.get(lineIndices[j][1]); + // Here we expect that v1.mPosX >= v2.mPosX and wont do intersection + // test the opposite way. + if (v1.mPosX > scanX && v2.mPosX < scanX) { + // There is an intersection, calculate coefficient telling 'how + // far' scanX is from v2. + double c = (scanX - v2.mPosX) / (v1.mPosX - v2.mPosX); + Vertex n = mArrTempVertices.remove(0); + n.set(v2); + n.mPosX = scanX; + n.mPosY += (v1.mPosY - v2.mPosY) * c; + if (DRAW_TEXTURE) { + n.mTexX += (v1.mTexX - v2.mTexX) * c; + n.mTexY += (v1.mTexY - v2.mTexY) * c; + } + if (DRAW_SHADOW) { + n.mPenumbraX += (v1.mPenumbraX - v2.mPenumbraX) * c; + n.mPenumbraY += (v1.mPenumbraY - v2.mPenumbraY) * c; + } + mArrIntersections.add(n); + } + } + return mArrIntersections; + } + + /** + * Getter for textures page for this mesh. + */ + public synchronized CurlPage getTexturePage() { + return mTexturePage; + } + + /** + * Renders our page curl mesh. + */ + public synchronized void onDrawFrame(GL10 gl) { + // First allocate texture if there is not one yet. + if (DRAW_TEXTURE && mTextureIds == null) { + // Generate texture. + mTextureIds = new int[2]; + gl.glGenTextures(2, mTextureIds, 0); + for (int textureId : mTextureIds) { + // Set texture attributes. + gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, + GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, + GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, + GL10.GL_CLAMP_TO_EDGE); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, + GL10.GL_CLAMP_TO_EDGE); + } + } + + if (DRAW_TEXTURE && mTexturePage.getTexturesChanged()) { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]); + Bitmap texture = mTexturePage.getTexture(mTextureRectFront, + CurlPage.SIDE_FRONT); + GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0); + texture.recycle(); + + mTextureBack = mTexturePage.hasBackTexture(); + if (mTextureBack) { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]); + texture = mTexturePage.getTexture(mTextureRectBack, + CurlPage.SIDE_BACK); + GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0); + texture.recycle(); + } else { + mTextureRectBack.set(mTextureRectFront); + } + + mTexturePage.recycle(); + reset(); + } + + // Some 'global' settings. + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + + // TODO: Drop shadow drawing is done temporarily here to hide some + // problems with its calculation. + if (DRAW_SHADOW) { + gl.glDisable(GL10.GL_TEXTURE_2D); + gl.glEnable(GL10.GL_BLEND); + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glEnableClientState(GL10.GL_COLOR_ARRAY); + gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors); + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mDropShadowCount); + gl.glDisableClientState(GL10.GL_COLOR_ARRAY); + gl.glDisable(GL10.GL_BLEND); + } + + if (DRAW_TEXTURE) { + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mBufTexCoords); + } + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices); + // Enable color array. + gl.glEnableClientState(GL10.GL_COLOR_ARRAY); + gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufColors); + + // Draw front facing blank vertices. + gl.glDisable(GL10.GL_TEXTURE_2D); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront); + + // Draw front facing texture. + if (DRAW_TEXTURE) { + gl.glEnable(GL10.GL_BLEND); + gl.glEnable(GL10.GL_TEXTURE_2D); + + if (!mFlipTexture || !mTextureBack) { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]); + } else { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]); + } + + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront); + + gl.glDisable(GL10.GL_BLEND); + gl.glDisable(GL10.GL_TEXTURE_2D); + } + + int backStartIdx = Math.max(0, mVerticesCountFront - 2); + int backCount = mVerticesCountFront + mVerticesCountBack - backStartIdx; + + // Draw back facing blank vertices. + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount); + + // Draw back facing texture. + if (DRAW_TEXTURE) { + gl.glEnable(GL10.GL_BLEND); + gl.glEnable(GL10.GL_TEXTURE_2D); + + if (mFlipTexture || !mTextureBack) { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]); + } else { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]); + } + + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount); + + gl.glDisable(GL10.GL_BLEND); + gl.glDisable(GL10.GL_TEXTURE_2D); + } + + // Disable textures and color array. + gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glDisableClientState(GL10.GL_COLOR_ARRAY); + + if (DRAW_POLYGON_OUTLINES) { + gl.glEnable(GL10.GL_BLEND); + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glLineWidth(1.0f); + gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f); + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices); + gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, mVerticesCountFront); + gl.glDisable(GL10.GL_BLEND); + } + + if (DRAW_CURL_POSITION) { + gl.glEnable(GL10.GL_BLEND); + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glLineWidth(1.0f); + gl.glColor4f(1.0f, 0.5f, 0.5f, 1.0f); + gl.glVertexPointer(2, GL10.GL_FLOAT, 0, mBufCurlPositionLines); + gl.glDrawArrays(GL10.GL_LINES, 0, mCurlPositionLinesCount * 2); + gl.glDisable(GL10.GL_BLEND); + } + + if (DRAW_SHADOW) { + gl.glEnable(GL10.GL_BLEND); + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glEnableClientState(GL10.GL_COLOR_ARRAY); + gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors); + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, mDropShadowCount, + mSelfShadowCount); + gl.glDisableClientState(GL10.GL_COLOR_ARRAY); + gl.glDisable(GL10.GL_BLEND); + } + + gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); + } + + /** + * Resets mesh to 'initial' state. Meaning this mesh will draw a plain + * textured rectangle after call to this method. + */ + public synchronized void reset() { + mBufVertices.position(0); + mBufColors.position(0); + if (DRAW_TEXTURE) { + mBufTexCoords.position(0); + } + for (int i = 0; i < 4; ++i) { + Vertex tmp = mArrTempVertices.get(0); + tmp.set(mRectangle[i]); + + if (mFlipTexture) { + tmp.mTexX *= mTextureRectBack.right; + tmp.mTexY *= mTextureRectBack.bottom; + tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK); + } else { + tmp.mTexX *= mTextureRectFront.right; + tmp.mTexY *= mTextureRectFront.bottom; + tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT); + } + + addVertex(tmp); + } + mVerticesCountFront = 4; + mVerticesCountBack = 0; + mBufVertices.position(0); + mBufColors.position(0); + if (DRAW_TEXTURE) { + mBufTexCoords.position(0); + } + + mDropShadowCount = mSelfShadowCount = 0; + } + + /** + * Resets allocated texture id forcing creation of new one. After calling + * this method you most likely want to set bitmap too as it's lost. This + * method should be called only once e.g GL context is re-created as this + * method does not release previous texture id, only makes sure new one is + * requested on next render. + */ + public synchronized void resetTexture() { + mTextureIds = null; + } + + /** + * If true, flips texture sideways. + */ + public synchronized void setFlipTexture(boolean flipTexture) { + mFlipTexture = flipTexture; + if (flipTexture) { + setTexCoords(1f, 0f, 0f, 1f); + } else { + setTexCoords(0f, 0f, 1f, 1f); + } + } + + /** + * Update mesh bounds. + */ + public void setRect(RectF r) { + mRectangle[0].mPosX = r.left; + mRectangle[0].mPosY = r.top; + mRectangle[1].mPosX = r.left; + mRectangle[1].mPosY = r.bottom; + mRectangle[2].mPosX = r.right; + mRectangle[2].mPosY = r.top; + mRectangle[3].mPosX = r.right; + mRectangle[3].mPosY = r.bottom; + } + + /** + * Sets texture coordinates to mRectangle vertices. + */ + private synchronized void setTexCoords(float left, float top, float right, + float bottom) { + mRectangle[0].mTexX = left; + mRectangle[0].mTexY = top; + mRectangle[1].mTexX = left; + mRectangle[1].mTexY = bottom; + mRectangle[2].mTexX = right; + mRectangle[2].mTexY = top; + mRectangle[3].mTexX = right; + mRectangle[3].mTexY = bottom; + } + + /** + * Simple fixed size array implementation. + */ + private class Array { + private Object[] mArray; + private int mCapacity; + private int mSize; + + public Array(int capacity) { + mCapacity = capacity; + mArray = new Object[capacity]; + } + + public void add(int index, T item) { + if (index < 0 || index > mSize || mSize >= mCapacity) { + throw new IndexOutOfBoundsException(); + } + for (int i = mSize; i > index; --i) { + mArray[i] = mArray[i - 1]; + } + mArray[index] = item; + ++mSize; + } + + public void add(T item) { + if (mSize >= mCapacity) { + throw new IndexOutOfBoundsException(); + } + mArray[mSize++] = item; + } + + public void addAll(Array array) { + if (mSize + array.size() > mCapacity) { + throw new IndexOutOfBoundsException(); + } + for (int i = 0; i < array.size(); ++i) { + mArray[mSize++] = array.get(i); + } + } + + public void clear() { + mSize = 0; + } + + @SuppressWarnings("unchecked") + public T get(int index) { + if (index < 0 || index >= mSize) { + throw new IndexOutOfBoundsException(); + } + return (T) mArray[index]; + } + + @SuppressWarnings("unchecked") + public T remove(int index) { + if (index < 0 || index >= mSize) { + throw new IndexOutOfBoundsException(); + } + T item = (T) mArray[index]; + for (int i = index; i < mSize - 1; ++i) { + mArray[i] = mArray[i + 1]; + } + --mSize; + return item; + } + + public int size() { + return mSize; + } + + } + + /** + * Holder for shadow vertex information. + */ + private class ShadowVertex { + public double mPenumbraColor; + public double mPenumbraX; + public double mPenumbraY; + public double mPosX; + public double mPosY; + public double mPosZ; + } + + /** + * Holder for vertex information. + */ + private class Vertex { + public int mColor; + public float mColorFactor; + public double mPenumbraX; + public double mPenumbraY; + public double mPosX; + public double mPosY; + public double mPosZ; + public double mTexX; + public double mTexY; + + public Vertex() { + mPosX = mPosY = mPosZ = mTexX = mTexY = 0; + mColorFactor = 1.0f; + } + + public void rotateZ(double theta) { + double cos = Math.cos(theta); + double sin = Math.sin(theta); + double x = mPosX * cos + mPosY * sin; + double y = mPosX * -sin + mPosY * cos; + mPosX = x; + mPosY = y; + double px = mPenumbraX * cos + mPenumbraY * sin; + double py = mPenumbraX * -sin + mPenumbraY * cos; + mPenumbraX = px; + mPenumbraY = py; + } + + public void set(Vertex vertex) { + mPosX = vertex.mPosX; + mPosY = vertex.mPosY; + mPosZ = vertex.mPosZ; + mTexX = vertex.mTexX; + mTexY = vertex.mTexY; + mPenumbraX = vertex.mPenumbraX; + mPenumbraY = vertex.mPenumbraY; + mColor = vertex.mColor; + mColorFactor = vertex.mColorFactor; + } + + public void translate(double dx, double dy) { + mPosX += dx; + mPosY += dy; + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlPage.java b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlPage.java new file mode 100644 index 000000000..0caa17010 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlPage.java @@ -0,0 +1,195 @@ +package io.legado.app.ui.book.read.page.delegate.curl; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.RectF; + +/** + * Storage class for page textures, blend colors and possibly some other values + * in the future. + * + * @author harism + */ +public class CurlPage { + + public static final int SIDE_BACK = 2; + public static final int SIDE_BOTH = 3; + public static final int SIDE_FRONT = 1; + + private int mColorBack; + private int mColorFront; + private Bitmap mTextureBack; + private Bitmap mTextureFront; + private boolean mTexturesChanged; + + /** + * Default constructor. + */ + public CurlPage() { + reset(); + } + + /** + * Getter for color. + */ + public int getColor(int side) { + switch (side) { + case SIDE_FRONT: + return mColorFront; + default: + return mColorBack; + } + } + + /** + * Calculates the next highest power of two for a given integer. + */ + private int getNextHighestPO2(int n) { + n -= 1; + n = n | (n >> 1); + n = n | (n >> 2); + n = n | (n >> 4); + n = n | (n >> 8); + n = n | (n >> 16); + n = n | (n >> 32); + return n + 1; + } + + /** + * Generates nearest power of two sized Bitmap for give Bitmap. Returns this + * new Bitmap using default return statement + original texture coordinates + * are stored into RectF. + */ + private Bitmap getTexture(Bitmap bitmap, RectF textureRect) { + // Bitmap original size. + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + // Bitmap size expanded to next power of two. This is done due to + // the requirement on many devices, texture width and height should + // be power of two. + int newW = getNextHighestPO2(w); + int newH = getNextHighestPO2(h); + + // TODO: Is there another way to create a bigger Bitmap and copy + // original Bitmap to it more efficiently? Immutable bitmap anyone? + Bitmap bitmapTex = Bitmap.createBitmap(newW, newH, bitmap.getConfig()); + Canvas c = new Canvas(bitmapTex); + c.drawBitmap(bitmap, 0, 0, null); + + // Calculate final texture coordinates. + float texX = (float) w / newW; + float texY = (float) h / newH; + textureRect.set(0f, 0f, texX, texY); + + return bitmapTex; + } + + /** + * Getter for textures. Creates Bitmap sized to nearest power of two, copies + * original Bitmap into it and returns it. RectF given as parameter is + * filled with actual texture coordinates in this new upscaled texture + * Bitmap. + */ + public Bitmap getTexture(RectF textureRect, int side) { + switch (side) { + case SIDE_FRONT: + return getTexture(mTextureFront, textureRect); + default: + return getTexture(mTextureBack, textureRect); + } + } + + /** + * Returns true if textures have changed. + */ + public boolean getTexturesChanged() { + return mTexturesChanged; + } + + /** + * Returns true if back siding texture exists and it differs from front + * facing one. + */ + public boolean hasBackTexture() { + return !mTextureFront.equals(mTextureBack); + } + + /** + * Recycles and frees underlying Bitmaps. + */ + public void recycle() { + if (mTextureFront != null) { + mTextureFront.recycle(); + } + mTextureFront = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); + mTextureFront.eraseColor(mColorFront); + if (mTextureBack != null) { + mTextureBack.recycle(); + } + mTextureBack = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); + mTextureBack.eraseColor(mColorBack); + mTexturesChanged = false; + } + + /** + * Resets this CurlPage into its initial state. + */ + public void reset() { + mColorBack = Color.WHITE; + mColorFront = Color.WHITE; + recycle(); + } + + /** + * Setter blend color. + */ + public void setColor(int color, int side) { + switch (side) { + case SIDE_FRONT: + mColorFront = color; + break; + case SIDE_BACK: + mColorBack = color; + break; + default: + mColorFront = mColorBack = color; + break; + } + } + + /** + * Setter for textures. + */ + public void setTexture(Bitmap texture, int side) { + if (texture == null) { + texture = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); + if (side == SIDE_BACK) { + texture.eraseColor(mColorBack); + } else { + texture.eraseColor(mColorFront); + } + } + switch (side) { + case SIDE_FRONT: + if (mTextureFront != null) + mTextureFront.recycle(); + mTextureFront = texture; + break; + case SIDE_BACK: + if (mTextureBack != null) + mTextureBack.recycle(); + mTextureBack = texture; + break; + case SIDE_BOTH: + if (mTextureFront != null) + mTextureFront.recycle(); + if (mTextureBack != null) + mTextureBack.recycle(); + mTextureFront = mTextureBack = texture; + break; + } + mTexturesChanged = true; + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt index c124d48a6..88bea027c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt @@ -3,7 +3,7 @@ package io.legado.app.ui.book.read.page.provider import android.graphics.Bitmap import io.legado.app.data.entities.Book import io.legado.app.help.BookHelp -import io.legado.app.model.localBook.EPUBFile +import io.legado.app.model.localBook.EpubFile import io.legado.app.utils.BitmapUtils import io.legado.app.utils.FileUtils import kotlinx.coroutines.runBlocking @@ -36,7 +36,7 @@ object ImageProvider { val vFile = BookHelp.getImage(book, src) if (!vFile.exists()) { if (book.isEpub()) { - EPUBFile.getImage(book, src)?.use { input -> + EpubFile.getImage(book, src)?.use { input -> val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath) FileOutputStream(newFile).use { output -> input.copyTo(output) diff --git a/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt b/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt index 17c067fd3..d538cc7a8 100644 --- a/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt +++ b/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt @@ -3,14 +3,14 @@ package io.legado.app.ui.config import android.app.Activity.RESULT_OK import android.content.Intent import android.net.Uri -import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import io.legado.app.R -import io.legado.app.constant.Permissions import io.legado.app.constant.PreferKey import io.legado.app.help.AppConfig import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.help.storage.Backup import io.legado.app.help.storage.BookWebDav import io.legado.app.help.storage.ImportOldData @@ -56,14 +56,18 @@ object BackupRestoreUi { fragment: Fragment, path: String ) { - fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - Coroutine.async { - AppConfig.backupPath = path - Backup.backup(fragment.requireContext(), path) - }.onSuccess { - fragment.toastOnUi(R.string.backup_success) + PermissionsCompat.Builder(fragment) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + Coroutine.async { + AppConfig.backupPath = path + Backup.backup(fragment.requireContext(), path) + }.onSuccess { + fragment.toastOnUi(R.string.backup_success) + } } - }.launch(Permissions.Group.STORAGE) + .request() } fun selectBackupFolder(fragment: Fragment, requestCode: Int = selectFolderRequestCode) { @@ -99,13 +103,17 @@ object BackupRestoreUi { } private fun restoreUsePermission(fragment: Fragment, path: String) { - fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - Coroutine.async { - AppConfig.backupPath = path - Restore.restoreDatabase(path) - Restore.restoreConfig(path) + PermissionsCompat.Builder(fragment) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + Coroutine.async { + AppConfig.backupPath = path + Restore.restoreDatabase(path) + Restore.restoreConfig(path) + } } - }.launch(Permissions.Group.STORAGE) + .request() } fun importOldData(fragment: Fragment) { diff --git a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt index 5d94d55e9..b44909711 100644 --- a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt @@ -10,18 +10,18 @@ import android.net.Uri import android.os.Bundle import android.os.Process import android.view.View -import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile import androidx.preference.ListPreference import androidx.preference.Preference import io.legado.app.R import io.legado.app.base.BasePreferenceFragment import io.legado.app.constant.EventBus -import io.legado.app.constant.Permissions import io.legado.app.constant.PreferKey import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH @@ -224,18 +224,25 @@ class OtherConfigFragment : BasePreferenceFragment(), } ?: toastOnUi("获取文件出错") } } else { - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - RealPathUtil.getPath(requireContext(), uri)?.let { path -> - val imgFile = File(path) - if (imgFile.exists()) { - var file = requireContext().externalFilesDir - file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name) - file.writeBytes(imgFile.readBytes()) - putPrefString(PreferKey.defaultCover, file.absolutePath) - CoverImageView.upDefaultCover() + PermissionsCompat.Builder(this) + .addPermissions( + Permissions.READ_EXTERNAL_STORAGE, + Permissions.WRITE_EXTERNAL_STORAGE + ) + .rationale(R.string.bg_image_per) + .onGranted { + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + val imgFile = File(path) + if (imgFile.exists()) { + var file = requireContext().externalFilesDir + file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name) + file.writeBytes(imgFile.readBytes()) + putPrefString(PreferKey.defaultCover, file.absolutePath) + CoverImageView.upDefaultCover() + } } } - }.launch(Permissions.Group.STORAGE) + .request() } } diff --git a/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt index 4f30f23a0..834e53eb9 100644 --- a/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt @@ -1,6 +1,8 @@ package io.legado.app.ui.config import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent import android.content.SharedPreferences import android.net.Uri import android.os.Build @@ -9,19 +11,19 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile import androidx.preference.Preference import io.legado.app.R import io.legado.app.base.BasePreferenceFragment import io.legado.app.constant.AppConst import io.legado.app.constant.EventBus -import io.legado.app.constant.Permissions import io.legado.app.constant.PreferKey import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.help.AppConfig import io.legado.app.help.LauncherIconHelp import io.legado.app.help.ThemeConfig +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH @@ -36,17 +38,8 @@ import java.io.File class ThemeConfigFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { - private val setLightBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) { - setBgFromUri(it, PreferKey.bgImage) { - upTheme(false) - } - } - - private val setDarkBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) { - setBgFromUri(it, PreferKey.bgImageN) { - upTheme(false) - } - } + private val requestCodeBgImage = 234 + private val requestCodeBgImageN = 342 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_config_theme) @@ -193,26 +186,26 @@ class ThemeConfigFragment : BasePreferenceFragment(), "themeList" -> ThemeListDialog().show(childFragmentManager, "themeList") "saveDayTheme", "saveNightTheme" -> saveThemeAlert(key) PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) { - setLightBgImage.launch("image/*") + selectImage(requestCodeBgImage) } else { selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> if (i == 0) { removePref(PreferKey.bgImage) upTheme(false) } else { - setLightBgImage.launch("image/*") + selectImage(requestCodeBgImage) } } } PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) { - setDarkBgImage.launch("image/*") + selectImage(requestCodeBgImageN) } else { selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> if (i == 0) { removePref(PreferKey.bgImageN) upTheme(true) } else { - setDarkBgImage.launch("image/*") + selectImage(requestCodeBgImageN) } } } @@ -220,6 +213,13 @@ class ThemeConfigFragment : BasePreferenceFragment(), return super.onPreferenceTreeClick(preference) } + private fun selectImage(requestCode: Int) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "image/*" + startActivityForResult(intent, requestCode) + } + @SuppressLint("InflateParams") private fun saveThemeAlert(key: String) { alert(R.string.theme_name) { @@ -279,19 +279,46 @@ class ThemeConfigFragment : BasePreferenceFragment(), } ?: toastOnUi("获取文件出错") } } else { - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - RealPathUtil.getPath(requireContext(), uri)?.let { path -> - val imgFile = File(path) - if (imgFile.exists()) { - var file = requireContext().externalFilesDir - file = FileUtils.createFileIfNotExist(file, preferenceKey, imgFile.name) - file.writeBytes(imgFile.readBytes()) - putPrefString(preferenceKey, file.absolutePath) - upPreferenceSummary(preferenceKey, file.absolutePath) - success() + PermissionsCompat.Builder(this) + .addPermissions( + Permissions.READ_EXTERNAL_STORAGE, + Permissions.WRITE_EXTERNAL_STORAGE + ) + .rationale(R.string.bg_image_per) + .onGranted { + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + val imgFile = File(path) + if (imgFile.exists()) { + var file = requireContext().externalFilesDir + file = FileUtils.createFileIfNotExist(file, preferenceKey, imgFile.name) + file.writeBytes(imgFile.readBytes()) + putPrefString(preferenceKey, file.absolutePath) + upPreferenceSummary(preferenceKey, file.absolutePath) + success() + } } } - }.launch(Permissions.Group.STORAGE) + .request() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + requestCodeBgImage -> if (resultCode == Activity.RESULT_OK) { + data?.data?.let { uri -> + setBgFromUri(uri, PreferKey.bgImage) { + upTheme(false) + } + } + } + requestCodeBgImageN -> if (resultCode == Activity.RESULT_OK) { + data?.data?.let { uri -> + setBgFromUri(uri, PreferKey.bgImageN) { + upTheme(true) + } + } + } } } diff --git a/app/src/main/java/io/legado/app/ui/filepicker/FilePicker.kt b/app/src/main/java/io/legado/app/ui/filepicker/FilePicker.kt index 68151ce0f..ff099f9ff 100644 --- a/app/src/main/java/io/legado/app/ui/filepicker/FilePicker.kt +++ b/app/src/main/java/io/legado/app/ui/filepicker/FilePicker.kt @@ -2,11 +2,11 @@ package io.legado.app.ui.filepicker import android.content.Intent import android.os.Build -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import io.legado.app.R -import io.legado.app.constant.Permissions +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert @Suppress("unused") @@ -247,15 +247,23 @@ object FilePicker { } private fun checkPermissions(fragment: Fragment, success: (() -> Unit)? = null) { - fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - success?.invoke() - }.launch(Permissions.Group.STORAGE) + PermissionsCompat.Builder(fragment) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + success?.invoke() + } + .request() } private fun checkPermissions(activity: AppCompatActivity, success: (() -> Unit)? = null) { - activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - success?.invoke() - }.launch(Permissions.Group.STORAGE) + PermissionsCompat.Builder(activity) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + success?.invoke() + } + .request() } private fun typesOfExtensions(allowExtensions: Array): Array { diff --git a/app/src/main/java/io/legado/app/ui/filepicker/FilePickerDialog.kt b/app/src/main/java/io/legado/app/ui/filepicker/FilePickerDialog.kt index 4eaa8aaea..ba708b677 100644 --- a/app/src/main/java/io/legado/app/ui/filepicker/FilePickerDialog.kt +++ b/app/src/main/java/io/legado/app/ui/filepicker/FilePickerDialog.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager @@ -15,6 +16,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.databinding.DialogFileChooserBinding +import io.legado.app.help.permission.Permissions import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.filepicker.adapter.FileAdapter import io.legado.app.ui.filepicker.adapter.PathAdapter @@ -69,7 +71,19 @@ class FilePickerDialog : DialogFragment(), override var isShowHomeDir: Boolean = false override var isShowUpDir: Boolean = true override var isShowHideDir: Boolean = false - + private val queryPermission = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + var hasPermission = true + it.forEach { (t, u) -> + if (!u) { + hasPermission = false + toastOnUi(t) + } + } + if (hasPermission) { + refreshCurrentDirPath(initPath) + } + } private var requestCode: Int = 0 var title: String? = null private var initPath = FileUtils.getSdCardPath() @@ -118,7 +132,7 @@ class FilePickerDialog : DialogFragment(), } initMenu() initContentView() - refreshCurrentDirPath(initPath) + queryPermission.launch(Permissions.Group.STORAGE) } private fun initMenu() { diff --git a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt index 8f6b7bf2f..004d3204b 100644 --- a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt @@ -8,16 +8,16 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.Toolbar import androidx.documentfile.provider.DocumentFile import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment -import io.legado.app.constant.Permissions import io.legado.app.constant.PreferKey import io.legado.app.databinding.DialogFontSelectBinding import io.legado.app.help.AppConfig +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.filepicker.FilePicker @@ -162,9 +162,13 @@ class FontSelectDialog : BaseDialogFragment(), } private fun loadFontFilesByPermission(path: String) { - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - loadFontFiles(path) - }.launch(Permissions.Group.STORAGE) + PermissionsCompat.Builder(this@FontSelectDialog) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + loadFontFiles(path) + } + .request() } private fun loadFontFiles(path: String) { diff --git a/avd.bat b/avd.bat new file mode 100644 index 000000000..9c6c97af0 --- /dev/null +++ b/avd.bat @@ -0,0 +1 @@ +emulator -avd %1 -dns-server 8.8.8.8 -no-snapshot-load \ No newline at end of file diff --git a/avd11.bat b/avd11.bat deleted file mode 100644 index fef788ee9..000000000 --- a/avd11.bat +++ /dev/null @@ -1 +0,0 @@ -emulator -avd android11 -dns-server 8.8.8.8 -no-snapshot-load \ No newline at end of file diff --git a/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java index 90b4d9b20..54807e7c8 100644 --- a/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java +++ b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java @@ -1,9 +1,10 @@ package me.ag2s.epublib.browsersupport; -import me.ag2s.epublib.domain.Book; +import java.util.EventObject; + +import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.util.StringUtil; -import java.util.EventObject; /** * Used to tell NavigationEventListener just what kind of navigation action @@ -19,7 +20,7 @@ public class NavigationEvent extends EventObject { private Resource oldResource; private int oldSpinePos; private Navigator navigator; - private Book oldBook; + private EpubBook oldBook; private int oldSectionPos; private String oldFragmentId; @@ -59,7 +60,7 @@ public class NavigationEvent extends EventObject { this.oldFragmentId = oldFragmentId; } - public Book getOldBook() { + public EpubBook getOldBook() { return oldBook; } @@ -122,11 +123,11 @@ public class NavigationEvent extends EventObject { } - public void setOldBook(Book oldBook) { + public void setOldBook(EpubBook oldBook) { this.oldBook = oldBook; } - public Book getCurrentBook() { + public EpubBook getCurrentBook() { return getNavigator().getBook(); } diff --git a/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java index 12595e925..e362624a0 100644 --- a/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java +++ b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java @@ -1,15 +1,15 @@ package me.ag2s.epublib.browsersupport; -import me.ag2s.epublib.domain.Book; -import me.ag2s.epublib.domain.Resource; import java.util.ArrayList; import java.util.List; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Resource; + /** * A history of the user's locations with the epub. * * @author paul.siegmann - * */ public class NavigationHistory implements NavigationEventListener { @@ -58,7 +58,7 @@ public class NavigationHistory implements NavigationEventListener { return currentSize; } - public void initBook(Book book) { + public void initBook(EpubBook book) { if (book == null) { return; } diff --git a/epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java b/epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java index 6ef314d49..744588d0c 100644 --- a/epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java +++ b/epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java @@ -1,11 +1,12 @@ package me.ag2s.epublib.browsersupport; -import me.ag2s.epublib.domain.Book; -import me.ag2s.epublib.domain.Resource; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Resource; + /** * A helper class for epub browser applications. * @@ -18,7 +19,7 @@ import java.util.List; public class Navigator implements Serializable { private static final long serialVersionUID = 1076126986424925474L; - private Book book; + private EpubBook book; private int currentSpinePos; private Resource currentResource; private int currentPagePos; @@ -30,7 +31,7 @@ public class Navigator implements Serializable { this(null); } - public Navigator(Book book) { + public Navigator(EpubBook book) { this.book = book; this.currentSpinePos = 0; if (book != null) { @@ -158,7 +159,7 @@ public class Navigator implements Serializable { return gotoSpineSection(book.getSpine().size() - 1, source); } - public void gotoBook(Book book, Object source) { + public void gotoBook(EpubBook book, Object source) { NavigationEvent navigationEvent = new NavigationEvent(source, this); this.book = book; this.currentFragmentId = null; @@ -193,7 +194,7 @@ public class Navigator implements Serializable { this.currentResource = book.getSpine().getResource(currentIndex); } - public Book getBook() { + public EpubBook getBook() { return book; } diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Book.java b/epublib/src/main/java/me/ag2s/epublib/domain/EpubBook.java similarity index 99% rename from epublib/src/main/java/me/ag2s/epublib/domain/Book.java rename to epublib/src/main/java/me/ag2s/epublib/domain/EpubBook.java index 405190a46..271cc7b6f 100644 --- a/epublib/src/main/java/me/ag2s/epublib/domain/Book.java +++ b/epublib/src/main/java/me/ag2s/epublib/domain/EpubBook.java @@ -39,7 +39,7 @@ import java.util.Map; * @author paul * @author jake */ -public class Book implements Serializable { +public class EpubBook implements Serializable { private static final long serialVersionUID = 2068355170895770100L; diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java b/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java index 5a44dfdcf..219c566c9 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java @@ -1,6 +1,6 @@ package me.ag2s.epublib.epub; -import me.ag2s.epublib.domain.Book; +import me.ag2s.epublib.domain.EpubBook; /** * Post-processes a book. @@ -16,5 +16,5 @@ public interface BookProcessor { */ BookProcessor IDENTITY_BOOKPROCESSOR = book -> book; - Book processBook(Book book); + EpubBook processBook(EpubBook book); } diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java b/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java index 9174313f8..44dfdf092 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java @@ -2,15 +2,17 @@ package me.ag2s.epublib.epub; import android.util.Log; -import me.ag2s.epublib.domain.Book; -//import io.documentnode.minilog.Logger; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import me.ag2s.epublib.domain.EpubBook; + +//import io.documentnode.minilog.Logger; + /** * A book processor that combines several other bookprocessors - * + *

* Fixes coverpage/coverimage. * Cleans up the XHTML. * @@ -30,7 +32,7 @@ public class BookProcessorPipeline implements BookProcessor { } @Override - public Book processBook(Book book) { + public EpubBook processBook(EpubBook book) { if (bookProcessors == null) { return book; } @@ -38,7 +40,7 @@ public class BookProcessorPipeline implements BookProcessor { try { book = bookProcessor.processBook(book); } catch (Exception e) { - Log.e(TAG,e.getMessage(), e); + Log.e(TAG, e.getMessage(), e); } } return book; diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java b/epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java index 81dfdaa67..2e96ceecb 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java @@ -2,15 +2,9 @@ package me.ag2s.epublib.epub; import android.util.Log; -import me.ag2s.epublib.Constants; -import me.ag2s.epublib.domain.Book; -import me.ag2s.epublib.domain.MediaType; -import me.ag2s.epublib.domain.Resource; -import me.ag2s.epublib.domain.Resources; -import me.ag2s.epublib.domain.MediaTypes; -import me.ag2s.epublib.util.ResourceUtil; -import me.ag2s.epublib.util.StringUtil; -//import io.documentnode.minilog.Logger; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + import java.io.IOException; import java.io.InputStream; import java.util.Arrays; @@ -18,8 +12,16 @@ import java.util.List; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; -import org.w3c.dom.Document; -import org.w3c.dom.Element; +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.MediaType; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.domain.Resources; +import me.ag2s.epublib.util.ResourceUtil; +import me.ag2s.epublib.util.StringUtil; + +//import io.documentnode.minilog.Logger; /** * Reads an epub file. @@ -32,27 +34,27 @@ public class EpubReader { private static String TAG= EpubReader.class.getName(); private BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR; - public Book readEpub(InputStream in) throws IOException { + public EpubBook readEpub(InputStream in) throws IOException { return readEpub(in, Constants.CHARACTER_ENCODING); } - public Book readEpub(ZipInputStream in) throws IOException { + public EpubBook readEpub(ZipInputStream in) throws IOException { return readEpub(in, Constants.CHARACTER_ENCODING); } - public Book readEpub(ZipFile zipfile) throws IOException { + public EpubBook readEpub(ZipFile zipfile) throws IOException { return readEpub(zipfile, Constants.CHARACTER_ENCODING); } /** * Read epub from inputstream * - * @param in the inputstream from which to read the epub + * @param in the inputstream from which to read the epub * @param encoding the encoding to use for the html files within the epub * @return the Book as read from the inputstream * @throws IOException */ - public Book readEpub(InputStream in, String encoding) throws IOException { + public EpubBook readEpub(InputStream in, String encoding) throws IOException { return readEpub(new ZipInputStream(in), encoding); } @@ -60,54 +62,53 @@ public class EpubReader { /** * Reads this EPUB without loading any resources into memory. * - * @param zipFile the file to load + * @param zipFile the file to load * @param encoding the encoding for XHTML files - * * @return this Book without loading all resources into memory. * @throws IOException */ - public Book readEpubLazy(ZipFile zipFile, String encoding) - throws IOException { + public EpubBook readEpubLazy(ZipFile zipFile, String encoding) + throws IOException { return readEpubLazy(zipFile, encoding, - Arrays.asList(MediaTypes.mediaTypes)); + Arrays.asList(MediaTypes.mediaTypes)); } - public Book readEpub(ZipInputStream in, String encoding) throws IOException { + public EpubBook readEpub(ZipInputStream in, String encoding) throws IOException { return readEpub(ResourcesLoader.loadResources(in, encoding)); } - public Book readEpub(ZipFile in, String encoding) throws IOException { + public EpubBook readEpub(ZipFile in, String encoding) throws IOException { return readEpub(ResourcesLoader.loadResources(in, encoding)); } /** * Reads this EPUB without loading all resources into memory. * - * @param zipFile the file to load - * @param encoding the encoding for XHTML files + * @param zipFile the file to load + * @param encoding the encoding for XHTML files * @param lazyLoadedTypes a list of the MediaType to load lazily * @return this Book without loading all resources into memory. * @throws IOException */ - public Book readEpubLazy(ZipFile zipFile, String encoding, - List lazyLoadedTypes) throws IOException { + public EpubBook readEpubLazy(ZipFile zipFile, String encoding, + List lazyLoadedTypes) throws IOException { Resources resources = ResourcesLoader - .loadResources(zipFile, encoding, lazyLoadedTypes); + .loadResources(zipFile, encoding, lazyLoadedTypes); return readEpub(resources); } - public Book readEpub(Resources resources) throws IOException { - return readEpub(resources, new Book()); + public EpubBook readEpub(Resources resources) throws IOException { + return readEpub(resources, new EpubBook()); } - public Book readEpub(Resources resources, Book result) throws IOException { + public EpubBook readEpub(Resources resources, EpubBook result) throws IOException { if (result == null) { - result = new Book(); + result = new EpubBook(); } handleMimeType(result, resources); String packageResourceHref = getPackageResourceHref(resources); Resource packageResource = processPackageResource(packageResourceHref, - result, resources); + result, resources); result.setOpfResource(packageResource); Resource ncxResource = processNcxResource(packageResource, result); result.setNcxResource(ncxResource); @@ -116,21 +117,21 @@ public class EpubReader { } - private Book postProcessBook(Book book) { + private EpubBook postProcessBook(EpubBook book) { if (bookProcessor != null) { book = bookProcessor.processBook(book); } return book; } - private Resource processNcxResource(Resource packageResource, Book book) { - Log.d(TAG,"OPF:getHref()"+packageResource.getHref()); + private Resource processNcxResource(Resource packageResource, EpubBook book) { + Log.d(TAG, "OPF:getHref()" + packageResource.getHref()); return NCXDocument.read(book, this); } - private Resource processPackageResource(String packageResourceHref, Book book, - Resources resources) { + private Resource processPackageResource(String packageResourceHref, EpubBook book, + Resources resources) { Resource packageResource = resources.remove(packageResourceHref); try { PackageDocumentReader.read(packageResource, this, book, resources); @@ -163,7 +164,7 @@ public class EpubReader { return result; } - private void handleMimeType(Book result, Resources resources) { + private void handleMimeType(EpubBook result, Resources resources) { resources.remove("mimetype"); } } diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java b/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java index 6506b339a..19238c001 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java @@ -2,11 +2,8 @@ package me.ag2s.epublib.epub; import android.util.Log; -import me.ag2s.epublib.domain.Book; -import me.ag2s.epublib.domain.MediaTypes; -import me.ag2s.epublib.domain.Resource; -import me.ag2s.epublib.util.IOUtil; -//import io.documentnode.minilog.Logger; +import org.xmlpull.v1.XmlSerializer; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -15,7 +12,13 @@ import java.io.Writer; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.xmlpull.v1.XmlSerializer; + +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.util.IOUtil; + +//import io.documentnode.minilog.Logger; /** * Generates an epub file. Not thread-safe, single use object. @@ -41,7 +44,7 @@ public class EpubWriter { } - public void write(Book book, OutputStream out) throws IOException { + public void write(EpubBook book, OutputStream out) throws IOException { book = processBook(book); ZipOutputStream resultStream = new ZipOutputStream(out); writeMimeType(resultStream); @@ -52,14 +55,14 @@ public class EpubWriter { resultStream.close(); } - private Book processBook(Book book) { + private EpubBook processBook(EpubBook book) { if (bookProcessor != null) { book = bookProcessor.processBook(book); } return book; } - private void initTOCResource(Book book) { + private void initTOCResource(EpubBook book) { Resource tocResource; try { tocResource = NCXDocument.createNCXResource(book); @@ -77,8 +80,8 @@ public class EpubWriter { } - private void writeResources(Book book, ZipOutputStream resultStream) - throws IOException { + private void writeResources(EpubBook book, ZipOutputStream resultStream) + throws IOException { for (Resource resource : book.getResources().getAll()) { writeResource(resource, resultStream); } @@ -107,11 +110,11 @@ public class EpubWriter { } - private void writePackageDocument(Book book, ZipOutputStream resultStream) - throws IOException { + private void writePackageDocument(EpubBook book, ZipOutputStream resultStream) + throws IOException { resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf")); XmlSerializer xmlSerializer = EpubProcessorSupport - .createXmlSerializer(resultStream); + .createXmlSerializer(resultStream); PackageDocumentWriter.write(this, xmlSerializer, book); xmlSerializer.flush(); // String resultAsString = result.toString(); diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocument.java b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocument.java index 7c44ed7f3..ec603088c 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocument.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocument.java @@ -2,31 +2,30 @@ package me.ag2s.epublib.epub; import android.util.Log; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xmlpull.v1.XmlSerializer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.Author; -import me.ag2s.epublib.domain.Book; +import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.TOCReference; import me.ag2s.epublib.domain.TableOfContents; -import me.ag2s.epublib.util.ResourceUtil; import me.ag2s.epublib.util.StringUtil; + //import io.documentnode.minilog.Logger; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; //import javax.xml.stream.FactoryConfigurationError; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xmlpull.v1.XmlSerializer; /** * Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/ @@ -75,7 +74,7 @@ public class NCXDocument { } - public static Resource read(Book book, EpubReader epubReader) { + public static Resource read(EpubBook book, EpubReader epubReader) { Log.d(TAG, book.getVersion()); String version = book.getVersion(); if (version.startsWith("2.")) { @@ -89,28 +88,28 @@ public class NCXDocument { } private static List readTOCReferences(NodeList navpoints, - Book book) { + EpubBook book) { Log.d(TAG, book.getVersion()); String version = book.getVersion(); if (version.startsWith("2.")) { - return NCXDocumentV2.readTOCReferences(navpoints,book); + return NCXDocumentV2.readTOCReferences(navpoints, book); } else if (version.startsWith("3.")) { - return NCXDocumentV3.readTOCReferences(navpoints,book); + return NCXDocumentV3.readTOCReferences(navpoints, book); } else { - return NCXDocumentV2.readTOCReferences(navpoints,book); + return NCXDocumentV2.readTOCReferences(navpoints, book); } } - static TOCReference readTOCReference(Element navpointElement, Book book) { + static TOCReference readTOCReference(Element navpointElement, EpubBook book) { Log.d(TAG, book.getVersion()); String version = book.getVersion(); if (version.startsWith("2.")) { - return NCXDocumentV2.readTOCReference(navpointElement,book); + return NCXDocumentV2.readTOCReference(navpointElement, book); } else if (version.startsWith("3.")) { - return NCXDocumentV3.readTOCReference(navpointElement,book); + return NCXDocumentV3.readTOCReference(navpointElement, book); } else { - return NCXDocumentV2.readTOCReference(navpointElement,book); + return NCXDocumentV2.readTOCReference(navpointElement, book); } } @@ -138,7 +137,7 @@ public class NCXDocument { } - public static void write(EpubWriter epubWriter, Book book, + public static void write(EpubWriter epubWriter, EpubBook book, ZipOutputStream resultStream) throws IOException { resultStream .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref())); @@ -158,13 +157,13 @@ public class NCXDocument { * @throws IllegalArgumentException * @1throws FactoryConfigurationError */ - public static void write(XmlSerializer xmlSerializer, Book book) + public static void write(XmlSerializer xmlSerializer, EpubBook book) throws IllegalArgumentException, IllegalStateException, IOException { write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), book.getMetadata().getAuthors(), book.getTableOfContents()); } - public static Resource createNCXResource(Book book) + public static Resource createNCXResource(EpubBook book) throws IllegalArgumentException, IllegalStateException, IOException { return createNCXResource(book.getMetadata().getIdentifiers(), book.getTitle(), book.getMetadata().getAuthors(), diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java index 1993177b6..87972b75b 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java @@ -19,7 +19,7 @@ import java.util.zip.ZipOutputStream; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.Author; -import me.ag2s.epublib.domain.Book; +import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; @@ -75,10 +75,10 @@ public class NCXDocumentV2 extends NCXDocument{ } - public static Resource read(Book book, EpubReader epubReader) { + public static Resource read(EpubBook book, EpubReader epubReader) { Resource ncxResource = null; if (book.getSpine().getTocResource() == null) { - Log.e(TAG,"Book does not contain a table of contents file"); + Log.e(TAG, "Book does not contain a table of contents file"); return ncxResource; } try { @@ -102,12 +102,12 @@ public class NCXDocumentV2 extends NCXDocument{ } static List readTOCReferences(NodeList navpoints, - Book book) { + EpubBook book) { if (navpoints == null) { return new ArrayList<>(); } List result = new ArrayList<>( - navpoints.getLength()); + navpoints.getLength()); for (int i = 0; i < navpoints.getLength(); i++) { Node node = navpoints.item(i); if (node.getNodeType() != Document.ELEMENT_NODE) { @@ -122,13 +122,13 @@ public class NCXDocumentV2 extends NCXDocument{ return result; } - static TOCReference readTOCReference(Element navpointElement, Book book) { + static TOCReference readTOCReference(Element navpointElement, EpubBook book) { String label = readNavLabel(navpointElement); //Log.d(TAG,"label:"+label); String tocResourceRoot = StringUtil - .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/'); + .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/'); if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref() - .length()) { + .length()) { tocResourceRoot = ""; } else { tocResourceRoot = tocResourceRoot + "/"; @@ -174,10 +174,10 @@ public class NCXDocumentV2 extends NCXDocument{ } - public static void write(EpubWriter epubWriter, Book book, - ZipOutputStream resultStream) throws IOException { + public static void write(EpubWriter epubWriter, EpubBook book, + ZipOutputStream resultStream) throws IOException { resultStream - .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref())); + .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref())); XmlSerializer out = EpubProcessorSupport.createXmlSerializer(resultStream); write(out, book); out.flush(); @@ -195,17 +195,17 @@ public class NCXDocumentV2 extends NCXDocument{ * @throws IllegalStateException * @throws IllegalArgumentException */ - public static void write(XmlSerializer xmlSerializer, Book book) - throws IllegalArgumentException, IllegalStateException, IOException { + public static void write(XmlSerializer xmlSerializer, EpubBook book) + throws IllegalArgumentException, IllegalStateException, IOException { write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), - book.getMetadata().getAuthors(), book.getTableOfContents()); + book.getMetadata().getAuthors(), book.getTableOfContents()); } - public static Resource createNCXResource(Book book) - throws IllegalArgumentException, IllegalStateException, IOException { + public static Resource createNCXResource(EpubBook book) + throws IllegalArgumentException, IllegalStateException, IOException { return createNCXResource(book.getMetadata().getIdentifiers(), - book.getTitle(), book.getMetadata().getAuthors(), - book.getTableOfContents()); + book.getTitle(), book.getMetadata().getAuthors(), + book.getTableOfContents()); } public static Resource createNCXResource(List identifiers, diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java index d1f6174cf..743d8b0f2 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java @@ -6,22 +6,14 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import org.xmlpull.v1.XmlSerializer; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; import me.ag2s.epublib.Constants; -import me.ag2s.epublib.domain.Author; -import me.ag2s.epublib.domain.Book; -import me.ag2s.epublib.domain.Identifier; -import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.TOCReference; import me.ag2s.epublib.domain.TableOfContents; @@ -69,7 +61,7 @@ public class NCXDocumentV3 extends NCXDocument { } - public static Resource read(Book book, EpubReader epubReader) { + public static Resource read(EpubBook book, EpubReader epubReader) { Resource ncxResource = null; if (book.getSpine().getTocResource() == null) { Log.e(TAG, "Book does not contain a table of contents file"); @@ -99,7 +91,7 @@ public class NCXDocumentV3 extends NCXDocument { return ncxResource; } - public static List doToc(Node n, Book book) { + public static List doToc(Node n, EpubBook book) { List result = new ArrayList<>(); if (n == null || n.getNodeType() != Document.ELEMENT_NODE) { @@ -118,7 +110,7 @@ public class NCXDocumentV3 extends NCXDocument { static List readTOCReferences(NodeList navpoints, - Book book) { + EpubBook book) { if (navpoints == null) { return new ArrayList<>(); } @@ -146,7 +138,7 @@ public class NCXDocumentV3 extends NCXDocument { } - static TOCReference readTOCReference(Element navpointElement, Book book) { + static TOCReference readTOCReference(Element navpointElement, EpubBook book) { String label = readNavLabel(navpointElement); //Log.d(TAG, "label:" + label); String tocResourceRoot = StringUtil diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java index c2a47e155..2dd935481 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java @@ -1,16 +1,19 @@ package me.ag2s.epublib.epub; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; + import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.Author; -import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.Date; +import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.util.StringUtil; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import javax.xml.namespace.QName; -import org.xmlpull.v1.XmlSerializer; public class PackageDocumentMetadataWriter extends PackageDocumentBase { @@ -23,17 +26,17 @@ public class PackageDocumentMetadataWriter extends PackageDocumentBase { * @throws IllegalStateException * @throws IllegalArgumentException */ - public static void writeMetaData(Book book, XmlSerializer serializer) - throws IllegalArgumentException, IllegalStateException, IOException { + public static void writeMetaData(EpubBook book, XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.metadata); serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE); serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); writeIdentifiers(book.getMetadata().getIdentifiers(), serializer); writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(), - serializer); + serializer); writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(), - serializer); + serializer); writeSimpleMetdataElements(DCTags.description, book.getMetadata().getDescriptions(), serializer); writeSimpleMetdataElements(DCTags.publisher, diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java index 86ecf3044..c986147d9 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java @@ -1,21 +1,12 @@ package me.ag2s.epublib.epub; import android.util.Log; -import android.widget.Toast; -import me.ag2s.epublib.Constants; -import me.ag2s.epublib.domain.Book; -import me.ag2s.epublib.domain.Guide; -import me.ag2s.epublib.domain.GuideReference; -import me.ag2s.epublib.domain.MediaType; -import me.ag2s.epublib.domain.Resource; -import me.ag2s.epublib.domain.Resources; -import me.ag2s.epublib.domain.Spine; -import me.ag2s.epublib.domain.SpineReference; -import me.ag2s.epublib.domain.MediaTypes; -import me.ag2s.epublib.util.ResourceUtil; -import me.ag2s.epublib.util.StringUtil; -//import io.documentnode.minilog.Logger; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -27,11 +18,23 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; + import javax.xml.parsers.ParserConfigurationException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Guide; +import me.ag2s.epublib.domain.GuideReference; +import me.ag2s.epublib.domain.MediaType; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.domain.Resources; +import me.ag2s.epublib.domain.Spine; +import me.ag2s.epublib.domain.SpineReference; +import me.ag2s.epublib.util.ResourceUtil; +import me.ag2s.epublib.util.StringUtil; + +//import io.documentnode.minilog.Logger; /** * Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf @@ -47,8 +50,8 @@ public class PackageDocumentReader extends PackageDocumentBase { public static void read( - Resource packageResource, EpubReader epubReader, Book book, - Resources resources) + Resource packageResource, EpubReader epubReader, EpubBook book, + Resources resources) throws UnsupportedEncodingException, SAXException, IOException, ParserConfigurationException { Document packageDocument = ResourceUtil.getAsDocument(packageResource); String packageHref = packageResource.getHref(); @@ -151,16 +154,16 @@ public class PackageDocumentReader extends PackageDocumentBase { * @param resources */ private static void readGuide(Document packageDocument, - EpubReader epubReader, Book book, Resources resources) { + EpubReader epubReader, EpubBook book, Resources resources) { Element guideElement = DOMUtil - .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), - NAMESPACE_OPF, OPFTags.guide); + .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), + NAMESPACE_OPF, OPFTags.guide); if (guideElement == null) { return; } Guide guide = book.getGuide(); NodeList guideReferences = guideElement - .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference); + .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference); for (int i = 0; i < guideReferences.getLength(); i++) { Element referenceElement = (Element) guideReferences.item(i); String resourceHref = DOMUtil @@ -400,17 +403,18 @@ public class PackageDocumentReader extends PackageDocumentBase { /** * Finds the cover resource in the packageDocument and adds it to the book if found. * Keeps the cover resource in the resources map + * * @param packageDocument * @param book * @1param resources */ - private static void readCover(Document packageDocument, Book book) { + private static void readCover(Document packageDocument, EpubBook book) { Collection coverHrefs = findCoverHrefs(packageDocument); for (String coverHref : coverHrefs) { Resource resource = book.getResources().getByHref(coverHref); if (resource == null) { - Log.e(TAG,"Cover resource " + coverHref + " not found"); + Log.e(TAG, "Cover resource " + coverHref + " not found"); continue; } if (resource.getMediaType() == MediaTypes.XHTML) { diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java index 8289da5d7..8853c8363 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java @@ -2,24 +2,26 @@ package me.ag2s.epublib.epub; import android.util.Log; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + import me.ag2s.epublib.Constants; -import me.ag2s.epublib.domain.Book; +import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Guide; import me.ag2s.epublib.domain.GuideReference; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Spine; import me.ag2s.epublib.domain.SpineReference; -import me.ag2s.epublib.util.ResourceUtil; import me.ag2s.epublib.util.StringUtil; + //import io.documentnode.minilog.Logger; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; //import javax.xml.stream.XMLStreamException; -import org.xmlpull.v1.XmlSerializer; /** * Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf @@ -33,17 +35,17 @@ public class PackageDocumentWriter extends PackageDocumentBase { private static String TAG= PackageDocumentWriter.class.getName(); public static void write(EpubWriter epubWriter, XmlSerializer serializer, - Book book) throws IOException { + EpubBook book) throws IOException { try { serializer.startDocument(Constants.CHARACTER_ENCODING, false); serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE); serializer.startTag(NAMESPACE_OPF, OPFTags.packageTag); serializer - .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.version, - "2.0"); + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.version, + "2.0"); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, - OPFAttributes.uniqueIdentifier, BOOK_ID_ID); + OPFAttributes.uniqueIdentifier, BOOK_ID_ID); PackageDocumentMetadataWriter.writeMetaData(book, serializer); @@ -71,26 +73,26 @@ public class PackageDocumentWriter extends PackageDocumentBase { * @throws IllegalArgumentException * 1@throws XMLStreamException */ - private static void writeSpine(Book book, EpubWriter epubWriter, - XmlSerializer serializer) - throws IllegalArgumentException, IllegalStateException, IOException { + private static void writeSpine(EpubBook book, EpubWriter epubWriter, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.spine); Resource tocResource = book.getSpine().getTocResource(); String tocResourceId = tocResource.getId(); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.toc, - tocResourceId); + tocResourceId); if (book.getCoverPage() != null // there is a cover page - && book.getSpine().findFirstResourceById(book.getCoverPage().getId()) - < 0) { // cover page is not already in the spine + && book.getSpine().findFirstResourceById(book.getCoverPage().getId()) + < 0) { // cover page is not already in the spine // write the cover html file serializer.startTag(NAMESPACE_OPF, OPFTags.itemref); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref, book.getCoverPage().getId()); serializer - .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear, - "no"); + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear, + "no"); serializer.endTag(NAMESPACE_OPF, OPFTags.itemref); } writeSpineItems(book.getSpine(), serializer); @@ -98,18 +100,18 @@ public class PackageDocumentWriter extends PackageDocumentBase { } - private static void writeManifest(Book book, EpubWriter epubWriter, - XmlSerializer serializer) - throws IllegalArgumentException, IllegalStateException, IOException { + private static void writeManifest(EpubBook book, EpubWriter epubWriter, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.manifest); serializer.startTag(NAMESPACE_OPF, OPFTags.item); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, - epubWriter.getNcxId()); + epubWriter.getNcxId()); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, - epubWriter.getNcxHref()); + epubWriter.getNcxHref()); serializer - .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, epubWriter.getNcxMediaType()); serializer.endTag(NAMESPACE_OPF, OPFTags.item); @@ -122,9 +124,9 @@ public class PackageDocumentWriter extends PackageDocumentBase { serializer.endTag(NAMESPACE_OPF, OPFTags.manifest); } - private static List getAllResourcesSortById(Book book) { + private static List getAllResourcesSortById(EpubBook book) { List allResources = new ArrayList( - book.getResources().getAll()); + book.getResources().getAll()); Collections.sort(allResources, new Comparator() { @Override @@ -137,25 +139,25 @@ public class PackageDocumentWriter extends PackageDocumentBase { /** * Writes a resources as an item element + * * @param resource * @param serializer * @throws IOException * @throws IllegalStateException - * @throws IllegalArgumentException - * 1@throws XMLStreamException + * @throws IllegalArgumentException 1@throws XMLStreamException */ - private static void writeItem(Book book, Resource resource, - XmlSerializer serializer) - throws IllegalArgumentException, IllegalStateException, IOException { + private static void writeItem(EpubBook book, Resource resource, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { if (resource == null || - (resource.getMediaType() == MediaTypes.NCX - && book.getSpine().getTocResource() != null)) { + (resource.getMediaType() == MediaTypes.NCX + && book.getSpine().getTocResource() != null)) { return; } if (StringUtil.isBlank(resource.getId())) { // log.error("resource id must not be empty (href: " + resource.getHref() // + ", mediatype:" + resource.getMediaType() + ")"); - Log.e(TAG,"resource id must not be empty (href: " + resource.getHref() + Log.e(TAG, "resource id must not be empty (href: " + resource.getHref() + ", mediatype:" + resource.getMediaType() + ")"); return; } @@ -196,22 +198,22 @@ public class PackageDocumentWriter extends PackageDocumentBase { serializer.startTag(NAMESPACE_OPF, OPFTags.itemref); serializer .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref, - spineReference.getResourceId()); + spineReference.getResourceId()); if (!spineReference.isLinear()) { serializer - .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear, - OPFValues.no); + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear, + OPFValues.no); } serializer.endTag(NAMESPACE_OPF, OPFTags.itemref); } } - private static void writeGuide(Book book, EpubWriter epubWriter, - XmlSerializer serializer) - throws IllegalArgumentException, IllegalStateException, IOException { + private static void writeGuide(EpubBook book, EpubWriter epubWriter, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.guide); ensureCoverPageGuideReferenceWritten(book.getGuide(), epubWriter, - serializer); + serializer); for (GuideReference reference : book.getGuide().getReferences()) { writeGuideReference(reference, serializer); } diff --git a/settings.gradle b/settings.gradle index 60c0c6c5c..6e5b9d8ee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1 @@ -include ':app' -include ':epublib' +include ':app',':epublib'