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