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; |
package xyz.fycz.myreader.ui.dialog; |
||||||
|
|
||||||
import android.content.Context; |
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 androidx.appcompat.app.AlertDialog; |
||||||
|
import com.google.android.material.textfield.TextInputLayout; |
||||||
import xyz.fycz.myreader.R; |
import xyz.fycz.myreader.R; |
||||||
|
import xyz.fycz.myreader.application.MyApplication; |
||||||
|
import xyz.fycz.myreader.util.StringHelper; |
||||||
|
|
||||||
/** |
/** |
||||||
* @author fengyue |
* @author fengyue |
||||||
* @date 2020/9/20 9:48 |
* @date 2020/9/20 9:48 |
||||||
*/ |
*/ |
||||||
public class MyAlertDialog { |
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); |
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 |
#Sat Oct 03 16:52:44 CST 2020 |
||||||
VERSION_CODE=150 |
VERSION_CODE=151 |
||||||
|
Loading…
Reference in new issue