parent
031c3ccc98
commit
ac07c5d670
Binary file not shown.
@ -0,0 +1,23 @@ |
||||
//Copyright (c) 2017. 章钦豪. All rights reserved.
|
||||
package xyz.fycz.myreader.base.observer; |
||||
|
||||
import io.reactivex.Observer; |
||||
import io.reactivex.disposables.Disposable; |
||||
|
||||
public abstract class MyObserver<T> implements Observer<T> { |
||||
|
||||
@Override |
||||
public void onSubscribe(Disposable d) { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void onError(Throwable e) { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void onComplete() { |
||||
|
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
package xyz.fycz.myreader.base.observer; |
||||
|
||||
import io.reactivex.SingleObserver; |
||||
import io.reactivex.disposables.Disposable; |
||||
|
||||
public abstract class MySingleObserver<T> implements SingleObserver<T> { |
||||
|
||||
@Override |
||||
public void onSubscribe(Disposable d) { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void onError(Throwable e) { |
||||
|
||||
} |
||||
} |
@ -1,11 +0,0 @@ |
||||
package xyz.fycz.myreader.common; |
||||
|
||||
|
||||
|
||||
public interface Common { |
||||
|
||||
int SUCCESS = 1; |
||||
int INSUCCESS = 0; |
||||
int LOGINEXIT = 2; |
||||
int MESSAGE_HINT = 3; |
||||
} |
@ -0,0 +1,178 @@ |
||||
package xyz.fycz.myreader.model.storage |
||||
|
||||
import android.content.Context |
||||
import android.net.Uri |
||||
import androidx.documentfile.provider.DocumentFile |
||||
|
||||
import io.reactivex.Single |
||||
import io.reactivex.SingleOnSubscribe |
||||
import io.reactivex.android.schedulers.AndroidSchedulers |
||||
import io.reactivex.schedulers.Schedulers |
||||
import xyz.fycz.myreader.application.MyApplication |
||||
import xyz.fycz.myreader.application.SysManager |
||||
import xyz.fycz.myreader.base.observer.MySingleObserver |
||||
import xyz.fycz.myreader.common.APPCONST |
||||
import xyz.fycz.myreader.greendao.GreenDaoManager |
||||
import xyz.fycz.myreader.greendao.service.BookMarkService |
||||
import xyz.fycz.myreader.greendao.service.BookService |
||||
import xyz.fycz.myreader.greendao.service.SearchHistoryService |
||||
import xyz.fycz.myreader.util.SharedPreUtils |
||||
import xyz.fycz.myreader.util.utils.DocumentUtil |
||||
import xyz.fycz.myreader.util.utils.FileUtils |
||||
import xyz.fycz.myreader.util.utils.GSON |
||||
import java.io.File |
||||
import java.util.concurrent.TimeUnit |
||||
|
||||
|
||||
object Backup { |
||||
|
||||
val backupPath = MyApplication.getApplication().filesDir.absolutePath + File.separator + "backup" |
||||
|
||||
val defaultPath by lazy { |
||||
APPCONST.BACKUP_FILE_DIR |
||||
} |
||||
|
||||
val backupFileNames by lazy { |
||||
arrayOf( |
||||
"myBooks.json", |
||||
"mySearchHistory.json", |
||||
"myBookMark.json", |
||||
"myBookGroup.json", |
||||
"setting.json", |
||||
"config.xml" |
||||
) |
||||
} |
||||
|
||||
fun autoBack() { |
||||
val lastBackup = SharedPreUtils.getInstance().getLong("lastBackup", 0) |
||||
if (System.currentTimeMillis() - lastBackup < TimeUnit.DAYS.toMillis(1)) { |
||||
return |
||||
} |
||||
val path = SharedPreUtils.getInstance().getString("backupPath", defaultPath) |
||||
if (path == null) { |
||||
backup(MyApplication.getmContext(), defaultPath, null, true) |
||||
} else { |
||||
backup(MyApplication.getmContext(), path, null, true) |
||||
} |
||||
} |
||||
|
||||
fun backup(context: Context, path: String, callBack: CallBack?, isAuto: Boolean = false) { |
||||
SharedPreUtils.getInstance().putLong("lastBackup", System.currentTimeMillis()) |
||||
Single.create(SingleOnSubscribe<Boolean> { e -> |
||||
BookService.getInstance().allBooks.let { |
||||
if (it.isNotEmpty()) { |
||||
val json = GSON.toJson(it) |
||||
FileUtils.getFile(backupPath + File.separator + "myBooks.json").writeText(json) |
||||
} |
||||
} |
||||
SearchHistoryService.getInstance().findAllSearchHistory().let { |
||||
if (it.isNotEmpty()) { |
||||
val json = GSON.toJson(it) |
||||
FileUtils.getFile(backupPath + File.separator + "mySearchHistory.json") |
||||
.writeText(json) |
||||
} |
||||
} |
||||
GreenDaoManager.getInstance().session.bookMarkDao.queryBuilder().list().let { |
||||
if (it.isNotEmpty()) { |
||||
val json = GSON.toJson(it) |
||||
FileUtils.getFile(backupPath + File.separator + "myBookMark.json") |
||||
.writeText(json) |
||||
} |
||||
} |
||||
GreenDaoManager.getInstance().session.bookGroupDao.queryBuilder().list().let { |
||||
if (it.isNotEmpty()) { |
||||
val json = GSON.toJson(it) |
||||
FileUtils.getFile(backupPath + File.separator + "myBookGroup.json") |
||||
.writeText(json) |
||||
} |
||||
} |
||||
val json = GSON.toJson(SysManager.getSetting()) |
||||
FileUtils.getFile(backupPath + File.separator + "setting.json") |
||||
.writeText(json) |
||||
Preferences.getSharedPreferences(context, backupPath, "config")?.let { sp -> |
||||
val edit = sp.edit() |
||||
SharedPreUtils.getInstance().all.map { |
||||
when (val value = it.value) { |
||||
is Int -> edit.putInt(it.key, value) |
||||
is Boolean -> edit.putBoolean(it.key, value) |
||||
is Long -> edit.putLong(it.key, value) |
||||
is Float -> edit.putFloat(it.key, value) |
||||
is String -> edit.putString(it.key, value) |
||||
else -> Unit |
||||
} |
||||
} |
||||
edit.commit() |
||||
} |
||||
WebDavHelp.backUpWebDav(backupPath) |
||||
if (path.isContentPath()) { |
||||
copyBackup(context, Uri.parse(path), isAuto) |
||||
} else { |
||||
copyBackup(path, isAuto) |
||||
} |
||||
e.onSuccess(true) |
||||
}).subscribeOn(Schedulers.io()) |
||||
.observeOn(AndroidSchedulers.mainThread()) |
||||
.subscribe(object : MySingleObserver<Boolean>() { |
||||
override fun onSuccess(t: Boolean) { |
||||
callBack?.backupSuccess() |
||||
} |
||||
|
||||
override fun onError(e: Throwable) { |
||||
e.printStackTrace() |
||||
callBack?.backupError(e.localizedMessage ?: "ERROR") |
||||
} |
||||
}) |
||||
} |
||||
|
||||
@Throws(Exception::class) |
||||
private fun copyBackup(context: Context, uri: Uri, isAuto: Boolean) { |
||||
synchronized(this) { |
||||
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc -> |
||||
for (fileName in backupFileNames) { |
||||
val file = File(backupPath + File.separator + fileName) |
||||
if (file.exists()) { |
||||
if (isAuto) { |
||||
treeDoc.findFile("auto")?.findFile(fileName)?.delete() |
||||
var autoDoc = treeDoc.findFile("auto") |
||||
if (autoDoc == null) { |
||||
autoDoc = treeDoc.createDirectory("auto") |
||||
} |
||||
autoDoc?.createFile("", fileName)?.let { |
||||
DocumentUtil.writeBytes(context, file.readBytes(), it) |
||||
} |
||||
} else { |
||||
treeDoc.findFile(fileName)?.delete() |
||||
treeDoc.createFile("", fileName)?.let { |
||||
DocumentUtil.writeBytes(context, file.readBytes(), it) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Throws(java.lang.Exception::class) |
||||
private fun copyBackup(path: String, isAuto: Boolean) { |
||||
synchronized(this) { |
||||
for (fileName in backupFileNames) { |
||||
if (isAuto) { |
||||
val file = File(backupPath + File.separator + fileName) |
||||
if (file.exists()) { |
||||
file.copyTo(FileUtils.getFile(path + File.separator + "auto" + File.separator + fileName), true) |
||||
} |
||||
} else { |
||||
val file = File(backupPath + File.separator + fileName) |
||||
if (file.exists()) { |
||||
file.copyTo(FileUtils.getFile(path + File.separator + fileName), true) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
interface CallBack { |
||||
fun backupSuccess() |
||||
fun backupError(msg: String) |
||||
} |
||||
} |
@ -0,0 +1,239 @@ |
||||
package xyz.fycz.myreader.model.storage |
||||
|
||||
import android.app.Activity |
||||
import android.app.Activity.RESULT_OK |
||||
import android.content.Intent |
||||
import android.net.Uri |
||||
import android.text.TextUtils |
||||
import androidx.core.content.ContextCompat |
||||
import androidx.documentfile.provider.DocumentFile |
||||
import io.reactivex.Single |
||||
import io.reactivex.SingleEmitter |
||||
import io.reactivex.android.schedulers.AndroidSchedulers |
||||
import io.reactivex.schedulers.Schedulers |
||||
import org.jetbrains.anko.alert |
||||
import org.jetbrains.anko.toast |
||||
import xyz.fycz.myreader.base.observer.MySingleObserver |
||||
import xyz.fycz.myreader.common.APPCONST |
||||
import xyz.fycz.myreader.model.storage.WebDavHelp.getWebDavFileNames |
||||
import xyz.fycz.myreader.model.storage.WebDavHelp.showRestoreDialog |
||||
import xyz.fycz.myreader.util.SharedPreUtils |
||||
import xyz.fycz.myreader.util.ToastUtils |
||||
import java.util.* |
||||
|
||||
object BackupRestoreUi : Backup.CallBack, Restore.CallBack { |
||||
|
||||
private const val backupSelectRequestCode = 22 |
||||
private const val restoreSelectRequestCode = 33 |
||||
|
||||
private fun getBackupPath(): String? { |
||||
return SharedPreUtils.getInstance().getString("backupPath", APPCONST.BACKUP_FILE_DIR) |
||||
} |
||||
|
||||
private fun setBackupPath(path: String?) { |
||||
if (path.isNullOrEmpty()) { |
||||
SharedPreUtils.getInstance().remove("backupPath") |
||||
} else { |
||||
SharedPreUtils.getInstance().putString("backupPath", path) |
||||
} |
||||
} |
||||
|
||||
override fun backupSuccess() { |
||||
ToastUtils.showSuccess("备份成功") |
||||
} |
||||
|
||||
override fun backupError(msg: String) { |
||||
ToastUtils.showError(msg) |
||||
} |
||||
|
||||
override fun restoreSuccess() { |
||||
ToastUtils.showSuccess("恢复成功") |
||||
} |
||||
|
||||
override fun restoreError(msg: String) { |
||||
ToastUtils.showError(msg) |
||||
} |
||||
|
||||
fun backup(activity: Activity) { |
||||
val backupPath = getBackupPath() |
||||
if (backupPath.isNullOrEmpty()) { |
||||
// selectBackupFolder(activity) |
||||
ToastUtils.showError("backupPath.isNullOrEmpty") |
||||
} else { |
||||
if (backupPath.isContentPath()) { |
||||
val uri = Uri.parse(backupPath) |
||||
val doc = DocumentFile.fromTreeUri(activity, uri) |
||||
if (doc?.canWrite() == true) { |
||||
Backup.backup(activity, backupPath, this) |
||||
} else { |
||||
// selectBackupFolder(activity) |
||||
ToastUtils.showError("doc?.canWrite() != true") |
||||
} |
||||
} else { |
||||
// backupUsePermission(activity) |
||||
ToastUtils.showError("backupPath.isNotContentPath") |
||||
} |
||||
} |
||||
} |
||||
|
||||
/*private fun backupUsePermission(activity: Activity, path: String = Backup.defaultPath) { |
||||
PermissionsCompat.Builder(activity) |
||||
.addPermissions(*Permissions.Group.STORAGE) |
||||
.rationale(R.string.get_storage_per) |
||||
.onGranted { |
||||
setBackupPath(path) |
||||
Backup.backup(activity, path, this) |
||||
} |
||||
.request() |
||||
} |
||||
|
||||
fun selectBackupFolder(activity: Activity) { |
||||
activity.alert { |
||||
titleResource = R.string.select_folder |
||||
items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index -> |
||||
when (index) { |
||||
0 -> { |
||||
setBackupPath(Backup.defaultPath) |
||||
backupUsePermission(activity) |
||||
} |
||||
1 -> { |
||||
try { |
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) |
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) |
||||
activity.startActivityForResult(intent, backupSelectRequestCode) |
||||
} catch (e: java.lang.Exception) { |
||||
e.printStackTrace() |
||||
activity.toast(e.localizedMessage ?: "ERROR") |
||||
} |
||||
} |
||||
2 -> { |
||||
PermissionsCompat.Builder(activity) |
||||
.addPermissions(*Permissions.Group.STORAGE) |
||||
.rationale(R.string.get_storage_per) |
||||
.onGranted { |
||||
selectBackupFolderApp(activity, false) |
||||
} |
||||
.request() |
||||
} |
||||
} |
||||
} |
||||
}.show() |
||||
} |
||||
|
||||
private fun selectBackupFolderApp(activity: Activity, isRestore: Boolean) { |
||||
val picker = FilePicker(activity, FilePicker.DIRECTORY) |
||||
picker.setBackgroundColor(ContextCompat.getColor(activity, R.color.background)) |
||||
picker.setTopBackgroundColor(ContextCompat.getColor(activity, R.color.background)) |
||||
picker.setItemHeight(30) |
||||
picker.setOnFilePickListener { currentPath: String -> |
||||
setBackupPath(currentPath) |
||||
if (isRestore) { |
||||
Restore.restore(currentPath, this) |
||||
} else { |
||||
Backup.backup(activity, currentPath, this) |
||||
} |
||||
} |
||||
picker.show() |
||||
}*/ |
||||
|
||||
fun restore(activity: Activity) { |
||||
Single.create { emitter: SingleEmitter<ArrayList<String>?> -> |
||||
emitter.onSuccess(getWebDavFileNames()) |
||||
}.subscribeOn(Schedulers.io()) |
||||
.observeOn(AndroidSchedulers.mainThread()) |
||||
.subscribe(object : MySingleObserver<ArrayList<String>?>() { |
||||
override fun onSuccess(strings: ArrayList<String>) { |
||||
if (!showRestoreDialog(activity, strings, this@BackupRestoreUi)) { |
||||
val path = getBackupPath() |
||||
if (TextUtils.isEmpty(path)) { |
||||
//selectRestoreFolder(activity) |
||||
ToastUtils.showError("TextUtils.isEmpty(path)") |
||||
} else { |
||||
if (path.isContentPath()) { |
||||
val uri = Uri.parse(path) |
||||
val doc = DocumentFile.fromTreeUri(activity, uri) |
||||
if (doc?.canWrite() == true) { |
||||
Restore.restore(activity, Uri.parse(path), this@BackupRestoreUi) |
||||
} else { |
||||
// selectRestoreFolder(activity) |
||||
ToastUtils.showError("doc?.canWrite() != true") |
||||
} |
||||
} else { |
||||
// restoreUsePermission(activity) |
||||
ToastUtils.showError("path.isNotContentPath") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
/*private fun restoreUsePermission(activity: Activity, path: String = Backup.defaultPath) { |
||||
PermissionsCompat.Builder(activity) |
||||
.addPermissions(*Permissions.Group.STORAGE) |
||||
.rationale(R.string.get_storage_per) |
||||
.onGranted { |
||||
setBackupPath(path) |
||||
Restore.restore(path, this) |
||||
} |
||||
.request() |
||||
} |
||||
|
||||
private fun selectRestoreFolder(activity: Activity) { |
||||
activity.alert { |
||||
titleResource = R.string.select_folder |
||||
items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index -> |
||||
when (index) { |
||||
0 -> restoreUsePermission(activity) |
||||
1 -> { |
||||
try { |
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) |
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) |
||||
activity.startActivityForResult(intent, restoreSelectRequestCode) |
||||
} catch (e: java.lang.Exception) { |
||||
e.printStackTrace() |
||||
activity.toast(e.localizedMessage ?: "ERROR") |
||||
} |
||||
} |
||||
2 -> { |
||||
PermissionsCompat.Builder(activity) |
||||
.addPermissions(*Permissions.Group.STORAGE) |
||||
.rationale(R.string.get_storage_per) |
||||
.onGranted { |
||||
selectBackupFolderApp(activity, true) |
||||
} |
||||
.request() |
||||
} |
||||
} |
||||
} |
||||
}.show() |
||||
}*/ |
||||
|
||||
/*fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { |
||||
when (requestCode) { |
||||
backupSelectRequestCode -> if (resultCode == RESULT_OK) { |
||||
data?.data?.let { uri -> |
||||
MApplication.getInstance().contentResolver.takePersistableUriPermission( |
||||
uri, |
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
||||
) |
||||
setBackupPath(uri.toString()) |
||||
Backup.backup(MApplication.getInstance(), uri.toString(), this) |
||||
} |
||||
} |
||||
restoreSelectRequestCode -> if (resultCode == RESULT_OK) { |
||||
data?.data?.let { uri -> |
||||
MApplication.getInstance().contentResolver.takePersistableUriPermission( |
||||
uri, |
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
||||
) |
||||
setBackupPath(uri.toString()) |
||||
Restore.restore(MApplication.getInstance(), uri, this) |
||||
} |
||||
} |
||||
} |
||||
}*/ |
||||
|
||||
} |
||||
|
||||
fun String?.isContentPath(): Boolean = this?.startsWith("content://") == true |
@ -0,0 +1,48 @@ |
||||
package xyz.fycz.myreader.model.storage |
||||
|
||||
import android.app.Activity |
||||
import android.content.Context |
||||
import android.content.ContextWrapper |
||||
import android.content.SharedPreferences |
||||
import java.io.File |
||||
|
||||
object Preferences { |
||||
|
||||
/** |
||||
* 用反射生成 SharedPreferences |
||||
* @param context |
||||
* @param dir |
||||
* @param fileName 文件名,不需要 '.xml' 后缀 |
||||
* @return |
||||
*/ |
||||
fun getSharedPreferences( |
||||
context: Context, |
||||
dir: String, |
||||
fileName: String |
||||
): SharedPreferences? { |
||||
try { |
||||
// 获取 ContextWrapper对象中的mBase变量。该变量保存了 ContextImpl 对象 |
||||
val fieldMBase = ContextWrapper::class.java.getDeclaredField("mBase") |
||||
fieldMBase.isAccessible = true |
||||
// 获取 mBase变量 |
||||
val objMBase = fieldMBase.get(context) |
||||
// 获取 ContextImpl.mPreferencesDir变量,该变量保存了数据文件的保存路径 |
||||
val fieldMPreferencesDir = objMBase.javaClass.getDeclaredField("mPreferencesDir") |
||||
fieldMPreferencesDir.isAccessible = true |
||||
// 创建自定义路径 |
||||
// String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+"/Android"; |
||||
val file = File(dir) |
||||
// 修改mPreferencesDir变量的值 |
||||
fieldMPreferencesDir.set(objMBase, file) |
||||
// 返回修改路径以后的 SharedPreferences :%FILE_PATH%/%fileName%.xml |
||||
return context.getSharedPreferences(fileName, Activity.MODE_PRIVATE) |
||||
} catch (e: NoSuchFieldException) { |
||||
e.printStackTrace() |
||||
} catch (e: IllegalArgumentException) { |
||||
e.printStackTrace() |
||||
} catch (e: IllegalAccessException) { |
||||
e.printStackTrace() |
||||
} |
||||
return null |
||||
} |
||||
} |
@ -0,0 +1,137 @@ |
||||
package xyz.fycz.myreader.model.storage |
||||
|
||||
import android.content.Context |
||||
import android.net.Uri |
||||
import androidx.documentfile.provider.DocumentFile |
||||
import io.reactivex.Single |
||||
import io.reactivex.SingleOnSubscribe |
||||
import io.reactivex.android.schedulers.AndroidSchedulers |
||||
import io.reactivex.schedulers.Schedulers |
||||
import xyz.fycz.myreader.application.MyApplication |
||||
import xyz.fycz.myreader.application.SysManager |
||||
import xyz.fycz.myreader.base.observer.MySingleObserver |
||||
import xyz.fycz.myreader.entity.Setting |
||||
import xyz.fycz.myreader.greendao.GreenDaoManager |
||||
import xyz.fycz.myreader.greendao.entity.Book |
||||
import xyz.fycz.myreader.greendao.entity.BookGroup |
||||
import xyz.fycz.myreader.greendao.entity.BookMark |
||||
import xyz.fycz.myreader.greendao.entity.SearchHistory |
||||
import xyz.fycz.myreader.util.SharedPreUtils |
||||
import xyz.fycz.myreader.util.utils.* |
||||
import java.io.File |
||||
|
||||
object Restore { |
||||
|
||||
fun restore(context: Context, uri: Uri, callBack: CallBack?) { |
||||
Single.create(SingleOnSubscribe<Boolean> { e -> |
||||
DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc -> |
||||
for (fileName in Backup.backupFileNames) { |
||||
if (doc.name == fileName) { |
||||
DocumentUtil.readBytes(context, doc.uri)?.let { |
||||
FileUtils.getFile(Backup.backupPath + File.separator + fileName) |
||||
.writeBytes(it) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
e.onSuccess(true) |
||||
}).subscribeOn(Schedulers.io()) |
||||
.observeOn(AndroidSchedulers.mainThread()) |
||||
.subscribe(object : MySingleObserver<Boolean>() { |
||||
override fun onSuccess(t: Boolean) { |
||||
restore(Backup.backupPath, callBack) |
||||
} |
||||
|
||||
override fun onError(e: Throwable) { |
||||
e.printStackTrace() |
||||
callBack?.restoreError(e.localizedMessage ?: "ERROR") |
||||
} |
||||
}) |
||||
} |
||||
|
||||
fun restore(path: String, callBack: CallBack?) { |
||||
Single.create(SingleOnSubscribe<Boolean> { e -> |
||||
try { |
||||
val file = FileUtils.getFile(path + File.separator + "myBooks.json") |
||||
val json = file.readText() |
||||
GSON.fromJsonArray<Book>(json)?.forEach { bookshelf -> |
||||
/*if (bookshelf.noteUrl != null) { |
||||
DbHelper.getDaoSession().bookShelfBeanDao.insertOrReplace(bookshelf) |
||||
} |
||||
if (bookshelf.bookInfoBean.noteUrl != null) { |
||||
DbHelper.getDaoSession().bookInfoBeanDao.insertOrReplace(bookshelf.bookInfoBean) |
||||
}*/ |
||||
GreenDaoManager.getInstance().session.bookDao.insertOrReplace(bookshelf) |
||||
} |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
try { |
||||
val file = FileUtils.getFile(path + File.separator + "mySearchHistory.json") |
||||
val json = file.readText() |
||||
GSON.fromJsonArray<SearchHistory>(json)?.let { |
||||
GreenDaoManager.getInstance().session.searchHistoryDao.insertOrReplaceInTx(it) |
||||
} |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
try { |
||||
val file = FileUtils.getFile(path + File.separator + "myBookMark.json") |
||||
val json = file.readText() |
||||
GSON.fromJsonArray<BookMark>(json)?.let { |
||||
GreenDaoManager.getInstance().session.bookMarkDao.insertOrReplaceInTx(it) |
||||
} |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
try { |
||||
val file = FileUtils.getFile(path + File.separator + "myBookGroup.json") |
||||
val json = file.readText() |
||||
GSON.fromJsonArray<BookGroup>(json)?.let { |
||||
GreenDaoManager.getInstance().session.bookGroupDao.insertOrReplaceInTx(it) |
||||
} |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
try { |
||||
val file = FileUtils.getFile(path + File.separator + "setting.json") |
||||
val json = file.readText() |
||||
SysManager.saveSetting(GSON.fromJsonObject<Setting>(json)) |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
Preferences.getSharedPreferences(MyApplication.getmContext(), path, "config")?.all?.map { |
||||
val edit = SharedPreUtils.getInstance() |
||||
when (val value = it.value) { |
||||
is Int -> edit.putInt(it.key, value) |
||||
is Boolean -> edit.putBoolean(it.key, value) |
||||
is Long -> edit.putLong(it.key, value) |
||||
is Float -> edit.putFloat(it.key, value) |
||||
is String -> edit.putString(it.key, value) |
||||
else -> Unit |
||||
} |
||||
edit.putInt("versionCode", MyApplication.getVersionCode()) |
||||
} |
||||
e.onSuccess(true) |
||||
}).subscribeOn(Schedulers.io()) |
||||
.observeOn(AndroidSchedulers.mainThread()) |
||||
.subscribe(object : MySingleObserver<Boolean>() { |
||||
override fun onSuccess(t: Boolean) { |
||||
MyApplication.getApplication().initNightTheme() |
||||
callBack?.restoreSuccess() |
||||
} |
||||
|
||||
override fun onError(e: Throwable) { |
||||
e.printStackTrace() |
||||
callBack?.restoreError(e.localizedMessage ?: "ERROR") |
||||
} |
||||
}) |
||||
} |
||||
|
||||
|
||||
interface CallBack { |
||||
fun restoreSuccess() |
||||
fun restoreError(msg: String) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,121 @@ |
||||
package xyz.fycz.myreader.model.storage |
||||
|
||||
import android.content.Context |
||||
import android.os.Handler |
||||
import android.os.Looper |
||||
import io.reactivex.Single |
||||
import io.reactivex.SingleOnSubscribe |
||||
import io.reactivex.android.schedulers.AndroidSchedulers |
||||
import io.reactivex.schedulers.Schedulers |
||||
import org.jetbrains.anko.selector |
||||
import xyz.fycz.myreader.base.observer.MySingleObserver |
||||
import xyz.fycz.myreader.common.APPCONST |
||||
import xyz.fycz.myreader.util.SharedPreUtils |
||||
import xyz.fycz.myreader.util.ToastUtils |
||||
import xyz.fycz.myreader.util.ZipUtils |
||||
import xyz.fycz.myreader.util.utils.FileUtils |
||||
import xyz.fycz.myreader.util.webdav.WebDav |
||||
import xyz.fycz.myreader.util.webdav.http.HttpAuth |
||||
import java.io.File |
||||
import java.text.SimpleDateFormat |
||||
import java.util.* |
||||
import kotlin.math.min |
||||
|
||||
object WebDavHelp { |
||||
private val zipFilePath = FileUtils.getCachePath() + "/backup" + ".zip" |
||||
private val unzipFilesPath by lazy { |
||||
FileUtils.getCachePath() |
||||
} |
||||
|
||||
private fun getWebDavUrl(): String { |
||||
var url = SharedPreUtils.getInstance().getString("webdavUrl", APPCONST.DEFAULT_WEB_DAV_URL) |
||||
if (url.isNullOrEmpty()) { |
||||
url = APPCONST.DEFAULT_WEB_DAV_URL |
||||
} |
||||
if (!url.endsWith("/")) url += "/" |
||||
return url |
||||
} |
||||
|
||||
private fun initWebDav(): Boolean { |
||||
val account = SharedPreUtils.getInstance().getString("webdavAccount", "") |
||||
val password = SharedPreUtils.getInstance().getString("webdavPassword", "") |
||||
if (!account.isNullOrBlank() && !password.isNullOrBlank()) { |
||||
HttpAuth.auth = HttpAuth.Auth(account, password) |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
fun getWebDavFileNames(): ArrayList<String> { |
||||
val url = getWebDavUrl() |
||||
val names = arrayListOf<String>() |
||||
try { |
||||
if (initWebDav()) { |
||||
var files = WebDav(url + "FYReader/").listFiles() |
||||
files = files.reversed() |
||||
for (index: Int in 0 until min(10, files.size)) { |
||||
files[index].displayName?.let { |
||||
names.add(it) |
||||
} |
||||
} |
||||
} |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
return names |
||||
} |
||||
|
||||
fun showRestoreDialog(context: Context, names: ArrayList<String>, callBack: Restore.CallBack?): Boolean { |
||||
return if (names.isNotEmpty()) { |
||||
context.selector(title = "选择恢复文件", items = names) { _, index -> |
||||
if (index in 0 until names.size) { |
||||
restoreWebDav(names[index], callBack) |
||||
} |
||||
} |
||||
true |
||||
} else { |
||||
false |
||||
} |
||||
} |
||||
|
||||
private fun restoreWebDav(name: String, callBack: Restore.CallBack?) { |
||||
Single.create(SingleOnSubscribe<Boolean> { e -> |
||||
getWebDavUrl().let { |
||||
val file = WebDav(it + "FYReader/" + name) |
||||
file.downloadTo(zipFilePath, true) |
||||
@Suppress("BlockingMethodInNonBlockingContext") |
||||
ZipUtils.unzipFile(zipFilePath, unzipFilesPath) |
||||
} |
||||
e.onSuccess(true) |
||||
}).subscribeOn(Schedulers.io()) |
||||
.observeOn(AndroidSchedulers.mainThread()) |
||||
.subscribe(object : MySingleObserver<Boolean>() { |
||||
override fun onSuccess(t: Boolean) { |
||||
Restore.restore(unzipFilesPath, callBack) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
fun backUpWebDav(path: String) { |
||||
try { |
||||
if (initWebDav()) { |
||||
val paths = arrayListOf(*Backup.backupFileNames) |
||||
for (i in 0 until paths.size) { |
||||
paths[i] = path + File.separator + paths[i] |
||||
} |
||||
FileUtils.deleteFile(zipFilePath) |
||||
if (ZipUtils.zipFiles(paths, zipFilePath)) { |
||||
WebDav(getWebDavUrl() + "FYReader").makeAsDir() |
||||
val putUrl = getWebDavUrl() + "FYReader/backup" + |
||||
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) |
||||
.format(Date(System.currentTimeMillis())) + ".zip" |
||||
WebDav(putUrl).upload(zipFilePath) |
||||
} |
||||
} |
||||
} catch (e: Exception) { |
||||
Handler(Looper.getMainLooper()).post { |
||||
ToastUtils.showError("WebDav\n${e.localizedMessage}") |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,131 @@ |
||||
package xyz.fycz.myreader.ui.activity; |
||||
|
||||
import android.os.Bundle; |
||||
import android.text.InputType; |
||||
import android.widget.LinearLayout; |
||||
import android.widget.TextView; |
||||
import androidx.appcompat.widget.Toolbar; |
||||
import butterknife.BindView; |
||||
import io.reactivex.Single; |
||||
import io.reactivex.SingleOnSubscribe; |
||||
import io.reactivex.android.schedulers.AndroidSchedulers; |
||||
import io.reactivex.schedulers.Schedulers; |
||||
import xyz.fycz.myreader.R; |
||||
import xyz.fycz.myreader.base.BaseActivity2; |
||||
import xyz.fycz.myreader.base.observer.MySingleObserver; |
||||
import xyz.fycz.myreader.common.APPCONST; |
||||
import xyz.fycz.myreader.model.storage.BackupRestoreUi; |
||||
import xyz.fycz.myreader.model.storage.WebDavHelp; |
||||
import xyz.fycz.myreader.ui.dialog.MyAlertDialog; |
||||
import xyz.fycz.myreader.util.SharedPreUtils; |
||||
import xyz.fycz.myreader.util.StringHelper; |
||||
import xyz.fycz.myreader.util.ToastUtils; |
||||
|
||||
import java.util.ArrayList; |
||||
|
||||
/** |
||||
* @author fengyue |
||||
* @date 2020/10/4 20:44 |
||||
*/ |
||||
public class WebDavSettingActivity extends BaseActivity2 { |
||||
@BindView(R.id.webdav_setting_webdav_url) |
||||
LinearLayout llWebdavUrl; |
||||
@BindView(R.id.tv_webdav_url) |
||||
TextView tvWebdavUrl; |
||||
@BindView(R.id.webdav_setting_webdav_account) |
||||
LinearLayout llWebdavAccount; |
||||
@BindView(R.id.tv_webdav_account) |
||||
TextView tvWebdavAccount; |
||||
@BindView(R.id.webdav_setting_webdav_password) |
||||
LinearLayout llWebdavPassword; |
||||
@BindView(R.id.tv_webdav_password) |
||||
TextView tvWebdavPassword; |
||||
@BindView(R.id.webdav_setting_webdav_restore) |
||||
LinearLayout llWebdavRestore; |
||||
|
||||
private String webdavUrl; |
||||
private String webdavAccount; |
||||
private String webdavPassword; |
||||
@Override |
||||
protected int getContentId() { |
||||
return R.layout.activity_webdav_setting; |
||||
} |
||||
|
||||
@Override |
||||
protected void setUpToolbar(Toolbar toolbar) { |
||||
super.setUpToolbar(toolbar); |
||||
setStatusBarColor(R.color.colorPrimary, true); |
||||
getSupportActionBar().setTitle(getString(R.string.webdav_setting)); |
||||
} |
||||
|
||||
@Override |
||||
protected void initData(Bundle savedInstanceState) { |
||||
super.initData(savedInstanceState); |
||||
webdavUrl = SharedPreUtils.getInstance().getString("webdavUrl", APPCONST.DEFAULT_WEB_DAV_URL); |
||||
webdavAccount = SharedPreUtils.getInstance().getString("webdavAccount", ""); |
||||
webdavPassword = SharedPreUtils.getInstance().getString("webdavPassword", ""); |
||||
} |
||||
|
||||
@Override |
||||
protected void initWidget() { |
||||
super.initWidget(); |
||||
tvWebdavUrl.setText(webdavUrl); |
||||
tvWebdavAccount.setText(StringHelper.isEmpty(webdavAccount) ? "请输入WebDav账号" : webdavAccount); |
||||
tvWebdavPassword.setText(StringHelper.isEmpty(webdavPassword) ? "请输入WebDav授权密码" : "************"); |
||||
} |
||||
|
||||
@Override |
||||
protected void initClick() { |
||||
super.initClick(); |
||||
final String[] webdavTexts = new String[3]; |
||||
llWebdavUrl.setOnClickListener(v -> { |
||||
MyAlertDialog.createInputDia(this, getString(R.string.webdav_url), |
||||
"", webdavUrl.equals(APPCONST.DEFAULT_WEB_DAV_URL) ? |
||||
"" : webdavUrl, true, 100, |
||||
text -> webdavTexts[0] = text, |
||||
(dialog, which) -> { |
||||
webdavUrl = webdavTexts[0]; |
||||
tvWebdavUrl.setText(webdavUrl); |
||||
SharedPreUtils.getInstance().putString("webdavUrl", webdavUrl); |
||||
dialog.dismiss(); |
||||
}); |
||||
}); |
||||
llWebdavAccount.setOnClickListener(v -> { |
||||
MyAlertDialog.createInputDia(this, getString(R.string.webdav_account), |
||||
"", webdavAccount, true, 100, |
||||
text -> webdavTexts[1] = text, |
||||
(dialog, which) -> { |
||||
webdavAccount = webdavTexts[1]; |
||||
tvWebdavAccount.setText(webdavAccount); |
||||
SharedPreUtils.getInstance().putString("webdavAccount", webdavAccount); |
||||
dialog.dismiss(); |
||||
}); |
||||
}); |
||||
llWebdavPassword.setOnClickListener(v -> { |
||||
MyAlertDialog.createInputDia(this, getString(R.string.webdav_password), |
||||
"", webdavPassword, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD, |
||||
true, 100, |
||||
text -> webdavTexts[2] = text, |
||||
(dialog, which) -> { |
||||
webdavPassword = webdavTexts[2]; |
||||
tvWebdavPassword.setText("************"); |
||||
SharedPreUtils.getInstance().putString("webdavPassword", webdavPassword); |
||||
dialog.dismiss(); |
||||
}); |
||||
}); |
||||
llWebdavRestore.setOnClickListener(v -> { |
||||
Single.create((SingleOnSubscribe<ArrayList<String>>) emitter -> { |
||||
emitter.onSuccess(WebDavHelp.INSTANCE.getWebDavFileNames()); |
||||
}).subscribeOn(Schedulers.io()) |
||||
.observeOn(AndroidSchedulers.mainThread()) |
||||
.subscribe(new MySingleObserver<ArrayList<String>>() { |
||||
@Override |
||||
public void onSuccess(ArrayList<String> strings) { |
||||
if (!WebDavHelp.INSTANCE.showRestoreDialog(WebDavSettingActivity.this, strings, BackupRestoreUi.INSTANCE)) { |
||||
ToastUtils.showWarring("没有备份"); |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
} |
@ -1,15 +1,89 @@ |
||||
package xyz.fycz.myreader.ui.dialog; |
||||
|
||||
import android.content.Context; |
||||
import android.content.DialogInterface; |
||||
import android.text.Editable; |
||||
import android.text.InputType; |
||||
import android.text.TextWatcher; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.inputmethod.InputMethodManager; |
||||
import android.widget.Button; |
||||
import android.widget.EditText; |
||||
import androidx.appcompat.app.AlertDialog; |
||||
import com.google.android.material.textfield.TextInputLayout; |
||||
import xyz.fycz.myreader.R; |
||||
import xyz.fycz.myreader.application.MyApplication; |
||||
import xyz.fycz.myreader.util.StringHelper; |
||||
|
||||
/** |
||||
* @author fengyue |
||||
* @date 2020/9/20 9:48 |
||||
*/ |
||||
public class MyAlertDialog { |
||||
public static AlertDialog.Builder build(Context context){ |
||||
public static AlertDialog.Builder build(Context context) { |
||||
return new AlertDialog.Builder(context, R.style.alertDialogTheme); |
||||
} |
||||
|
||||
public static AlertDialog createInputDia(Context context, String title, String hint, String initText, |
||||
Integer inputType, boolean cancelable, int maxLen, onInputChangeListener oic, |
||||
DialogInterface.OnClickListener posListener) { |
||||
View view = LayoutInflater.from(context).inflate(R.layout.edit_dialog, null); |
||||
TextInputLayout textInputLayout = view.findViewById(R.id.text_input_lay); |
||||
|
||||
textInputLayout.setCounterMaxLength(maxLen); |
||||
EditText editText = textInputLayout.getEditText(); |
||||
editText.setHint(hint); |
||||
if (inputType != null) editText.setInputType(inputType); |
||||
if (!StringHelper.isEmpty(initText)) editText.setText(initText); |
||||
editText.requestFocus(); |
||||
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); |
||||
MyApplication.getHandler().postDelayed(() -> imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED), 220); |
||||
AlertDialog inputDia = build(context) |
||||
.setTitle(title) |
||||
.setView(view) |
||||
.setCancelable(cancelable) |
||||
.setPositiveButton("确认", (dialog, which) -> { |
||||
posListener.onClick(dialog, which); |
||||
imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED); |
||||
}) |
||||
.setNegativeButton("取消", (dialog, which) -> { |
||||
imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED); |
||||
}) |
||||
.show(); |
||||
Button posBtn = inputDia.getButton(AlertDialog.BUTTON_POSITIVE); |
||||
posBtn.setEnabled(false); |
||||
editText.addTextChangedListener(new TextWatcher() { |
||||
@Override |
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void onTextChanged(CharSequence s, int start, int before, int count) { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void afterTextChanged(Editable s) { |
||||
String text = editText.getText().toString(); |
||||
if (editText.getText().length() > 0 && editText.getText().length() <= maxLen && !text.equals(initText)) { |
||||
posBtn.setEnabled(true); |
||||
} else { |
||||
posBtn.setEnabled(false); |
||||
} |
||||
oic.onChange(text); |
||||
} |
||||
}); |
||||
return inputDia; |
||||
} |
||||
public static AlertDialog createInputDia(Context context, String title, String hint, String initText, |
||||
boolean cancelable, int maxLen, onInputChangeListener oic, |
||||
DialogInterface.OnClickListener posListener) { |
||||
return createInputDia(context, title, hint, initText, InputType.TYPE_CLASS_TEXT, cancelable, maxLen, oic, posListener); |
||||
} |
||||
|
||||
public interface onInputChangeListener{ |
||||
void onChange(String text); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,387 @@ |
||||
package xyz.fycz.myreader.util.utils; |
||||
|
||||
import android.content.Context; |
||||
import android.net.Uri; |
||||
import androidx.annotation.NonNull; |
||||
import androidx.documentfile.provider.DocumentFile; |
||||
|
||||
import java.io.File; |
||||
import java.io.InputStream; |
||||
import java.io.OutputStream; |
||||
import java.util.regex.Pattern; |
||||
|
||||
/** |
||||
* Created by PureDark on 2016/9/24. |
||||
*/ |
||||
|
||||
@SuppressWarnings({"unused", "WeakerAccess"}) |
||||
public class DocumentUtil { |
||||
|
||||
private static Pattern FilePattern = Pattern.compile("[\\\\/:*?\"<>|]"); |
||||
|
||||
public static boolean isFileExist(Context context, String fileName, String rootPath, String... subDirs) { |
||||
Uri rootUri; |
||||
if (rootPath.startsWith("content")) |
||||
rootUri = Uri.parse(rootPath); |
||||
else |
||||
rootUri = Uri.parse(Uri.decode(rootPath)); |
||||
return isFileExist(context, fileName, rootUri, subDirs); |
||||
} |
||||
|
||||
public static boolean isFileExist(Context context, String fileName, Uri rootUri, String... subDirs) { |
||||
DocumentFile root; |
||||
if ("content".equals(rootUri.getScheme())) |
||||
root = DocumentFile.fromTreeUri(context, rootUri); |
||||
else |
||||
root = DocumentFile.fromFile(new File(rootUri.getPath())); |
||||
return isFileExist(fileName, root, subDirs); |
||||
} |
||||
|
||||
public static boolean isFileExist(String fileName, DocumentFile root, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(root, subDirs); |
||||
if (parent == null) |
||||
return false; |
||||
fileName = filenameFilter(Uri.decode(fileName)); |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file != null && file.exists()) |
||||
return true; |
||||
return false; |
||||
} |
||||
|
||||
public static DocumentFile createDirIfNotExist(Context context, String rootPath, String... subDirs) { |
||||
Uri rootUri; |
||||
if (rootPath.startsWith("content")) |
||||
rootUri = Uri.parse(rootPath); |
||||
else |
||||
rootUri = Uri.parse(Uri.decode(rootPath)); |
||||
return createDirIfNotExist(context, rootUri, subDirs); |
||||
} |
||||
|
||||
public static DocumentFile createDirIfNotExist(Context context, Uri rootUri, String... subDirs) { |
||||
DocumentFile root; |
||||
if ("content".equals(rootUri.getScheme())) |
||||
root = DocumentFile.fromTreeUri(context, rootUri); |
||||
else |
||||
root = DocumentFile.fromFile(new File(rootUri.getPath())); |
||||
return createDirIfNotExist(root, subDirs); |
||||
} |
||||
|
||||
public static DocumentFile createDirIfNotExist(@NonNull DocumentFile root, String... subDirs) { |
||||
DocumentFile parent = root; |
||||
try { |
||||
for (String subDir1 : subDirs) { |
||||
String subDirName = filenameFilter(Uri.decode(subDir1)); |
||||
DocumentFile subDir = parent.findFile(subDirName); |
||||
if (subDir == null) { |
||||
subDir = parent.createDirectory(subDirName); |
||||
} |
||||
parent = subDir; |
||||
} |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
return null; |
||||
} |
||||
return parent; |
||||
} |
||||
|
||||
public static DocumentFile createFileIfNotExist(Context context, String fileName, String rootPath, String... subDirs) { |
||||
Uri rootUri; |
||||
if (rootPath.startsWith("content")) |
||||
rootUri = Uri.parse(rootPath); |
||||
else |
||||
rootUri = Uri.parse(Uri.decode(rootPath)); |
||||
return createFileIfNotExist(context, "", fileName, rootUri, subDirs); |
||||
} |
||||
|
||||
public static DocumentFile createFileIfNotExist(Context context, String fileName, Uri rootUri, String... subDirs) { |
||||
return createFileIfNotExist(context, "", fileName, rootUri, subDirs); |
||||
} |
||||
|
||||
public static DocumentFile createFileIfNotExist(Context context, String mimeType, String fileName, String rootPath, String... subDirs) { |
||||
Uri rootUri; |
||||
if (rootPath.startsWith("content")) |
||||
rootUri = Uri.parse(rootPath); |
||||
else |
||||
rootUri = Uri.parse(Uri.decode(rootPath)); |
||||
return createFileIfNotExist(context, mimeType, fileName, rootUri, subDirs); |
||||
} |
||||
|
||||
public static DocumentFile createFileIfNotExist(Context context, String mimeType, String fileName, Uri rootUri, String... subDirs) { |
||||
DocumentFile parent = createDirIfNotExist(context, rootUri, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
fileName = filenameFilter(Uri.decode(fileName)); |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) { |
||||
file = parent.createFile(mimeType, fileName); |
||||
} |
||||
return file; |
||||
} |
||||
|
||||
public static boolean deleteFile(Context context, String fileName, String rootPath, String... subDirs) { |
||||
Uri rootUri; |
||||
if (rootPath.startsWith("content")) |
||||
rootUri = Uri.parse(rootPath); |
||||
else |
||||
rootUri = Uri.parse(Uri.decode(rootPath)); |
||||
return deleteFile(context, fileName, rootUri, subDirs); |
||||
} |
||||
|
||||
public static boolean deleteFile(Context context, String fileName, Uri rootUri, String... subDirs) { |
||||
DocumentFile root; |
||||
if ("content".equals(rootUri.getScheme())) |
||||
root = DocumentFile.fromTreeUri(context, rootUri); |
||||
else |
||||
root = DocumentFile.fromFile(new File(rootUri.getPath())); |
||||
return deleteFile(fileName, root, subDirs); |
||||
} |
||||
|
||||
public static boolean deleteFile(String fileName, DocumentFile root, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(root, subDirs); |
||||
if (parent == null) |
||||
return false; |
||||
fileName = filenameFilter(Uri.decode(fileName)); |
||||
DocumentFile file = parent.findFile(fileName); |
||||
return file != null && file.exists() && file.delete(); |
||||
} |
||||
|
||||
public static boolean writeBytes(Context context, byte[] data, String fileName, String rootPath, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(context, rootPath, subDirs); |
||||
if (parent == null) |
||||
return false; |
||||
DocumentFile file = parent.findFile(fileName); |
||||
return writeBytes(context, data, file.getUri()); |
||||
} |
||||
|
||||
public static boolean writeBytes(Context context, byte[] data, String fileName, Uri rootUri, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(context, rootUri, subDirs); |
||||
if (parent == null) |
||||
return false; |
||||
fileName = filenameFilter(Uri.decode(fileName)); |
||||
DocumentFile file = parent.findFile(fileName); |
||||
return writeBytes(context, data, file.getUri()); |
||||
} |
||||
|
||||
public static boolean writeBytes(Context context, byte[] data, String fileName, DocumentFile root, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(root, subDirs); |
||||
if (parent == null) |
||||
return false; |
||||
fileName = filenameFilter(Uri.decode(fileName)); |
||||
DocumentFile file = parent.findFile(fileName); |
||||
return writeBytes(context, data, file.getUri()); |
||||
} |
||||
|
||||
public static boolean writeBytes(Context context, byte[] data, DocumentFile file) { |
||||
return writeBytes(context, data, file.getUri()); |
||||
} |
||||
|
||||
public static boolean writeBytes(Context context, byte[] data, Uri fileUri) { |
||||
try { |
||||
OutputStream out = context.getContentResolver().openOutputStream(fileUri, "wt"); //Write file need open with truncate mode, the mode truncate file upon opening (to zero bytes)
|
||||
out.write(data); |
||||
out.close(); |
||||
return true; |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
public static boolean writeFromInputStream(Context context, InputStream inStream, DocumentFile file) { |
||||
return writeFromInputStream(context, inStream, file.getUri()); |
||||
} |
||||
|
||||
public static boolean writeFromInputStream(Context context, InputStream inStream, Uri fileUri) { |
||||
try { |
||||
OutputStream out = context.getContentResolver().openOutputStream(fileUri); |
||||
int byteread; |
||||
byte[] buffer = new byte[1024]; |
||||
while ((byteread = inStream.read(buffer)) > 0) { |
||||
out.write(buffer, 0, byteread); |
||||
} |
||||
inStream.close(); |
||||
out.close(); |
||||
return true; |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
public static byte[] readBytes(Context context, String fileName, String rootPath, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(context, rootPath, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return readBytes(context, file.getUri()); |
||||
} |
||||
|
||||
public static byte[] readBytes(Context context, String fileName, Uri rootUri, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(context, rootUri, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
fileName = filenameFilter(Uri.decode(fileName)); |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return readBytes(context, file.getUri()); |
||||
} |
||||
|
||||
public static byte[] readBytes(Context context, String fileName, DocumentFile root, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(root, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
fileName = filenameFilter(Uri.decode(fileName)); |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return readBytes(context, file.getUri()); |
||||
} |
||||
|
||||
public static byte[] readBytes(Context context, DocumentFile file) { |
||||
if (file == null) |
||||
return null; |
||||
return readBytes(context, file.getUri()); |
||||
} |
||||
|
||||
public static byte[] readBytes(Context context, Uri fileUri) { |
||||
try { |
||||
InputStream fis = context.getContentResolver().openInputStream(fileUri); |
||||
int len = fis.available(); |
||||
byte[] buffer = new byte[len]; |
||||
fis.read(buffer); |
||||
fis.close(); |
||||
return buffer; |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
public static DocumentFile getDirDocument(Context context, String rootPath, String... subDirs) { |
||||
Uri rootUri; |
||||
if (rootPath.startsWith("content")) |
||||
rootUri = Uri.parse(rootPath); |
||||
else |
||||
rootUri = Uri.parse(Uri.decode(rootPath)); |
||||
return getDirDocument(context, rootUri, subDirs); |
||||
} |
||||
|
||||
public static DocumentFile getDirDocument(Context context, Uri rootUri, String... subDirs) { |
||||
DocumentFile root; |
||||
if ("content".equals(rootUri.getScheme())) |
||||
root = DocumentFile.fromTreeUri(context, rootUri); |
||||
else |
||||
root = DocumentFile.fromFile(new File(rootUri.getPath())); |
||||
return getDirDocument(root, subDirs); |
||||
} |
||||
|
||||
public static DocumentFile getDirDocument(DocumentFile root, String... subDirs) { |
||||
DocumentFile parent = root; |
||||
for (int i = 0; i < subDirs.length; i++) { |
||||
String subDirName = Uri.decode(subDirs[i]); |
||||
DocumentFile subDir = parent.findFile(subDirName); |
||||
if (subDir != null) |
||||
parent = subDir; |
||||
else |
||||
return null; |
||||
} |
||||
return parent; |
||||
} |
||||
|
||||
public static OutputStream getFileOutputSteam(Context context, String fileName, String rootPath, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(context, rootPath, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return getFileOutputSteam(context, file.getUri()); |
||||
} |
||||
|
||||
public static OutputStream getFileOutputSteam(Context context, String fileName, Uri rootUri, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(context, rootUri, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return getFileOutputSteam(context, file.getUri()); |
||||
} |
||||
|
||||
public static OutputStream getFileOutputSteam(Context context, String fileName, DocumentFile root, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(root, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return getFileOutputSteam(context, file.getUri()); |
||||
} |
||||
|
||||
public static OutputStream getFileOutputSteam(Context context, DocumentFile file) { |
||||
return getFileOutputSteam(context, file.getUri()); |
||||
} |
||||
|
||||
public static OutputStream getFileOutputSteam(Context context, Uri fileUri) { |
||||
try { |
||||
OutputStream out = context.getContentResolver().openOutputStream(fileUri); |
||||
return out; |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
public static InputStream getFileInputSteam(Context context, String fileName, String rootPath, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(context, rootPath, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return getFileInputSteam(context, file.getUri()); |
||||
} |
||||
|
||||
public static InputStream getFileInputSteam(Context context, String fileName, Uri rootUri, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(context, rootUri, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
fileName = filenameFilter(Uri.decode(fileName)); |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return getFileInputSteam(context, file.getUri()); |
||||
} |
||||
|
||||
public static InputStream getFileInputSteam(Context context, String fileName, DocumentFile root, String... subDirs) { |
||||
DocumentFile parent = getDirDocument(root, subDirs); |
||||
if (parent == null) |
||||
return null; |
||||
DocumentFile file = parent.findFile(fileName); |
||||
if (file == null) |
||||
return null; |
||||
return getFileInputSteam(context, file.getUri()); |
||||
} |
||||
|
||||
public static InputStream getFileInputSteam(Context context, DocumentFile file) { |
||||
return getFileInputSteam(context, file.getUri()); |
||||
} |
||||
|
||||
public static InputStream getFileInputSteam(Context context, Uri fileUri) { |
||||
try { |
||||
InputStream in = context.getContentResolver().openInputStream(fileUri); |
||||
return in; |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
public static String filenameFilter(String str) { |
||||
return str == null ? null : FilePattern.matcher(str).replaceAll("_"); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,43 @@ |
||||
package xyz.fycz.myreader.util.utils |
||||
|
||||
import com.google.gson.Gson |
||||
import com.google.gson.GsonBuilder |
||||
import com.google.gson.JsonSyntaxException |
||||
import com.google.gson.reflect.TypeToken |
||||
import org.jetbrains.anko.attempt |
||||
import java.lang.reflect.ParameterizedType |
||||
import java.lang.reflect.Type |
||||
|
||||
val GSON: Gson by lazy { |
||||
GsonBuilder() |
||||
.disableHtmlEscaping() |
||||
.setPrettyPrinting() |
||||
.create() |
||||
} |
||||
|
||||
inline fun <reified T> genericType() = object : TypeToken<T>() {}.type |
||||
|
||||
|
||||
@Throws(JsonSyntaxException::class) |
||||
inline fun <reified T> Gson.fromJsonObject(json: String?): T? {//可转成任意类型 |
||||
return attempt { |
||||
val result: T? = fromJson(json, genericType<T>()) |
||||
result |
||||
}.value |
||||
} |
||||
|
||||
@Throws(JsonSyntaxException::class) |
||||
inline fun <reified T> Gson.fromJsonArray(json: String?): List<T>? { |
||||
return attempt { |
||||
val result: List<T>? = fromJson(json, ParameterizedTypeImpl(T::class.java)) |
||||
result |
||||
}.value |
||||
} |
||||
|
||||
class ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType { |
||||
override fun getRawType(): Type = List::class.java |
||||
|
||||
override fun getOwnerType(): Type? = null |
||||
|
||||
override fun getActualTypeArguments(): Array<Type> = arrayOf(clazz) |
||||
} |
@ -0,0 +1,50 @@ |
||||
package xyz.fycz.myreader.util.utils |
||||
|
||||
import android.content.Context |
||||
import android.graphics.Bitmap |
||||
import android.graphics.drawable.Drawable |
||||
import android.net.Uri |
||||
import androidx.annotation.DrawableRes |
||||
import com.bumptech.glide.Glide |
||||
import com.bumptech.glide.RequestBuilder |
||||
import java.io.File |
||||
|
||||
object ImageLoader { |
||||
|
||||
fun load(context: Context, path: String?): RequestBuilder<Drawable> { |
||||
return when { |
||||
path.isNullOrEmpty() -> Glide.with(context).load(path) |
||||
path.startsWith("http", true) -> Glide.with(context).load(path) |
||||
else -> try { |
||||
Glide.with(context).load(File(path)) |
||||
} catch (e: Exception) { |
||||
Glide.with(context).load(path) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder<Drawable> { |
||||
return Glide.with(context).load(resId) |
||||
} |
||||
|
||||
fun load(context: Context, file: File?): RequestBuilder<Drawable> { |
||||
return Glide.with(context).load(file) |
||||
} |
||||
|
||||
fun load(context: Context, uri: Uri?): RequestBuilder<Drawable> { |
||||
return Glide.with(context).load(uri) |
||||
} |
||||
|
||||
fun load(context: Context, drawable: Drawable?): RequestBuilder<Drawable> { |
||||
return Glide.with(context).load(drawable) |
||||
} |
||||
|
||||
fun load(context: Context, bitmap: Bitmap?): RequestBuilder<Drawable> { |
||||
return Glide.with(context).load(bitmap) |
||||
} |
||||
|
||||
fun load(context: Context, bytes: ByteArray?): RequestBuilder<Drawable> { |
||||
return Glide.with(context).load(bytes) |
||||
} |
||||
|
||||
} |
@ -0,0 +1 @@ |
||||
## 用于网络备份的WebDav |
@ -0,0 +1,250 @@ |
||||
package xyz.fycz.myreader.util.webdav |
||||
|
||||
import xyz.fycz.myreader.util.webdav.http.Handler |
||||
import xyz.fycz.myreader.util.webdav.http.HttpAuth |
||||
import okhttp3.* |
||||
import org.jsoup.Jsoup |
||||
import xyz.fycz.myreader.util.HttpUtil |
||||
import java.io.File |
||||
import java.io.IOException |
||||
import java.io.InputStream |
||||
import java.io.UnsupportedEncodingException |
||||
import java.net.MalformedURLException |
||||
import java.net.URL |
||||
import java.net.URLEncoder |
||||
import java.util.* |
||||
|
||||
class WebDav @Throws(MalformedURLException::class) |
||||
constructor(urlStr: String) { |
||||
companion object { |
||||
// 指定返回哪些属性 |
||||
private const val DIR = |
||||
"""<?xml version="1.0"?> |
||||
<a:propfind xmlns:a="DAV:"> |
||||
<a:prop> |
||||
<a:displayname/> |
||||
<a:resourcetype/> |
||||
<a:getcontentlength/> |
||||
<a:creationdate/> |
||||
<a:getlastmodified/> |
||||
%s |
||||
</a:prop> |
||||
</a:propfind>""" |
||||
} |
||||
|
||||
private val url: URL = URL(null, urlStr, Handler) |
||||
private val httpUrl: String? by lazy { |
||||
val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://") |
||||
try { |
||||
return@lazy URLEncoder.encode(raw, "UTF-8") |
||||
.replace("\\+".toRegex(), "%20") |
||||
.replace("%3A".toRegex(), ":") |
||||
.replace("%2F".toRegex(), "/") |
||||
} catch (e: UnsupportedEncodingException) { |
||||
e.printStackTrace() |
||||
return@lazy null |
||||
} |
||||
} |
||||
|
||||
var displayName: String? = null |
||||
var size: Long = 0 |
||||
var exists = false |
||||
var parent = "" |
||||
var urlName = "" |
||||
get() { |
||||
if (field.isEmpty()) { |
||||
this.urlName = ( |
||||
if (parent.isEmpty()) url.file |
||||
else url.toString().replace(parent, "") |
||||
).replace("/", "") |
||||
} |
||||
return field |
||||
} |
||||
|
||||
fun getPath() = url.toString() |
||||
|
||||
fun getHost() = url.host |
||||
|
||||
/** |
||||
* 填充文件信息。实例化WebDAVFile对象时,并没有将远程文件的信息填充到实例中。需要手动填充! |
||||
* |
||||
* @return 远程文件是否存在 |
||||
*/ |
||||
@Throws(IOException::class) |
||||
fun indexFileInfo(): Boolean { |
||||
propFindResponse(ArrayList())?.let { response -> |
||||
if (!response.isSuccessful) { |
||||
this.exists = false |
||||
return false |
||||
} |
||||
response.body()?.let { |
||||
if (it.string().isNotEmpty()) { |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
/** |
||||
* 列出当前路径下的文件 |
||||
* |
||||
* @param propsList 指定列出文件的哪些属性 |
||||
* @return 文件列表 |
||||
*/ |
||||
@Throws(IOException::class) |
||||
@JvmOverloads |
||||
fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> { |
||||
propFindResponse(propsList)?.let { response -> |
||||
if (response.isSuccessful) { |
||||
response.body()?.let { body -> |
||||
return parseDir(body.string()) |
||||
} |
||||
} |
||||
} |
||||
return ArrayList() |
||||
} |
||||
|
||||
@Throws(IOException::class) |
||||
private fun propFindResponse(propsList: ArrayList<String>, depth: Int = 1): Response? { |
||||
val requestProps = StringBuilder() |
||||
for (p in propsList) { |
||||
requestProps.append("<a:").append(p).append("/>\n") |
||||
} |
||||
val requestPropsStr: String |
||||
requestPropsStr = if (requestProps.toString().isEmpty()) { |
||||
DIR.replace("%s", "") |
||||
} else { |
||||
String.format(DIR, requestProps.toString() + "\n") |
||||
} |
||||
httpUrl?.let { url -> |
||||
val request = Request.Builder() |
||||
.url(url) |
||||
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 |
||||
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 |
||||
.method("PROPFIND", RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)) |
||||
|
||||
HttpAuth.auth?.let { |
||||
request.header( |
||||
"Authorization", |
||||
Credentials.basic(it.user, it.pass) |
||||
) |
||||
} |
||||
request.header("Depth", if (depth < 0) "infinity" else depth.toString()) |
||||
return HttpUtil.getOkHttpClient().newCall(request.build()).execute() |
||||
} |
||||
return null |
||||
} |
||||
|
||||
private fun parseDir(s: String): List<WebDav> { |
||||
val list = ArrayList<WebDav>() |
||||
val document = Jsoup.parse(s) |
||||
val elements = document.getElementsByTag("d:response") |
||||
httpUrl?.let { url -> |
||||
val baseUrl = if (url.endsWith("/")) url else "$url/" |
||||
for (element in elements) { |
||||
val href = element.getElementsByTag("d:href")[0].text() |
||||
if (!href.endsWith("/")) { |
||||
val fileName = href.substring(href.lastIndexOf("/") + 1) |
||||
val webDavFile: WebDav |
||||
try { |
||||
webDavFile = WebDav(baseUrl + fileName) |
||||
webDavFile.displayName = fileName |
||||
webDavFile.urlName = href |
||||
list.add(webDavFile) |
||||
} catch (e: MalformedURLException) { |
||||
e.printStackTrace() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return list |
||||
} |
||||
|
||||
/** |
||||
* 根据自己的URL,在远程处创建对应的文件夹 |
||||
* |
||||
* @return 是否创建成功 |
||||
*/ |
||||
@Throws(IOException::class) |
||||
fun makeAsDir(): Boolean { |
||||
httpUrl?.let { url -> |
||||
val request = Request.Builder() |
||||
.url(url) |
||||
.method("MKCOL", null) |
||||
return execRequest(request) |
||||
} |
||||
return false |
||||
} |
||||
|
||||
/** |
||||
* 下载到本地 |
||||
* |
||||
* @param savedPath 本地的完整路径,包括最后的文件名 |
||||
* @param replaceExisting 是否替换本地的同名文件 |
||||
* @return 下载是否成功 |
||||
*/ |
||||
fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean { |
||||
if (File(savedPath).exists()) { |
||||
if (!replaceExisting) return false |
||||
} |
||||
val inputS = getInputStream() ?: return false |
||||
File(savedPath).writeBytes(inputS.readBytes()) |
||||
return true |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
*/ |
||||
@Throws(IOException::class) |
||||
@JvmOverloads |
||||
fun upload(localPath: String, contentType: String? = null): Boolean { |
||||
val file = File(localPath) |
||||
if (!file.exists()) return false |
||||
val mediaType = if (contentType == null) null else MediaType.parse(contentType) |
||||
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 |
||||
val fileBody = RequestBody.create(mediaType, file) |
||||
httpUrl?.let { |
||||
val request = Request.Builder() |
||||
.url(it) |
||||
.put(fileBody) |
||||
return execRequest(request) |
||||
} |
||||
return false |
||||
} |
||||
|
||||
/** |
||||
* 执行请求,获取响应结果 |
||||
* @param requestBuilder 因为还需要追加验证信息,所以此处传递Request.Builder的对象,而不是Request的对象 |
||||
* @return 请求执行的结果 |
||||
*/ |
||||
@Throws(IOException::class) |
||||
private fun execRequest(requestBuilder: Request.Builder): Boolean { |
||||
HttpAuth.auth?.let { |
||||
requestBuilder.header( |
||||
"Authorization", |
||||
Credentials.basic(it.user, it.pass) |
||||
) |
||||
} |
||||
val response = HttpUtil.getOkHttpClient().newCall(requestBuilder.build()).execute() |
||||
return response.isSuccessful |
||||
} |
||||
|
||||
private fun getInputStream(): InputStream? { |
||||
httpUrl?.let { url -> |
||||
val request = Request.Builder().url(url) |
||||
HttpAuth.auth?.let { |
||||
request.header("Authorization", Credentials.basic(it.user, it.pass)) |
||||
} |
||||
try { |
||||
return HttpUtil.getOkHttpClient().newCall(request.build()).execute().body()?.byteStream() |
||||
} catch (e: IOException) { |
||||
e.printStackTrace() |
||||
} catch (e: IllegalArgumentException) { |
||||
e.printStackTrace() |
||||
} |
||||
} |
||||
return null |
||||
} |
||||
|
||||
} |
@ -0,0 +1,16 @@ |
||||
package xyz.fycz.myreader.util.webdav.http |
||||
|
||||
import java.net.URL |
||||
import java.net.URLConnection |
||||
import java.net.URLStreamHandler |
||||
|
||||
object Handler : URLStreamHandler() { |
||||
|
||||
override fun getDefaultPort(): Int { |
||||
return 80 |
||||
} |
||||
|
||||
public override fun openConnection(u: URL): URLConnection? { |
||||
return null |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
package xyz.fycz.myreader.util.webdav.http |
||||
|
||||
object HttpAuth { |
||||
|
||||
var auth: Auth? = null |
||||
|
||||
class Auth internal constructor(val user: String, val pass: String) |
||||
|
||||
} |
@ -0,0 +1,158 @@ |
||||
package xyz.fycz.myreader.widget |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.content.Context |
||||
import android.graphics.* |
||||
import android.graphics.drawable.Drawable |
||||
import android.text.TextPaint |
||||
import android.util.AttributeSet |
||||
import com.bumptech.glide.load.DataSource |
||||
import com.bumptech.glide.load.engine.GlideException |
||||
import com.bumptech.glide.request.RequestListener |
||||
import com.bumptech.glide.request.target.Target |
||||
import xyz.fycz.myreader.R |
||||
import xyz.fycz.myreader.util.utils.ImageLoader |
||||
|
||||
|
||||
class CoverImageView : androidx.appcompat.widget.AppCompatImageView { |
||||
internal var width: Float = 0.toFloat() |
||||
internal var height: Float = 0.toFloat() |
||||
private var nameHeight = 0f |
||||
private var authorHeight = 0f |
||||
private val namePaint = TextPaint() |
||||
private val authorPaint = TextPaint() |
||||
private var name: String? = null |
||||
private var author: String? = null |
||||
private var loadFailed = false |
||||
|
||||
constructor(context: Context) : super(context) |
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) |
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( |
||||
context, |
||||
attrs, |
||||
defStyleAttr |
||||
) |
||||
|
||||
init { |
||||
namePaint.typeface = Typeface.DEFAULT_BOLD |
||||
namePaint.isAntiAlias = true |
||||
namePaint.textAlign = Paint.Align.CENTER |
||||
namePaint.textSkewX = -0.2f |
||||
authorPaint.typeface = Typeface.DEFAULT |
||||
authorPaint.isAntiAlias = true |
||||
authorPaint.textAlign = Paint.Align.CENTER |
||||
authorPaint.textSkewX = -0.1f |
||||
} |
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { |
||||
val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) |
||||
val measuredHeight = measuredWidth * 7 / 5 |
||||
super.onMeasure( |
||||
widthMeasureSpec, |
||||
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) |
||||
) |
||||
} |
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { |
||||
super.onLayout(changed, left, top, right, bottom) |
||||
width = getWidth().toFloat() |
||||
height = getHeight().toFloat() |
||||
namePaint.textSize = width / 6 |
||||
namePaint.strokeWidth = namePaint.textSize / 10 |
||||
authorPaint.textSize = width / 9 |
||||
authorPaint.strokeWidth = authorPaint.textSize / 10 |
||||
nameHeight = height / 2 |
||||
authorHeight = nameHeight + authorPaint.fontSpacing |
||||
} |
||||
|
||||
override fun onDraw(canvas: Canvas) { |
||||
if (width >= 10 && height > 10) { |
||||
@SuppressLint("DrawAllocation") |
||||
val path = Path() |
||||
//四个圆角 |
||||
path.moveTo(10f, 0f) |
||||
path.lineTo(width - 10, 0f) |
||||
path.quadTo(width, 0f, width, 10f) |
||||
path.lineTo(width, height - 10) |
||||
path.quadTo(width, height, width - 10, height) |
||||
path.lineTo(10f, height) |
||||
path.quadTo(0f, height, 0f, height - 10) |
||||
path.lineTo(0f, 10f) |
||||
path.quadTo(0f, 0f, 10f, 0f) |
||||
|
||||
canvas.clipPath(path) |
||||
} |
||||
super.onDraw(canvas) |
||||
if (!loadFailed) return |
||||
name?.let { |
||||
namePaint.color = Color.WHITE |
||||
namePaint.style = Paint.Style.STROKE |
||||
canvas.drawText(it, width / 2, nameHeight, namePaint) |
||||
namePaint.color = Color.RED |
||||
namePaint.style = Paint.Style.FILL |
||||
canvas.drawText(it, width / 2, nameHeight, namePaint) |
||||
} |
||||
author?.let { |
||||
authorPaint.color = Color.WHITE |
||||
authorPaint.style = Paint.Style.STROKE |
||||
canvas.drawText(it, width / 2, authorHeight, authorPaint) |
||||
authorPaint.color = Color.RED |
||||
authorPaint.style = Paint.Style.FILL |
||||
canvas.drawText(it, width / 2, authorHeight, authorPaint) |
||||
} |
||||
} |
||||
|
||||
fun setHeight(height: Int) { |
||||
val width = height * 5 / 7 |
||||
minimumWidth = width |
||||
} |
||||
|
||||
private fun setText(name: String?, author: String?) { |
||||
this.name = |
||||
when { |
||||
name == null -> null |
||||
name.length > 5 -> name.substring(0, 4) + "…" |
||||
else -> name |
||||
} |
||||
this.author = |
||||
when { |
||||
author == null -> null |
||||
author.length > 8 -> author.substring(0, 7) + "…" |
||||
else -> author |
||||
} |
||||
} |
||||
|
||||
fun load(path: String?, name: String?, author: String?) { |
||||
setText(name, author) |
||||
ImageLoader.load(context, path)//Glide自动识别http://和file:// |
||||
.placeholder(R.mipmap.default_cover) |
||||
.error(R.mipmap.default_cover) |
||||
.listener(object : RequestListener<Drawable> { |
||||
override fun onLoadFailed( |
||||
e: GlideException?, |
||||
model: Any?, |
||||
target: Target<Drawable>?, |
||||
isFirstResource: Boolean |
||||
): Boolean { |
||||
loadFailed = true |
||||
return false |
||||
} |
||||
|
||||
override fun onResourceReady( |
||||
resource: Drawable?, |
||||
model: Any?, |
||||
target: Target<Drawable>?, |
||||
dataSource: DataSource?, |
||||
isFirstResource: Boolean |
||||
): Boolean { |
||||
loadFailed = false |
||||
return false |
||||
} |
||||
|
||||
}) |
||||
.centerCrop() |
||||
.into(this) |
||||
} |
||||
} |
@ -0,0 +1,105 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout |
||||
xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
xmlns:tools="http://schemas.android.com/tools" |
||||
android:orientation="vertical"> |
||||
|
||||
<include layout="@layout/toolbar"/> |
||||
<LinearLayout |
||||
android:id="@+id/webdav_setting_webdav_url" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="60dp" |
||||
android:paddingTop="8dp" |
||||
android:paddingLeft="20dp" |
||||
android:paddingRight="20dp" |
||||
android:orientation="vertical" |
||||
android:background="@drawable/selector_common_bg"> |
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:textSize="@dimen/text_normal_size" |
||||
android:textColor="@color/textPrimary" |
||||
android:text="@string/webdav_url"/> |
||||
<TextView |
||||
android:id="@+id/tv_webdav_url" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:paddingTop="5dp" |
||||
android:textColor="@color/textSecondary" |
||||
tools:text="https://dav.jianguoyun.com/dav/"/> |
||||
</LinearLayout> |
||||
|
||||
<LinearLayout |
||||
android:id="@+id/webdav_setting_webdav_account" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="60dp" |
||||
android:paddingTop="8dp" |
||||
android:paddingLeft="20dp" |
||||
android:paddingRight="20dp" |
||||
android:orientation="vertical" |
||||
android:background="@drawable/selector_common_bg"> |
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:textSize="@dimen/text_normal_size" |
||||
android:textColor="@color/textPrimary" |
||||
android:text="@string/webdav_account"/> |
||||
<TextView |
||||
android:id="@+id/tv_webdav_account" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:paddingTop="5dp" |
||||
android:textColor="@color/textSecondary" |
||||
tools:text="输入你的WebDav账号"/> |
||||
</LinearLayout> |
||||
|
||||
<LinearLayout |
||||
android:id="@+id/webdav_setting_webdav_password" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="60dp" |
||||
android:paddingTop="8dp" |
||||
android:paddingLeft="20dp" |
||||
android:paddingRight="20dp" |
||||
android:orientation="vertical" |
||||
android:background="@drawable/selector_common_bg"> |
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:textSize="@dimen/text_normal_size" |
||||
android:textColor="@color/textPrimary" |
||||
android:text="@string/webdav_password"/> |
||||
<TextView |
||||
android:id="@+id/tv_webdav_password" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:paddingTop="5dp" |
||||
android:textColor="@color/textSecondary" |
||||
tools:text="输入你的WebDav授权密码"/> |
||||
</LinearLayout> |
||||
|
||||
<LinearLayout |
||||
android:id="@+id/webdav_setting_webdav_restore" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="60dp" |
||||
android:paddingTop="8dp" |
||||
android:paddingLeft="20dp" |
||||
android:paddingRight="20dp" |
||||
android:orientation="vertical" |
||||
android:background="@drawable/selector_common_bg"> |
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:textSize="@dimen/text_normal_size" |
||||
android:textColor="@color/textPrimary" |
||||
android:text="@string/menu_backup_restore"/> |
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:paddingTop="5dp" |
||||
android:textColor="@color/textSecondary" |
||||
android:text="@string/webdav_restore_tip"/> |
||||
</LinearLayout> |
||||
|
||||
</LinearLayout> |
@ -1,2 +1,2 @@ |
||||
#Fri Oct 02 22:23:04 CST 2020 |
||||
VERSION_CODE=150 |
||||
#Sat Oct 03 16:52:44 CST 2020 |
||||
VERSION_CODE=151 |
||||
|
Loading…
Reference in new issue