Merge pull request #1 from gedoor/master

Sys code
pull/913/head
ag2s20150909 4 years ago committed by GitHub
commit 8e8c4d22a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      app/build.gradle
  2. 4
      app/src/main/AndroidManifest.xml
  3. 13
      app/src/main/assets/defaultData/rssSources.json
  4. 1
      app/src/main/assets/updateLog.md
  5. 53
      app/src/main/java/io/legado/app/help/LocalConfig.kt
  6. 20
      app/src/main/java/io/legado/app/help/permission/ActivitySource.kt
  7. 19
      app/src/main/java/io/legado/app/help/permission/FragmentSource.kt
  8. 7
      app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt
  9. 7
      app/src/main/java/io/legado/app/help/permission/OnPermissionsGrantedCallback.kt
  10. 9
      app/src/main/java/io/legado/app/help/permission/OnPermissionsResultCallback.kt
  11. 14
      app/src/main/java/io/legado/app/help/permission/OnRequestPermissionsResultCallback.kt
  12. 84
      app/src/main/java/io/legado/app/help/permission/PermissionActivity.kt
  13. 2
      app/src/main/java/io/legado/app/help/permission/Permissions.kt
  14. 94
      app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt
  15. 204
      app/src/main/java/io/legado/app/help/permission/Request.kt
  16. 69
      app/src/main/java/io/legado/app/help/permission/RequestManager.kt
  17. 20
      app/src/main/java/io/legado/app/help/permission/RequestPlugins.kt
  18. 12
      app/src/main/java/io/legado/app/help/permission/RequestSource.kt
  19. 160
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  20. 6
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  21. 41
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  22. 12
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  23. 29
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  24. 957
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlMesh.java
  25. 195
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlPage.java
  26. 4
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt
  27. 20
      app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt
  28. 15
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  29. 65
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  30. 20
      app/src/main/java/io/legado/app/ui/filepicker/FilePicker.kt
  31. 18
      app/src/main/java/io/legado/app/ui/filepicker/FilePickerDialog.kt
  32. 12
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  33. 1
      avd.bat
  34. 1
      avd11.bat
  35. 13
      epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java
  36. 8
      epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java
  37. 13
      epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java
  38. 2
      epublib/src/main/java/me/ag2s/epublib/domain/EpubBook.java
  39. 4
      epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java
  40. 12
      epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java
  41. 59
      epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java
  42. 25
      epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java
  43. 53
      epublib/src/main/java/me/ag2s/epublib/epub/NCXDocument.java
  44. 16
      epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java
  45. 18
      epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java
  46. 17
      epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java
  47. 48
      epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java
  48. 36
      epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java
  49. 3
      settings.gradle

@ -211,11 +211,6 @@ dependencies {
// //
implementation 'com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.1.3' 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'
// }
} }

@ -171,6 +171,10 @@
<activity <activity
android:name=".ui.audio.AudioPlayActivity" android:name=".ui.audio.AudioPlayActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<!-- 授权界面 -->
<activity
android:name=".help.permission.PermissionActivity"
android:theme="@style/Activity.Permission" />
<!-- 二维码扫描 --> <!-- 二维码扫描 -->
<activity <activity
android:name=".ui.qrcode.QrCodeActivity" android:name=".ui.qrcode.QrCodeActivity"

@ -1,9 +1,18 @@
[ [
{ {
"articleStyle": 0,
"customOrder": 1, "customOrder": 1,
"enableJs": true, "enableJs": true,
"enabled": true, "enabled": true,
"singleUrl": true,
"sourceIcon": "http:\/\/ku.mumuceo.com\/static\/images\/applogo\/yuedu.png",
"sourceName": "使用说明",
"sourceUrl": "https://www.yuque.com/legado"
},
{
"articleStyle": 0,
"customOrder": 2,
"enableJs": true,
"enabled": true,
"loadWithBaseUrl": false, "loadWithBaseUrl": false,
"ruleArticles": "class.release-main-section||tbody@tr", "ruleArticles": "class.release-main-section||tbody@tr",
"ruleContent": "@js:\nif(baseUrl.match(\/90pan\/)){\nresult=result.replace(\/true\\||<a href=\"([^\"]+)\".*?><.*?>([^<]+)<\\\/span><\\\/a>\/g,'<h4><a href=\"$1\" style=\"color:#000\">$2<\/a><\/h4><p style=\"font-size:10px\">$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+'<br><p>下载地址:<\/p><a style=\"font-size:13px\">'+href+'<\/a>'\n}", "ruleContent": "@js:\nif(baseUrl.match(\/90pan\/)){\nresult=result.replace(\/true\\||<a href=\"([^\"]+)\".*?><.*?>([^<]+)<\\\/span><\\\/a>\/g,'<h4><a href=\"$1\" style=\"color:#000\">$2<\/a><\/h4><p style=\"font-size:10px\">$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+'<br><p>下载地址:<\/p><a style=\"font-size:13px\">'+href+'<\/a>'\n}",
@ -19,7 +28,7 @@
"style": "" "style": ""
}, },
{ {
"customOrder": 1, "customOrder": 3,
"enableJs": true, "enableJs": true,
"enabled": true, "enabled": true,
"singleUrl": true, "singleUrl": true,

@ -7,6 +7,7 @@
* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右 * 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右
* js添加一个并发访问的方法 java.ajaxAll(urlList: Array<String>) 返回 Array<StrResponse?> * js添加一个并发访问的方法 java.ajaxAll(urlList: Array<String>) 返回 Array<StrResponse?>
* 优化目录并发访问 * 优化目录并发访问
* 添加自定义epublib,支持epub v3解析目录。by ag2s20150909
**2021/03/19** **2021/03/19**
* 修复图片地址参数缺少的bug * 修复图片地址参数缺少的bug

@ -10,6 +10,33 @@ object LocalConfig {
private val localConfig = private val localConfig =
appCtx.getSharedPreferences("local", Context.MODE_PRIVATE) 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 var versionCode
get() = localConfig.getLong(versionCodeKey, 0) get() = localConfig.getLong(versionCodeKey, 0)
set(value) { set(value) {
@ -46,30 +73,4 @@ object LocalConfig {
return true 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")
} }

@ -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<AppCompatActivity> = WeakReference(activity)
override val context: Context?
get() = actRef.get()
override fun startActivity(intent: Intent) {
actRef.get()?.startActivity(intent)
}
}

@ -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<Fragment> = WeakReference(fragment)
override val context: Context?
get() = fragRef.get()?.requireContext()
override fun startActivity(intent: Intent) {
fragRef.get()?.startActivity(intent)
}
}

@ -0,0 +1,7 @@
package io.legado.app.help.permission
interface OnPermissionsDeniedCallback {
fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>)
}

@ -0,0 +1,7 @@
package io.legado.app.help.permission
interface OnPermissionsGrantedCallback {
fun onPermissionsGranted(requestCode: Int)
}

@ -0,0 +1,9 @@
package io.legado.app.help.permission
interface OnPermissionsResultCallback {
fun onPermissionsGranted(requestCode: Int)
fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>)
}

@ -0,0 +1,14 @@
package io.legado.app.help.permission
import android.content.Intent
interface OnRequestPermissionsResultCallback {
fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
)
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
}

@ -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<String>,
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"
}
}

@ -1,4 +1,4 @@
package io.legado.app.constant package io.legado.app.help.permission
@Suppress("unused") @Suppress("unused")
object Permissions { object Permissions {

@ -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<String>()
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<String>) -> Unit): Builder {
request.setOnDeniedCallback(object : OnPermissionsDeniedCallback {
override fun onPermissionsDenied(
requestCode: Int,
deniedPermissions: Array<String>
) {
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
}
}
}

@ -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<String>? = 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<String>?
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<PermissionActivity> {
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<String>?): Array<String>? {
if (permissions != null) {
val deniedPermissionList = ArrayList<String>()
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<PermissionActivity> {
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<String>) {
try {
deniedCallback?.onPermissionsDenied(requestCode, deniedPermissions)
} catch (ignore: Exception) {
}
RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
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
}
}

@ -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<Request>? = 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<String>) {
startNextRequest()
}
}

@ -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
}
}

@ -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)
}

@ -5,14 +5,12 @@ import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import io.legado.app.data.appDb 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.BookChapter
import io.legado.app.data.entities.EpubChapter
import io.legado.app.utils.* import io.legado.app.utils.*
import me.ag2s.epublib.domain.EpubBook
import me.ag2s.epublib.domain.Book
import me.ag2s.epublib.domain.MediaTypes import me.ag2s.epublib.domain.MediaTypes
import me.ag2s.epublib.domain.Resources import me.ag2s.epublib.domain.Resources
import me.ag2s.epublib.domain.TOCReference
import me.ag2s.epublib.epub.EpubReader import me.ag2s.epublib.epub.EpubReader
import me.ag2s.epublib.util.ResourceUtil import me.ag2s.epublib.util.ResourceUtil
import org.jsoup.Jsoup import org.jsoup.Jsoup
@ -26,15 +24,15 @@ import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
class EPUBFile(var book: io.legado.app.data.entities.Book) { class EpubFile(var book: Book) {
companion object { companion object {
private var eFile: EPUBFile? = null private var eFile: EpubFile? = null
@Synchronized @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) { if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) {
eFile = EPUBFile(book) eFile = EpubFile(book)
return eFile!! return eFile!!
} }
eFile?.book = book eFile?.book = book
@ -42,30 +40,30 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) {
} }
@Synchronized @Synchronized
fun getChapterList(book: io.legado.app.data.entities.Book): ArrayList<BookChapter> { fun getChapterList(book: Book): ArrayList<BookChapter> {
return getEFile(book).getChapterList() return getEFile(book).getChapterList()
} }
@Synchronized @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) return getEFile(book).getContent(chapter)
} }
@Synchronized @Synchronized
fun getImage( fun getImage(
book: io.legado.app.data.entities.Book, book: Book,
href: String href: String
): InputStream? { ): InputStream? {
return getEFile(book).getImage(href) return getEFile(book).getImage(href)
} }
@Synchronized @Synchronized
fun upBookInfo(book: io.legado.app.data.entities.Book) { fun upBookInfo(book: Book) {
return getEFile(book).upBookInfo() return getEFile(book).upBookInfo()
} }
} }
private var epubBook: Book? = null private var epubBook: EpubBook? = null
private var mCharset: Charset = Charset.defaultCharset() private var mCharset: Charset = Charset.defaultCharset()
init { init {
@ -102,7 +100,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) {
} }
/*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/ /*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/
private fun readEpub(input: InputStream?): Book? { private fun readEpub(input: InputStream?): EpubBook? {
if (input == null) return null if (input == null) return null
try { try {
val inZip = ZipInputStream(input) val inZip = ZipInputStream(input)
@ -156,7 +154,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) {
} }
/*选择去除正文中的H标签,部分书籍标题与阅读标题重复待优化*/ /*选择去除正文中的H标签,部分书籍标题与阅读标题重复待优化*/
var tag = io.legado.app.data.entities.Book.hTag var tag = Book.hTag
if (book.getDelTag(tag)) { if (book.getDelTag(tag)) {
body.getElementsByTag("h1")?.remove() body.getElementsByTag("h1")?.remove()
body.getElementsByTag("h2")?.remove() body.getElementsByTag("h2")?.remove()
@ -168,7 +166,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) {
} }
/*选择去除正文中的img标签,目前图片支持效果待优化*/ /*选择去除正文中的img标签,目前图片支持效果待优化*/
tag = io.legado.app.data.entities.Book.imgTag tag = Book.imgTag
if (book.getDelTag(tag)) { if (book.getDelTag(tag)) {
body.getElementsByTag("img")?.remove() body.getElementsByTag("img")?.remove()
} }
@ -177,7 +175,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) {
elements.select("script").remove() elements.select("script").remove()
elements.select("style").remove() elements.select("style").remove()
/*选择去除正文中的ruby标签,目前注释支持效果待优化*/ /*选择去除正文中的ruby标签,目前注释支持效果待优化*/
tag = io.legado.app.data.entities.Book.rubyTag tag = Book.rubyTag
var html = elements.outerHtml() var html = elements.outerHtml()
if (book.getDelTag(tag)) { if (book.getDelTag(tag)) {
html = html.replace("<ruby>\\s?([\\u4e00-\\u9fa5])\\s?.*?</ruby>".toRegex(), "$1") html = html.replace("<ruby>\\s?([\\u4e00-\\u9fa5])\\s?.*?</ruby>".toRegex(), "$1")
@ -212,15 +210,7 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) {
private fun getChapterList(): ArrayList<BookChapter> { private fun getChapterList(): ArrayList<BookChapter> {
val chapterList = ArrayList<BookChapter>() val chapterList = ArrayList<BookChapter>()
epubBook?.let { eBook -> epubBook?.tableOfContents?.allUniqueResources?.forEachIndexed { index, resource ->
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 var title = resource.title
if (TextUtils.isEmpty(title)) { if (TextUtils.isEmpty(title)) {
try { try {
@ -235,133 +225,19 @@ class EPUBFile(var book: io.legado.app.data.entities.Book) {
} }
} }
val chapter = BookChapter() val chapter = BookChapter()
chapter.index = i chapter.index = index
chapter.bookUrl = book.bookUrl chapter.bookUrl = book.bookUrl
chapter.url = resource.href chapter.url = resource.href
if (i == 0 && title.isEmpty()) { if (index == 0 && title.isEmpty()) {
chapter.title = "封面" chapter.title = "封面"
} else { } else {
chapter.title = title chapter.title = title
} }
chapterList.add(chapter) 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.latestChapterTitle = chapterList.lastOrNull()?.title
book.totalChapterNum = chapterList.size book.totalChapterNum = chapterList.size
return chapterList return chapterList
} }
/*获取当前章节的子章节。部分书籍一个章节包含多个html文件,(一些精排书籍,每一章节正文前的标题、标题封面、引言等都会有独立html)*/
/*需在读取常规章节列表后调用,遍历书籍全内容,根据href检索原不包含在章节内的html归属父章节*/
private fun getChildChapter(chapterList: ArrayList<BookChapter>) {
epubBook?.let {
val contents = it.contents
val chapters = ArrayList<EpubChapter>()
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)
}
i++
}
}
appDb.epubChapterDao.deleteByName(book.bookUrl)
if (chapters.size > 0) appDb.epubChapterDao.insert(*chapters.toTypedArray())
}
}
/*获取书籍起始页内容。部分书籍第一章之前存在封面,引言,扉页等内容*/
/*tile获取不同书籍风格杂乱,格式化处理待优化*/
private var durIndex = 0
private fun parseFirstPage(
chapterList: ArrayList<BookChapter>,
refs: List<TOCReference>?
) {
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.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
}
chapterList.add(chapter)
durIndex++
i++
}
}
private fun parseMenu(
chapterList: ArrayList<BookChapter>,
refs: List<TOCReference>?,
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)
}
}
}
} }

@ -22,7 +22,7 @@ object LocalBook {
fun getChapterList(book: Book): ArrayList<BookChapter> { fun getChapterList(book: Book): ArrayList<BookChapter> {
return if (book.isEpub()) { return if (book.isEpub()) {
EPUBFile.getChapterList(book) EpubFile.getChapterList(book)
} else { } else {
AnalyzeTxtFile().analyze(book) AnalyzeTxtFile().analyze(book)
} }
@ -30,7 +30,7 @@ object LocalBook {
fun getContext(book: Book, chapter: BookChapter): String? { fun getContext(book: Book, chapter: BookChapter): String? {
return if (book.isEpub()) { return if (book.isEpub()) {
EPUBFile.getContent(book, chapter) EpubFile.getContent(book, chapter)
} else { } else {
AnalyzeTxtFile.getContent(book, chapter) AnalyzeTxtFile.getContent(book, chapter)
} }
@ -83,7 +83,7 @@ object LocalBook {
"${MD5Utils.md5Encode16(path)}.jpg" "${MD5Utils.md5Encode16(path)}.jpg"
) )
) )
if (book.isEpub()) EPUBFile.upBookInfo(book) if (book.isEpub()) EpubFile.upBookInfo(book)
appDb.bookDao.insert(book) appDb.bookDao.insert(book)
return book return book
} }

@ -1,18 +1,19 @@
package io.legado.app.ui.book.info.edit package io.legado.app.ui.book.info.edit
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Permissions
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.databinding.ActivityBookInfoEditBinding 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.ui.book.changecover.ChangeCoverDialog
import io.legado.app.utils.* import io.legado.app.utils.*
import java.io.File import java.io.File
@ -21,9 +22,7 @@ class BookInfoEditActivity :
VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(), VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(),
ChangeCoverDialog.CallBack { ChangeCoverDialog.CallBack {
private val selectCover = registerForActivityResult(ActivityResultContracts.GetContent()) { private val resultSelectCover = 132
coverChangeTo(it)
}
override val viewModel: BookInfoEditViewModel override val viewModel: BookInfoEditViewModel
by viewModels() by viewModels()
@ -61,7 +60,7 @@ class BookInfoEditActivity :
} }
} }
tvSelectCover.setOnClickListener { tvSelectCover.setOnClickListener {
selectCover.launch("image/*") selectImage()
} }
tvRefreshCover.setOnClickListener { tvRefreshCover.setOnClickListener {
viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString() 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) { override fun coverChangeTo(coverUrl: String) {
viewModel.book?.customCoverUrl = coverUrl viewModel.book?.customCoverUrl = coverUrl
binding.tieCoverUrl.setText(coverUrl) binding.tieCoverUrl.setText(coverUrl)
@ -117,7 +123,13 @@ class BookInfoEditActivity :
} ?: toastOnUi("获取文件出错") } ?: toastOnUi("获取文件出错")
} }
} else { } else {
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { 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 -> RealPathUtil.getPath(this, uri)?.let { path ->
val imgFile = File(path) val imgFile = File(path)
if (imgFile.exists()) { if (imgFile.exists()) {
@ -127,8 +139,21 @@ class BookInfoEditActivity :
coverChangeTo(file.absolutePath) 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)
}
}
}
}
}
} }

@ -8,7 +8,6 @@ import android.os.Bundle
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
@ -16,10 +15,11 @@ import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Permissions
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.databinding.ActivityImportBookBinding import io.legado.app.databinding.ActivityImportBookBinding
import io.legado.app.help.AppConfig 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.lib.theme.backgroundColor
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.filepicker.FilePickerDialog
@ -148,12 +148,16 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
} }
else -> { else -> {
binding.tvEmptyMsg.visible() binding.tvEmptyMsg.visible()
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
rootDoc = null rootDoc = null
subDocs.clear() subDocs.clear()
path = lastPath path = lastPath
upPath() upPath()
}.launch(Permissions.Group.STORAGE) }
.request()
} }
} }
} }

@ -8,17 +8,17 @@ import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialog
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.Permissions
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogReadBgTextBinding import io.legado.app.databinding.DialogReadBgTextBinding
import io.legado.app.databinding.ItemBgImageBinding import io.legado.app.databinding.ItemBgImageBinding
import io.legado.app.help.ReadBookConfig 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.dialogs.alert
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor 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 binding by viewBinding(DialogReadBgTextBinding::bind)
private val requestCodeBg = 123
private val requestCodeExport = 131 private val requestCodeExport = 131
private val requestCodeImport = 132 private val requestCodeImport = 132
private val configFileName = "readConfig.zip" private val configFileName = "readConfig.zip"
@ -190,9 +191,10 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
} }
private fun selectImage() { private fun selectImage() {
registerForActivityResult(ActivityResultContracts.GetContent()) { val intent = Intent(Intent.ACTION_GET_CONTENT)
setBgFromUri(it) intent.addCategory(Intent.CATEGORY_OPENABLE)
}.launch("image/*") intent.type = "image/*"
startActivityForResult(intent, requestCodeBg)
} }
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
@ -369,7 +371,13 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) { when (requestCode) {
requestCodeBg -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
setBgFromUri(uri)
}
}
requestCodeImport -> if (resultCode == RESULT_OK) { requestCodeImport -> if (resultCode == RESULT_OK) {
data?.data?.let { uri -> data?.data?.let { uri ->
importConfig(uri) importConfig(uri)
@ -399,13 +407,20 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
} ?: toastOnUi("获取文件出错") } ?: toastOnUi("获取文件出错")
} }
} else { } else {
registerForActivityResult(ActivityResultContracts.RequestPermission()) { 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 -> RealPathUtil.getPath(requireContext(), uri)?.let { path ->
ReadBookConfig.durConfig.setCurBg(2, path) ReadBookConfig.durConfig.setCurBg(2, path)
ReadBookConfig.upBg() ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} }
}.launch(Permissions.READ_EXTERNAL_STORAGE) }
.request()
} }
} }
} }

@ -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<ShadowVertex> mArrDropShadowVertices;
private Array<Vertex> mArrIntersections;
private Array<Vertex> mArrOutputVertices;
private Array<Vertex> mArrRotatedVertices;
private Array<Double> mArrScanLines;
private Array<ShadowVertex> mArrSelfShadowVertices;
private Array<ShadowVertex> mArrTempShadowVertices;
private Array<Vertex> 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<Double>(maxCurlSplits + 2);
mArrOutputVertices = new Array<Vertex>(7);
mArrRotatedVertices = new Array<Vertex>(4);
mArrIntersections = new Array<Vertex>(2);
mArrTempVertices = new Array<Vertex>(7 + 4);
for (int i = 0; i < 7 + 4; ++i) {
mArrTempVertices.add(new Vertex());
}
if (DRAW_SHADOW) {
mArrSelfShadowVertices = new Array<ShadowVertex>(
(mMaxCurlSplits + 2) * 2);
mArrDropShadowVertices = new Array<ShadowVertex>(
(mMaxCurlSplits + 2) * 2);
mArrTempShadowVertices = new Array<ShadowVertex>(
(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<Vertex> 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<Vertex> 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<Vertex> getIntersections(Array<Vertex> 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<T> {
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<T> 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;
}
}
}

@ -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;
}
}

@ -3,7 +3,7 @@ package io.legado.app.ui.book.read.page.provider
import android.graphics.Bitmap import android.graphics.Bitmap
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp 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.BitmapUtils
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -36,7 +36,7 @@ object ImageProvider {
val vFile = BookHelp.getImage(book, src) val vFile = BookHelp.getImage(book, src)
if (!vFile.exists()) { if (!vFile.exists()) {
if (book.isEpub()) { if (book.isEpub()) {
EPUBFile.getImage(book, src)?.use { input -> EpubFile.getImage(book, src)?.use { input ->
val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath) val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath)
FileOutputStream(newFile).use { output -> FileOutputStream(newFile).use { output ->
input.copyTo(output) input.copyTo(output)

@ -3,14 +3,14 @@ package io.legado.app.ui.config
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Permissions
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine 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.Backup
import io.legado.app.help.storage.BookWebDav import io.legado.app.help.storage.BookWebDav
import io.legado.app.help.storage.ImportOldData import io.legado.app.help.storage.ImportOldData
@ -56,14 +56,18 @@ object BackupRestoreUi {
fragment: Fragment, fragment: Fragment,
path: String path: String
) { ) {
fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
Coroutine.async { Coroutine.async {
AppConfig.backupPath = path AppConfig.backupPath = path
Backup.backup(fragment.requireContext(), path) Backup.backup(fragment.requireContext(), path)
}.onSuccess { }.onSuccess {
fragment.toastOnUi(R.string.backup_success) fragment.toastOnUi(R.string.backup_success)
} }
}.launch(Permissions.Group.STORAGE) }
.request()
} }
fun selectBackupFolder(fragment: Fragment, requestCode: Int = selectFolderRequestCode) { fun selectBackupFolder(fragment: Fragment, requestCode: Int = selectFolderRequestCode) {
@ -99,13 +103,17 @@ object BackupRestoreUi {
} }
private fun restoreUsePermission(fragment: Fragment, path: String) { private fun restoreUsePermission(fragment: Fragment, path: String) {
fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
Coroutine.async { Coroutine.async {
AppConfig.backupPath = path AppConfig.backupPath = path
Restore.restoreDatabase(path) Restore.restoreDatabase(path)
Restore.restoreConfig(path) Restore.restoreConfig(path)
} }
}.launch(Permissions.Group.STORAGE) }
.request()
} }
fun importOldData(fragment: Fragment) { fun importOldData(fragment: Fragment) {

@ -10,18 +10,18 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BasePreferenceFragment import io.legado.app.base.BasePreferenceFragment
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.Permissions
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp 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.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
@ -224,7 +224,13 @@ class OtherConfigFragment : BasePreferenceFragment(),
} ?: toastOnUi("获取文件出错") } ?: toastOnUi("获取文件出错")
} }
} else { } else {
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { 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 -> RealPathUtil.getPath(requireContext(), uri)?.let { path ->
val imgFile = File(path) val imgFile = File(path)
if (imgFile.exists()) { if (imgFile.exists()) {
@ -235,7 +241,8 @@ class OtherConfigFragment : BasePreferenceFragment(),
CoverImageView.upDefaultCover() CoverImageView.upDefaultCover()
} }
} }
}.launch(Permissions.Group.STORAGE) }
.request()
} }
} }

@ -1,6 +1,8 @@
package io.legado.app.ui.config package io.legado.app.ui.config
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -9,19 +11,19 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference import androidx.preference.Preference
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BasePreferenceFragment import io.legado.app.base.BasePreferenceFragment
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.Permissions
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.LauncherIconHelp import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ThemeConfig 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.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
@ -36,17 +38,8 @@ import java.io.File
class ThemeConfigFragment : BasePreferenceFragment(), class ThemeConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val setLightBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) { private val requestCodeBgImage = 234
setBgFromUri(it, PreferKey.bgImage) { private val requestCodeBgImageN = 342
upTheme(false)
}
}
private val setDarkBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) {
setBgFromUri(it, PreferKey.bgImageN) {
upTheme(false)
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_theme) addPreferencesFromResource(R.xml.pref_config_theme)
@ -193,26 +186,26 @@ class ThemeConfigFragment : BasePreferenceFragment(),
"themeList" -> ThemeListDialog().show(childFragmentManager, "themeList") "themeList" -> ThemeListDialog().show(childFragmentManager, "themeList")
"saveDayTheme", "saveNightTheme" -> saveThemeAlert(key) "saveDayTheme", "saveNightTheme" -> saveThemeAlert(key)
PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) { PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) {
setLightBgImage.launch("image/*") selectImage(requestCodeBgImage)
} else { } else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) { if (i == 0) {
removePref(PreferKey.bgImage) removePref(PreferKey.bgImage)
upTheme(false) upTheme(false)
} else { } else {
setLightBgImage.launch("image/*") selectImage(requestCodeBgImage)
} }
} }
} }
PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) { PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) {
setDarkBgImage.launch("image/*") selectImage(requestCodeBgImageN)
} else { } else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) { if (i == 0) {
removePref(PreferKey.bgImageN) removePref(PreferKey.bgImageN)
upTheme(true) upTheme(true)
} else { } else {
setDarkBgImage.launch("image/*") selectImage(requestCodeBgImageN)
} }
} }
} }
@ -220,6 +213,13 @@ class ThemeConfigFragment : BasePreferenceFragment(),
return super.onPreferenceTreeClick(preference) 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") @SuppressLint("InflateParams")
private fun saveThemeAlert(key: String) { private fun saveThemeAlert(key: String) {
alert(R.string.theme_name) { alert(R.string.theme_name) {
@ -279,7 +279,13 @@ class ThemeConfigFragment : BasePreferenceFragment(),
} ?: toastOnUi("获取文件出错") } ?: toastOnUi("获取文件出错")
} }
} else { } else {
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { 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 -> RealPathUtil.getPath(requireContext(), uri)?.let { path ->
val imgFile = File(path) val imgFile = File(path)
if (imgFile.exists()) { if (imgFile.exists()) {
@ -291,7 +297,28 @@ class ThemeConfigFragment : BasePreferenceFragment(),
success() 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)
}
}
}
} }
} }

@ -2,11 +2,11 @@ package io.legado.app.ui.filepicker
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.legado.app.R 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 import io.legado.app.lib.dialogs.alert
@Suppress("unused") @Suppress("unused")
@ -247,15 +247,23 @@ object FilePicker {
} }
private fun checkPermissions(fragment: Fragment, success: (() -> Unit)? = null) { private fun checkPermissions(fragment: Fragment, success: (() -> Unit)? = null) {
fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
success?.invoke() success?.invoke()
}.launch(Permissions.Group.STORAGE) }
.request()
} }
private fun checkPermissions(activity: AppCompatActivity, success: (() -> Unit)? = null) { private fun checkPermissions(activity: AppCompatActivity, success: (() -> Unit)? = null) {
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { PermissionsCompat.Builder(activity)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
success?.invoke() success?.invoke()
}.launch(Permissions.Group.STORAGE) }
.request()
} }
private fun typesOfExtensions(allowExtensions: Array<String>): Array<String> { private fun typesOfExtensions(allowExtensions: Array<String>): Array<String> {

@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@ -15,6 +16,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.databinding.DialogFileChooserBinding import io.legado.app.databinding.DialogFileChooserBinding
import io.legado.app.help.permission.Permissions
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.filepicker.adapter.FileAdapter import io.legado.app.ui.filepicker.adapter.FileAdapter
import io.legado.app.ui.filepicker.adapter.PathAdapter import io.legado.app.ui.filepicker.adapter.PathAdapter
@ -69,7 +71,19 @@ class FilePickerDialog : DialogFragment(),
override var isShowHomeDir: Boolean = false override var isShowHomeDir: Boolean = false
override var isShowUpDir: Boolean = true override var isShowUpDir: Boolean = true
override var isShowHideDir: Boolean = false 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 private var requestCode: Int = 0
var title: String? = null var title: String? = null
private var initPath = FileUtils.getSdCardPath() private var initPath = FileUtils.getSdCardPath()
@ -118,7 +132,7 @@ class FilePickerDialog : DialogFragment(),
} }
initMenu() initMenu()
initContentView() initContentView()
refreshCurrentDirPath(initPath) queryPermission.launch(Permissions.Group.STORAGE)
} }
private fun initMenu() { private fun initMenu() {

@ -8,16 +8,16 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.Permissions
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.databinding.DialogFontSelectBinding import io.legado.app.databinding.DialogFontSelectBinding
import io.legado.app.help.AppConfig 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.dialogs.alert
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.filepicker.FilePicker
@ -162,9 +162,13 @@ class FontSelectDialog : BaseDialogFragment(),
} }
private fun loadFontFilesByPermission(path: String) { private fun loadFontFilesByPermission(path: String) {
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { PermissionsCompat.Builder(this@FontSelectDialog)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
loadFontFiles(path) loadFontFiles(path)
}.launch(Permissions.Group.STORAGE) }
.request()
} }
private fun loadFontFiles(path: String) { private fun loadFontFiles(path: String) {

@ -0,0 +1 @@
emulator -avd %1 -dns-server 8.8.8.8 -no-snapshot-load

@ -1 +0,0 @@
emulator -avd android11 -dns-server 8.8.8.8 -no-snapshot-load

@ -1,9 +1,10 @@
package me.ag2s.epublib.browsersupport; 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.domain.Resource;
import me.ag2s.epublib.util.StringUtil; import me.ag2s.epublib.util.StringUtil;
import java.util.EventObject;
/** /**
* Used to tell NavigationEventListener just what kind of navigation action * Used to tell NavigationEventListener just what kind of navigation action
@ -19,7 +20,7 @@ public class NavigationEvent extends EventObject {
private Resource oldResource; private Resource oldResource;
private int oldSpinePos; private int oldSpinePos;
private Navigator navigator; private Navigator navigator;
private Book oldBook; private EpubBook oldBook;
private int oldSectionPos; private int oldSectionPos;
private String oldFragmentId; private String oldFragmentId;
@ -59,7 +60,7 @@ public class NavigationEvent extends EventObject {
this.oldFragmentId = oldFragmentId; this.oldFragmentId = oldFragmentId;
} }
public Book getOldBook() { public EpubBook getOldBook() {
return oldBook; return oldBook;
} }
@ -122,11 +123,11 @@ public class NavigationEvent extends EventObject {
} }
public void setOldBook(Book oldBook) { public void setOldBook(EpubBook oldBook) {
this.oldBook = oldBook; this.oldBook = oldBook;
} }
public Book getCurrentBook() { public EpubBook getCurrentBook() {
return getNavigator().getBook(); return getNavigator().getBook();
} }

@ -1,15 +1,15 @@
package me.ag2s.epublib.browsersupport; package me.ag2s.epublib.browsersupport;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Resource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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. * A history of the user's locations with the epub.
* *
* @author paul.siegmann * @author paul.siegmann
*
*/ */
public class NavigationHistory implements NavigationEventListener { public class NavigationHistory implements NavigationEventListener {
@ -58,7 +58,7 @@ public class NavigationHistory implements NavigationEventListener {
return currentSize; return currentSize;
} }
public void initBook(Book book) { public void initBook(EpubBook book) {
if (book == null) { if (book == null) {
return; return;
} }

@ -1,11 +1,12 @@
package me.ag2s.epublib.browsersupport; package me.ag2s.epublib.browsersupport;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Resource;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Resource;
/** /**
* A helper class for epub browser applications. * A helper class for epub browser applications.
* *
@ -18,7 +19,7 @@ import java.util.List;
public class Navigator implements Serializable { public class Navigator implements Serializable {
private static final long serialVersionUID = 1076126986424925474L; private static final long serialVersionUID = 1076126986424925474L;
private Book book; private EpubBook book;
private int currentSpinePos; private int currentSpinePos;
private Resource currentResource; private Resource currentResource;
private int currentPagePos; private int currentPagePos;
@ -30,7 +31,7 @@ public class Navigator implements Serializable {
this(null); this(null);
} }
public Navigator(Book book) { public Navigator(EpubBook book) {
this.book = book; this.book = book;
this.currentSpinePos = 0; this.currentSpinePos = 0;
if (book != null) { if (book != null) {
@ -158,7 +159,7 @@ public class Navigator implements Serializable {
return gotoSpineSection(book.getSpine().size() - 1, source); 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); NavigationEvent navigationEvent = new NavigationEvent(source, this);
this.book = book; this.book = book;
this.currentFragmentId = null; this.currentFragmentId = null;
@ -193,7 +194,7 @@ public class Navigator implements Serializable {
this.currentResource = book.getSpine().getResource(currentIndex); this.currentResource = book.getSpine().getResource(currentIndex);
} }
public Book getBook() { public EpubBook getBook() {
return book; return book;
} }

@ -39,7 +39,7 @@ import java.util.Map;
* @author paul * @author paul
* @author jake * @author jake
*/ */
public class Book implements Serializable { public class EpubBook implements Serializable {
private static final long serialVersionUID = 2068355170895770100L; private static final long serialVersionUID = 2068355170895770100L;

@ -1,6 +1,6 @@
package me.ag2s.epublib.epub; package me.ag2s.epublib.epub;
import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.EpubBook;
/** /**
* Post-processes a book. * Post-processes a book.
@ -16,5 +16,5 @@ public interface BookProcessor {
*/ */
BookProcessor IDENTITY_BOOKPROCESSOR = book -> book; BookProcessor IDENTITY_BOOKPROCESSOR = book -> book;
Book processBook(Book book); EpubBook processBook(EpubBook book);
} }

@ -2,15 +2,17 @@ package me.ag2s.epublib.epub;
import android.util.Log; import android.util.Log;
import me.ag2s.epublib.domain.Book;
//import io.documentnode.minilog.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
//import io.documentnode.minilog.Logger;
/** /**
* A book processor that combines several other bookprocessors * A book processor that combines several other bookprocessors
* * <p>
* Fixes coverpage/coverimage. * Fixes coverpage/coverimage.
* Cleans up the XHTML. * Cleans up the XHTML.
* *
@ -30,7 +32,7 @@ public class BookProcessorPipeline implements BookProcessor {
} }
@Override @Override
public Book processBook(Book book) { public EpubBook processBook(EpubBook book) {
if (bookProcessors == null) { if (bookProcessors == null) {
return book; return book;
} }
@ -38,7 +40,7 @@ public class BookProcessorPipeline implements BookProcessor {
try { try {
book = bookProcessor.processBook(book); book = bookProcessor.processBook(book);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG,e.getMessage(), e); Log.e(TAG, e.getMessage(), e);
} }
} }
return book; return book;

@ -2,15 +2,9 @@ package me.ag2s.epublib.epub;
import android.util.Log; import android.util.Log;
import me.ag2s.epublib.Constants; import org.w3c.dom.Document;
import me.ag2s.epublib.domain.Book; import org.w3c.dom.Element;
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 java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
@ -18,8 +12,16 @@ import java.util.List;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import org.w3c.dom.Document; import me.ag2s.epublib.Constants;
import org.w3c.dom.Element; 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. * Reads an epub file.
@ -32,15 +34,15 @@ public class EpubReader {
private static String TAG= EpubReader.class.getName(); private static String TAG= EpubReader.class.getName();
private BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR; 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); 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); 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); return readEpub(zipfile, Constants.CHARACTER_ENCODING);
} }
@ -52,7 +54,7 @@ public class EpubReader {
* @return the Book as read from the inputstream * @return the Book as read from the inputstream
* @throws IOException * @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); return readEpub(new ZipInputStream(in), encoding);
} }
@ -62,21 +64,20 @@ public class EpubReader {
* *
* @param zipFile the file to load * @param zipFile the file to load
* @param encoding the encoding for XHTML files * @param encoding the encoding for XHTML files
*
* @return this Book without loading all resources into memory. * @return this Book without loading all resources into memory.
* @throws IOException * @throws IOException
*/ */
public Book readEpubLazy(ZipFile zipFile, String encoding) public EpubBook readEpubLazy(ZipFile zipFile, String encoding)
throws IOException { throws IOException {
return readEpubLazy(zipFile, encoding, 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)); 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)); return readEpub(ResourcesLoader.loadResources(in, encoding));
} }
@ -89,20 +90,20 @@ public class EpubReader {
* @return this Book without loading all resources into memory. * @return this Book without loading all resources into memory.
* @throws IOException * @throws IOException
*/ */
public Book readEpubLazy(ZipFile zipFile, String encoding, public EpubBook readEpubLazy(ZipFile zipFile, String encoding,
List<MediaType> lazyLoadedTypes) throws IOException { List<MediaType> lazyLoadedTypes) throws IOException {
Resources resources = ResourcesLoader Resources resources = ResourcesLoader
.loadResources(zipFile, encoding, lazyLoadedTypes); .loadResources(zipFile, encoding, lazyLoadedTypes);
return readEpub(resources); return readEpub(resources);
} }
public Book readEpub(Resources resources) throws IOException { public EpubBook readEpub(Resources resources) throws IOException {
return readEpub(resources, new Book()); 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) { if (result == null) {
result = new Book(); result = new EpubBook();
} }
handleMimeType(result, resources); handleMimeType(result, resources);
String packageResourceHref = getPackageResourceHref(resources); String packageResourceHref = getPackageResourceHref(resources);
@ -116,20 +117,20 @@ public class EpubReader {
} }
private Book postProcessBook(Book book) { private EpubBook postProcessBook(EpubBook book) {
if (bookProcessor != null) { if (bookProcessor != null) {
book = bookProcessor.processBook(book); book = bookProcessor.processBook(book);
} }
return book; return book;
} }
private Resource processNcxResource(Resource packageResource, Book book) { private Resource processNcxResource(Resource packageResource, EpubBook book) {
Log.d(TAG,"OPF:getHref()"+packageResource.getHref()); Log.d(TAG, "OPF:getHref()" + packageResource.getHref());
return NCXDocument.read(book, this); return NCXDocument.read(book, this);
} }
private Resource processPackageResource(String packageResourceHref, Book book, private Resource processPackageResource(String packageResourceHref, EpubBook book,
Resources resources) { Resources resources) {
Resource packageResource = resources.remove(packageResourceHref); Resource packageResource = resources.remove(packageResourceHref);
try { try {
@ -163,7 +164,7 @@ public class EpubReader {
return result; return result;
} }
private void handleMimeType(Book result, Resources resources) { private void handleMimeType(EpubBook result, Resources resources) {
resources.remove("mimetype"); resources.remove("mimetype");
} }
} }

@ -2,11 +2,8 @@ package me.ag2s.epublib.epub;
import android.util.Log; import android.util.Log;
import me.ag2s.epublib.domain.Book; import org.xmlpull.v1.XmlSerializer;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.util.IOUtil;
//import io.documentnode.minilog.Logger;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -15,7 +12,13 @@ import java.io.Writer;
import java.util.zip.CRC32; import java.util.zip.CRC32;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; 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. * 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); book = processBook(book);
ZipOutputStream resultStream = new ZipOutputStream(out); ZipOutputStream resultStream = new ZipOutputStream(out);
writeMimeType(resultStream); writeMimeType(resultStream);
@ -52,14 +55,14 @@ public class EpubWriter {
resultStream.close(); resultStream.close();
} }
private Book processBook(Book book) { private EpubBook processBook(EpubBook book) {
if (bookProcessor != null) { if (bookProcessor != null) {
book = bookProcessor.processBook(book); book = bookProcessor.processBook(book);
} }
return book; return book;
} }
private void initTOCResource(Book book) { private void initTOCResource(EpubBook book) {
Resource tocResource; Resource tocResource;
try { try {
tocResource = NCXDocument.createNCXResource(book); tocResource = NCXDocument.createNCXResource(book);
@ -77,7 +80,7 @@ public class EpubWriter {
} }
private void writeResources(Book book, ZipOutputStream resultStream) private void writeResources(EpubBook book, ZipOutputStream resultStream)
throws IOException { throws IOException {
for (Resource resource : book.getResources().getAll()) { for (Resource resource : book.getResources().getAll()) {
writeResource(resource, resultStream); writeResource(resource, resultStream);
@ -107,7 +110,7 @@ public class EpubWriter {
} }
private void writePackageDocument(Book book, ZipOutputStream resultStream) private void writePackageDocument(EpubBook book, ZipOutputStream resultStream)
throws IOException { throws IOException {
resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf")); resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf"));
XmlSerializer xmlSerializer = EpubProcessorSupport XmlSerializer xmlSerializer = EpubProcessorSupport

@ -2,31 +2,30 @@ package me.ag2s.epublib.epub;
import android.util.Log; 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.Constants;
import me.ag2s.epublib.domain.Author; 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.Identifier;
import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.TOCReference; import me.ag2s.epublib.domain.TOCReference;
import me.ag2s.epublib.domain.TableOfContents; import me.ag2s.epublib.domain.TableOfContents;
import me.ag2s.epublib.util.ResourceUtil;
import me.ag2s.epublib.util.StringUtil; import me.ag2s.epublib.util.StringUtil;
//import io.documentnode.minilog.Logger; //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 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/ * 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()); Log.d(TAG, book.getVersion());
String version = book.getVersion(); String version = book.getVersion();
if (version.startsWith("2.")) { if (version.startsWith("2.")) {
@ -89,28 +88,28 @@ public class NCXDocument {
} }
private static List<TOCReference> readTOCReferences(NodeList navpoints, private static List<TOCReference> readTOCReferences(NodeList navpoints,
Book book) { EpubBook book) {
Log.d(TAG, book.getVersion()); Log.d(TAG, book.getVersion());
String version = book.getVersion(); String version = book.getVersion();
if (version.startsWith("2.")) { if (version.startsWith("2.")) {
return NCXDocumentV2.readTOCReferences(navpoints,book); return NCXDocumentV2.readTOCReferences(navpoints, book);
} else if (version.startsWith("3.")) { } else if (version.startsWith("3.")) {
return NCXDocumentV3.readTOCReferences(navpoints,book); return NCXDocumentV3.readTOCReferences(navpoints, book);
} else { } 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()); Log.d(TAG, book.getVersion());
String version = book.getVersion(); String version = book.getVersion();
if (version.startsWith("2.")) { if (version.startsWith("2.")) {
return NCXDocumentV2.readTOCReference(navpointElement,book); return NCXDocumentV2.readTOCReference(navpointElement, book);
} else if (version.startsWith("3.")) { } else if (version.startsWith("3.")) {
return NCXDocumentV3.readTOCReference(navpointElement,book); return NCXDocumentV3.readTOCReference(navpointElement, book);
} else { } 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 { ZipOutputStream resultStream) throws IOException {
resultStream resultStream
.putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref())); .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref()));
@ -158,13 +157,13 @@ public class NCXDocument {
* @throws IllegalArgumentException * @throws IllegalArgumentException
* @1throws FactoryConfigurationError * @1throws FactoryConfigurationError
*/ */
public static void write(XmlSerializer xmlSerializer, Book book) public static void write(XmlSerializer xmlSerializer, EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
book.getMetadata().getAuthors(), book.getTableOfContents()); book.getMetadata().getAuthors(), book.getTableOfContents());
} }
public static Resource createNCXResource(Book book) public static Resource createNCXResource(EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
return createNCXResource(book.getMetadata().getIdentifiers(), return createNCXResource(book.getMetadata().getIdentifiers(),
book.getTitle(), book.getMetadata().getAuthors(), book.getTitle(), book.getMetadata().getAuthors(),

@ -19,7 +19,7 @@ import java.util.zip.ZipOutputStream;
import me.ag2s.epublib.Constants; import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Author; 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.Identifier;
import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource; 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; Resource ncxResource = null;
if (book.getSpine().getTocResource() == 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; return ncxResource;
} }
try { try {
@ -102,7 +102,7 @@ public class NCXDocumentV2 extends NCXDocument{
} }
static List<TOCReference> readTOCReferences(NodeList navpoints, static List<TOCReference> readTOCReferences(NodeList navpoints,
Book book) { EpubBook book) {
if (navpoints == null) { if (navpoints == null) {
return new ArrayList<>(); return new ArrayList<>();
} }
@ -122,7 +122,7 @@ public class NCXDocumentV2 extends NCXDocument{
return result; return result;
} }
static TOCReference readTOCReference(Element navpointElement, Book book) { static TOCReference readTOCReference(Element navpointElement, EpubBook book) {
String label = readNavLabel(navpointElement); String label = readNavLabel(navpointElement);
//Log.d(TAG,"label:"+label); //Log.d(TAG,"label:"+label);
String tocResourceRoot = StringUtil String tocResourceRoot = StringUtil
@ -174,7 +174,7 @@ public class NCXDocumentV2 extends NCXDocument{
} }
public static void write(EpubWriter epubWriter, Book book, public static void write(EpubWriter epubWriter, EpubBook book,
ZipOutputStream resultStream) throws IOException { ZipOutputStream resultStream) throws IOException {
resultStream resultStream
.putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref())); .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref()));
@ -195,13 +195,13 @@ public class NCXDocumentV2 extends NCXDocument{
* @throws IllegalStateException * @throws IllegalStateException
* @throws IllegalArgumentException * @throws IllegalArgumentException
*/ */
public static void write(XmlSerializer xmlSerializer, Book book) public static void write(XmlSerializer xmlSerializer, EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
book.getMetadata().getAuthors(), book.getTableOfContents()); book.getMetadata().getAuthors(), book.getTableOfContents());
} }
public static Resource createNCXResource(Book book) public static Resource createNCXResource(EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
return createNCXResource(book.getMetadata().getIdentifiers(), return createNCXResource(book.getMetadata().getIdentifiers(),
book.getTitle(), book.getMetadata().getAuthors(), book.getTitle(), book.getMetadata().getAuthors(),

@ -6,22 +6,14 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xmlpull.v1.XmlSerializer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import me.ag2s.epublib.Constants; import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.TOCReference; import me.ag2s.epublib.domain.TOCReference;
import me.ag2s.epublib.domain.TableOfContents; 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; Resource ncxResource = null;
if (book.getSpine().getTocResource() == 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");
@ -99,7 +91,7 @@ public class NCXDocumentV3 extends NCXDocument {
return ncxResource; return ncxResource;
} }
public static List<TOCReference> doToc(Node n, Book book) { public static List<TOCReference> doToc(Node n, EpubBook book) {
List<TOCReference> result = new ArrayList<>(); List<TOCReference> result = new ArrayList<>();
if (n == null || n.getNodeType() != Document.ELEMENT_NODE) { if (n == null || n.getNodeType() != Document.ELEMENT_NODE) {
@ -118,7 +110,7 @@ public class NCXDocumentV3 extends NCXDocument {
static List<TOCReference> readTOCReferences(NodeList navpoints, static List<TOCReference> readTOCReferences(NodeList navpoints,
Book book) { EpubBook book) {
if (navpoints == null) { if (navpoints == null) {
return new ArrayList<>(); 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); String label = readNavLabel(navpointElement);
//Log.d(TAG, "label:" + label); //Log.d(TAG, "label:" + label);
String tocResourceRoot = StringUtil String tocResourceRoot = StringUtil

@ -1,16 +1,19 @@
package me.ag2s.epublib.epub; 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.Constants;
import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.Author;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Date; import me.ag2s.epublib.domain.Date;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.util.StringUtil; 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 { public class PackageDocumentMetadataWriter extends PackageDocumentBase {
@ -23,7 +26,7 @@ public class PackageDocumentMetadataWriter extends PackageDocumentBase {
* @throws IllegalStateException * @throws IllegalStateException
* @throws IllegalArgumentException * @throws IllegalArgumentException
*/ */
public static void writeMetaData(Book book, XmlSerializer serializer) public static void writeMetaData(EpubBook book, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.metadata); serializer.startTag(NAMESPACE_OPF, OPFTags.metadata);
serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE); serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);

@ -1,21 +1,12 @@
package me.ag2s.epublib.epub; package me.ag2s.epublib.epub;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
import me.ag2s.epublib.Constants; import org.w3c.dom.Document;
import me.ag2s.epublib.domain.Book; import org.w3c.dom.Element;
import me.ag2s.epublib.domain.Guide; import org.w3c.dom.NodeList;
import me.ag2s.epublib.domain.GuideReference; import org.xml.sax.SAXException;
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 java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
@ -27,11 +18,23 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element; import me.ag2s.epublib.Constants;
import org.w3c.dom.NodeList; import me.ag2s.epublib.domain.EpubBook;
import org.xml.sax.SAXException; 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 * Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf
@ -47,7 +50,7 @@ public class PackageDocumentReader extends PackageDocumentBase {
public static void read( public static void read(
Resource packageResource, EpubReader epubReader, Book book, Resource packageResource, EpubReader epubReader, EpubBook book,
Resources resources) Resources resources)
throws UnsupportedEncodingException, SAXException, IOException, ParserConfigurationException { throws UnsupportedEncodingException, SAXException, IOException, ParserConfigurationException {
Document packageDocument = ResourceUtil.getAsDocument(packageResource); Document packageDocument = ResourceUtil.getAsDocument(packageResource);
@ -151,7 +154,7 @@ public class PackageDocumentReader extends PackageDocumentBase {
* @param resources * @param resources
*/ */
private static void readGuide(Document packageDocument, private static void readGuide(Document packageDocument,
EpubReader epubReader, Book book, Resources resources) { EpubReader epubReader, EpubBook book, Resources resources) {
Element guideElement = DOMUtil Element guideElement = DOMUtil
.getFirstElementByTagNameNS(packageDocument.getDocumentElement(), .getFirstElementByTagNameNS(packageDocument.getDocumentElement(),
NAMESPACE_OPF, OPFTags.guide); NAMESPACE_OPF, OPFTags.guide);
@ -400,17 +403,18 @@ public class PackageDocumentReader extends PackageDocumentBase {
/** /**
* Finds the cover resource in the packageDocument and adds it to the book if found. * Finds the cover resource in the packageDocument and adds it to the book if found.
* Keeps the cover resource in the resources map * Keeps the cover resource in the resources map
*
* @param packageDocument * @param packageDocument
* @param book * @param book
* @1param resources * @1param resources
*/ */
private static void readCover(Document packageDocument, Book book) { private static void readCover(Document packageDocument, EpubBook book) {
Collection<String> coverHrefs = findCoverHrefs(packageDocument); Collection<String> coverHrefs = findCoverHrefs(packageDocument);
for (String coverHref : coverHrefs) { for (String coverHref : coverHrefs) {
Resource resource = book.getResources().getByHref(coverHref); Resource resource = book.getResources().getByHref(coverHref);
if (resource == null) { if (resource == null) {
Log.e(TAG,"Cover resource " + coverHref + " not found"); Log.e(TAG, "Cover resource " + coverHref + " not found");
continue; continue;
} }
if (resource.getMediaType() == MediaTypes.XHTML) { if (resource.getMediaType() == MediaTypes.XHTML) {

@ -2,24 +2,26 @@ package me.ag2s.epublib.epub;
import android.util.Log; 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.Constants;
import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Guide; import me.ag2s.epublib.domain.Guide;
import me.ag2s.epublib.domain.GuideReference; import me.ag2s.epublib.domain.GuideReference;
import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.Spine; import me.ag2s.epublib.domain.Spine;
import me.ag2s.epublib.domain.SpineReference; import me.ag2s.epublib.domain.SpineReference;
import me.ag2s.epublib.util.ResourceUtil;
import me.ag2s.epublib.util.StringUtil; import me.ag2s.epublib.util.StringUtil;
//import io.documentnode.minilog.Logger; //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 javax.xml.stream.XMLStreamException;
import org.xmlpull.v1.XmlSerializer;
/** /**
* Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf * Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf
@ -33,7 +35,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
private static String TAG= PackageDocumentWriter.class.getName(); private static String TAG= PackageDocumentWriter.class.getName();
public static void write(EpubWriter epubWriter, XmlSerializer serializer, public static void write(EpubWriter epubWriter, XmlSerializer serializer,
Book book) throws IOException { EpubBook book) throws IOException {
try { try {
serializer.startDocument(Constants.CHARACTER_ENCODING, false); serializer.startDocument(Constants.CHARACTER_ENCODING, false);
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
@ -71,7 +73,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
* @throws IllegalArgumentException * @throws IllegalArgumentException
* 1@throws XMLStreamException * 1@throws XMLStreamException
*/ */
private static void writeSpine(Book book, EpubWriter epubWriter, private static void writeSpine(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer) XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.spine); serializer.startTag(NAMESPACE_OPF, OPFTags.spine);
@ -98,7 +100,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
} }
private static void writeManifest(Book book, EpubWriter epubWriter, private static void writeManifest(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer) XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.manifest); serializer.startTag(NAMESPACE_OPF, OPFTags.manifest);
@ -122,7 +124,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
serializer.endTag(NAMESPACE_OPF, OPFTags.manifest); serializer.endTag(NAMESPACE_OPF, OPFTags.manifest);
} }
private static List<Resource> getAllResourcesSortById(Book book) { private static List<Resource> getAllResourcesSortById(EpubBook book) {
List<Resource> allResources = new ArrayList<Resource>( List<Resource> allResources = new ArrayList<Resource>(
book.getResources().getAll()); book.getResources().getAll());
Collections.sort(allResources, new Comparator<Resource>() { Collections.sort(allResources, new Comparator<Resource>() {
@ -137,14 +139,14 @@ public class PackageDocumentWriter extends PackageDocumentBase {
/** /**
* Writes a resources as an item element * Writes a resources as an item element
*
* @param resource * @param resource
* @param serializer * @param serializer
* @throws IOException * @throws IOException
* @throws IllegalStateException * @throws IllegalStateException
* @throws IllegalArgumentException * @throws IllegalArgumentException 1@throws XMLStreamException
* 1@throws XMLStreamException
*/ */
private static void writeItem(Book book, Resource resource, private static void writeItem(EpubBook book, Resource resource,
XmlSerializer serializer) XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
if (resource == null || if (resource == null ||
@ -155,7 +157,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
if (StringUtil.isBlank(resource.getId())) { if (StringUtil.isBlank(resource.getId())) {
// log.error("resource id must not be empty (href: " + resource.getHref() // log.error("resource id must not be empty (href: " + resource.getHref()
// + ", mediatype:" + resource.getMediaType() + ")"); // + ", 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() + ")"); + ", mediatype:" + resource.getMediaType() + ")");
return; return;
} }
@ -206,7 +208,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
} }
} }
private static void writeGuide(Book book, EpubWriter epubWriter, private static void writeGuide(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer) XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.guide); serializer.startTag(NAMESPACE_OPF, OPFTags.guide);

@ -1,2 +1 @@
include ':app' include ':app',':epublib'
include ':epublib'

Loading…
Cancel
Save