diff --git a/app/build.gradle b/app/build.gradle index 7111c400d..0219ed133 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion compileSdk rootProject.ext.compileSdkVersion - versionCode 157 - versionName "3.0.6" + versionCode 160 + versionName "3.0.7" multiDexEnabled true resValue "string", "authorities", defaultConfig.applicationId + '.debug.cameraupload.provider' @@ -184,7 +184,6 @@ android { // Firebase implementation platform('com.google.firebase:firebase-bom:33.2.0') implementation 'com.google.firebase:firebase-crashlytics' - implementation 'com.google.firebase:firebase-analytics' //media3 final def media3_version = '1.4.1' @@ -204,6 +203,9 @@ android { implementation "androidx.webkit:webkit:1.12.1" implementation 'com.google.android.flexbox:flexbox:3.0.0' +// implementation "androidx.paging:paging-runtime:3.3.5" +// implementation "androidx.paging:paging-rxjava3:3.3.5" + //https://github.com/material-components/material-components-android implementation "com.google.android.material:material:1.12.0" @@ -237,6 +239,17 @@ android { //https://github.com/KunMinX/UnPeek-LiveData implementation 'com.kunminx.arch:unpeek-livedata:7.8.0' + implementation 'org.commonmark:commonmark:0.21.0' +// implementation 'org.commonmark:commonmark-ext-gfm-tables:0.21.0' // 如果需要表格支持 + implementation 'org.commonmark:commonmark-ext-autolink:0.21.0' // 如果需要自动链接支持 + + //https://github.com/lzyzsd/JsBridge + implementation 'com.github.lzyzsd:jsbridge:1.0.4' + +// https://github.com/rubensousa/GravitySnapHelper +// implementation 'com.github.rubensousa:gravitysnaphelper:2.2.2' + + // implementation 'com.blankj:utilcode:1.30.7' @@ -246,8 +259,8 @@ android { //https://github.com/panpf/stickyitemdecoration implementation "io.github.panpf.stickyitemdecoration:stickyitemdecoration:1.0.2" - //preference - implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' + //Thanks: https://github.com/RikkaApps + //implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' //https://github.com/michaellee123/LiveEventBus implementation 'com.github.michaellee123:LiveEventBus:1.8.14' @@ -287,20 +300,20 @@ android { implementation 'com.github.yydcdut.RxMarkdown:markdown-processor:v0.1.3' -// //markdown + //https://noties.io/Markwon/ // final def markwon_version = '4.6.2' // implementation "io.noties.markwon:core:$markwon_version" // implementation "io.noties.markwon:editor:$markwon_version" -// implementation "io.noties.markwon:ext-latex:$markwon_version" // implementation "io.noties.markwon:ext-strikethrough:$markwon_version" // implementation "io.noties.markwon:ext-tables:$markwon_version" // implementation "io.noties.markwon:ext-tasklist:$markwon_version" +// implementation "io.noties.markwon:ext-latex:$markwon_version" // implementation "io.noties.markwon:html:$markwon_version" // implementation "io.noties.markwon:image:$markwon_version" // implementation "io.noties.markwon:image-glide:$markwon_version" -// implementation "io.noties.markwon:inline-parser:$markwon_version" // implementation "io.noties.markwon:linkify:$markwon_version" -// implementation "io.noties.markwon:syntax-highlight:$markwon_version" +// implementation "io.noties.markwon:inline-parser:$markwon_version" +// implementation "io.noties.markwon:recycler:$markwon_version" implementation 'com.madgag.spongycastle:core:1.54.0.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2846f793c..7bb73c189 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -204,13 +204,14 @@ - - + + + getAccountList() { List list = new ArrayList<>(); diff --git a/app/src/main/java/com/seafile/seadroid2/annotation/NotSupport.java b/app/src/main/java/com/seafile/seadroid2/annotation/NotSupport.java new file mode 100644 index 000000000..e36d44f7d --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/annotation/NotSupport.java @@ -0,0 +1,15 @@ +package com.seafile.seadroid2.annotation; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Target; + +/** + * There are no plans to develop this feature. + */ +@Target({TYPE, METHOD, CONSTRUCTOR, FIELD}) +public @interface NotSupport { +} diff --git a/app/src/main/java/com/seafile/seadroid2/config/GlideLoadConfig.java b/app/src/main/java/com/seafile/seadroid2/config/GlideLoadConfig.java index 4056c3008..c8cf72948 100644 --- a/app/src/main/java/com/seafile/seadroid2/config/GlideLoadConfig.java +++ b/app/src/main/java/com/seafile/seadroid2/config/GlideLoadConfig.java @@ -1,25 +1,32 @@ package com.seafile.seadroid2.config; +import androidx.annotation.DrawableRes; + +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.signature.ObjectKey; import com.seafile.seadroid2.R; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.ui.WidgetUtils; public class GlideLoadConfig { -// public static GlideUrl getGlideUrl(String url) { -// -// Account account = SupportAccountManager.getInstance().getCurrentAccount(); -// if (account == null) { -// return new GlideUrl(url, new LazyHeaders.Builder().build()); -// } -// -// String token = account.token; -// -// return new GlideUrl(url, new LazyHeaders.Builder() -// .addHeader("Authorization", "Token " + token) -// .build()); -// } + public static GlideUrl getGlideUrl(String url) { + + Account account = SupportAccountManager.getInstance().getCurrentAccount(); + if (account == null) { + return new GlideUrl(url, new LazyHeaders.Builder().build()); + } + + String token = account.token; + + return new GlideUrl(url, new LazyHeaders.Builder() + .addHeader("Authorization", "Token " + token) + .build()); + } // // public static GlideUrl getGlideUrl(String url, String token) { // return new GlideUrl(url, new LazyHeaders.Builder() @@ -34,12 +41,33 @@ public static RequestOptions getAvatarOptions() { .override(WidgetUtils.getThumbnailWidth(), WidgetUtils.getThumbnailWidth()); } + private final static RequestOptions _cacheableThumbnailOptions = new RequestOptions() + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .error(R.drawable.icon_image_error_filled) + .override(128); + + /** + * Get cacheable thumbnail options, width and height are both 128 + */ + public static RequestOptions getCacheableThumbnailOptions() { + return _cacheableThumbnailOptions; + } + public static RequestOptions getOptions() { return new RequestOptions() + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .fallback(R.drawable.file_image) .placeholder(R.drawable.file_image); } + + public static RequestOptions getCustomDrawableOptions(@DrawableRes int resId) { + return new RequestOptions() + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .fallback(resId) + .placeholder(resId); + } + public static RequestOptions getOptions(String key) { return new RequestOptions() .fallback(R.drawable.file_image) diff --git a/app/src/main/java/com/seafile/seadroid2/config/OriGlideUrl.java b/app/src/main/java/com/seafile/seadroid2/config/OriGlideUrl.java new file mode 100644 index 000000000..15b003beb --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/config/OriGlideUrl.java @@ -0,0 +1,38 @@ +package com.seafile.seadroid2.config; + +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.Headers; +import com.seafile.seadroid2.framework.util.SLogs; + +import java.net.URL; + +public class OriGlideUrl extends GlideUrl { + private String oriCacheKey; + + public OriGlideUrl(URL url) { + super(url); + } + + public OriGlideUrl(String url) { + super(url); + } + + public OriGlideUrl(String url, String oriCacheKey) { + super(url); + this.oriCacheKey = oriCacheKey; + } + + + public OriGlideUrl(URL url, Headers headers) { + super(url, headers); + } + + public OriGlideUrl(String url, Headers headers) { + super(url, headers); + } + + @Override + public String getCacheKey() { + return oriCacheKey; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/config/WebViewActionConstant.java b/app/src/main/java/com/seafile/seadroid2/config/WebViewActionConstant.java new file mode 100644 index 000000000..5cdb5fda2 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/config/WebViewActionConstant.java @@ -0,0 +1,19 @@ +package com.seafile.seadroid2.config; + +public class WebViewActionConstant { + public final static String APP_VERSION_GET = "app.version.get"; + public final static String APP_TOAST_SHOW = "app.toast.show"; + public final static String PAGE_FINISH = "page.finish"; + public final static String PAGE_STATUS_COLOR_SET = "page.status.color.set"; + public final static String PAGE_STATUS_HEIGHT_GET = "page.status.height.get"; + + public static class CallJsFunction { + public final static String SDOC_OUTLINES_DATA_GET = "sdoc.outline.data.get"; + public final static String SDOC_OUTLINES_DATA_SELECT = "sdoc.outline.data.select"; + + public static class CallJsFunctionRequestCode { + public final static int REQ = 1001; + } + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/context/NavContext.java b/app/src/main/java/com/seafile/seadroid2/context/NavContext.java index 590b8d29e..d2b622ae8 100644 --- a/app/src/main/java/com/seafile/seadroid2/context/NavContext.java +++ b/app/src/main/java/com/seafile/seadroid2/context/NavContext.java @@ -5,7 +5,9 @@ import com.seafile.seadroid2.framework.data.model.BaseModel; import com.seafile.seadroid2.framework.data.db.entities.DirentModel; import com.seafile.seadroid2.framework.data.db.entities.RepoModel; +import com.seafile.seadroid2.framework.data.model.ContextModel; import com.seafile.seadroid2.framework.util.Utils; +import com.seafile.seadroid2.preferences.ContextStackPreferenceHelper; import java.util.Stack; @@ -32,6 +34,9 @@ public void clear() { } } + /** + * Push a model to the stack + */ public void push(BaseModel model) { if (model instanceof RepoModel) { //clear @@ -39,14 +44,74 @@ public void push(BaseModel model) { //push navStack.push(model); + + saveToSp(); + } else if (model instanceof DirentModel) { //stack navStack.push(model); + saveToSp(); + } else { throw new IllegalArgumentException("model must be RepoMode or DirentsModel."); } } + public void restoreNavContextFromSp() { + + navStack.clear(); + + Stack stack = ContextStackPreferenceHelper.getStack(); + if (stack != null && !stack.isEmpty()) { + for (ContextModel contextModel : stack) { + if (contextModel.type.equals("repo")) { + RepoModel repoModel = new RepoModel(); + repoModel.repo_id = contextModel.repo_id; + repoModel.repo_name = contextModel.repo_name; + repoModel.permission = contextModel.permission; + navStack.push(repoModel); + } else if (contextModel.type.equals("dirent")) { + DirentModel direntModel = new DirentModel(); + direntModel.repo_id = contextModel.repo_id; + direntModel.repo_name = contextModel.repo_name; + direntModel.full_path = contextModel.full_path; + direntModel.parent_dir = Utils.getParentPath(direntModel.full_path); + direntModel.name = Utils.getFileNameFromPath(contextModel.full_path); + direntModel.uid = direntModel.getUID(); + direntModel.permission = contextModel.permission; + navStack.push(direntModel); + } + } + } + + } + + private void saveToSp() { + Stack stack = new Stack<>(); + if (!navStack.isEmpty()) { + for (BaseModel baseModel : navStack) { + ContextModel contextModel = new ContextModel(); + + if (baseModel instanceof RepoModel e) { + contextModel.repo_id = e.repo_id; + contextModel.repo_name = e.repo_name; + contextModel.type = "repo"; + contextModel.full_path = "/"; + contextModel.permission = e.permission; + } else if (baseModel instanceof DirentModel e) { + contextModel.repo_id = e.repo_id; + contextModel.repo_name = e.repo_name; + contextModel.type = "dirent"; + contextModel.full_path = e.full_path; + contextModel.permission = e.permission; + } + stack.add(contextModel); + } + } + + ContextStackPreferenceHelper.saveStack(stack); + } + public void pop() { if (navStack.empty()) { return; @@ -54,6 +119,9 @@ public void pop() { //stack navStack.pop(); + + saveToSp(); + } public void switchToPath(RepoModel repoModel, String full_path) { @@ -87,6 +155,8 @@ public void switchToPath(RepoModel repoModel, String full_path) { for (DirentModel model : stack) { navStack.push(model); } + + saveToSp(); } /** diff --git a/app/src/main/java/com/seafile/seadroid2/enums/ActionModeCallbackType.java b/app/src/main/java/com/seafile/seadroid2/enums/ActionModeCallbackType.java index 91d496292..3a506655b 100644 --- a/app/src/main/java/com/seafile/seadroid2/enums/ActionModeCallbackType.java +++ b/app/src/main/java/com/seafile/seadroid2/enums/ActionModeCallbackType.java @@ -1,5 +1,5 @@ package com.seafile.seadroid2.enums; public enum ActionModeCallbackType { - CREATE, DESTORY, SELECT_ALL, SELECT_NONE + CREATE, DESTROY, SELECT_ALL, SELECT_NONE } diff --git a/app/src/main/java/com/seafile/seadroid2/enums/TransferDataSource.java b/app/src/main/java/com/seafile/seadroid2/enums/TransferDataSource.java index f8e4ee4df..a0b35e122 100644 --- a/app/src/main/java/com/seafile/seadroid2/enums/TransferDataSource.java +++ b/app/src/main/java/com/seafile/seadroid2/enums/TransferDataSource.java @@ -4,11 +4,13 @@ public enum TransferDataSource { ALBUM_BACKUP, /** - * + * (automatically) + * folder backup */ FOLDER_BACKUP, /** + * (manually) * share_to_seafile/upload_from_local */ FILE_BACKUP, diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/DirentDAO.java b/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/DirentDAO.java index faf9800dc..80705ca43 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/DirentDAO.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/DirentDAO.java @@ -20,17 +20,19 @@ public interface DirentDAO { Single> getListByAccount(String related_account); @Query("select * from dirents where parent_dir = :parent_dir and repo_id = :repo_id") - Single> getListByParentPath(String repo_id, String parent_dir); - - @Query("select * from dirents where parent_dir = :parent_dir and repo_id = :repo_id") - Single> getImageListByParentPath(String repo_id, String parent_dir); - + Single> getListByParentPathAsync(String repo_id, String parent_dir); @Query("select * from dirents where parent_dir = :parent_dir and repo_id = :repo_id") List getListByParentPathSync(String repo_id, String parent_dir); + @Query("select * from dirents where type='file' and parent_dir = :parent_dir and repo_id = :repo_id") + Single> getFileListByParentPath(String repo_id, String parent_dir); + + /** + * get special one by full_path + */ @Query("select * from dirents where full_path = :full_path and repo_id = :repo_id") - Single> getListByFullPath(String repo_id, String full_path); + Single> getListByFullPathAsync(String repo_id, String full_path); @Query("select * from dirents where full_path = :full_path and repo_id = :repo_id limit 1") List getListByFullPathSync(String repo_id, String full_path); diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/FileTransferDAO.java b/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/FileTransferDAO.java index 86585bf64..797747e97 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/FileTransferDAO.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/FileTransferDAO.java @@ -50,6 +50,9 @@ public interface FileTransferDAO { @Delete void deleteOne(FileTransferEntity entity); + @Delete + void deleteMultiple(List entity); + @Delete Single deleteOneAsync(FileTransferEntity entity); @@ -66,20 +69,20 @@ public interface FileTransferDAO { /** * Modify the task that is being in IN_PROGRESS to CANCELLED */ - @Query("update file_transfer_list set transfer_result = 'USER_CANCELLED', transfer_status = 'CANCELLED' where related_account = :related_account and data_source in ( :feats ) and transfer_status in ('IN_PROGRESS','WAITING')") - Completable cancelByDataSource(String related_account, List feats); + @Query("update file_transfer_list set transfer_status = 'CANCELLED', transfer_result = 'USER_CANCELLED' where related_account = :related_account and data_source in ( :feats ) and transfer_status in ('IN_PROGRESS','WAITING')") + Completable cancelAllByDataSource(String related_account, List feats); - @Query("update file_transfer_list set transfer_result = 'CANCELLED', transfer_status = 'CANCELLED', transfer_result = :result where related_account = :related_account and data_source = :dataSource and transfer_status in ('IN_PROGRESS','WAITING')") - void cancel(String related_account, TransferDataSource dataSource, TransferResult result); + @Query("update file_transfer_list set transfer_status = 'CANCELLED', transfer_result = :result where related_account = :related_account and data_source = 'FILE_BACKUP' and transfer_status in ('IN_PROGRESS','WAITING')") + void cancelWithFileBackup(String related_account, TransferResult result); - @Query("update file_transfer_list set transfer_status = 'CANCELLED', transfer_result = :result where data_source = 'FILE_BACKUP' and transfer_status in ('IN_PROGRESS','WAITING')") - void cancelWithFileBackup(TransferResult result); - - @Query("update file_transfer_list set transfer_status = 'CANCELLED', transfer_result = 'CANCELLED', data_status = -1, transferred_size = 0 where data_source in ('FILE_BACKUP','FOLDER_BACKUP')") - void cancelAllWithFileBackup(); + /** + * remove all tasks for ['FILE_BACKUP','FOLDER_BACKUP','ALBUM_BACKUP'] + */ + @Query("update file_transfer_list set transfer_status = 'CANCELLED', transfer_result = 'USER_CANCELLED', data_status = -1, transferred_size = 0 where related_account = :related_account and data_source in ('FILE_BACKUP','FOLDER_BACKUP','ALBUM_BACKUP')") + Completable removeAllUploadByAccount(String related_account); - @Query("select * from file_transfer_list where related_account = :related_account and data_source in ( :feats ) and data_status = 0 order by created_at asc") - Single> getListByFeatAsync(String related_account, List feats); + @Query("select * from file_transfer_list where related_account = :related_account and data_source in ( :feats ) order by created_at asc") + Single> getListByDataSourceAsync(String related_account, List feats); @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = :transfer_action and is_auto_transfer = 1 and transfer_status in ('IN_PROGRESS', 'WAITING') and data_source = :feature and data_status = 0 order by created_at asc limit 1") List getOnePendingTransferSync(String related_account, TransferAction transfer_action, TransferDataSource feature); @@ -87,27 +90,26 @@ public interface FileTransferDAO { @Query("select COUNT(*) from file_transfer_list where related_account = :related_account and transfer_action = :transfer_action and is_auto_transfer = 1 and transfer_status in ('IN_PROGRESS', 'WAITING') and data_source = :feature and data_status = 0") int countPendingTransferSync(String related_account, TransferAction transfer_action, TransferDataSource feature); + @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = :transferAction and transfer_status = :transferStatus and data_status = 0 order by created_at") + Single> getByActionAndStatusAsync(String related_account, TransferAction transferAction, TransferStatus transferStatus); - @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = :transfer_action and is_auto_transfer = 1 and transfer_status in ('IN_PROGRESS', 'WAITING') and data_status = 0 order by created_at asc limit :limit offset :offset") - List getPagePendingListTransferSync(String related_account, TransferAction transfer_action, int limit, int offset); @Query("select * from file_transfer_list where transfer_action = :transfer_action and is_auto_transfer = 1 and transfer_status in ('IN_PROGRESS', 'WAITING') and data_source = :feature and data_status = 0 order by created_at asc limit 1") List getOnePendingTransferAllAccountSync(TransferAction transfer_action, TransferDataSource feature); @Query("select * from file_transfer_list where related_account = :related_account and is_auto_transfer = 1 and transfer_action = 'DOWNLOAD' and transfer_status in ('IN_PROGRESS', 'WAITING') and data_status = 0 order by created_at asc limit 1") - List getOnePendingDownloadByActionSync(String related_account); + List getOnePendingDownloadByAccountSync(String related_account); @Query("select COUNT(*) from file_transfer_list where related_account = :related_account and is_auto_transfer = 1 and transfer_action = 'DOWNLOAD' and transfer_status in ('IN_PROGRESS', 'WAITING') and data_status = 0") int countPendingDownloadListSync(String related_account); + @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = 'UPLOAD' and data_source in ('FOLDER_BACKUP','FILE_BACKUP','ALBUM_BACKUP') and data_status = 0 order by created_at desc limit :limit offset :offset") + Single> getPageUploadListAsync(String related_account, int limit, int offset); - @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = 'UPLOAD' and data_source in ('FOLDER_BACKUP','FILE_BACKUP','ALBUM_BACKUP') and data_status = 0 order by modified_at desc limit :limit offset :offset") - List getPageUploadListSync(String related_account, int limit, int offset); - - @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = 'DOWNLOAD' and data_status = 0 order by modified_at desc limit :limit offset :offset") - List getPageDownloadListSync(String related_account, int limit, int offset); + @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = 'DOWNLOAD' and data_status = 0 order by created_at desc limit :limit offset :offset") + Single> getPageDownloadListAsync(String related_account, int limit, int offset); - @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = :transferAction and target_path = :target_path and data_status = 0 order by created_at desc limit 1") + @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = :transferAction and target_path = :target_path and data_status = 0 order by created_at desc limit 1") List getByTargetPathSync(String related_account, TransferAction transferAction, String target_path); @@ -127,16 +129,12 @@ public interface FileTransferDAO { @Query("select * from file_transfer_list where repo_id = :repoId and full_path IN(:fullPaths) and transfer_action = :transfer_action order by created_at asc") List getListByFullPathsSync(String repoId, List fullPaths, TransferAction transfer_action); - @Query("select * from file_transfer_list where repo_id = :repoId and transfer_action = :transfer_action order by created_at asc limit :limit offset :offset") - List getPageListSync(String repoId, TransferAction transfer_action, int limit, int offset); - - @Query("select * from file_transfer_list where related_account = :related_account and transfer_action = :transferAction and full_path = :full_path and data_status = 0 order by created_at") - List getListByFullPathSync(String related_account, TransferAction transferAction, String full_path); + @Query("select * from file_transfer_list where repo_id = :repoId and transfer_action = :transferAction and full_path = :full_path and data_status = 0 order by created_at") + Single> getListByFullPathAsync(String repoId, TransferAction transferAction, String full_path); @Query("select COUNT(*) from file_transfer_list where repo_id = :repoId and full_path = :fullPath and transfer_action = :transfer_action and data_source = :feature and data_status = 0 ") int checkOneByFullPath(String repoId, String fullPath, TransferAction transfer_action, TransferDataSource feature); - @RawQuery int updateEntityStatus(SupportSQLiteQuery query); diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/PermissionDAO.java b/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/PermissionDAO.java index 66fe2de1a..6018ed59b 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/PermissionDAO.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/db/dao/PermissionDAO.java @@ -21,12 +21,11 @@ public interface PermissionDAO { @Query("select * from permissions where id IN (:ids)") Single> getByIdsAsync(List ids); - @Query("select * from permissions where repo_id = :repoId and id = :id limit 1") - Single> getWithAsync(String repoId, int id); - @Query("select * from permissions where repo_id = :repoId") Single> getByRepoIdAsync(String repoId); + @Query("select * from permissions where repo_id = :repoId and id = :pid limit 1") + Single> getByRepoAndIdAsync(String repoId, int pid); @Query("DELETE FROM permissions") diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/DirentModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/DirentModel.java index fdcddfdf2..23fe3b00c 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/DirentModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/DirentModel.java @@ -94,6 +94,10 @@ public boolean isDir() { return TextUtils.equals(type, "dir"); } + public String getName() { + return name; + } + public String getSubtitle() { if (TextUtils.isEmpty(timestamp)) { timestamp = Utils.translateCommitTime(mtime * 1000); diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/FileTransferEntity.java b/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/FileTransferEntity.java index 0ca8f6f12..1c1d9d97b 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/FileTransferEntity.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/FileTransferEntity.java @@ -106,8 +106,12 @@ public class FileTransferEntity extends BaseModel { *
* eg. /storage/emulated/0/DCIM/xxx.jpg or /storage/emulated/0/Downloads/xxx.txt (locally) *

+ *

UPLOAD (FILE_BACKUP Manually)

+ *

+ * full_path is the absolute path to the file stored locally *
- *
+ * eg. /storage/emulated/0/Android/media/(package_name)/Seafile/(repo_name)/ + *

*

DOWNLOAD

*

full_path is the relative path to the file in the repository.
* eg. /a/b/c/d.txt (in remote repo)

@@ -361,44 +365,6 @@ public static FileTransferEntity convertDirentModel2This(boolean is_block, boole return entity; } - public static FileTransferEntity convertDirentFileModel2This(RepoModel repoModel, String full_path, boolean is_auto_transfer, DirentFileModel direntModel) { - FileTransferEntity entity = new FileTransferEntity(); - entity.full_path = full_path; -// entity.target_path = direntModel.full_path; - entity.data_source = TransferDataSource.DOWNLOAD; - entity.repo_id = repoModel.repo_id; - entity.repo_name = repoModel.repo_name; - entity.related_account = repoModel.related_account; - - - entity.file_id = direntModel.id; - entity.setParent_path(Utils.getParentPath(full_path)); - entity.file_name = direntModel.name; - entity.file_format = FileTools.getFileExtension(entity.full_path); - entity.mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entity.file_format); - entity.file_size = direntModel.size; - entity.file_md5 = null; - - entity.is_auto_transfer = is_auto_transfer; - -// entity.is_block = repoModel.canLocalDecrypt(); - entity.file_strategy = ExistingFileStrategy.AUTO; - - entity.is_copy_to_local = true; - - long now = System.currentTimeMillis(); - entity.created_at = now; - entity.modified_at = direntModel.mtime * 1000; - entity.action_end_at = 0L; - - entity.transfer_action = TransferAction.DOWNLOAD; - entity.transfer_status = TransferStatus.WAITING; - entity.transfer_result = TransferResult.NO_RESULT; - - entity.uid = entity.getUID(); - - return entity; - } public static FileTransferEntity convertDirentRecursiveModel2This(RepoModel repoModel, DirentRecursiveFileModel model) { @@ -444,7 +410,7 @@ public static FileTransferEntity convertDirentRecursiveModel2This(RepoModel repo return entity; } - public static FileTransferEntity convert2ThisForUploadFileSyncWorker(Account account, RepoModel repoModel, File file, String backupPath) { + public static FileTransferEntity convert2ThisForUploadFileSyncWorker(Account account, File file, String backupPath) { if (!file.isFile()) { return null; } @@ -466,8 +432,8 @@ public static FileTransferEntity convert2ThisForUploadFileSyncWorker(Account acc entity.mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entity.file_format); // entity.is_block = repoModel.encrypted; - entity.repo_id = repoModel.repo_id; - entity.repo_name = repoModel.repo_name; +// entity.repo_id = repoModel.repo_id; +// entity.repo_name = repoModel.repo_name; entity.related_account = account.getSignature(); entity.data_source = TransferDataSource.FOLDER_BACKUP; entity.created_at = System.currentTimeMillis(); @@ -489,7 +455,7 @@ public static FileTransferEntity convert2ThisForUploadFileSyncWorker(Account acc } - public static FileTransferEntity convert2ThisForUploadMediaSyncWorker(Account account, String repo_id, String repo_name, File file, String parenPath, long dateAdd, boolean isRemoteExists) { + public static FileTransferEntity convert2ThisForUploadMediaSyncWorker(Account account, File file, String parenPath, long dateAdd, boolean isRemoteExists) { long now = System.currentTimeMillis(); FileTransferEntity entity = new FileTransferEntity(); @@ -502,8 +468,8 @@ public static FileTransferEntity convert2ThisForUploadMediaSyncWorker(Account ac entity.file_md5 = FileUtils.getFileMD5ToString(entity.full_path).toLowerCase(); entity.mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entity.file_format); // entity.is_block = false; //album backup is not store in encrypted repo. - entity.repo_id = repo_id; - entity.repo_name = repo_name; +// entity.repo_id = repo_id; +// entity.repo_name = repo_name; entity.related_account = account.getSignature(); entity.created_at = now; entity.modified_at = now; diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/PermissionEntity.java b/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/PermissionEntity.java index 81f830563..c08d90f35 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/PermissionEntity.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/PermissionEntity.java @@ -7,10 +7,10 @@ import androidx.room.Ignore; import com.seafile.seadroid2.framework.data.model.BaseModel; +import com.seafile.seadroid2.framework.data.model.permission.PermissionParentModel; import com.seafile.seadroid2.framework.data.model.permission.PermissionWrapperModel; import java.util.HashMap; -import java.util.Set; @Entity(tableName = "permissions", primaryKeys = {"repo_id", "id"}) public class PermissionEntity extends BaseModel { @@ -32,74 +32,29 @@ public class PermissionEntity extends BaseModel { public boolean modify; public boolean download_external_link; + public PermissionEntity() { + } /** - * key -> repo_id or dirent_id(uid) + * @return false if the permission is empty */ - @Ignore - public HashMap cachedMap; - - public boolean hasId(String id) { - if (cachedMap == null) { - return false; - } - - return cachedMap.containsKey(id); - } - - public void cacheBaseModel(BaseModel model) { - if (cachedMap == null) { - cachedMap = new HashMap<>(); - return; - } - - if (model instanceof RepoModel m) { - cachedMap.put(m.repo_id, m); - } else if (model instanceof DirentModel m) { - cachedMap.put(m.uid, m); - } - } - - public void removeById(String id) { - if (cachedMap == null) { - return; - } - - cachedMap.remove(id); - } - - public void removeByModel(BaseModel model) { - if (cachedMap == null) { - return; - } - - if (model instanceof RepoModel m) { - cachedMap.remove(m.repo_id); - } else if (model instanceof DirentModel m) { - cachedMap.remove(m.uid); - } - } - - public boolean isEmptyIds() { - return cachedMap == null || cachedMap.isEmpty(); - } - - public PermissionEntity() { + public boolean isValid() { + return !TextUtils.isEmpty(repo_id); } - public PermissionEntity(@NonNull String repoId, @NonNull PermissionWrapperModel model) { - this.id = model.id; - this.name = model.name; - this.description = model.description; - - this.create = model.permission.create; - this.upload = model.permission.upload; - this.download = model.permission.download; - this.copy = model.permission.copy; - this.delete = model.permission.delete; - this.modify = model.permission.modify; - this.download_external_link = model.permission.download_external_link; - this.preview = model.permission.preview; + public PermissionEntity(@NonNull String repoId, @NonNull PermissionParentModel p) { + this.id = p.id; + this.name = p.name; + this.description = p.description; + + this.create = p.permission.create; + this.upload = p.permission.upload; + this.download = p.permission.download; + this.copy = p.permission.copy; + this.delete = p.permission.delete; + this.modify = p.permission.modify; + this.download_external_link = p.permission.download_external_link; + this.preview = p.permission.preview; this.repo_id = repoId; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/StarredModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/StarredModel.java index 360cb8d70..61496e97e 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/StarredModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/db/entities/StarredModel.java @@ -60,7 +60,7 @@ public boolean isRepo() { return TextUtils.equals("/", path) && is_dir; } - public static DirentModel converterThis2DirentModel(StarredModel model) { + public static DirentModel convert2DirentModel(StarredModel model) { DirentModel d = new DirentModel(); d.full_path = model.path; d.type = model.is_dir ? "dir" : "file"; @@ -68,6 +68,7 @@ public static DirentModel converterThis2DirentModel(StarredModel model) { d.name = model.obj_name; d.repo_id = model.repo_id; d.repo_name = model.repo_name; + d.parent_dir = Utils.getParentPath(model.path); return d; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/ContextModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/ContextModel.java new file mode 100644 index 000000000..70ac51ebf --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/ContextModel.java @@ -0,0 +1,14 @@ +package com.seafile.seadroid2.framework.data.model; + +public class ContextModel { + public String repo_id; + public String repo_name; //repo_name + + public String type; //repo/dirent + + /** + * parent_dir + name + */ + public String full_path; + public String permission; +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/WebRouteModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/WebRouteModel.java new file mode 100644 index 000000000..8fae7b728 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/WebRouteModel.java @@ -0,0 +1,11 @@ +package com.seafile.seadroid2.framework.data.model; + +public class WebRouteModel { + public int v = 1; + public String action; + + /** + * params data + */ + public String data; +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/activities/ActivityModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/activities/ActivityModel.java index 08cffe25f..53cdffc6f 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/activities/ActivityModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/activities/ActivityModel.java @@ -47,7 +47,7 @@ public boolean isDir() { } - public static DirentModel converterThis2DirentModel(ActivityModel model){ + public static DirentModel convert2DirentModel(ActivityModel model){ DirentModel d = new DirentModel(); d.full_path = model.path; d.type = model.obj_type; @@ -55,7 +55,7 @@ public static DirentModel converterThis2DirentModel(ActivityModel model){ d.name = model.name; d.repo_id = model.repo_id; d.repo_name = model.repo_name; - + d.parent_dir = Utils.getParentPath(model.path); return d; } } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocCommentModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentModel.java similarity index 70% rename from app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocCommentModel.java rename to app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentModel.java index 9260574cd..06c7b55cb 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocCommentModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentModel.java @@ -1,13 +1,15 @@ -package com.seafile.seadroid2.framework.data.model.sdoc; +package com.seafile.seadroid2.framework.data.model.docs_comment; import com.google.gson.annotations.JsonAdapter; import com.seafile.seadroid2.framework.data.model.adapter.OffsetDateTimeAdapter; +import com.seafile.seadroid2.framework.data.model.sdoc.SDocCommentReplyModel; +import com.seafile.seadroid2.view.rich_edittext.RichEditText; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.List; -public class SDocCommentModel { +public class DocsCommentModel { public int id; public String item_name; public String parent_path; @@ -16,7 +18,7 @@ public class SDocCommentModel { public List replies; public String comment; public String repo_id; - public String resolved; + public boolean resolved; public String user_contact_email; public String user_email; public String user_name; @@ -29,6 +31,10 @@ public class SDocCommentModel { public String detail; + + public boolean isContainImage = false; + public List commentList; + public String getCreatedAtFriendlyText() { return created_at.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).replace("T", " "); } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentWrapperModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentWrapperModel.java new file mode 100644 index 000000000..e3506abb4 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentWrapperModel.java @@ -0,0 +1,5 @@ +package com.seafile.seadroid2.framework.data.model.docs_comment; + +public class DocsCommentWrapperModel { + public DocsCommentModel comment; +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentsWrapperModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentsWrapperModel.java new file mode 100644 index 000000000..4ec05381b --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsCommentsWrapperModel.java @@ -0,0 +1,7 @@ +package com.seafile.seadroid2.framework.data.model.docs_comment; + +import java.util.List; + +public class DocsCommentsWrapperModel { + public List comments; +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsUploadResultModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsUploadResultModel.java new file mode 100644 index 000000000..a5d8c2974 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/docs_comment/DocsUploadResultModel.java @@ -0,0 +1,7 @@ +package com.seafile.seadroid2.framework.data.model.docs_comment; + +import java.util.List; + +public class DocsUploadResultModel { + public List relative_path; +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionListWrapperModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionListWrapperModel.java index 8694d5045..902a6a4f3 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionListWrapperModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionListWrapperModel.java @@ -1,9 +1,7 @@ package com.seafile.seadroid2.framework.data.model.permission; -import com.seafile.seadroid2.framework.data.db.entities.PermissionEntity; - import java.util.List; public class PermissionListWrapperModel { - public List permission_list; + public List permission_list; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionParentModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionParentModel.java new file mode 100644 index 000000000..60171f14a --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionParentModel.java @@ -0,0 +1,8 @@ +package com.seafile.seadroid2.framework.data.model.permission; + +public class PermissionParentModel { + public int id; + public String description; + public String name; + public PermissionModel permission; +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionWrapperModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionWrapperModel.java index 46f14fdb5..e14ddd5fd 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionWrapperModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/permission/PermissionWrapperModel.java @@ -1,8 +1,5 @@ package com.seafile.seadroid2.framework.data.model.permission; public class PermissionWrapperModel { - public int id; - public String description; - public String name; - public PermissionModel permission; + public PermissionParentModel permission; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocDetailModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileDetailModel.java similarity index 91% rename from app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocDetailModel.java rename to app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileDetailModel.java index 9f9aa26ba..77ea5f381 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocDetailModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileDetailModel.java @@ -13,7 +13,7 @@ import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; -public class SDocDetailModel implements Parcelable { +public class FileDetailModel implements Parcelable { private String type; private String id; @@ -183,10 +183,10 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeByte(this.canEdit ? (byte) 1 : (byte) 0); } - public SDocDetailModel() { + public FileDetailModel() { } - protected SDocDetailModel(Parcel in) { + protected FileDetailModel(Parcel in) { this.type = in.readString(); this.id = in.readString(); this.name = in.readString(); @@ -203,15 +203,15 @@ protected SDocDetailModel(Parcel in) { this.canEdit = in.readByte() != 0; } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override - public SDocDetailModel createFromParcel(Parcel source) { - return new SDocDetailModel(source); + public FileDetailModel createFromParcel(Parcel source) { + return new FileDetailModel(source); } @Override - public SDocDetailModel[] newArray(int size) { - return new SDocDetailModel[size]; + public FileDetailModel[] newArray(int size) { + return new FileDetailModel[size]; } }; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileProfileConfigModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileProfileConfigModel.java new file mode 100644 index 000000000..13a9549dd --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileProfileConfigModel.java @@ -0,0 +1,33 @@ +package com.seafile.seadroid2.framework.data.model.sdoc; + +import com.seafile.seadroid2.framework.data.model.user.UserWrapperModel; + +public class FileProfileConfigModel { + public UserWrapperModel users; + public FileDetailModel detail; + public MetadataConfigModel metadataConfigModel; + + public MetadataConfigModel getMetadataConfigModel() { + return metadataConfigModel; + } + + public void setMetadataConfigModel(MetadataConfigModel metadataConfigModel) { + this.metadataConfigModel = metadataConfigModel; + } + + public UserWrapperModel getUsers() { + return users; + } + + public void setUsers(UserWrapperModel users) { + this.users = users; + } + + public FileDetailModel getDetail() { + return detail; + } + + public void setDetail(FileDetailModel detail) { + this.detail = detail; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocRecordWrapperModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileRecordWrapperModel.java similarity index 57% rename from app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocRecordWrapperModel.java rename to app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileRecordWrapperModel.java index 4f1748c07..a104efff1 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocRecordWrapperModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/FileRecordWrapperModel.java @@ -5,7 +5,7 @@ import java.util.List; -public class SDocRecordWrapperModel implements Parcelable { +public class FileRecordWrapperModel implements Parcelable { public List metadata; public List results; @@ -20,23 +20,23 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeTypedList(this.results); } - public SDocRecordWrapperModel() { + public FileRecordWrapperModel() { } - protected SDocRecordWrapperModel(Parcel in) { + protected FileRecordWrapperModel(Parcel in) { this.metadata = in.createTypedArrayList(MetadataModel.CREATOR); this.results = in.createTypedArrayList(RecordResultModel.CREATOR); } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override - public SDocRecordWrapperModel createFromParcel(Parcel source) { - return new SDocRecordWrapperModel(source); + public FileRecordWrapperModel createFromParcel(Parcel source) { + return new FileRecordWrapperModel(source); } @Override - public SDocRecordWrapperModel[] newArray(int size) { - return new SDocRecordWrapperModel[size]; + public FileRecordWrapperModel[] newArray(int size) { + return new FileRecordWrapperModel[size]; } }; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/MetadataConfigModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/MetadataConfigModel.java index 78fbf7894..74fe5d623 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/MetadataConfigModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/MetadataConfigModel.java @@ -2,4 +2,8 @@ public class MetadataConfigModel { public boolean enabled; + //public boolean tags_enabled; + //public String tags_lang; + //public String details_settings; + //public boolean ocr_enabled; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/OutlineItemModel.java similarity index 74% rename from app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocModel.java rename to app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/OutlineItemModel.java index d8b316122..9ef5cea74 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/OutlineItemModel.java @@ -4,11 +4,11 @@ import java.util.List; -public class SDocModel extends BaseModel { +public class OutlineItemModel extends BaseModel { public String id; public String type; public String text; public boolean indent; - public List children; + public List children; public SDocDataModel data; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/RecordResultModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/RecordResultModel.java index 11386d1ed..e602da6ef 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/RecordResultModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/RecordResultModel.java @@ -37,6 +37,7 @@ public class RecordResultModel implements Parcelable { public String _suffix; public String _description; public List _owner; + public List _reviewer; @JsonAdapter(RecordResultDeserializer.class) public Map dynamicFields; @@ -49,6 +50,7 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeStringList(this._collaborators); + dest.writeStringList(this._reviewer); dest.writeString(this._creator); dest.writeString(this._ctime); dest.writeString(this._file_creator); @@ -80,6 +82,7 @@ public RecordResultModel() { protected RecordResultModel(Parcel in) { this._collaborators = in.createStringArrayList(); + this._reviewer = in.createStringArrayList(); this._creator = in.readString(); this._ctime = in.readString(); this._file_creator = in.readString(); diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocCommentWrapperModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocCommentWrapperModel.java deleted file mode 100644 index 796389d0c..000000000 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocCommentWrapperModel.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.seafile.seadroid2.framework.data.model.sdoc; - -import java.util.List; - -public class SDocCommentWrapperModel { - public List comments; -} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocWrapperModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocOutlineWrapperModel.java similarity index 67% rename from app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocWrapperModel.java rename to app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocOutlineWrapperModel.java index 03e7cfdea..3452258f7 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocWrapperModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocOutlineWrapperModel.java @@ -2,9 +2,9 @@ import java.util.List; -public class SDocWrapperModel { +public class SDocOutlineWrapperModel { public int version; public int format_version; public String last_modify_user; - public List elements; + public List elements; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocPageOptionsModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocPageOptionsModel.java index 74e1c0119..cdb715378 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocPageOptionsModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocPageOptionsModel.java @@ -13,6 +13,8 @@ public class SDocPageOptionsModel implements Parcelable { public boolean isLocked; public boolean isStarred; + public boolean enableMetadataManagement; + @Override public String toString() { return "SDocPageOptionsModel{" + @@ -42,6 +44,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.repoName); dest.writeByte(this.isLocked ? (byte) 1 : (byte) 0); dest.writeByte(this.isStarred ? (byte) 1 : (byte) 0); + dest.writeByte(this.enableMetadataManagement ? (byte) 1 : (byte) 0); } public SDocPageOptionsModel() { @@ -56,6 +59,7 @@ protected SDocPageOptionsModel(Parcel in) { this.repoName = in.readString(); this.isLocked = in.readByte() != 0; this.isStarred = in.readByte() != 0; + this.enableMetadataManagement = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @@ -69,180 +73,4 @@ public SDocPageOptionsModel[] newArray(int size) { return new SDocPageOptionsModel[size]; } }; -} - -// -// \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocProfileConfigModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocProfileConfigModel.java deleted file mode 100644 index 658ef12cd..000000000 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/sdoc/SDocProfileConfigModel.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.seafile.seadroid2.framework.data.model.sdoc; - -import com.seafile.seadroid2.framework.data.model.user.UserWrapperModel; - -public class SDocProfileConfigModel { - public UserWrapperModel users; - public MetadataConfigModel metadata; - public SDocDetailModel detail; - - public UserWrapperModel getUsers() { - return users; - } - - public void setUsers(UserWrapperModel users) { - this.users = users; - } - - public MetadataConfigModel getMetadata() { - return metadata; - } - - public void setMetadata(MetadataConfigModel metadata) { - this.metadata = metadata; - } - - public SDocDetailModel getDetail() { - return detail; - } - - public void setDetail(SDocDetailModel detail) { - this.detail = detail; - } -} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/model/search/SearchModel.java b/app/src/main/java/com/seafile/seadroid2/framework/data/model/search/SearchModel.java index 51d5cb61d..45230ee61 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/model/search/SearchModel.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/data/model/search/SearchModel.java @@ -6,6 +6,7 @@ import com.seafile.seadroid2.R; import com.seafile.seadroid2.framework.data.db.entities.DirentModel; +import com.seafile.seadroid2.framework.data.db.entities.RepoModel; import com.seafile.seadroid2.framework.data.model.BaseModel; import com.seafile.seadroid2.framework.util.Icons; import com.seafile.seadroid2.framework.util.Utils; @@ -66,7 +67,23 @@ public int getIcon() { return Icons.getFileIcon(getTitle()); } - public static DirentModel converterThis2DirentModel(SearchModel model) { + public static RepoModel convert2RepoModel(SearchModel model) { + RepoModel d = new RepoModel(); + d.type = model.repo_type; + d.repo_id = model.repo_id; + d.repo_name = model.repo_name; + d.related_account = "";//? +// d.last_modified = model.last_modified; + d.size = model.size; + + d.owner_email = model.repo_owner_email; + d.owner_name = model.repo_owner_name; + d.owner_contact_email = model.repo_owner_contact_email; + + return d; + } + + public static DirentModel convert2DirentModel(SearchModel model) { DirentModel d = new DirentModel(); d.full_path = model.fullpath; d.type = model.is_dir ? "dir" : "file"; @@ -76,6 +93,7 @@ public static DirentModel converterThis2DirentModel(SearchModel model) { d.repo_name = model.repo_name; d.last_modified_at = model.last_modified; d.size = model.size; + d.parent_dir = Utils.getParentPath(model.fullpath); return d; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/data/repository/DirentsRepository.java b/app/src/main/java/com/seafile/seadroid2/framework/data/repository/DirentsRepository.java deleted file mode 100644 index 809e37401..000000000 --- a/app/src/main/java/com/seafile/seadroid2/framework/data/repository/DirentsRepository.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.seafile.seadroid2.framework.data.repository; - -import com.seafile.seadroid2.framework.data.db.dao.DirentDAO; -import com.seafile.seadroid2.framework.data.db.entities.DirentModel; -import com.seafile.seadroid2.framework.data.model.repo.DirentWrapperModel; -import com.seafile.seadroid2.ui.repo.RepoService; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; - -import io.reactivex.Completable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.schedulers.Schedulers; - -public class DirentsRepository { - private DirentDAO dao; - private RepoService service; - - public DirentsRepository(RepoService service, DirentDAO dao) { - this.service = service; - this.dao = dao; - } - - public Single> getDirents(String repoId, String parent_dir) { - // 先尝试从数据库中获取数据 - Single> localData = dao.getListByParentPath(repoId, parent_dir) - .onErrorResumeNext(throwable -> Single.just(Collections.emptyList())); - - // 发起网络请求并保存到数据库 - Single> remoteData = service.getDirents(repoId, parent_dir) - .map(new Function>() { - @Override - public List apply(DirentWrapperModel direntWrapperModel) throws Exception { - return direntWrapperModel.dirent_list; - } - }) - .doOnSuccess(new Consumer>() { - @Override - public void accept(List direntModels) throws Exception { - -// dao.insertAll(dirents) - } - }); - - return Single.concat(localData, remoteData).first(Collections.emptyList()); - } - - public void insert(List dirents) { - Completable.fromCallable(new Callable() { - @Override - public Boolean call() throws Exception { - dao.insertAll(dirents); - return true; - } - }).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); - } - - public void deleteAll() { - dao.deleteAll(); - } -} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/datastore/DataStoreKeys.java b/app/src/main/java/com/seafile/seadroid2/framework/datastore/DataStoreKeys.java index 243f74ebf..35871b1be 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/datastore/DataStoreKeys.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/datastore/DataStoreKeys.java @@ -36,4 +36,6 @@ public class DataStoreKeys { public static final String KEY_DARK_MODE = "key_dark_mode"; public static final String KEY_SERVER_CERT_INFO = "key_server_cert_info"; + + public static final String KEY_NAV_CONTEXT_STACK = "key_nav_context_stack"; } \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/framework/datastore/sp_livedata/FolderBackupSharePreferenceHelper.java b/app/src/main/java/com/seafile/seadroid2/framework/datastore/sp_livedata/FolderBackupSharePreferenceHelper.java index 4fe6e2b00..4e0316935 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/datastore/sp_livedata/FolderBackupSharePreferenceHelper.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/datastore/sp_livedata/FolderBackupSharePreferenceHelper.java @@ -17,6 +17,8 @@ import java.lang.reflect.Type; import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; public class FolderBackupSharePreferenceHelper { @@ -155,6 +157,23 @@ public static void writeLastScanTimeForPath(String absPath) { sp.edit().putLong(k, System.currentTimeMillis()).apply(); } + public static void clearLastScanTimeForAllPath() { + SharedPreferences sp = Settings.getUserSharedPreferences(); + if (sp == null) { + return; + } + + Map m = sp.getAll(); + m.forEach(new BiConsumer() { + @Override + public void accept(String key, Object o) { + if (key.startsWith(SettingsManager.FOLDER_BACKUP_LAST_TIME_PREFIX)) { + sp.edit().remove(key).apply(); + } + } + }); + } + public static void clearLastScanTimeForPath(String absPath) { SharedPreferences sp = Settings.getUserSharedPreferences(); if (sp == null) { diff --git a/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/FileSyncService.java b/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/FileSyncService.java index 18a4d3b0f..bc361263f 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/FileSyncService.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/FileSyncService.java @@ -18,7 +18,6 @@ import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.framework.util.Utils; import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; -import com.seafile.seadroid2.framework.worker.observer.MediaContentObserver; import com.seafile.seadroid2.ui.camera_upload.CameraUploadManager; import org.apache.commons.io.monitor.FileAlterationListener; @@ -118,10 +117,10 @@ public void onCreate() { BackgroundJobManagerImpl.getInstance().startDownloadChainWorker(); //folder backup upload worker - BackgroundJobManagerImpl.getInstance().startFolderChainWorker(false); + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(false); //file upload backup - BackgroundJobManagerImpl.getInstance().startFileUploadWorker(); + BackgroundJobManagerImpl.getInstance().startFileManualUploadWorker(); //bus TransferBusHelper.getTransferObserver().observeForever(transferOpTypeObserver); @@ -142,7 +141,7 @@ private void onBusEvent(TransferOpType opType) { resetFolderMonitor(); - BackgroundJobManagerImpl.getInstance().cancelAllFolderUploadWorker(); + BackgroundJobManagerImpl.getInstance().cancelFolderAutoUploadWorker(); } } @@ -162,14 +161,14 @@ private void onBusEvent(TransferOpType opType) { private void startFolderMonitor() { boolean isBackupEnable = FolderBackupSharePreferenceHelper.readBackupSwitch(); - if (isBackupEnable){ + if (isBackupEnable) { List pathList = FolderBackupSharePreferenceHelper.readBackupPathsAsList(); boolean isFound = pathList.stream().anyMatch(IGNORE_PATHS::contains); if (!isFound) { pathList.add(IGNORE_PATHS.get(0)); } startFolderMonitor(pathList); - }else{ + } else { resetFolderMonitor(); } } @@ -233,7 +232,7 @@ private void doBackup(String action, File file) { BackgroundJobManagerImpl.getInstance().startCheckDownloadedFileChainWorker(file.getAbsolutePath()); } } else { - BackgroundJobManagerImpl.getInstance().startFolderChainWorker(true); + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(true); } } @@ -294,6 +293,7 @@ public void onStop(FileAlterationObserver observer) { public void onDestroy() { super.onDestroy(); + SLogs.e("file monitor service destroy"); stopFolderMonitor(); // diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/observer/MediaContentObserver.java b/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/MediaContentObserver.java similarity index 85% rename from app/src/main/java/com/seafile/seadroid2/framework/worker/observer/MediaContentObserver.java rename to app/src/main/java/com/seafile/seadroid2/framework/file_monitor/MediaContentObserver.java index 449e2d7fc..f7b861819 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/observer/MediaContentObserver.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/MediaContentObserver.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.framework.worker.observer; +package com.seafile.seadroid2.framework.file_monitor; import android.content.ContentResolver; import android.content.Context; @@ -6,10 +6,10 @@ import android.net.Uri; import android.os.Handler; import android.provider.MediaStore; +import android.widget.Toast; -import com.seafile.seadroid2.framework.datastore.sp_livedata.AlbumBackupSharePreferenceHelper; +import com.seafile.seadroid2.SeadroidApplication; import com.seafile.seadroid2.framework.util.SLogs; -import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; import com.seafile.seadroid2.ui.camera_upload.CameraUploadManager; /** @@ -39,7 +39,9 @@ public MediaContentObserver(Context context, Handler handler) { public void onChange(boolean selfChange) { super.onChange(selfChange); - SLogs.d("A new file is detected and the Media task begins"); + String newVer = MediaStore.getVersion(SeadroidApplication.getAppContext()); + SLogs.e("媒体库新版本:"+newVer); + SLogs.e("A new file is detected and the Media task begins"); CameraUploadManager.getInstance().performFullSync(); } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/observer/RecursiveFileObserver.java b/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/RecursiveFileObserver.java similarity index 98% rename from app/src/main/java/com/seafile/seadroid2/framework/worker/observer/RecursiveFileObserver.java rename to app/src/main/java/com/seafile/seadroid2/framework/file_monitor/RecursiveFileObserver.java index 749189476..744e4e447 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/observer/RecursiveFileObserver.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/RecursiveFileObserver.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.framework.worker.observer; +package com.seafile.seadroid2.framework.file_monitor; import android.os.Build; import android.os.FileObserver; diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/observer/SingleFolderPathFileObserver.java b/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/SingleFolderPathFileObserver.java similarity index 95% rename from app/src/main/java/com/seafile/seadroid2/framework/worker/observer/SingleFolderPathFileObserver.java rename to app/src/main/java/com/seafile/seadroid2/framework/file_monitor/SingleFolderPathFileObserver.java index a0a87a6ed..04c2ac62d 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/observer/SingleFolderPathFileObserver.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/file_monitor/SingleFolderPathFileObserver.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.framework.worker.observer; +package com.seafile.seadroid2.framework.file_monitor; import org.apache.commons.io.IOCase; import org.apache.commons.io.monitor.FileAlterationObserver; diff --git a/app/src/main/java/com/seafile/seadroid2/framework/http/BaseOkHttpClient.java b/app/src/main/java/com/seafile/seadroid2/framework/http/BaseOkHttpClient.java index 1e3f73424..236bb76c4 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/http/BaseOkHttpClient.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/http/BaseOkHttpClient.java @@ -43,17 +43,24 @@ public BaseOkHttpClient(Account account) { protected List getInterceptors() { + List interceptors = getInterceptorsWithoutToken(); + if (account != null && !TextUtils.isEmpty(account.token)) { + interceptors.add(new HeaderInterceptor(account.token)); + } + + return interceptors; + } + + protected List getInterceptorsWithoutToken() { + List interceptors = new ArrayList<>(); //print log HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC); - loggingInterceptor.setLevel(BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.BASIC); +// loggingInterceptor.setLevel(BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.BASIC); interceptors.add(loggingInterceptor); - if (account != null && !TextUtils.isEmpty(account.token)) { - interceptors.add(new HeaderInterceptor(account.token)); - } // interceptors.add(new AddCookiesInterceptor()); // interceptors.add(new ReceivedCookiesInterceptor()); diff --git a/app/src/main/java/com/seafile/seadroid2/framework/http/HttpIO.java b/app/src/main/java/com/seafile/seadroid2/framework/http/HttpIO.java index 00702a34a..2fd29011f 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/http/HttpIO.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/http/HttpIO.java @@ -80,6 +80,14 @@ public static HttpIO getCurrentInstance() { return INSTANCE; } + public static void removeInstanceByAccount(Account account) { + if (account == null) { + return; + } + + IO_MAP.remove(account.getSignature()); + } + /** * Not logged in/Log in to another server */ @@ -153,6 +161,7 @@ public T execute(Class clazz) { private Retrofit createRetrofit() { Retrofit.Builder rBuilder = new Retrofit.Builder(); + rBuilder.baseUrl(getServerUrl()); rBuilder.addConverterFactory(ConverterFactory.create()); rBuilder.addCallAdapterFactory(RxJava2CallAdapterFactory.create()); @@ -162,7 +171,6 @@ private Retrofit createRetrofit() { return rBuilder.build(); } - public void downloadBinarySync(String url, File destinationFile, ProgressListener callback) throws IOException { OkHttpClient client = getOkHttpClient().getOkClient(); @@ -178,7 +186,6 @@ public void onProgress(long transferSize, long totalSize) { try (BinaryFileDownloader fileDownloader = new BinaryFileDownloader(client, fileWriter)) { fileDownloader.download(url); - } catch (Exception e) { if (callback != null) { callback.isCancelled(); diff --git a/app/src/main/java/com/seafile/seadroid2/framework/http/SafeOkHttpClient.java b/app/src/main/java/com/seafile/seadroid2/framework/http/SafeOkHttpClient.java index 25524a5c1..cc46d68b6 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/http/SafeOkHttpClient.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/http/SafeOkHttpClient.java @@ -2,6 +2,7 @@ import com.blankj.utilcode.util.CollectionUtils; import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.ssl.CertsManager; import com.seafile.seadroid2.ssl.SSLSeafileSocketFactory; import com.seafile.seadroid2.ssl.SSLTrustManager; @@ -12,6 +13,7 @@ import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; @@ -31,8 +33,28 @@ import okhttp3.Request; public class SafeOkHttpClient extends BaseOkHttpClient { + private final List _interceptors = new ArrayList<>(); + public SafeOkHttpClient(Account account) { super(account); + + _interceptors.addAll(getInterceptors()); + } + + public SafeOkHttpClient(Account account, boolean isCustomToken) { + super(account); + + if (isCustomToken) { + _interceptors.addAll(getInterceptorsWithoutToken()); + } else { + _interceptors.addAll(getInterceptors()); + } + } + + public void addInterceptors(List s) { + if (s != null) { + _interceptors.addAll(s); + } } public static TrustManager[] getTrustManagers() { @@ -69,24 +91,15 @@ public OkHttpClient getOkClient() { //https if (account.getServer().startsWith("https://")) { //ssl - SSLSocketFactory factory = SSLTrustManager.instance().getSSLSocketFactory(account); TrustManager[] trustManagers = SSLTrustManager.instance().getTrustManagers(account); X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; builder.sslSocketFactory(factory, trustManager); - builder.hostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { - //check host - if (account.getServerDomainName().equals(hostname)) { - return true; - } - - //check by default verifier - HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier(); - return verifier.verify(hostname, session); + return true; } }); } @@ -129,9 +142,8 @@ public boolean verify(String hostname, SSLSession session) { builder.networkInterceptors().add(REWRITE_CACHE_CONTROL_INTERCEPTOR); //add interceptors - List interceptors = getInterceptors(); - if (!CollectionUtils.isEmpty(interceptors)) { - for (Interceptor i : interceptors) { + if (!CollectionUtils.isEmpty(_interceptors)) { + for (Interceptor i : _interceptors) { builder.interceptors().add(i); } } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/notification/base/BaseNotification.java b/app/src/main/java/com/seafile/seadroid2/framework/notification/base/BaseNotification.java index 7e1707778..d8f1b9b1d 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/notification/base/BaseNotification.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/notification/base/BaseNotification.java @@ -46,7 +46,6 @@ public BaseNotification(Context context) { hasPermission = i == android.content.pm.PackageManager.PERMISSION_GRANTED; } - init(); } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/util/ContentResolvers.java b/app/src/main/java/com/seafile/seadroid2/framework/util/ContentResolvers.java new file mode 100644 index 000000000..de86a85ee --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/util/ContentResolvers.java @@ -0,0 +1,55 @@ +package com.seafile.seadroid2.framework.util; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ContentResolvers { + + public static String getFileNameFromUri(ContentResolver contentResolver, Uri uri) { + String fileName = null; + String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME}; + try (Cursor cursor = contentResolver.query(uri, projection, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + int index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME); + fileName = cursor.getString(index); + } + } + + return fileName; + } + + // 获取文件内容的方法 + public static byte[] getFileContentFromUri(ContentResolver contentResolver, Uri uri) { + byte[] fileContent = null; + InputStream inputStream = null; + try { + inputStream = contentResolver.openInputStream(uri); + if (inputStream != null) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + fileContent = outputStream.toByteArray(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return fileContent; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/util/GlideCache.java b/app/src/main/java/com/seafile/seadroid2/framework/util/GlideCache.java index f69075797..e18e22d25 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/util/GlideCache.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/util/GlideCache.java @@ -45,8 +45,7 @@ public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder File[] externalMediaDirs = SeadroidApplication.getAppContext().getExternalMediaDirs(); String rootPath = externalMediaDirs[0].getAbsolutePath(); File dirPath = new File(rootPath + "/GlideCache/"); - builder.setDiskCache(new DiskLruCacheFactory(dirPath.getAbsolutePath(), 1024 * 1024 * 100)); - GlideApp.tearDown(); + builder.setDiskCache(new DiskLruCacheFactory(dirPath.getAbsolutePath(), 1024 * 1024 * 500)); } @Override diff --git a/app/src/main/java/com/seafile/seadroid2/framework/util/Logs.java b/app/src/main/java/com/seafile/seadroid2/framework/util/Logs.java index 6f0f36604..9eb6ad4d0 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/util/Logs.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/util/Logs.java @@ -38,12 +38,11 @@ public class Logs { /** * will delete log files that have not been modified for a period of time */ - private static final long MAX_TIME = 30L * 24 * 60 * 60 * 1000; + private static final long MAX_TIME = 30L * 24 * 60 * 60 * 1000;// 30 days public static void init() { LogConfiguration config = new LogConfiguration.Builder() - .logLevel(BuildConfig.DEBUG ? LogLevel.ALL - : LogLevel.ERROR) + .logLevel(BuildConfig.DEBUG ? LogLevel.ALL : LogLevel.INFO) .tag(LOG_TAG) // .enableThreadInfo() // .enableStackTrace(2) diff --git a/app/src/main/java/com/seafile/seadroid2/framework/util/Objs.java b/app/src/main/java/com/seafile/seadroid2/framework/util/Objs.java index 275987c7f..bbe327bb1 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/util/Objs.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/util/Objs.java @@ -41,6 +41,7 @@ import com.seafile.seadroid2.ui.dialog_fragment.AppChoiceDialogFragment; import com.seafile.seadroid2.ui.dialog_fragment.GetShareLinkPasswordDialogFragment; import com.seafile.seadroid2.ui.repo.RepoService; +import com.seafile.seadroid2.ui.comparator.NaturalOrderComparator; import com.seafile.seadroid2.ui.star.StarredService; import java.io.File; @@ -170,7 +171,7 @@ public SingleSource> apply(RepoWrapperModel repoWrapperModel) th }); } - public static List parseRepoListForAdapter(List list, String related_account, boolean isFilter) { + public static List parseRepoListForAdapter(List list, String related_account, boolean isFilterUnavailable) { if (CollectionUtils.isEmpty(list)) { return Collections.emptyList(); } @@ -179,7 +180,7 @@ public static List parseRepoListForAdapter(List list, Stri list.get(i).related_account = related_account; } - if (isFilter) { + if (isFilterUnavailable) { list = list.stream().filter(f -> !f.encrypted && f.hasWritePermission()).collect(Collectors.toList()); } @@ -332,41 +333,32 @@ private static List sortRepos(List repos) { boolean isAscending = Settings.FILE_LIST_SORT_ASCENDING.queryValue(); if (SortBy.NAME == by) { - newRepos = repos.stream().sorted(new Comparator() { - @Override - public int compare(RepoModel o1, RepoModel o2) { - if (isAscending) { - return o1.repo_name.compareTo(o2.repo_name); - } - return -o1.repo_name.compareTo(o2.repo_name); - } - }).collect(Collectors.toList()); + if (isAscending) { + newRepos = repos.stream().sorted(new NaturalOrderComparator()).collect(Collectors.toList()); + } else { + newRepos = repos.stream().sorted(new NaturalOrderComparator().reversed()).collect(Collectors.toList()); + } } else if (SortBy.TYPE == by) { //todo not supported } else if (SortBy.SIZE == by) { newRepos = repos.stream().sorted(new Comparator() { @Override public int compare(RepoModel o1, RepoModel o2) { - if (o1.size == o2.size) { - return 0; - } if (isAscending) { - return o1.size < o2.size ? -1 : 1; + return Long.compare(o1.size, o2.size); } - return o1.size > o2.size ? -1 : 1; + return -Long.compare(o1.size, o2.size); } }).collect(Collectors.toList()); } else if (SortBy.LAST_MODIFIED == by) { newRepos = repos.stream().sorted(new Comparator() { @Override public int compare(RepoModel o1, RepoModel o2) { - if (o1.last_modified_long == o2.last_modified_long) { - return 0; - } + if (isAscending) { - return o1.last_modified_long < o2.last_modified_long ? -1 : 1; + return Long.compare(o1.last_modified_long, o2.last_modified_long); } - return o1.last_modified_long > o2.last_modified_long ? -1 : 1; + return -Long.compare(o1.last_modified_long, o2.last_modified_long); } }).collect(Collectors.toList()); } @@ -614,15 +606,11 @@ private static List sortDirents(List list) { boolean isAscending = Settings.FILE_LIST_SORT_ASCENDING.queryValue(); if (SortBy.NAME == by) { - newList = list.stream().sorted(new Comparator() { - @Override - public int compare(DirentModel o1, DirentModel o2) { - if (isAscending) { - return o1.name.compareTo(o2.name); - } - return -o1.name.compareTo(o2.name); - } - }).collect(Collectors.toList()); + if (isAscending) { + newList = list.stream().sorted(new NaturalOrderComparator()).collect(Collectors.toList()); + } else { + newList = list.stream().sorted(new NaturalOrderComparator().reversed()).collect(Collectors.toList()); + } } else if (SortBy.TYPE == by) { //todo not supported } else if (SortBy.SIZE == by) { @@ -646,7 +634,6 @@ public int compare(DirentModel o1, DirentModel o2) { } }).collect(Collectors.toList()); } - return newList; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/util/StringUtils.java b/app/src/main/java/com/seafile/seadroid2/framework/util/StringUtils.java index 6d8d5c876..975d1926b 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/util/StringUtils.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/util/StringUtils.java @@ -129,9 +129,13 @@ public static String trimStart(String input, String character) { } /** - *
-     * ""string"" => "string"
-     * 
+ *
+ *
+     *  ""string"" => "string"
+     *  null => null
+     *  "" => ""
+     * 
+ *
*/ public static String deString(String input) { if (TextUtils.isEmpty(input)) { @@ -143,6 +147,25 @@ public static String deString(String input) { return input; } + /** + *
+ *
+     * ""string"" => "string"
+     * null => ""
+     * "" => ""
+     * 
+ *
+ */ + public static String deStringReturnNonNull(String input) { + if (TextUtils.isEmpty(input)) { + return ""; + } + + input = trim(input, "\""); + + return input; + } + public static int getStringAsciiSum(String input) { int sum = 0; for (int i = 0; i < input.length(); i++) { @@ -151,7 +174,7 @@ public static int getStringAsciiSum(String input) { } return sum; } - + public static int getHexStringAsciiSum(String input) { byte[] bytes = hexStringToByteArray(input); @@ -173,4 +196,18 @@ public static byte[] hexStringToByteArray(String s) { } return data; } + + /** + * => org.apache.commons.lang3.StringUtils#countMatches(c,c) + */ + public static int countMatches(String inputStr, String matchStr) { + if (TextUtils.isEmpty(inputStr)) { + return 0; + } + if (TextUtils.isEmpty(matchStr)) { + return 0; + } + + return org.apache.commons.lang3.StringUtils.countMatches(inputStr, matchStr); + } } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/util/TakeCameras.java b/app/src/main/java/com/seafile/seadroid2/framework/util/TakeCameras.java index 6ae1bdc0d..3c147fdea 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/util/TakeCameras.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/util/TakeCameras.java @@ -7,6 +7,8 @@ import androidx.core.content.FileProvider; import com.blankj.utilcode.util.PathUtils; +import com.blankj.utilcode.util.TimeUtils; +import com.google.android.gms.common.util.DataUtils; import com.seafile.seadroid2.BuildConfig; import java.io.File; @@ -35,15 +37,22 @@ public static File getMediaStoragePath(String folderName) { public static Pair buildTakePhotoUri(Context context) { File parentFolder = getMediaStoragePath("images"); - String fileName = "sf_photo_" + System.currentTimeMillis() + ".jpg"; + String fileName = getOneNewName("jpg"); File file = new File(parentFolder, fileName); Uri uri = FileProvider.getUriForFile(context, BuildConfig.FILE_PROVIDER_AUTHORITIES, file); return new Pair<>(uri, file); } + private static String getOneNewName(String prefix) { + long mills = System.currentTimeMillis(); + long seconds = mills / 1000; + String date = TimeUtils.millis2String(mills, "yyyyMMdd"); + return date + "_" + seconds + "." + prefix; + } + public static Pair buildTakeVideoUri(Context context) { File parentFolder = getMediaStoragePath("videos"); - String fileName = "sf_video" + System.currentTimeMillis() + ".mp4"; + String fileName = getOneNewName("mp4"); File file = new File(parentFolder, fileName); Uri uri = FileProvider.getUriForFile(context, BuildConfig.FILE_PROVIDER_AUTHORITIES, file); return new Pair<>(uri, file); diff --git a/app/src/main/java/com/seafile/seadroid2/framework/util/ThumbnailUtils.java b/app/src/main/java/com/seafile/seadroid2/framework/util/ThumbnailUtils.java new file mode 100644 index 000000000..28218c789 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/framework/util/ThumbnailUtils.java @@ -0,0 +1,11 @@ +package com.seafile.seadroid2.framework.util; + +import com.blankj.utilcode.util.EncodeUtils; + +public class ThumbnailUtils { + + public static String convertThumbnailUrl(String serverUrl, String repoId, String fullPath) { + String newFilePath = EncodeUtils.urlEncode(fullPath); + return String.format("%sapi2/repos/%s/thumbnail/?p=%s&size=%s", serverUrl, repoId, newFilePath, 128); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/framework/util/Utils.java b/app/src/main/java/com/seafile/seadroid2/framework/util/Utils.java index 886356dbf..f8e928077 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/util/Utils.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/util/Utils.java @@ -96,6 +96,7 @@ public static String getPathFromFullPath(String path) { } else return parent; } + public static String getParentPath(String path) { if (path == null) { // the caller should not give null @@ -107,6 +108,10 @@ public static String getParentPath(String path) { return "/"; } + if ("/".equals(path)) { + return "/"; + } + if (path.endsWith("/")) { path = path.substring(0, path.lastIndexOf("/")); } @@ -521,8 +526,6 @@ public static void hideSystemNavigationBar(Activity activity) { } - - public static void startCameraSyncJob(Context context) { // JobScheduler mJobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); // JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, new ComponentName(context.getPackageName(), MediaSchedulerService.class.getName())); diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/BackgroundJobManagerImpl.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/BackgroundJobManagerImpl.java index bb5a51740..4fec53302 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/BackgroundJobManagerImpl.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/BackgroundJobManagerImpl.java @@ -2,6 +2,7 @@ import android.text.TextUtils; +import androidx.work.BackoffPolicy; import androidx.work.Constraints; import androidx.work.Data; import androidx.work.ExistingWorkPolicy; @@ -107,8 +108,8 @@ public WorkManager getWorkManager() { /////////////////// /// media worker /////////////////// - public void startMediaChainWorker(boolean isForce) { - cancelAllMediaWorker(); + public void startMediaWorkerChain(boolean isForce) { + cancelMediaWorker(); OneTimeWorkRequest scanRequest = getMediaScanRequest(isForce); OneTimeWorkRequest uploadRequest = getMediaUploadRequest(); @@ -136,7 +137,8 @@ private OneTimeWorkRequest getMediaScanRequest(boolean isForce) { private OneTimeWorkRequest getMediaUploadRequest() { NetworkType networkType = NetworkType.UNMETERED; - if (AlbumBackupSharePreferenceHelper.readAllowDataPlanSwitch()) { + boolean isAllowData = AlbumBackupSharePreferenceHelper.readAllowDataPlanSwitch(); + if (isAllowData) { networkType = NetworkType.CONNECTED; } @@ -149,13 +151,14 @@ private OneTimeWorkRequest getMediaUploadRequest() { return oneTimeRequestBuilder(UploadMediaFileAutomaticallyWorker.class) .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.LINEAR, 5, TimeUnit.SECONDS) .setInitialDelay(1, TimeUnit.SECONDS) .setId(UploadMediaFileAutomaticallyWorker.UID) .build(); } //cancel media - public void cancelAllMediaWorker() { + public void cancelMediaWorker() { cancelById(UploadMediaFileAutomaticallyWorker.UID); cancelById(MediaBackupScannerWorker.UID); } @@ -163,8 +166,8 @@ public void cancelAllMediaWorker() { /////////////////// /// upload folder /////////////////// - public void startFolderChainWorker(boolean isForce) { - cancelAllFolderUploadWorker(); + public void startFolderAutoBackupWorkerChain(boolean isForce) { + cancelFolderAutoUploadWorker(); OneTimeWorkRequest scanRequest = getFolderScanRequest(isForce); OneTimeWorkRequest uploadRequest = getFolderUploadRequest(); @@ -204,12 +207,13 @@ private OneTimeWorkRequest getFolderUploadRequest() { return oneTimeRequestBuilder(UploadFolderFileAutomaticallyWorker.class) .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.LINEAR, 5, TimeUnit.SECONDS) .setInitialDelay(1, TimeUnit.SECONDS) .setId(UploadFolderFileAutomaticallyWorker.UID) .build(); } - public void cancelAllFolderUploadWorker() { + public void cancelFolderAutoUploadWorker() { cancelById(FolderBackupScannerWorker.UID); cancelById(UploadFolderFileAutomaticallyWorker.UID); } @@ -217,7 +221,7 @@ public void cancelAllFolderUploadWorker() { /////////////////// /// upload file /////////////////// - public void startFileUploadWorker() { + public void startFileManualUploadWorker() { String workerName = UploadFileManuallyWorker.class.getSimpleName(); OneTimeWorkRequest request = getFileUploadRequest(); getWorkManager().enqueueUniqueWork(workerName, ExistingWorkPolicy.KEEP, request); @@ -229,6 +233,10 @@ private OneTimeWorkRequest getFileUploadRequest() { .build(); } + public void cancelFileManualUploadWorker() { + cancelById(UploadFileManuallyWorker.UID); + } + /////////////////// /// download /////////////////// diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/TransferEvent.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/TransferEvent.java index 6adf0e260..a747ff15b 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/TransferEvent.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/TransferEvent.java @@ -41,11 +41,16 @@ public class TransferEvent { /** * because of an OUT_OF_QUOTA error, the current upload worker was canceled */ - public static final String EVENT_CANCEL_OUT_OF_QUOTA = "transfer_cancel_with_out_of_quota"; + public static final String EVENT_CANCEL_WITH_OUT_OF_QUOTA = "transfer_cancel_with_out_of_quota"; /** * because of an NETWORK error, the current upload worker was canceled */ public static final String EVENT_CANCEL_WITH_NETWORK_ERR = "transfer_cancel_with_network_err"; + /** + * Manual or passive cancellation + */ + public static final String EVENT_CANCEL_WITH_BY_STOPPED = "transfer_cancel_with_stopped"; + } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/download/BaseDownloadWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/download/BaseDownloadWorker.java index 976508f3d..b108685fd 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/download/BaseDownloadWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/download/BaseDownloadWorker.java @@ -90,7 +90,7 @@ public String isInterrupt(TransferResult result) { } else if (result == TransferResult.FILE_NOT_FOUND) { // finishFlagEvent = null; } else if (result == TransferResult.OUT_OF_QUOTA) { - finishFlagEvent = TransferEvent.EVENT_CANCEL_OUT_OF_QUOTA; + finishFlagEvent = TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA; } else if (result == TransferResult.NETWORK_CONNECTION) { finishFlagEvent = TransferEvent.EVENT_CANCEL_WITH_NETWORK_ERR; } else if (result == TransferResult.SSL_EXCEPTION) { diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/download/DownloadFileScanWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/download/DownloadFileScanWorker.java index ea14e5b4e..6def872f9 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/download/DownloadFileScanWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/download/DownloadFileScanWorker.java @@ -172,33 +172,30 @@ private void insertIntoDbWhenDirentIsFile(Account account, DirentModel direntMod } RepoModel repoModel = repoModels.get(0); - DirentFileModel direntFileModel = fetchFile(direntModel); - if (direntFileModel == null) { - return; - } - - FileTransferEntity transferEntity = FileTransferEntity.convertDirentModel2This(repoModel.canLocalDecrypt(), direntModel); - - //newest file id - transferEntity.file_id = direntFileModel.id; - transferEntity.file_size = direntFileModel.size; - transferEntity.target_path = DataManager.getLocalRepoFile(account, transferEntity).getAbsolutePath(); + List existsList = AppDatabase.getInstance().fileTransferDAO().getListByFullPathsSync(direntModel.repo_id, CollectionUtils.newArrayList(direntModel.full_path), TransferAction.DOWNLOAD); + if (!CollectionUtils.isEmpty(existsList)) { - List existsList = AppDatabase.getInstance().fileTransferDAO().getListByFullPathsSync(direntModel.repo_id, CollectionUtils.newArrayList(transferEntity.full_path), TransferAction.DOWNLOAD); + DirentFileModel direntFileModel = fetchFile(direntModel); + if (direntFileModel == null) { + return; + } - if (!CollectionUtils.isEmpty(existsList)) { FileTransferEntity existEntity = existsList.get(0); if (TransferStatus.SUCCEEDED == existEntity.transfer_status) { - if (TextUtils.equals(existsList.get(0).file_id, transferEntity.file_id)) { - //it's the same file,do not insert into db. - SLogs.d("file download: skip file(local exists): " + transferEntity.full_path); - ToastUtils.showLong(R.string.download_finished); + if (TextUtils.equals(existsList.get(0).file_id, direntFileModel.id)) { + SLogs.d("file download: skip file(local exists): " + existEntity.full_path); return; } } } + + FileTransferEntity transferEntity = FileTransferEntity.convertDirentModel2This(repoModel.canLocalDecrypt(), direntModel); + //newest file id + transferEntity.file_id = direntModel.id; + transferEntity.file_size = direntModel.size; + transferEntity.target_path = DataManager.getLocalRepoFile(account, transferEntity).getAbsolutePath(); transferEntity.transfer_status = TransferStatus.WAITING; transferEntity.transfer_result = TransferResult.NO_RESULT; transferEntity.transferred_size = 0; diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/download/DownloadWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/download/DownloadWorker.java index 941089ba9..6bb3141fe 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/download/DownloadWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/download/DownloadWorker.java @@ -47,6 +47,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -97,7 +98,7 @@ public Result doWork() { //count int pendingCount = AppDatabase.getInstance().fileTransferDAO().countPendingDownloadListSync(account.getSignature()); if (pendingCount <= 0) { - SLogs.eDebug("download list is empty."); + SLogs.i("download list is empty."); return Result.success(getFinishData(false)); } @@ -119,7 +120,7 @@ public Result doWork() { List list = AppDatabase .getInstance() .fileTransferDAO() - .getOnePendingDownloadByActionSync(account.getSignature()); + .getOnePendingDownloadByAccountSync(account.getSignature()); if (CollectionUtils.isEmpty(list)) { break; } @@ -150,7 +151,7 @@ public Result doWork() { } } - SLogs.d("all task run"); + SLogs.i("all task run"); // if (isDownloaded) { @@ -174,7 +175,7 @@ private Data getFinishData(boolean isDownloaded) { private final FileTransferProgressListener.TransferProgressListener progressListener = new FileTransferProgressListener.TransferProgressListener() { @Override public void onProgressNotify(FileTransferEntity fileTransferEntity, int percent, long transferredSize, long totalSize) { - SLogs.d(fileTransferEntity.file_name + " -> progress:" + percent); + SLogs.i(fileTransferEntity.file_name + " -> progress:" + percent); int diff = AppDatabase.getInstance().fileTransferDAO().countPendingDownloadListSync(fileTransferEntity.related_account); @@ -190,7 +191,7 @@ public void onProgressNotify(FileTransferEntity fileTransferEntity, int percent, }; private void transferFile(Account account, FileTransferEntity transferEntity) throws Exception { - SLogs.d("download start:" + transferEntity.full_path); + SLogs.i("download start:" + transferEntity.full_path); //show notification int diff = AppDatabase.getInstance().fileTransferDAO().countPendingDownloadListSync(transferEntity.related_account); @@ -200,7 +201,7 @@ private void transferFile(Account account, FileTransferEntity transferEntity) th List repoModels = AppDatabase.getInstance().repoDao().getByIdSync(transferEntity.repo_id); if (CollectionUtils.isEmpty(repoModels)) { - SLogs.d("no repo for repoId: " + transferEntity.repo_id); + SLogs.i("no repo for repoId: " + transferEntity.repo_id); return; } @@ -224,13 +225,13 @@ private void downloadFile(Account account, FileTransferEntity transferEntity) th File localFile = DataManager.getLocalRepoFile(account, transferEntity); if (localFile.exists() && transferEntity.file_strategy == ExistingFileStrategy.SKIP) { - SLogs.d("skip this file, file_strategy is SKIP :" + localFile.getAbsolutePath()); + SLogs.i("skip this file, file_strategy is SKIP :" + localFile.getAbsolutePath()); return; } download(transferEntity, dlink, localFile); - SLogs.d("download finish:" + transferEntity.full_path); + SLogs.i("download finish:" + transferEntity.full_path); } private Pair getDownloadLink(FileTransferEntity transferEntity, boolean isReUsed) throws SeafException, IOException { @@ -267,8 +268,6 @@ private Pair getDownloadLink(FileTransferEntity transferEntity, } private void download(FileTransferEntity fileTransferEntity, String dlink, File localFile) throws Exception { - - fileTransferProgressListener.setFileTransferEntity(fileTransferEntity); fileTransferEntity.transfer_status = TransferStatus.IN_PROGRESS; @@ -293,8 +292,8 @@ private void download(FileTransferEntity fileTransferEntity, String dlink, File long fileSize = responseBody.contentLength(); if (fileSize == -1) { - SLogs.d("download file error -> contentLength is -1"); - SLogs.d(localFile.getAbsolutePath()); + SLogs.e("download file error -> contentLength is -1"); + SLogs.e(localFile.getAbsolutePath()); fileSize = fileTransferEntity.file_size; @@ -328,10 +327,12 @@ private void download(FileTransferEntity fileTransferEntity, String dlink, File //important tempFile.renameTo(localFile); +// Path path = java.nio.file.Files.move(tempFile.toPath(), localFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); +// boolean isSuccess = path.toFile().exists(); if (localFile.length() != fileSize) { - SLogs.d("download file error -> localFile.size != downloadedSize"); - SLogs.d(localFile.getAbsolutePath()); + SLogs.e("download file error -> localFile.size != downloadedSize"); + SLogs.e(localFile.getAbsolutePath()); updateEntityErrorState(fileTransferEntity); } else { updateEntitySuccessState(fileTransferEntity, localFile); @@ -393,7 +394,7 @@ private void downloadFileByBlock(Account account, FileTransferEntity transferEnt File localFile = DataManager.getLocalRepoFile(account, transferEntity); if (localFile.exists() && transferEntity.file_strategy == ExistingFileStrategy.SKIP) { - SLogs.d("skip this file, file_strategy is SKIP :" + localFile.getAbsolutePath()); + SLogs.i("skip this file, file_strategy is SKIP :" + localFile.getAbsolutePath()); return; } @@ -498,7 +499,7 @@ private void downloadBlock(FileBlocks fileBlocks, String blockId, String dlink, responseBody.close(); if (localFile.length() != tempFileSize) { - SLogs.d("Rename file error : " + localFile.getAbsolutePath()); + SLogs.i("Rename file error : " + localFile.getAbsolutePath()); throw SeafException.networkException; } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/BaseUploadWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/BaseUploadWorker.java index d3742b207..541ab61af 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/BaseUploadWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/BaseUploadWorker.java @@ -49,6 +49,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.net.SocketException; import java.net.SocketTimeoutException; import java.nio.file.Files; import java.security.NoSuchAlgorithmException; @@ -102,7 +103,14 @@ private TransferResult parseTransferException(Exception e) { return TransferResult.SSL_EXCEPTION; } else if (e instanceof SocketTimeoutException) { return TransferResult.NETWORK_CONNECTION; + } else if (e instanceof SocketException) { +// SocketException exception = (SocketException) e; +// SLogs.e(exception); + return TransferResult.USER_CANCELLED; } else if (e instanceof IOException) { + if (TextUtils.equals("Canceled", e.getMessage())) { + return TransferResult.USER_CANCELLED; + } return TransferResult.NETWORK_CONNECTION; } @@ -140,7 +148,7 @@ public String isInterrupt(TransferResult result) { } else if (result == TransferResult.FILE_NOT_FOUND) { // finishFlagEvent = null; } else if (result == TransferResult.OUT_OF_QUOTA) { - finishFlagEvent = TransferEvent.EVENT_CANCEL_OUT_OF_QUOTA; + finishFlagEvent = TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA; } else if (result == TransferResult.NETWORK_CONNECTION) { finishFlagEvent = TransferEvent.EVENT_CANCEL_WITH_NETWORK_ERR; } else if (result == TransferResult.SSL_EXCEPTION) { @@ -151,6 +159,8 @@ public String isInterrupt(TransferResult result) { finishFlagEvent = TransferEvent.EVENT_FINISH; } else if (result == TransferResult.UNKNOWN) { finishFlagEvent = TransferEvent.EVENT_FINISH; + } else if (result == TransferResult.USER_CANCELLED) { + finishFlagEvent = TransferEvent.EVENT_CANCEL_WITH_BY_STOPPED; } return finishFlagEvent; @@ -361,6 +371,11 @@ public void onStopped() { // cancelNotification(); + SLogs.e("BaseUploadWorker onStopped"); + currentTransferEntity.transfer_status = TransferStatus.CANCELLED; + currentTransferEntity.transfer_result = TransferResult.USER_CANCELLED; + AppDatabase.getInstance().fileTransferDAO().update(currentTransferEntity); + if (newCall != null && !newCall.isCanceled()) { newCall.cancel(); } @@ -376,59 +391,67 @@ private void notifyProgress(String fileName, int percent) { showForegroundAsync(f); } + private FileTransferEntity currentTransferEntity; + public void transferFile(Account account, FileTransferEntity transferEntity) throws IOException, SeafException, JSONException { SLogs.d("start transfer, full_path: " + transferEntity.full_path); + currentTransferEntity = transferEntity; List repoModels = AppDatabase.getInstance().repoDao().getByIdSync(transferEntity.repo_id); if (CollectionUtils.isEmpty(repoModels)) { - SLogs.d("no repo for repoId: " + transferEntity.repo_id); + SLogs.d("no repo for repoId: " + currentTransferEntity.repo_id); - transferEntity.transfer_status = TransferStatus.FAILED; - transferEntity.transfer_result = TransferResult.CANCELLED; - AppDatabase.getInstance().fileTransferDAO().update(transferEntity); + currentTransferEntity.transfer_status = TransferStatus.FAILED; + currentTransferEntity.transfer_result = TransferResult.CANCELLED; + AppDatabase.getInstance().fileTransferDAO().update(currentTransferEntity); return; } //update modified_at field - transferEntity.modified_at = System.currentTimeMillis(); - AppDatabase.getInstance().fileTransferDAO().update(transferEntity); - notifyProgress(transferEntity.file_name, 0); - SLogs.d("start transfer, target_path: " + transferEntity.target_path); + currentTransferEntity.modified_at = System.currentTimeMillis(); + AppDatabase.getInstance().fileTransferDAO().update(currentTransferEntity); +// + //show notification + notifyProgress(currentTransferEntity.file_name, 0); + SLogs.d("start transfer, target_path: " + currentTransferEntity.target_path); RepoModel repo = repoModels.get(0); - if (repo.canLocalDecrypt()) { - uploadBlockFile(account, repo, transferEntity); - } else { - uploadFile(account, repo, transferEntity); - } + uploadFile(account, repo); + +// RepoModel repo = repoModels.get(0); +// if (repo.canLocalDecrypt()) { +// uploadBlockFile(account, repo, transferEntity); +// } else { +// uploadFile(account, repo, transferEntity); +// } } /** * upload file */ - private void uploadFile(Account account, RepoModel repoModel, FileTransferEntity transferEntity) throws IOException, SeafException { + private void uploadFile(Account account, RepoModel repoModel) throws IOException, SeafException { if (isStopped()) { return; } - File file = new File(transferEntity.full_path); + File file = new File(currentTransferEntity.full_path); if (!file.exists()) { throw SeafException.notFoundException; } - ExistingFileStrategy fileStrategy = transferEntity.file_strategy; + ExistingFileStrategy fileStrategy = currentTransferEntity.file_strategy; if (fileStrategy == ExistingFileStrategy.AUTO) { - fileStrategy = checkRemoteFileExists(account, repoModel, transferEntity); + fileStrategy = checkRemoteFileExists(account, repoModel, currentTransferEntity); } if (fileStrategy == ExistingFileStrategy.SKIP) { - SLogs.d("folder backup: skip file(remote exists): " + transferEntity.target_path); + SLogs.d("folder backup: skip file(remote exists): " + currentTransferEntity.target_path); - transferEntity.transfer_status = TransferStatus.SUCCEEDED; - transferEntity.transfer_result = TransferResult.TRANSMITTED; - AppDatabase.getInstance().fileTransferDAO().update(transferEntity); + currentTransferEntity.transfer_status = TransferStatus.SUCCEEDED; + currentTransferEntity.transfer_result = TransferResult.TRANSMITTED; + AppDatabase.getInstance().fileTransferDAO().update(currentTransferEntity); return; } @@ -437,8 +460,8 @@ private void uploadFile(Account account, RepoModel repoModel, FileTransferEntity MultipartBody.Builder builder = new MultipartBody.Builder(); builder.setType(MultipartBody.FORM); - if (transferEntity.file_strategy == ExistingFileStrategy.REPLACE) { - builder.addFormDataPart("target_file", transferEntity.target_path); + if (currentTransferEntity.file_strategy == ExistingFileStrategy.REPLACE) { + builder.addFormDataPart("target_file", currentTransferEntity.target_path); } else { //parent_dir: / is repo root builder.addFormDataPart("parent_dir", "/"); @@ -446,7 +469,7 @@ private void uploadFile(Account account, RepoModel repoModel, FileTransferEntity // parent_dir is the root directory. // when select the root of the repo, relative_path is null. - String dir = transferEntity.getParent_path(); + String dir = currentTransferEntity.getParent_path(); dir = StringUtils.removeStart(dir, "/"); // builder.addFormDataPart("relative_path", dir); @@ -454,11 +477,11 @@ private void uploadFile(Account account, RepoModel repoModel, FileTransferEntity // - fileTransferProgressListener.setFileTransferEntity(transferEntity); + fileTransferProgressListener.setFileTransferEntity(currentTransferEntity); //db - transferEntity.transfer_status = TransferStatus.IN_PROGRESS; - AppDatabase.getInstance().fileTransferDAO().update(transferEntity); + currentTransferEntity.transfer_status = TransferStatus.IN_PROGRESS; + AppDatabase.getInstance().fileTransferDAO().update(currentTransferEntity); ProgressRequestBody progressRequestBody = new ProgressRequestBody(file, fileTransferProgressListener); @@ -467,7 +490,7 @@ private void uploadFile(Account account, RepoModel repoModel, FileTransferEntity RequestBody requestBody = builder.build(); //get upload link - String uploadUrl = getFileUploadUrl(transferEntity.repo_id, transferEntity.getParent_path(), transferEntity.file_strategy == ExistingFileStrategy.REPLACE); + String uploadUrl = getFileUploadUrl(currentTransferEntity.repo_id, currentTransferEntity.getParent_path(), currentTransferEntity.file_strategy == ExistingFileStrategy.REPLACE); if (TextUtils.isEmpty(uploadUrl)) { throw SeafException.networkException; } @@ -485,34 +508,34 @@ private void uploadFile(Account account, RepoModel repoModel, FileTransferEntity .build(); newCall = HttpIO.getCurrentInstance().getOkHttpClient().getOkClient().newCall(request); - Response response = newCall.execute(); + String str; + try (Response response = newCall.execute()) { + if (!response.isSuccessful()) { + String b = response.body() != null ? response.body().string() : null; + SLogs.d("result,failed:" + b); - if (!response.isSuccessful()) { - String b = response.body() != null ? response.body().string() : null; - SLogs.d("result,failed:" + b); + // + newCall.cancel(); - // - newCall.cancel(); + //[text={"error": "Out of quota.\n"}] + if (b != null && b.toLowerCase().contains("out of quota")) { + throw SeafException.OUT_OF_QUOTA; + } - //[text={"error": "Out of quota.\n"}] - if (b != null && b.toLowerCase().contains("out of quota")) { - throw SeafException.OUT_OF_QUOTA; + throw SeafException.networkException; } - throw SeafException.networkException; + str = response.body().string(); } - - String str = response.body().string(); String fileId = str.replace("\"", ""); SLogs.d("result,file ID:" + str); - updateSuccess(transferEntity, fileId, file); + updateSuccess(fileId, file); } catch (Exception e) { throw e; } } - private void uploadBlockFile(Account account, RepoModel repoModel, FileTransferEntity transferEntity) throws SeafException, IOException, JSONException { if (isStopped()) { return; @@ -744,37 +767,35 @@ private void commitUpload(String link, List blkIds, FileTransferEntity t String fileId = response.body().string(); - updateSuccess(transferEntity, fileId, file); + updateSuccess(fileId, file); } catch (Exception e) { throw e; } } - private void updateSuccess(FileTransferEntity transferEntity, String fileId, File file) { + private void updateSuccess(String fileId, File file) { //db - transferEntity.file_id = fileId; - transferEntity.transferred_size = file.length(); - transferEntity.action_end_at = System.currentTimeMillis(); - transferEntity.modified_at = transferEntity.action_end_at; - transferEntity.file_original_modified_at = file.lastModified(); - transferEntity.transfer_result = TransferResult.TRANSMITTED; - transferEntity.transfer_status = TransferStatus.SUCCEEDED; + currentTransferEntity.file_id = fileId; + currentTransferEntity.transferred_size = file.length(); + currentTransferEntity.action_end_at = System.currentTimeMillis(); + currentTransferEntity.modified_at = currentTransferEntity.action_end_at; + currentTransferEntity.file_original_modified_at = file.lastModified(); + currentTransferEntity.transfer_result = TransferResult.TRANSMITTED; + currentTransferEntity.transfer_status = TransferStatus.SUCCEEDED; - AppDatabase.getInstance().fileTransferDAO().update(transferEntity); + AppDatabase.getInstance().fileTransferDAO().update(currentTransferEntity); //update - List direntList = AppDatabase.getInstance().direntDao().getListByFullPathSync(transferEntity.repo_id, transferEntity.full_path); + List direntList = AppDatabase.getInstance().direntDao().getListByFullPathSync(currentTransferEntity.repo_id, currentTransferEntity.full_path); if (!CollectionUtils.isEmpty(direntList)) { DirentModel direntModel = direntList.get(0); - direntModel.last_modified_at = transferEntity.modified_at; + direntModel.last_modified_at = currentTransferEntity.modified_at; direntModel.id = fileId; - direntModel.size = transferEntity.file_size; - direntModel.transfer_status = transferEntity.transfer_status; + direntModel.size = currentTransferEntity.file_size; + direntModel.transfer_status = currentTransferEntity.transfer_status; AppDatabase.getInstance().direntDao().update(direntModel); } - - } private final int BUFFER_SIZE = 2 * 1024 * 1024; diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/FolderBackupScannerWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/FolderBackupScannerWorker.java index 576941ec5..fb3f1472d 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/FolderBackupScannerWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/FolderBackupScannerWorker.java @@ -126,8 +126,8 @@ private Data getOutData() { } private boolean checkCanScan() { - boolean isOpenBackup = FolderBackupSharePreferenceHelper.readBackupSwitch(); - if (!isOpenBackup) { + boolean isTurnOn = FolderBackupSharePreferenceHelper.readBackupSwitch(); + if (!isTurnOn) { return false; } @@ -178,7 +178,7 @@ private void traverseBackupPath(Account account, RepoConfig repoConfig, List tList = CollectionUtils.newArrayList(); for (File file : subFiles) { - FileTransferEntity fEntity = FileTransferEntity.convert2ThisForUploadFileSyncWorker(account, repoModel, file, backupPath); + FileTransferEntity fEntity = FileTransferEntity.convert2ThisForUploadFileSyncWorker(account, file, backupPath); if (fEntity != null) { tList.add(fEntity); } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/MediaBackupScannerWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/MediaBackupScannerWorker.java index a54daf2a8..6f0224a1d 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/MediaBackupScannerWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/MediaBackupScannerWorker.java @@ -97,7 +97,6 @@ public Result doWork() { return Result.success(getOutData()); } - //todo String title = getApplicationContext().getString(R.string.settings_camera_upload_info_title); String subTitle = getApplicationContext().getString(R.string.is_scanning); @@ -517,7 +516,7 @@ private void checkAndInsert(String parent, List> fi String parentPath = Utils.pathJoin(parent, "/"); - FileTransferEntity fileTransferEntity = FileTransferEntity.convert2ThisForUploadMediaSyncWorker(account, repoConfig.getRepoID(), repoConfig.getRepoName(), file, parentPath, absPathPair.getThird(), isRemoteExists); + FileTransferEntity fileTransferEntity = FileTransferEntity.convert2ThisForUploadMediaSyncWorker(account, file, parentPath, absPathPair.getThird(), isRemoteExists); transferList.add(fileTransferEntity); } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadFileManuallyWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadFileManuallyWorker.java index 6333bf534..8093e0f9f 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadFileManuallyWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadFileManuallyWorker.java @@ -96,9 +96,9 @@ private ListenableWorker.Result start() { boolean isAmple = calcQuota(CollectionUtils.newArrayList(transferEntity)); if (!isAmple) { getGeneralNotificationHelper().showErrorNotification(R.string.above_quota, R.string.settings_folder_backup_info_title); - AppDatabase.getInstance().fileTransferDAO().cancelWithFileBackup(TransferResult.OUT_OF_QUOTA); + AppDatabase.getInstance().fileTransferDAO().cancelWithFileBackup(transferEntity.related_account, TransferResult.OUT_OF_QUOTA); - finishFlagEvent = TransferEvent.EVENT_CANCEL_OUT_OF_QUOTA; + finishFlagEvent = TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA; break; } } catch (Exception e) { diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadFolderFileAutomaticallyWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadFolderFileAutomaticallyWorker.java index 174e2813f..2af843848 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadFolderFileAutomaticallyWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadFolderFileAutomaticallyWorker.java @@ -27,6 +27,7 @@ import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; import com.seafile.seadroid2.framework.worker.TransferEvent; import com.seafile.seadroid2.framework.worker.TransferWorker; +import com.seafile.seadroid2.ui.folder_backup.RepoConfig; import java.util.List; import java.util.UUID; @@ -65,36 +66,46 @@ public void onStopped() { super.onStopped(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - SLogs.e("文件夹备份已停止:" + getStopReason()); + SLogs.e("Folder backup stopped, reason:" + getStopReason()); } } + private boolean isFirstShow = true; + private void startShowNotification(){ + if (!isFirstShow) { + return; + } + + isFirstShow = false; + ForegroundInfo foregroundInfo = notificationManager.getForegroundNotification(); + showForegroundAsync(foregroundInfo); + + + sendEvent(TransferEvent.EVENT_TRANSFERRING, TransferDataSource.FILE_BACKUP); + } + private Result start() { // notificationManager.cancel(); + SLogs.d("start upload file worker"); Account account = SupportAccountManager.getInstance().getCurrentAccount(); if (account == null) { return Result.success(); } - boolean isEnable = FolderBackupSharePreferenceHelper.readBackupSwitch(); - if (!isEnable) { + boolean canExec = can(); + if (!canExec) { return Result.success(); } - boolean isUploaded = false; - - - ForegroundInfo foregroundInfo = notificationManager.getForegroundNotification(); - showForegroundAsync(foregroundInfo); + if (repoConfig == null) { + return Result.success(); + } -// notificationManager.showNotification(); + boolean isUploaded = false; String finishFlagEvent = null; - SLogs.d("start upload file worker"); - sendEvent(TransferEvent.EVENT_TRANSFERRING, TransferDataSource.FILE_BACKUP); - while (true) { if (isStopped()) { break; @@ -113,9 +124,14 @@ private Result start() { break; } - FileTransferEntity transferEntity = transferList.get(0); + startShowNotification(); + isUploaded = true; + FileTransferEntity transferEntity = transferList.get(0); + transferEntity.repo_id = repoConfig.getRepoID(); + transferEntity.repo_name = repoConfig.getRepoName(); + try { transferFile(account, transferEntity); @@ -126,15 +142,19 @@ private Result start() { TransferResult transferResult = onException(transferEntity, e); - notifyError(transferResult); + if (!isStopped()) { + + notifyError(transferResult); - sendTransferEvent(transferEntity, false); + sendTransferEvent(transferEntity, false); + } String finishFlag = isInterrupt(transferResult); if (!TextUtils.isEmpty(finishFlag)) { finishFlagEvent = finishFlag; break; } + } } @@ -170,4 +190,25 @@ private Result start() { return Result.success(outputData); } + private RepoConfig repoConfig; + + private boolean can() { + boolean isTurnOn = FolderBackupSharePreferenceHelper.readBackupSwitch(); + if (!isTurnOn) { + return false; + } + + List backupPaths = FolderBackupSharePreferenceHelper.readBackupPathsAsList(); + if (CollectionUtils.isEmpty(backupPaths)) { + return false; + } + + repoConfig = FolderBackupSharePreferenceHelper.readRepoConfig(); + if (repoConfig == null) { + return false; + } + + return true; + } + } diff --git a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadMediaFileAutomaticallyWorker.java b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadMediaFileAutomaticallyWorker.java index dd568ac18..9a6c6f590 100644 --- a/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadMediaFileAutomaticallyWorker.java +++ b/app/src/main/java/com/seafile/seadroid2/framework/worker/upload/UploadMediaFileAutomaticallyWorker.java @@ -22,12 +22,14 @@ import com.seafile.seadroid2.enums.TransferDataSource; import com.seafile.seadroid2.enums.TransferResult; import com.seafile.seadroid2.framework.datastore.sp_livedata.AlbumBackupSharePreferenceHelper; +import com.seafile.seadroid2.framework.datastore.sp_livedata.FolderBackupSharePreferenceHelper; import com.seafile.seadroid2.framework.notification.AlbumBackupNotificationHelper; import com.seafile.seadroid2.framework.notification.base.BaseTransferNotificationHelper; import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; import com.seafile.seadroid2.framework.worker.TransferEvent; import com.seafile.seadroid2.framework.worker.TransferWorker; +import com.seafile.seadroid2.ui.folder_backup.RepoConfig; import java.util.List; import java.util.UUID; @@ -54,9 +56,24 @@ public BaseTransferNotificationHelper getNotification() { return albumNotificationHelper; } + boolean isFirstShow = true; + private void startShowNotification(){ + if (!isFirstShow) { + return; + } + + //send start transfer event + sendEvent(TransferEvent.EVENT_TRANSFERRING, TransferDataSource.ALBUM_BACKUP); + + // show foreground notification + ForegroundInfo foregroundInfo = albumNotificationHelper.getForegroundNotification(); + showForegroundAsync(foregroundInfo); + } + @NonNull @Override public ListenableWorker.Result doWork() { + SLogs.d("start upload media worker"); Account account = SupportAccountManager.getInstance().getCurrentAccount(); if (account == null) { @@ -66,17 +83,14 @@ public ListenableWorker.Result doWork() { return ListenableWorker.Result.success(); } - boolean isEnable = AlbumBackupSharePreferenceHelper.readBackupSwitch(); - if (!isEnable) { - return ListenableWorker.Result.success(); + boolean canExec = can(); + if (!canExec) { + return Result.success(); } - //send start transfer event - sendEvent(TransferEvent.EVENT_TRANSFERRING, TransferDataSource.ALBUM_BACKUP); - - // show foreground notification - ForegroundInfo foregroundInfo = albumNotificationHelper.getForegroundNotification(); - showForegroundAsync(foregroundInfo); + if (repoConfig == null) { + return Result.success(); + } // String finishFlagEvent = null; @@ -86,8 +100,6 @@ public ListenableWorker.Result doWork() { break; } - SLogs.d("start upload media worker"); - List transferList = AppDatabase .getInstance() .fileTransferDAO() @@ -99,11 +111,15 @@ public ListenableWorker.Result doWork() { break; } - FileTransferEntity transferEntity = transferList.get(0); + startShowNotification(); isUploaded = true; - try { + FileTransferEntity transferEntity = transferList.get(0); + transferEntity.repo_id = repoConfig.getRepoID(); + transferEntity.repo_name = repoConfig.getRepoName(); + + try { transferFile(account, transferEntity); sendTransferEvent(transferEntity, true); @@ -113,9 +129,11 @@ public ListenableWorker.Result doWork() { TransferResult transferResult = onException(transferEntity, e); - notifyError(transferResult); + if (!isStopped()) { + notifyError(transferResult); - sendTransferEvent(transferEntity, false); + sendTransferEvent(transferEntity, false); + } String finishFlag = isInterrupt(transferResult); if (!TextUtils.isEmpty(finishFlag)) { @@ -156,4 +174,20 @@ public ListenableWorker.Result doWork() { .build(); return Result.success(outputData); } + + private RepoConfig repoConfig; + + private boolean can() { + boolean isTurnOn = AlbumBackupSharePreferenceHelper.readBackupSwitch(); + if (!isTurnOn) { + return false; + } + + repoConfig = AlbumBackupSharePreferenceHelper.readRepoConfig(); + if (repoConfig == null) { + return false; + } + + return true; + } } diff --git a/app/src/main/java/com/seafile/seadroid2/listener/FileTransferProgressListener.java b/app/src/main/java/com/seafile/seadroid2/listener/FileTransferProgressListener.java index 67ea45d50..426afc6cd 100644 --- a/app/src/main/java/com/seafile/seadroid2/listener/FileTransferProgressListener.java +++ b/app/src/main/java/com/seafile/seadroid2/listener/FileTransferProgressListener.java @@ -43,10 +43,26 @@ public void onProgressNotify(long cur, long total) { fileTransferEntity.transferred_size = cur; - int percent = (int) ((float) cur / (float) total * 100); + int percent = calcu(cur, total); progressListener.onProgressNotify(fileTransferEntity, percent, cur, total); } + public int onProgress(long cur, long total) { + + long nowt = System.currentTimeMillis(); + if (nowt - temp < 1000) { + return -1; + } + + temp = nowt; + return calcu(cur, total); + } + + public int calcu(long cur, long total) { + int percent = (int) ((float) cur / (float) total * 100); + return percent; + } + public interface TransferProgressListener { void onProgressNotify(FileTransferEntity fileTransferEntity, int percent, long transferredSize, long totalSize); } diff --git a/app/src/main/java/com/seafile/seadroid2/preferences/ContextStackPreferenceHelper.java b/app/src/main/java/com/seafile/seadroid2/preferences/ContextStackPreferenceHelper.java new file mode 100644 index 000000000..745bf1776 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/preferences/ContextStackPreferenceHelper.java @@ -0,0 +1,34 @@ +package com.seafile.seadroid2.preferences; + +import com.blankj.utilcode.util.GsonUtils; +import com.google.gson.reflect.TypeToken; +import com.seafile.seadroid2.framework.data.model.ContextModel; +import com.seafile.seadroid2.framework.datastore.DataStoreKeys; + +import java.lang.reflect.Type; +import java.util.Stack; + +public class ContextStackPreferenceHelper { + private static final String STACK_KEY = DataStoreKeys.KEY_NAV_CONTEXT_STACK; + + // save Stack to SharedPreferences + public static void saveStack(Stack stack) { + String json = GsonUtils.toJson(stack); + Settings.getCommonPreferences().edit().putString(STACK_KEY, json).apply(); + } + + public static void clearStack() { + Settings.getCommonPreferences().edit().remove(STACK_KEY).apply(); + } + + public static Stack getStack() { + String json = Settings.getCommonPreferences().getString(STACK_KEY, null); + if (json == null) { + return new Stack<>(); + } + + Type type = new TypeToken>() { + }.getType(); + return GsonUtils.fromJson(json, type); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/preferences/Settings.java b/app/src/main/java/com/seafile/seadroid2/preferences/Settings.java index 2519d4d0c..86be26851 100644 --- a/app/src/main/java/com/seafile/seadroid2/preferences/Settings.java +++ b/app/src/main/java/com/seafile/seadroid2/preferences/Settings.java @@ -47,8 +47,11 @@ public Settings() { ////////////////// public static SettingsLiveData USER_INFO; public static SettingsLiveData SPACE_INFO; + public static SettingsLiveData USER_SERVER_INFO; public static SettingsLiveData NIGHT_MODE; public static SettingsLiveData APP_NIGHT_MODE; + + @Deprecated public static SettingsLiveData USER_GESTURE_LOCK_SWITCH; // public static SettingsLiveData USER_GESTURE_LOCK_TIMESTAMP; @@ -135,11 +138,12 @@ public static void initUserSettings(Account account) { //user USER_INFO = new StringSettingLiveData(_account.getEncryptSignature(), R.string.pref_key_user_info, R.string.settings_account_info_load_data); SPACE_INFO = new StringSettingLiveData(_account.getEncryptSignature(), R.string.pref_key_user_space, R.string.settings_account_info_load_data); + USER_SERVER_INFO = new StringSettingLiveData(_account.getEncryptSignature(), R.string.pref_key_user_server, Resources.ID_NULL); NIGHT_MODE = new EnumSettingLiveData<>(NightMode.class, _account.getEncryptSignature(), R.string.pref_key_night_mode, R.string.pref_default_value_night_mode); APP_NIGHT_MODE = new EnumSettingLiveData<>(NightMode.class, _account.getEncryptSignature(), R.string.pref_key_current_night_mode, R.string.pref_default_value_night_mode); - CLIENT_ENCRYPT_SWITCH = new BooleanSettingLiveData(_account.getEncryptSignature(), R.string.pref_key_security_client_encrypt); +// CLIENT_ENCRYPT_SWITCH = new BooleanSettingLiveData(_account.getEncryptSignature(), R.string.pref_key_security_client_encrypt); - USER_GESTURE_LOCK_SWITCH = new BooleanSettingLiveData(_account.getEncryptSignature(), R.string.pref_key_gesture_lock); +// USER_GESTURE_LOCK_SWITCH = new BooleanSettingLiveData(_account.getEncryptSignature(), R.string.pref_key_gesture_lock); // USER_GESTURE_LOCK_TIMESTAMP = new LongSettingLiveData(_account.getEncryptSignature(), R.string.pref_key_gesture_lock_timestamp, R.string.pref_default_value_key_gesture_lock_timestamp); @@ -167,11 +171,12 @@ public static void initUserSettings(Account account) { REGISTER_LIST.add(USER_INFO); REGISTER_LIST.add(SPACE_INFO); + REGISTER_LIST.add(USER_SERVER_INFO); REGISTER_LIST.add(NIGHT_MODE); REGISTER_LIST.add(APP_NIGHT_MODE); - REGISTER_LIST.add(USER_GESTURE_LOCK_SWITCH); +// REGISTER_LIST.add(USER_GESTURE_LOCK_SWITCH); // REGISTER_LIST.add(USER_GESTURE_LOCK_TIMESTAMP); - REGISTER_LIST.add(CLIENT_ENCRYPT_SWITCH); +// REGISTER_LIST.add(CLIENT_ENCRYPT_SWITCH); REGISTER_LIST.add(ALBUM_BACKUP_SWITCH); REGISTER_LIST.add(ALBUM_BACKUP_SELECTED_REPO); REGISTER_LIST.add(ALBUM_BACKUP_STATE); diff --git a/app/src/main/java/com/seafile/seadroid2/provider/SeafileProvider.java b/app/src/main/java/com/seafile/seadroid2/provider/SeafileProvider.java index afcccb1be..388e701ca 100644 --- a/app/src/main/java/com/seafile/seadroid2/provider/SeafileProvider.java +++ b/app/src/main/java/com/seafile/seadroid2/provider/SeafileProvider.java @@ -917,13 +917,17 @@ private void includeStarredFileDirent(MatrixCursor result, Account account, Star * @param path the path of the directory. * @param result Cursor object over which to signal the client. */ - private void fetchDirentAsync(Account account, RepoModel repoModel, final String path, MatrixCursor result) { + private void fetchDirentAsync(Account account, RepoModel repoModel, String path, MatrixCursor result) { String id = DocumentIdParser.buildId(account, repoModel.repo_id, path); final Uri uri = DocumentsContract.buildChildDocumentsUri(AUTHORITY_OF_DOCUMENTS, id); result.setNotificationUri(getContext().getContentResolver(), uri); + if (!path.endsWith("/")) { + path = path + "/"; + } + Single> resultSingle = Objs.getDirentsSingleFromServer(account, repoModel.repo_id, repoModel.repo_name, path); Disposable disposable = resultSingle.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/com/seafile/seadroid2/ssl/CertsManager.java b/app/src/main/java/com/seafile/seadroid2/ssl/CertsManager.java index d39f018d5..2cdf472a6 100644 --- a/app/src/main/java/com/seafile/seadroid2/ssl/CertsManager.java +++ b/app/src/main/java/com/seafile/seadroid2/ssl/CertsManager.java @@ -17,7 +17,7 @@ */ public final class CertsManager { - private final Map cachedCerts = Maps.newConcurrentMap(); + private final Map cachedCerts = Maps.newConcurrentMap(); private static CertsManager instance; @@ -36,7 +36,7 @@ public void saveCertForAccount(final Account account, boolean rememberChoice) { } final X509Certificate cert = certs.get(0); - cachedCerts.put(account, cert); + cachedCerts.put(account.getServer(), cert); if (rememberChoice) { // save cert info to shared preferences @@ -46,12 +46,23 @@ public void saveCertForAccount(final Account account, boolean rememberChoice) { } } + public void deleteCertForAccount(final Account account) { + if (account == null) { + return; + } + + cachedCerts.remove(account.getServer()); + + String keyPrefix = EncryptUtils.encryptMD5ToString(account.getServer()); + Settings.getCommonPreferences().edit().remove(DataStoreKeys.KEY_SERVER_CERT_INFO + "_" + keyPrefix).apply(); + } public X509Certificate getCertificate(Account account) { - X509Certificate cert = cachedCerts.get(account); + X509Certificate cert = cachedCerts.get(account.getServer()); if (cert != null) { return cert; } + String keyPrefix = EncryptUtils.encryptMD5ToString(account.getServer()); String certBase64 = Settings.getCommonPreferences().getString(DataStoreKeys.KEY_SERVER_CERT_INFO + "_" + keyPrefix, null); if (TextUtils.isEmpty(certBase64)) { @@ -60,7 +71,7 @@ public X509Certificate getCertificate(Account account) { cert = CertsHelper.convertToCert(certBase64); if (cert != null) { - cachedCerts.put(account, cert); + cachedCerts.put(account.getServer(), cert); } return cert; diff --git a/app/src/main/java/com/seafile/seadroid2/ssl/SSLTrustManager.java b/app/src/main/java/com/seafile/seadroid2/ssl/SSLTrustManager.java index fc0ea4f36..c75f1312c 100644 --- a/app/src/main/java/com/seafile/seadroid2/ssl/SSLTrustManager.java +++ b/app/src/main/java/com/seafile/seadroid2/ssl/SSLTrustManager.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Set; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/account/AccountDetailActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/account/AccountDetailActivity.java index b96d0ce27..7dc20596b 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/account/AccountDetailActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/account/AccountDetailActivity.java @@ -1,40 +1,24 @@ package com.seafile.seadroid2.ui.account; -import android.app.Dialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; -import android.text.method.HideReturnsTransformationMethod; -import android.text.method.PasswordTransformationMethod; import android.util.Log; import android.util.Pair; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.CheckBox; import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; import androidx.core.app.NavUtils; import androidx.core.app.TaskStackBuilder; import androidx.lifecycle.Observer; -import com.blankj.utilcode.constant.RegexConstants; import com.blankj.utilcode.util.NetworkUtils; -import com.blankj.utilcode.util.RegexUtils; -import com.google.android.material.button.MaterialButton; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.textfield.TextInputLayout; import com.seafile.seadroid2.R; import com.seafile.seadroid2.SeafException; import com.seafile.seadroid2.account.Account; @@ -150,9 +134,9 @@ private void initViewModel() { @Override public void onChanged(Boolean aBoolean) { if (aBoolean) { - showProgressDialog(); + showLoadingDialog(); } else { - dismissProgressDialog(); + dismissLoadingDialog(); } } }); @@ -274,7 +258,7 @@ private void onLoggedIn(Account loginAccount) { @Override protected void onDestroy() { - dismissProgressDialog(); + dismissLoadingDialog(); super.onDestroy(); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/account/AccountViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/account/AccountViewModel.java index a79bcc7e0..db2cfc8dc 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/account/AccountViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/account/AccountViewModel.java @@ -17,6 +17,8 @@ import com.seafile.seadroid2.framework.http.HttpIO; import com.seafile.seadroid2.account.AccountUtils; import com.seafile.seadroid2.framework.util.DeviceIdManager; +import com.seafile.seadroid2.preferences.ContextStackPreferenceHelper; +import com.seafile.seadroid2.ssl.CertsManager; import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; import com.seafile.seadroid2.ui.dialog_fragment.SignOutDialogFragment; import com.seafile.seadroid2.ui.main.MainService; @@ -181,7 +183,7 @@ private Call getLoginCall(Account tempAccount, String pwd, String to body.put("client_version", appVersion); body.put("platform_version", Build.VERSION.RELEASE); - Map requestBody = generateRequestBody(body); + Map requestBody = genRequestBody(body); return HttpIO.getInstanceByAccount(tempAccount).execute(AccountService.class).login(headers, requestBody); } @@ -214,9 +216,6 @@ public void accept(Throwable throwable) throws Exception { }); } - /** - * @see SignOutDialogFragment#onPositiveClick() - */ public void deleteAccount(Account account) { Account curAccount = SupportAccountManager.getInstance().getCurrentAccount(); @@ -224,6 +223,9 @@ public void deleteAccount(Account account) { if (curAccount != null && curAccount.equals(account)) { // AccountUtils.logout(account); + } else { + HttpIO.removeInstanceByAccount(account); + CertsManager.instance().deleteCertForAccount(account); } //delete local account diff --git a/app/src/main/java/com/seafile/seadroid2/ui/account/AccountsActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/account/AccountsActivity.java index 97ee1984d..ce5e726df 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/account/AccountsActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/account/AccountsActivity.java @@ -3,7 +3,6 @@ import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.accounts.OnAccountsUpdateListener; -import android.app.Dialog; import android.content.Intent; import android.os.Bundle; import android.util.Log; @@ -21,7 +20,6 @@ import com.blankj.utilcode.util.ActivityUtils; import com.blankj.utilcode.util.AppUtils; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.seafile.seadroid2.R; import com.seafile.seadroid2.framework.data.ServerInfo; import com.seafile.seadroid2.framework.datastore.sp.AppDataManager; @@ -157,9 +155,9 @@ private void initViewModel() { @Override public void onChanged(Boolean aBoolean) { if (aBoolean) { - showProgressDialog(); + showLoadingDialog(); } else { - dismissProgressDialog(); + dismissLoadingDialog(); } } }); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/account/SeafileAuthenticatorActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/account/SeafileAuthenticatorActivity.java index a993ea2a5..6267eb6c1 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/account/SeafileAuthenticatorActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/account/SeafileAuthenticatorActivity.java @@ -23,13 +23,13 @@ import androidx.core.app.NavUtils; import androidx.core.app.TaskStackBuilder; -import com.google.firebase.analytics.FirebaseAnalytics; import com.seafile.seadroid2.R; import com.seafile.seadroid2.account.Authenticator; import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.config.Constants; import com.seafile.seadroid2.framework.http.HttpIO; import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.preferences.ContextStackPreferenceHelper; import com.seafile.seadroid2.ui.camera_upload.CameraUploadManager; import java.util.Locale; @@ -294,6 +294,8 @@ private void finishLogin(Intent intent) { SupportAccountManager.getInstance().setUserData(newAccount, Authenticator.KEY_SHIB, "shib"); } + // + ContextStackPreferenceHelper.clearStack(); //save current account SupportAccountManager.getInstance().saveCurrentAccount(newAccountName); //reset httpio instance diff --git a/app/src/main/java/com/seafile/seadroid2/ui/activities/AllActivitiesFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/activities/AllActivitiesFragment.java index 24d1dfbc4..923e76dcb 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/activities/AllActivitiesFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/activities/AllActivitiesFragment.java @@ -40,7 +40,7 @@ import com.seafile.seadroid2.ui.file.FileActivity; import com.seafile.seadroid2.ui.main.MainActivity; import com.seafile.seadroid2.ui.markdown.MarkdownActivity; -import com.seafile.seadroid2.ui.media.image_preview.ImagePreviewActivity; +import com.seafile.seadroid2.ui.media.image_preview2.CarouselImagePreviewActivity; import com.seafile.seadroid2.ui.media.player.exoplayer.CustomExoVideoPlayerActivity; import com.seafile.seadroid2.ui.sdoc.SDocWebViewActivity; import com.seafile.seadroid2.view.TipsViews; @@ -97,12 +97,12 @@ public void onFirstResume() { private void initAdapter() { adapter = new ActivityAdapter(); + TextView tipView = TipsViews.getTipTextView(requireContext()); tipView.setText(R.string.no_starred_file); tipView.setOnClickListener(v -> reload()); adapter.setStateView(tipView); adapter.setStateViewEnable(false); - adapter.setAnimationEnable(true); adapter.setOnItemClickListener((baseQuickAdapter, view, i) -> { ActivityModel activityModel = (ActivityModel) adapter.getItems().get(i); @@ -274,7 +274,7 @@ private void open(RepoModel repoModel, ActivityModel activityModel) { } else if (Utils.isViewableImage(activityModel.name)) { - Intent getIntent = ImagePreviewActivity.startThisFromActivity(requireContext(), activityModel); + Intent getIntent = CarouselImagePreviewActivity.startThisFromActivity(requireContext(), activityModel); imagePreviewActivityLauncher.launch(getIntent); } else if (activityModel.name.endsWith(Constants.Format.DOT_SDOC)) { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/activities/MineActivitiesFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/activities/MineActivitiesFragment.java index 69369814b..882e38022 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/activities/MineActivitiesFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/activities/MineActivitiesFragment.java @@ -12,15 +12,17 @@ import com.seafile.seadroid2.R; import com.seafile.seadroid2.SeafException; -import com.seafile.seadroid2.ui.base.fragment.BaseFragmentWithVM; +import com.seafile.seadroid2.annotation.NotSupport; import com.seafile.seadroid2.databinding.LayoutFrameSwipeRvBinding; import com.seafile.seadroid2.framework.data.model.activities.ActivityModel; +import com.seafile.seadroid2.ui.base.fragment.BaseFragmentWithVM; import com.seafile.seadroid2.view.TipsViews; import java.util.List; import kotlin.Pair; +@NotSupport @Deprecated public class MineActivitiesFragment extends BaseFragmentWithVM { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/base/BaseActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/base/BaseActivity.java index db0bf401e..34702fc2e 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/base/BaseActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/base/BaseActivity.java @@ -3,15 +3,12 @@ import android.app.Dialog; import android.os.Bundle; -import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.Toolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.seafile.seadroid2.R; -import com.seafile.seadroid2.framework.helper.NightModeHelper; /** * A base activity that handles common functionality in the app. This includes Action Bar tweaks. @@ -50,31 +47,31 @@ public void setContentView(int layoutResID) { getActionBarToolbar(); } - private Dialog dialog; + private Dialog loadingDialog; - public void showProgressDialog(boolean isShow) { + public void showLoadingDialog(boolean isShow) { if (isShow) { - showProgressDialog(); + showLoadingDialog(); } else { - dismissProgressDialog(); + dismissLoadingDialog(); } } - public void showProgressDialog() { - if (dialog == null) { + public void showLoadingDialog() { + if (loadingDialog == null) { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setView(R.layout.layout_dialog_progress_bar); - dialog = builder.create(); + loadingDialog = builder.create(); } - if (!dialog.isShowing()) { - dialog.show(); + if (!loadingDialog.isShowing()) { + loadingDialog.show(); } } - public void dismissProgressDialog() { - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); + public void dismissLoadingDialog() { + if (loadingDialog != null && loadingDialog.isShowing()) { + loadingDialog.dismiss(); } } } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/base/BaseMediaSelectorActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/base/BaseMediaSelectorActivity.java new file mode 100644 index 000000000..e38c5bb2e --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/base/BaseMediaSelectorActivity.java @@ -0,0 +1,174 @@ +package com.seafile.seadroid2.ui.base; + +import android.Manifest; +import android.net.Uri; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.PickVisualMediaRequest; +import androidx.activity.result.contract.ActivityResultContracts; + +import com.blankj.utilcode.util.ToastUtils; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.framework.util.TakeCameras; +import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; +import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetHelper; +import com.seafile.seadroid2.ui.bottomsheetmenu.OnMenuClickListener; + +import java.io.File; +import java.util.List; + +import kotlin.Pair; + +public class BaseMediaSelectorActivity extends BaseActivityWithVM { + private ActivityResultLauncher cameraPermissionLauncher, cameraPermissionLauncher1, storagePermissionLauncher; + private ActivityResultLauncher takePhotoLauncher; + private ActivityResultLauncher shootVideoLauncher; + private ActivityResultLauncher pickMediaLauncher; + private ActivityResultLauncher pickMultipleMediaLauncher; + private ActivityResultLauncher fileChooseLauncher; + private Pair uriPair; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initLauncher(); + } + + private void initLauncher() { + cameraPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback() { + @Override + public void onActivityResult(Boolean result) { + if (result) { + uriPair = TakeCameras.buildTakePhotoUri(BaseMediaSelectorActivity.this); + takePhotoLauncher.launch(uriPair.getFirst()); + } else { + ToastUtils.showLong(R.string.permission_camera); + } + } + }); + + cameraPermissionLauncher1 = registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback() { + @Override + public void onActivityResult(Boolean result) { + if (result) { + uriPair = TakeCameras.buildTakePhotoUri(BaseMediaSelectorActivity.this); + shootVideoLauncher.launch(uriPair.getFirst()); + } else { + ToastUtils.showLong(R.string.permission_camera); + } + } + }); + + storagePermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback() { + @Override + public void onActivityResult(Boolean o) { + if (o) { + fileChooseLauncher.launch("*/*"); + } else { + ToastUtils.showLong(R.string.permission_manage_external_storage_rationale); + } + } + }); + + takePhotoLauncher = registerForActivityResult(new ActivityResultContracts.TakePicture(), new ActivityResultCallback() { + @Override + public void onActivityResult(Boolean o) { + if (!o) { + return; + } + onMediaPicked(uriPair.getFirst()); + } + }); + + shootVideoLauncher = registerForActivityResult(new ActivityResultContracts.CaptureVideo(), new ActivityResultCallback() { + @Override + public void onActivityResult(Boolean o) { + if (!o) { + return; + } + onMediaPicked(uriPair.getFirst()); + } + }); + + pickMediaLauncher = registerForActivityResult(new ActivityResultContracts.PickVisualMedia(), new ActivityResultCallback() { + @Override + public void onActivityResult(Uri o) { + onMediaPicked(o); + } + }); + + pickMultipleMediaLauncher = registerForActivityResult(new ActivityResultContracts.PickMultipleVisualMedia(9), new ActivityResultCallback>() { + @Override + public void onActivityResult(List o) { + for (Uri uri : o) { + onMediaPicked(uri); + } + } + }); + + fileChooseLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), new ActivityResultCallback() { + @Override + public void onActivityResult(Uri o) { + onMediaPicked(o); + } + }); + } + + public void onMediaPicked(Uri uri) { + SLogs.d(uri.toString()); + } + + /////////////////////////////////////////////////////// + + public void showPickPhotoSheetDialog(boolean isPickMultiWhenMenuIdIsViewFile) { + BottomSheetHelper.buildSheet(this, R.menu.bottom_sheet_camera_album_select, new OnMenuClickListener() { + @Override + public void onMenuClick(MenuItem menuItem) { + if (menuItem.getItemId() == R.id.take_photo) { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA); + } else if (menuItem.getItemId() == R.id.view_file) { + if (isPickMultiWhenMenuIdIsViewFile) { + pickMultipleMediaLauncher.launch(new PickVisualMediaRequest.Builder() + .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE) + .build()); + } else { + pickMediaLauncher.launch(new PickVisualMediaRequest.Builder() + .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE) + .build()); + } + } + } + }).show(getSupportFragmentManager()); + } + + public void showPickPhotoAndVideoSheetDialog(boolean isPickMultiWhenMenuIdIsViewFile) { + BottomSheetHelper.buildSheet(this, R.menu.bottom_sheet_camera_album_select, new OnMenuClickListener() { + @Override + public void onMenuClick(MenuItem menuItem) { + if (menuItem.getItemId() == R.id.take_photo) { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA); + } else if (menuItem.getItemId() == R.id.view_file) { + if (isPickMultiWhenMenuIdIsViewFile) { + pickMultipleMediaLauncher.launch(new PickVisualMediaRequest.Builder() + .setMediaType(ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE) + .build()); + } else { + pickMediaLauncher.launch(new PickVisualMediaRequest.Builder() + .setMediaType(ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE) + .build()); + } + } + } + }).show(getSupportFragmentManager()); + } + + public void pickFile() { + storagePermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE); + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/base/adapter/BaseAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/base/adapter/BaseAdapter.java index a4e8203a0..6d98aa141 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/base/adapter/BaseAdapter.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/base/adapter/BaseAdapter.java @@ -8,6 +8,4 @@ public abstract class BaseAdapter extends BaseQuic public void d(String d) { SLogs.d(this.getClass().getSimpleName() + " => " + d); } - - } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/base/fragment/BaseFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/base/fragment/BaseFragment.java index 3de25e4de..d661b7ebc 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/base/fragment/BaseFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/base/fragment/BaseFragment.java @@ -1,7 +1,11 @@ package com.seafile.seadroid2.ui.base.fragment; +import android.app.Dialog; + import androidx.fragment.app.Fragment; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.seafile.seadroid2.R; import com.seafile.seadroid2.framework.util.SLogs; public class BaseFragment extends Fragment { @@ -29,4 +33,32 @@ public void onFirstResume() { public void onOtherResume() { } + + private Dialog dialog; + + public void showLoadingDialog(boolean isShow) { + if (isShow) { + showLoadingDialog(); + } else { + dismissLoadingDialog(); + } + } + + public void showLoadingDialog() { + if (dialog == null) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); + builder.setView(R.layout.layout_dialog_progress_bar); + dialog = builder.create(); + } + + if (!dialog.isShowing()) { + dialog.show(); + } + } + + public void dismissLoadingDialog() { + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + } } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/base/viewmodel/BaseViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/base/viewmodel/BaseViewModel.java index 8cbc4d260..d0cf1911f 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/base/viewmodel/BaseViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/base/viewmodel/BaseViewModel.java @@ -13,6 +13,7 @@ import com.seafile.seadroid2.R; import com.seafile.seadroid2.SeadroidApplication; import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.annotation.Todo; import com.seafile.seadroid2.framework.data.model.ResultModel; import com.seafile.seadroid2.framework.http.HttpIO; import com.seafile.seadroid2.framework.util.SLogs; @@ -27,6 +28,7 @@ import java.net.SocketTimeoutException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; @@ -118,7 +120,19 @@ protected void onCleared() { } } - public Map generateRequestBody(Map requestDataMap) { + + @Todo + public Map genObjRequestBody(Map params) { + Map requestBodyMap = new HashMap<>(); + for (Map.Entry entry : params.entrySet()) { + String value = entry.getValue().toString(); + RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), value); + requestBodyMap.put(entry.getKey(), requestBody); + } + return requestBodyMap; + } + + public Map genRequestBody(Map requestDataMap) { Map requestBodyMap = new HashMap<>(); if (requestDataMap == null || requestDataMap.isEmpty()) { requestBodyMap.put("x-test", RequestBody.create(MediaType.parse("multipart/form-data"), "test")); @@ -181,7 +195,6 @@ public void addFlowableDisposable(Flowable flowable, Consumer onNext, .subscribe(onNext, throwable, onComplete)); } - public void addCompletableDisposable(Completable completable, Action action) { compositeDisposable.add(completable .subscribeOn(Schedulers.io()) @@ -250,11 +263,16 @@ public SeafException getExceptionByThrowable(Throwable throwable) throws IOExcep return SeafException.notFoundException; } + //HTTP_STATUS_REPO_PASSWORD_REQUIRED if (440 == httpException.code()) { return SeafException.invalidPassword; } +// //500: HTTP_INTERNAL_ERROR +// if (HttpURLConnection.HTTP_INTERNAL_ERROR == httpException.code()) { +// return SeafException.networkException; +// } if (resp != null) { try { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/camera_upload/AlbumBackupAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/camera_upload/AlbumBackupAdapter.java index f0c0e647d..a74964f84 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/camera_upload/AlbumBackupAdapter.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/camera_upload/AlbumBackupAdapter.java @@ -6,7 +6,6 @@ import android.content.Context; import android.content.SyncResult; import android.os.Bundle; -import android.util.Log; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; @@ -50,14 +49,14 @@ public boolean onUnsyncableAccount() { public void onSyncCanceled(Thread thread) { super.onSyncCanceled(thread); SLogs.e("onSyncCanceled ->" + thread.getName()); - BackgroundJobManagerImpl.getInstance().cancelAllMediaWorker(); + BackgroundJobManagerImpl.getInstance().cancelMediaWorker(); } @Override public void onSyncCanceled() { super.onSyncCanceled(); SLogs.e("onSyncCanceled"); - BackgroundJobManagerImpl.getInstance().cancelAllMediaWorker(); + BackgroundJobManagerImpl.getInstance().cancelMediaWorker(); } @Override @@ -66,13 +65,11 @@ public void onPerformSync(android.accounts.Account account, ContentProviderClient provider, SyncResult syncResult) { - SLogs.e("onPerformSync!"); + boolean isForce = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL); + SLogs.e("albumBackupAdapter - onPerformSync, isForce -> " + isForce); Account seafileAccount = SupportAccountManager.getInstance().getSeafileAccount(account); - /** - * this should never occur, as camera upload is supposed to be disabled once the camera upload - * account signs out. - */ + // this should never occur, as camera upload is supposed to be disabled once the camera upload account signs out. if (!seafileAccount.hasValidToken()) { SLogs.e("This account has no auth token. Disable camera upload."); syncResult.stats.numAuthExceptions++; @@ -87,8 +84,7 @@ public void onPerformSync(android.accounts.Account account, return; } - //start - boolean isForce = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL); - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(isForce); + // start + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(isForce); } } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/camera_upload/CameraUploadManager.java b/app/src/main/java/com/seafile/seadroid2/ui/camera_upload/CameraUploadManager.java index eef46c498..31fe7d0cc 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/camera_upload/CameraUploadManager.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/camera_upload/CameraUploadManager.java @@ -8,6 +8,7 @@ import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; import java.util.List; @@ -122,6 +123,8 @@ public void disableCameraUpload() { ContentResolver.cancelSync(account.getAndroidAccount(), AUTHORITY); ContentResolver.setIsSyncable(account.getAndroidAccount(), AUTHORITY, 0); } + + BackgroundJobManagerImpl.getInstance().cancelMediaWorker(); } /** diff --git a/app/src/main/java/com/seafile/seadroid2/ui/comparator/NaturalOrderComparator.java b/app/src/main/java/com/seafile/seadroid2/ui/comparator/NaturalOrderComparator.java new file mode 100644 index 000000000..fb7936e1e --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/comparator/NaturalOrderComparator.java @@ -0,0 +1,98 @@ +package com.seafile.seadroid2.ui.comparator; + +import android.text.TextUtils; + +import com.seafile.seadroid2.framework.data.db.entities.DirentModel; +import com.seafile.seadroid2.framework.data.db.entities.RepoModel; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Comparator; + +public class NaturalOrderComparator implements Comparator { + + @Override + public Comparator reversed() { + return Comparator.super.reversed(); + } + + @Override + public int compare(Object s1, Object s2) { + String name1 = ""; + String name2 = ""; + if (s1 instanceof RepoModel m1 && s2 instanceof RepoModel m2) { + name1 = m1.repo_name; + name2 = m2.repo_name; + } else if (s1 instanceof DirentModel m1 && s2 instanceof DirentModel m2) { + name1 = m1.name; + name2 = m2.name; + } + + if (TextUtils.isEmpty(name1) || TextUtils.isEmpty(name2)) { + return -1; + } + + int len1 = name1.length(); + int len2 = name2.length(); + int i1 = 0, i2 = 0; + + while (i1 < len1 && i2 < len2) { + char c1 = name1.charAt(i1); + char c2 = name2.charAt(i2); + + // If it's all numbers, the entire number part is extracted for comparison + if (Character.isDigit(c1) && Character.isDigit(c2)) { + BigDecimal num1 = extractNumber(name1, i1); + BigDecimal num2 = extractNumber(name2, i2); + int cmp = num1.compareTo(num2); + if (cmp != 0) { + return cmp; + } + + // Skip the part of the number that has been processed + i1 = skipDigits(name1, i1); + i2 = skipDigits(name2, i2); + } else { + // 否则按字符比较 + if (c1 != c2) { + + // If c1 is lowercase and c2 is uppercase, c1 should come first + if (Character.isLowerCase(c1) && Character.isUpperCase(c2)) { + return -1; + } + // If c1 is uppercase and c2 is lowercase, c2 should come first + if (Character.isUpperCase(c1) && Character.isLowerCase(c2)) { + return 1; + } + + return c1 - c2; + } + i1++; + i2++; + } + } + + // If the previous parts are all the same, compare by length + return len1 - len2; + } + + private BigDecimal extractNumber(String s, int start) { + int end = start; + while (end < s.length() && Character.isDigit(s.charAt(end))) { + end++; + } + return new BigDecimal(s.substring(start, end)); + } + + private int skipDigits(String s, int start) { + while (start < s.length() && Character.isDigit(s.charAt(start))) { + start++; + } + return start; + } + + @Override + public boolean equals(Object obj) { + return false; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/dialog/SslConfirmDialog.java b/app/src/main/java/com/seafile/seadroid2/ui/dialog/SslConfirmDialog.java index 747149ce7..c448c3fc8 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/dialog/SslConfirmDialog.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/dialog/SslConfirmDialog.java @@ -115,7 +115,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { binding.notAfter.setText(not_available); } - builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + builder.setPositiveButton(R.string.ignore, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Log.d(DEBUG_TAG, "listener.onAccepted is called"); @@ -123,7 +123,7 @@ public void onClick(DialogInterface dialog, int which) { } }); - builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Log.d(DEBUG_TAG, "listener.onRejected is called"); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/NewRepoDialogFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/NewRepoDialogFragment.java index 9c190e6ae..6f73b9fa3 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/NewRepoDialogFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/NewRepoDialogFragment.java @@ -6,6 +6,7 @@ import android.widget.LinearLayout; import com.blankj.utilcode.util.ToastUtils; +import com.google.android.material.materialswitch.MaterialSwitch; import com.google.android.material.switchmaterial.SwitchMaterial; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; @@ -30,7 +31,7 @@ public int getDialogTitleRes() { public void initView(LinearLayout containerView) { super.initView(containerView); - SwitchMaterial materialSwitch = getDialogView().findViewById(R.id.widget_switch); + MaterialSwitch materialSwitch = getDialogView().findViewById(R.id.widget_switch); TextInputLayout pwd1 = getDialogView().findViewById(R.id.new_repo_input_layout_pwd_1); TextInputLayout pwd2 = getDialogView().findViewById(R.id.new_repo_input_layout_pwd_2); pwd1.setHint(String.format( @@ -71,7 +72,7 @@ protected void onPositiveClick() { } TextInputEditText name = getDialogView().findViewById(R.id.new_repo_edit_name); - SwitchMaterial materialSwitch = getDialogView().findViewById(R.id.widget_switch); + MaterialSwitch materialSwitch = getDialogView().findViewById(R.id.widget_switch); if (materialSwitch.isChecked()) { TextInputEditText pwd1 = getDialogView().findViewById(R.id.new_repo_edit_pwd_1); String pwd1Str = pwd1.getText() == null ? "" : pwd1.getText().toString(); @@ -92,7 +93,7 @@ private boolean checkData() { return false; } - SwitchMaterial materialSwitch = getDialogView().findViewById(R.id.widget_switch); + MaterialSwitch materialSwitch = getDialogView().findViewById(R.id.widget_switch); if (!materialSwitch.isChecked()) { return true; } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/SignOutDialogFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/SignOutDialogFragment.java index e92e93430..50a170f1a 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/SignOutDialogFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/SignOutDialogFragment.java @@ -8,6 +8,7 @@ import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.account.AccountUtils; +import com.seafile.seadroid2.ssl.CertsManager; import com.seafile.seadroid2.ui.base.fragment.CustomDialogFragment; public class SignOutDialogFragment extends CustomDialogFragment { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/NewDirViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/NewDirViewModel.java index 4fb497a1c..40f1c1db5 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/NewDirViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/NewDirViewModel.java @@ -39,7 +39,7 @@ public void createNewDir(String p, String repo_id) { Map requestDataMap = new HashMap<>(); requestDataMap.put("operation", "mkdir"); - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single single = HttpIO.getCurrentInstance().execute(DialogService.class).createDir(repo_id, p, bodyMap); addSingleDisposable(single, new Consumer() { @@ -77,7 +77,7 @@ public void createNewFile(String filePathName, String repo_id) { Map requestDataMap = new HashMap<>(); requestDataMap.put("operation", "create"); - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single single = HttpIO.getCurrentInstance().execute(DialogService.class).createFile(repo_id, filePathName, bodyMap); addSingleDisposable(single, new Consumer() { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/NewRepoViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/NewRepoViewModel.java index 09958fa5c..e3163a7cb 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/NewRepoViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/NewRepoViewModel.java @@ -40,7 +40,7 @@ public void createNewRepo(String repoName, String description, String password) if (!TextUtils.isEmpty(password)) { requestDataMap.put("passwd", password); } - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single single = HttpIO.getCurrentInstance().execute(DialogService.class).createRepo(bodyMap); addSingleDisposable(single, new Consumer() { @Override diff --git a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/PasswordViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/PasswordViewModel.java index 61642b09c..a5dde46cd 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/PasswordViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/PasswordViewModel.java @@ -135,7 +135,7 @@ private void remoteVerify(RepoModel repoModel, String password) { Map requestDataMap = new HashMap<>(); requestDataMap.put("password", password); - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single netSingle = HttpIO.getCurrentInstance().execute(DialogService.class).setPassword(repoModel.repo_id, bodyMap); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/RenameRepoViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/RenameRepoViewModel.java index 13300d1d5..b9f331461 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/RenameRepoViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/dialog_fragment/viewmodel/RenameRepoViewModel.java @@ -38,7 +38,7 @@ public void renameRepo(String repoName, String repoId) { Map requestDataMap = new HashMap<>(); requestDataMap.put("repo_name", repoName); - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single single = HttpIO.getCurrentInstance().execute(DialogService.class).renameRepo(repoId, bodyMap); @@ -64,7 +64,7 @@ public void renameDir(String repoId, String curPath, String newName) { Map requestDataMap = new HashMap<>(); requestDataMap.put("operation", "rename"); requestDataMap.put("newname", newName); - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single single = HttpIO.getCurrentInstance().execute(DialogService.class).renameDir(repoId, curPath, bodyMap); @@ -90,7 +90,7 @@ public void renameFile(String repoId, String curPath, String newName) { Map requestDataMap = new HashMap<>(); requestDataMap.put("operation", "rename"); requestDataMap.put("newname", newName); - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single single = HttpIO.getCurrentInstance().execute(DialogService.class).renameFile(repoId, curPath, bodyMap); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentAdapter.java similarity index 58% rename from app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentAdapter.java rename to app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentAdapter.java index 7037b50fd..b165be586 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentAdapter.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentAdapter.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.ui.sdoc.comments; +package com.seafile.seadroid2.ui.docs_comment; import static com.seafile.seadroid2.config.Constants.DP.DP_4; import static com.seafile.seadroid2.config.Constants.DP.DP_8; @@ -7,7 +7,9 @@ import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -20,20 +22,28 @@ import com.bumptech.glide.Glide; import com.google.android.flexbox.FlexboxLayout; import com.seafile.seadroid2.R; -import com.seafile.seadroid2.databinding.ItemSdocCommentBinding; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocCommentModel; +import com.seafile.seadroid2.config.GlideLoadConfig; +import com.seafile.seadroid2.databinding.ItemFileCommentBinding; +import com.seafile.seadroid2.databinding.LayoutImageBinding; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsCommentModel; +import com.seafile.seadroid2.framework.util.GlideApp; +import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.ui.base.adapter.BaseAdapter; +import com.seafile.seadroid2.ui.media.image_preview2.OnlyImagePreviewActivity; +import com.seafile.seadroid2.view.rich_edittext.RichEditText; +import com.seafile.seadroid2.widget.SimpleMarkdownParser; import com.yydcdut.markdown.MarkdownConfiguration; import com.yydcdut.markdown.MarkdownProcessor; import com.yydcdut.markdown.MarkdownTextView; import com.yydcdut.markdown.loader.DefaultLoader; import com.yydcdut.markdown.syntax.text.TextFactory; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; -public class SDocCommentAdapter extends BaseAdapter { +public class DocsCommentAdapter extends BaseAdapter { public static int SCREEN_WIDTH = ScreenUtils.getScreenWidth(); public static int WIDTH = SizeUtils.dp2px(120); public static int IMAGE_WIDTH = (SCREEN_WIDTH - WIDTH) / 3; @@ -56,7 +66,7 @@ private MarkdownProcessor getProcessor() { } @Override - protected void onBindViewHolder(@NonNull SDocCommentViewHolder holder, int i, @Nullable SDocCommentModel model) { + protected void onBindViewHolder(@NonNull DocsCommentViewHolder holder, int i, @Nullable DocsCommentModel model) { if (model == null) { return; } @@ -69,83 +79,81 @@ protected void onBindViewHolder(@NonNull SDocCommentViewHolder holder, int i, @N holder.binding.commentNickName.setText(model.user_name); holder.binding.commentTime.setText(model.getCreatedAtFriendlyText()); -// if (TextUtils.isEmpty(model.resolved) || "false".equals(model.resolved.toLowerCase(Locale.getDefault()))) { -// holder.binding.commentContentContainer.setBackgroundResource(R.drawable.shape_stroke1_radius8_solid_grey); -// } else { - holder.binding.commentContentContainer.setBackgroundResource(R.drawable.shape_stroke1_radius8_solid_white); -// } + if (model.resolved) { + holder.binding.container.setBackgroundResource(R.color.comment_resolved_color); + holder.binding.commentResolved.setVisibility(View.VISIBLE); + } else { + holder.binding.commentResolved.setVisibility(View.INVISIBLE); + holder.binding.container.setBackgroundResource(R.color.window_background_color); + } holder.binding.commentContentContainer.removeAllViews(); - appendTextToFlex(holder.binding.commentContentContainer, model.comment); + addViews(holder, model); } -// //![](https://dev.seafile.com/e-Hek4ng6iRbCw7h-bXsc6jA.png)\n是是是\n -// private void addViews(SDocCommentModel model, SDocCommentViewHolder holder) { -// int index = 0; -// for (RichEditText.RichContentModel commentModel : model.commentList) { + private void addViews(DocsCommentViewHolder holder, DocsCommentModel model) { + int index = 0; + for (RichEditText.RichContentModel commentModel : model.commentList) { // //0 is text, 1 is image -// if (commentModel.type == 0) { -// appendTextToFlex(holder.binding.commentContentContainer, commentModel.content, model.isContainImage); -// } else { -// appendImageToFlex(holder.binding.commentContentContainer, model.commentList, commentModel.content, index); -// index++; -// } -// } -// } + if (commentModel.type == 0) { + appendMovementTextToFlex(holder.binding.commentContentContainer, commentModel.content); + } else { + appendImageToFlex(holder.binding.commentContentContainer, model.commentList, commentModel.content, index); + index++; + } + } + } @NonNull @Override - protected SDocCommentViewHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) { - ItemSdocCommentBinding binding = ItemSdocCommentBinding.inflate(LayoutInflater.from(context), viewGroup, false); - return new SDocCommentViewHolder(binding); + protected DocsCommentViewHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) { + ItemFileCommentBinding binding = ItemFileCommentBinding.inflate(LayoutInflater.from(context), viewGroup, false); + return new DocsCommentViewHolder(binding); } -// public void appendImageToFlex(FlexboxLayout parent, List commentList, String url, int position) { -// LayoutImageBinding fileBinding = LayoutImageBinding.inflate(LayoutInflater.from(getContext())); -// fileBinding.uploadImage.setScaleType(ImageView.ScaleType.CENTER_CROP); -// -// FlexboxLayout.LayoutParams f = new FlexboxLayout.LayoutParams(IMAGE_WIDTH, IMAGE_WIDTH); -// f.setMargins(DP_4, DP_4, DP_4, DP_4); -// fileBinding.getRoot().setLayoutParams(f); -// -// Glide.with(getContext()) -// .load(GlideLoadConfig.getGlideUrl(url)) -// .apply(GlideLoadConfig.getOptions(IMAGE_WIDTH, IMAGE_WIDTH)) -// .into(fileBinding.uploadImage); -// fileBinding.uploadImage.setOnClickListener(new View.OnClickListener() { -// @Override -// public void onClick(View v) { -// List ll = commentList.stream().filter(f -> f.type == 1).map(m -> m.content).collect(Collectors.toList()); -// MediaPlayerModel model = new MediaPlayerModel(); -// model.index = position; -// model.urls = ll; -// MediaPlayerActivity.startThis(getContext(), model); -// } -// }); -// -// -// parent.addView(fileBinding.getRoot()); -// } - - public void appendTextToFlex(FlexboxLayout parent, String comment) { + public void appendImageToFlex(FlexboxLayout parent, List commentList, String url, int position) { + LayoutImageBinding fileBinding = LayoutImageBinding.inflate(LayoutInflater.from(getContext())); + fileBinding.uploadImage.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + OnlyImagePreviewActivity.startThis(getContext(), url); + + } + }); + + GlideApp.with(getContext()) + .load(url) + .apply(GlideLoadConfig.getOptions()) + .into(fileBinding.uploadImage); + + FlexboxLayout.LayoutParams f = new FlexboxLayout.LayoutParams(IMAGE_WIDTH, IMAGE_WIDTH); + f.setMargins(DP_4, DP_4, DP_4, DP_4); + + parent.addView(fileBinding.getRoot(),f); + } + + + public void appendMovementTextToFlex(FlexboxLayout parent, String text) { + MarkdownTextView textView = new MarkdownTextView(getContext()); textView.setPadding(DP_4, DP_8, DP_4, DP_8); -// textView.setText(getProcessor().parse(comment)); - textView.setText(comment); + textView.setText(getProcessor().parse(text)); +// textView.setText(text); textView.setMovementMethod(LinkMovementMethod.getInstance()); textView.setId(android.R.id.text1); textView.setTextSize(14); - textView.setTextColor(ContextCompat.getColor(getContext(), R.color.material_blue_grey_900)); + textView.setTextColor(ContextCompat.getColor(getContext(), R.color.item_title_color)); FlexboxLayout.LayoutParams f = new FlexboxLayout.LayoutParams(-1, -2); parent.addView(textView, f); } - public void submitData(List list) { + public void submitData(List list) { if (CollectionUtils.isEmpty(list)) { submitList(list); return; @@ -167,15 +175,15 @@ public int getNewListSize() { @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - SDocCommentModel newT = getItems().get(oldItemPosition); - SDocCommentModel oldT = list.get(newItemPosition); + DocsCommentModel newT = getItems().get(oldItemPosition); + DocsCommentModel oldT = list.get(newItemPosition); return newT.id == oldT.id; } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - SDocCommentModel newT = getItems().get(oldItemPosition); - SDocCommentModel oldT = list.get(newItemPosition); + DocsCommentModel newT = getItems().get(oldItemPosition); + DocsCommentModel oldT = list.get(newItemPosition); return newT.id == oldT.id && Objects.equals(newT.resolved, oldT.resolved) diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentUserAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentUserAdapter.java similarity index 81% rename from app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentUserAdapter.java rename to app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentUserAdapter.java index f36926541..7fa75c505 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentUserAdapter.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentUserAdapter.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.ui.sdoc.comments; +package com.seafile.seadroid2.ui.docs_comment; import android.content.Context; import android.text.TextUtils; @@ -11,13 +11,15 @@ import com.bumptech.glide.Glide; import com.seafile.seadroid2.R; +import com.seafile.seadroid2.annotation.Todo; import com.seafile.seadroid2.databinding.ItemUserAvatarBinding; import com.seafile.seadroid2.framework.data.model.user.UserModel; import com.seafile.seadroid2.ui.base.adapter.BaseAdapter; -public class SDocCommentUserAdapter extends BaseAdapter { +@Todo +public class DocsCommentUserAdapter extends BaseAdapter { @Override - protected void onBindViewHolder(@NonNull SDocCommentUserViewHolder holder, int i, @Nullable UserModel model) { + protected void onBindViewHolder(@NonNull DocsCommentUserViewHolder holder, int i, @Nullable UserModel model) { if (i == 0) { setMargins(holder.binding.itemUserContainer, 0, 0, 0, 0); @@ -39,9 +41,9 @@ protected void onBindViewHolder(@NonNull SDocCommentUserViewHolder holder, int i @NonNull @Override - protected SDocCommentUserViewHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) { + protected DocsCommentUserViewHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) { ItemUserAvatarBinding binding = ItemUserAvatarBinding.inflate(LayoutInflater.from(context), viewGroup, false); - return new SDocCommentUserViewHolder(binding); + return new DocsCommentUserViewHolder(binding); } public void setMargins(View v, int l, int t, int r, int b) { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentUserViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentUserViewHolder.java similarity index 57% rename from app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentUserViewHolder.java rename to app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentUserViewHolder.java index 2cd81d913..bdf791ba8 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentUserViewHolder.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentUserViewHolder.java @@ -1,15 +1,17 @@ -package com.seafile.seadroid2.ui.sdoc.comments; +package com.seafile.seadroid2.ui.docs_comment; import androidx.annotation.NonNull; +import com.seafile.seadroid2.annotation.Todo; import com.seafile.seadroid2.databinding.ItemUserAvatarBinding; import com.seafile.seadroid2.ui.base.viewholder.BaseViewHolder; -public class SDocCommentUserViewHolder extends BaseViewHolder { +@Todo +public class DocsCommentUserViewHolder extends BaseViewHolder { public ItemUserAvatarBinding binding; - public SDocCommentUserViewHolder(@NonNull ItemUserAvatarBinding binding) { + public DocsCommentUserViewHolder(@NonNull ItemUserAvatarBinding binding) { super(binding.getRoot()); this.binding = binding; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentViewHolder.java new file mode 100644 index 000000000..9e13d8f22 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentViewHolder.java @@ -0,0 +1,17 @@ +package com.seafile.seadroid2.ui.docs_comment; + +import androidx.annotation.NonNull; + +import com.seafile.seadroid2.databinding.ItemFileCommentBinding; +import com.seafile.seadroid2.ui.base.viewholder.BaseViewHolder; + +public class DocsCommentViewHolder extends BaseViewHolder { + + public ItemFileCommentBinding binding; + + public DocsCommentViewHolder(@NonNull ItemFileCommentBinding binding) { + super(binding.getRoot()); + + this.binding = binding; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentViewModel.java new file mode 100644 index 000000000..e4c5b0499 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentViewModel.java @@ -0,0 +1,370 @@ +package com.seafile.seadroid2.ui.docs_comment; + +import android.content.ContentResolver; +import android.net.Uri; +import android.text.TextUtils; + +import androidx.lifecycle.MutableLiveData; + +import com.blankj.utilcode.util.CloneUtils; +import com.blankj.utilcode.util.CollectionUtils; +import com.blankj.utilcode.util.TimeUtils; +import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.framework.data.model.ResultModel; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsCommentWrapperModel; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsUploadResultModel; +import com.seafile.seadroid2.framework.data.model.sdoc.MetadataConfigModel; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsCommentModel; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsCommentsWrapperModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileDetailModel; +import com.seafile.seadroid2.framework.data.model.sdoc.OutlineItemModel; +import com.seafile.seadroid2.framework.data.model.sdoc.SDocPageOptionsModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileProfileConfigModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileRecordWrapperModel; +import com.seafile.seadroid2.framework.data.model.sdoc.SDocOutlineWrapperModel; +import com.seafile.seadroid2.framework.data.model.user.UserWrapperModel; +import com.seafile.seadroid2.framework.http.HttpIO; +import com.seafile.seadroid2.framework.util.ContentResolvers; +import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.framework.util.StringUtils; +import com.seafile.seadroid2.framework.util.Utils; +import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; +import com.seafile.seadroid2.ui.sdoc.DocsCommentService; +import com.seafile.seadroid2.view.rich_edittext.RichEditText; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function3; +import kotlin.Pair; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +public class DocsCommentViewModel extends BaseViewModel { + + private final MutableLiveData _fileCommentLiveData = new MutableLiveData<>(); + private final MutableLiveData _postCommentLiveData = new MutableLiveData<>(); + + public MutableLiveData getPostCommentLiveData() { + return _postCommentLiveData; + } + + public MutableLiveData getSdocCommentLiveData() { + return _fileCommentLiveData; + } + + public void loadDocComments(SDocPageOptionsModel pageOptionsModel) { + if (TextUtils.isEmpty(pageOptionsModel.seadocServerUrl)) { + return; + } + getRefreshLiveData().setValue(true); + + String sdocServerUrl = pageOptionsModel.seadocServerUrl; + if (!sdocServerUrl.endsWith("/")) { + sdocServerUrl = sdocServerUrl + "/"; + } + + Account curAccount = SupportAccountManager.getInstance().getCurrentAccount(); + Account partialAccount = CloneUtils.deepClone(curAccount, Account.class); + partialAccount.setToken(pageOptionsModel.seadocAccessToken); + partialAccount.setServer(sdocServerUrl); + + Single commentSingle = HttpIO.getInstanceByAccount(partialAccount).execute(DocsCommentService.class).getComments(pageOptionsModel.docUuid); + addSingleDisposable(commentSingle, new Consumer() { + @Override + public void accept(DocsCommentsWrapperModel docsCommentsWrapperModel) throws Exception { + docsCommentsWrapperModel.comments = docsCommentsWrapperModel.comments.stream().sorted(new Comparator() { + @Override + public int compare(DocsCommentModel o1, DocsCommentModel o2) { + return o1.created_at.compareTo(o2.created_at); + } + }).map(new Function() { + @Override + public DocsCommentModel apply(DocsCommentModel docsCommentModel) { + + Pair> pair = formatContent(docsCommentModel.comment); + if (pair != null) { + docsCommentModel.commentList = pair.getSecond(); + docsCommentModel.isContainImage = pair.getFirst(); + } + + return docsCommentModel; + } + }).collect(Collectors.toList()); + + getSdocCommentLiveData().setValue(docsCommentsWrapperModel); + getRefreshLiveData().setValue(false); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + SLogs.e(throwable); + getRefreshLiveData().setValue(false); + } + }); + } + + + private Pair> formatContent(String comment) { + if (TextUtils.isEmpty(comment)) { + return null; + } + + List models = CollectionUtils.newArrayList(); + + String[] lines = org.apache.commons.lang3.StringUtils.split(comment, "\n\n"); + for (String line : lines) { + if (TextUtils.isEmpty(line)) { + continue; + } + + int imgLabelCount = StringUtils.countMatches(line, imgPrefix); + int imgMdCount = StringUtils.countMatches(line, imgMdPrefix); + if (imgMdCount == 0 && imgLabelCount == 0) { + RichEditText.RichContentModel m = new RichEditText.RichContentModel(); + m.type = 0; + m.content = line; + models.add(m); + + continue; + } + + if (imgLabelCount > 0) { + models.addAll(getImageLabelResult(line)); + } + + if (imgMdCount > 0) { + models.addAll(getImageMdResult(line)); + } + } + return new Pair<>(true, models); + } + + private final String imgPrefix = " getImageLabelResult(String s) { + List models = CollectionUtils.newArrayList(); + + int start = org.apache.commons.lang3.StringUtils.indexOf(s, imgPrefix); + int end = org.apache.commons.lang3.StringUtils.indexOf(s, imgSuffix); + if (start > 0) { + String startStr = org.apache.commons.lang3.StringUtils.substring(s, 0, start); + RichEditText.RichContentModel m = new RichEditText.RichContentModel(); + m.type = 0; + m.content = startStr; + models.add(m); + } + + String sss = org.apache.commons.lang3.StringUtils.substring(s, start, end + 1); + String a = sss.replaceAll("", " "); + String content = a.trim();// URLEncoder.encode(,"utf-8"); + if (!TextUtils.isEmpty(content) && !TextUtils.equals("null", content.toLowerCase(Locale.getDefault()))) { + RichEditText.RichContentModel m = new RichEditText.RichContentModel(); + m.type = 1; + m.content = content; + models.add(m); + } + + s = s.substring(end + 1); + if (!org.apache.commons.lang3.StringUtils.isEmpty(s)) { + RichEditText.RichContentModel m = new RichEditText.RichContentModel(); + m.type = 0; + m.content = s; + models.add(m); + } + + return models; + } + + private List getImageMdResult(String s) { + List models = CollectionUtils.newArrayList(); + + int start = org.apache.commons.lang3.StringUtils.indexOf(s, imgMdPrefix); + int end = org.apache.commons.lang3.StringUtils.indexOf(s, imgMdSuffix); + if (start > 0) { + //check text before image content + String startStr = org.apache.commons.lang3.StringUtils.substring(s, 0, start); + RichEditText.RichContentModel m = new RichEditText.RichContentModel(); + m.type = 0; + m.content = startStr; + models.add(m); + } + + String sss = org.apache.commons.lang3.StringUtils.substring(s, start + 4, end); + String content = sss.trim();// URLEncoder.encode(,"utf-8"); + if (!TextUtils.isEmpty(content) && !TextUtils.equals("null", content.toLowerCase(Locale.getDefault()))) { + RichEditText.RichContentModel m = new RichEditText.RichContentModel(); + m.type = 1; + m.content = content; + models.add(m); + } + + s = s.substring(end + 1); + if (!org.apache.commons.lang3.StringUtils.isEmpty(s)) { + RichEditText.RichContentModel m = new RichEditText.RichContentModel(); + m.type = 0; + m.content = s; + models.add(m); + } + + return models; + } + + public void markResolve(String sdocServerUrl, String token, String sdocUid, long commentId, Consumer consumer) { + getRefreshLiveData().setValue(true); + + if (!sdocServerUrl.endsWith("/")) { + sdocServerUrl = sdocServerUrl + "/"; + } + + Account curAccount = SupportAccountManager.getInstance().getCurrentAccount(); + Account partialAccount = CloneUtils.deepClone(curAccount, Account.class); + partialAccount.setServer(sdocServerUrl); + partialAccount.setToken(token); + + Map params = new HashMap<>(); + params.put("resolved", true); + + Single resolvedSingle = HttpIO.getInstanceByAccount(partialAccount).execute(DocsCommentService.class).markResolved(sdocUid, commentId, params); + addSingleDisposable(resolvedSingle, new Consumer() { + @Override + public void accept(ResultModel resultModel) throws Exception { + getRefreshLiveData().setValue(false); + if (consumer != null) { + consumer.accept(commentId); + } + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) { + getRefreshLiveData().setValue(false); + } + }); + } + + public void delete(String sdocServerUrl, String token, String sdocUid, long commentId, Consumer consumer) { + getRefreshLiveData().setValue(true); + + if (!sdocServerUrl.endsWith("/")) { + sdocServerUrl = sdocServerUrl + "/"; + } + + Account curAccount = SupportAccountManager.getInstance().getCurrentAccount(); + Account partialAccount = CloneUtils.deepClone(curAccount, Account.class); + partialAccount.setServer(sdocServerUrl); + partialAccount.setToken(token); + + Single resolvedSingle = HttpIO.getInstanceByAccount(partialAccount) + .execute(DocsCommentService.class) + .delete(sdocUid, commentId); + addSingleDisposable(resolvedSingle, new Consumer() { + @Override + public void accept(ResultModel resultModel) throws Exception { + getRefreshLiveData().setValue(false); + if (consumer != null) { + consumer.accept(commentId); + } + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) { + getRefreshLiveData().setValue(false); + } + }); + } + + + public void uploadFile(ContentResolver contentResolver, Uri uri, String docUid, String token, Consumer consumer, Consumer errorCallBack) { + String fileName = ContentResolvers.getFileNameFromUri(contentResolver, uri); + + byte[] fileContent = ContentResolvers.getFileContentFromUri(contentResolver, uri); + RequestBody body = RequestBody.create(MediaType.parse("application/octet-stream"), fileContent); + MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", fileName, body); + + Map partMap = new HashMap<>(); + partMap.put("authorization", RequestBody.create(MediaType.parse("text"), "Token " + token)); + + Account account = SupportAccountManager.getInstance().getCurrentAccount(); + account.token = token; + Flowable uploadFile = HttpIO.getInstanceByAccount(account).execute(DocsCommentService.class).upload(docUid, filePart, partMap); + + addFlowableDisposable(uploadFile, new Consumer() { + @Override + public void accept(DocsUploadResultModel resultModel) throws Exception { + if (consumer != null) { + + String sName = resultModel.relative_path.get(0); + String sUrl = HttpIO.getCurrentInstance().getServerUrl(); + String absUrl = Utils.pathJoin(sUrl, "api", "v2.1", "seadoc", "download-image", docUid, sName); + + consumer.accept(absUrl); + } + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + if (errorCallBack != null) { + errorCallBack.accept(uri.toString()); + } + } + }); + + } + + + public void postComment(SDocPageOptionsModel pageOptionsModel, String comment, String elementId) { + getRefreshLiveData().setValue(true); + String sdocServerUrl = pageOptionsModel.seadocServerUrl; + + if (!sdocServerUrl.endsWith("/")) { + sdocServerUrl = sdocServerUrl + "/"; + } + + Account curAccount = SupportAccountManager.getInstance().getCurrentAccount(); + Account partialAccount = CloneUtils.deepClone(curAccount, Account.class); + partialAccount.setToken(pageOptionsModel.seadocAccessToken); + partialAccount.setServer(sdocServerUrl); + + Map detail = new HashMap<>(); + detail.put("element_id", "0"); + detail.put("comment", comment); + + Map params = new HashMap<>(); + params.put("comment", comment); + params.put("detail", detail); + params.put("author", partialAccount.email); + params.put("updated_at", TimeUtils.getNowString()); + + + Single single = HttpIO.getInstanceByAccount(partialAccount).execute(DocsCommentService.class).postComment(pageOptionsModel.docUuid, params); + addSingleDisposable(single, new Consumer() { + @Override + public void accept(DocsCommentWrapperModel docsCommentWrapperModel) throws Exception { + getRefreshLiveData().setValue(false); + getPostCommentLiveData().setValue(true); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + getRefreshLiveData().setValue(false); + getSeafExceptionLiveData().setValue(getExceptionByThrowable(throwable)); + } + }); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentsActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentsActivity.java new file mode 100644 index 000000000..44bdb6b38 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/docs_comment/DocsCommentsActivity.java @@ -0,0 +1,322 @@ +package com.seafile.seadroid2.ui.docs_comment; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.PopupMenu; +import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.Observer; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.blankj.utilcode.util.CollectionUtils; +import com.blankj.utilcode.util.KeyboardUtils; +import com.blankj.utilcode.util.ToastUtils; +import com.chad.library.adapter4.BaseQuickAdapter; +import com.chad.library.adapter4.QuickAdapterHelper; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.databinding.ActivityDocCommentBinding; +import com.seafile.seadroid2.databinding.ToolbarActionbarBinding; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsCommentModel; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsCommentsWrapperModel; +import com.seafile.seadroid2.framework.data.model.sdoc.SDocPageOptionsModel; +import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.ui.base.BaseMediaSelectorActivity; +import com.seafile.seadroid2.view.rich_edittext.RichEditText; + +import java.util.List; + +import io.reactivex.functions.Consumer; + +public class DocsCommentsActivity extends BaseMediaSelectorActivity { + private ActivityDocCommentBinding binding; + private ToolbarActionbarBinding bindingOfToolbar; + + private DocsCommentAdapter adapter; + private DocsCommentUserAdapter userAdapter; + + private SDocPageOptionsModel pageOptionsModel; + + public static void start(Context context, SDocPageOptionsModel pageModel) { + Intent starter = new Intent(context, DocsCommentsActivity.class); + starter.putExtra("pageOption", pageModel); + context.startActivity(starter); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityDocCommentBinding.inflate(getLayoutInflater()); + bindingOfToolbar = ToolbarActionbarBinding.bind(binding.toolbar.getRoot()); + + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + + setContentView(binding.getRoot()); + + if (getIntent() == null || !getIntent().hasExtra("pageOption")) { + throw new IllegalArgumentException("pageOption is null"); + } + + pageOptionsModel = getIntent().getParcelableExtra("pageOption"); + + initView(); + + initViewModel(); + + initAdapter(); + + refreshData(); + + } + + + private final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); + + private void initView() { + Toolbar toolbar = bindingOfToolbar.toolbarActionbar; + + toolbar.setTitle(""); + setSupportActionBar(toolbar); + toolbar.setTitle(pageOptionsModel.docName); + + toolbar.setNavigationOnClickListener(v -> { + finish(); + }); + + //refresh listener + binding.swipeRefreshLayout.setOnRefreshListener(this::refreshData); + + binding.rv.setLayoutManager(linearLayoutManager); + + binding.photoView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showPickPhotoSheetDialog(false); + } + }); + +// // +// LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false); +// binding.rvUserList.setLayoutManager(linearLayoutManager); + + binding.submit.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + submitData(); + } + }); +// +// binding.richEditText.setOnRichAtListener(new OnRichAtListener() { +// @Override +// public void onCall(EditText editText) { +// showCollaboratorSelector(editText); +// } +// }); + } + + + protected void initViewModel() { + getViewModel().getRefreshLiveData().observe(this, new Observer() { + @Override + public void onChanged(Boolean aBoolean) { + binding.swipeRefreshLayout.setRefreshing(aBoolean); + } + }); + + getViewModel().getPostCommentLiveData().observe(this, new Observer() { + @Override + public void onChanged(Boolean model) { + //remove all + binding.richEditText.removeAllViews(); + + refreshData(); + } + }); + + getViewModel().getSeafExceptionLiveData().observe(this, new Observer() { + @Override + public void onChanged(SeafException e) { + ToastUtils.showLong(e.getMessage()); + } + }); + +// getViewModel().getUserListLiveData().observe(this, new Observer>() { +// @Override +// public void onChanged(List relatedUserModels) { +// userAdapter.submitList(relatedUserModels); +// } +// }); + getViewModel().getSdocCommentLiveData().observe(this, new Observer() { + @Override + public void onChanged(DocsCommentsWrapperModel model) { + adapter.setStateViewEnable(true); + adapter.submitData(model.comments); + + linearLayoutManager.smoothScrollToPosition(binding.rv, null, adapter.getItemCount() - 1); + + } + }); + } + + + private void initAdapter() { +// userAdapter = new DocsCommentUserAdapter(); +// userAdapter.setAnimationEnable(true); +// +// userAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { +// @Override +// public void onClick(@NonNull BaseQuickAdapter baseQuickAdapter, @NonNull View view, int i) { +// if (CollectionUtils.isEmpty(userAdapter.getItems())) { +// return; +// } +// +//// showCollaboratorSelector(); +// } +// }); +// binding.rvUserList.setAdapter(userAdapter); +// +// if (CollectionUtils.isEmpty(strategyModel.participants)) { +// binding.rvUserList.setVisibility(View.GONE); +// } else { +// userAdapter.submitList(strategyModel.participants); +// } + + adapter = new DocsCommentAdapter(); + adapter.setStateViewLayout(this, R.layout.layout_empty); + adapter.setStateViewEnable(false); + adapter.setAnimationEnable(true); + + adapter.addOnItemChildClickListener(R.id.comment_more, new BaseQuickAdapter.OnItemChildClickListener() { + @Override + public void onItemClick(@NonNull BaseQuickAdapter baseQuickAdapter, @NonNull View view, int i) { +// buildMoreDialog(i); + initPopupMenu(i, view); + } + }); + + QuickAdapterHelper helper = new QuickAdapterHelper.Builder(adapter).build(); + binding.rv.setAdapter(helper.getAdapter()); + } + + private void initPopupMenu(int position, View showView) { + DocsCommentModel model = adapter.getItems().get(position); + + PopupMenu popupMenu = new PopupMenu(this, showView); + popupMenu.getMenuInflater().inflate(R.menu.menu_comment_mark_delete, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.mark_resolve) { + getViewModel().markResolve(pageOptionsModel.seadocServerUrl, pageOptionsModel.seadocAccessToken, pageOptionsModel.docUuid, model.id, new Consumer() { + @Override + public void accept(Long aLong) throws Exception { + model.resolved = true; + adapter.set(position, model); + } + }); + } else if (item.getItemId() == R.id.delete) { + showDeleteDialog(position, model.id); + } + return false; + } + }); + popupMenu.show(); + } + + private void showDeleteDialog(int position, int cId) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.delete_confirm); + builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + getViewModel().delete(pageOptionsModel.seadocServerUrl, pageOptionsModel.seadocAccessToken, pageOptionsModel.docUuid, cId, new Consumer() { + @Override + public void accept(Long aLong) throws Exception { + adapter.removeAt(position); + } + }); + } + }); + + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + builder.show(); + } + + private void refreshData() { + getViewModel().loadDocComments(pageOptionsModel); + } + + public void onMediaPicked(Uri uri) { + super.onMediaPicked(uri); + uploadFile(uri); + } + + + private void uploadFile(Uri o) { + if (null == o) { + return; + } + + SLogs.d(o.toString()); + + binding.richEditText.insertImage(o); + + ContentResolver contentResolver = getContentResolver(); + + //upload file + getViewModel().uploadFile(contentResolver, o, pageOptionsModel.docUuid, pageOptionsModel.seadocAccessToken, new Consumer() { + @Override + public void accept(String absUrl) { + binding.richEditText.updateUploadState(o.toString(), absUrl); + } + }, new Consumer() { + @Override + public void accept(String s) throws Exception { + ToastUtils.showLong(R.string.upload_failed); + } + }); + } + + private void submitData() { + // + KeyboardUtils.hideSoftInput(getWindow()); + + List models = binding.richEditText.buildRichEditData(); + if (CollectionUtils.isEmpty(models)) { + return; + } + + StringBuilder sb = new StringBuilder(); + + models.forEach(f -> { + if (f.type == 0) { + sb.append(f.content).append("\n\n"); + } else if (f.type == 1) { + sb.append("![](").append(f.content).append(")"); + } + }); + + if (TextUtils.isEmpty(sb.toString())) { + return; + } + + // 0 is root comment + getViewModel().postComment(pageOptionsModel, sb.toString(), "0"); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/editor/EditorActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/editor/EditorActivity.java index 89bec8bf6..d5a8fb531 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/editor/EditorActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/editor/EditorActivity.java @@ -1,6 +1,5 @@ package com.seafile.seadroid2.ui.editor; -import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -9,7 +8,6 @@ import android.text.TextWatcher; import android.view.Menu; import android.view.MenuItem; -import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.Nullable; @@ -18,7 +16,6 @@ import com.blankj.utilcode.util.EncryptUtils; import com.blankj.utilcode.util.ToastUtils; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.seafile.seadroid2.R; import com.seafile.seadroid2.SeafException; import com.seafile.seadroid2.framework.util.SLogs; @@ -97,9 +94,9 @@ private void initViewModel() { @Override public void onChanged(Boolean aBoolean) { if (aBoolean) { - showProgressDialog(); + showLoadingDialog(); } else { - dismissProgressDialog(); + dismissLoadingDialog(); } } }); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/profile/SDocProfileDialog.java b/app/src/main/java/com/seafile/seadroid2/ui/file_profile/FileProfileDialog.java similarity index 92% rename from app/src/main/java/com/seafile/seadroid2/ui/sdoc/profile/SDocProfileDialog.java rename to app/src/main/java/com/seafile/seadroid2/ui/file_profile/FileProfileDialog.java index a8d9086f7..118c35f94 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/profile/SDocProfileDialog.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/file_profile/FileProfileDialog.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.ui.sdoc.profile; +package com.seafile.seadroid2.ui.file_profile; import static com.seafile.seadroid2.config.Constants.DP.DP_4; @@ -22,8 +22,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; import com.blankj.utilcode.util.CollectionUtils; import com.blankj.utilcode.util.SizeUtils; @@ -33,17 +31,16 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.card.MaterialCardView; import com.google.android.material.imageview.ShapeableImageView; -import com.google.android.material.internal.ViewUtils; import com.seafile.seadroid2.R; import com.seafile.seadroid2.config.ColumnType; import com.seafile.seadroid2.config.GlideLoadConfig; -import com.seafile.seadroid2.databinding.DialogSdocProfileBinding; +import com.seafile.seadroid2.databinding.DialogFileProfileBinding; +import com.seafile.seadroid2.framework.data.model.sdoc.FileDetailModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileRecordWrapperModel; import com.seafile.seadroid2.framework.data.model.sdoc.MetadataConfigDataModel; import com.seafile.seadroid2.framework.data.model.sdoc.MetadataModel; import com.seafile.seadroid2.framework.data.model.sdoc.OptionsTagModel; import com.seafile.seadroid2.framework.data.model.sdoc.RecordResultModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocDetailModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocRecordWrapperModel; import com.seafile.seadroid2.framework.data.model.user.UserModel; import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.framework.util.Utils; @@ -55,15 +52,16 @@ import java.util.List; import java.util.Optional; +public class FileProfileDialog extends BottomSheetDialogFragment { -public class SDocProfileDialog extends BottomSheetDialogFragment { - - private SDocDetailModel docDetailModel; - private SDocRecordWrapperModel recordWrapperModel; + private FileDetailModel docDetailModel; + private FileRecordWrapperModel recordWrapperModel; private ArrayList relatedUsers; + private boolean isShowTitle = false; - public static SDocProfileDialog newInstance(SDocDetailModel docDetailModel, SDocRecordWrapperModel recordWrapperModel, List relatedUsers) { + public static FileProfileDialog newInstance(FileDetailModel docDetailModel, FileRecordWrapperModel recordWrapperModel, List relatedUsers, boolean isShowTitle) { Bundle args = new Bundle(); + args.putBoolean("isShowTitle", isShowTitle); args.putParcelable("detailModel", docDetailModel); if (!CollectionUtils.isEmpty(relatedUsers)) { @@ -74,13 +72,17 @@ public static SDocProfileDialog newInstance(SDocDetailModel docDetailModel, SDoc args.putParcelableArrayList("relatedUsers", new ArrayList<>(relatedUsers)); } - SDocProfileDialog fragment = new SDocProfileDialog(); + FileProfileDialog fragment = new FileProfileDialog(); fragment.setArguments(args); return fragment; } - public static SDocProfileDialog newInstance(SDocDetailModel docDetailModel, List relatedUsers) { - return newInstance(docDetailModel, null, relatedUsers); + public static FileProfileDialog newInstance(FileDetailModel docDetailModel, List relatedUsers) { + return newInstance(docDetailModel, null, relatedUsers, false); + } + + public static FileProfileDialog newInstance(FileDetailModel docDetailModel, List relatedUsers, boolean isShowTitle) { + return newInstance(docDetailModel, null, relatedUsers, isShowTitle); } @Override @@ -91,6 +93,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { throw new IllegalArgumentException("detailModel is null"); } + isShowTitle = getArguments().getBoolean("isShowTitle"); docDetailModel = getArguments().getParcelable("detailModel"); recordWrapperModel = getArguments().getParcelable("recordModel"); relatedUsers = getArguments().getParcelableArrayList("relatedUsers"); @@ -102,12 +105,12 @@ public void onCreate(@Nullable Bundle savedInstanceState) { initFixedValueIfMetadataNotEnable(); } - DialogSdocProfileBinding profileBinding; + private DialogFileProfileBinding profileBinding; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - profileBinding = DialogSdocProfileBinding.inflate(inflater, container, false); + profileBinding = DialogFileProfileBinding.inflate(inflater, container, false); return profileBinding.getRoot(); } @@ -115,33 +118,18 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(requireContext()); -// View bottomSheetInternal = bottomSheetDialog.findViewById(R.id.design_bottom_sheet); -// BottomSheetBehavior.from(bottomSheetInternal).setPeekHeight(800); - -// View bottomSheetContent = bottomSheetInternal.findViewById(R.id.bottom_drawer_2); -// ViewUtils.doOnApplyWindowInsets(bottomSheetContent, new ViewUtils.OnApplyWindowInsetsListener() { -// @Override -// public WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets, ViewUtils.RelativePadding initialPadding) { -// // Add the inset in the inner NestedScrollView instead to make the edge-to-edge behavior -// // consistent - i.e., the extra padding will only show at the bottom of all content, i.e., -// // only when you can no longer scroll down to show more content. -// ViewCompat.setPaddingRelative(bottomSheetContent, -// initialPadding.start, -// initialPadding.top, -// initialPadding.end, -// initialPadding.bottom + insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom); -// return insets; -// } -// }); - - return bottomSheetDialog; + return new BottomSheetDialog(requireContext()); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + if (isShowTitle) { + profileBinding.title.setVisibility(View.VISIBLE); + profileBinding.title.setText(docDetailModel.getName()); + } + setData(profileBinding.detailsContainer); } @@ -149,7 +137,7 @@ private void initFixedValueIfMetadataNotEnable() { if (recordWrapperModel != null) { return; } - recordWrapperModel = new SDocRecordWrapperModel(); + recordWrapperModel = new FileRecordWrapperModel(); RecordResultModel sizeModel = new RecordResultModel(); sizeModel._size = docDetailModel.getSize(); @@ -205,6 +193,7 @@ private void addMetadataView(LinearLayout parent, MetadataModel metadata) { parseViewByType(getContext(), parent, metadata); } + //not support: _tags private final List _fixedField = List.of("_size", "_file_modifier", "_file_mtime", "_description", "_collaborators", "_reviewer", "_status"); private Object getValueByKey(String key) { @@ -250,6 +239,8 @@ private int getResNameByKey(String key) { return R.string._done; case "_outdated": return R.string._outdated; + case "_tags": + return R.string._tags; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -506,10 +497,6 @@ private void parseSingleSelect(LinearLayout view, MetadataModel model) { textView.setText(option.name); } - -// if (!TextUtils.isEmpty(option.borderColor)) { -// } - if (!TextUtils.isEmpty(option.textColor)) { textView.setTextColor(Color.parseColor(option.textColor)); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/folder_backup/FolderBackupConfigActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/folder_backup/FolderBackupConfigActivity.java index 2e7a880ff..5f57d4a22 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/folder_backup/FolderBackupConfigActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/folder_backup/FolderBackupConfigActivity.java @@ -26,7 +26,7 @@ public class FolderBackupConfigActivity extends BaseActivity { public static final String FOLDER_BACKUP_SELECT_TYPE = "folder_backup_select_type"; public static final String BACKUP_SELECT_PATHS = "backup_select_paths"; - private RepoModel repoModel; + private Account mAccount; private boolean isChooseFolderPage; private boolean isChooseRepoPage; @@ -75,7 +75,7 @@ public void onClick(View v) { initFragment(); } - + private void initFragment() { String selectMode = getIntent().getStringExtra(FOLDER_BACKUP_SELECT_TYPE); isChooseFolderPage = "folder".equals(selectMode); @@ -101,7 +101,8 @@ public void saveRepoConfig() { Pair pair = objSelectorFragment.getBackupInfo(); mAccount = pair.first; - repoModel = pair.second; + RepoModel repoModel = pair.second; + Intent intent = new Intent(); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/main/MainActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/main/MainActivity.java index 3cdc20e98..7e3ffc1e3 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/main/MainActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/main/MainActivity.java @@ -16,7 +16,6 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.View; import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResult; @@ -37,14 +36,12 @@ import com.blankj.utilcode.util.ToastUtils; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.navigation.NavigationBarView; -import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.crashlytics.FirebaseCrashlytics; import com.seafile.seadroid2.R; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.context.NavContext; import com.seafile.seadroid2.databinding.ActivityMainBinding; -import com.seafile.seadroid2.enums.ActionModeCallbackType; import com.seafile.seadroid2.enums.FileViewType; import com.seafile.seadroid2.enums.NightMode; import com.seafile.seadroid2.enums.SortBy; @@ -159,11 +156,7 @@ private void initFireBase() { if (account != null) { FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance(); crashlytics.setUserId(account.getSignature()); - - FirebaseAnalytics analytics = FirebaseAnalytics.getInstance(this); - analytics.setUserId(account.getSignature()); } - } private void initOnBackPressedDispatcher() { @@ -292,7 +285,7 @@ public void accept(EncKeyCacheEntity encKeyCacheEntity) throws Exception { } else { getNavContext().switchToPath(repoModel, finalPath); binding.pager.setCurrentItem(0); - getReposFragment().loadData(); + getReposFragment().loadDataAsFirst(); refreshToolbarTitle(); } } @@ -301,7 +294,7 @@ public void accept(EncKeyCacheEntity encKeyCacheEntity) throws Exception { } else { getNavContext().switchToPath(repoModel, finalPath); binding.pager.setCurrentItem(0); - getReposFragment().loadData(); + getReposFragment().loadDataAsFirst(); refreshToolbarTitle(); } } @@ -374,7 +367,7 @@ private void initViewPager() { viewPager2Adapter.addFragments(fragments); binding.pager.setOffscreenPageLimit(fragments.size()); binding.pager.setAdapter(viewPager2Adapter); - binding.pager.setUserInputEnabled(true); + binding.pager.setUserInputEnabled(false); binding.pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { @@ -439,7 +432,7 @@ public void onChanged(Boolean aBoolean) { if (binding.pager.getCurrentItem() == 0) { RepoQuickFragment fragment = (RepoQuickFragment) mainViewModel.getFragments().get(0); fragment.clearExpireRefreshMap(); - fragment.loadData(); + fragment.loadDataAsFirst(); } } }); @@ -473,25 +466,8 @@ public void onChanged(NightMode nightMode) { // } } }); - - mainViewModel.getOnActionModeLiveData().observe(this, new Observer() { - @Override - public void onChanged(ActionModeCallbackType callbackType) { - - onShowRepoActionMode(callbackType); - - } - }); } - private void onShowRepoActionMode(ActionModeCallbackType type) { - if (type == ActionModeCallbackType.CREATE) { - binding.pager.setUserInputEnabled(false); - } else if (type == ActionModeCallbackType.DESTORY) { - binding.pager.setUserInputEnabled(true); - } - - } private void refreshToolbarTitle() { if (!getNavContext().inRepo()) { @@ -510,7 +486,7 @@ private void refreshToolbarTitle() { private void bindService() { Intent syncIntent = new Intent(this, FileSyncService.class); bindService(syncIntent, syncConnection, Context.BIND_AUTO_CREATE); - startService(syncIntent); +// startService(syncIntent); //It doesn't need to run in the background } private final ServiceConnection syncConnection = new ServiceConnection() { @@ -916,7 +892,7 @@ public void onActionStatus(boolean isDone) { if (isDone) { getNavContext().switchToPath(repoModel, path); binding.pager.setCurrentItem(0); - getReposFragment().loadData(); + getReposFragment().loadDataAsFirst(); refreshToolbarTitle(); } } @@ -971,22 +947,24 @@ private void checkCurrentPathHasWritePermission(java.util.function.Consumer() { - @Override - public void accept(PermissionEntity entity) throws Exception { - consumer.accept(entity != null && entity.create); + mainViewModel.getPermissionFromLocal(m.repo_id, m.getCustomPermissionNum(), entity -> { + if (entity == null) { + consumer.accept(false); + return; } + consumer.accept(entity.create); }); } } else if (baseModel instanceof DirentModel m) { if (!m.isCustomPermission()) { consumer.accept(m.hasWritePermission()); } else { - mainViewModel.getPermissionFromLocal(m.repo_id, m.getCustomPermissionNum(), new Consumer() { - @Override - public void accept(PermissionEntity entity) throws Exception { - consumer.accept(entity != null && entity.create); + mainViewModel.getPermissionFromLocal(m.repo_id, m.getCustomPermissionNum(), entity -> { + if (entity == null) { + consumer.accept(false); + return; } + consumer.accept(entity.create); }); } } @@ -1069,15 +1047,38 @@ public void accept(Boolean aBoolean) { private int permission_media_select_type = -1; private void takePhoto() { - permission_media_select_type = 0; - cameraPermissionLauncher.launch(Manifest.permission.CAMERA); + checkCurrentPathHasWritePermission(new java.util.function.Consumer() { + @Override + public void accept(Boolean aBoolean) { + if (!aBoolean) { + ToastUtils.showLong(R.string.library_read_only); + return; + } + + permission_media_select_type = 0; + cameraPermissionLauncher.launch(Manifest.permission.CAMERA); + } + }); + } private void takeVideo() { - permission_media_select_type = 1; - cameraPermissionLauncher.launch(Manifest.permission.CAMERA); + checkCurrentPathHasWritePermission(new java.util.function.Consumer() { + @Override + public void accept(Boolean aBoolean) { + if (!aBoolean) { + ToastUtils.showLong(R.string.library_read_only); + return; + } + + permission_media_select_type = 1; + cameraPermissionLauncher.launch(Manifest.permission.CAMERA); + } + }); + } + private void takeFile(boolean isSingleSelect) { String[] mimeTypes = new String[]{"*/*"}; if (isSingleSelect) { @@ -1205,7 +1206,7 @@ public void onActivityResult(Boolean o) { ///////////////////////////// private void doSelectedMultiFile(List uriList) { - showProgressDialog(); + showLoadingDialog(); try { RepoModel repoModel = getNavContext().getRepoModel(); @@ -1213,15 +1214,15 @@ private void doSelectedMultiFile(List uriList) { mainViewModel.checkLocalDirent(curAccount, this, repoModel, parent_dir, uriList, new Consumer>() { @Override - public void accept(List newUris) throws Exception { + public void accept(List newUris) { - dismissProgressDialog(); + dismissLoadingDialog(); if (!CollectionUtils.isEmpty(newUris)) { ToastUtils.showLong(R.string.added_to_upload_tasks); //start worker - BackgroundJobManagerImpl.getInstance().startFileUploadWorker(); + BackgroundJobManagerImpl.getInstance().startFileManualUploadWorker(); } } }); @@ -1232,14 +1233,14 @@ public void accept(List newUris) throws Exception { } private void doSelectSingleFile(Uri uri) { - showProgressDialog(); + showLoadingDialog(); try { String fileName = Utils.getFilenameFromUri(this, uri); String parent_dir = getNavContext().getNavPath(); String fullPath = Utils.pathJoin(parent_dir, fileName); RepoModel repoModel = getNavContext().getRepoModel(); - mainViewModel.checkRemoteDirent(this, repoModel.repo_id, fullPath, new Consumer() { + mainViewModel.checkRemoteDirent(repoModel.repo_id, fullPath, new Consumer() { @Override public void accept(DirentFileModel direntFileModel) throws Exception { if (direntFileModel != null) { @@ -1248,7 +1249,7 @@ public void accept(DirentFileModel direntFileModel) throws Exception { addUploadTask(repoModel, getNavContext().getNavPath(), uri, false); } - dismissProgressDialog(); + dismissLoadingDialog(); } }); } catch (Exception e) { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/main/MainViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/main/MainViewModel.java index 691070670..4588e5dfd 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/main/MainViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/main/MainViewModel.java @@ -14,7 +14,6 @@ import com.seafile.seadroid2.R; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; -import com.seafile.seadroid2.enums.ActionModeCallbackType; import com.seafile.seadroid2.framework.data.db.entities.EncKeyCacheEntity; import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; import com.seafile.seadroid2.framework.data.db.entities.PermissionEntity; @@ -24,7 +23,7 @@ import com.seafile.seadroid2.enums.TransferResult; import com.seafile.seadroid2.enums.TransferStatus; import com.seafile.seadroid2.framework.data.model.permission.PermissionListWrapperModel; -import com.seafile.seadroid2.framework.data.model.permission.PermissionWrapperModel; +import com.seafile.seadroid2.framework.data.model.permission.PermissionParentModel; import com.seafile.seadroid2.framework.datastore.DataManager; import com.seafile.seadroid2.framework.util.Utils; import com.seafile.seadroid2.framework.worker.ExistingFileStrategy; @@ -41,7 +40,7 @@ import com.seafile.seadroid2.framework.http.HttpIO; import com.seafile.seadroid2.ui.activities.AllActivitiesFragment; import com.seafile.seadroid2.ui.repo.RepoQuickFragment; -import com.seafile.seadroid2.ui.settings.TabSettingsFragment; +import com.seafile.seadroid2.ui.settings.TabSettings2Fragment; import com.seafile.seadroid2.ui.star.StarredQuickFragment; import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; @@ -82,11 +81,6 @@ public class MainViewModel extends BaseViewModel { private final MutableLiveData OnNavChangeListenerLiveData = new MutableLiveData<>(); private final MutableLiveData _searchViewExpandedLiveData = new MutableLiveData<>(false); - private final MutableLiveData _onActionModeLiveData = new MutableLiveData<>(); - - public MutableLiveData getOnActionModeLiveData() { - return _onActionModeLiveData; - } public MutableLiveData getSearchViewExpandedLiveData() { return _searchViewExpandedLiveData; @@ -142,7 +136,7 @@ public NavContext getNavContext() { RepoQuickFragment.newInstance(), StarredQuickFragment.newInstance(), AllActivitiesFragment.newInstance(), - TabSettingsFragment.newInstance() + TabSettings2Fragment.newInstance() ); public List getFragments() { @@ -236,125 +230,20 @@ public void accept(Throwable throwable) throws Exception { } public void getPermissionFromLocal(String repoId, int pNum, Consumer consumer) { - Single> pSingle = AppDatabase.getInstance().permissionDAO().getWithAsync(repoId, pNum); - Single s = pSingle.flatMap(new Function, SingleSource>() { + Single> pSingle = AppDatabase.getInstance().permissionDAO().getByRepoAndIdAsync(repoId, pNum); + addSingleDisposable(pSingle, new Consumer>() { @Override - public SingleSource apply(List pList) throws Exception { - - if (CollectionUtils.isEmpty(pList)) { - return null; - } + public void accept(List permissionEntities) throws Exception { - return Single.just(pList.get(0)); - } - }).flatMap(new Function>() { - @Override - public SingleSource apply(PermissionEntity entity) throws Exception { - Single> r = getLoadRepoPermissionFromRemoteSingle(repoId); - - return r.flatMap(new Function, SingleSource>() { - @Override - public SingleSource apply(List permissionEntities) throws Exception { - if (CollectionUtils.isEmpty(permissionEntities)) { - return null; - - } - Optional p = permissionEntities.stream().filter(f -> f.id == pNum).findFirst(); - if (p.isPresent()) { - return Single.just(p.get()); - } - return null; - } - }); - } - }); - - addSingleDisposable(s, new Consumer() { - @Override - public void accept(PermissionEntity entity) throws Exception { if (consumer != null) { - consumer.accept(entity); - } - } - }); - } - - - private Single> getLoadRepoPermissionFromRemoteSingle(String repoId) { - Single single = HttpIO.getCurrentInstance().execute(RepoService.class).getCustomSharePermissions(repoId); - return single.flatMap(new Function>>() { - @Override - public SingleSource> apply(PermissionListWrapperModel wrapperModel) throws Exception { - - List list = CollectionUtils.newArrayList(); - - for (PermissionWrapperModel model : wrapperModel.permission_list) { - list.add(new PermissionEntity(repoId, model)); - } - - Completable insertCompletable = AppDatabase.getInstance().permissionDAO().insertAllAsync(list); - Single insertAllSingle = insertCompletable.toSingleDefault(0L); - return insertAllSingle.flatMap(new Function>>() { - @Override - public SingleSource> apply(Long aLong) throws Exception { - SLogs.d("The list has been inserted into the local database"); - return Single.just(list); - } - }); - } - }); - } - - - public void getRepoModelFromLocal(String repoId, Consumer> consumer) { - //from db - Single> dbSingle = AppDatabase.getInstance().repoDao().getRepoById(repoId); - Single> r = dbSingle.flatMap(new Function, SingleSource>>() { - @Override - public SingleSource> apply(List repoModels) throws Exception { - if (CollectionUtils.isEmpty(repoModels)) { - return null; - } - - RepoModel repoModel = repoModels.get(0); - if (TextUtils.isEmpty(repoModel.permission)) { - return Single.just(new Pair<>(repoModel, null)); - } - - if (!repoModel.isCustomPermission()) { - return Single.just(new Pair<>(repoModel, null)); - } - - int pNum = repoModel.getCustomPermissionNum(); - - Single> pSingle = AppDatabase.getInstance().permissionDAO().getWithAsync(repoId, pNum); - - return pSingle.flatMap(new Function, SingleSource>>() { - @Override - public SingleSource> apply(List permissionEntities) throws Exception { - if (CollectionUtils.isEmpty(permissionEntities)) { - return Single.just(new Pair<>(repoModel, null)); - } - - return Single.just(new Pair<>(repoModel, permissionEntities.get(0))); + if (CollectionUtils.isEmpty(permissionEntities)) { + consumer.accept(null); + } else { + consumer.accept(permissionEntities.get(0)); } - }); - } - }); - - addSingleDisposable(r, new Consumer>() { - @Override - public void accept(Pair pair) throws Exception { - if (consumer != null) { - consumer.accept(pair); } } - }, new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - SLogs.e(throwable); - } }); } @@ -418,7 +307,7 @@ public void accept(List uris) throws Exception { }); } - public void checkRemoteDirent(Context context, String repoId, String fullPath, Consumer consumer) throws IOException { + public void checkRemoteDirent(String repoId, String fullPath, Consumer consumer) throws IOException { Single detailSingle = HttpIO.getCurrentInstance() .execute(FileService.class) .getFileDetail(repoId, fullPath); @@ -467,22 +356,30 @@ public void subscribe(SingleEmitter emitter) throws Exception { }); } + /** + *
    + *
  1. copy the file to the app's internal cache(/0/Android/media/package_name/Seafile/...).
  2. + *
  3. generate the FileTransferEntity, and insert into DB.
  4. + *
  5. start UploadFileManuallyWorker
  6. + *
  7. it will be deleted when UploadFileManuallyWorker end
  8. + *
      + */ public void addUploadTask(Account account, Context context, RepoModel repoModel, String parentDir, Uri sourceUri, boolean isReplace, Consumer consumer) { ToastUtils.showLong(R.string.upload_waiting); Single single = getCopyFileSingle(account, context, sourceUri, repoModel.repo_id, repoModel.repo_name, isReplace); addSingleDisposable(single, new Consumer() { @Override - public void accept(File file) throws Exception { - addUploadTask(account, repoModel, parentDir, file.getAbsolutePath(), isReplace, consumer); + public void accept(File appLocalCacheFile) throws Exception { + addUploadTask(account, repoModel, parentDir, appLocalCacheFile.getAbsolutePath(), isReplace, consumer); } }); } - private FileTransferEntity getUploadTransferEntity(Account account, RepoModel repoModel, String parentDir, String localFilePath, boolean isReplace) { + private FileTransferEntity getUploadTransferEntity(Account account, RepoModel repoModel, String parentDir, String appLocalCacheFilePath, boolean isReplace) { FileTransferEntity entity = new FileTransferEntity(); - File file = new File(localFilePath); + File file = new File(appLocalCacheFilePath); if (!file.exists()) { return null; } @@ -515,12 +412,12 @@ private FileTransferEntity getUploadTransferEntity(Account account, RepoModel re return entity; } - public void addUploadTask(Account account, RepoModel repoModel, String parentDir, String localFilePath, boolean isReplace, Consumer consumer) { + public void addUploadTask(Account account, RepoModel repoModel, String parentDir, String appLocalCacheFile, boolean isReplace, Consumer consumer) { Single single = Single.create(new SingleOnSubscribe() { @Override public void subscribe(SingleEmitter emitter) throws Exception { - FileTransferEntity entity = getUploadTransferEntity(account, repoModel, parentDir, localFilePath, isReplace); + FileTransferEntity entity = getUploadTransferEntity(account, repoModel, parentDir, appLocalCacheFile, isReplace); AppDatabase.getInstance().fileTransferDAO().insert(entity); @@ -538,7 +435,7 @@ public void accept(FileTransferEntity transferEntity) throws Exception { } //start worker - BackgroundJobManagerImpl.getInstance().startFileUploadWorker(); + BackgroundJobManagerImpl.getInstance().startFileManualUploadWorker(); if (consumer != null) { consumer.accept(transferEntity); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_info/FileDetailViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_info/FileDetailViewModel.java new file mode 100644 index 000000000..de54a2ca1 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_info/FileDetailViewModel.java @@ -0,0 +1,7 @@ +package com.seafile.seadroid2.ui.media.image_info; + +import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; + +public class FileDetailViewModel extends BaseViewModel { + +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselAdapter.java deleted file mode 100644 index 54053a91f..000000000 --- a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselAdapter.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.seafile.seadroid2.ui.media.image_preview; - -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; - -import com.seafile.seadroid2.framework.data.db.entities.DirentModel; - -/** - * An adapter that displays {@link CarouselItem}s for a Carousel. - */ -class CarouselAdapter extends ListAdapter { - - private static final DiffUtil.ItemCallback DIFF_CALLBACK = - new DiffUtil.ItemCallback() { - @Override - public boolean areItemsTheSame( - @NonNull DirentModel oldItem, @NonNull DirentModel newItem) { - // User properties may have changed if reloaded from the DB, but ID is fixed - return oldItem == newItem; - } - - @Override - public boolean areContentsTheSame( - @NonNull DirentModel oldItem, @NonNull DirentModel newItem) { - return false; - } - }; - - private final CarouselItemListener listener; - @LayoutRes - private final int itemLayoutRes; - - CarouselAdapter(CarouselItemListener listener, @LayoutRes int itemLayoutRes) { - super(DIFF_CALLBACK); - this.listener = listener; - this.itemLayoutRes = itemLayoutRes; - } - - @NonNull - @Override - public CarouselItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int pos) { - return new CarouselItemViewHolder( - LayoutInflater.from(viewGroup.getContext()) - .inflate(itemLayoutRes, viewGroup, false), listener); - } - - @Override - public void onBindViewHolder(@NonNull CarouselItemViewHolder carouselItemViewHolder, int pos) { - carouselItemViewHolder.bind(getItem(pos)); - } - -} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItemListener.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItemListener.java deleted file mode 100644 index 8f1870fd9..000000000 --- a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItemListener.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.seafile.seadroid2.ui.media.image_preview; - -/** An interface for items in a carousel. */ -interface CarouselItemListener { - void onItemClicked(CarouselItem item, int position); -} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItemViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItemViewHolder.java deleted file mode 100644 index e2c3bf60b..000000000 --- a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItemViewHolder.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.seafile.seadroid2.ui.media.image_preview; - - -import android.view.View; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.seafile.seadroid2.R; -import com.seafile.seadroid2.framework.data.db.entities.DirentModel; - -/** - * An {@link RecyclerView.ViewHolder} that displays an item inside a Carousel. - */ -class CarouselItemViewHolder extends RecyclerView.ViewHolder { - - private final ImageView imageView; - private final CarouselItemListener listener; - - CarouselItemViewHolder(@NonNull View itemView, CarouselItemListener listener) { - super(itemView); - imageView = itemView.findViewById(R.id.carousel_image_view); - this.listener = listener; - } - - void bind(DirentModel item) { -// Glide.with(imageView.getContext()).load(item.full_path).centerCrop().into(imageView); -// imageView.setContentDescription(imageView.getResources().getString(item.getContentDescRes())); -// itemView.setOnClickListener(v -> listener.onItemClicked(item, getBindingAdapterPosition())); - } -} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/ImagePreviewActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/ImagePreviewActivity.java deleted file mode 100644 index 46b08dad9..000000000 --- a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/ImagePreviewActivity.java +++ /dev/null @@ -1,308 +0,0 @@ -package com.seafile.seadroid2.ui.media.image_preview; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.View; - -import androidx.activity.OnBackPressedCallback; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.Observer; -import androidx.viewpager2.widget.ViewPager2; - -import com.blankj.utilcode.util.BarUtils; -import com.blankj.utilcode.util.CollectionUtils; -import com.blankj.utilcode.util.NetworkUtils; -import com.blankj.utilcode.util.ToastUtils; -import com.seafile.seadroid2.R; -import com.seafile.seadroid2.databinding.ActivityImagePreviewBinding; -import com.seafile.seadroid2.framework.data.db.entities.DirentModel; -import com.seafile.seadroid2.framework.data.db.entities.RepoModel; -import com.seafile.seadroid2.framework.data.db.entities.StarredModel; -import com.seafile.seadroid2.framework.data.model.activities.ActivityModel; -import com.seafile.seadroid2.framework.data.model.search.SearchModel; -import com.seafile.seadroid2.framework.util.Objs; -import com.seafile.seadroid2.ui.adapter.ViewPager2Adapter; -import com.seafile.seadroid2.ui.base.BaseActivityWithVM; -import com.seafile.seadroid2.ui.dialog_fragment.DeleteFileDialogFragment; -import com.seafile.seadroid2.ui.dialog_fragment.listener.OnRefreshDataListener; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import io.reactivex.functions.Consumer; - -public class ImagePreviewActivity extends BaseActivityWithVM { - private ActivityImagePreviewBinding binding; - private ViewPager2Adapter adapter; - private DirentModel currentDirent; - private List direntList; - private boolean isDataOperated = false; - - public static Intent startThisFromRepo(Context context, DirentModel direntModel) { - Intent intent = new Intent(context, ImagePreviewActivity.class); - intent.putExtra("dirent", direntModel); - intent.putExtra("query_db", true); - return intent; - } - - public static Intent startThisFromStarred(Context context, StarredModel starredModel) { - Intent intent = new Intent(context, ImagePreviewActivity.class); - intent.putExtra("dirent", StarredModel.converterThis2DirentModel(starredModel)); - return intent; - } - - public static Intent startThisFromActivity(Context context, ActivityModel starredModel) { - Intent intent = new Intent(context, ImagePreviewActivity.class); - intent.putExtra("dirent", ActivityModel.converterThis2DirentModel(starredModel)); - return intent; - } - - - public static void startThisFromSearch(Context context, SearchModel starredModel) { - Intent intent = new Intent(context, ImagePreviewActivity.class); - intent.putExtra("dirent", SearchModel.converterThis2DirentModel(starredModel)); - context.startActivity(intent); - } - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityImagePreviewBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - BarUtils.setNavBarVisibility(this, false); - BarUtils.setStatusBarVisibility(this, false); - - initData(); - - initView(); - initViewModel(); - - getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - if (isDataOperated) { - setResult(RESULT_OK); - } - finish(); - } - }); - } - - @Override - protected void onPostCreate(@Nullable Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - - loadData(); - } - - private void initData() { - - if (getIntent() == null) { - throw new IllegalArgumentException("Intent is null"); - } - - currentDirent = getIntent().getParcelableExtra("dirent"); - } - - private void initView() { - View.OnClickListener onClickListener = v -> { - int id = v.getId(); - if (id == R.id.gallery_download_photo) { - downloadFile(); - } else if (id == R.id.gallery_delete_photo) { - deleteFile(); - } else if (id == R.id.gallery_star_photo) { - starFile(); - } else if (id == R.id.gallery_share_photo) { - shareFile(); - } - }; - - binding.galleryDownloadPhoto.setOnClickListener(onClickListener); - binding.galleryDeletePhoto.setOnClickListener(onClickListener); - binding.galleryStarPhoto.setOnClickListener(onClickListener); - binding.gallerySharePhoto.setOnClickListener(onClickListener); - } - - private void initViewModel() { - getViewModel().getRefreshLiveData().observe(this, new Observer() { - @Override - public void onChanged(Boolean aBoolean) { - if (aBoolean) { - showProgressDialog(); - } else { - dismissProgressDialog(); - } - } - }); - - getViewModel().getStarLiveData().observe(this, new Observer() { - @Override - public void onChanged(Boolean aBoolean) { - if (aBoolean) { - isDataOperated = true; - } - ToastUtils.showLong(aBoolean ? R.string.star_file_succeed : R.string.star_file_failed); - } - }); - - getViewModel().getListLiveData().observe(this, new Observer>() { - @Override - public void onChanged(List direntModels) { - - if (CollectionUtils.isEmpty(direntModels)) { - direntModels = CollectionUtils.newArrayList(currentDirent); - } - direntList = direntModels; - - notifyFragmentList(); - } - }); - } - - private void loadData() { - getViewModel().getRepoModelFromDB(currentDirent.repo_id, new Consumer() { - @Override - public void accept(RepoModel repoModel) { - if (!repoModel.hasWritePermission()) { - binding.galleryDeletePhoto.setVisibility(View.GONE); - } - - getViewModel().loadData(currentDirent.repo_id, currentDirent.parent_dir); - - } - }); - } - - private void notifyFragmentList() { - if (CollectionUtils.isEmpty(direntList)) { - return; - } - - adapter = new ViewPager2Adapter(this); - List fragments = new ArrayList<>(); - for (DirentModel direntModel : direntList) { - PhotoFragment photoFragment = PhotoFragment.newInstance(direntModel); - photoFragment.setOnPhotoTapListener((view, x, y) -> hideOrShowToolBar()); - fragments.add(photoFragment); - } - - adapter.addFragments(fragments); - - binding.pager.setAdapter(adapter); - binding.pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int position) { - super.onPageSelected(position); - - String fs = String.format(Locale.ROOT, "%d/%d", (position + 1), direntList.size()); - binding.galleryPageIndex.setText(fs); - - DirentModel model = direntList.get(position); - binding.galleryPageName.setText(model.name); - } - }); - - navToSelectedPage(); - } - - private boolean showToolBar = false; - - private void hideOrShowToolBar() { - binding.galleryToolBar.setVisibility(!showToolBar ? View.VISIBLE : View.GONE); - binding.pageIndexContainer.setVisibility(!showToolBar ? View.VISIBLE : View.GONE); - showToolBar = !showToolBar; - } - - /** - * Dynamically navigate to the starting page index selected by user - * by default the starting page index is 0 - */ - private void navToSelectedPage() { - int size = direntList.size(); - int mPageIndex = -1; - for (int i = 0; i < size; i++) { - if (direntList.get(i).name.equals(currentDirent.name)) { - binding.pager.setCurrentItem(i); - binding.galleryPageIndex.setText(String.valueOf(i + 1)); - mPageIndex = i; - break; - } - } - - if (mPageIndex != -1) { - binding.galleryPageIndex.setText(String.format(Locale.ROOT, "%d/%d", (mPageIndex + 1), size)); - } - } - - private DirentModel getSelectedDirent() { - int index = binding.pager.getCurrentItem(); - return direntList.get(index); - } - - private void deleteFile() { - - int position = binding.pager.getCurrentItem(); - - DirentModel direntModel = getSelectedDirent(); - - DeleteFileDialogFragment dialogFragment = DeleteFileDialogFragment.newInstance(CollectionUtils.newArrayList(direntModel.uid)); - dialogFragment.setRefreshListener(new OnRefreshDataListener() { - @Override - public void onActionStatus(boolean isDone) { - if (isDone) { - isDataOperated = true; - - ToastUtils.showLong(R.string.delete_successful); - adapter.removeFragment(position); - adapter.notifyItemRemoved(position); - - if (adapter.getItemCount() == 0) { - setResult(RESULT_OK); - finish(); - } else { - String fs = String.format(Locale.ROOT, "%d/%d", (position + 1), adapter.getFragments().size()); - binding.galleryPageIndex.setText(fs); - } - } - } - }); - dialogFragment.show(getSupportFragmentManager(), DeleteFileDialogFragment.class.getSimpleName()); - } - - private void starFile() { - if (!NetworkUtils.isConnected()) { - ToastUtils.showLong(R.string.network_down); - return; - } - - DirentModel direntModel = getSelectedDirent(); - if (direntModel.starred) { - getViewModel().unStar(direntModel.repo_id, direntModel.full_path); - } else { - getViewModel().star(direntModel.repo_id, direntModel.full_path); - } - } - - private void shareFile() { - DirentModel direntModel = getSelectedDirent(); - Objs.showCreateShareLinkDialog(this, getSupportFragmentManager(), direntModel, false); - } - - - private void downloadFile() { - isDataOperated = true; - - DirentModel direntModel = getSelectedDirent(); - getViewModel().download(direntModel.repo_id, direntModel.full_path); - } - - - -} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/ImagePreviewViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/ImagePreviewViewModel.java index 79db9d4b4..90c9e7ba7 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/ImagePreviewViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/ImagePreviewViewModel.java @@ -1,23 +1,26 @@ package com.seafile.seadroid2.ui.media.image_preview; import android.text.TextUtils; +import android.util.Pair; import androidx.lifecycle.MutableLiveData; import com.blankj.utilcode.util.CollectionUtils; import com.blankj.utilcode.util.ToastUtils; -import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SeafException; import com.seafile.seadroid2.framework.data.db.AppDatabase; import com.seafile.seadroid2.framework.data.db.entities.DirentModel; import com.seafile.seadroid2.framework.data.db.entities.RepoModel; import com.seafile.seadroid2.framework.data.model.ResultModel; import com.seafile.seadroid2.framework.data.model.repo.Dirent2Model; -import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.framework.data.model.sdoc.FileDetailModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileProfileConfigModel; +import com.seafile.seadroid2.framework.data.model.user.UserWrapperModel; import com.seafile.seadroid2.framework.util.Utils; import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; -import com.seafile.seadroid2.ui.repo.RepoService; import com.seafile.seadroid2.framework.http.HttpIO; import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; +import com.seafile.seadroid2.ui.sdoc.DocsCommentService; import com.seafile.seadroid2.ui.star.StarredService; import java.util.HashMap; @@ -26,52 +29,122 @@ import java.util.stream.Collectors; import io.reactivex.Single; +import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; import okhttp3.RequestBody; public class ImagePreviewViewModel extends BaseViewModel { - private MutableLiveData> ListLiveData = new MutableLiveData<>(); - private final MutableLiveData StarLiveData = new MutableLiveData<>(); + private final MutableLiveData> _imageListLiveData = new MutableLiveData<>(); + private final MutableLiveData _starredLiveData = new MutableLiveData<>(); + private final MutableLiveData>> _repoAndListLiveData = new MutableLiveData<>(); - public MutableLiveData getStarLiveData() { - return StarLiveData; + public MutableLiveData>> getRepoAndListLiveData() { + return _repoAndListLiveData; } - public MutableLiveData> getListLiveData() { - return ListLiveData; + public MutableLiveData getStarredLiveData() { + return _starredLiveData; } + public MutableLiveData> getImageListLiveData() { + return _imageListLiveData; + } + + private final MutableLiveData _fileProfileConfigLiveData = new MutableLiveData<>(); + + public MutableLiveData getFileDetailLiveData() { + return _fileProfileConfigLiveData; + } + + public void getFileDetail(String repoId, String path) { + getRefreshLiveData().setValue(true); + + Single userSingle = HttpIO.getCurrentInstance().execute(DocsCommentService.class).getRelatedUsers(repoId); + Single detailSingle = HttpIO.getCurrentInstance().execute(DocsCommentService.class).getFileDetail(repoId, path); + + Single s = Single.zip(detailSingle, userSingle, new BiFunction() { + @Override + public FileProfileConfigModel apply(FileDetailModel docDetailModel, UserWrapperModel userWrapperModel) throws Exception { + FileProfileConfigModel configModel = new FileProfileConfigModel(); + configModel.setDetail(docDetailModel); + configModel.setUsers(userWrapperModel); + return configModel; + } + }); - public void getRepoModelFromDB(String repoId, Consumer consumer) { - //from db - Single> singleDb = AppDatabase.getInstance().repoDao().getRepoById(repoId); - addSingleDisposable(singleDb, new Consumer>() { + addSingleDisposable(s, new Consumer() { @Override - public void accept(List repoModels) throws Exception { - if (consumer != null) { - if (CollectionUtils.isEmpty(repoModels)) { - //no data in sqlite, request RepoApi again - consumer.accept(null); - } else { - consumer.accept(repoModels.get(0)); - } + public void accept(FileProfileConfigModel fileProfileConfigModel) throws Exception { + getFileDetailLiveData().setValue(fileProfileConfigModel); + getRefreshLiveData().setValue(false); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) { + getRefreshLiveData().setValue(false); + } + }); + } + + + public void load(String repoId, String parentPath, String name, boolean isLoadOtherImagesInSameDirectory) { + if (TextUtils.isEmpty(repoId) || TextUtils.isEmpty(parentPath) || TextUtils.isEmpty(name)) { + return; + } + + getRefreshLiveData().setValue(true); + + Single> repoSingle = AppDatabase.getInstance().repoDao().getRepoById(repoId); + + Single> fileSingle; + if (isLoadOtherImagesInSameDirectory) { + fileSingle = AppDatabase.getInstance().direntDao().getFileListByParentPath(repoId, parentPath); + } else { + String fullPath = Utils.pathJoin(parentPath, name); + fileSingle = AppDatabase.getInstance().direntDao().getListByFullPathAsync(repoId, fullPath); + } + + Single>> single = Single.zip(repoSingle, fileSingle, new BiFunction, List, Pair>>() { + @Override + public Pair> apply(List models, List direntModels) throws Exception { + if (CollectionUtils.isEmpty(models)) { + throw SeafException.notFoundException; } + + RepoModel repoModel = models.get(0); + List dirents = direntModels.stream() + .filter(f -> Utils.isViewableImage(f.name)) + .collect(Collectors.toList()); + + return new Pair<>(repoModel, dirents); + } + }); + + addSingleDisposable(single, new Consumer>>() { + @Override + public void accept(Pair> repoModelListPair) throws Exception { + getRefreshLiveData().setValue(false); + getRepoAndListLiveData().setValue(repoModelListPair); } }, new Consumer() { @Override public void accept(Throwable throwable) throws Exception { - SLogs.e(throwable); + getRefreshLiveData().setValue(false); + SeafException seafException = getExceptionByThrowable(throwable); + getSeafExceptionLiveData().setValue(seafException); } }); + + } public void loadData(String repoID, String parentPath) { if (TextUtils.isEmpty(parentPath)) { - getListLiveData().setValue(CollectionUtils.newArrayList()); + getImageListLiveData().setValue(CollectionUtils.newArrayList()); return; } - Single> single = AppDatabase.getInstance().direntDao().getListByParentPath(repoID, parentPath); + Single> single = AppDatabase.getInstance().direntDao().getListByParentPathAsync(repoID, parentPath); addSingleDisposable(single, new Consumer>() { @Override public void accept(List direntModels) throws Exception { @@ -80,14 +153,14 @@ public void accept(List direntModels) throws Exception { .filter(f -> !f.isDir() && Utils.isViewableImage(f.name)) .collect(Collectors.toList()); - getListLiveData().setValue(ds); + getImageListLiveData().setValue(ds); } }); } public void download(String repoID, String fullPath) { - Single> single = AppDatabase.getInstance().direntDao().getListByFullPath(repoID, fullPath); + Single> single = AppDatabase.getInstance().direntDao().getListByFullPathAsync(repoID, fullPath); addSingleDisposable(single, new Consumer>() { @Override public void accept(List direntModels) throws Exception { @@ -110,7 +183,7 @@ public void star(String repoId, String path) { Map requestDataMap = new HashMap<>(); requestDataMap.put("repo_id", repoId); requestDataMap.put("path", path); - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single single = HttpIO.getCurrentInstance().execute(StarredService.class).star(bodyMap); addSingleDisposable(single, new Consumer() { @@ -118,14 +191,13 @@ public void star(String repoId, String path) { public void accept(Dirent2Model resultModel) throws Exception { getRefreshLiveData().setValue(false); - getStarLiveData().setValue(true); - ToastUtils.showLong(R.string.star_file_succeed); + getStarredLiveData().setValue(true); } }, new Consumer() { @Override public void accept(Throwable throwable) throws Exception { getRefreshLiveData().setValue(false); - getStarLiveData().setValue(false); + String errMsg = getErrorMsgByThrowable(throwable); ToastUtils.showLong(errMsg); } @@ -141,13 +213,13 @@ public void unStar(String repoId, String path) { public void accept(ResultModel resultModel) throws Exception { getRefreshLiveData().setValue(false); - getStarLiveData().setValue(true); - ToastUtils.showLong(R.string.unstar); + getStarredLiveData().setValue(false); } }, new Consumer() { @Override public void accept(Throwable throwable) throws Exception { getRefreshLiveData().setValue(false); + String errMsg = getErrorMsgByThrowable(throwable); ToastUtils.showLong(errMsg); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/PhotoFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/PhotoFragment.java index d735d9062..87b2c6415 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/PhotoFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/PhotoFragment.java @@ -2,77 +2,101 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.text.TextUtils; +import android.text.style.ClickableSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.webkit.MimeTypeMap; import android.widget.ImageView; -import android.widget.ProgressBar; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.webkit.CookieManagerCompat; -import androidx.webkit.UserAgentMetadata; +import androidx.lifecycle.Observer; -import com.blankj.utilcode.util.EncodeUtils; -import com.blankj.utilcode.util.SizeUtils; +import com.blankj.utilcode.util.EncryptUtils; +import com.blankj.utilcode.util.FileUtils; +import com.blankj.utilcode.util.SpanUtils; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.signature.ObjectKey; import com.github.chrisbanes.photoview.OnPhotoTapListener; -import com.github.chrisbanes.photoview.PhotoView; import com.seafile.seadroid2.R; -import com.seafile.seadroid2.account.Account; -import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.compat.ContextCompatKt; +import com.seafile.seadroid2.config.OriGlideUrl; import com.seafile.seadroid2.databinding.FragmentPhotoViewBinding; import com.seafile.seadroid2.framework.data.db.entities.DirentModel; -import com.seafile.seadroid2.framework.datastore.DataManager; +import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; import com.seafile.seadroid2.framework.util.GlideApp; +import com.seafile.seadroid2.framework.util.GlideRequest; import com.seafile.seadroid2.framework.util.SLogs; -import com.seafile.seadroid2.ui.base.fragment.BaseFragment; +import com.seafile.seadroid2.framework.util.ThumbnailUtils; +import com.seafile.seadroid2.ui.base.fragment.BaseFragmentWithVM; -import java.io.File; -import java.util.Locale; +import kotlin.Pair; -public class PhotoFragment extends BaseFragment { +public class PhotoFragment extends BaseFragmentWithVM { - private Account account; - private DirentModel direntModel; + private FragmentPhotoViewBinding binding; + + private String repoId, repoName, fullPath; + private String imageUrl; private OnPhotoTapListener onPhotoTapListener; - private FragmentPhotoViewBinding binding; + private String serverUrl; public void setOnPhotoTapListener(OnPhotoTapListener onPhotoTapListener) { this.onPhotoTapListener = onPhotoTapListener; } - public static PhotoFragment newInstance(DirentModel direntModel) { + public static PhotoFragment newInstance(String url) { Bundle args = new Bundle(); - args.putParcelable("dirent", direntModel); + args.putString("image_url", url); PhotoFragment fragment = new PhotoFragment(); fragment.setArguments(args); return fragment; } + public static PhotoFragment newInstance(String serverUrl, String repoId, String repoName, String fullPath) { + Bundle args = new Bundle(); + args.putString("repoId", repoId); + args.putString("repoName", repoName); + args.putString("fullPath", fullPath); + args.putString("serverUrl", serverUrl); + PhotoFragment fragment = new PhotoFragment(); + fragment.setArguments(args); + return fragment; + } + + public static PhotoFragment newInstance(String serverUrl, DirentModel direntModel) { + return newInstance(serverUrl, direntModel.repo_id, direntModel.repo_name, direntModel.full_path); + } + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - account = SupportAccountManager.getInstance().getCurrentAccount(); - Bundle args = getArguments(); if (args == null) { return; } - direntModel = args.getParcelable("dirent"); - if (null == direntModel) { - throw new IllegalArgumentException("DirentModel is null"); - } + repoId = args.getString("repoId"); + repoName = args.getString("repoName"); + fullPath = args.getString("fullPath"); + imageUrl = args.getString("image_url"); + serverUrl = args.getString("serverUrl"); + if (TextUtils.isEmpty(repoId) && TextUtils.isEmpty(imageUrl)) { + throw new IllegalStateException("the args is invalid"); + } } @Nullable @@ -86,11 +110,25 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + TextView descTextView = binding.errorView.findViewById(R.id.desc); + SpanUtils.with(descTextView) + .append(getString(R.string.error_image_load)) + .setForegroundColor(ContextCompatKt.getColorCompat(requireContext(), R.color.black)) + .append(",") + .append(" ") + .append(getString(R.string.retry_with_click)) + .setClickSpan(ContextCompatKt.getColorCompat(requireContext(), R.color.fancy_orange), true, new View.OnClickListener() { + @Override + public void onClick(View v) { + load(); + } + }) + .create(); binding.photoView.setZoomable(true); binding.photoView.setZoomTransitionDuration(300); - binding.photoView.setMaximumScale(5f); - binding.photoView.setMinimumScale(0.8f); + binding.photoView.setMaximumScale(3f); + binding.photoView.setMinimumScale(1f); binding.photoView.setOnPhotoTapListener(new OnPhotoTapListener() { @Override public void onPhotoTap(ImageView view, float x, float y) { @@ -100,41 +138,101 @@ public void onPhotoTap(ImageView view, float x, float y) { } }); - ProgressBar progressBar = view.findViewById(R.id.progress_bar); + intViewModel(); - File file = DataManager.getLocalRepoFile(account, direntModel.repo_id, direntModel.repo_name, direntModel.full_path); - if (file.exists()) { - progressBar.setVisibility(View.GONE); + load(); + } - GlideApp.with(requireContext()) - .load(file) - .into(binding.photoView); - return; + + private void intViewModel() { + getViewModel().getSeafExceptionLiveData().observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(SeafException e) { + binding.progressBar.setVisibility(View.GONE); + binding.errorView.setVisibility(View.VISIBLE); + } + }); + + getViewModel().getCheckLocalLiveData().observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(Pair pair) { + DirentModel direntModel = pair.getFirst(); + + if (direntModel == null) { + binding.photoView.setImageResource(R.drawable.icon_image_error_filled); + binding.progressBar.setVisibility(View.GONE); + return; + } + + FileTransferEntity transferEntity = pair.getSecond(); + if (transferEntity != null && FileUtils.isFileExists(transferEntity.target_path)) { + //no exists local file + if (isGif(fullPath)) { + loadOriGifUrl(transferEntity.target_path); + } else { + loadOriUrl(transferEntity.target_path); + } + } else { +// if (isGif(fullPath)) { +// getViewModel().download(direntModel); +// } else { +// getViewModel().requestOriginalUrl(direntModel); +// } + getViewModel().download(direntModel); + } + } + }); + + getViewModel().getOriginalUrlLiveData().observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String oriUrl) { + loadOriUrl(oriUrl); + } + }); + + getViewModel().getDownloadedPathLiveData().observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String rawPath) { + if (isGif(fullPath)) { + loadOriGifUrl(rawPath); + } else { + loadOriUrl(rawPath); + } + } + }); + } + + private void load() { + if (binding.progressBar.getVisibility() == View.GONE) { + binding.progressBar.setVisibility(View.VISIBLE); } - String url = getUrl(); - if (url == null) { - binding.photoView.setImageResource(R.drawable.icon_image_error_filled); - return; + if (binding.errorView.getVisibility() == View.VISIBLE) { + binding.errorView.setVisibility(View.GONE); + } + + if (!TextUtils.isEmpty(imageUrl)) { + loadUrl(imageUrl); + } else { + loadThumbnailAndRequestRawUrl(); } + } - RequestOptions opt = new RequestOptions() - .skipMemoryCache(true) - .error(R.drawable.icon_image_error_filled) - .diskCacheStrategy(DiskCacheStrategy.NONE); - GlideApp.with(requireContext()) + private void loadUrl(String url) { + GlideApp.with(this) .load(url) - .apply(opt) - .fitCenter() + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .transition(DrawableTransitionOptions.withCrossFade()) .listener(new RequestListener() { @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target target, boolean isFirstResource) { binding.progressBar.setVisibility(View.GONE); + binding.errorView.setVisibility(View.VISIBLE); return false; } @Override - public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { binding.progressBar.setVisibility(View.GONE); return false; } @@ -142,21 +240,98 @@ public boolean onResourceReady(Drawable resource, Object model, Target .into(binding.photoView); } + private void loadThumbnailAndRequestRawUrl() { + getViewModel().checkLocal(repoId, fullPath); + } - private String getUrl() { - Account account = SupportAccountManager.getInstance().getCurrentAccount(); - if (account == null) { - return null; + private void loadOriUrl(String oriUrl) { +// String thumbnailUrl = convertThumbnailUrl(fullPath); +// String thumbKey = EncryptUtils.encryptMD5ToString(thumbnailUrl); +// // load thumbnail first +// GlideRequest thumbnailRequest = GlideApp.with(this) +// .load(thumbnailUrl) +// .diskCacheStrategy(DiskCacheStrategy.ALL) +// .signature(new ObjectKey(thumbKey)) +// .addListener(new RequestListener() { +// @Override +// public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target target, boolean isFirstResource) { +// return false; +// } +// +// @Override +// public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { +//// SLogs.e("缩略图:" + dataSource.name() + ": " + isFirstResource + ": " + thumbKey + ": " + thumbnailUrl); +// return false; +// } +// }); + +// String oriCacheKey = EncryptUtils.encryptMD5ToString(repoId + fullPath); + //new OriGlideUrl() + GlideApp.with(this) + .load(oriUrl) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transition(DrawableTransitionOptions.withCrossFade()) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target target, boolean isFirstResource) { + binding.progressBar.setVisibility(View.GONE); + binding.errorView.setVisibility(View.VISIBLE); + return false; + } + + @Override + public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { + binding.progressBar.setVisibility(View.GONE); + // 图片加载成功 +// SLogs.e("原图:" + dataSource.name() + ": " + isFirstResource + ": " + oriCacheKey + ": " + oriUrl); + return false; + } + }) + .into(binding.photoView); + } + + + private void loadOriGifUrl(String rawUrl) { + GlideApp.with(this) + .asGif() + .load(rawUrl) + .diskCacheStrategy(DiskCacheStrategy.NONE)// + .placeholder(binding.photoView.getDrawable()) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target target, boolean isFirstResource) { + binding.progressBar.setVisibility(View.GONE); + binding.errorView.setVisibility(View.VISIBLE); + return false; + } + + @Override + public boolean onResourceReady(@NonNull GifDrawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { + binding.progressBar.setVisibility(View.GONE); + // 图片加载成功 + SLogs.e(dataSource.name() + ": " + isFirstResource + ": " + rawUrl); + return false; + } + }) + .into(binding.photoView); + } + + private boolean isGif(String fileName) { + if (TextUtils.isEmpty(fileName)) { + return false; } -// //https://dev.seafile.com/seafhttp/repos/4809a6f3-250c-4435-bdd8-b68f34c128d1/files//6f64603fd19f9ec45d05ec379e69e22.gif/?op=download -// //https://dev.seafile.com/seahub/repo/4809a6f3-250c-4435-bdd8-b68f34c128d1/raw/6f64603fd19f9ec45d05ec379e69e22.gif -// if (direntModel.name.toLowerCase().endsWith(".gif")) { -// return String.format(Locale.ROOT, "%srepo/%s/raw/%s", account.getServer(), direntModel.repo_id, direntModel.name); -// } + String f = MimeTypeMap.getFileExtensionFromUrl(fileName); + String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(f); + return mime != null && mime.equalsIgnoreCase("image/gif"); + } -// return String.format("%srepo/%s/raw%s", account.getServer(), repoId, fileFullPath); - int size = SizeUtils.dp2px(300); - return String.format("%sapi2/repos/%s/thumbnail/?p=%s&size=%s", account.getServer(), direntModel.repo_id, EncodeUtils.urlEncode(direntModel.full_path), size); + private String convertThumbnailUrl(String fullPath) { + if (TextUtils.isEmpty(serverUrl)) { + return null; + } + + return ThumbnailUtils.convertThumbnailUrl(serverUrl, repoId, fullPath); } + } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/PhotoViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/PhotoViewModel.java new file mode 100644 index 000000000..a54839cb0 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/PhotoViewModel.java @@ -0,0 +1,252 @@ +package com.seafile.seadroid2.ui.media.image_preview; + +import android.text.TextUtils; + +import androidx.lifecycle.MutableLiveData; + +import com.blankj.utilcode.util.CollectionUtils; +import com.blankj.utilcode.util.FileUtils; +import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.enums.TransferAction; +import com.seafile.seadroid2.enums.TransferResult; +import com.seafile.seadroid2.enums.TransferStatus; +import com.seafile.seadroid2.framework.data.db.AppDatabase; +import com.seafile.seadroid2.framework.data.db.entities.DirentModel; +import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; +import com.seafile.seadroid2.framework.datastore.DataManager; +import com.seafile.seadroid2.framework.http.HttpIO; +import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.listener.FileTransferProgressListener; +import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; +import com.seafile.seadroid2.ui.file.FileService; + +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.file.Path; +import java.util.List; + +import io.reactivex.Single; +import io.reactivex.SingleEmitter; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.SingleSource; +import io.reactivex.functions.BiFunction; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; +import kotlin.Pair; +import okhttp3.Call; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class PhotoViewModel extends BaseViewModel { + private final MutableLiveData _downloadedUrlLiveData = new MutableLiveData<>(); + private final MutableLiveData _originalUrlLiveData = new MutableLiveData<>(); + + private final MutableLiveData> _checkLocalLiveData = new MutableLiveData<>(); + + public MutableLiveData getDownloadedPathLiveData() { + return _downloadedUrlLiveData; + } + + public MutableLiveData getOriginalUrlLiveData() { + return _originalUrlLiveData; + } + + public MutableLiveData> getCheckLocalLiveData() { + return _checkLocalLiveData; + } + + public void checkLocal(String repoId, String fullPath) { + Single> direntSingle = AppDatabase.getInstance().direntDao().getListByFullPathAsync(repoId, fullPath); + Single> transferSingle = AppDatabase.getInstance().fileTransferDAO().getListByFullPathAsync(repoId, TransferAction.DOWNLOAD, fullPath); + Single> rSingle = Single.zip(direntSingle, transferSingle, new BiFunction, List, Pair>() { + @Override + public Pair apply(List direntModels, List fileTransferEntities) throws Exception { + if (CollectionUtils.isEmpty(direntModels)) { + return null; + } + + if (CollectionUtils.isEmpty(fileTransferEntities)) { + return new Pair<>(direntModels.get(0), null); + } + + return new Pair<>(direntModels.get(0), fileTransferEntities.get(0)); + } + }); + + addSingleDisposable(rSingle, new Consumer>() { + @Override + public void accept(Pair pair) throws Exception { + getCheckLocalLiveData().setValue(pair); + } + }); + + } + + private final int SEGMENT_SIZE = 8192; + private final FileTransferProgressListener fileTransferProgressListener = new FileTransferProgressListener(); + + public void requestOriginalUrl(DirentModel direntModel) { + Single downloadUrlSingle = HttpIO.getCurrentInstance() + .execute(FileService.class) + .getFileDownloadLinkAsync(direntModel.repo_id, direntModel.full_path); + + addSingleDisposable(downloadUrlSingle, new Consumer() { + @Override + public void accept(String dlink) throws Exception { + // + dlink = StringUtils.replace(dlink, "\"", ""); + int i = dlink.lastIndexOf('/'); + if (i == -1) { + return; + } + + dlink = dlink.substring(0, i) + "/" + URLEncoder.encode(dlink.substring(i + 1), "UTF-8"); + + getOriginalUrlLiveData().setValue(dlink); + } + }); + } + + public void download(DirentModel direntModel) { + Single downloadUrlSingle = HttpIO.getCurrentInstance() + .execute(FileService.class) + .getFileDownloadLinkAsync(direntModel.repo_id, direntModel.full_path); + + Single downloadedFilePathSingle = downloadUrlSingle.flatMap(new Function>() { + @Override + public SingleSource apply(String dlink) throws Exception { + // + dlink = StringUtils.replace(dlink, "\"", ""); + int i = dlink.lastIndexOf('/'); + if (i == -1) { + return null; + } + + dlink = dlink.substring(0, i) + "/" + URLEncoder.encode(dlink.substring(i + 1), "UTF-8"); + +// _originalUrlLiveData.postValue(dlink); + + return Single.just(dlink); + } + }).flatMap(new Function>() { + @Override + public SingleSource apply(String s) throws Exception { + if (TextUtils.isEmpty(s)) { + //download url is null + throw SeafException.networkException; + } + + FileTransferEntity transferEntity = FileTransferEntity.convertDirentModel2This(false, direntModel); + + return getDownloadSingle(transferEntity, s); + } + }).flatMap(new Function>() { + @Override + public SingleSource apply(FileTransferEntity transferEntity) throws Exception { + return Single.just(transferEntity.target_path); + } + }); + + addSingleDisposable(downloadedFilePathSingle, new Consumer() { + @Override + public void accept(String s) { + getDownloadedPathLiveData().setValue(s); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + SeafException seafException = getExceptionByThrowable(throwable); + getSeafExceptionLiveData().setValue(seafException); + } + }); + } + + private Single getDownloadSingle(FileTransferEntity transferEntity, String dlink) { + return Single.create(new SingleOnSubscribe() { + @Override + public void subscribe(SingleEmitter emitter) throws Exception { + + Account currentAccount = SupportAccountManager.getInstance().getCurrentAccount(); + File localFile = DataManager.getLocalRepoFile(currentAccount, transferEntity); + + transferEntity.target_path = localFile.getAbsolutePath(); + AppDatabase.getInstance().fileTransferDAO().insert(transferEntity); + + Request request = new Request.Builder() + .url(dlink) + .get() + .build(); + + Call newCall = HttpIO.getCurrentInstance().getOkHttpClient().getOkClient().newCall(request); + + try (Response response = newCall.execute()) { + if (!response.isSuccessful()) { + emitter.onError(SeafException.networkException); + return; + } + + ResponseBody responseBody = response.body(); + if (responseBody == null) { + emitter.onError(SeafException.networkException); + return; + } + + long fileSize = responseBody.contentLength(); + if (fileSize == -1) { + SLogs.d("download file error -> contentLength is -1"); + SLogs.d(localFile.getAbsolutePath()); + + fileSize = transferEntity.file_size; + } + + File tempFile = DataManager.createTempFile(); + try (InputStream inputStream = responseBody.byteStream(); + FileOutputStream fileOutputStream = new FileOutputStream(tempFile)) { + + int bytesRead; + byte[] buffer = new byte[SEGMENT_SIZE]; + while ((bytesRead = inputStream.read(buffer, 0, buffer.length)) != -1) { + fileOutputStream.write(buffer, 0, bytesRead); + } + } + + //important + Path path = java.nio.file.Files.move(tempFile.toPath(), localFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + boolean isSuccess = path.toFile().exists(); + if (isSuccess) { + SLogs.e("move file success: " + path); + transferEntity.transferred_size = fileSize; + transferEntity.action_end_at = System.currentTimeMillis(); + transferEntity.file_original_modified_at = transferEntity.action_end_at; + transferEntity.transfer_result = TransferResult.TRANSMITTED; + transferEntity.transfer_status = TransferStatus.SUCCEEDED; + + transferEntity.file_md5 = FileUtils.getFileMD5ToString(transferEntity.target_path).toLowerCase(); + + AppDatabase.getInstance().fileTransferDAO().update(transferEntity); + + emitter.onSuccess(transferEntity); + } else { + SLogs.e("move file failed: " + path); + + transferEntity.transfer_result = TransferResult.FILE_ERROR; + transferEntity.transfer_status = TransferStatus.FAILED; + AppDatabase.getInstance().fileTransferDAO().update(transferEntity); + + emitter.onError(SeafException.transferFileException); + } + } catch (Exception e) { + emitter.onError(e); + } + } + }); + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselAdapter.java new file mode 100644 index 000000000..130d43ccd --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselAdapter.java @@ -0,0 +1,193 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seafile.seadroid2.ui.media.image_preview2; + +import android.content.Context; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import com.blankj.utilcode.util.EncodeUtils; +import com.blankj.utilcode.util.EncryptUtils; +import com.blankj.utilcode.util.ScreenUtils; +import com.blankj.utilcode.util.SizeUtils; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.signature.ObjectKey; +import com.google.common.base.Strings; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.framework.data.db.entities.DirentModel; +import com.seafile.seadroid2.framework.http.HttpIO; +import com.seafile.seadroid2.framework.util.GlideApp; +import com.seafile.seadroid2.framework.util.ThumbnailUtils; + +public class CarouselAdapter extends ListAdapter { + + boolean isLogin = SupportAccountManager.getInstance().isLogin(); + + private static final DiffUtil.ItemCallback DIFF_CALLBACK = + new DiffUtil.ItemCallback<>() { + @Override + public boolean areItemsTheSame( + @NonNull DirentModel oldItem, @NonNull DirentModel newItem) { + // User properties may have changed if reloaded from the DB, but ID is fixed + return oldItem == newItem; + } + + @Override + public boolean areContentsTheSame( + @NonNull DirentModel oldItem, @NonNull DirentModel newItem) { + return false; + } + }; + + private final CarouselItemListener listener; + @LayoutRes + private final int itemLayoutRes; + private int sidePadding; + private int itemWidth; + + public interface CarouselItemListener { + void onItemClicked(DirentModel item, int position); + } + + public CarouselAdapter(Context context, CarouselItemListener listener) { + super(DIFF_CALLBACK); + this.listener = listener; + this.itemLayoutRes = R.layout.item_carousel_item_vertical; + + itemWidth = context.getResources().getDimensionPixelSize(R.dimen.carousel_item_width); + int itemMargin = context.getResources().getDimensionPixelSize(R.dimen.carousel_item_margin); + + int screenWidth = ScreenUtils.getAppScreenWidth(); + sidePadding = (screenWidth - itemWidth) / 2 - itemMargin * 2; + } + + public CarouselAdapter(Context context, CarouselItemListener listener, @LayoutRes int itemLayoutRes) { + super(DIFF_CALLBACK); + this.listener = listener; + this.itemLayoutRes = itemLayoutRes; + + itemWidth = context.getResources().getDimensionPixelSize(R.dimen.carousel_item_width); + int itemMargin = context.getResources().getDimensionPixelSize(R.dimen.carousel_item_margin); + + + int screenWidth = ScreenUtils.getAppScreenWidth(); + sidePadding = (screenWidth - itemWidth) / 2 - itemMargin * 2; + } + + @Override + public DirentModel getItem(int position) { + return super.getItem(position); + } + + @NonNull + @Override + public CarouselItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int pos) { + return new CarouselItemViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(itemLayoutRes, viewGroup, false)); + } + + @Override + public void onBindViewHolder(@NonNull CarouselItemViewHolder carouselItemViewHolder, int pos) { + bind(carouselItemViewHolder, pos); + } + + + private void bind(CarouselItemViewHolder holder, int pos) { + DirentModel model = getItem(pos); +// holder.textView.setText(pos + ""); + + holder.imageView.setOnClickListener(v -> { + listener.onItemClicked(model, pos); + }); + + if (TextUtils.isEmpty(model.name)) { + holder.itemView.getLayoutParams().width = sidePadding; + holder.imageView.setVisibility(View.INVISIBLE); + return; + } + + holder.imageView.setVisibility(View.VISIBLE); + holder.itemView.getLayoutParams().width = itemWidth; + + + String thumbnailUrl = convertThumbnailUrl(model.repo_id, model.full_path); + if (TextUtils.isEmpty(thumbnailUrl)) { + holder.imageView.setImageResource(R.drawable.shape_solid_grey100_radius_4); + return; + } + String thumbKey = EncryptUtils.encryptMD5ToString(thumbnailUrl); + GlideApp.with(holder.imageView) + .load(thumbnailUrl) + .signature(new ObjectKey(thumbKey)) + .apply(opt) + .into(holder.imageView); + } + + private final RequestOptions opt = new RequestOptions() + .placeholder(R.drawable.shape_solid_grey100_radius_4) + .fallback(R.drawable.shape_solid_grey100_radius_4) + .error(R.drawable.icon_format_pic) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC); + + private String convertThumbnailUrl(String repoId, String fullPath) { + String serverUrl = getServerUrl(); + if (TextUtils.isEmpty(serverUrl)) { + return null; + } + + return ThumbnailUtils.convertThumbnailUrl(serverUrl, repoId, fullPath); + } + + private String serverUrl; + + private String getServerUrl() { + if (!isLogin) { + return null; + } + + if (TextUtils.isEmpty(serverUrl)) { + serverUrl = HttpIO.getCurrentInstance().getServerUrl(); + } + + return serverUrl; + } + + public static class CarouselItemViewHolder extends RecyclerView.ViewHolder { + + public final ImageView imageView; + public final TextView textView; + + CarouselItemViewHolder(@NonNull View itemView) { + super(itemView); + imageView = itemView.findViewById(R.id.image_view); + textView = itemView.findViewById(R.id.text_view); + } + + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselImagePreviewActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselImagePreviewActivity.java new file mode 100644 index 000000000..892ef3839 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselImagePreviewActivity.java @@ -0,0 +1,645 @@ +package com.seafile.seadroid2.ui.media.image_preview2; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Pair; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.activity.OnBackPressedCallback; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; + +import com.blankj.utilcode.util.BarUtils; +import com.blankj.utilcode.util.CollectionUtils; +import com.blankj.utilcode.util.NetworkUtils; +import com.blankj.utilcode.util.ScreenUtils; +import com.blankj.utilcode.util.ToastUtils; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.compat.ContextCompatKt; +import com.seafile.seadroid2.context.CopyMoveContext; +import com.seafile.seadroid2.databinding.ActivityCarouselImagePreviewBinding; +import com.seafile.seadroid2.enums.OpType; +import com.seafile.seadroid2.framework.data.db.entities.DirentModel; +import com.seafile.seadroid2.framework.data.db.entities.RepoModel; +import com.seafile.seadroid2.framework.data.db.entities.StarredModel; +import com.seafile.seadroid2.framework.data.model.activities.ActivityModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileProfileConfigModel; +import com.seafile.seadroid2.framework.data.model.search.SearchModel; +import com.seafile.seadroid2.framework.http.HttpIO; +import com.seafile.seadroid2.framework.util.Objs; +import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.framework.util.Utils; +import com.seafile.seadroid2.ui.adapter.ViewPager2Adapter; +import com.seafile.seadroid2.ui.base.BaseActivityWithVM; +import com.seafile.seadroid2.ui.dialog_fragment.CopyMoveDialogFragment; +import com.seafile.seadroid2.ui.dialog_fragment.DeleteFileDialogFragment; +import com.seafile.seadroid2.ui.dialog_fragment.listener.OnRefreshDataListener; +import com.seafile.seadroid2.ui.file_profile.FileProfileDialog; +import com.seafile.seadroid2.ui.media.image_preview.ImagePreviewViewModel; +import com.seafile.seadroid2.ui.media.image_preview.PhotoFragment; +import com.seafile.seadroid2.ui.selector.ObjSelectorActivity; +import com.seafile.seadroid2.view.snap_recyclerview.GravitySnapHelper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class CarouselImagePreviewActivity extends BaseActivityWithVM implements Toolbar.OnMenuItemClickListener { + private ActivityCarouselImagePreviewBinding binding; + + private ViewPager2Adapter adapter; + private CarouselAdapter carouselAdapter; + + private List direntList; + private List carouselDirentList; + private boolean isHide = false; + private boolean isNightMode = false; + + private String repoId, repoName, parentDir, name; + private boolean load_other_images_in_same_directory = false; + private int carouselItemWidth = 0; + private int carouselItemMargin = 0; + +// public static Intent startThisFromDocsComment(Context context, String url) { +// Intent intent = new Intent(context, CarouselImagePreviewActivity.class); +// intent.putExtra("image_url", url);//Load other images in the same folder +// intent.putExtra("load_other_images_in_same_directory", false);//Load other images in the same folder +// return intent; +// } + + public static Intent startThisFromObjs(Context context, DirentModel direntModel) { + Intent intent = new Intent(context, CarouselImagePreviewActivity.class); + intent.putExtra("repo_id", direntModel.repo_id); + intent.putExtra("repo_name", direntModel.repo_name); + intent.putExtra("parent_dir", direntModel.parent_dir); + intent.putExtra("name", direntModel.name); + intent.putExtra("load_other_images_in_same_directory", true);//Load other images in the same folder + return intent; + } + + public static Intent startThisFromStarred(Context context, StarredModel model) { + Intent intent = new Intent(context, CarouselImagePreviewActivity.class); + intent.putExtra("repo_id", model.repo_id); + intent.putExtra("repo_name", model.repo_name); + intent.putExtra("parent_dir", Utils.getParentPath(model.path)); + intent.putExtra("name", model.obj_name); + intent.putExtra("load_other_images_in_same_directory", false);//Load other images in the same folder + return intent; + } + + public static Intent startThisFromActivity(Context context, ActivityModel model) { + Intent intent = new Intent(context, CarouselImagePreviewActivity.class); + intent.putExtra("repo_id", model.repo_id); + intent.putExtra("repo_name", model.repo_name); + intent.putExtra("parent_dir", Utils.getParentPath(model.path)); + intent.putExtra("name", model.name); + intent.putExtra("load_other_images_in_same_directory", false);//Load other images in the same folder + return intent; + } + + public static Intent startThisFromSearch(Context context, SearchModel model) { + Intent intent = new Intent(context, CarouselImagePreviewActivity.class); + intent.putExtra("repo_id", model.repo_id); + intent.putExtra("repo_name", model.repo_name); + intent.putExtra("parent_dir", Utils.getParentPath(model.fullpath)); + intent.putExtra("name", model.name); + intent.putExtra("load_other_images_in_same_directory", false);//Load other images in the same folder + return intent; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityCarouselImagePreviewBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + // full screen + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + + int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + isNightMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES; + + //init status bar + int color = ContextCompatKt.getColorCompat(this, R.color.bar_background_color); + BarUtils.setStatusBarColor(this, color); + BarUtils.setStatusBarLightMode(this, !isNightMode); + + //init toolbar margin top + ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) binding.toolbarActionbar.getLayoutParams(); + layoutParams.topMargin = BarUtils.getStatusBarHeight(); + + + carouselItemWidth = getResources().getDimensionPixelSize(R.dimen.carousel_item_width); + carouselItemMargin = getResources().getDimensionPixelSize(R.dimen.carousel_item_margin); + + Toolbar toolbar = getActionBarToolbar(); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(null); + + toolbar.setNavigationOnClickListener(v -> { + setResult(RESULT_OK); + + finish(); + }); + } + + getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + setResult(RESULT_OK); + + finish(); + } + }); + + initParams(); + initView(); + + + initPager(); + initCarouselRecyclerView(); + +// bindPager(); + + initViewModel(); + + getViewModel().load(repoId, parentDir, name, load_other_images_in_same_directory); + } + + private void initParams() { + Intent intent = getIntent(); + if (intent == null) { + throw new RuntimeException("intent is null"); + } + + repoId = intent.getStringExtra("repo_id"); + if (TextUtils.isEmpty(repoId)) { + throw new RuntimeException("repoId is empty"); + } + + + repoName = intent.getStringExtra("repo_name"); + parentDir = intent.getStringExtra("parent_dir"); + name = intent.getStringExtra("name"); + + load_other_images_in_same_directory = intent.getBooleanExtra("load_other_images_in_same_directory", false); + } + + private void initView() { + View.OnClickListener onClickListener = v -> { + int id = v.getId(); + if (id == R.id.gallery_delete_photo) { + deleteFile(); + } else if (id == R.id.gallery_star_photo) { + starFile(); + } else if (id == R.id.gallery_share_photo) { + shareFile(); + } + }; + + binding.galleryDeletePhoto.setOnClickListener(onClickListener); + binding.galleryStarPhoto.setOnClickListener(onClickListener); + binding.gallerySharePhoto.setOnClickListener(onClickListener); + } + + private void initViewModel() { + getViewModel().getRefreshLiveData().observe(this, new Observer() { + @Override + public void onChanged(Boolean aBoolean) { + if (aBoolean) { + showLoadingDialog(); + } else { + dismissLoadingDialog(); + } + } + }); + + getViewModel().getRepoAndListLiveData().observe(this, new Observer>>() { + @Override + public void onChanged(Pair> pair) { + submitData(pair); + } + }); + + getViewModel().getStarredLiveData().observe(this, new Observer() { + @Override + public void onChanged(Boolean aBoolean) { +// ToastUtils.showLong(aBoolean ? R.string.star_file_succeed : R.string.star_file_failed); + + int index = binding.pager.getCurrentItem(); + direntList.get(index).starred = aBoolean; + + notifyCurrentStarredStatus(); + } + }); + + getViewModel().getFileDetailLiveData().observe(this, new Observer() { + @Override + public void onChanged(FileProfileConfigModel configModel) { + + DirentModel direntModel = getSelectedDirent(); + if (direntModel == null) { + return; + } + + String key = direntModel.full_path; + fileDetailHashMap.put(key, configModel); + + showProfileDialog(configModel); + } + }); + } + + private void initPager() { + adapter = new ViewPager2Adapter(this); + binding.pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + notifyCurrentStarredStatus(); + } + }); + binding.pager.setOffscreenPageLimit(7); + binding.pager.setAdapter(adapter); + + // + if (isNightMode) { + int color = ContextCompatKt.getColorCompat(this, R.color.bar_background_color); + binding.pager.setBackgroundColor(color); + } + } + + private final LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false); + private final GravitySnapHelper snapHelper = new GravitySnapHelper(Gravity.CENTER); + + private void initCarouselRecyclerView() { + carouselAdapter = new CarouselAdapter(this, new CarouselAdapter.CarouselItemListener() { + @Override + public void onItemClicked(DirentModel item, int snapPosition) { + if (snapHelper.getCurrentSnappedPosition() == snapPosition) { + ToastUtils.showLong("same"); + return; + } + + snapHelper.smoothScrollToPosition(snapPosition); + } + }); + + binding.recyclerView.setAdapter(carouselAdapter); + binding.recyclerView.setLayoutManager(layoutManager); + + binding.recyclerView.addOnScrollListener(new CenterScaleXYRecyclerViewScrollListener(this)); + + int screenWidth = ScreenUtils.getAppScreenWidth();//1080/3=360 + int sidePadding = (screenWidth - carouselItemWidth) / 2 - carouselItemMargin * 2;//170-4=166 + binding.recyclerView.addItemDecoration(new LinearEdgeDecoration(sidePadding, sidePadding, RecyclerView.HORIZONTAL, false)); + + snapHelper.attachToRecyclerView(binding.recyclerView); + } + + private void bindPager() { + bindPager(binding.pager, snapHelper); + } + + private int whoScroll = -1; + + public void bindPager(ViewPager2 pager, GravitySnapHelper snapHelper) { + pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + if (whoScroll == 1) { + whoScroll = -1; + return; + } + + whoScroll = 0; + SLogs.e("currentPagerPosition: " + position); + snapHelper.smoothScrollToPosition(position); + } + }); + + snapHelper.setSnapListener(new GravitySnapHelper.SnapListener() { + @Override + public void onSnap(int snapPosition) { + + if (whoScroll == 0) { + whoScroll = -1; + return; + } + + whoScroll = 1; + SLogs.e("currentSnapPosition: " + snapPosition); + pager.setCurrentItem(snapPosition, false); + } + }); + } + + private void submitData(Pair> pair) { + if (pair == null) { + return; + } + + RepoModel repoModel = pair.first; + if (repoModel == null) { + return; + } + + if (!repoModel.hasWritePermission()) { + binding.galleryDeletePhoto.setVisibility(View.GONE); + } + + direntList = pair.second; + // + List fragments = new ArrayList<>(); + for (DirentModel direntModel : direntList) { + PhotoFragment photoFragment = PhotoFragment.newInstance(getServerUrl(), direntModel); + photoFragment.setOnPhotoTapListener((view, x, y) -> hideOrShowToolBar()); + fragments.add(photoFragment); + } + + adapter.addFragments(fragments); + adapter.notifyItemRangeInserted(0, direntList.size()); + + carouselDirentList = new ArrayList<>(); +// carouselDirentList.add(new DirentModel()); + carouselDirentList.addAll(direntList); +// carouselDirentList.add(new DirentModel()); + + carouselAdapter.submitList(carouselDirentList); + + binding.recyclerView.postDelayed(new Runnable() { + @Override + public void run() { + navToSelectedPage(); + } + }, 50); + } + + private String server_url; + private final boolean isLogin = SupportAccountManager.getInstance().isLogin(); + + private String getServerUrl() { + if (!TextUtils.isEmpty(server_url)) { + return server_url; + } + + if (!isLogin) { + return null; + } + + server_url = HttpIO.getCurrentInstance().getServerUrl(); + return server_url; + } + + private void hideOrShowToolBar() { + binding.galleryToolBar.setVisibility(isHide ? View.VISIBLE : View.GONE); + binding.recyclerView.setVisibility(isHide ? View.VISIBLE : View.GONE); + binding.toolbarActionbar.setVisibility(isHide ? View.VISIBLE : View.INVISIBLE); + + if (isNightMode) { + //The background color has been set in the #initPager(), and no longer updated in night mode + } else { + int color = ContextCompatKt.getColorCompat(this, isHide ? R.color.material_grey_100 : R.color.material_grey_911); + binding.pager.setBackgroundColor(color); + + BarUtils.setNavBarColor(this, color); + BarUtils.setStatusBarColor(this, color); + + BarUtils.setStatusBarLightMode(this, isHide); + BarUtils.setNavBarLightMode(this, isHide); + } + + isHide = !isHide; + } + + /** + * Dynamically navigate to the starting page index selected by user + * by default the starting page index is 0 + */ + private void navToSelectedPage() { + int size = direntList.size(); + int index = -1; + + for (int i = 0; i < size; i++) { + if (direntList.get(i).name.equals(name)) { + index = i; + break; + } + } + + if (index != -1) { + int x = (carouselItemWidth + carouselItemMargin * 2) * index; + binding.recyclerView.scrollBy(x, 0); + binding.pager.setCurrentItem(index, false); + } + + bindPager(); + } + + private void notifyCurrentStarredStatus() { + DirentModel direntModel = getSelectedDirent(); + if (direntModel == null) { + return; + } + + if (direntModel.starred) { + binding.galleryStarPhoto.setImageResource(R.drawable.baseline_starred_filled_24); + } else { + binding.galleryStarPhoto.setImageResource(R.drawable.baseline_starred_outline_24); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + Toolbar toolbar = getActionBarToolbar(); + toolbar.inflateMenu(R.menu.menu_image_list_preview); + toolbar.setOnMenuItemClickListener(this); + + return true; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } else if (item.getItemId() == R.id.copy) { + copy(); + } else if (item.getItemId() == R.id.info) { + preShowProfileDialog(); + } + + return super.onOptionsItemSelected(item); + } + + private DirentModel getSelectedDirent() { + if (CollectionUtils.isEmpty(direntList)) { + return null; + } + + int index = binding.pager.getCurrentItem(); + if (index > direntList.size() - 1) { + return null; + } + + return direntList.get(index); + } + + private void deleteFile() { + + int position = binding.pager.getCurrentItem(); + DirentModel direntModel = getSelectedDirent(); + + DeleteFileDialogFragment dialogFragment = DeleteFileDialogFragment.newInstance(CollectionUtils.newArrayList(direntModel.uid)); + dialogFragment.setRefreshListener(new OnRefreshDataListener() { + @Override + public void onActionStatus(boolean isDone) { + if (isDone) { + direntList.remove(position); + adapter.removeFragment(position); + adapter.notifyItemRemoved(position); + + int carouselIndex = position + 1; + carouselDirentList.remove(carouselIndex); + carouselAdapter.notifyItemRemoved(carouselIndex); + + ToastUtils.showLong(R.string.delete_successful); + + if (adapter.getItemCount() == 0) { + setResult(RESULT_OK); + finish(); + } + } + } + }); + dialogFragment.show(getSupportFragmentManager(), DeleteFileDialogFragment.class.getSimpleName()); + } + + private void starFile() { + if (!NetworkUtils.isConnected()) { + ToastUtils.showLong(R.string.network_down); + return; + } + + DirentModel direntModel = getSelectedDirent(); + if (direntModel == null) { + return; + } + + if (direntModel.starred) { + getViewModel().unStar(direntModel.repo_id, direntModel.full_path); + } else { + getViewModel().star(direntModel.repo_id, direntModel.full_path); + } + } + + private void shareFile() { + DirentModel direntModel = getSelectedDirent(); + Objs.showCreateShareLinkDialog(this, getSupportFragmentManager(), direntModel, false); + } + + + private HashMap fileDetailHashMap = new HashMap<>(); + + private void preShowProfileDialog() { + DirentModel direntModel = getSelectedDirent(); + if (direntModel == null) { + return; + } + + String key = direntModel.full_path; + if (fileDetailHashMap.containsKey(key)) { + showProfileDialog(fileDetailHashMap.get(key)); + } else { + getViewModel().getFileDetail(repoId, key); + } + } + + private void showProfileDialog(FileProfileConfigModel model) { + FileProfileDialog detailDialog = FileProfileDialog.newInstance(model.detail, model.users.user_list, true); + detailDialog.show(getSupportFragmentManager(), FileProfileDialog.class.getSimpleName()); + } + + private CopyMoveContext copyMoveContext = null; + + private void copy() { + DirentModel direntModel = getSelectedDirent(); + if (direntModel == null) { + return; + } + + chooseCopyMoveDest(direntModel, OpType.COPY); + } + + /** + * Choose copy/move destination for multiple files + */ + private void chooseCopyMoveDest(DirentModel direntModel, OpType op) { + + copyMoveContext = new CopyMoveContext(repoId, repoName, parentDir, CollectionUtils.newArrayList(direntModel), op); + copyMoveLauncher.launch(ObjSelectorActivity.getStartIntent(this)); + } + + private final ActivityResultLauncher copyMoveLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult o) { + if (o.getResultCode() != Activity.RESULT_OK || o.getData() == null) { + return; + } + + String dstRepoId = o.getData().getStringExtra(ObjSelectorActivity.DATA_REPO_ID); + String dstDir = o.getData().getStringExtra(ObjSelectorActivity.DATA_DIR); + String disRepoName = o.getData().getStringExtra(ObjSelectorActivity.DATA_REPO_NAME); + + copyMoveContext.setDest(dstRepoId, dstDir, disRepoName); + + doCopyMove(); + } + }); + + private void doCopyMove() { + if (copyMoveContext == null) { + return; + } + + if (!copyMoveContext.checkCopyMoveToSubfolder()) { + ToastUtils.showLong(copyMoveContext.isCopy() + ? R.string.cannot_copy_folder_to_subfolder + : R.string.cannot_move_folder_to_subfolder); + return; + } + + CopyMoveDialogFragment dialogFragment = CopyMoveDialogFragment.newInstance(); + dialogFragment.initData(copyMoveContext); + dialogFragment.setRefreshListener(new OnRefreshDataListener() { + @Override + public void onActionStatus(boolean isDone) { + if (isDone) { + ToastUtils.showLong(copyMoveContext.isCopy() ? R.string.copied_successfully : R.string.moved_successfully); + } + } + }); + dialogFragment.show(getSupportFragmentManager(), CopyMoveDialogFragment.class.getSimpleName()); + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItem.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselItem.java similarity index 93% rename from app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItem.java rename to app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselItem.java index 821ed483e..202ee4838 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview/CarouselItem.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CarouselItem.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.seafile.seadroid2.ui.media.image_preview; +package com.seafile.seadroid2.ui.media.image_preview2; import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; @@ -22,7 +22,7 @@ /** * A data class that holds all information related to an item inside a Carousel. */ -class CarouselItem { +public class CarouselItem { @DrawableRes private final int drawableRes; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CenterScaleXYRecyclerViewScrollListener.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CenterScaleXYRecyclerViewScrollListener.java new file mode 100644 index 000000000..9e0dd3d71 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/CenterScaleXYRecyclerViewScrollListener.java @@ -0,0 +1,121 @@ +package com.seafile.seadroid2.ui.media.image_preview2; + +import android.content.Context; +import android.os.Handler; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.blankj.utilcode.util.ScreenUtils; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.framework.util.SLogs; + +public class CenterScaleXYRecyclerViewScrollListener extends RecyclerView.OnScrollListener { + + private float maxScale = 1.2f; + private float minScale = 1.0f; + + private final int centerX; + private final float maxDistance;// 12dp is (view width / 2) + margin(2dp) + private final int itemWidth, itemMargin; + + private LinearLayoutManager layoutManager; + + private final Handler handler = new Handler(); + private boolean isPendingUpdate = false; + + public CenterScaleXYRecyclerViewScrollListener(Context context) { + itemWidth = context.getResources().getDimensionPixelSize(R.dimen.carousel_item_width); + itemMargin = context.getResources().getDimensionPixelSize(R.dimen.carousel_item_margin); + maxDistance = (float) itemWidth / 2 + itemMargin; + centerX = ScreenUtils.getAppScreenWidth() / 2; + } + + public CenterScaleXYRecyclerViewScrollListener(Context context, float maxScale, float minScale) { + this.maxScale = maxScale; + this.minScale = minScale; + + itemWidth = context.getResources().getDimensionPixelSize(R.dimen.carousel_item_width); + itemMargin = context.getResources().getDimensionPixelSize(R.dimen.carousel_item_margin); + maxDistance = (float) itemWidth / 2 + itemMargin; + centerX = ScreenUtils.getAppScreenWidth() / 2; + + } + + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + adjustChildScale(recyclerView, 0); // 滚动停止时更新缩放状态 + } + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); +// if (isPendingUpdate) return; +// isPendingUpdate = true; + + adjustChildScale(recyclerView, dx); + +// handler.postDelayed(() -> { +// adjustChildScale(recyclerView, dx); +// +// isPendingUpdate = false; +// }, 25); + } + + private void adjustChildScale(RecyclerView recyclerView, int dx) { + if (layoutManager == null) { + layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + } + + if (layoutManager == null) { + throw new IllegalStateException("LayoutManager is null"); + } + + int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition(); + int lastVisiblePosition = layoutManager.findLastVisibleItemPosition(); + if (firstVisiblePosition == RecyclerView.NO_POSITION || lastVisiblePosition == RecyclerView.NO_POSITION) { + return; // 防止意外状态 + } + + for (int i = firstVisiblePosition; i <= lastVisiblePosition; i++) { + View view = layoutManager.findViewByPosition(i); + if (view == null) { + continue; + } + + int left = view.getLeft(); + if (left == 0) { + continue; + } + + // 计算每个 item 的中心点与屏幕中心的距离 + int viewCenterX = left + itemWidth / 2; + + float distanceFromCenter = Math.abs(centerX - viewCenterX); + + // 1.2f - 4/60 * 0.2f + float scale = maxScale - (distanceFromCenter / maxDistance) * (maxScale - minScale); + scale = Math.max(minScale, scale); // 确保不小于最小缩放比例 + +// if (i == firstVisiblePosition) { +// SLogs.d("firstVisiblePosition: " + firstVisiblePosition + ", itemWidth: " + itemWidth + ", left: " + left + ", viewCenterX: " + viewCenterX + ", centerX: " + centerX + ", distanceFromCenter: " + distanceFromCenter + ", scale: " + scale); +// } + + float alpha = 1.0f - (distanceFromCenter / maxDistance) * 0.8f; // 透明度范围 1.0 到 0.6 + alpha = Math.max(0.8f, alpha); + view.setAlpha(alpha); + + // 仅当 scale 有变化时才更新 + if (Math.abs(view.getScaleX() - scale) > 0.01f) { + view.setScaleX(scale); + view.setScaleY(scale); + } + } + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/LinearEdgeDecoration.kt b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/LinearEdgeDecoration.kt new file mode 100644 index 000000000..b5a86b45c --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/LinearEdgeDecoration.kt @@ -0,0 +1,63 @@ +package com.seafile.seadroid2.ui.media.image_preview2 + +import android.graphics.Rect +import android.view.View +import androidx.annotation.Px +import androidx.recyclerview.widget.RecyclerView + +class LinearEdgeDecoration( + @Px private val startPadding: Int, + @Px private val endPadding: Int = startPadding, + private val orientation: Int = RecyclerView.VERTICAL, + private val inverted: Boolean = false +) : RecyclerView.ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, view: View, parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + val layoutManager: RecyclerView.LayoutManager = parent.layoutManager!! + val layoutParams = view.layoutParams as RecyclerView.LayoutParams + val position = layoutParams.viewAdapterPosition + val itemCount = layoutManager.itemCount + + if (position == RecyclerView.NO_POSITION + || itemCount == 0 + || (position > 0 && position < itemCount - 1) + ) { + return + } + + if (orientation == RecyclerView.HORIZONTAL) { + if (position == 0) { + if (!inverted) { + outRect.left = startPadding + } else { + outRect.right = startPadding + } + } else if (position == itemCount - 1) { + if (!inverted) { + outRect.right = endPadding + } else { + outRect.left = endPadding + } + } + } else { + if (position == 0) { + if (!inverted) { + outRect.top = startPadding + } else { + outRect.bottom = startPadding + } + } else if (position == itemCount - 1) { + if (!inverted) { + outRect.bottom = endPadding + } else { + outRect.top = endPadding + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/OnlyImagePreviewActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/OnlyImagePreviewActivity.java new file mode 100644 index 000000000..69e61df3d --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/media/image_preview2/OnlyImagePreviewActivity.java @@ -0,0 +1,139 @@ +package com.seafile.seadroid2.ui.media.image_preview2; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; + +import com.blankj.utilcode.util.BarUtils; +import com.blankj.utilcode.util.CollectionUtils; +import com.seafile.seadroid2.databinding.ActivityOnlyImagePreviewBinding; +import com.seafile.seadroid2.ui.adapter.ViewPager2Adapter; +import com.seafile.seadroid2.ui.base.BaseActivityWithVM; +import com.seafile.seadroid2.ui.media.image_preview.ImagePreviewViewModel; +import com.seafile.seadroid2.ui.media.image_preview.PhotoFragment; + +import java.util.ArrayList; +import java.util.List; + +public class OnlyImagePreviewActivity extends BaseActivityWithVM { + private ActivityOnlyImagePreviewBinding binding; + private ViewPager2Adapter adapter; + private List imageUrls; + private int position; + + public static void startThis(Context context, String url) { + Intent intent = new Intent(context, OnlyImagePreviewActivity.class); + intent.putStringArrayListExtra("image_urls", CollectionUtils.newArrayList(url)); + intent.putExtra("position", 0); + context.startActivity(intent); + } + + public static void startThis(Context context, ArrayList urls, int position) { + Intent intent = new Intent(context, OnlyImagePreviewActivity.class); + intent.putStringArrayListExtra("image_urls", urls); + intent.putExtra("position", position); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityOnlyImagePreviewBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + BarUtils.setNavBarVisibility(this, false); + BarUtils.setStatusBarVisibility(this, false); + + initData(); + + initView(); + initViewModel(); + + getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + finish(); + } + }); + } + + @Override + protected void onPostCreate(@Nullable Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + + notifyFragmentList(); + } + + private void initData() { + if (getIntent() == null) { + throw new IllegalArgumentException("Intent is null"); + } + + imageUrls = getIntent().getStringArrayListExtra("image_urls"); + position = getIntent().getIntExtra("position", 0); + } + + private void initView() { + + } + + private void initViewModel() { + getViewModel().getRefreshLiveData().observe(this, new Observer() { + @Override + public void onChanged(Boolean aBoolean) { + if (aBoolean) { + showLoadingDialog(); + } else { + dismissLoadingDialog(); + } + } + }); + } + + private void notifyFragmentList() { + if (CollectionUtils.isEmpty(imageUrls)) { + return; + } + + adapter = new ViewPager2Adapter(this); + List fragments = new ArrayList<>(); + for (String url : imageUrls) { + PhotoFragment photoFragment = PhotoFragment.newInstance(url); + photoFragment.setOnPhotoTapListener((view, x, y) -> hideOrShowToolBar()); + fragments.add(photoFragment); + } + + adapter.addFragments(fragments); + + binding.pager.setAdapter(adapter); +// binding.pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { +// @Override +// public void onPageSelected(int position) { +// super.onPageSelected(position); +// +// String fs = String.format(Locale.ROOT, "%d/%d", (position + 1), direntList.size()); +// binding.galleryPageIndex.setText(fs); +// +// DirentModel model = direntList.get(position); +// binding.galleryPageName.setText(model.name); +// } +// }); + + binding.pager.setCurrentItem(position); + + } + + private boolean showToolBar = false; + + private void hideOrShowToolBar() { +// binding.galleryToolBar.setVisibility(!showToolBar ? View.VISIBLE : View.GONE); +// binding.pageIndexContainer.setVisibility(!showToolBar ? View.VISIBLE : View.GONE); + showToolBar = !showToolBar; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/RefreshStatusEnum.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/RefreshStatusEnum.java new file mode 100644 index 000000000..8eff91476 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/RefreshStatusEnum.java @@ -0,0 +1,23 @@ +package com.seafile.seadroid2.ui.repo; + +public enum RefreshStatusEnum { + /** + * In other ways, this is also a transmit frequency controller + */ + NO(0), + /** + * only refresh data from local db + */ + ONLY_LOCAL(1), + + /** + * local first, then remote + */ + LOCAL_BEFORE_REMOTE(2), + REMOTE(3); + + + RefreshStatusEnum(int i) { + + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoQuickAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoQuickAdapter.java index ba3ce56f4..8f6cc5518 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoQuickAdapter.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoQuickAdapter.java @@ -17,10 +17,16 @@ import com.blankj.utilcode.util.CollectionUtils; import com.blankj.utilcode.util.EncodeUtils; +import com.blankj.utilcode.util.EncryptUtils; import com.blankj.utilcode.util.FileUtils; import com.blankj.utilcode.util.SizeUtils; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.signature.ObjectKey; import com.seafile.seadroid2.R; import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.config.AbsLayoutItemType; import com.seafile.seadroid2.config.Constants; import com.seafile.seadroid2.config.GlideLoadConfig; @@ -41,10 +47,17 @@ import com.seafile.seadroid2.framework.data.model.search.SearchModel; import com.seafile.seadroid2.framework.http.HttpIO; import com.seafile.seadroid2.framework.util.GlideApp; +import com.seafile.seadroid2.framework.util.GlideOptions; +import com.seafile.seadroid2.framework.util.ThumbnailUtils; import com.seafile.seadroid2.framework.util.Utils; import com.seafile.seadroid2.ui.base.adapter.BaseMultiAdapter; +import com.seafile.seadroid2.ui.repo.vh.AccountViewHolder; +import com.seafile.seadroid2.ui.repo.vh.DirentGalleryViewHolder; +import com.seafile.seadroid2.ui.repo.vh.DirentGridViewHolder; +import com.seafile.seadroid2.ui.repo.vh.DirentViewHolder; +import com.seafile.seadroid2.ui.repo.vh.RepoViewHolder; +import com.seafile.seadroid2.ui.repo.vh.UnsupportedViewHolder; import com.seafile.seadroid2.ui.viewholder.GroupItemViewHolder; -import com.seafile.seadroid2.widget.AnimatedStateListDrawableCompatUtils; import java.util.ArrayList; import java.util.Collections; @@ -54,7 +67,7 @@ import java.util.stream.Collectors; public class RepoQuickAdapter extends BaseMultiAdapter { - private final String SERVER = HttpIO.getCurrentInstance().getServerUrl(); + private boolean onActionMode; @@ -307,11 +320,7 @@ private void onBindRepos(RepoViewHolder holder, RepoModel model, @NonNull List payloads) { + int color; if (!CollectionUtils.isEmpty(payloads)) { Bundle bundle = (Bundle) payloads.get(0); boolean isChecked = bundle.getBoolean("is_check"); // holder.binding.getRoot().setChecked(model.is_checked); - - if (isChecked) { - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_checked); - } else { - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_unchecked); - } + updateItemMultiSelectViewWithPayload(holder.binding.itemMultiSelect, isChecked); return; } + holder.binding.itemTitle.setText(model.name); // holder.binding.getRoot().setBackground(AnimatedStateListDrawableCompatUtils.createDrawableCompat(getContext())); @@ -468,7 +464,7 @@ private void onBindDirentsGrid(DirentGridViewHolder holder, DirentModel model, @ holder.binding.itemOutline.setVisibility(View.VISIBLE); } - if (repoEncrypted || !Utils.isViewableImage(model.name)) { + if (model.isDir() || repoEncrypted || (!Utils.isViewableImage(model.name) && !Utils.isVideoFile(model.name))) { holder.binding.itemIcon.setScaleType(ImageView.ScaleType.FIT_CENTER); holder.binding.itemIcon.setImageResource(model.getIcon()); } else { @@ -477,21 +473,7 @@ private void onBindDirentsGrid(DirentGridViewHolder holder, DirentModel model, @ } //action mode - if (onActionMode) { - holder.binding.itemMultiSelect.setVisibility(View.VISIBLE); -// holder.binding.getRoot().setChecked(model.is_checked); - - if (model.is_checked) { - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_checked); - } else { - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_unchecked); - } - } else { - holder.binding.itemMultiSelect.setVisibility(View.GONE); - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_unchecked); - -// holder.binding.getRoot().setChecked(false); - } + updateItemMultiSelectView(holder.binding.itemMultiSelect, model.is_checked); if (model.starred) { holder.binding.itemTitle.setCompoundDrawables(null, null, getStarDrawable(), null); @@ -501,43 +483,60 @@ private void onBindDirentsGrid(DirentGridViewHolder holder, DirentModel model, @ } private void onBindDirentsGallery(DirentGalleryViewHolder holder, DirentModel model, @NonNull List payloads) { + int color; if (!CollectionUtils.isEmpty(payloads)) { Bundle bundle = (Bundle) payloads.get(0); boolean isChecked = bundle.getBoolean("is_check"); // holder.binding.getRoot().setChecked(model.is_checked); - - if (isChecked) { - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_checked); - } else { - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_unchecked); - } + updateItemMultiSelectViewWithPayload(holder.binding.itemMultiSelect, isChecked); return; } // holder.binding.getRoot().setBackground(AnimatedStateListDrawableCompatUtils.createDrawableCompat(getContext())); - if (repoEncrypted || !Utils.isViewableImage(model.name)) { + if (model.isDir() || repoEncrypted || (!Utils.isViewableImage(model.name) && !Utils.isVideoFile(model.name))) { holder.binding.itemIcon.setImageResource(model.getIcon()); } else { loadImage(model, holder.binding.itemIcon); } + updateItemMultiSelectView(holder.binding.itemMultiSelect, model.is_checked); + } + + private void updateItemMultiSelectViewWithPayload(ImageView imageView, boolean isChecked) { + int color; + + if (isChecked) { + color = ContextCompat.getColor(getContext(), R.color.fancy_orange); + imageView.setImageResource(R.drawable.ic_checkbox_checked); + } else { + color = ContextCompat.getColor(getContext(), R.color.bottom_sheet_pop_disable_color); + imageView.setImageResource(R.drawable.ic_checkbox_unchecked); + } + imageView.setImageTintList(ColorStateList.valueOf(color)); + } + + private void updateItemMultiSelectView(ImageView imageView, boolean isChecked) { + int color; //action mode if (onActionMode) { - holder.binding.itemMultiSelect.setVisibility(View.VISIBLE); + imageView.setVisibility(View.VISIBLE); // holder.binding.getRoot().setChecked(model.is_checked); - if (model.is_checked) { - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_checked); + if (isChecked) { + color = ContextCompat.getColor(getContext(), R.color.fancy_orange); + imageView.setImageResource(R.drawable.ic_checkbox_checked); } else { - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_unchecked); + color = ContextCompat.getColor(getContext(), R.color.bottom_sheet_pop_disable_color); + imageView.setImageResource(R.drawable.ic_checkbox_unchecked); } } else { // holder.binding.getRoot().setChecked(false); - - holder.binding.itemMultiSelect.setVisibility(View.GONE); - holder.binding.itemMultiSelect.setImageResource(R.drawable.ic_checkbox_unchecked); + color = ContextCompat.getColor(getContext(), R.color.bottom_sheet_pop_disable_color); + imageView.setVisibility(View.GONE); + imageView.setImageResource(R.drawable.ic_checkbox_unchecked); } + imageView.setImageTintList(ColorStateList.valueOf(color)); } private void onBindSearch(DirentViewHolder holder, SearchModel model) { @@ -546,7 +545,8 @@ private void onBindSearch(DirentViewHolder holder, SearchModel model) { // holder.binding.getRoot().setBackground(AnimatedStateListDrawableCompatUtils.createDrawableCompat(getContext())); - if (repoEncrypted || !Utils.isViewableImage(model.name)) { + + if (repoEncrypted || (!Utils.isViewableImage(model.name) && !Utils.isVideoFile(model.name))) { holder.binding.itemIcon.setImageResource(model.getIcon()); } else { DirentModel direntModel = new DirentModel(); @@ -565,30 +565,46 @@ private void onBindSearch(DirentViewHolder holder, SearchModel model) { } private void loadImage(DirentModel direntModel, ImageView imageView) { - - if (direntModel.name.toLowerCase().endsWith(".gif")) { - imageView.setImageResource(direntModel.getIcon()); - } else { - String url = convertThumbnailUrl(direntModel); - GlideApp.with(getContext()).load(url) - .apply(GlideLoadConfig.getOptions()) + String thumbnailUrl = convertThumbnailUrl(direntModel); + if (TextUtils.isEmpty(thumbnailUrl)) { + GlideApp.with(getContext()) + .load(direntModel.getIcon()) + .apply(GlideLoadConfig.getCacheableThumbnailOptions()) .into(imageView); + return; } + String thumbKey = EncryptUtils.encryptMD5ToString(thumbnailUrl); + GlideApp.with(getContext()) + .load(thumbnailUrl) + .signature(new ObjectKey(thumbKey)) + .apply(GlideLoadConfig.getCustomDrawableOptions(direntModel.getIcon())) + .into(imageView); } - private String convertThumbnailUrl(DirentModel direntModel) { - return convertThumbnailUrl(direntModel, 128); - } + private String server_url; + private final boolean isLogin = SupportAccountManager.getInstance().isLogin(); + + private String getServerUrl() { + if (!TextUtils.isEmpty(server_url)) { + return server_url; + } + + if (!isLogin) { + return null; + } - private String convertMiddleUrl(DirentModel direntModel) { - return convertThumbnailUrl(direntModel, 256); + server_url = HttpIO.getCurrentInstance().getServerUrl(); + return server_url; } - private String convertThumbnailUrl(DirentModel direntModel, int size) { - String newFilePath = EncodeUtils.urlEncode(direntModel.full_path); - return String.format(Locale.ROOT, "%sapi2/repos/%s/thumbnail/?p=%s&size=%d", SERVER, direntModel.repo_id, newFilePath, size); + private String convertThumbnailUrl(DirentModel direntModel) { + String serverUrl = getServerUrl(); + if (TextUtils.isEmpty(serverUrl)) { + return null; + } + return ThumbnailUtils.convertThumbnailUrl(serverUrl, direntModel.repo_id, direntModel.full_path); } public void setOnActionMode(boolean on) { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoQuickFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoQuickFragment.java index 33fb7f094..de8786a11 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoQuickFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoQuickFragment.java @@ -45,7 +45,7 @@ import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.enums.ActionModeCallbackType; -import com.seafile.seadroid2.framework.data.model.repo.RepoPermissionWrapper; +import com.seafile.seadroid2.framework.data.db.entities.PermissionEntity; import com.seafile.seadroid2.framework.datastore.sp.SettingsManager; import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetMenuAdapter; import com.seafile.seadroid2.config.AbsLayoutItemType; @@ -85,10 +85,9 @@ import com.seafile.seadroid2.ui.dialog_fragment.listener.OnRefreshDataListener; import com.seafile.seadroid2.ui.dialog_fragment.listener.OnResultListener; import com.seafile.seadroid2.ui.file.FileActivity; -import com.seafile.seadroid2.ui.main.MainActivity; import com.seafile.seadroid2.ui.main.MainViewModel; import com.seafile.seadroid2.ui.markdown.MarkdownActivity; -import com.seafile.seadroid2.ui.media.image_preview.ImagePreviewActivity; +import com.seafile.seadroid2.ui.media.image_preview2.CarouselImagePreviewActivity; import com.seafile.seadroid2.ui.media.player.exoplayer.CustomExoVideoPlayerActivity; import com.seafile.seadroid2.ui.sdoc.SDocWebViewActivity; import com.seafile.seadroid2.ui.search.SearchViewModel; @@ -96,20 +95,17 @@ import com.seafile.seadroid2.view.TipsViews; import java.io.File; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import java.util.stream.Stream; import io.reactivex.functions.Consumer; +import kotlin.Pair; public class RepoQuickFragment extends BaseFragmentWithVM { private static final String KEY_REPO_SCROLL_POSITION = "repo_scroll_position"; - private static final String KEY_REPO_LIST = "key_in_repo_list"; - - private final int forceRefreshInterval = 1000 * 60 * 5;//5m + private static final String KEY_REPO_LIST = "key_repo_list"; private final HashMap mRefreshStatusExpireTimeMap = new HashMap<>(); private final HashMap mPermissionRefreshStatusExpireTimeMap = new HashMap<>(); @@ -152,7 +148,11 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = LayoutFastRvBinding.inflate(inflater, container, false); - binding.swipeRefreshLayout.setOnRefreshListener(() -> loadData(true)); + + binding.swipeRefreshLayout.setOnRefreshListener(() -> { + removeScrolledPosition(); + loadData(RefreshStatusEnum.LOCAL_BEFORE_REMOTE); + }); return binding.getRoot(); } @@ -174,15 +174,25 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat @Override public void onFirstResume() { super.onFirstResume(); - loadData(true); + + restoreNavContext(); + + loadData(isForce(true)); } + private void restoreNavContext() { + NavContext navContext = getNavContext(); + navContext.restoreNavContextFromSp(); + mainViewModel.getOnNavContextChangeListenerLiveData().setValue(true); + } + + @Override public void onOtherResume() { super.onOtherResume(); - if (isForce()) { - loadData(true); - } + + loadData(isForce(false)); + } private void initRv() { @@ -194,6 +204,15 @@ private void initRv() { .build(); binding.rv.addItemDecoration(decoration); + binding.rv.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (RecyclerView.SCROLL_STATE_IDLE == newState) { + saveScrollPosition(); + } + } + }); //layout manager binding.rv.setLayoutManager(getGridLayoutManager()); @@ -249,20 +268,15 @@ private void initAdapter() { //update bar title startOrUpdateContextualActionBar(); + List selectedList = adapter.getSelectedList(); + BaseModel baseModel = adapter.getItems().get(i); - List selected = adapter.getSelectedList(); - if (baseModel instanceof RepoModel m) { - if (CollectionUtils.isEmpty(selected)) { - getViewModel().inflateRepoMenu(requireContext()); - } else { - getViewModel().inflateRepoMenuWithParams(requireContext(), m, getDisableMenuIds(), getWillBeRemovedMenuIds(), isPermissionForce(m.repo_id)); - } - } else if (baseModel instanceof DirentModel m) { - if (CollectionUtils.isEmpty(selected)) { - getViewModel().inflateDirentMenu(requireContext()); - } else { - getViewModel().inflateDirentMenuWithParams(requireContext(), CollectionUtils.newArrayList(m), m.is_checked, getDisableMenuIds(), getWillBeRemovedMenuIds(), isPermissionForce(m.repo_id)); - } + if (baseModel instanceof RepoModel) { + List selectedModels = selectedList.stream().map(b -> (RepoModel) b).collect(Collectors.toList()); + getViewModel().inflateRepoMenuWithSelected(requireContext(), selectedModels, getDisableMenuIds(), getWillBeRemovedMenuIds()); + } else if (baseModel instanceof DirentModel) { + List selectedModels = selectedList.stream().map(b -> (DirentModel) b).collect(Collectors.toList()); + getViewModel().inflateDirentMenuWithSelected(requireContext(), selectedModels, getDisableMenuIds(), getWillBeRemovedMenuIds()); } return; @@ -301,7 +315,6 @@ private void initAdapter() { binding.rv.setAdapter(adapter); } - private void initViewModel() { getViewModel().getRefreshLiveData().observe(getViewLifecycleOwner(), new Observer() { @Override @@ -313,11 +326,7 @@ public void onChanged(Boolean aBoolean) { getViewModel().getShowLoadingDialogLiveData().observe(getViewLifecycleOwner(), new Observer() { @Override public void onChanged(Boolean aBoolean) { - - MainActivity mainActivity = (MainActivity) getActivity(); - if (mainActivity != null) { - mainActivity.showProgressDialog(aBoolean); - } + showLoadingDialog(aBoolean); } }); @@ -325,7 +334,7 @@ public void onChanged(Boolean aBoolean) { getViewModel().getStarredLiveData().observe(getViewLifecycleOwner(), aBoolean -> { if (aBoolean) { - loadData(true); + loadData(RefreshStatusEnum.REMOTE); } closeActionMode(); @@ -352,11 +361,11 @@ public void onChanged(List menuItems) { mainViewModel.getOnResortListLiveData().observe(getViewLifecycleOwner(), a -> { - loadData(); + loadDataAsFirst(); }); mainViewModel.getOnForceRefreshRepoListLiveData().observe(getViewLifecycleOwner(), aBoolean -> { - loadData(true); + loadData(RefreshStatusEnum.REMOTE); }); mainViewModel.getOnSearchLiveData().observe(getViewLifecycleOwner(), new Observer() { @@ -366,12 +375,6 @@ public void onChanged(String s) { } }); - mainViewModel.getOnActionModeLiveData().observe(getViewLifecycleOwner(), new Observer() { - @Override - public void onChanged(ActionModeCallbackType callbackType) { - onShowActionMode(callbackType); - } - }); searchViewModel.getSearchListLiveData().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(List searchModels) { @@ -389,26 +392,25 @@ public void onChanged(FileViewType fileViewType) { Settings.FILE_LIST_SORT_BY.observe(getViewLifecycleOwner(), new Observer() { @Override public void onChanged(SortBy sortBy) { - reloadData(); + loadData(RefreshStatusEnum.ONLY_LOCAL); } }); Settings.FILE_LIST_SORT_ASCENDING.observe(getViewLifecycleOwner(), new Observer() { @Override public void onChanged(Boolean aBoolean) { - reloadData(); + loadData(RefreshStatusEnum.ONLY_LOCAL); } }); Settings.FILE_LIST_SORT_FOLDER_FIRST.observe(getViewLifecycleOwner(), new Observer() { @Override public void onChanged(Boolean aBoolean) { - reloadData(); + loadData(RefreshStatusEnum.ONLY_LOCAL); } }); } - @Override public void onPause() { super.onPause(); @@ -430,7 +432,7 @@ private List getDisableMenuIds() { if (baseModel instanceof RepoModel m) { } else if (baseModel instanceof DirentModel m) { - if (m.isDir()){ + if (m.isDir()) { return CollectionUtils.newArrayList(R.id.upload); } } @@ -585,56 +587,33 @@ private void onBottomSheetItemClick(MenuItem item) { } } - private void onShowActionMode(ActionModeCallbackType callbackType) { + private void onShowActionMode(ActionModeCallbackType actionModeType) { - if (callbackType == ActionModeCallbackType.CREATE) { + if (actionModeType == ActionModeCallbackType.CREATE) { int p = Constants.DP.DP_32 * 4; binding.rv.setPadding(0, 0, 0, p); - } else if (callbackType == ActionModeCallbackType.DESTORY) { + } else if (actionModeType == ActionModeCallbackType.DESTROY) { int p = 0; binding.rv.setPadding(0, 0, 0, p); } - if (callbackType == ActionModeCallbackType.CREATE) { + if (actionModeType == ActionModeCallbackType.CREATE || actionModeType == ActionModeCallbackType.SELECT_ALL) { if (!adapter.isOnActionMode()) { adapter.setOnActionMode(true); } - //select repo list - List models = adapter.getSelectedList(); - if (!getNavContext().inRepo()) { - if (CollectionUtils.isEmpty(models)) { - //click the select item of MenuItem - getViewModel().inflateRepoMenu(requireContext()); - } else { - //When press and hold to select some list item, only one can be selected - RepoModel repoModel = (RepoModel) models.get(0); - getViewModel().inflateRepoMenuWithParams(requireContext(), repoModel, getDisableMenuIds(), getWillBeRemovedMenuIds(), isPermissionForce(repoModel.repo_id)); - } - } else { - if (CollectionUtils.isEmpty(models)) { - getViewModel().inflateDirentMenu(requireContext()); - } else { - DirentModel direntModel = (DirentModel) models.get(0); - getViewModel().inflateDirentMenuWithParams(requireContext(), CollectionUtils.newArrayList(direntModel), true, getDisableMenuIds(), getWillBeRemovedMenuIds(), isPermissionForce(direntModel.repo_id)); - } - } - } else if (callbackType == ActionModeCallbackType.SELECT_ALL) { // - List baseModels = adapter.getSelectedList(); + List selectedList = adapter.getSelectedList(); if (!getNavContext().inRepo()) { - List repoModels = baseModels.stream().map(baseModel -> (RepoModel) baseModel).collect(Collectors.toList()); - getViewModel().inflateRepoMenuWithParams(requireContext(), repoModels, true, getDisableMenuIds(), getWillBeRemovedMenuIds(), false); + List selectedModels = selectedList.stream().map(b -> (RepoModel) b).collect(Collectors.toList()); + getViewModel().inflateRepoMenuWithSelected(requireContext(), selectedModels, getDisableMenuIds(), getWillBeRemovedMenuIds()); } else { - List direntModels = baseModels.stream().map(baseModel -> (DirentModel) baseModel).collect(Collectors.toList()); - getViewModel().inflateDirentMenuWithParams(requireContext(), direntModels, true, getDisableMenuIds(), getWillBeRemovedMenuIds(), false); + List direntModels = selectedList.stream().map(baseModel -> (DirentModel) baseModel).collect(Collectors.toList()); + getViewModel().inflateDirentMenuWithSelected(requireContext(), direntModels, getDisableMenuIds(), getWillBeRemovedMenuIds()); } - - } else if (callbackType == ActionModeCallbackType.SELECT_NONE) { - //clear menu permission list - getViewModel().clearCachePermissionMap(); + } else if (actionModeType == ActionModeCallbackType.SELECT_NONE) { // if (!getNavContext().inRepo()) { @@ -642,7 +621,7 @@ private void onShowActionMode(ActionModeCallbackType callbackType) { } else { getViewModel().inflateDirentMenu(requireContext()); } - } else if (callbackType == ActionModeCallbackType.DESTORY) { + } else if (actionModeType == ActionModeCallbackType.DESTROY) { removeFloatingView(); closeActionMode(); } else { @@ -661,10 +640,6 @@ private void removeFloatingView() { return; } - //clear permission list cache - getViewModel().clearCachePermissionMap(); - - View decorView = requireActivity().getWindow().getDecorView(); FrameLayout content = decorView.findViewById(android.R.id.content); content.removeView(floatingView); @@ -673,10 +648,6 @@ private void removeFloatingView() { bottomSheetMenuAdapter = null; } - public void reloadData() { - loadData(isForce()); - } - private FileViewType lastViewType; private void switchRecyclerViewLayout(FileViewType newViewType) { @@ -711,11 +682,11 @@ private void switchRecyclerViewLayout(FileViewType newViewType) { if (FileViewType.GALLERY != lastViewType && newViewType != FileViewType.GALLERY) { adapter.notifyItemRangeChanged(0, adapter.getItemCount()); } else { - loadData(); + loadDataAsFirst(); } //If SPAN_COUNT is updated, then the data in the ScrollPosition is meaningless - removeScrollPositionExcludeRoot(); + removeScrolledPositionExcludeRoot(); lastViewType = newViewType; } @@ -784,52 +755,69 @@ public void clearExpireRefreshMap() { mRefreshStatusExpireTimeMap.clear(); } - private boolean isForce() { + + private final long THROTTLE_FORCE_ALL_MS = 1000 * 60 * 5; // 5秒 + private final long THROTTLE_FORCE_LOCAL_MS = 5000; // 5秒 + + private RefreshStatusEnum isForce() { + return isForce(false); + } + + /** + * It will not be refreshed within 2 seconds, + * only local data will be refreshed within 5 seconds, + * and be forced to refresh in other cases + */ + private RefreshStatusEnum isForce(boolean isFirst) { + if (isFirst) { + return RefreshStatusEnum.LOCAL_BEFORE_REMOTE; + } + long now = TimeUtils.getNowMills(); - Long expire; + Long lastMills; if (!getNavContext().inRepo()) { - expire = mRefreshStatusExpireTimeMap.get(KEY_REPO_LIST); + lastMills = mRefreshStatusExpireTimeMap.get(KEY_REPO_LIST); } else { String k = getNavContext().getRepoModel().repo_id + getNavContext().getNavPath(); - expire = mRefreshStatusExpireTimeMap.get(k); + lastMills = mRefreshStatusExpireTimeMap.get(k); } - return expire == null || now > expire; - } - private boolean isPermissionForce(String repoId) { - long now = TimeUtils.getNowMills(); - Long expire = mPermissionRefreshStatusExpireTimeMap.get(repoId); + if (lastMills == null) { + return RefreshStatusEnum.REMOTE; + } - boolean ss = expire == null || now > expire; - if (ss) { - mPermissionRefreshStatusExpireTimeMap.put(repoId, (now + forceRefreshInterval)); + if (now - lastMills > THROTTLE_FORCE_ALL_MS) { + return RefreshStatusEnum.REMOTE; } - return ss; - } + if (now - lastMills > THROTTLE_FORCE_LOCAL_MS) { + return RefreshStatusEnum.REMOTE; + } - public void loadData() { - loadData(false); + return RefreshStatusEnum.NO; } - public void loadData(boolean forceRefresh) { - removeCurrentScrollPosition(); + public void loadDataAsFirst() { + loadData(RefreshStatusEnum.LOCAL_BEFORE_REMOTE); + } - if (forceRefresh) { + public void loadData(RefreshStatusEnum refreshStatus) { + SLogs.e("loadData -> " + refreshStatus); + if (refreshStatus.ordinal() >= RefreshStatusEnum.LOCAL_BEFORE_REMOTE.ordinal()) { long now = TimeUtils.getNowMills(); - now += forceRefreshInterval; - if (!getNavContext().inRepo()) { - mRefreshStatusExpireTimeMap.put(KEY_REPO_LIST, now); + String key; + if (getNavContext().inRepo()) { + key = getNavContext().getRepoModel().repo_id + getNavContext().getNavPath(); } else { - String k = getNavContext().getRepoModel().repo_id + getNavContext().getNavPath(); - mRefreshStatusExpireTimeMap.put(k, now); + key = KEY_REPO_LIST; } + mRefreshStatusExpireTimeMap.put(key, now); //force refresh permission list mPermissionRefreshStatusExpireTimeMap.clear(); } - getViewModel().loadData(getNavContext(), forceRefresh); + getViewModel().loadData(getNavContext(), refreshStatus); } private void notifyDataChanged(List repoModels) { @@ -857,9 +845,9 @@ private void showEmptyTip() { if (FileViewType.GALLERY == type) { showErrorView(R.string.no_album_type_data); } else if (getNavContext().inRepo()) { - showErrorView(R.string.no_repo); - } else { showErrorView(R.string.dir_empty); + } else { + showErrorView(R.string.no_repo); } } @@ -879,42 +867,69 @@ private void showErrorView(int textRes) { private void showErrorView(String msg) { adapter.submitList(null); TextView tipView = TipsViews.getTipTextView(requireContext()); + tipView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + loadData(RefreshStatusEnum.LOCAL_BEFORE_REMOTE); + } + }); tipView.setText(msg); adapter.setStateView(tipView); adapter.setStateViewEnable(true); } + /** + * TODO improve: go into the folder first, and then load the data + */ private void navTo(BaseModel model) { //save - saveScrollPosition(); - if (model instanceof RepoModel model1) { getNavContext().push(model1); - loadData(isForce()); + loadDataAsFirst(); } else if (model instanceof DirentModel direntModel) { if (direntModel.isDir()) { getNavContext().push(direntModel); - loadData(isForce()); + loadDataAsFirst(); } else { open(direntModel); } } else if (model instanceof SearchModel searchModel) { + navToForSearch(searchModel); + } + } + + private void navToForSearch(SearchModel searchModel) { + if (searchModel.isDir() && "/".equals(searchModel.fullpath)) { + getViewModel().getRepoModelEntity(searchModel.repo_id, new Consumer() { + @Override + public void accept(RepoModel repoModel) throws Exception { + if (repoModel == null) { + ToastUtils.showLong(R.string.repo_not_found); + return; + } + + getNavContext().push(repoModel); + loadDataAsFirst(); + } + }); - DirentModel direntModel = SearchModel.converterThis2DirentModel(searchModel); + + } else { + DirentModel direntModel = SearchModel.convert2DirentModel(searchModel); if (direntModel.isDir()) { String repoId = getNavContext().getRepoModel().repo_id; //switch to special path in special repo if (TextUtils.equals(repoId, direntModel.repo_id)) { getNavContext().switchToPath(getNavContext().getRepoModel(), searchModel.fullpath); - loadData(isForce()); + loadDataAsFirst(); } else { - getViewModel().getRepoModelAndPermissionEntity(searchModel.repo_id, isPermissionForce(searchModel.repo_id), new Consumer() { + getViewModel().getRepoModelAndPermissionEntity(searchModel.repo_id, new Consumer>() { @Override - public void accept(RepoPermissionWrapper wrapper) throws Exception { - getNavContext().switchToPath(wrapper.repoModel, searchModel.fullpath); - loadData(isForce()); + public void accept(Pair pair) { + getNavContext().switchToPath(pair.getFirst(), searchModel.fullpath); + loadDataAsFirst(); } }); } @@ -930,10 +945,13 @@ public boolean backTo() { adapter.setOnActionMode(false); } else { // - removeCurrentScrollPosition(); + removeScrolledPosition(); + // getNavContext().pop(); - getViewModel().loadData(getNavContext(), false); + + //there may be some issues: no data in some cases. + loadData(RefreshStatusEnum.ONLY_LOCAL); //notify navContext changed mainViewModel.getOnNavContextChangeListenerLiveData().setValue(true); @@ -988,8 +1006,9 @@ private void saveScrollPosition() { final int index = gridLayoutManager.findFirstVisibleItemPosition(); final ScrollState state = new ScrollState(index, top); + SLogs.d(state.toString()); - removeCurrentScrollPosition(); + removeScrolledPosition(); if (!getNavContext().inRepo()) { scrollPositions.put(KEY_REPO_SCROLL_POSITION, state); @@ -999,7 +1018,7 @@ private void saveScrollPosition() { } } - private void removeScrollPositionExcludeRoot() { + private void removeScrolledPositionExcludeRoot() { if (!scrollPositions.isEmpty()) { ScrollState rootState = scrollPositions.get(KEY_REPO_SCROLL_POSITION); scrollPositions.clear(); @@ -1007,7 +1026,7 @@ private void removeScrollPositionExcludeRoot() { } } - private void removeCurrentScrollPosition() { + private void removeScrolledPosition() { if (!getNavContext().inRepo()) { scrollPositions.remove(KEY_REPO_SCROLL_POSITION); } else { @@ -1100,13 +1119,7 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { inflater.inflate(R.menu.repos_fragment_menu, menu); if (adapter == null) return true; - mainViewModel.getOnActionModeLiveData().setValue(ActionModeCallbackType.CREATE); - -// // to hidden "r" permissions files or folder -// if (!getNavContext().isParentHasWritePermission()) { -// menu.findItem(R.id.action_mode_delete).setVisible(false); -// menu.findItem(R.id.action_mode_move).setVisible(false); -// } + onShowActionMode(ActionModeCallbackType.CREATE); return true; } @@ -1141,14 +1154,14 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { if (itemId == R.id.action_mode_select_all) { adapter.setItemSelected(!allItemsSelected); - startOrUpdateContextualActionBar(); - if (!allItemsSelected) { - mainViewModel.getOnActionModeLiveData().setValue(ActionModeCallbackType.SELECT_ALL); + onShowActionMode(ActionModeCallbackType.SELECT_ALL); } else { - mainViewModel.getOnActionModeLiveData().setValue(ActionModeCallbackType.SELECT_NONE); + onShowActionMode(ActionModeCallbackType.SELECT_NONE); } + startOrUpdateContextualActionBar(); + allItemsSelected = !allItemsSelected; } @@ -1160,7 +1173,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public void onDestroyActionMode(ActionMode mode) { if (adapter == null) return; - mainViewModel.getOnActionModeLiveData().setValue(ActionModeCallbackType.DESTORY); + onShowActionMode(ActionModeCallbackType.DESTROY); } } @@ -1179,7 +1192,7 @@ private void open(DirentModel dirent) { // because pic thumbnail under encrypted repo was not supported at the server side if (Utils.isViewableImage(fileName) && !repoModel.encrypted) { - Intent getIntent = ImagePreviewActivity.startThisFromRepo(requireContext(), dirent); + Intent getIntent = CarouselImagePreviewActivity.startThisFromObjs(requireContext(), dirent); imagePreviewActivityLauncher.launch(getIntent); return; @@ -1281,7 +1294,7 @@ public void onActionStatus(boolean isDone) { ToastUtils.showLong(R.string.rename_successful); } - loadData(true); + loadData(RefreshStatusEnum.REMOTE); closeActionMode(); } }); @@ -1301,7 +1314,7 @@ public void deleteRepo(List repoModels) { } closeActionMode(); - loadData(true); + loadData(RefreshStatusEnum.REMOTE); }); dialogFragment.show(getChildFragmentManager(), DeleteRepoDialogFragment.class.getSimpleName()); } @@ -1322,7 +1335,7 @@ public void onActionStatus(boolean isDone) { closeActionMode(); - loadData(true); + loadData(RefreshStatusEnum.REMOTE); } }); dialogFragment.show(getChildFragmentManager(), DeleteFileDialogFragment.class.getSimpleName()); @@ -1463,7 +1476,7 @@ public void onActionStatus(boolean isDone) { closeActionMode(); - loadData(true); + loadData(RefreshStatusEnum.REMOTE); } } }); @@ -1478,7 +1491,7 @@ public void onActivityResult(ActivityResult o) { return; } - loadData(true); + loadData(RefreshStatusEnum.REMOTE); } }); @@ -1486,7 +1499,7 @@ public void onActivityResult(ActivityResult o) { @Override public void onActivityResult(ActivityResult o) { if (o.getResultCode() != Activity.RESULT_OK) { - loadData(true); + loadData(RefreshStatusEnum.REMOTE); return; } @@ -1504,7 +1517,7 @@ public void onActivityResult(ActivityResult o) { ToastUtils.showLong(R.string.download_finished); } - loadData(true); + loadData(RefreshStatusEnum.REMOTE); File destinationFile = new File(localFullPath); if ("export".equals(action)) { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoService.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoService.java index ef7678051..4cae4127d 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoService.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoService.java @@ -1,25 +1,15 @@ package com.seafile.seadroid2.ui.repo; -import com.seafile.seadroid2.framework.data.db.entities.PermissionEntity; import com.seafile.seadroid2.framework.data.db.entities.RepoModel; -import com.seafile.seadroid2.framework.data.model.ResultModel; import com.seafile.seadroid2.framework.data.model.permission.PermissionListWrapperModel; +import com.seafile.seadroid2.framework.data.model.permission.PermissionParentModel; import com.seafile.seadroid2.framework.data.model.permission.PermissionWrapperModel; -import com.seafile.seadroid2.framework.data.model.repo.Dirent2Model; import com.seafile.seadroid2.framework.data.model.repo.DirentWrapperModel; import com.seafile.seadroid2.framework.data.model.repo.RepoWrapperModel; -import java.security.Permission; -import java.util.Map; - import io.reactivex.Single; -import okhttp3.RequestBody; import retrofit2.Call; -import retrofit2.http.DELETE; import retrofit2.http.GET; -import retrofit2.http.Multipart; -import retrofit2.http.POST; -import retrofit2.http.PartMap; import retrofit2.http.Path; import retrofit2.http.Query; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoViewModel.java index daca7b503..a39f90663 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoViewModel.java @@ -15,9 +15,7 @@ import com.seafile.seadroid2.SeafException; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; -import com.seafile.seadroid2.annotation.Todo; -import com.seafile.seadroid2.annotation.Unstable; -import com.seafile.seadroid2.framework.data.model.repo.RepoPermissionWrapper; +import com.seafile.seadroid2.framework.data.model.permission.PermissionWrapperModel; import com.seafile.seadroid2.ui.bottomsheetmenu.ActionMenu; import com.seafile.seadroid2.context.NavContext; import com.seafile.seadroid2.enums.FileViewType; @@ -30,8 +28,6 @@ import com.seafile.seadroid2.framework.data.model.BaseModel; import com.seafile.seadroid2.framework.data.model.ResultModel; import com.seafile.seadroid2.enums.TransferStatus; -import com.seafile.seadroid2.framework.data.model.permission.PermissionListWrapperModel; -import com.seafile.seadroid2.framework.data.model.permission.PermissionWrapperModel; import com.seafile.seadroid2.framework.data.model.repo.Dirent2Model; import com.seafile.seadroid2.framework.http.HttpIO; import com.seafile.seadroid2.framework.util.Utils; @@ -46,14 +42,14 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import java.util.stream.Collectors; -import io.reactivex.Completable; import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.SingleSource; import io.reactivex.functions.Action; -import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import kotlin.Pair; @@ -63,6 +59,54 @@ public class RepoViewModel extends BaseViewModel { private final MutableLiveData> _objListLiveData = new MutableLiveData<>(); private final MutableLiveData _starredLiveData = new MutableLiveData<>(); + private final MutableLiveData> _menuItemListLiveData = new MutableLiveData<>(); + /** + * {@link #getCurrentNavPermissionCacheMap()} + */ + private final ConcurrentHashMap _permissionMap = new ConcurrentHashMap<>(); + + /** + * There are only 6 cases at most. + *
      +     *     repo's permission:
      +     *     <"repo-?", PermissionEntity()>
      +     *
      +     *     dirent's permission:
      +     *     <"rw", PermissionEntity()>
      +     *     <"r", PermissionEntity()>
      +     *     <"manage", PermissionEntity()>
      +     *     <"cloud-edit", PermissionEntity()>
      +     *     <"cloud-preview", PermissionEntity()>
      +     * 
      + */ + public ConcurrentHashMap getCurrentNavPermissionCacheMap() { + return _permissionMap; + } + + private void addRepoPermissionIntoMap(PermissionEntity permission) { + getCurrentNavPermissionCacheMap().put("repo", permission); + } + + private PermissionEntity getRepoPermissionFromMap() { + return getCurrentNavPermissionCacheMap().get("repo"); + } + + private boolean isNotExistsRepoPermission() { + return !getCurrentNavPermissionCacheMap().containsKey("repo"); + } + + private void removeNonRepoPermission() { + getCurrentNavPermissionCacheMap().keySet().removeIf(s -> !"repo".equals(s)); + } + + public void removeAllPermission() { + getCurrentNavPermissionCacheMap().clear(); + } + + + public MutableLiveData> getMenuItemListLiveData() { + return _menuItemListLiveData; + } public MutableLiveData getStarredLiveData() { return _starredLiveData; @@ -87,28 +131,50 @@ public void accept(List list) throws Exception { } - public void loadData(NavContext context, boolean forceRefresh) { + public void loadData(NavContext context, RefreshStatusEnum refreshStatus) { Account account = SupportAccountManager.getInstance().getCurrentAccount(); if (account == null) { return; } - if (!context.inRepo()) { - loadReposFromLocal(account, forceRefresh); - } else if (forceRefresh) { - loadDirentsFromRemote(account, context); - } else { - FileViewType fileViewType = Settings.FILE_LIST_VIEW_TYPE.queryValue(); - if (FileViewType.GALLERY == fileViewType) { - loadDirentsFromLocalWithGalleryViewType(account, context); + //force refresh + if (RefreshStatusEnum.REMOTE == refreshStatus) { + if (context.inRepo()) { + loadDirentsFromRemote(account, context); + } else { + loadReposFromRemote(account); + } + } else if (RefreshStatusEnum.LOCAL_BEFORE_REMOTE == refreshStatus) { + if (context.inRepo()) { + FileViewType fileViewType = Settings.FILE_LIST_VIEW_TYPE.queryValue(); + if (FileViewType.GALLERY == fileViewType) { + loadDirentsFromLocalWithGalleryViewType(account, context, true); + } else { + loadDirentsFromLocal(account, context, true); + } + } else { + loadReposFromLocal(account, true); + } + } else if (RefreshStatusEnum.ONLY_LOCAL == refreshStatus) { + if (context.inRepo()) { + FileViewType fileViewType = Settings.FILE_LIST_VIEW_TYPE.queryValue(); + if (FileViewType.GALLERY == fileViewType) { + loadDirentsFromLocalWithGalleryViewType(account, context, false); + } else { + loadDirentsFromLocal(account, context, false); + } } else { - loadDirentsFromLocal(account, context); + loadReposFromLocal(account, false); } + } else { + //RefreshStatusEnum.NO: do nothing } } - private void loadReposFromLocal(Account account, boolean isForce) { - if (isForce) { + private void loadReposFromLocal(Account account, boolean isLoadRemoteData) { + removeAllPermission(); + + if (isLoadRemoteData) { getRefreshLiveData().setValue(true); } @@ -125,7 +191,7 @@ public void accept(List repoModels) { List list = Objs.parseRepoListForAdapter(repoModels, account.getSignature(), false); getObjListLiveData().setValue(list); - if (isForce) { + if (isLoadRemoteData) { loadReposFromRemote(account); } else { getRefreshLiveData().setValue(false); @@ -135,6 +201,9 @@ public void accept(List repoModels) { } private void loadReposFromRemote(Account account) { + + removeAllPermission(); + if (!NetworkUtils.isConnected()) { getRefreshLiveData().setValue(false); return; @@ -164,71 +233,107 @@ public void accept(Throwable throwable) throws Exception { }); } - private void loadDirentsFromLocalWithGalleryViewType(Account account, NavContext context) { - getRefreshLiveData().setValue(true); + private Single> getLoadDirentsFromLocalSingle(Account account, NavContext navContext) { + RepoModel repoModel = navContext.getRepoModel(); + + String repoId = repoModel.repo_id; + String parentDir = navContext.getNavPath(); + boolean isRepoCustomPermission = repoModel.isCustomPermission(); + String repoPermName = repoModel.permission; - String repoId = context.getRepoModel().repo_id; - String parentDir = context.getNavPath(); + if (!parentDir.endsWith("/")) { + parentDir = parentDir + "/"; + } + + Single> direntDBSingle = AppDatabase.getInstance().direntDao().getListByParentPathAsync(repoId, parentDir); + Single> curParentDownloadedList = AppDatabase.getInstance().fileTransferDAO().getDownloadedListByParentAsync(repoId, parentDir); - Single> direntDBSingle = AppDatabase.getInstance().direntDao().getListByParentPath(repoId, parentDir); - addSingleDisposable(direntDBSingle, new Consumer>() { + List> singles = new ArrayList<>(); + singles.add(direntDBSingle); + singles.add(curParentDownloadedList); + + if (isRepoCustomPermission && isNotExistsRepoPermission()) { + //get special number permission from db + int pNum = repoModel.getCustomPermissionNum(); + Single> pSingle = AppDatabase.getInstance().permissionDAO().getByRepoAndIdAsync(repoId, pNum); + singles.add(pSingle); + } + + return Single.zip(singles, new Function>() { @Override - public void accept(List direntModels) throws Exception { + public List apply(Object[] results) throws Exception { - List rets = CollectionUtils.newArrayList(); - for (DirentModel direntModel : direntModels) { - if (Utils.isViewableImage(direntModel.name) || Utils.isVideoFile(direntModel.name)) { - rets.add(direntModel); + List direntList = (List) results[0]; + if (CollectionUtils.isEmpty(direntList)) { + return direntList; + } + + List downloadedList = (List) results[1]; + if (!CollectionUtils.isEmpty(downloadedList)) { + for (DirentModel direntModel : direntList) { + String fullPath = direntModel.parent_dir + direntModel.name; + Optional firstOp = downloadedList.stream().filter(f -> TextUtils.equals(fullPath, f.full_path)).findFirst(); + if (firstOp.isPresent()) { + FileTransferEntity entity = firstOp.get(); + if (entity.transfer_status == TransferStatus.SUCCEEDED) { + direntModel.transfer_status = entity.transfer_status; + direntModel.local_file_path = entity.target_path; + } + } } } - if (CollectionUtils.isEmpty(rets)) { - loadDirentsFromRemote(account, context); + //cache repo permission + if (isRepoCustomPermission && isNotExistsRepoPermission()) { + List permissionList = (List) results[2]; + if (!CollectionUtils.isEmpty(permissionList)) { + addRepoPermissionIntoMap(permissionList.get(0)); + } } else { - getObjListLiveData().setValue(Objs.parseLocalDirents(rets)); - getRefreshLiveData().setValue(false); + addRepoPermissionIntoMap(new PermissionEntity(repoId, repoModel.permission)); } + + return direntList; } }); } - private void loadDirentsFromLocal(Account account, NavContext context) { + private void loadDirentsFromLocalWithGalleryViewType(Account account, NavContext navContext, boolean isLoadRemoteData) { getRefreshLiveData().setValue(true); - String repoId = context.getRepoModel().repo_id; - String parentDir = context.getNavPath(); - - Single> direntDBSingle = AppDatabase.getInstance().direntDao().getListByParentPath(repoId, parentDir); - Single> curParentDownloadedList = AppDatabase.getInstance().fileTransferDAO().getDownloadedListByParentAsync(repoId, parentDir); - - Single> resultSingle = Single.zip(direntDBSingle, curParentDownloadedList, new BiFunction, List, List>() { + Single> r = getLoadDirentsFromLocalSingle(account, navContext); + addSingleDisposable(r, new Consumer>() { @Override - public List apply(List direntModels, List cur_parent_downloaded_list) throws Exception { - if (CollectionUtils.isEmpty(direntModels)) { - return direntModels; - } + public void accept(List direntModels) throws Exception { + List rets = CollectionUtils.newArrayList(); for (DirentModel direntModel : direntModels) { - String fullPath = direntModel.parent_dir + direntModel.name; - Optional firstOp = cur_parent_downloaded_list.stream().filter(f -> TextUtils.equals(fullPath, f.full_path)).findFirst(); - if (firstOp.isPresent()) { - FileTransferEntity entity = firstOp.get(); - if (entity.transfer_status == TransferStatus.SUCCEEDED) { - direntModel.transfer_status = entity.transfer_status; - direntModel.local_file_path = entity.target_path; - } + if (Utils.isViewableImage(direntModel.name) || Utils.isVideoFile(direntModel.name)) { + rets.add(direntModel); } } - return direntModels; + if (isLoadRemoteData) { + loadDirentsFromRemote(account, navContext); + } else { + getObjListLiveData().setValue(Objs.parseLocalDirents(rets)); + getRefreshLiveData().setValue(false); + } } }); + } - addSingleDisposable(resultSingle, new Consumer>() { + private void loadDirentsFromLocal(Account account, NavContext navContext, boolean isLoadRemoteData) { + getRefreshLiveData().setValue(true); + + Single> r = getLoadDirentsFromLocalSingle(account, navContext); + + addSingleDisposable(r, new Consumer>() { @Override public void accept(List direntModels) throws Exception { - if (CollectionUtils.isEmpty(direntModels)) { - loadDirentsFromRemote(account, context); + + if (isLoadRemoteData) { + loadDirentsFromRemote(account, navContext); } else { getObjListLiveData().setValue(Objs.parseLocalDirents(direntModels)); getRefreshLiveData().setValue(false); @@ -237,23 +342,56 @@ public void accept(List direntModels) throws Exception { }); } - private void loadDirentsFromRemote(Account account, NavContext context) { + private void loadDirentsFromRemote(Account account, NavContext navContext) { if (!NetworkUtils.isConnected()) { getRefreshLiveData().setValue(false); + getSeafExceptionLiveData().setValue(SeafException.networkException); return; } - String repoId = context.getRepoModel().repo_id; - String repoName = context.getRepoModel().repo_name; - String parentDir = context.getNavPath(); + RepoModel repoModel = navContext.getRepoModel(); + String repoId = repoModel.repo_id; + String repoName = navContext.getRepoModel().repo_name; - if ("/".equals(parentDir)) { - loadPermissionFromRemote(repoId); + String parentDir = navContext.getNavPath(); + if (!parentDir.endsWith("/")) { + parentDir = parentDir + "/"; } - Single> resultSingle = Objs.getDirentsSingleFromServer(account, repoId, repoName, parentDir); + Single> direntSingle = Objs.getDirentsSingleFromServer(account, repoId, repoName, parentDir); + List> singles = new ArrayList<>(); + singles.add(direntSingle); - addSingleDisposable(resultSingle, new Consumer>() { + boolean isRepoCustomPermission = navContext.getRepoModel().isCustomPermission(); + if (isRepoCustomPermission) { + Single permissionWrapperModelSingle = HttpIO.getCurrentInstance().execute(RepoService.class).getCustomSharePermissionById(repoId, repoModel.getCustomPermissionNum()); + singles.add(permissionWrapperModelSingle); + } + + Single> r = Single.zip(singles, new Function>() { + @Override + public List apply(Object[] results) throws Exception { + + List direntList = (List) results[0]; + if (CollectionUtils.isEmpty(direntList)) { + return direntList; + } + + //cache repo permission + if (isRepoCustomPermission) { + PermissionWrapperModel permissionWrapperModel = (PermissionWrapperModel) results[1]; + if (permissionWrapperModel != null) { + addRepoPermissionIntoMap(new PermissionEntity(repoId, permissionWrapperModel.permission)); + } + } else { + addRepoPermissionIntoMap(new PermissionEntity(repoId, repoModel.permission)); + } + + return direntList; + } + }); + + addSingleDisposable(r, new Consumer>() { @Override public void accept(List direntModels) throws Exception { @@ -288,398 +426,279 @@ public void accept(Throwable throwable) throws Exception { }); } - public List inflateMenu(Context context, int rid) { - ActionMenu menu = new ActionMenu(context); - - MenuInflater inflater = new MenuInflater(context); - inflater.inflate(rid, menu); - - List items = new ArrayList<>(menu.size()); - for (int i = 0; i < menu.size(); i++) { - items.add(menu.getItem(i)); - } - - return items; - } - - private final MutableLiveData> _menuItemListLiveData = new MutableLiveData<>(); - - public MutableLiveData> getMenuItemListLiveData() { - return _menuItemListLiveData; - } - - - public void getRepoModelAndPermissionEntity(String repoId, boolean isForce, Consumer consumer) { - Single>> r = getRepoModelAndAllPermissionSingle(repoId, isForce); - addSingleDisposable(r, new Consumer>>() { + public void getRepoModelEntity(String repoId, Consumer consumer) { + Single> r = AppDatabase.getInstance().repoDao().getRepoById(repoId); + addSingleDisposable(r, new Consumer>() { @Override - public void accept(Pair> pair) throws Exception { + public void accept(List rs) throws Exception { if (consumer != null) { - if (CollectionUtils.isEmpty(pair.getSecond())) { - consumer.accept(new RepoPermissionWrapper(pair.getFirst(), null)); - return; - } - - List list = pair.getSecond(); - - RepoModel repoModel = pair.getFirst(); - Optional permission = list.stream().filter(f -> f.id == repoModel.getCustomPermissionNum()).findFirst(); - - if (!permission.isPresent()) { - consumer.accept(new RepoPermissionWrapper(pair.getFirst(), null)); - return; + if (CollectionUtils.isEmpty(rs)) { + consumer.accept(null); + } else { + consumer.accept(rs.get(0)); } + } + } + }); + } - consumer.accept(new RepoPermissionWrapper(pair.getFirst(), permission.get())); + public void getRepoModelAndPermissionEntity(String repoId, Consumer> consumer) { + Single> r = getRepoModelAndAllPermissionSingle(repoId); + addSingleDisposable(r, new Consumer>() { + @Override + public void accept(Pair pair) throws Exception { + if (consumer != null) { + consumer.accept(pair); } } }); } /** - * do not use - *
    1. get the list of libraries first.
    2. - *
    3. obtain the corresponding local permission data from the library list
    4. - *
    5. if the length of the local permission data list and the library list are the same, the data will be returned directly
    6. - *
    7. if not, send multiple requests to the server concurrently to obtain the permission list data
    8. - *
    9. merge the permission list data and the library list data to return
    10. + * get the repoModel and repoMode‘s PermissionEntity from local, if not exist, get from remote. */ - @Unstable - @Todo - private Single> getMultipleRepoModelAndRelatePermissionDataSingle(List repoIds, boolean isForce) { - Single> dbSingle = AppDatabase.getInstance().repoDao().getRepoListByIds(repoIds); - return dbSingle.flatMap(new Function, SingleSource>>() { + private Single> getRepoModelAndAllPermissionSingle(String repoId) { + Single> repoSingle = AppDatabase.getInstance().repoDao().getRepoById(repoId); + return repoSingle.flatMap(new Function, SingleSource>>() { @Override - public SingleSource> apply(List repoModels) throws Exception { + public SingleSource> apply(List repoModels) throws Exception { if (CollectionUtils.isEmpty(repoModels)) { - return Single.error(new IllegalArgumentException("No RepoModels found for the given repoIds")); + return Single.just(new Pair<>(null, null)); } - if (isForce) { - // return empty list - List wrappers = repoModels.stream() - .map(model -> new RepoPermissionWrapper(model, null)) - .collect(Collectors.toList()); - return Single.just(wrappers); + RepoModel repoModel = repoModels.get(0); + if (repoModel.isCustomPermission()) { + return Single.just(new Pair<>(repoModel, new PermissionEntity())); } - return handleRepoPermissions(repoModels); + return Single.just(new Pair<>(repoModel, new PermissionEntity(repoId, repoModel.permission))); + } - }).onErrorResumeNext(throwable -> { - // 错误处理,记录日志或返回默认值 - SLogs.e("Error in getRepoModelAndAllPermissionSingle2", throwable); - return Single.error(throwable); - }); - } + }).flatMap(new Function, SingleSource>>() { + @Override + public SingleSource> apply(Pair pair) throws Exception { - @Unstable - @Todo - private Single> handleRepoPermissions(List repoModels) { - List customPermissionIds = repoModels.stream() - .filter(RepoModel::isCustomPermission) - .map(RepoModel::getCustomPermissionNum) - .collect(Collectors.toList()); - - if (customPermissionIds.isEmpty()) { - // 如果没有自定义权限,直接返回默认权限 - List wrappers = repoModels.stream() - .map(repo -> new RepoPermissionWrapper(repo, - new PermissionEntity(repo.repo_id, repo.permission))) - .collect(Collectors.toList()); - return Single.just(wrappers); - } + if (pair.getFirst() == null) { + return Single.error(SeafException.notFoundException); + } - Single> pSingle = AppDatabase.getInstance().permissionDAO().getByIdsAsync(customPermissionIds); - return pSingle.map(permissionEntities -> mapPermissionsToWrappers(repoModels, permissionEntities, customPermissionIds)); - } + if (pair.getSecond().isValid()) { + return Single.just(pair); + } - @Unstable - @Todo - private List mapPermissionsToWrappers(List repoModels, - List permissionEntities, - List customPermissionIds) { - boolean isValid = customPermissionIds.size() == permissionEntities.size(); - List wrappers = new ArrayList<>(); + RepoModel repoModel = pair.getFirst(); + Single> pSingle = AppDatabase.getInstance().permissionDAO().getByRepoAndIdAsync(repoId, repoModel.getCustomPermissionNum()); + return pSingle.flatMap((Function, SingleSource>>) pList -> { + //no data in local db + if (CollectionUtils.isEmpty(pList)) { + return Single.just(new Pair<>(pair.getFirst(), new PermissionEntity())); + } - for (RepoModel model : repoModels) { - if (model.isCustomPermission()) { - Optional matchedPermission = permissionEntities.stream() - .filter(entity -> TextUtils.equals(entity.repo_id, model.repo_id)) - .findFirst(); - wrappers.add(new RepoPermissionWrapper(model, matchedPermission.orElse(null))); - } else { - wrappers.add(new RepoPermissionWrapper(model, - new PermissionEntity(model.repo_id, model.permission))); + //get first permission + return Single.just(new Pair<>(pair.getFirst(), pList.get(0))); + }); } - } - - // 如果权限不完整,需从远程加载 - if (!isValid) { - wrappers.forEach(wrapper -> { - if (wrapper.getPermission() == null) { - wrapper.setPermission(null); + }).flatMap(new Function, SingleSource>>() { + @Override + public SingleSource> apply(Pair pair) throws Exception { + if (pair.getSecond().isValid()) { + return Single.just(pair); } - }); - } - return wrappers; + Single permissionSingle = getLoadRepoPermissionFromRemoteSingle(repoId, pair.getFirst().getCustomPermissionNum()); + return permissionSingle.flatMap(new Function>>() { + @Override + public SingleSource> apply(PermissionEntity p1) throws Exception { + if (p1.isValid()) { + return Single.just(new Pair<>(pair.getFirst(), p1)); + } + + return Single.just(new Pair<>(pair.getFirst(), new PermissionEntity())); + } + }); + } + }); + } /** * get the repoModel and repoMode‘s PermissionEntity from local, if not exist, get from remote. - * if isForce is true, get from remote directly and save to db */ - private Single>> getRepoModelAndAllPermissionSingle(String repoId, boolean isForce) { - Single> dbSingle = AppDatabase.getInstance().repoDao().getRepoById(repoId); - return dbSingle.flatMap(new Function, SingleSource>>>() { - @Override - public SingleSource>> apply(List repoModels) throws Exception { - if (CollectionUtils.isEmpty(repoModels)) { - return null; - } - - RepoModel repoModel = repoModels.get(0); - if (TextUtils.isEmpty(repoModel.permission)) { - //This issue doesn't actually happen, but it's still checked again from the remote check - return Single.just(new Pair<>(repoModel, null)); - } + private Single getRepoModelAndPermissionSingle(String repoId, int pNum) { + //get permission from db by special number + + Single> pSingle = AppDatabase.getInstance().permissionDAO().getByRepoAndIdAsync(repoId, pNum); + return pSingle.flatMap((Function, SingleSource>) pList -> { + //no data in local db + if (CollectionUtils.isEmpty(pList)) { + return Single.just(new PermissionEntity()); + } - if (isForce) { - //get permission from remote - return Single.just(new Pair<>(repoModel, null)); - } + //get first permission + return Single.just(pList.get(0)); + }).flatMap(new Function>() { + @Override + public SingleSource apply(PermissionEntity p) throws Exception { + if (p.isValid()) { + return Single.just(p); + } - //get special number permission from db - Single> pSingle = AppDatabase.getInstance().permissionDAO().getByRepoIdAsync(repoId); + Single permissionSingle = getLoadRepoPermissionFromRemoteSingle(repoId, pNum); + return permissionSingle.flatMap(new Function>() { + @Override + public SingleSource apply(PermissionEntity p1) throws Exception { + return Single.just(p1); + } + }); + } + }); - return pSingle.flatMap((Function, SingleSource>>>) pList -> { + } - //no data in local db - if (CollectionUtils.isEmpty(pList)) { - return Single.just(new Pair<>(repoModel, null)); - } + private Single getLoadRepoPermissionFromRemoteSingle(String repoId, int pNum) { + Single single = HttpIO.getCurrentInstance().execute(RepoService.class).getCustomSharePermissionById(repoId, pNum); + return single.flatMap(new Function>() { + @Override + public SingleSource apply(PermissionWrapperModel wrapperModel) throws Exception { + if (wrapperModel == null || wrapperModel.permission == null) { + return Single.just(new PermissionEntity()); + } - //get first permission - return Single.just(new Pair<>(repoModel, pList)); - }); - } - }) - //from remote - .flatMap((Function>, SingleSource>>>) pair -> { - if (pair.getSecond() != null) { - return Single.just(pair); - } + PermissionEntity permission = new PermissionEntity(repoId, wrapperModel.permission); - Single> permissionSingle = getLoadRepoPermissionFromRemoteSingle(repoId); - return permissionSingle.flatMap((Function, SingleSource>>>) remoteList -> { - if (CollectionUtils.isEmpty(remoteList)) { - return Single.just(pair); - } + AppDatabase.getInstance().permissionDAO().insert(permission); + SLogs.d("The list has been inserted into the local database"); - return Single.just(new Pair<>(pair.getFirst(), remoteList)); - }); - }); + return Single.just(permission); + } + }); } - /** - *
      -     *     <"rw", PermissionEntity(permission,ids)>
      -     *     <"r", PermissionEntity(permission,ids)>
      -     *     <"custom-48", PermissionEntity(permission,ids)>
      -     *     <"custom-49", PermissionEntity(permission,ids)>
      -     * 
      - */ - private final HashMap _permissionMap = new HashMap<>(); + public void inflateRepoMenu(Context context) { - public HashMap getPermissionStackMap() { - return _permissionMap; - } + removeAllPermission(); - public void inflateRepoMenu(Context context) { - getPermissionStackMap().clear(); toParseMenu(context, R.menu.bottom_sheet_op_repo, null, CollectionUtils.newArrayList(R.id.unstar)); } /** - * @param repoModels + * @param selectedRepoModels */ - public void inflateRepoMenuWithParams(Context context, List repoModels, boolean is_checked, List disableMenuIds, List removedMenuIds, boolean isForce) { - if (CollectionUtils.isEmpty(repoModels)) { + public void inflateRepoMenuWithSelected(Context context, List selectedRepoModels, List disableMenuIds, List removedMenuIds) { + if (CollectionUtils.isEmpty(selectedRepoModels)) { + inflateRepoMenu(context); return; } int menuId = R.menu.bottom_sheet_op_repo; - if (!is_checked) { - multipleRemoveCachedRepoPermissionMapData(repoModels); - - toParseMenu(context, menuId, disableMenuIds, removedMenuIds); - return; - } + if (selectedRepoModels.size() == 1) { + RepoModel repoModel = selectedRepoModels.get(0); + if (repoModel.isCustomPermission()) { + getRefreshLiveData().setValue(true); + Single r = getRepoModelAndPermissionSingle(repoModel.repo_id, repoModel.getCustomPermissionNum()); + addSingleDisposable(r, new Consumer() { + @Override + public void accept(PermissionEntity permission) throws Exception { + getRefreshLiveData().setValue(false); + List pList = !permission.isValid() ? null : CollectionUtils.newArrayList(permission); + toParseMenu(context, menuId, pList, disableMenuIds, removedMenuIds); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + getRefreshLiveData().setValue(false); + toParseMenu(context, menuId, disableMenuIds, removedMenuIds); + } + }); + } else { + List permissionEntities = CollectionUtils.newArrayList(new PermissionEntity(repoModel.repo_id, repoModel.permission)); + toParseMenu(context, menuId, permissionEntities, disableMenuIds, removedMenuIds); + } - if (repoModels.size() == 1) { - inflateRepoMenuWithParams(context, repoModels.get(0), disableMenuIds, removedMenuIds, isForce); } else { + + // List permissionEntities = CollectionUtils.newArrayList(); - for (RepoModel repoModel : repoModels) { + for (RepoModel repoModel : selectedRepoModels) { //NOTICE this is a special permission("r"), not a real permission //because: currently, multiple repo lists cannot be deleted at the same time //it will be fixed later permissionEntities.add(new PermissionEntity(repoModel.repo_id, "r")); } - multipleCacheRepoPermissionMapData(repoModels, permissionEntities); - - toParseMenu(context, menuId, disableMenuIds, removedMenuIds); + toParseMenu(context, menuId, permissionEntities, disableMenuIds, removedMenuIds); } } - public void inflateRepoMenuWithParams(Context context, RepoModel repoModel, List disableMenuIds, List removedMenuIds, boolean isForce) { - int menuId = R.menu.bottom_sheet_op_repo; - - if (!repoModel.is_checked) { - //remove permission - removeCachedPermissionMapData(repoModel.permission, repoModel.repo_id); - - toParseMenu(context, menuId, disableMenuIds, removedMenuIds); - return; - } - - Single>> r = getRepoModelAndAllPermissionSingle(repoModel.repo_id, isForce); - addSingleDisposable(r, new Consumer>>() { - @Override - public void accept(Pair> pair) throws Exception { - - multipleCacheRepoPermissionMapData(CollectionUtils.newArrayList(repoModel), pair.getSecond()); - - toParseMenu(context, menuId, disableMenuIds, removedMenuIds); - } - }); - } - public void inflateDirentMenu(Context context) { - getPermissionStackMap().clear(); + removeNonRepoPermission(); - toParseMenu(context, R.menu.bottom_sheet_op_dirent, null, CollectionUtils.newArrayList(R.id.unstar)); + toParseMenu(context, R.menu.bottom_sheet_op_dirent, null, null, CollectionUtils.newArrayList(R.id.unstar)); } - public void inflateDirentMenuWithParams(Context context, List direntModels, boolean isChecked, List disableMenuIds, List removedMenuIds, boolean isForce) { - if (CollectionUtils.isEmpty(direntModels)) { + public void inflateDirentMenuWithSelected(Context context, List selectedDirentList, List disableMenuIds, List removedMenuIds) { + if (CollectionUtils.isEmpty(selectedDirentList)) { + inflateDirentMenu(context); return; } - int menuId = R.menu.bottom_sheet_op_dirent; + removeNonRepoPermission(); - if (!isChecked) { - //remove permission - multipleRemoveCachedDirentPermissionMapData(direntModels); + for (DirentModel model : selectedDirentList) { + if (model.isCustomPermission()) { + //custom permission is same as repo permission + continue; + } - toParseMenu(context, menuId, disableMenuIds, removedMenuIds); - return; + cachePermissionMapData(model.permission, new PermissionEntity(model.repo_id, model.permission)); } - String repo_id = direntModels.get(0).repo_id; - - Single>> r = getRepoModelAndAllPermissionSingle(repo_id, isForce); - addSingleDisposable(r, new Consumer>>() { - @Override - public void accept(Pair> pair) throws Exception { - RepoModel repoModel = pair.getFirst(); - List permissionList = pair.getSecond(); + int menuId = R.menu.bottom_sheet_op_dirent; - multipleCacheDirentPermissionMapData(direntModels, permissionList); + if (selectedDirentList.size() == 1) { + PermissionEntity repoPerm = getRepoPermissionFromMap(); + DirentModel direntModel = selectedDirentList.get(0); - toParseMenu(context, menuId, disableMenuIds, removedMenuIds); + List permissionList = null; + if (direntModel.isCustomPermission()) { + if (direntModel.getCustomPermissionNum() == repoPerm.id) { + permissionList = new ArrayList<>(CollectionUtils.newArrayList(repoPerm)); + } else { + //没有这个情况 + } + } else if (direntModel.permission.equals(repoPerm.name)) { + permissionList = new ArrayList<>(CollectionUtils.newArrayList(repoPerm)); + } else { + //dirent's permissions can only be one of these 5 permission: "rw"/"r"/"cloud-edit"/"cloud-preview"/"manage" + permissionList = new ArrayList<>(CollectionUtils.newArrayList(new PermissionEntity(direntModel.repo_id, direntModel.permission))); } - }); - } - public void clearCachePermissionMap() { - getPermissionStackMap().clear(); - } - - private void multipleRemoveCachedDirentPermissionMapData(List direntModels) { - if (CollectionUtils.isEmpty(direntModels)) { - return; + toParseMenu(context, menuId, permissionList, disableMenuIds, removedMenuIds); + } else { + List permissionList = new ArrayList<>(getCurrentNavPermissionCacheMap().values()); + toParseMenu(context, menuId, permissionList, disableMenuIds, removedMenuIds); } - for (DirentModel direntModel : direntModels) { - removeCachedPermissionMapData(direntModel.permission, direntModel.uid); - } - } - private void multipleRemoveCachedRepoPermissionMapData(List repoModels) { - if (CollectionUtils.isEmpty(repoModels)) { - return; - } - for (RepoModel repoModel : repoModels) { - removeCachedPermissionMapData(repoModel.permission, repoModel.repo_id); - } } - private void removeCachedPermissionMapData(String permission, String id) { - if (!getPermissionStackMap().containsKey(permission)) { - return; - } - PermissionEntity entity = getPermissionStackMap().get(permission); - if (entity == null) { - return; - } + private void cachePermissionMapData(String permissionName, @NonNull PermissionEntity entity) { - if (!entity.hasId(id)) { + if (getCurrentNavPermissionCacheMap().containsKey(permissionName)) { return; } - entity.removeById(id); - - // - if (entity.isEmptyIds()) { - getPermissionStackMap().remove(permission); - } + getCurrentNavPermissionCacheMap().put(permissionName, entity); } - private void multipleCacheDirentPermissionMapData(List models, List entities) { - if (CollectionUtils.isEmpty(models)) { - return; - } - - for (DirentModel model : models) { - if (!model.isCustomPermission()) { - cachePermissionMapData(model.permission, model, new PermissionEntity(model.repo_id, model.permission)); - } else { - entities.stream().filter(f -> f.id == model.getCustomPermissionNum()).findFirst().ifPresent(entity -> cachePermissionMapData(model.permission, model, entity)); - } - } - } - - private void multipleCacheRepoPermissionMapData(List models, List entities) { - if (CollectionUtils.isEmpty(models)) { - return; - } - - for (RepoModel model : models) { - if (!model.isCustomPermission()) { - cachePermissionMapData(model.permission, model, new PermissionEntity(model.repo_id, model.permission)); - } else { - entities.stream().filter(f -> f.id == model.getCustomPermissionNum()).findFirst().ifPresent(entity -> cachePermissionMapData(model.permission, model, entity)); - } - } - } - - private void cachePermissionMapData(String permission, BaseModel baseModel, @NonNull PermissionEntity entity) { - if (!getPermissionStackMap().containsKey(permission)) { - entity.cacheBaseModel(baseModel); - } else { - PermissionEntity entity1 = getPermissionStackMap().get(permission); - assert entity1 != null; - entity1.cacheBaseModel(baseModel); - } - - getPermissionStackMap().put(permission, entity); + private void toParseMenu(Context context, int menuId, List permissionList, List disableMenuIds, List removedMenuIds) { + List items = parseMenu(context, menuId, permissionList, disableMenuIds, removedMenuIds); + getMenuItemListLiveData().setValue(items); } private void toParseMenu(Context context, int menuId, List disableMenuIds, List removedMenuIds) { - List permissionList = new ArrayList<>(getPermissionStackMap().values()); + List permissionList = new ArrayList<>(getCurrentNavPermissionCacheMap().values()); List items = parseMenu(context, menuId, permissionList, disableMenuIds, removedMenuIds); getMenuItemListLiveData().setValue(items); } @@ -693,6 +712,7 @@ private List parseMenu(Context context, int menuId, List item.getItemId() != R.id.unstar).collect(Collectors.toList()); } @@ -753,41 +773,20 @@ private List parseMenu(Context context, int menuId, List> getLoadRepoPermissionFromRemoteSingle(String repoId) { - Single single = HttpIO.getCurrentInstance().execute(RepoService.class).getCustomSharePermissions(repoId); - return single.flatMap(new Function>>() { - @Override - public SingleSource> apply(PermissionListWrapperModel wrapperModel) throws Exception { + private List inflateMenu(Context context, int rid) { + ActionMenu menu = new ActionMenu(context); - List list = CollectionUtils.newArrayList(); + MenuInflater inflater = new MenuInflater(context); + inflater.inflate(rid, menu); - for (PermissionWrapperModel model : wrapperModel.permission_list) { - list.add(new PermissionEntity(repoId, model)); - } + List items = new ArrayList<>(menu.size()); + for (int i = 0; i < menu.size(); i++) { + items.add(menu.getItem(i)); + } - Completable insertCompletable = AppDatabase.getInstance().permissionDAO().insertAllAsync(list); - Single insertAllSingle = insertCompletable.toSingleDefault(0L); - return insertAllSingle.flatMap(new Function>>() { - @Override - public SingleSource> apply(Long aLong) throws Exception { - SLogs.d("The list has been inserted into the local database"); - return Single.just(list); - } - }); - } - }); + return items; } - private void loadPermissionFromRemote(String repoId) { - Single> r = getLoadRepoPermissionFromRemoteSingle(repoId); - - addSingleDisposable(r, new Consumer>() { - @Override - public void accept(List list) throws Exception { - SLogs.e("permission has been loaded"); - } - }); - } public void multiStarOrNot(List selectedList, boolean isStar) { if (CollectionUtils.isEmpty(selectedList)) { @@ -852,7 +851,7 @@ private Flowable getStarSingle(String repoId, String path, boolean isStar) { requestDataMap.put("repo_id", repoId); requestDataMap.put("path", path); - Map bodyMap = generateRequestBody(requestDataMap); + Map bodyMap = genRequestBody(requestDataMap); Single single = HttpIO.getCurrentInstance().execute(StarredService.class).star(bodyMap); return single.toFlowable(); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/ScrollState.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/ScrollState.java index 9951c5e6e..be6c0d0d4 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/ScrollState.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/ScrollState.java @@ -8,4 +8,12 @@ public ScrollState(int index, int top) { this.index = index; this.top = top; } + + @Override + public String toString() { + return "ScrollState{" + + "index=" + index + + ", top=" + top + + '}'; + } } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/AccountViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/AccountViewHolder.java similarity index 90% rename from app/src/main/java/com/seafile/seadroid2/ui/repo/AccountViewHolder.java rename to app/src/main/java/com/seafile/seadroid2/ui/repo/vh/AccountViewHolder.java index 1fb894211..c6ef02bb0 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/AccountViewHolder.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/AccountViewHolder.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.ui.repo; +package com.seafile.seadroid2.ui.repo.vh; import androidx.annotation.NonNull; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/DirentGalleryViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentGalleryViewHolder.java similarity index 80% rename from app/src/main/java/com/seafile/seadroid2/ui/repo/DirentGalleryViewHolder.java rename to app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentGalleryViewHolder.java index dddccb62d..8d74de816 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/DirentGalleryViewHolder.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentGalleryViewHolder.java @@ -1,9 +1,8 @@ -package com.seafile.seadroid2.ui.repo; +package com.seafile.seadroid2.ui.repo.vh; import androidx.annotation.NonNull; import com.seafile.seadroid2.databinding.ItemDirentGalleryBinding; -import com.seafile.seadroid2.databinding.ItemDirentGridBinding; import com.seafile.seadroid2.ui.base.viewholder.BaseViewHolder; public class DirentGalleryViewHolder extends BaseViewHolder { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/DirentGridViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentGridViewHolder.java similarity index 80% rename from app/src/main/java/com/seafile/seadroid2/ui/repo/DirentGridViewHolder.java rename to app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentGridViewHolder.java index 0ce06f4c5..7fb9c6764 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/DirentGridViewHolder.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentGridViewHolder.java @@ -1,8 +1,7 @@ -package com.seafile.seadroid2.ui.repo; +package com.seafile.seadroid2.ui.repo.vh; import androidx.annotation.NonNull; -import com.seafile.seadroid2.databinding.ItemDirentBinding; import com.seafile.seadroid2.databinding.ItemDirentGridBinding; import com.seafile.seadroid2.ui.base.viewholder.BaseViewHolder; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/DirentViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentViewHolder.java similarity index 90% rename from app/src/main/java/com/seafile/seadroid2/ui/repo/DirentViewHolder.java rename to app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentViewHolder.java index 11f360458..6a8bceb41 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/DirentViewHolder.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/DirentViewHolder.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.ui.repo; +package com.seafile.seadroid2.ui.repo.vh; import androidx.annotation.NonNull; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/RepoViewHolder.java similarity index 90% rename from app/src/main/java/com/seafile/seadroid2/ui/repo/RepoViewHolder.java rename to app/src/main/java/com/seafile/seadroid2/ui/repo/vh/RepoViewHolder.java index a754fa84e..8645bc86b 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/RepoViewHolder.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/RepoViewHolder.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.ui.repo; +package com.seafile.seadroid2.ui.repo.vh; import androidx.annotation.NonNull; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/repo/UnsupportedViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/UnsupportedViewHolder.java similarity index 90% rename from app/src/main/java/com/seafile/seadroid2/ui/repo/UnsupportedViewHolder.java rename to app/src/main/java/com/seafile/seadroid2/ui/repo/vh/UnsupportedViewHolder.java index 0481652b6..f99dd89ca 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/repo/UnsupportedViewHolder.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/repo/vh/UnsupportedViewHolder.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.ui.repo; +package com.seafile.seadroid2.ui.repo.vh; import androidx.annotation.NonNull; diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/DocsCommentService.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/DocsCommentService.java new file mode 100644 index 000000000..f411fe28e --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/DocsCommentService.java @@ -0,0 +1,63 @@ +package com.seafile.seadroid2.ui.sdoc; + +import com.seafile.seadroid2.framework.data.model.ResultModel; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsCommentWrapperModel; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsCommentsWrapperModel; +import com.seafile.seadroid2.framework.data.model.docs_comment.DocsUploadResultModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileDetailModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileRecordWrapperModel; +import com.seafile.seadroid2.framework.data.model.sdoc.MetadataConfigModel; +import com.seafile.seadroid2.framework.data.model.sdoc.SDocOutlineWrapperModel; +import com.seafile.seadroid2.framework.data.model.user.UserWrapperModel; + +import java.util.Map; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Part; +import retrofit2.http.PartMap; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface DocsCommentService { + @GET("api2/repos/{repo_id}/file/detail/") + Single getFileDetail(@Path("repo_id") String repoId, @Query("p") String path); + + @GET("api/v2.1/repos/{repo_id}/related-users/") + Single getRelatedUsers(@Path("repo_id") String repoId); + + @GET("api/v2.1/repos/{repo_id}/metadata/") + Single getMetadata(@Path("repo_id") String repoId); + + @GET("api/v2.1/repos/{repo_id}/metadata/record/") + Single getRecords(@Path("repo_id") String repoId, @Query("parent_dir") String parentDir, @Query("name") String name, @Query("file_name") String fileName); + + // + @GET("api/v1/docs/{uuid}/comment/") + Single getComments(@Path("uuid") String uuid); + + @GET("api/v1/docs/{uuid}/") + Single getElements(@Path("uuid") String uuid); + + @Multipart + @POST("api/v2.1/seadoc/upload-image/{sdoc_uuid}/") + Flowable upload(@Path("sdoc_uuid") String docUid, @Part() MultipartBody.Part file, @PartMap Map map); + + @POST("api/v1/docs/{sdoc_uuid}/comment/") + Single postComment(@Path("sdoc_uuid") String uuid, @Body Map map); + + @PUT("api/v1/docs/{sdoc_uuid}/comment/{comment_id}/") + Single markResolved(@Path("sdoc_uuid") String docUid, @Path("comment_id") long commentId, @Body Map map); + + @DELETE("api/v1/docs/{sdoc_uuid}/comment/{comment_id}/") + Single delete(@Path("sdoc_uuid") String docUid, @Path("comment_id") long commentId); + +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocService.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocService.java deleted file mode 100644 index 8010a80a4..000000000 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocService.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.seafile.seadroid2.ui.sdoc; - -import com.seafile.seadroid2.framework.data.model.sdoc.MetadataConfigModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocCommentWrapperModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocDetailModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocRecordWrapperModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocWrapperModel; -import com.seafile.seadroid2.framework.data.model.user.UserWrapperModel; - -import io.reactivex.Single; -import retrofit2.http.GET; -import retrofit2.http.Path; -import retrofit2.http.Query; - -public interface SDocService { - @GET("api2/repos/{repo_id}/file/detail/") - Single getFileDetail(@Path("repo_id") String repoId, @Query("p") String path); - - @GET("api/v2.1/repos/{repo_id}/related-users/") - Single getRelatedUsers(@Path("repo_id") String repoId); - - @GET("api/v2.1/repos/{repo_id}/metadata/") - Single getMetadata(@Path("repo_id") String repoId); - - @GET("api/v2.1/repos/{repo_id}/metadata/record/") - Single getRecords(@Path("repo_id") String repoId, @Query("parent_dir") String parentDir, @Query("name") String name); - - // - @GET("api/v1/docs/{uuid}/comment/") - Single getComments(@Path("uuid") String uuid); - - @GET("api/v1/docs/{uuid}/") - Single getElements(@Path("uuid") String uuid); -} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocViewModel.java index d0e157108..602210998 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocViewModel.java @@ -8,79 +8,88 @@ import com.blankj.utilcode.util.CollectionUtils; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.framework.data.model.sdoc.FileDetailModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileProfileConfigModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileRecordWrapperModel; import com.seafile.seadroid2.framework.data.model.sdoc.MetadataConfigModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocCommentModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocCommentWrapperModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocDetailModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocModel; +import com.seafile.seadroid2.framework.data.model.sdoc.OutlineItemModel; +import com.seafile.seadroid2.framework.data.model.sdoc.SDocOutlineWrapperModel; import com.seafile.seadroid2.framework.data.model.sdoc.SDocPageOptionsModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocProfileConfigModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocRecordWrapperModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocWrapperModel; import com.seafile.seadroid2.framework.data.model.user.UserWrapperModel; import com.seafile.seadroid2.framework.http.HttpIO; import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.framework.util.StringUtils; import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; -import java.util.Comparator; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import io.reactivex.Single; +import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function3; public class SDocViewModel extends BaseViewModel { - private final MutableLiveData _fileProfileConfigLiveData = new MutableLiveData<>(); - private final MutableLiveData _sdocRecordLiveData = new MutableLiveData<>(); - private final MutableLiveData _sdocCommentLiveData = new MutableLiveData<>(); - private final MutableLiveData> _sdocElementListLiveData = new MutableLiveData<>(); + private final MutableLiveData _fileProfileConfigLiveData = new MutableLiveData<>(); + private final MutableLiveData _fileRecordLiveData = new MutableLiveData<>(); + private final MutableLiveData> _sdocElementListLiveData = new MutableLiveData<>(); - public MutableLiveData getFileDetailLiveData() { + public MutableLiveData getFileDetailLiveData() { return _fileProfileConfigLiveData; } - public MutableLiveData getSdocRecordLiveData() { - return _sdocRecordLiveData; + public MutableLiveData getSdocRecordLiveData() { + return _fileRecordLiveData; } - public MutableLiveData getSdocCommentLiveData() { - return _sdocCommentLiveData; - } - - public MutableLiveData> getSdocElementLiveData() { + public MutableLiveData> getSdocElementLiveData() { return _sdocElementListLiveData; } - public void initSDocConfig(String repoId, String path) { - Single userSingle = HttpIO.getCurrentInstance().execute(SDocService.class).getRelatedUsers(repoId); - Single metadataSingle = HttpIO.getCurrentInstance().execute(SDocService.class).getMetadata(repoId); - Single detailSingle = HttpIO.getCurrentInstance().execute(SDocService.class).getFileDetail(repoId, path); + public void loadFileDetail(String repoId, String path, boolean isMetadataEnable) { - Single s = Single.zip(detailSingle, userSingle, metadataSingle, new Function3() { - @Override - public SDocProfileConfigModel apply(SDocDetailModel docDetailModel, UserWrapperModel userWrapperModel, MetadataConfigModel metadataConfigModel) throws Exception { - SDocProfileConfigModel configModel = new SDocProfileConfigModel(); - configModel.setDetail(docDetailModel); - configModel.setUsers(userWrapperModel); - configModel.setMetadata(metadataConfigModel); - return configModel; - } - }); + Single userSingle = HttpIO.getCurrentInstance().execute(DocsCommentService.class).getRelatedUsers(repoId); + + //Even if isMetadataEnable is enabled, you still need to check whether the enable field of MetadataConfigModel is available + Single metadataSingle = HttpIO.getCurrentInstance().execute(DocsCommentService.class).getMetadata(repoId); + Single detailSingle = HttpIO.getCurrentInstance().execute(DocsCommentService.class).getFileDetail(repoId, path); + + Single s; + if (isMetadataEnable) { + s = Single.zip(detailSingle, userSingle, metadataSingle, new Function3() { + @Override + public FileProfileConfigModel apply(FileDetailModel fileDetailModel, UserWrapperModel userWrapperModel, MetadataConfigModel metadataConfigModel) throws Exception { + FileProfileConfigModel configModel = new FileProfileConfigModel(); + configModel.setDetail(fileDetailModel); + configModel.setUsers(userWrapperModel); + configModel.setMetadataConfigModel(metadataConfigModel); + return configModel; + } + }); + } else { + s = Single.zip(detailSingle, userSingle, new BiFunction() { + @Override + public FileProfileConfigModel apply(FileDetailModel fileDetailModel, UserWrapperModel userWrapperModel) throws Exception { + FileProfileConfigModel configModel = new FileProfileConfigModel(); + configModel.setDetail(fileDetailModel); + configModel.setUsers(userWrapperModel); + return configModel; + } + }); + } - addSingleDisposable(s, new Consumer() { + addSingleDisposable(s, new Consumer() { @Override - public void accept(SDocProfileConfigModel sDocProfileConfigModel) throws Exception { - getFileDetailLiveData().setValue(sDocProfileConfigModel); + public void accept(FileProfileConfigModel fileProfileConfigModel) throws Exception { + getFileDetailLiveData().setValue(fileProfileConfigModel); } }); } - public void getRecords(String repoId, String path) { + public void loadRecords(String repoId, String path) { if (TextUtils.isEmpty(path) || TextUtils.equals("/", path)) { return; } @@ -101,21 +110,26 @@ public void getRecords(String repoId, String path) { name = path; } - Single single = HttpIO.getCurrentInstance().execute(SDocService.class).getRecords(repoId, parent_dir, name); - addSingleDisposable(single, new Consumer() { + if (TextUtils.isEmpty(parent_dir)) { + parent_dir = "/"; + } + + Single single = HttpIO.getCurrentInstance().execute(DocsCommentService.class).getRecords(repoId, parent_dir, name, name); + addSingleDisposable(single, new Consumer() { @Override - public void accept(SDocRecordWrapperModel sDocRecordWrapperModel) throws Exception { - getSdocRecordLiveData().setValue(sDocRecordWrapperModel); + public void accept(FileRecordWrapperModel fileRecordWrapperModel) throws Exception { + getSdocRecordLiveData().setValue(fileRecordWrapperModel); } }); } public static final List _AllowedElementTypes = List.of("header1", "header2", "header3"); - public void getSDocElements(SDocPageOptionsModel pageOptionsModel) { + public void loadSdocElements(SDocPageOptionsModel pageOptionsModel) { if (TextUtils.isEmpty(pageOptionsModel.seadocServerUrl)) { return; } + getRefreshLiveData().setValue(true); String sdocServerUrl = pageOptionsModel.seadocServerUrl; if (!sdocServerUrl.endsWith("/")) { @@ -127,19 +141,19 @@ public void getSDocElements(SDocPageOptionsModel pageOptionsModel) { partialAccount.setToken(pageOptionsModel.seadocAccessToken); partialAccount.setServer(sdocServerUrl); - Single single = HttpIO.getInstanceByAccount(partialAccount).execute(SDocService.class).getElements(pageOptionsModel.docUuid); - addSingleDisposable(single, new Consumer() { + Single single = HttpIO.getInstanceByAccount(partialAccount).execute(DocsCommentService.class).getElements(pageOptionsModel.docUuid); + addSingleDisposable(single, new Consumer() { @Override - public void accept(SDocWrapperModel wrapperModel) throws Exception { + public void accept(SDocOutlineWrapperModel wrapperModel) throws Exception { if (wrapperModel == null || wrapperModel.elements == null) { getSdocElementLiveData().setValue(null); return; } - List newList = wrapperModel.elements.stream().filter(new Predicate() { + List newList = wrapperModel.elements.stream().filter(new Predicate() { @Override - public boolean test(SDocModel sDocModel) { + public boolean test(OutlineItemModel sDocModel) { if (!_AllowedElementTypes.contains(sDocModel.type)) { return false; } @@ -150,9 +164,9 @@ public boolean test(SDocModel sDocModel) { return true; } - }).map(new Function() { + }).map(new Function() { @Override - public SDocModel apply(SDocModel sDocModel) { + public OutlineItemModel apply(OutlineItemModel sDocModel) { if (!TextUtils.isEmpty(sDocModel.text)) { return sDocModel; } @@ -162,7 +176,7 @@ public SDocModel apply(SDocModel sDocModel) { } String text = ""; - for (SDocModel child : sDocModel.children) { + for (OutlineItemModel child : sDocModel.children) { if (!TextUtils.isEmpty(child.text)) { String nt = StringUtils.trim(child.text, "\n").trim(); text = text.concat(nt); @@ -174,50 +188,14 @@ public SDocModel apply(SDocModel sDocModel) { }).collect(Collectors.toList()); getSdocElementLiveData().setValue(newList); + getRefreshLiveData().setValue(false); } }, new Consumer() { @Override public void accept(Throwable throwable) throws Exception { SLogs.e(throwable); + getRefreshLiveData().setValue(false); } }); - - } - - public void getSDocComments(SDocPageOptionsModel pageOptionsModel) { - if (TextUtils.isEmpty(pageOptionsModel.seadocServerUrl)) { - return; - } - - String sdocServerUrl = pageOptionsModel.seadocServerUrl; - if (!sdocServerUrl.endsWith("/")) { - sdocServerUrl = sdocServerUrl + "/"; - } - - Account curAccount = SupportAccountManager.getInstance().getCurrentAccount(); - Account partialAccount = CloneUtils.deepClone(curAccount, Account.class); - partialAccount.setToken(pageOptionsModel.seadocAccessToken); - partialAccount.setServer(sdocServerUrl); - - Single commentSingle = HttpIO.getInstanceByAccount(partialAccount).execute(SDocService.class).getComments(pageOptionsModel.docUuid); - addSingleDisposable(commentSingle, new Consumer() { - @Override - public void accept(SDocCommentWrapperModel sDocCommentWrapperModel) throws Exception { - sDocCommentWrapperModel.comments = sDocCommentWrapperModel.comments.stream().sorted(new Comparator() { - @Override - public int compare(SDocCommentModel o1, SDocCommentModel o2) { - return -o1.created_at.compareTo(o2.created_at); - } - }).collect(Collectors.toList()); - - getSdocCommentLiveData().setValue(sDocCommentWrapperModel); - } - }, new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - SLogs.e(throwable); - } - }); - } } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocWebViewActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocWebViewActivity.java index 39cf80e1e..2b3430698 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocWebViewActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/SDocWebViewActivity.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.text.TextUtils; import android.view.View; +import android.webkit.ConsoleMessage; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebView; @@ -22,26 +23,28 @@ import com.blankj.utilcode.util.ActivityUtils; import com.blankj.utilcode.util.GsonUtils; import com.blankj.utilcode.util.ToastUtils; +import com.github.lzyzsd.jsbridge.CallBackFunction; +import com.seafile.seadroid2.R; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; -import com.seafile.seadroid2.annotation.Unstable; import com.seafile.seadroid2.databinding.ActivitySeaWebviewProBinding; import com.seafile.seadroid2.databinding.ToolbarActionbarProgressBarBinding; import com.seafile.seadroid2.enums.WebViewPreviewType; +import com.seafile.seadroid2.framework.data.model.sdoc.FileProfileConfigModel; +import com.seafile.seadroid2.framework.data.model.sdoc.FileRecordWrapperModel; +import com.seafile.seadroid2.framework.data.model.sdoc.OutlineItemModel; import com.seafile.seadroid2.framework.data.model.sdoc.SDocPageOptionsModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocProfileConfigModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocRecordWrapperModel; import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.framework.util.StringUtils; +import com.seafile.seadroid2.listener.OnItemClickListener; import com.seafile.seadroid2.ui.base.BaseActivityWithVM; -import com.seafile.seadroid2.ui.sdoc.comments.SDocCommentsActivity; -import com.seafile.seadroid2.ui.sdoc.directory.SDocDirectoryDialog; -import com.seafile.seadroid2.ui.sdoc.profile.SDocProfileDialog; -import com.seafile.seadroid2.ui.webview.SeaWebViewActivity; +import com.seafile.seadroid2.ui.docs_comment.DocsCommentsActivity; +import com.seafile.seadroid2.ui.file_profile.FileProfileDialog; +import com.seafile.seadroid2.ui.sdoc.outline.SDocOutlineDialog; +import com.seafile.seadroid2.view.webview.OnWebPageListener; import com.seafile.seadroid2.view.webview.PreloadWebView; import com.seafile.seadroid2.view.webview.SeaWebView; -@Unstable public class SDocWebViewActivity extends BaseActivityWithVM { private ActivitySeaWebviewProBinding binding; private ToolbarActionbarProgressBarBinding toolBinding; @@ -51,13 +54,14 @@ public class SDocWebViewActivity extends BaseActivityWithVM { private String path; private String targetUrl; - private SDocProfileConfigModel configModel; + private FileProfileConfigModel configModel; + private SDocPageOptionsModel pageOptionsData; /** * not support, please use SeaWebViewActivity instead */ public static void openSdoc(Context context, String repoName, String repoID, String path) { - Intent intent = new Intent(context, SeaWebViewActivity.class); + Intent intent = new Intent(context, SDocWebViewActivity.class); intent.putExtra("previewType", WebViewPreviewType.SDOC.name()); intent.putExtra("repoName", repoName); intent.putExtra("repoID", repoID); @@ -74,16 +78,22 @@ protected void onCreate(Bundle savedInstanceState) { toolBinding = ToolbarActionbarProgressBarBinding.bind(binding.toolProgressBar.getRoot()); - init(); initUI(); + init(); + initViewModel(); + mWebView.setOnWebPageListener(new OnWebPageListener() { + @Override + public void onPageFinished(WebView view, String url) { + canLoadPageConfigData(); + } + }); + //let's go mWebView.load(targetUrl); - - getViewModel().initSDocConfig(repoId, path); } private void init() { @@ -101,6 +111,7 @@ private void init() { WebViewPreviewType previewTypeEnum = WebViewPreviewType.valueOf(previewType); if (previewTypeEnum == WebViewPreviewType.SDOC) { + String repoName = intent.getStringExtra("repoName"); repoId = intent.getStringExtra("repoID"); path = intent.getStringExtra("filePath"); @@ -118,7 +129,6 @@ private void init() { } else { throw new IllegalArgumentException("previewType is not SDOC"); } - } private void initUI() { @@ -131,21 +141,22 @@ private void initUI() { mWebView = PreloadWebView.getInstance().getWebView(this); + if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { WebSettingsCompat.setAlgorithmicDarkeningAllowed(mWebView.getSettings(), true); } + //chrome client + mWebView.setWebChromeClient(mWebChromeClient); + NestedScrollView.LayoutParams ll = new NestedScrollView.LayoutParams(-1, -1); mWebView.setLayoutParams(ll); binding.nsv.addView(mWebView); - //chrome client - mWebView.setWebChromeClient(mWebChromeClient); - - binding.sdocDirectory.setOnClickListener(new View.OnClickListener() { + binding.sdocOutline.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showDirectoryDialog(); + showOutlineDialog(); } }); binding.sdocProfile.setOnClickListener(new View.OnClickListener() { @@ -174,32 +185,46 @@ public void handleOnBackPressed() { } private void initViewModel() { - getViewModel().getFileDetailLiveData().observe(this, new Observer() { + getViewModel().getFileDetailLiveData().observe(this, new Observer() { @Override - public void onChanged(SDocProfileConfigModel sDocProfileConfigModel) { - configModel = sDocProfileConfigModel; + public void onChanged(FileProfileConfigModel fileProfileConfigModel) { + configModel = fileProfileConfigModel; hideProgressBar(); } }); - getViewModel().getSdocRecordLiveData().observe(this, new Observer() { + + getViewModel().getSdocRecordLiveData().observe(this, new Observer() { @Override - public void onChanged(SDocRecordWrapperModel sDocRecordWrapperModel) { - SDocProfileDialog dialog = SDocProfileDialog.newInstance(configModel.detail, sDocRecordWrapperModel, configModel.users.user_list); - dialog.show(getSupportFragmentManager(), SDocProfileDialog.class.getSimpleName()); + public void onChanged(FileRecordWrapperModel fileRecordWrapperModel) { + FileProfileDialog dialog = FileProfileDialog.newInstance(configModel.detail, fileRecordWrapperModel, configModel.users.user_list, false); + dialog.show(getSupportFragmentManager(), FileProfileDialog.class.getSimpleName()); } }); } - private void showDirectoryDialog() { - getSDocConfigData(new Consumer() { + private void showOutlineDialog() { + readSDocOutlineList(new Consumer() { @Override - public void accept(SDocPageOptionsModel model) { - if (TextUtils.isEmpty(model.seadocServerUrl) || TextUtils.isEmpty(model.docUuid)) { - return; - } + public void accept(String s) { + SDocOutlineDialog dialog = SDocOutlineDialog.newInstance(s); + dialog.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(OutlineItemModel outlineItemModel, int position) { - SDocDirectoryDialog dialog = SDocDirectoryDialog.newInstance(model); - dialog.show(getSupportFragmentManager(), SDocDirectoryDialog.class.getSimpleName()); + callJsOutline(outlineItemModel); + } + }); + dialog.show(getSupportFragmentManager(), SDocOutlineDialog.class.getSimpleName()); + } + }); + } + + private void callJsOutline(OutlineItemModel outlineItemModel) { + String param = GsonUtils.toJson(outlineItemModel); + mWebView.callJsFunction("sdoc.outline.data.select", param, new CallBackFunction() { + @Override + public void onCallBack(String data) { + SLogs.e(data); } }); } @@ -209,24 +234,38 @@ private void showProfileDialog() { return; } - if (configModel.metadata.enabled) { - getViewModel().getRecords(repoId, path); - } else { - SDocProfileDialog dialog = SDocProfileDialog.newInstance(configModel.detail, configModel.users.user_list); - dialog.show(getSupportFragmentManager(), SDocProfileDialog.class.getSimpleName()); - } + readSDocPageOptionsData(new Consumer() { + @Override + public void accept(SDocPageOptionsModel model) { + if (model.enableMetadataManagement) { + if (configModel != null && configModel.metadataConfigModel != null && configModel.metadataConfigModel.enabled) { + getViewModel().loadRecords(repoId, path); + } else if (configModel != null) { + FileProfileDialog dialog = FileProfileDialog.newInstance(configModel.detail, configModel.users.user_list); + dialog.show(getSupportFragmentManager(), FileProfileDialog.class.getSimpleName()); + } + } else { + FileProfileDialog dialog = FileProfileDialog.newInstance(configModel.detail, configModel.users.user_list); + dialog.show(getSupportFragmentManager(), FileProfileDialog.class.getSimpleName()); + } + } + }); } private void showCommentsActivity() { - getSDocConfigData(new Consumer() { + readSDocPageOptionsData(new Consumer() { @Override public void accept(SDocPageOptionsModel model) { - SDocCommentsActivity.start(SDocWebViewActivity.this, model); + DocsCommentsActivity.start(SDocWebViewActivity.this, model); } }); } - private void getSDocConfigData(Consumer continuation) { + private void readSDocPageOptionsData(Consumer continuation) { + if (pageOptionsData != null) { + continuation.accept(pageOptionsData); + return; + } String js = "(function() {" + " if (window.app && window.app.pageOptions) {" + @@ -238,20 +277,79 @@ private void getSDocConfigData(Consumer continuation) { mWebView.evaluateJavascript(js, new ValueCallback() { @Override public void onReceiveValue(String value) { - SLogs.e(value); if (!TextUtils.isEmpty(value)) { value = StringUtils.deString(value).replace("\\", ""); - SDocPageOptionsModel configModel1 = GsonUtils.fromJson(value, SDocPageOptionsModel.class); - if (configModel1 != null) { - continuation.accept(configModel1); - SLogs.d("获取的 PageOption 对象数据: " + configModel1); + pageOptionsData = GsonUtils.fromJson(value, SDocPageOptionsModel.class); + if (pageOptionsData != null) { + continuation.accept(pageOptionsData); } else { - SLogs.d("获取的 PageOption 对象数据是空的"); + SLogs.e("read sodc page options data from web, an exception occurred in the parsing data"); + SLogs.e(value); + ToastUtils.showShort(R.string.unknow_error); } } else { + SLogs.e("read sodc page options data from web: " + value); + ToastUtils.showShort(R.string.unknow_error); + } + } + }); + } + + private void readSDocOutlineList(Consumer continuation) { + String js = + "(function() {" + + " if (window.seadroid && window.seadroid.outlines) {" + + " return JSON.stringify(window.seadroid.outlines);" + + " } else {" + + " return null;" + + " }" + + "})();"; + mWebView.evaluateJavascript(js, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (TextUtils.isEmpty(value)) { + SLogs.e(value); + ToastUtils.showShort(R.string.empty_data); + continuation.accept(value); + return; + } + + value = StringUtils.deStringReturnNonNull(value).replace("\\", ""); + if (continuation != null) { + continuation.accept(value); + } + } + }); + } + + @Deprecated + private void readSeafileTokenData(Consumer continuation) { + String js = + "(function() {" + + " if (window.seafile && window.seafile.accessToken) {" + + " return JSON.stringify(window.seafile.accessToken);" + + " } else {" + + " return null;" + + " }" + + "})();"; + mWebView.evaluateJavascript(js, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + SLogs.e(value); + if (TextUtils.isEmpty(value)) { SLogs.d("doc uuid is empty."); - ToastUtils.showShort("doc uuid is empty."); + ToastUtils.showShort("outline is empty."); + return; + } + + value = StringUtils.deStringReturnNonNull(value).replace("\\", ""); + value = StringUtils.deStringReturnNonNull(value); + + if (continuation != null) { + continuation.accept(value); + } else { + } } }); @@ -275,7 +373,31 @@ public void onProgressChanged(WebView view, int newProgress) { @Override public void onReceivedTitle(WebView view, String title) { super.onReceivedTitle(view, title); - toolBinding.toolbarActionbar.setTitle(title); +// toolBinding.toolbarActionbar.setTitle(title); + } + + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + if (consoleMessage != null) { + switch (consoleMessage.messageLevel()) { + case ERROR: + SLogs.e("web e log: line: " + consoleMessage.lineNumber() + ", message: " + consoleMessage.message()); + break; + case DEBUG: + SLogs.d("web d log: line: " + consoleMessage.lineNumber() + ", message: " + consoleMessage.message()); + break; + case WARNING: + SLogs.w("web w log: line: " + consoleMessage.lineNumber() + ", message: " + consoleMessage.message()); + break; + case TIP: + SLogs.i("web i log: line: " + consoleMessage.lineNumber() + ", message: " + consoleMessage.message()); + break; + default: + SLogs.e("web default log: line: " + consoleMessage.lineNumber() + ", message: " + consoleMessage.message()); + break; + } + } + return super.onConsoleMessage(consoleMessage); } }; @@ -297,6 +419,16 @@ private void hideProgressBar() { } } + private void canLoadPageConfigData() { + readSDocPageOptionsData(new Consumer() { + @Override + public void accept(SDocPageOptionsModel model) { + getViewModel().loadFileDetail(repoId, path, model.enableMetadataManagement); + } + }); + + } + @Override protected void onPause() { super.onPause(); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentViewHolder.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentViewHolder.java deleted file mode 100644 index e23915268..000000000 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentViewHolder.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.seafile.seadroid2.ui.sdoc.comments; - -import androidx.annotation.NonNull; - -import com.seafile.seadroid2.databinding.ItemSdocCommentBinding; -import com.seafile.seadroid2.ui.base.viewholder.BaseViewHolder; - -public class SDocCommentViewHolder extends BaseViewHolder { - - public ItemSdocCommentBinding binding; - - public SDocCommentViewHolder(@NonNull ItemSdocCommentBinding binding) { - super(binding.getRoot()); - - this.binding = binding; - } -} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentsActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentsActivity.java deleted file mode 100644 index 20e40e77d..000000000 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/comments/SDocCommentsActivity.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.seafile.seadroid2.ui.sdoc.comments; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.view.WindowManager; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -import androidx.lifecycle.Observer; -import androidx.recyclerview.widget.LinearLayoutManager; - -import com.chad.library.adapter4.BaseQuickAdapter; -import com.chad.library.adapter4.QuickAdapterHelper; -import com.seafile.seadroid2.R; -import com.seafile.seadroid2.databinding.ActivitySdocCommentBinding; -import com.seafile.seadroid2.databinding.ToolbarActionbarBinding; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocCommentModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocCommentWrapperModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocPageOptionsModel; -import com.seafile.seadroid2.ui.base.BaseActivityWithVM; -import com.seafile.seadroid2.ui.sdoc.SDocViewModel; - -public class SDocCommentsActivity extends BaseActivityWithVM { - private ActivitySdocCommentBinding binding; - private ToolbarActionbarBinding bindingOfToolbar; - - private Toolbar toolbar; - - private SDocCommentAdapter adapter; - private SDocCommentUserAdapter userAdapter; - - private SDocPageOptionsModel pageOptionsModel; - - public static void start(Context context, SDocPageOptionsModel pageModel) { - Intent starter = new Intent(context, SDocCommentsActivity.class); - starter.putExtra("pageOption", pageModel); - context.startActivity(starter); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivitySdocCommentBinding.inflate(getLayoutInflater()); - bindingOfToolbar = ToolbarActionbarBinding.bind(binding.toolbar.getRoot()); - - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - - setContentView(binding.getRoot()); - - if (getIntent() == null || !getIntent().hasExtra("pageOption")) { - throw new IllegalArgumentException("pageOption is null"); - } - - pageOptionsModel = getIntent().getParcelableExtra("pageOption"); - - initView(); - - initViewModel(); - - initAdapter(); - - refreshData(); - } - - private void initView() { - toolbar = bindingOfToolbar.toolbarActionbar; - - toolbar.setTitle(""); - setSupportActionBar(toolbar); - toolbar.setTitle(pageOptionsModel.docName); - - toolbar.setNavigationOnClickListener(v -> { - finish(); - }); - - //refresh listener - binding.swipeRefreshLayout.setOnRefreshListener(this::refreshData); - - binding.rv.setLayoutManager(new LinearLayoutManager(this)); -// // -// LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false); -// binding.rvUserList.setLayoutManager(linearLayoutManager); -// binding.photoView.setOnClickListener(new View.OnClickListener() { -// @Override -// public void onClick(View v) { -// showPickPhotoSheetDialog(true); -// } -// }); -// -// binding.submit.setOnClickListener(new View.OnClickListener() { -// @Override -// public void onClick(View v) { -// submitData(); -// } -// }); -// -// binding.richEditText.setOnRichAtListener(new OnRichAtListener() { -// @Override -// public void onCall(EditText editText) { -// showCollaboratorSelector(editText); -// } -// }); - } - - protected void initViewModel() { - getViewModel().getRefreshLiveData().observe(this, new Observer() { - @Override - public void onChanged(Boolean aBoolean) { - binding.swipeRefreshLayout.setRefreshing(aBoolean); - } - }); - -// getViewModel().getPostCommentLiveData().observe(this, new Observer() { -// @Override -// public void onChanged(TableRowCommentModel model) { -// //remove all -// binding.richEditText.removeAllViews(); -// -// refreshData(true); -// } -// }); - -// getViewModel().getUserListLiveData().observe(this, new Observer>() { -// @Override -// public void onChanged(List relatedUserModels) { -// userAdapter.submitList(relatedUserModels); -// } -// }); - getViewModel().getSdocCommentLiveData().observe(this, new Observer() { - @Override - public void onChanged(SDocCommentWrapperModel sDocCommentWrapperModel) { - adapter.setStateViewEnable(true); - - adapter.submitData(sDocCommentWrapperModel.comments); - } - }); - } - - - private void initAdapter() { -// userAdapter = new SDocCommentUserAdapter(); -// userAdapter.setAnimationEnable(true); -// -// userAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { -// @Override -// public void onClick(@NonNull BaseQuickAdapter baseQuickAdapter, @NonNull View view, int i) { -// if (CollectionUtils.isEmpty(userAdapter.getItems())) { -// return; -// } -// -//// showCollaboratorSelector(); -// } -// }); -// binding.rvUserList.setAdapter(userAdapter); -// -// if (CollectionUtils.isEmpty(strategyModel.participants)) { -// binding.rvUserList.setVisibility(View.GONE); -// } else { -// userAdapter.submitList(strategyModel.participants); -// } - - adapter = new SDocCommentAdapter(); - adapter.setStateViewLayout(this, R.layout.layout_empty); - adapter.setStateViewEnable(false); - adapter.setAnimationEnable(true); - - adapter.addOnItemChildClickListener(R.id.comment_more, new BaseQuickAdapter.OnItemChildClickListener() { - @Override - public void onItemClick(@NonNull BaseQuickAdapter baseQuickAdapter, @NonNull View view, int i) { - - } - }); - - QuickAdapterHelper helper = new QuickAdapterHelper.Builder(adapter).build(); - binding.rv.setAdapter(helper.getAdapter()); - } - - private void refreshData() { - getViewModel().getSDocComments(pageOptionsModel); - } -} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/directory/SDocDirectoryAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/directory/SDocDirectoryAdapter.java deleted file mode 100644 index 98f3505ef..000000000 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/directory/SDocDirectoryAdapter.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.seafile.seadroid2.ui.sdoc.directory; - -import static com.seafile.seadroid2.config.Constants.DP.DP_8; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.seafile.seadroid2.databinding.ItemSdocDirectoryBinding; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocModel; -import com.seafile.seadroid2.ui.base.adapter.BaseAdapter; -import com.seafile.seadroid2.ui.base.viewholder.BaseViewHolder; - -public class SDocDirectoryAdapter extends BaseAdapter { - - private final int _paddingStart = DP_8; - - @NonNull - @Override - protected SDocDirectoryHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) { - ItemSdocDirectoryBinding binding = ItemSdocDirectoryBinding.inflate(LayoutInflater.from(context)); - return new SDocDirectoryHolder(binding); - } - - @Override - protected void onBindViewHolder(@NonNull SDocDirectoryHolder holder, int i, @Nullable SDocModel sDocModel) { - if (sDocModel == null) { - return; - } - - int padding; - if ("header1".equals(sDocModel.type)) { - padding = _paddingStart; - } else if ("header2".equals(sDocModel.type)) { - padding = _paddingStart * 3; - } else if ("header3".equals(sDocModel.type)) { - padding = _paddingStart * 6; - } else { - return; - } - - - holder.binding.title.setPadding(padding, 0, 0, 0); - holder.binding.title.setText(sDocModel.text); - } - - - public static class SDocDirectoryHolder extends BaseViewHolder { - ItemSdocDirectoryBinding binding; - - public SDocDirectoryHolder(@NonNull ItemSdocDirectoryBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } -} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineAdapter.java new file mode 100644 index 000000000..5d98e72f2 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineAdapter.java @@ -0,0 +1,56 @@ +package com.seafile.seadroid2.ui.sdoc.outline; + +import static com.seafile.seadroid2.config.Constants.DP.DP_8; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.seafile.seadroid2.databinding.ItemSdocOutlineBinding; +import com.seafile.seadroid2.framework.data.model.sdoc.OutlineItemModel; +import com.seafile.seadroid2.ui.base.adapter.BaseAdapter; +import com.seafile.seadroid2.ui.base.viewholder.BaseViewHolder; + +public class SDocOutlineAdapter extends BaseAdapter { + + private final int _paddingStart = DP_8; + + @NonNull + @Override + protected SDocOutlineHolder onCreateViewHolder(@NonNull Context context, @NonNull ViewGroup viewGroup, int i) { + ItemSdocOutlineBinding binding = ItemSdocOutlineBinding.inflate(LayoutInflater.from(context), viewGroup, false); + return new SDocOutlineHolder(binding); + } + + @Override + protected void onBindViewHolder(@NonNull SDocOutlineHolder holder, int i, @Nullable OutlineItemModel outlineItemModel) { + if (outlineItemModel == null) { + return; + } + + int padding = 0; + if ("header1".equals(outlineItemModel.type)) { + padding = _paddingStart; + } else if ("header2".equals(outlineItemModel.type)) { + padding = _paddingStart * 3; + } else if ("header3".equals(outlineItemModel.type)) { + padding = _paddingStart * 6; + } + + holder.binding.title.setPadding(padding, 0, 0, 0); + holder.binding.title.setText(outlineItemModel.text); + } + + + public static class SDocOutlineHolder extends BaseViewHolder { + ItemSdocOutlineBinding binding; + + public SDocOutlineHolder(@NonNull ItemSdocOutlineBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineDialog.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineDialog.java new file mode 100644 index 000000000..b22ff9314 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineDialog.java @@ -0,0 +1,158 @@ +package com.seafile.seadroid2.ui.sdoc.outline; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.blankj.utilcode.util.CollectionUtils; +import com.blankj.utilcode.util.GsonUtils; +import com.blankj.utilcode.util.ToastUtils; +import com.chad.library.adapter4.BaseQuickAdapter; +import com.chad.library.adapter4.QuickAdapterHelper; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.gson.reflect.TypeToken; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.databinding.DialogSdocDirectoryBinding; +import com.seafile.seadroid2.framework.data.model.sdoc.OutlineItemModel; +import com.seafile.seadroid2.framework.util.StringUtils; +import com.seafile.seadroid2.listener.OnItemClickListener; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class SDocOutlineDialog extends BottomSheetDialogFragment { + + private DialogSdocDirectoryBinding binding; + private SDocOutlineAdapter adapter; + private List outlineItemList; + private OnItemClickListener onItemClickListener; + + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + this.onItemClickListener = onItemClickListener; + } + + public static SDocOutlineDialog newInstance(String outlineStr) { + Bundle args = new Bundle(); + args.putString("outline_value", outlineStr); + SDocOutlineDialog fragment = new SDocOutlineDialog(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() == null || !getArguments().containsKey("outline_value")) { + throw new IllegalArgumentException("outline_value is null"); + } + + String value = getArguments().getString("outline_value"); + + Type listType = new TypeToken>() { + }.getType(); + + outlineItemList = GsonUtils.fromJson(value, listType); + } + + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = DialogSdocDirectoryBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @SuppressLint("RestrictedApi") + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return new BottomSheetDialog(requireContext()); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + binding.rv.setLayoutManager(new LinearLayoutManager(requireContext())); + + adapter = new SDocOutlineAdapter(); + adapter.setAnimationEnable(true); + adapter.setStateViewLayout(requireContext(), R.layout.layout_empty); + adapter.setStateViewEnable(false); + adapter.addOnItemChildClickListener(R.id.text_container, new BaseQuickAdapter.OnItemChildClickListener() { + @Override + public void onItemClick(@NonNull BaseQuickAdapter baseQuickAdapter, @NonNull View view, int i) { + OutlineItemModel outlineItemModel = adapter.getItems().get(i); + if (onItemClickListener != null) + onItemClickListener.onItemClick(outlineItemModel, i); + + dismiss(); + } + }); + + + QuickAdapterHelper helper = new QuickAdapterHelper.Builder(adapter).build(); + binding.rv.setAdapter(helper.getAdapter()); + + load(); + } + + public static final List _AllowedElementTypes = List.of("header1", "header2", "header3"); + + private void load() { + if (CollectionUtils.isEmpty(outlineItemList)) { + adapter.setStateViewEnable(true); + return; + } + + List newList = outlineItemList.stream().filter(new Predicate() { + @Override + public boolean test(OutlineItemModel sDocModel) { + if (!_AllowedElementTypes.contains(sDocModel.type)) { + return false; + } + + if (TextUtils.isEmpty(sDocModel.text) && CollectionUtils.isEmpty(sDocModel.children)) { + return false; + } + + return true; + } + }).map(new Function() { + @Override + public OutlineItemModel apply(OutlineItemModel sDocModel) { + if (!TextUtils.isEmpty(sDocModel.text)) { + return sDocModel; + } + + if (CollectionUtils.isEmpty(sDocModel.children)) { + return sDocModel; + } + + String text = ""; + for (OutlineItemModel child : sDocModel.children) { + if (!TextUtils.isEmpty(child.text)) { + String nt = StringUtils.trim(child.text, "\n").trim(); + text = text.concat(nt); + } + } + sDocModel.text = text; + return sDocModel; + } + }).collect(Collectors.toList()); + + adapter.setStateViewEnable(true); + adapter.submitList(newList); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/directory/SDocDirectoryDialog.java b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineRemoteDialog.java similarity index 54% rename from app/src/main/java/com/seafile/seadroid2/ui/sdoc/directory/SDocDirectoryDialog.java rename to app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineRemoteDialog.java index 16103d29d..2378d046d 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/sdoc/directory/SDocDirectoryDialog.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/sdoc/outline/SDocOutlineRemoteDialog.java @@ -1,4 +1,4 @@ -package com.seafile.seadroid2.ui.sdoc.directory; +package com.seafile.seadroid2.ui.sdoc.outline; import android.annotation.SuppressLint; import android.app.Dialog; @@ -9,40 +9,35 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; -import com.blankj.utilcode.util.ToastUtils; import com.chad.library.adapter4.BaseQuickAdapter; import com.chad.library.adapter4.QuickAdapterHelper; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import com.google.android.material.internal.ViewUtils; import com.seafile.seadroid2.R; import com.seafile.seadroid2.databinding.DialogSdocDirectoryBinding; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocModel; +import com.seafile.seadroid2.framework.data.model.sdoc.OutlineItemModel; import com.seafile.seadroid2.framework.data.model.sdoc.SDocPageOptionsModel; -import com.seafile.seadroid2.framework.data.model.sdoc.SDocWrapperModel; import com.seafile.seadroid2.ui.sdoc.SDocViewModel; import java.util.List; -public class SDocDirectoryDialog extends BottomSheetDialogFragment { +public class SDocOutlineRemoteDialog extends BottomSheetDialogFragment { private SDocViewModel viewModel; private SDocPageOptionsModel pageOptionsModel; private DialogSdocDirectoryBinding binding; - private SDocDirectoryAdapter adapter; + private SDocOutlineAdapter adapter; - public static SDocDirectoryDialog newInstance(SDocPageOptionsModel pageOptionsModel) { + public static SDocOutlineRemoteDialog newInstance(SDocPageOptionsModel pageOptionsModel) { Bundle args = new Bundle(); args.putParcelable("pageOption", pageOptionsModel); - SDocDirectoryDialog fragment = new SDocDirectoryDialog(); + SDocOutlineRemoteDialog fragment = new SDocOutlineRemoteDialog(); fragment.setArguments(args); return fragment; } @@ -70,29 +65,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(requireContext()); -// bottomSheetDialog.setContentView(binding.getRoot()); -// -// View bottomSheetInternal = bottomSheetDialog.findViewById(R.id.design_bottom_sheet); -//// BottomSheetBehavior.from(bottomSheetInternal).setPeekHeight(800); -// -// View bottomSheetContent = bottomSheetInternal.findViewById(R.id.bottom_drawer_2); -// ViewUtils.doOnApplyWindowInsets(bottomSheetContent, new ViewUtils.OnApplyWindowInsetsListener() { -// @Override -// public WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets, ViewUtils.RelativePadding initialPadding) { -// // Add the inset in the inner NestedScrollView instead to make the edge-to-edge behavior -// // consistent - i.e., the extra padding will only show at the bottom of all content, i.e., -// // only when you can no longer scroll down to show more content. -// ViewCompat.setPaddingRelative(bottomSheetContent, -// initialPadding.start, -// initialPadding.top, -// initialPadding.end, -// initialPadding.bottom + insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom); -// return insets; -// } -// }); - - return bottomSheetDialog; + return new BottomSheetDialog(requireContext()); } @Override @@ -100,15 +73,14 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat super.onViewCreated(view, savedInstanceState); binding.rv.setLayoutManager(new LinearLayoutManager(requireContext())); - adapter = new SDocDirectoryAdapter(); + adapter = new SDocOutlineAdapter(); adapter.setAnimationEnable(true); adapter.setStateViewLayout(requireContext(),R.layout.layout_empty); adapter.setStateViewEnable(false); - adapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { + adapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { @Override - public void onClick(@NonNull BaseQuickAdapter baseQuickAdapter, @NonNull View view, int i) { - SDocModel sDocModel = adapter.getItems().get(i); - ToastUtils.showLong(sDocModel.text); + public void onClick(@NonNull BaseQuickAdapter baseQuickAdapter, @NonNull View view, int i) { + OutlineItemModel outlineItemModel = adapter.getItems().get(i); dismiss(); } }); @@ -122,16 +94,16 @@ public void onClick(@NonNull BaseQuickAdapter baseQuickAdapter, @N } private void initViewModel() { - viewModel.getSdocElementLiveData().observe(getViewLifecycleOwner(), new Observer>() { + viewModel.getSdocElementLiveData().observe(getViewLifecycleOwner(), new Observer>() { @Override - public void onChanged(List sDocModels) { + public void onChanged(List outlineItemModels) { adapter.setStateViewEnable(true); - adapter.submitList(sDocModels); + adapter.submitList(outlineItemModels); } }); } private void load() { - viewModel.getSDocElements(pageOptionsModel); + viewModel.loadSdocElements(pageOptionsModel); } } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/search/Search2Activity.java b/app/src/main/java/com/seafile/seadroid2/ui/search/Search2Activity.java index 7e5265d6e..3709a375a 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/search/Search2Activity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/search/Search2Activity.java @@ -33,7 +33,6 @@ import com.chad.library.adapter4.loadState.LoadState; import com.chad.library.adapter4.loadState.trailing.TrailingLoadStateAdapter; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.firebase.analytics.FirebaseAnalytics; import com.seafile.seadroid2.R; import com.seafile.seadroid2.SeafException; import com.seafile.seadroid2.account.Account; @@ -49,7 +48,7 @@ import com.seafile.seadroid2.ui.base.adapter.LogicLoadMoreAdapter; import com.seafile.seadroid2.ui.file.FileActivity; import com.seafile.seadroid2.ui.main.MainActivity; -import com.seafile.seadroid2.ui.media.image_preview.ImagePreviewActivity; +import com.seafile.seadroid2.ui.media.image_preview2.CarouselImagePreviewActivity; import com.seafile.seadroid2.ui.media.player.exoplayer.CustomExoVideoPlayerActivity; import com.seafile.seadroid2.ui.sdoc.SDocWebViewActivity; import com.seafile.seadroid2.view.TipsViews; @@ -66,6 +65,7 @@ /** * Search Activity */ +@Deprecated public class Search2Activity extends BaseActivityWithVM implements Toolbar.OnMenuItemClickListener { private ActivitySearch2Binding binding; @@ -96,12 +96,6 @@ protected void onCreate(Bundle savedInstanceState) { handleIntent(getIntent()); - - //firebase - event -login - Bundle eventBundle = new Bundle(); - eventBundle.putString(FirebaseAnalytics.Param.METHOD, Search2Activity.class.getSimpleName()); - FirebaseAnalytics.getInstance(this).logEvent(FirebaseAnalytics.Event.SEARCH, eventBundle); - } private void initView(Bundle bundle) { @@ -380,7 +374,8 @@ private void open(RepoModel repoModel, SearchModel searchedFile, String fileName } else if (Utils.isViewableImage(fileName) && !repoModel.encrypted) { // Encrypted repo does not support gallery, // because pic thumbnail under encrypted repo was not supported at the server side - ImagePreviewActivity.startThisFromSearch(this, searchedFile); + Intent intent = CarouselImagePreviewActivity.startThisFromSearch(this, searchedFile); + startActivity(intent); } else if (Utils.isVideoFile(fileName)) { // is video file final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setItems(R.array.video_download_array, new DialogInterface.OnClickListener() { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/search/SearchService.java b/app/src/main/java/com/seafile/seadroid2/ui/search/SearchService.java index e05461f7b..5b3387e79 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/search/SearchService.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/search/SearchService.java @@ -12,6 +12,7 @@ public interface SearchService { + //search_ftypes @GET("api2/search/") Single search(@Query("search_repo") String repoId, @Query("q") String q, diff --git a/app/src/main/java/com/seafile/seadroid2/ui/selector/ObjSelectorActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/selector/ObjSelectorActivity.java index 0bd581ed2..bbc6a06f0 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/selector/ObjSelectorActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/selector/ObjSelectorActivity.java @@ -1,5 +1,6 @@ package com.seafile.seadroid2.ui.selector; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.MenuItem; @@ -19,6 +20,7 @@ import com.github.panpf.recycler.sticky.StickyItemDecoration; import com.seafile.seadroid2.R; import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.config.AbsLayoutItemType; import com.seafile.seadroid2.context.NavContext; import com.seafile.seadroid2.databinding.ActivitySelectorObjBinding; @@ -68,6 +70,12 @@ public class ObjSelectorActivity extends BaseActivity { private ObjSelectorViewModel viewModel; private Account mAccount; + public static Intent getStartIntent(Context context) { + Intent intent = new Intent(context, ObjSelectorActivity.class); + intent.putExtra(ObjSelectorActivity.DATA_ACCOUNT, SupportAccountManager.getInstance().getCurrentAccount()); + return intent; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -112,7 +120,7 @@ public void handleOnBackPressed() { mStep = STEP_CHOOSE_REPO; } - loadData(); + checkLoginState(); } private void initView() { @@ -146,6 +154,16 @@ public void onClick(View view) { }); } + private void checkLoginState() { + boolean isLogin = SupportAccountManager.getInstance().isLogin(); + if (!isLogin) { + binding.ok.setEnabled(false); + binding.newFolder.setEnabled(false); + } + + loadData(); + } + private void onOkClick() { if (!mNavContext.inRepo()) { ToastUtils.showLong(R.string.choose_a_library); @@ -273,41 +291,65 @@ public void onResultData(RepoModel uRepoModel) { } private void checkCurrentPathHasWritePermission(java.util.function.Consumer consumer) { - DirentModel direntModel = mNavContext.getTopDirentModel(); - if (!direntModel.isCustomPermission()) { - consumer.accept(direntModel.hasWritePermission()); + BaseModel model = mNavContext.getTopModel(); + + String repo_id = null; + int pNum = 0; + if (model instanceof RepoModel m) { + if (!m.isCustomPermission()) { + consumer.accept(m.hasWritePermission()); + return; + } else { + repo_id = m.repo_id; + pNum = m.getCustomPermissionNum(); + } + } else if (model instanceof DirentModel m) { + if (!m.isCustomPermission()) { + consumer.accept(m.hasWritePermission()); + return; + } else { + repo_id = m.repo_id; + pNum = m.getCustomPermissionNum(); + } } else { - viewModel.getPermissionFromLocal(direntModel.repo_id, direntModel.getCustomPermissionNum(), new Consumer() { - @Override - public void accept(PermissionEntity entity) throws Exception { - consumer.accept(entity != null && entity.create); - } - }); + consumer.accept(false); + return; } - } + viewModel.getPermissionFromLocal(repo_id, pNum, new Consumer() { + @Override + public void accept(PermissionEntity entity) throws Exception { + if (!entity.isValid()) { + consumer.accept(false); + return; + } + + consumer.accept(entity.create); + } + }); + } private void showNewDirDialog() { if (!mNavContext.inRepo()) { ToastUtils.showLong(R.string.choose_a_library); return; } - checkCurrentPathHasWritePermission(new java.util.function.Consumer() { - @Override - public void accept(Boolean aBoolean) { - String rid = mNavContext.getRepoModel().repo_id; - String parentPath = mNavContext.getNavPath(); - NewDirFileDialogFragment dialogFragment = NewDirFileDialogFragment.newInstance(rid, parentPath, true); - dialogFragment.setRefreshListener(new OnRefreshDataListener() { - @Override - public void onActionStatus(boolean isDone) { - if (isDone) { - loadData(); - } + + checkCurrentPathHasWritePermission(aBoolean -> { + + + String rid = mNavContext.getRepoModel().repo_id; + String parentPath = mNavContext.getNavPath(); + NewDirFileDialogFragment dialogFragment = NewDirFileDialogFragment.newInstance(rid, parentPath, true); + dialogFragment.setRefreshListener(new OnRefreshDataListener() { + @Override + public void onActionStatus(boolean isDone) { + if (isDone) { + loadData(); } - }); - dialogFragment.show(getSupportFragmentManager(), NewDirFileDialogFragment.class.getSimpleName()); - } + } + }); + dialogFragment.show(getSupportFragmentManager(), NewDirFileDialogFragment.class.getSimpleName()); }); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/selector/ObjSelectorViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/selector/ObjSelectorViewModel.java index d1e4cd03b..37acb6436 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/selector/ObjSelectorViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/selector/ObjSelectorViewModel.java @@ -12,6 +12,7 @@ import com.seafile.seadroid2.framework.data.db.entities.EncKeyCacheEntity; import com.seafile.seadroid2.framework.data.db.entities.PermissionEntity; import com.seafile.seadroid2.framework.data.model.permission.PermissionListWrapperModel; +import com.seafile.seadroid2.framework.data.model.permission.PermissionParentModel; import com.seafile.seadroid2.framework.data.model.permission.PermissionWrapperModel; import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; import com.seafile.seadroid2.context.NavContext; @@ -63,9 +64,9 @@ public void loadAccount() { } /** - * @param isFilter Filter out encrypted and read-only repo + * @param isFilterUnavailable Filter out encrypted and read-only repo */ - public void loadReposFromNet(Account account, boolean isFilter) { + public void loadReposFromNet(Account account, boolean isFilterUnavailable) { getRefreshLiveData().setValue(true); Single singleNet = HttpIO.getInstanceByAccount(account).execute(RepoService.class).getRepos(); @@ -78,7 +79,7 @@ public void accept(RepoWrapperModel repoWrapperModel) throws Exception { return; } - List list = Objs.parseRepoListForAdapter(repoWrapperModel.repos, account.getSignature(), isFilter); + List list = Objs.parseRepoListForAdapter(repoWrapperModel.repos, account.getSignature(), isFilterUnavailable); getObjsListLiveData().setValue(list); getRefreshLiveData().setValue(false); } @@ -124,13 +125,13 @@ public void accept(Throwable throwable) throws Exception { public void getPermissionFromLocal(String repoId, int pNum, Consumer consumer) { - Single> pSingle = AppDatabase.getInstance().permissionDAO().getWithAsync(repoId, pNum); + Single> pSingle = AppDatabase.getInstance().permissionDAO().getByRepoAndIdAsync(repoId, pNum); Single s = pSingle.flatMap(new Function, SingleSource>() { @Override public SingleSource apply(List pList) throws Exception { if (CollectionUtils.isEmpty(pList)) { - return null; + return Single.just(new PermissionEntity()); } return Single.just(pList.get(0)); @@ -138,20 +139,21 @@ public SingleSource apply(List pList) throws }).flatMap(new Function>() { @Override public SingleSource apply(PermissionEntity entity) throws Exception { - Single> r = getLoadRepoPermissionFromRemoteSingle(repoId); - return r.flatMap(new Function, SingleSource>() { + if (entity.isValid()) { + return Single.just(entity); + } + + Single r = getLoadRepoPermissionFromRemoteSingle(repoId, pNum); + return r.flatMap(new Function>() { @Override - public SingleSource apply(List permissionEntities) throws Exception { - if (CollectionUtils.isEmpty(permissionEntities)) { - return null; + public SingleSource apply(PermissionEntity permission) throws Exception { + if (permission == null) { + return Single.just(new PermissionEntity()); } - Optional p = permissionEntities.stream().filter(f -> f.id == pNum).findFirst(); - if (p.isPresent()) { - return Single.just(p.get()); - } - return null; + + return Single.just(permission); } }); } @@ -168,27 +170,20 @@ public void accept(PermissionEntity entity) throws Exception { } - private Single> getLoadRepoPermissionFromRemoteSingle(String repoId) { - Single single = HttpIO.getCurrentInstance().execute(RepoService.class).getCustomSharePermissions(repoId); - return single.flatMap(new Function>>() { + private Single getLoadRepoPermissionFromRemoteSingle(String repoId, int pNum) { + Single single = HttpIO.getCurrentInstance().execute(RepoService.class).getCustomSharePermissionById(repoId, pNum); + return single.flatMap(new Function>() { @Override - public SingleSource> apply(PermissionListWrapperModel wrapperModel) throws Exception { - - List list = CollectionUtils.newArrayList(); - - for (PermissionWrapperModel model : wrapperModel.permission_list) { - list.add(new PermissionEntity(repoId, model)); + public SingleSource apply(PermissionWrapperModel wrapperModel) throws Exception { + if (wrapperModel == null || wrapperModel.permission == null) { + return Single.just(new PermissionEntity()); } + PermissionEntity permission = new PermissionEntity(repoId, wrapperModel.permission); - Completable insertCompletable = AppDatabase.getInstance().permissionDAO().insertAllAsync(list); - Single insertAllSingle = insertCompletable.toSingleDefault(0L); - return insertAllSingle.flatMap(new Function>>() { - @Override - public SingleSource> apply(Long aLong) throws Exception { - SLogs.d("The list has been inserted into the local database"); - return Single.just(list); - } - }); + AppDatabase.getInstance().permissionDAO().insert(permission); + SLogs.d("The list has been inserted into the local database"); + + return Single.just(permission); } }); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsAlbumBackupAdvanced2Fragment.java b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsAlbumBackupAdvanced2Fragment.java new file mode 100644 index 000000000..1bbff96c9 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsAlbumBackupAdvanced2Fragment.java @@ -0,0 +1,197 @@ +package com.seafile.seadroid2.ui.settings; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Observer; +import androidx.preference.Preference; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.SeadroidApplication; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.framework.datastore.sp_livedata.AlbumBackupSharePreferenceHelper; +import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.preferences.RenameSharePreferenceFragmentCompat; +import com.seafile.seadroid2.preferences.Settings; +import com.seafile.seadroid2.ui.camera_upload.CameraUploadConfigActivity; +import com.seafile.seadroid2.ui.camera_upload.GalleryBucketUtils; +import com.seafile.seadroid2.widget.prefs.TextSwitchPreference; +import com.seafile.seadroid2.widget.prefs.TextTitleSummaryPreference; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +public class SettingsAlbumBackupAdvanced2Fragment extends RenameSharePreferenceFragmentCompat { + + private final Account currentAccount = SupportAccountManager.getInstance().getCurrentAccount(); + + public static SettingsAlbumBackupAdvanced2Fragment newInstance() { + return new SettingsAlbumBackupAdvanced2Fragment(); + } + + @Override + public String getSharePreferenceSuffix() { + if (currentAccount != null) { + return currentAccount.getEncryptSignature(); + } + return null; + } + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + + setPreferencesFromResource(R.xml.prefs_settings_camera_backup_advance_2, rootKey); + } + + private boolean isFirstLoadData = true; + + @Override + public void onResume() { + super.onResume(); + if (isFirstLoadData) { + + onFirstResume(); + + isFirstLoadData = false; + } + } + + public void onFirstResume() { + + initPrefView(); + + initPrefLiveData(); + + // delay updates to avoid flickering + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + dataSwitch.setChecked(dataSwitch.isChecked()); + videoSwitch.setChecked(videoSwitch.isChecked()); + updateSelectedBucketIdsSummary(); + } + }, 500); + } + + private TextSwitchPreference bucketsSwitch, dataSwitch, videoSwitch; + private TextTitleSummaryPreference selectedBucketPref; + + private void initPrefView() { + bucketsSwitch = findPreference(getString(R.string.pref_key_album_backup_advanced_buckets_switch)); + dataSwitch = findPreference(getString(R.string.pref_key_album_backup_advanced_data_plan_switch)); + videoSwitch = findPreference(getString(R.string.pref_key_album_backup_advanced_allow_video_switch)); + + // + selectedBucketPref = findPreference(getString(R.string.pref_key_album_backup_advanced_buckets_select)); + if (selectedBucketPref != null) { + selectedBucketPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + launchAlbumSelect(); + return true; + } + }); + } + } + + private void initPrefLiveData() { + Settings.ALBUM_BACKUP_ADVANCE_DATA_PLAN_SWITCH.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(Boolean isChecked) { + SLogs.e("相册备份-高级-允许移动流量:" + isChecked); + } + }); + + Settings.ALBUM_BACKUP_ADVANCE_ALLOW_VIDEO_SWITCH.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(Boolean isChecked) { + SLogs.e("相册备份-高级-允许视频上传:" + isChecked); + } + }); + + Settings.ALBUM_BACKUP_ADVANCE_BUCKETS_SWITCH.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(Boolean isChecked) { + SLogs.e("相册备份-高级-自选相册:" + isChecked); + + switchBucket(isChecked); + } + }); + + Settings.ALBUM_BACKUP_ADVANCE_BUCKETS_SELECT.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String s) { + SLogs.e("相册备份-高级-自选的相册列表:" + s); + if (isFirstLoadData) { + return; + } + + selectedBucketPref.setSummary(s); + } + }); + } + + private void switchBucket(boolean isChecked) { + bucketsSwitch.setChecked(isChecked); + + if (isChecked) { + bucketsSwitch.setDividerPosition(2); + bucketsSwitch.setRadiusPosition(0); + } else { + bucketsSwitch.setDividerPosition(0); + bucketsSwitch.setRadiusPosition(1); + } + + selectedBucketPref.setVisible(isChecked); + } + + private void launchAlbumSelect() { + Intent intent = new Intent(requireActivity(), CameraUploadConfigActivity.class); + intent.putExtra(CameraUploadConfigActivity.CAMERA_UPLOAD_LOCAL_DIRECTORIES, true); + selectAlbumLauncher.launch(intent); + } + + private final ActivityResultLauncher selectAlbumLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult o) { + updateSelectedBucketIdsSummary(); + } + }); + + + private void updateSelectedBucketIdsSummary() { + List bucketNames = new ArrayList<>(); + + List bucketIds = AlbumBackupSharePreferenceHelper.readBucketIds(); + List tempBuckets = GalleryBucketUtils.getMediaBuckets(SeadroidApplication.getAppContext()); + LinkedHashSet bucketsSet = new LinkedHashSet<>(tempBuckets.size()); + bucketsSet.addAll(tempBuckets); + List allBuckets = new ArrayList<>(bucketsSet.size()); + allBuckets.addAll(bucketsSet); + + for (GalleryBucketUtils.Bucket bucket : allBuckets) { + if (bucketIds.contains(bucket.id)) { + bucketNames.add(bucket.name); + } + } + + if (bucketNames.isEmpty()) { + selectedBucketPref.setSummary(R.string.not_set); + bucketsSwitch.setChecked(false); + } else { + selectedBucketPref.setSummary(TextUtils.join(", ", bucketNames)); + bucketsSwitch.setChecked(true); + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsAlbumBackupAdvancedActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsAlbumBackupAdvancedActivity.java index d59bc846b..28dbead13 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsAlbumBackupAdvancedActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsAlbumBackupAdvancedActivity.java @@ -39,7 +39,7 @@ public void onCreate(Bundle savedInstanceState) { getSupportActionBar().setTitle(R.string.settings_camera_upload_advanced_feature_title); } - FragmentUtils.add(getSupportFragmentManager(), SettingsAlbumBackupAdvancedFragment.newInstance(), R.id.settings_fragment_container); + FragmentUtils.add(getSupportFragmentManager(), SettingsAlbumBackupAdvanced2Fragment.newInstance(), R.id.settings_fragment_container); getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { @Override diff --git a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsCameraBackupAdvanceFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsCameraBackupAdvanceFragment.java index de440bb9e..d90f17892 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsCameraBackupAdvanceFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsCameraBackupAdvanceFragment.java @@ -81,7 +81,7 @@ public boolean onPreferenceChange(@NonNull Preference preference, Object newValu boolean isCustom = (Boolean) newValue; AlbumBackupManager.writeAllowDataPlanSwitch(isCustom); - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(isCustom); + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(isCustom); return true; } @@ -98,7 +98,7 @@ public boolean onPreferenceChange(@NonNull Preference preference, Object newValu boolean isCustom = (Boolean) newValue; AlbumBackupManager.writeAllowVideoSwitch(isCustom); - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(isCustom); + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(isCustom); return true; } @@ -146,7 +146,7 @@ private void scanCustomDirs(boolean isCustomScanOn) { List selectedBuckets = new ArrayList<>(); AlbumBackupManager.writeBucketIds(selectedBuckets); - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(false); + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(false); refreshPreferenceView(); } @@ -193,7 +193,7 @@ public void onActivityResult(ActivityResult o) { return; } - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(true); + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(true); refreshPreferenceView(); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsFragment.java index a48934fc3..133f635e8 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsFragment.java @@ -301,13 +301,13 @@ private void doWorkInfoLiveData(WorkInfo workInfo) { } if (String.valueOf(TransferDataSource.ALBUM_BACKUP).equals(dataType)) { - if (TransferEvent.EVENT_CANCEL_OUT_OF_QUOTA.equals(progressEvent)) { + if (TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA.equals(progressEvent)) { mCameraBackupState.setSummary(R.string.above_quota); } else if (TransferEvent.EVENT_TRANSFERRING.equals(progressEvent)) { viewModel.countAlbumBackupPendingList(requireContext()); } } else if (String.valueOf(TransferDataSource.FOLDER_BACKUP).equals(dataType)) { - if (TransferEvent.EVENT_CANCEL_OUT_OF_QUOTA.equals(progressEvent)) { + if (TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA.equals(progressEvent)) { mFolderBackupState.setSummary(R.string.above_quota); } else if (TransferEvent.EVENT_TRANSFERRING.equals(progressEvent)) { viewModel.countFolderBackupPendingList(requireContext()); @@ -629,7 +629,7 @@ public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle result) refreshFolderBackNetworkMode(which); //restart - BackgroundJobManagerImpl.getInstance().startFolderChainWorker(true); + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(true); } }); @@ -716,10 +716,10 @@ private void setCameraPreferencesVisible(boolean isChecked) { private void switchCameraWorker(boolean isChecked) { if (isChecked) { CameraUploadManager.getInstance().setCameraAccount(currentAccount); - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(true); + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(true); } else { CameraUploadManager.getInstance().disableCameraUpload(); - BackgroundJobManagerImpl.getInstance().cancelAllMediaWorker(); + BackgroundJobManagerImpl.getInstance().cancelMediaWorker(); } } @@ -733,7 +733,7 @@ private void refreshFolderBackupView(boolean isSync) { setFolderPreferencesVisible(isFolderAutomaticBackup); if (!isFolderAutomaticBackup) { - BackgroundJobManagerImpl.getInstance().cancelAllFolderUploadWorker(); + BackgroundJobManagerImpl.getInstance().cancelFolderAutoUploadWorker(); if (fileSyncService != null) { // fileSyncService.stopFolderMonitor(); } @@ -759,7 +759,7 @@ private void refreshFolderBackupView(boolean isSync) { // fileSyncService.startFolderMonitor(); } - BackgroundJobManagerImpl.getInstance().startFolderChainWorker(true); + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(true); } } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsFragmentViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsFragmentViewModel.java index 887899ddf..35dfc97ea 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsFragmentViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/settings/SettingsFragmentViewModel.java @@ -63,14 +63,13 @@ public AccountInfo apply(ServerInfoModel serverInfoModel, AccountInfo accountInf public void accept(AccountInfo accountInfo) throws Exception { getRefreshLiveData().setValue(false); - String dn = accountInfo.getDisplayName(); - String df = accountInfo.getSpaceUsed(); - // fixme ? Settings.USER_INFO.putValue(""); Settings.SPACE_INFO.putValue(""); - Settings.USER_INFO.putValue(dn); - Settings.SPACE_INFO.putValue(df); + Settings.USER_SERVER_INFO.putValue(""); + Settings.USER_INFO.putValue(accountInfo.getName()); + Settings.USER_SERVER_INFO.putValue(accountInfo.getServer()); + Settings.SPACE_INFO.putValue(accountInfo.getSpaceUsed()); } }); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/settings/TabSettings2Fragment.java b/app/src/main/java/com/seafile/seadroid2/ui/settings/TabSettings2Fragment.java new file mode 100644 index 000000000..6d308468f --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/settings/TabSettings2Fragment.java @@ -0,0 +1,1004 @@ +package com.seafile.seadroid2.ui.settings; + +import static android.app.Activity.RESULT_OK; +import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.text.Spanned; +import android.text.TextUtils; +import android.view.View; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.text.HtmlCompat; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.SwitchPreferenceCompat; +import androidx.work.Data; +import androidx.work.WorkInfo; + +import com.blankj.utilcode.util.AppUtils; +import com.blankj.utilcode.util.CollectionUtils; +import com.blankj.utilcode.util.TimeUtils; +import com.blankj.utilcode.util.ToastUtils; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.bus.TransferBusHelper; +import com.seafile.seadroid2.config.Constants; +import com.seafile.seadroid2.enums.NetworkMode; +import com.seafile.seadroid2.enums.TransferDataSource; +import com.seafile.seadroid2.framework.datastore.StorageManager; +import com.seafile.seadroid2.framework.datastore.sp_livedata.AlbumBackupSharePreferenceHelper; +import com.seafile.seadroid2.framework.datastore.sp_livedata.FolderBackupSharePreferenceHelper; +import com.seafile.seadroid2.framework.util.PermissionUtil; +import com.seafile.seadroid2.framework.util.SLogs; +import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; +import com.seafile.seadroid2.framework.worker.TransferEvent; +import com.seafile.seadroid2.framework.worker.TransferWorker; +import com.seafile.seadroid2.framework.worker.upload.FolderBackupScannerWorker; +import com.seafile.seadroid2.framework.worker.upload.MediaBackupScannerWorker; +import com.seafile.seadroid2.framework.worker.upload.UploadFolderFileAutomaticallyWorker; +import com.seafile.seadroid2.framework.worker.upload.UploadMediaFileAutomaticallyWorker; +import com.seafile.seadroid2.preferences.RenameSharePreferenceFragmentCompat; +import com.seafile.seadroid2.preferences.Settings; +import com.seafile.seadroid2.ui.account.AccountsActivity; +import com.seafile.seadroid2.ui.camera_upload.CameraUploadConfigActivity; +import com.seafile.seadroid2.ui.camera_upload.CameraUploadManager; +import com.seafile.seadroid2.ui.dialog_fragment.ClearCacheDialogFragment; +import com.seafile.seadroid2.ui.dialog_fragment.ClearPasswordDialogFragment; +import com.seafile.seadroid2.ui.dialog_fragment.SignOutDialogFragment; +import com.seafile.seadroid2.ui.dialog_fragment.SwitchStorageDialogFragment; +import com.seafile.seadroid2.ui.dialog_fragment.listener.OnRefreshDataListener; +import com.seafile.seadroid2.ui.folder_backup.FolderBackupConfigActivity; +import com.seafile.seadroid2.ui.folder_backup.FolderBackupSelectedPathActivity; +import com.seafile.seadroid2.ui.folder_backup.RepoConfig; +import com.seafile.seadroid2.ui.main.MainActivity; +import com.seafile.seadroid2.ui.selector.ObjSelectorActivity; +import com.seafile.seadroid2.ui.webview.SeaWebViewActivity; +import com.seafile.seadroid2.widget.prefs.SimpleMenuPreference; +import com.seafile.seadroid2.widget.prefs.TextSwitchPreference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class TabSettings2Fragment extends RenameSharePreferenceFragmentCompat { + + private final Account currentAccount = SupportAccountManager.getInstance().getCurrentAccount(); + + private SettingsFragmentViewModel viewModel; +// private SwitchPreferenceCompat gestureSwitch; + + // album backup + private TextSwitchPreference mAlbumBackupSwitch; + private Preference mAlbumBackupRepo; + private Preference mAlbumBackupAdvanced; + private Preference mAlbumBackupState; + + //folder backup + private TextSwitchPreference mFolderBackupSwitch; + private ListPreference mFolderBackupNetworkMode; + private Preference mFolderBackupSelectRepo; + private Preference mFolderBackupSelectFolder; + private Preference mFolderBackupState; + + + public static TabSettings2Fragment newInstance() { + return new TabSettings2Fragment(); + } + + @Override + public String getSharePreferenceSuffix() { + if (currentAccount != null) { + return currentAccount.getEncryptSignature(); + } + return null; + } + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + //NOTICE: super() + super.onCreatePreferences(savedInstanceState, rootKey); + + setPreferencesFromResource(R.xml.prefs_settings_2, rootKey); + + SimpleMenuPreference.setLightFixEnabled(true); + + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + getListView().setPadding(0, 0, 0, Constants.DP.DP_32); + getListView().setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.bar_background_color)); + + } + + private boolean isFirstLoadData = true; + + @Override + public void onResume() { + super.onResume(); + if (isFirstLoadData) { + + onFirstResume(); + + isFirstLoadData = false; + } + + if (canLoad()) { + loadData(); + } + } + + public void onFirstResume() { + initPref(); + +// initGestureConfig(); + + initPrefLiveData(); + + initWorkerListener(); + + // delay updates to avoid flickering + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + switchAlbumBackupState(mAlbumBackupSwitch.isChecked()); + switchFolderBackupState(mFolderBackupSwitch.isChecked()); + } + }, 500); + + } + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + viewModel = new ViewModelProvider(this).get(SettingsFragmentViewModel.class); + } + + private long last_time = 0L; + + private boolean canLoad() { + long now = TimeUtils.getNowMills(); + if (now - last_time > 300000) {//5m + last_time = now; + return true; + } + return false; + } + + private void initPref() { + if (currentAccount == null) { + return; + } + + initAccountPref(); + + initSignOutPref(); + + initAlbumBackupPref(); + + initFolderBackupPref(); + + initCachePref(); + + initAboutPref(); + + initPolicyPref(); + } + + private void initAccountPref() { + //user pref + Preference userPref = findPreference(getString(R.string.pref_key_user_info)); + if (userPref != null) { + userPref.setOnPreferenceClickListener(preference -> { + Intent newIntent = new Intent(requireActivity(), AccountsActivity.class); + newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(newIntent); + return true; + }); + } + +// gestureSwitch = findPreference(getString(R.string.pref_key_gesture_lock)); + } + + private void initSignOutPref() { + //sign out +// ButtonPreference buttonPreference = findPreference(getString(R.string.pref_key_sign_out)); +// buttonPreference.getButton().setOnClickListener(new View.OnClickListener() { +// @Override +// public void onClick(View v) { +// onPreferenceSignOutClicked(); +// } +// }); + findPreference(getString(R.string.pref_key_sign_out)).setOnPreferenceClickListener(preference -> { + onPreferenceSignOutClicked(); + return true; + }); + + //clear pwd + findPreference(getString(R.string.pref_key_security_clear_password)).setOnPreferenceClickListener(preference -> { + // clear password + clearPassword(); + return true; + }); + } + + private void initAlbumBackupPref() { + // Camera Upload + mAlbumBackupSwitch = findPreference(getString(R.string.pref_key_album_backup_switch)); + mAlbumBackupRepo = findPreference(getString(R.string.pref_key_album_backup_repo_select)); + mAlbumBackupState = findPreference(getString(R.string.pref_key_album_backup_state)); + mAlbumBackupAdvanced = findPreference(getString(R.string.pref_key_album_backup_advanced)); + + if (mAlbumBackupAdvanced != null) { + mAlbumBackupAdvanced.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + Intent intent = new Intent(requireActivity(), SettingsAlbumBackupAdvancedActivity.class); + albumBackupAdvanceLauncher.launch(intent); + return true; + } + }); + } + + if (mAlbumBackupRepo != null) { + mAlbumBackupRepo.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + // choose remote library + Intent intent = new Intent(requireActivity(), CameraUploadConfigActivity.class); + intent.putExtra(CameraUploadConfigActivity.CAMERA_UPLOAD_REMOTE_LIBRARY, true); + cameraBackupConfigLauncher.launch(intent); + return true; + } + }); + } + + } + + private void initFolderBackupPref() { + //folder backup + mFolderBackupSwitch = findPreference(getString(R.string.pref_key_folder_backup_switch)); + mFolderBackupSelectRepo = findPreference(getString(R.string.pref_key_folder_backup_repo_select)); + mFolderBackupSelectFolder = findPreference(getString(R.string.pref_key_folder_backup_folder_select)); + mFolderBackupState = findPreference(getString(R.string.pref_key_folder_backup_state)); + mFolderBackupNetworkMode = findPreference(getString(R.string.pref_key_folder_backup_network_mode)); + + //repo + if (mFolderBackupSelectRepo != null) { + mFolderBackupSelectRepo.setOnPreferenceClickListener(preference -> { + + Intent intent = new Intent(requireActivity(), FolderBackupConfigActivity.class); + intent.putExtra(FolderBackupConfigActivity.FOLDER_BACKUP_SELECT_TYPE, "repo"); + folderBackupConfigLauncher.launch(intent); + + return true; + }); + } + + // + if (mFolderBackupSelectFolder != null) { + mFolderBackupSelectFolder.setOnPreferenceClickListener(preference -> { + + List backupPathList = FolderBackupSharePreferenceHelper.readBackupPathsAsList(); + + Intent intent; + if (CollectionUtils.isEmpty(backupPathList)) { + intent = new Intent(requireActivity(), FolderBackupConfigActivity.class); + } else { + intent = new Intent(requireActivity(), FolderBackupSelectedPathActivity.class); + } + intent.putExtra(FolderBackupConfigActivity.FOLDER_BACKUP_SELECT_TYPE, "folder"); + folderBackupConfigLauncher.launch(intent); + + return true; + }); + } + } + + private void initCachePref() { + // Clear cache + Preference cachePref = findPreference(getString(R.string.pref_key_cache_clear)); + if (cachePref != null) { + cachePref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + clearCache(); + return true; + } + }); + } + + Preference locPref = findPreference(getString(R.string.pref_key_cache_location)); + if (locPref != null) { + // Storage selection only works on KitKat or later + if (StorageManager.getInstance().supportsMultipleStorageLocations()) { + updateStorageLocationSummary(); + locPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + SwitchStorageDialogFragment dialogFragment = SwitchStorageDialogFragment.newInstance(); + dialogFragment.show(getChildFragmentManager(), SwitchStorageDialogFragment.class.getSimpleName()); + return true; + } + }); + } else { + locPref.setVisible(false); + } + } + } + + private void initAboutPref() { + String appVersion = AppUtils.getAppVersionName(); + + // App Version + Preference versionPref = findPreference(getString(R.string.pref_key_about_version)); + if (versionPref != null) { + versionPref.setSummary(appVersion); + } + + // About author + Preference authorPref = findPreference(getString(R.string.pref_key_about_author)); + if (authorPref != null) { + authorPref.setOnPreferenceClickListener(preference -> { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + Spanned span = HtmlCompat.fromHtml(getString(R.string.settings_about_author_info, appVersion), FROM_HTML_MODE_LEGACY); + builder.setMessage(span); + builder.show(); + return true; + }); + } + } + + private void initPolicyPref() { + String country = Locale.getDefault().getCountry(); + String language = Locale.getDefault().getLanguage(); + Preference policyPref = findPreference(getString(R.string.pref_key_about_privacy)); + if (policyPref != null) { + if (TextUtils.equals("CN", country) || TextUtils.equals("zh", language)) { + policyPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + SeaWebViewActivity.openUrlDirectly(requireContext(), Constants.URL_PRIVACY); + return true; + } + }); + } else { + policyPref.setVisible(false); + } + } + } + + private void onPreferenceSignOutClicked() { + SignOutDialogFragment dialogFragment = new SignOutDialogFragment(); + dialogFragment.setRefreshListener(isDone -> { + if (isDone) { + Intent intent = new Intent(requireActivity(), MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + requireActivity().finish(); + } + }); + dialogFragment.show(getChildFragmentManager(), SignOutDialogFragment.class.getSimpleName()); + } + + private void initGestureConfig() { +// boolean isChecked = Settings.SETTINGS_GESTURE.queryValue(); +// gestureSwitch.setChecked(isChecked); +// Settings.USER_GESTURE_LOCK_SWITCH.putValue(isChecked); + } + + private void initPrefLiveData() { + ////////////////// + /// user + ////////////////// + Settings.USER_INFO.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String s) { + findPreference(getString(R.string.pref_key_user_info)).setTitle(s); + + } + }); + + Settings.USER_SERVER_INFO.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String s) { + findPreference(getString(R.string.pref_key_user_server)).setSummary(s); + } + }); + + Settings.SPACE_INFO.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String s) { + findPreference(getString(R.string.pref_key_user_space)).setSummary(s); + } + }); + +// Settings.USER_GESTURE_LOCK_SWITCH.observe(getViewLifecycleOwner(), new Observer() { +// @Override +// public void onChanged(Boolean aBoolean) { +// +// if (aBoolean) { +// // inverse checked status +// Intent newIntent = new Intent(getActivity(), CreateGesturePasswordActivity.class); +// newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); +// gestureLauncher.launch(newIntent); +// +// } else { +// LockPatternUtils mLockPatternUtils = new LockPatternUtils(getActivity()); +// mLockPatternUtils.clearLock(); +// +// Settings.SETTINGS_GESTURE_LOCK_TIMESTAMP.putValue(0L); +// } +// +// Settings.SETTINGS_GESTURE.putValue(aBoolean); +// } +// }); + + ////////////////// + /// album backup + ////////////////// + Settings.ALBUM_BACKUP_SWITCH.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(Boolean aBoolean) { + SLogs.e("album switch:" + aBoolean); + + if (aBoolean) { + requestCameraStoragePermission(); + } else { + AlbumBackupSharePreferenceHelper.writeRepoConfig(null); + switchAlbumBackupState(false); + dispatchAlbumBackupWork(false); + } + + } + }); + + Settings.ALBUM_BACKUP_STATE.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String s) { + SLogs.e("album state:" + s); + mAlbumBackupState.setSummary(s); + } + }); + + ////////////////// + /// folder backup + ////////////////// + Settings.FOLDER_BACKUP_SWITCH.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(Boolean aBoolean) { + SLogs.e("folder switch:" + aBoolean); + + if (Boolean.TRUE.equals(aBoolean)) { + requestFolderStoragePermission(); + } else { + //clear + FolderBackupSharePreferenceHelper.writeRepoConfig(null); + + switchFolderBackupState(false); + dispatchFolderBackupWork(false); + } + } + }); + + Settings.FOLDER_BACKUP_NETWORK_MODE.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(NetworkMode netWorkMode) { + SLogs.e("folder network:" + netWorkMode.name()); + } + }); + + Settings.FOLDER_BACKUP_STATE.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String s) { + SLogs.e("folder state:" + s); + + if (mFolderBackupState != null) { + mFolderBackupState.setSummary(s); + } + } + }); + + ////////////////// + /// cache + ////////////////// + Settings.CACHE_SIZE.observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(String s) { + SLogs.e("cache size:" + s); + findPreference(getString(R.string.pref_key_cache_info)).setSummary(s); + } + }); + } + + + private void initWorkerListener() { + BackgroundJobManagerImpl.getInstance().getWorkManager() + .getWorkInfoByIdLiveData(MediaBackupScannerWorker.UID) + .observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(WorkInfo workInfo) { + checkScanWorkInfo(TransferDataSource.ALBUM_BACKUP, workInfo); + } + }); + + BackgroundJobManagerImpl.getInstance().getWorkManager() + .getWorkInfoByIdLiveData(FolderBackupScannerWorker.UID) + .observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(WorkInfo workInfo) { + checkScanWorkInfo(TransferDataSource.FOLDER_BACKUP, workInfo); + } + }); + + BackgroundJobManagerImpl.getInstance().getWorkManager() + .getWorkInfoByIdLiveData(UploadFolderFileAutomaticallyWorker.UID) + .observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(WorkInfo workInfo) { + checkOutputData(workInfo); + } + }); + + BackgroundJobManagerImpl.getInstance().getWorkManager() + .getWorkInfoByIdLiveData(UploadMediaFileAutomaticallyWorker.UID) + .observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(WorkInfo workInfo) { + checkOutputData(workInfo); + } + }); + } + + private void checkScanWorkInfo(TransferDataSource dataSource, WorkInfo workInfo) { + if (null == workInfo) { + return; + } + + Data outData = workInfo.getOutputData(); + String outDataEvent = outData.getString(TransferWorker.KEY_DATA_EVENT); + String outDataType = outData.getString(TransferWorker.KEY_DATA_TYPE); + + //scan end + if (TextUtils.equals(String.valueOf(TransferDataSource.ALBUM_BACKUP), outDataType)) { + if (TransferEvent.EVENT_SCAN_END.equals(outDataEvent)) { +// mAlbumBackupState.setSummary(R.string.done); + SLogs.e("album scan end"); + return; + } + } else if (TextUtils.equals(String.valueOf(TransferDataSource.FOLDER_BACKUP), outDataType)) { + if (TransferEvent.EVENT_SCAN_END.equals(outDataEvent)) { +// mFolderBackupState.setSummary(R.string.done); + SLogs.e("folder scan end"); + return; + } + } + + Data progressData = workInfo.getProgress(); + String pDataEvent = progressData.getString(TransferWorker.KEY_DATA_EVENT); + if (TextUtils.isEmpty(pDataEvent)) { + return; + } + + if (TransferDataSource.ALBUM_BACKUP == dataSource) { + if (TransferEvent.EVENT_SCANNING.equals(pDataEvent)) { + mAlbumBackupState.setSummary(R.string.is_scanning); + } + } else if (TransferDataSource.FOLDER_BACKUP == dataSource) { + if (TransferEvent.EVENT_SCANNING.equals(pDataEvent)) { + mFolderBackupState.setSummary(R.string.is_scanning); + } + } + } + + private void checkOutputData(WorkInfo workInfo) { + if (null == workInfo) { + return; + } + + Data outData = workInfo.getOutputData(); + String outDataEvent = outData.getString(TransferWorker.KEY_DATA_EVENT); + String outDataType = outData.getString(TransferWorker.KEY_DATA_TYPE); + + if (TextUtils.equals(String.valueOf(TransferDataSource.ALBUM_BACKUP), outDataType)) { + if (TransferEvent.EVENT_FINISH.equals(outDataEvent)) { + mAlbumBackupState.setSummary(R.string.settings_cuc_finish_title); + } else if (TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA.equals(outDataEvent)) { + mAlbumBackupState.setSummary(R.string.above_quota); + } else if (TransferEvent.EVENT_CANCEL_WITH_BY_STOPPED.equals(outDataEvent)) { + mAlbumBackupState.setSummary(R.string.canceled); + } + } else if (TextUtils.equals(String.valueOf(TransferDataSource.FOLDER_BACKUP), outDataType)) { + if (TransferEvent.EVENT_FINISH.equals(outDataEvent)) { + mFolderBackupState.setSummary(R.string.folder_backup_waiting_state); + } else if (TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA.equals(outDataEvent)) { + mFolderBackupState.setSummary(R.string.above_quota); + } else if (TransferEvent.EVENT_CANCEL_WITH_BY_STOPPED.equals(outDataEvent)) { + mFolderBackupState.setSummary(R.string.canceled); + } + } else { + checkProgressData(workInfo); + } + } + + private void checkProgressData(WorkInfo workInfo) { + if (null == workInfo) { + return; + } + + Data progressData = workInfo.getProgress(); + String dataType = progressData.getString(TransferWorker.KEY_DATA_TYPE); + String progressEvent = progressData.getString(TransferWorker.KEY_DATA_EVENT); + String progressFileName = progressData.getString(TransferWorker.DATA_TRANSFER_NAME_KEY); + + if (TextUtils.equals(String.valueOf(TransferDataSource.ALBUM_BACKUP), dataType)) { + if (TransferEvent.EVENT_TRANSFERRING.equals(progressEvent)) { + viewModel.countAlbumBackupPendingList(requireContext()); + } + } else if (TextUtils.equals(String.valueOf(TransferDataSource.FOLDER_BACKUP), dataType)) { + if (TransferEvent.EVENT_TRANSFERRING.equals(progressEvent)) { + viewModel.countFolderBackupPendingList(requireContext()); + } + } + } + + private void loadData() { + //get account data + viewModel.getAccountInfo(); + + // Cache size + calculateCacheSize(); + + // + if (mAlbumBackupSwitch.isChecked()) { + viewModel.countAlbumBackupPendingList(requireContext()); + } + + if (mFolderBackupSwitch.isChecked()) { + viewModel.countFolderBackupPendingList(requireContext()); + } + } + + //0 : no one + private int whoIsRequestingPermission = 0; + + private void requestCameraStoragePermission() { + if (PermissionUtil.checkExternalStoragePermission(requireContext())) { + + Intent intent = new Intent(requireActivity(), CameraUploadConfigActivity.class); + cameraBackupConfigLauncher.launch(intent); + + } else { + whoIsRequestingPermission = 1; + + PermissionUtil.requestExternalStoragePermission(requireContext(), multiplePermissionLauncher, manageStoragePermissionLauncher, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + //on cancel click + ToastUtils.showLong(R.string.permission_manage_external_storage_rationale); + switchAlbumBackupState(false); + } + }); + } + } + + private void requestFolderStoragePermission() { + if (PermissionUtil.checkExternalStoragePermission(requireContext())) { + switchFolderBackupState(true); + dispatchFolderBackupWork(true); + } else { + whoIsRequestingPermission = 2; + PermissionUtil.requestExternalStoragePermission(requireContext(), multiplePermissionLauncher, manageStoragePermissionLauncher, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + //on cancel click + ToastUtils.showLong(R.string.permission_manage_external_storage_rationale); + switchFolderBackupState(false); + } + }); + } + } + + private void switchAlbumBackupState(boolean isEnable) { + mAlbumBackupSwitch.setChecked(isEnable); + + if (isEnable) { + //all + mAlbumBackupSwitch.setRadiusPosition(2); + mAlbumBackupSwitch.setDividerPosition(2); + } else { + mAlbumBackupSwitch.setRadiusPosition(1); + mAlbumBackupSwitch.setDividerPosition(0); + mAlbumBackupState.setSummary(null); + } + + //change UI + mAlbumBackupRepo.setVisible(isEnable); + mAlbumBackupState.setVisible(isEnable); + mAlbumBackupAdvanced.setVisible(isEnable); + + updateAlbumBackupSelectedRepoSummary(); + } + + private void updateAlbumBackupSelectedRepoSummary() { + Account camAccount = CameraUploadManager.getInstance().getCameraAccount(); + RepoConfig repoConfig = AlbumBackupSharePreferenceHelper.readRepoConfig(); + if (camAccount != null && repoConfig != null) { + mAlbumBackupRepo.setSummary(repoConfig.getRepoName()); + } else { + mAlbumBackupRepo.setSummary(null); + } + } + + private void dispatchAlbumBackupWork(boolean isEnable) { + if (isEnable) { + CameraUploadManager.getInstance().setCameraAccount(currentAccount); + CameraUploadManager.getInstance().performSync(); + } else { + CameraUploadManager.getInstance().disableCameraUpload(); + } + } + + private void switchFolderBackupState(boolean isEnable) { + mFolderBackupSwitch.setChecked(isEnable); + + if (isEnable) { + //all + mFolderBackupSwitch.setRadiusPosition(2); + mFolderBackupSwitch.setDividerPosition(2); + } else { + mFolderBackupSwitch.setRadiusPosition(1); + mFolderBackupSwitch.setDividerPosition(0); + mFolderBackupState.setSummary(null); + } + + mFolderBackupNetworkMode.setVisible(isEnable); + mFolderBackupSelectRepo.setVisible(isEnable); + mFolderBackupSelectFolder.setVisible(isEnable); + mFolderBackupState.setVisible(isEnable); + + updateFolderBackupSelectedRepoAndFolderSummary(); + } + + private void updateFolderBackupSelectedRepoAndFolderSummary() { + RepoConfig repoConfig = FolderBackupSharePreferenceHelper.readRepoConfig(); + + if (repoConfig != null && !TextUtils.isEmpty(repoConfig.getRepoName())) { + mFolderBackupSelectRepo.setSummary(repoConfig.getRepoName()); + } else { + mFolderBackupSelectRepo.setSummary(getString(R.string.folder_backup_select_repo_hint)); + } + + List pathList = FolderBackupSharePreferenceHelper.readBackupPathsAsList(); + if (CollectionUtils.isEmpty(pathList)) { + mFolderBackupSelectFolder.setSummary("0"); + } else { + mFolderBackupSelectFolder.setSummary(String.valueOf(pathList.size())); + } + } + + private void dispatchFolderBackupWork(boolean isEnable) { + if (!isEnable) { + TransferBusHelper.resetFileMonitor(); + return; + } + + RepoConfig repoConfig = FolderBackupSharePreferenceHelper.readRepoConfig(); + List pathList = FolderBackupSharePreferenceHelper.readBackupPathsAsList(); + + if (!CollectionUtils.isEmpty(pathList) && repoConfig != null) { + TransferBusHelper.startFileMonitor(); + + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(true); + } + } + + private void clearPassword() { + ClearPasswordDialogFragment dialogFragment = ClearPasswordDialogFragment.newInstance(); + dialogFragment.setRefreshListener(new OnRefreshDataListener() { + @Override + public void onActionStatus(boolean isDone) { + if (isDone) { + ToastUtils.showLong(R.string.clear_password_successful); + } else { + ToastUtils.showLong(R.string.clear_password_failed); + } + } + }); + dialogFragment.show(getChildFragmentManager(), ClearPasswordDialogFragment.class.getSimpleName()); + } + + private void updateStorageLocationSummary() { + String summary = StorageManager.getInstance().getStorageLocation().description; + findPreference(getString(R.string.pref_key_cache_location)).setSummary(summary); + } + + private void clearCache() { + ClearCacheDialogFragment dialogFragment = ClearCacheDialogFragment.newInstance(); + dialogFragment.setRefreshListener(new OnRefreshDataListener() { + @Override + public void onActionStatus(boolean isDone) { + if (isDone) { + calculateCacheSize(); + ToastUtils.showLong(R.string.settings_clear_cache_success); + } else { + ToastUtils.showLong(R.string.settings_clear_cache_failed); + } + } + }); + dialogFragment.show(getChildFragmentManager(), ClearCacheDialogFragment.class.getSimpleName()); + } + + private void calculateCacheSize() { + viewModel.calculateCacheSize(); + } + + private final ActivityResultLauncher folderBackupConfigLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult o) { + if (o.getResultCode() != RESULT_OK) { + return; + } + + Intent data = o.getData(); + if (null == data) { + return; + } + + String selectType = data.getStringExtra(FolderBackupConfigActivity.FOLDER_BACKUP_SELECT_TYPE); + if ("repo".equals(selectType)) { + + RepoConfig repoConfig = null; + if (data.hasExtra(ObjSelectorActivity.DATA_REPO_ID)) { + String repoId = data.getStringExtra(ObjSelectorActivity.DATA_REPO_ID); + String repoName = data.getStringExtra(ObjSelectorActivity.DATA_REPO_NAME); + Account account = data.getParcelableExtra(ObjSelectorActivity.DATA_ACCOUNT); + + repoConfig = new RepoConfig(repoId, repoName, account.getEmail(), account.getSignature()); + } + + FolderBackupSharePreferenceHelper.writeRepoConfig(repoConfig); + + } else if ("folder".equals(selectType)) { + + ArrayList selectedFolderPaths = data.getStringArrayListExtra(FolderBackupConfigActivity.BACKUP_SELECT_PATHS); + FolderBackupSharePreferenceHelper.writeBackupPathsAsString(selectedFolderPaths); + + } + + updateFolderBackupSelectedRepoAndFolderSummary(); + + dispatchFolderBackupWork(mFolderBackupSwitch.isChecked()); + } + }); + + private final ActivityResultLauncher albumBackupAdvanceLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult o) { + if (o.getResultCode() != RESULT_OK) { + return; + } + + updateAlbumBackupSelectedRepoSummary(); + + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(true); + } + }); + + private final ActivityResultLauncher cameraBackupConfigLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult o) { + if (o.getResultCode() == RESULT_OK) { + //The dispatch function needs to be put first + dispatchAlbumBackupWork(true); + + switchAlbumBackupState(true); + } else { + //The dispatch function needs to be put first + dispatchAlbumBackupWork(false); + + if (o.getData() != null) { + boolean isChooseRepo = o.getData().getBooleanExtra(CameraUploadConfigActivity.CAMERA_UPLOAD_REMOTE_LIBRARY, false); + boolean isChooseDir = o.getData().getBooleanExtra(CameraUploadConfigActivity.CAMERA_UPLOAD_LOCAL_DIRECTORIES, false); + if (!isChooseRepo && !isChooseDir) { + switchAlbumBackupState(false); + } else { + SLogs.d("isChooseRepo?" + isChooseRepo); + SLogs.d("isChooseDir?" + isChooseDir); + } + } else { + switchAlbumBackupState(false); + } + + + } + } + }); + +// private final ActivityResultLauncher gestureLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { +// @Override +// public void onActivityResult(ActivityResult o) { +// if (o.getResultCode() != RESULT_OK) { +// gestureSwitch.setChecked(false); +// } +// } +// }); + + private final ActivityResultLauncher multiplePermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { + @Override + public void onActivityResult(Map o) { + if (o.isEmpty()) { + return; + } + + for (Map.Entry stringBooleanEntry : o.entrySet()) { + if (Boolean.FALSE.equals(stringBooleanEntry.getValue())) { + + ToastUtils.showLong(R.string.permission_manage_external_storage_rationale); + + if (whoIsRequestingPermission == 1) { + switchAlbumBackupState(false); + } else if (whoIsRequestingPermission == 2) { + switchFolderBackupState(false); + } + return; + } + } + + if (whoIsRequestingPermission == 1) { + + Intent intent = new Intent(requireActivity(), CameraUploadConfigActivity.class); + cameraBackupConfigLauncher.launch(intent); + + } else if (whoIsRequestingPermission == 2) { + //on livedata change + } + } + }); + + private final ActivityResultLauncher manageStoragePermissionLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult o) { + if (o.getResultCode() != RESULT_OK) { + ToastUtils.showLong(R.string.get_storage_permission_failed); + + if (whoIsRequestingPermission == 1) { + switchAlbumBackupState(false); + } else if (whoIsRequestingPermission == 2) { + switchFolderBackupState(false); + } + return; + } + + if (whoIsRequestingPermission == 1) { + + Intent intent = new Intent(requireActivity(), CameraUploadConfigActivity.class); + cameraBackupConfigLauncher.launch(intent); + + } else if (whoIsRequestingPermission == 2) { + //on livedata change + + } + } + }); +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/settings/TabSettingsFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/settings/TabSettingsFragment.java index a80a83bdf..d76b53a02 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/settings/TabSettingsFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/settings/TabSettingsFragment.java @@ -5,11 +5,9 @@ import android.content.DialogInterface; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.text.Spanned; import android.text.TextUtils; -import android.view.View; import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; @@ -37,13 +35,10 @@ import com.seafile.seadroid2.bus.TransferBusHelper; import com.seafile.seadroid2.config.Constants; import com.seafile.seadroid2.enums.NetworkMode; -import com.seafile.seadroid2.enums.NightMode; import com.seafile.seadroid2.enums.TransferDataSource; -import com.seafile.seadroid2.framework.data.ServerInfo; import com.seafile.seadroid2.framework.datastore.StorageManager; import com.seafile.seadroid2.framework.datastore.sp_livedata.AlbumBackupSharePreferenceHelper; import com.seafile.seadroid2.framework.datastore.sp_livedata.FolderBackupSharePreferenceHelper; -import com.seafile.seadroid2.framework.helper.NightModeHelper; import com.seafile.seadroid2.framework.util.PermissionUtil; import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; @@ -53,7 +48,6 @@ import com.seafile.seadroid2.framework.worker.upload.MediaBackupScannerWorker; import com.seafile.seadroid2.framework.worker.upload.UploadFolderFileAutomaticallyWorker; import com.seafile.seadroid2.framework.worker.upload.UploadMediaFileAutomaticallyWorker; -import com.seafile.seadroid2.gesturelock.LockPatternUtils; import com.seafile.seadroid2.preferences.RenameSharePreferenceFragmentCompat; import com.seafile.seadroid2.preferences.Settings; import com.seafile.seadroid2.ui.account.AccountsActivity; @@ -67,25 +61,23 @@ import com.seafile.seadroid2.ui.folder_backup.FolderBackupConfigActivity; import com.seafile.seadroid2.ui.folder_backup.FolderBackupSelectedPathActivity; import com.seafile.seadroid2.ui.folder_backup.RepoConfig; -import com.seafile.seadroid2.ui.gesture.CreateGesturePasswordActivity; import com.seafile.seadroid2.ui.main.MainActivity; import com.seafile.seadroid2.ui.selector.ObjSelectorActivity; import com.seafile.seadroid2.ui.webview.SeaWebViewActivity; -import com.seafile.seadroid2.widget.prefs.ButtonPreference; +import com.seafile.seadroid2.widget.prefs.SimpleMenuPreference; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; -import rikka.preference.SimpleMenuPreference; public class TabSettingsFragment extends RenameSharePreferenceFragmentCompat { private final Account currentAccount = SupportAccountManager.getInstance().getCurrentAccount(); private SettingsFragmentViewModel viewModel; - private SwitchPreferenceCompat gestureSwitch; +// private SwitchPreferenceCompat gestureSwitch; // album backup private SwitchPreferenceCompat mAlbumBackupSwitch; @@ -115,7 +107,7 @@ public String getSharePreferenceSuffix() { @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { - //NOTICE: super() + //NOTICE: super firstly super.onCreatePreferences(savedInstanceState, rootKey); setPreferencesFromResource(R.xml.prefs_settings, rootKey); @@ -142,7 +134,7 @@ public void onResume() { public void onFirstResume() { initPref(); - initGestureConfig(); +// initGestureConfig(); initPrefLiveData(); @@ -203,7 +195,7 @@ private void initAccountPref() { }); } - gestureSwitch = findPreference(getString(R.string.pref_key_gesture_lock)); +// gestureSwitch = findPreference(getString(R.string.pref_key_gesture_lock)); } private void initSignOutPref() { @@ -388,8 +380,8 @@ private void onPreferenceSignOutClicked() { } private void initGestureConfig() { - boolean isChecked = Settings.SETTINGS_GESTURE.queryValue(); - gestureSwitch.setChecked(isChecked); +// boolean isChecked = Settings.SETTINGS_GESTURE.queryValue(); +// gestureSwitch.setChecked(isChecked); // Settings.USER_GESTURE_LOCK_SWITCH.putValue(isChecked); } @@ -411,26 +403,26 @@ public void onChanged(String s) { } }); - Settings.USER_GESTURE_LOCK_SWITCH.observe(getViewLifecycleOwner(), new Observer() { - @Override - public void onChanged(Boolean aBoolean) { - - if (aBoolean) { - // inverse checked status - Intent newIntent = new Intent(getActivity(), CreateGesturePasswordActivity.class); - newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - gestureLauncher.launch(newIntent); - - } else { - LockPatternUtils mLockPatternUtils = new LockPatternUtils(getActivity()); - mLockPatternUtils.clearLock(); - - Settings.SETTINGS_GESTURE_LOCK_TIMESTAMP.putValue(0L); - } - - Settings.SETTINGS_GESTURE.putValue(aBoolean); - } - }); +// Settings.USER_GESTURE_LOCK_SWITCH.observe(getViewLifecycleOwner(), new Observer() { +// @Override +// public void onChanged(Boolean aBoolean) { +// +// if (aBoolean) { +// // inverse checked status +// Intent newIntent = new Intent(getActivity(), CreateGesturePasswordActivity.class); +// newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); +// gestureLauncher.launch(newIntent); +// +// } else { +// LockPatternUtils mLockPatternUtils = new LockPatternUtils(getActivity()); +// mLockPatternUtils.clearLock(); +// +// Settings.SETTINGS_GESTURE_LOCK_TIMESTAMP.putValue(0L); +// } +// +// Settings.SETTINGS_GESTURE.putValue(aBoolean); +// } +// }); ////////////////// /// album backup @@ -445,9 +437,10 @@ public void onChanged(Boolean aBoolean) { } else { mAlbumBackupSwitch.setChecked(false); AlbumBackupSharePreferenceHelper.writeRepoConfig(null); + switchAlbumBackupState(false); + dispatchAlbumBackupWork(false); } - switchAlbumBackupState(aBoolean); } }); @@ -557,14 +550,18 @@ private void checkScanWorkInfo(TransferDataSource dataSource, WorkInfo workInfo) Data outData = workInfo.getOutputData(); String outDataEvent = outData.getString(TransferWorker.KEY_DATA_EVENT); String outDataType = outData.getString(TransferWorker.KEY_DATA_TYPE); + + //scan end if (TextUtils.equals(String.valueOf(TransferDataSource.ALBUM_BACKUP), outDataType)) { if (TransferEvent.EVENT_SCAN_END.equals(outDataEvent)) { - mAlbumBackupState.setSummary(R.string.done); +// mAlbumBackupState.setSummary(R.string.done); + SLogs.e("album scan end"); return; } } else if (TextUtils.equals(String.valueOf(TransferDataSource.FOLDER_BACKUP), outDataType)) { if (TransferEvent.EVENT_SCAN_END.equals(outDataEvent)) { - mFolderBackupState.setSummary(R.string.done); +// mFolderBackupState.setSummary(R.string.done); + SLogs.e("folder scan end"); return; } } @@ -598,14 +595,18 @@ private void checkOutputData(WorkInfo workInfo) { if (TextUtils.equals(String.valueOf(TransferDataSource.ALBUM_BACKUP), outDataType)) { if (TransferEvent.EVENT_FINISH.equals(outDataEvent)) { mAlbumBackupState.setSummary(R.string.settings_cuc_finish_title); - } else if (TransferEvent.EVENT_CANCEL_OUT_OF_QUOTA.equals(outDataEvent)) { + } else if (TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA.equals(outDataEvent)) { mAlbumBackupState.setSummary(R.string.above_quota); + } else if (TransferEvent.EVENT_CANCEL_WITH_BY_STOPPED.equals(outDataEvent)) { + mAlbumBackupState.setSummary(R.string.canceled); } } else if (TextUtils.equals(String.valueOf(TransferDataSource.FOLDER_BACKUP), outDataType)) { if (TransferEvent.EVENT_FINISH.equals(outDataEvent)) { mFolderBackupState.setSummary(R.string.folder_backup_waiting_state); - } else if (TransferEvent.EVENT_CANCEL_OUT_OF_QUOTA.equals(outDataEvent)) { + } else if (TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA.equals(outDataEvent)) { mFolderBackupState.setSummary(R.string.above_quota); + } else if (TransferEvent.EVENT_CANCEL_WITH_BY_STOPPED.equals(outDataEvent)) { + mFolderBackupState.setSummary(R.string.canceled); } } else { checkProgressData(workInfo); @@ -696,6 +697,10 @@ private void switchAlbumBackupState(boolean isEnable) { mAlbumBackupState.setVisible(isEnable); mAlbumBackupAdvanced.setVisible(isEnable); + if (!isEnable) { + mAlbumBackupState.setSummary(null); + } + updateAlbumBackupSelectedRepoSummary(); } @@ -724,6 +729,10 @@ private void switchFolderBackupState(boolean isEnable) { mFolderBackupSelectFolder.setVisible(isEnable); mFolderBackupState.setVisible(isEnable); + if (!isEnable) { + mFolderBackupState.setSummary(null); + } + updateFolderBackupSelectedRepoAndFolderSummary(); } @@ -756,7 +765,7 @@ private void dispatchFolderBackupWork(boolean isEnable) { if (!CollectionUtils.isEmpty(pathList) && repoConfig != null) { TransferBusHelper.startFileMonitor(); - BackgroundJobManagerImpl.getInstance().startFolderChainWorker(true); + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(true); } } @@ -822,6 +831,18 @@ public void onActivityResult(ActivityResult o) { Account account = data.getParcelableExtra(ObjSelectorActivity.DATA_ACCOUNT); repoConfig = new RepoConfig(repoId, repoName, account.getEmail(), account.getSignature()); + + //check for RepoConfig that already exists + RepoConfig currentRepoConfig = FolderBackupSharePreferenceHelper.readRepoConfig(); + if (currentRepoConfig != null) { + //means the repo config is changed, need to clear the last scan time for all paths + if (!repoConfig.getRepoID().equals(currentRepoConfig.getRepoID())) { + FolderBackupSharePreferenceHelper.clearLastScanTimeForAllPath(); + } + } + } else { + //clear the repo config, which means need to clear the last scan time for all paths + FolderBackupSharePreferenceHelper.clearLastScanTimeForAllPath(); } FolderBackupSharePreferenceHelper.writeRepoConfig(repoConfig); @@ -848,7 +869,7 @@ public void onActivityResult(ActivityResult o) { updateAlbumBackupSelectedRepoSummary(); - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(true); + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(true); } }); @@ -856,10 +877,12 @@ public void onActivityResult(ActivityResult o) { @Override public void onActivityResult(ActivityResult o) { if (o.getResultCode() == RESULT_OK) { + //The dispatch function needs to be put first dispatchAlbumBackupWork(true); - updateAlbumBackupSelectedRepoSummary(); + switchAlbumBackupState(true); } else { + //The dispatch function needs to be put first dispatchAlbumBackupWork(false); if (o.getData() != null) { @@ -880,14 +903,14 @@ public void onActivityResult(ActivityResult o) { } }); - private final ActivityResultLauncher gestureLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { - @Override - public void onActivityResult(ActivityResult o) { - if (o.getResultCode() != RESULT_OK) { - gestureSwitch.setChecked(false); - } - } - }); +// private final ActivityResultLauncher gestureLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { +// @Override +// public void onActivityResult(ActivityResult o) { +// if (o.getResultCode() != RESULT_OK) { +// gestureSwitch.setChecked(false); +// } +// } +// }); private final ActivityResultLauncher multiplePermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override diff --git a/app/src/main/java/com/seafile/seadroid2/ui/share/ShareToSeafileViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/share/ShareToSeafileViewModel.java index f684fe058..b210b3869 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/share/ShareToSeafileViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/share/ShareToSeafileViewModel.java @@ -164,7 +164,7 @@ public void subscribe(SingleEmitter emitter) throws Exception { addSingleDisposable(booleanSingle, new Consumer() { @Override public void accept(Boolean aBoolean) throws Exception { - BackgroundJobManagerImpl.getInstance().startFileUploadWorker(); + BackgroundJobManagerImpl.getInstance().startFileManualUploadWorker(); getActionLiveData().setValue(true); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/star/StarredQuickFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/star/StarredQuickFragment.java index d51578abf..8ce181552 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/star/StarredQuickFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/star/StarredQuickFragment.java @@ -28,23 +28,23 @@ import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.annotation.Unstable; -import com.seafile.seadroid2.framework.datastore.sp.SettingsManager; -import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetHelper; -import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetMenuFragment; -import com.seafile.seadroid2.ui.bottomsheetmenu.OnMenuClickListener; import com.seafile.seadroid2.config.Constants; import com.seafile.seadroid2.databinding.LayoutFrameSwipeRvBinding; import com.seafile.seadroid2.framework.data.db.entities.StarredModel; import com.seafile.seadroid2.framework.data.model.ResultModel; import com.seafile.seadroid2.framework.datastore.DataManager; +import com.seafile.seadroid2.framework.datastore.sp.SettingsManager; import com.seafile.seadroid2.framework.util.Utils; import com.seafile.seadroid2.ui.WidgetUtils; import com.seafile.seadroid2.ui.base.fragment.BaseFragmentWithVM; +import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetHelper; +import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetMenuFragment; +import com.seafile.seadroid2.ui.bottomsheetmenu.OnMenuClickListener; import com.seafile.seadroid2.ui.file.FileActivity; import com.seafile.seadroid2.ui.main.MainActivity; import com.seafile.seadroid2.ui.main.MainViewModel; import com.seafile.seadroid2.ui.markdown.MarkdownActivity; -import com.seafile.seadroid2.ui.media.image_preview.ImagePreviewActivity; +import com.seafile.seadroid2.ui.media.image_preview2.CarouselImagePreviewActivity; import com.seafile.seadroid2.ui.media.player.exoplayer.CustomExoVideoPlayerActivity; import com.seafile.seadroid2.ui.sdoc.SDocWebViewActivity; import com.seafile.seadroid2.view.TipsViews; @@ -245,7 +245,7 @@ private void open(StarredModel model) { } else if (Utils.isViewableImage(model.obj_name)) { - Intent getIntent = ImagePreviewActivity.startThisFromStarred(requireContext(), model); + Intent getIntent = CarouselImagePreviewActivity.startThisFromStarred(requireContext(), model); imagePreviewActivityLauncher.launch(getIntent); } else if (model.obj_name.endsWith(Constants.Format.DOT_SDOC)) { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/DownloadListFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/DownloadListFragment.java index 9d26702e8..40f8efe17 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/DownloadListFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/DownloadListFragment.java @@ -11,9 +11,11 @@ import androidx.work.Data; import androidx.work.WorkInfo; +import com.blankj.utilcode.util.CollectionUtils; import com.blankj.utilcode.util.ToastUtils; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.seafile.seadroid2.R; +import com.seafile.seadroid2.enums.TransferStatus; import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; import com.seafile.seadroid2.enums.TransferAction; import com.seafile.seadroid2.enums.TransferDataSource; @@ -124,37 +126,58 @@ public void deleteSelectedItems(List list) { } private void showDeleteConfirmDialog(List list) { - showConfirmDialog(new DialogInterface.OnClickListener() { + if (CollectionUtils.isEmpty(list)) { + return; + } + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); + builder.setTitle(R.string.delete_records); + + String deleteFile = getString(R.string.delete_local_file_sametime); + CharSequence[] sequences = new CharSequence[1]; + sequences[0] = deleteFile; + boolean[] booleans = new boolean[1]; + booleans[0] = true; + builder.setMultiChoiceItems(sequences, booleans, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + booleans[which] = isChecked; + } + }); + + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { + doDeleteSelectedItem(list, booleans[0]); + dialog.dismiss(); + } + }); - BackgroundJobManagerImpl.getInstance().cancelDownloadWorker(); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.show(); + } - getViewModel().removeSpecialDownloadListTask(list, new Consumer() { - @Override - public void accept(Boolean aBoolean) throws Exception { + private void doDeleteSelectedItem(List list, boolean isDeleteLocalFile) { + getViewModel().getShowLoadingDialogLiveData().setValue(true); - BackgroundJobManagerImpl.getInstance().startDownloadChainWorker(); + BackgroundJobManagerImpl.getInstance().cancelDownloadWorker(); - ToastUtils.showLong(R.string.deleted); + getViewModel().removeSpecialDownloadListTask(list, isDeleteLocalFile, new Consumer() { + @Override + public void accept(Boolean aBoolean) { - dialog.dismiss(); + BackgroundJobManagerImpl.getInstance().startDownloadChainWorker(); - refreshData(); - } - }); - } - }); - } + //You never know which item a user will select, so we need to remove them one by one, and then resort. + for (FileTransferEntity fileTransferEntity : list) { + removeSpecialEntity(fileTransferEntity.uid); + } - private void showConfirmDialog(DialogInterface.OnClickListener listener) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); - builder.setTitle(R.string.delete); - builder.setMessage(R.string.delete_records_and_file); - builder.setPositiveButton(R.string.ok, listener); + getViewModel().getShowLoadingDialogLiveData().setValue(false); - builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); - builder.show(); + ToastUtils.showLong(R.string.deleted); + } + }); } @Override @@ -163,18 +186,9 @@ public void restartSelectedItems(List list) { @Override public void accept(Boolean aBoolean) throws Exception { ToastUtils.showLong(R.string.transfer_list_restart_all); - } - }); - - getViewModel().restartUpload(list, new Consumer() { - @Override - public void accept(Boolean aBoolean) throws Exception { - - // BackgroundJobManagerImpl.getInstance().startDownloadChainWorker(); } }); - } /** @@ -197,10 +211,24 @@ public void accept(Boolean aBoolean) throws Exception { * remove all download tasks */ public void removeAllTasks() { - showConfirmDialog(new DialogInterface.OnClickListener() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); + builder.setTitle(R.string.delete_records); + + String deleteFile = getString(R.string.delete_local_file_sametime); + CharSequence[] sequences = new CharSequence[1]; + sequences[0] = deleteFile; + boolean[] booleans = new boolean[1]; + booleans[0] = true; + builder.setMultiChoiceItems(sequences, booleans, new DialogInterface.OnMultiChoiceClickListener() { @Override - public void onClick(DialogInterface dialog, int which) { + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + booleans[which] = isChecked; + } + }); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { //cancel worker BackgroundJobManagerImpl.getInstance().cancelDownloadWorker(); @@ -211,10 +239,15 @@ public void accept(Boolean aBoolean) throws Exception { refreshData(); } - }); + }, booleans[0]); } }); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.show(); + + } + } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferActivity.java index a393ca992..0a8cfddc8 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferActivity.java @@ -18,6 +18,8 @@ import com.google.android.material.tabs.TabLayoutMediator; import com.seafile.seadroid2.R; import com.seafile.seadroid2.databinding.TransferListLayoutBinding; +import com.seafile.seadroid2.enums.TransferAction; +import com.seafile.seadroid2.enums.TransferStatus; import com.seafile.seadroid2.framework.notification.base.NotificationUtils; import com.seafile.seadroid2.ui.adapter.ViewPager2Adapter; import com.seafile.seadroid2.ui.base.BaseActivity; @@ -70,6 +72,12 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setTitle(R.string.transfer_tasks); } + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); /** this is hacky to explicitly call onNewIntent() * because it was never called when start the TransferActivity @@ -140,7 +148,7 @@ private void initViewPager() { viewPager2Adapter.addFragments(fragments); binding.pager.setAdapter(viewPager2Adapter); binding.pager.setOffscreenPageLimit(1); - + binding.pager.setUserInputEnabled(false); String[] tabs = getResources().getStringArray(R.array.transfer_list_titles); @@ -165,26 +173,13 @@ public boolean onCreateOptionsMenu(Menu menu) { return true; } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - // MenuItem cancel = menu.findItem(R.id.cancel_transfer_tasks); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - @Override public boolean onMenuItemClick(MenuItem item) { int whichTab = binding.slidingTabs.getSelectedTabPosition(); - if (item.getItemId() == R.id.cancel_transfer_tasks) { + if (item.getItemId() == android.R.id.home) { + finish(); + } else if (item.getItemId() == R.id.cancel_transfer_tasks) { if (whichTab == 0) { getDownloadFragment().cancelAllTasks(); } else { @@ -198,6 +193,19 @@ public boolean onMenuItemClick(MenuItem item) { getUploadFragment().removeAllTasks(); } } +// else if (item.getItemId() == R.id.retry_all_cancelled_transfer_tasks) { +// if (whichTab == 0) { +// getDownloadFragment().restartAllSpecialStatusTasks(TransferAction.DOWNLOAD, TransferStatus.CANCELLED); +// } else { +// getUploadFragment().restartAllSpecialStatusTasks(TransferAction.UPLOAD, TransferStatus.CANCELLED); +// } +// } else if (item.getItemId() == R.id.retry_all_cancelled_transfer_tasks) { +// if (whichTab == 0) { +// getDownloadFragment().restartAllSpecialStatusTasks(TransferAction.DOWNLOAD, TransferStatus.FAILED); +// } else { +// getUploadFragment().restartAllSpecialStatusTasks(TransferAction.UPLOAD, TransferStatus.FAILED); +// } +// } return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListAdapter.java index 5a97437db..834e088bf 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListAdapter.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListAdapter.java @@ -97,8 +97,12 @@ private void onBindHolder(TransferItemViewHolder holder, FileTransferEntity enti } //target path - String targetPath = Utils.pathJoin(entity.repo_name, entity.getParent_path()); - holder.binding.transferTargetPath.setText(targetPath); + if (!TextUtils.isEmpty(entity.repo_name)) { + String targetPath = Utils.pathJoin(entity.repo_name, entity.getParent_path()); + holder.binding.transferTargetPath.setText(targetPath); + } else { + holder.binding.transferTargetPath.setText(entity.getParent_path()); + } //file name holder.binding.transferFileName.setText(entity.file_name); diff --git a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListFragment.java index c8f764aeb..4cb504e6e 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListFragment.java @@ -23,18 +23,21 @@ import com.blankj.utilcode.util.ToastUtils; import com.chad.library.adapter4.BaseQuickAdapter; import com.chad.library.adapter4.QuickAdapterHelper; +import com.chad.library.adapter4.loadState.LoadState; +import com.chad.library.adapter4.loadState.trailing.TrailingLoadStateAdapter; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.seafile.seadroid2.R; -import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetHelper; -import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetMenuFragment; import com.seafile.seadroid2.databinding.LayoutFrameSwipeRvBinding; -import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; import com.seafile.seadroid2.enums.TransferAction; import com.seafile.seadroid2.enums.TransferStatus; +import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; import com.seafile.seadroid2.framework.worker.TransferEvent; import com.seafile.seadroid2.framework.worker.TransferWorker; +import com.seafile.seadroid2.ui.base.adapter.LoadMoreAdapter; import com.seafile.seadroid2.ui.base.fragment.BaseFragment; +import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetHelper; +import com.seafile.seadroid2.ui.bottomsheetmenu.BottomSheetMenuFragment; import com.seafile.seadroid2.view.TipsViews; import java.util.List; @@ -49,7 +52,15 @@ public abstract class TransferListFragment extends BaseFragment { protected TransferActivity activity = null; private LinearLayoutManager layoutManager; private TransferListViewModel viewModel; - private ConcurrentHashMap positionMap; + + /** + * key is uid, value is position + */ + private final ConcurrentHashMap positionMap = new ConcurrentHashMap<>(); + + private int pageIndex = 0; + private final int pageSize = 100; + private QuickAdapterHelper helper; @Override public void onAttach(@NonNull Context context) { @@ -89,11 +100,20 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat @Override public void onFirstResume() { super.onFirstResume(); - refreshData(); + loadNext(true); } private void initAdapter() { adapter = new TransferListAdapter(); + TextView tipView = TipsViews.getTipTextView(requireContext()); + if (getTransferAction() == TransferAction.DOWNLOAD) { + tipView.setText(R.string.no_download_tasks); + } else { + tipView.setText(R.string.no_upload_tasks); + } + adapter.setStateView(tipView); + adapter.setStateViewEnable(false); + adapter.setTransferAction(getTransferAction()); adapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { @Override @@ -106,6 +126,7 @@ public void onClick(@NonNull BaseQuickAdapter baseQuickAd } } }); + adapter.setOnItemLongClickListener(new BaseQuickAdapter.OnItemLongClickListener() { @Override public boolean onLongClick(@NonNull BaseQuickAdapter baseQuickAdapter, @NonNull View view, int i) { @@ -134,30 +155,82 @@ public boolean onLongClick(@NonNull BaseQuickAdapter base showBottomSheet(adapter.getItems().get(i)); }); + LoadMoreAdapter loadMoreAdapter = new LoadMoreAdapter(); + loadMoreAdapter.setOnLoadMoreListener(new TrailingLoadStateAdapter.OnTrailingListener() { + @Override + public void onLoad() { + loadNext(false); + } -// LogicLoadMoreAdapter loadMoreAdapter = getLogicLoadMoreAdapter(); + @Override + public void onFailRetry() { + } + + @Override + public boolean isAllowLoading() { + return !binding.swipeRefreshLayout.isRefreshing(); + } + }); - QuickAdapterHelper helper = new QuickAdapterHelper.Builder(adapter) + helper = new QuickAdapterHelper.Builder(adapter) + .setTrailingLoadStateAdapter(loadMoreAdapter) .build(); binding.rv.setAdapter(helper.getAdapter()); } + private void initViewModel() { getViewModel().getRefreshLiveData().observe(getViewLifecycleOwner(), aBoolean -> binding.swipeRefreshLayout.setRefreshing(aBoolean)); + getViewModel().getShowLoadingDialogLiveData().observe(getViewLifecycleOwner(), new Observer() { + @Override + public void onChanged(Boolean aBoolean) { + showLoadingDialog(aBoolean); + } + }); getViewModel().getTransferListLiveData().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(List list) { - positionMap = new ConcurrentHashMap<>(list.size()); + submitData(list); + } + }); + } - for (int i = 0; i < list.size(); i++) { - positionMap.put(list.get(i).uid, i); - } + private void submitData(List list) { + if (CollectionUtils.isEmpty(list)) { + if (pageIndex <= 1) { + adapter.setStateViewEnable(true); + adapter.submitList(null); + } + return; + } - notifyDataChanged(list); + if (pageIndex == 1) { + for (int i = 0; i < list.size(); i++) { + positionMap.put(list.get(i).uid, i); } - }); + adapter.submitList(list); + + } else { + int start = adapter.getItemCount(); + int end = start + list.size(); + + for (int i = start; i < end; i++) { + int index = i - start; + positionMap.put(list.get(index).uid, i); + } + adapter.addAll(list); + } + + if (list.size() < pageSize) { + helper.setTrailingLoadState(new LoadState.NotLoading(true)); + if (helper.getTrailingLoadStateAdapter() != null) { + helper.getTrailingLoadStateAdapter().checkDisableLoadMoreIfNotFullPage(); + } + } else { + helper.setTrailingLoadState(new LoadState.NotLoading(false)); + } } public void showBottomSheet(FileTransferEntity entity) { @@ -176,44 +249,77 @@ public void showBottomSheet(FileTransferEntity entity) { builder.removeMenu(R.id.upload); builder.removeMenu(R.id.download); - builder.removeMenu(R.id.pause); - - //not supported yet -// if (getTransferAction() == TransferAction.DOWNLOAD) { -// builder.removeMenu(R.id.upload); -// } else if (getTransferAction() == TransferAction.UPLOAD) { -// builder.removeMenu(R.id.download); -// } builder.show(getChildFragmentManager()); } protected void onBottomSheetFileDelete(FileTransferEntity entity) { - showDeleteConfirmDialog(entity); + if (TransferAction.DOWNLOAD == getTransferAction()) { + showDeleteDownloadConfirmDialog(entity); + } else { + showDeleteUploadConfirmDialog(entity); + } } - private void showDeleteConfirmDialog(FileTransferEntity entity) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); - builder.setTitle(R.string.delete); + private void showDeleteDownloadConfirmDialog(FileTransferEntity entity) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); + builder.setTitle(R.string.delete_records); - if (getTransferAction() == TransferAction.DOWNLOAD) { - builder.setMessage(R.string.delete_records_and_file); - } else {//do not delete file when upload - builder.setMessage(R.string.delete_records); - } + String deleteFile = getString(R.string.delete_local_file_sametime); + CharSequence[] sequences = new CharSequence[1]; + sequences[0] = deleteFile; + boolean[] booleans = new boolean[1]; + booleans[0] = true; + builder.setMultiChoiceItems(sequences, booleans, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + booleans[which] = isChecked; + } + }); + + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + getViewModel().getShowLoadingDialogLiveData().setValue(true); + + getViewModel().deleteTransferData(entity, booleans[0], getTransferAction(), new Consumer() { + @Override + public void accept(Boolean aBoolean) throws Exception { + ToastUtils.showLong(R.string.deleted); + + removeSpecialEntity(entity.uid); + + getViewModel().getShowLoadingDialogLiveData().setValue(false); + } + }); + dialog.dismiss(); + } + }); + + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.show(); + } + + private void showDeleteUploadConfirmDialog(FileTransferEntity entity) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); + builder.setTitle(R.string.delete_records); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - getViewModel().deleteTransferData(entity, getTransferAction(), new Consumer() { + getViewModel().getShowLoadingDialogLiveData().setValue(true); + + getViewModel().deleteTransferData(entity, false, getTransferAction(), new Consumer() { @Override public void accept(Boolean aBoolean) throws Exception { ToastUtils.showLong(R.string.deleted); - dialog.dismiss(); - refreshData(); + removeSpecialEntity(entity.uid); + + getViewModel().getShowLoadingDialogLiveData().setValue(false); } }); + dialog.dismiss(); } }); @@ -221,6 +327,36 @@ public void accept(Boolean aBoolean) throws Exception { builder.show(); } + public void restartAllSpecialStatusTasks(TransferAction transferAction, TransferStatus transferStatus) { + getViewModel().restartSpecialStatusTask(transferAction, transferStatus, new Consumer() { + @Override + public void accept(Boolean aBoolean) { + if (aBoolean) { + if (TransferAction.DOWNLOAD == transferAction) { + // + BackgroundJobManagerImpl.getInstance().cancelDownloadWorker(); + + // + BackgroundJobManagerImpl.getInstance().startDownloadChainWorker(); + } else { + + // + BackgroundJobManagerImpl.getInstance().cancelMediaWorker(); + BackgroundJobManagerImpl.getInstance().cancelFolderAutoUploadWorker(); + BackgroundJobManagerImpl.getInstance().cancelFileManualUploadWorker(); + + // + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(false); + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(false); + BackgroundJobManagerImpl.getInstance().startFileManualUploadWorker(); + } + } else { + ToastUtils.showLong(R.string.done); + } + } + }); + } + private void toggleAdapterItemSelectedOnLongClick(int i) { //action mode on if (!adapter.getActionMode()) { @@ -233,31 +369,55 @@ private void toggleAdapterItemSelectedOnLongClick(int i) { adapter.notifyItemChanged(i); } - public void startContextualActionMode() { - if (actionMode == null) { - // start the actionMode - actionMode = activity.startSupportActionMode(new ActionModeCallback()); - } - } - public abstract TransferAction getTransferAction(); public abstract void deleteSelectedItems(List list); public abstract void restartSelectedItems(List list); + public ConcurrentHashMap getPositionMap() { + return positionMap; + } + + protected void removeSpecialEntity(String uid) { + Integer integer = positionMap.get(uid); + if (integer == null) { + return; + } + + adapter.notifyItemRemoved(integer); + adapter.getItems().remove(integer.intValue()); + + resort(); + } + + protected void resort() { + // + positionMap.clear(); + + + List list = adapter.getItems(); + for (int i = 0; i < list.size(); i++) { + positionMap.put(list.get(i).uid, i); + } + } + protected void refreshData() { + pageIndex = 0; loadNext(true); } private void loadNext(boolean isShowRefresh) { - getViewModel().loadData(getTransferAction(), isShowRefresh); + pageIndex++; + + if (pageIndex <= 1) { + positionMap.clear(); + } + + getViewModel().loadData(getTransferAction(), pageIndex, pageSize, isShowRefresh); } public void notifyProgressById(String transferId, long transferredSize, int percent, String event) { - if (positionMap == null) { - return; - } if (TextUtils.isEmpty(transferId)) { return; @@ -304,32 +464,6 @@ public boolean isItemVisible(int position) { return position >= firstVisibleItemPosition && position <= lastVisibleItemPosition; } - - private void notifyDataChanged(List list) { - if (CollectionUtils.isEmpty(list)) { - showEmptyTip(); - } else { - adapter.submitList(list); - } - } - - private void showEmptyTip() { - if (getTransferAction() == TransferAction.DOWNLOAD) { - showAdapterTip(R.string.no_download_tasks); - } else { - showAdapterTip(R.string.no_upload_tasks); - } - } - - private void showAdapterTip(int textRes) { - adapter.submitList(null); - TextView tipView = TipsViews.getTipTextView(requireContext()); - tipView.setText(textRes); - tipView.setOnClickListener(v -> refreshData()); - adapter.setStateView(tipView); - adapter.setStateViewEnable(true); - } - public void cancelSelectItems() { if (adapter == null) return; @@ -341,6 +475,14 @@ public void cancelSelectItems() { } } + + public void startContextualActionMode() { + if (actionMode == null) { + // start the actionMode + actionMode = activity.startSupportActionMode(new ActionModeCallback()); + } + } + public ActionMode getActionMode() { return actionMode; } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListViewModel.java b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListViewModel.java index 039022d5b..5704e2f14 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListViewModel.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/TransferListViewModel.java @@ -6,20 +6,19 @@ import com.blankj.utilcode.util.FileUtils; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; -import com.seafile.seadroid2.framework.data.db.AppDatabase; -import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; import com.seafile.seadroid2.enums.TransferAction; import com.seafile.seadroid2.enums.TransferDataSource; import com.seafile.seadroid2.enums.TransferResult; import com.seafile.seadroid2.enums.TransferStatus; +import com.seafile.seadroid2.framework.data.db.AppDatabase; +import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; +import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.framework.worker.BackgroundJobManagerImpl; import com.seafile.seadroid2.framework.worker.upload.UploadFileManuallyWorker; import com.seafile.seadroid2.framework.worker.upload.UploadFolderFileAutomaticallyWorker; import com.seafile.seadroid2.framework.worker.upload.UploadMediaFileAutomaticallyWorker; import com.seafile.seadroid2.ui.base.viewmodel.BaseViewModel; -import com.seafile.seadroid2.framework.util.SLogs; -import java.util.ArrayList; import java.util.List; import io.reactivex.Completable; @@ -33,28 +32,44 @@ public class TransferListViewModel extends BaseViewModel { + private Account account; private final MutableLiveData> mTransferListLiveData = new MutableLiveData<>(); public MutableLiveData> getTransferListLiveData() { return mTransferListLiveData; } - public void loadData(TransferAction transferAction, boolean isShowRefresh) { + public void loadData(TransferAction transferAction, int pageIndex, int pageSize, boolean isShowRefresh) { if (isShowRefresh) { getRefreshLiveData().setValue(true); } - Account account = SupportAccountManager.getInstance().getCurrentAccount(); + if (account == null) { + account = SupportAccountManager.getInstance().getCurrentAccount(); + } if (account == null) { getRefreshLiveData().setValue(false); return; } - Single> single = queryPageData(account, transferAction); + int offset = (pageIndex - 1) * pageSize; + + Single> single; + if (TransferAction.UPLOAD == transferAction) { + single = AppDatabase + .getInstance() + .fileTransferDAO() + .getPageUploadListAsync(account.getSignature(), pageSize, offset); + } else { + single = AppDatabase + .getInstance() + .fileTransferDAO() + .getPageDownloadListAsync(account.getSignature(), pageSize, offset); + } - addSingleDisposable(single, pair -> { - getTransferListLiveData().setValue(pair); + addSingleDisposable(single, list -> { + getTransferListLiveData().setValue(list); if (isShowRefresh) { getRefreshLiveData().setValue(false); @@ -62,44 +77,8 @@ public void loadData(TransferAction transferAction, boolean isShowRefresh) { }); } - private Single> queryPageData(Account account, TransferAction transferAction) { - return Single.create(new SingleOnSubscribe>() { - @Override - public void subscribe(SingleEmitter> emitter) throws Exception { - - int pageSize = 100; - - List list = new ArrayList<>(); - for (int page = 1; page <= 10; page++) { - int offset = (page - 1) * pageSize; - - List temp; - if (TransferAction.UPLOAD == transferAction) { - temp = AppDatabase - .getInstance() - .fileTransferDAO() - .getPageUploadListSync(account.getSignature(), pageSize, offset); - } else { - temp = AppDatabase - .getInstance() - .fileTransferDAO() - .getPageDownloadListSync(account.getSignature(), pageSize, offset); - } - if (CollectionUtils.isEmpty(temp)) { - break; - } - - list.addAll(temp); - } - - SLogs.e("本次查询的大小:" + list.size()); - emitter.onSuccess(list); - } - }); - } - - public void deleteTransferData(FileTransferEntity fileTransferEntity, TransferAction transferAction, Consumer consumer) { + public void deleteTransferData(FileTransferEntity fileTransferEntity, boolean isDeleteLocalFile, TransferAction transferAction, Consumer consumer) { Single single = Single.create(new SingleOnSubscribe() { @Override @@ -110,7 +89,15 @@ public void subscribe(SingleEmitter emitter) throws Exception { BackgroundJobManagerImpl.getInstance().cancelDownloadWorker(); } + if (isDeleteLocalFile) { + FileUtils.delete(fileTransferEntity.target_path); + } + AppDatabase.getInstance().fileTransferDAO().deleteOne(fileTransferEntity); + + BackgroundJobManagerImpl.getInstance().startDownloadChainWorker(); + + } else if (TransferDataSource.FILE_BACKUP == fileTransferEntity.data_source) { if (fileTransferEntity.transfer_status == TransferStatus.IN_PROGRESS) { BackgroundJobManagerImpl.getInstance().cancelById(UploadFileManuallyWorker.UID); @@ -119,31 +106,34 @@ public void subscribe(SingleEmitter emitter) throws Exception { AppDatabase.getInstance().fileTransferDAO().deleteOne(fileTransferEntity); FileUtils.delete(fileTransferEntity.full_path); - BackgroundJobManagerImpl.getInstance().startFileUploadWorker(); + BackgroundJobManagerImpl.getInstance().startFileManualUploadWorker(); } else if (TransferDataSource.FOLDER_BACKUP == fileTransferEntity.data_source) { + // BackgroundJobManagerImpl.getInstance().cancelById(UploadFolderFileAutomaticallyWorker.UID); + //Delete data logically, not physically. fileTransferEntity.data_status = -1; fileTransferEntity.transfer_result = TransferResult.NO_RESULT; fileTransferEntity.transfer_status = TransferStatus.CANCELLED; fileTransferEntity.transferred_size = 0; - AppDatabase.getInstance().fileTransferDAO().insert(fileTransferEntity); + AppDatabase.getInstance().fileTransferDAO().update(fileTransferEntity); - BackgroundJobManagerImpl.getInstance().startFolderChainWorker(true); + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(true); } else if (TransferDataSource.ALBUM_BACKUP == fileTransferEntity.data_source) { BackgroundJobManagerImpl.getInstance().cancelById(UploadMediaFileAutomaticallyWorker.UID); + //Delete data logically, not physically. fileTransferEntity.data_status = -1; fileTransferEntity.transfer_result = TransferResult.NO_RESULT; fileTransferEntity.transfer_status = TransferStatus.CANCELLED; fileTransferEntity.transferred_size = 0; - AppDatabase.getInstance().fileTransferDAO().insert(fileTransferEntity); + AppDatabase.getInstance().fileTransferDAO().update(fileTransferEntity); - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(true); + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(true); } emitter.onSuccess(true); } @@ -152,10 +142,6 @@ public void subscribe(SingleEmitter emitter) throws Exception { addSingleDisposable(single, new Consumer() { @Override public void accept(Boolean aBoolean) throws Exception { - if (transferAction == TransferAction.DOWNLOAD) { - FileUtils.delete(fileTransferEntity.target_path); - } - consumer.accept(true); } }); @@ -163,8 +149,8 @@ public void accept(Boolean aBoolean) throws Exception { public void cancelAllUploadTask(Consumer consumer) { Account account = SupportAccountManager.getInstance().getCurrentAccount(); - List dataSources = CollectionUtils.newArrayList(TransferDataSource.ALBUM_BACKUP, TransferDataSource.FOLDER_BACKUP); - Completable completable = AppDatabase.getInstance().fileTransferDAO().cancelByDataSource(account.getSignature(), dataSources); + List dataSources = CollectionUtils.newArrayList(TransferDataSource.ALBUM_BACKUP, TransferDataSource.FILE_BACKUP, TransferDataSource.FOLDER_BACKUP); + Completable completable = AppDatabase.getInstance().fileTransferDAO().cancelAllByDataSource(account.getSignature(), dataSources); addCompletableDisposable(completable, new Action() { @Override public void run() throws Exception { @@ -175,8 +161,12 @@ public void run() throws Exception { public void cancelAllDownloadTask(Consumer consumer) { Account account = SupportAccountManager.getInstance().getCurrentAccount(); + if (account == null) { + return; + } + List dataSources = CollectionUtils.newArrayList(TransferDataSource.DOWNLOAD); - Completable completable = AppDatabase.getInstance().fileTransferDAO().cancelByDataSource(account.getSignature(), dataSources); + Completable completable = AppDatabase.getInstance().fileTransferDAO().cancelAllByDataSource(account.getSignature(), dataSources); addCompletableDisposable(completable, new Action() { @Override public void run() throws Exception { @@ -185,30 +175,64 @@ public void run() throws Exception { }); } - public void removeAllDownloadTask(Consumer consumer) { + + public void removeSpecialDownloadListTask(List list, boolean isDeleteLocalFile, Consumer consumer) { + Single single = Single.create(new SingleOnSubscribe() { + @Override + public void subscribe(SingleEmitter emitter) throws Exception { + + for (FileTransferEntity entity : list) { + //delete record + AppDatabase.getInstance().fileTransferDAO().deleteOne(entity); + + if (isDeleteLocalFile) { + FileUtils.delete(entity.target_path); + } + SLogs.d("deleted : " + entity.target_path); + } + + emitter.onSuccess(true); + } + }); + + addSingleDisposable(single, new Consumer() { + @Override + public void accept(Boolean aBoolean) throws Exception { + if (consumer != null) { + consumer.accept(true); + } + } + }); + } + + public void removeAllDownloadTask(Consumer consumer, boolean isDeleteLocalFile) { getRefreshLiveData().setValue(true); Account account = SupportAccountManager.getInstance().getCurrentAccount(); + if (account == null) { + getRefreshLiveData().setValue(false); + return; + } + List features = CollectionUtils.newArrayList(TransferDataSource.DOWNLOAD); - Single> single = AppDatabase.getInstance().fileTransferDAO().getListByFeatAsync(account.getSignature(), features); + //query all data, including deleted data, based on different users + Single> single = AppDatabase.getInstance().fileTransferDAO().getListByDataSourceAsync(account.getSignature(), features); Single single1 = single.flatMap(new Function, SingleSource>() { @Override - public SingleSource apply(List entities) throws Exception { + public SingleSource apply(List transferList) throws Exception { return Single.create(new SingleOnSubscribe() { @Override public void subscribe(SingleEmitter emitter) throws Exception { - - AppDatabase.getInstance().fileTransferDAO().deleteAllByAction(account.getSignature(), TransferAction.DOWNLOAD); - - for (FileTransferEntity entity : entities) { - //delete record - if (entity.transfer_action == TransferAction.DOWNLOAD) { + if (isDeleteLocalFile) { + for (FileTransferEntity entity : transferList) { FileUtils.delete(entity.target_path); SLogs.d("deleted : " + entity.target_path); } } + AppDatabase.getInstance().fileTransferDAO().deleteAllByAction(account.getSignature(), TransferAction.DOWNLOAD); + emitter.onSuccess(true); } }); @@ -232,11 +256,11 @@ public void subscribe(SingleEmitter emitter) throws Exception { for (FileTransferEntity entity : list) { entity.data_status = -1; - entity.transfer_result = TransferResult.NO_RESULT; entity.transfer_status = TransferStatus.CANCELLED; + entity.transfer_result = TransferResult.USER_CANCELLED; entity.transferred_size = 0; - AppDatabase.getInstance().fileTransferDAO().insert(entity); + AppDatabase.getInstance().fileTransferDAO().update(entity); } emitter.onSuccess(true); @@ -253,55 +277,60 @@ public void accept(Boolean aBoolean) throws Exception { }); } - public void removeSpecialDownloadListTask(List list, Consumer consumer) { - Single single = Single.create(new SingleOnSubscribe() { - @Override - public void subscribe(SingleEmitter emitter) throws Exception { - - for (FileTransferEntity entity : list) { - //delete record - AppDatabase.getInstance().fileTransferDAO().deleteOne(entity); - - FileUtils.delete(entity.target_path); - SLogs.d("deleted : " + entity.target_path); - } + public void removeAllUploadTask(Consumer consumer) { + getShowLoadingDialogLiveData().setValue(true); - emitter.onSuccess(true); - } - }); + Account account = SupportAccountManager.getInstance().getCurrentAccount(); - addSingleDisposable(single, new Consumer() { + Completable completable = AppDatabase.getInstance().fileTransferDAO().removeAllUploadByAccount(account.getSignature()); + addCompletableDisposable(completable, new Action() { @Override - public void accept(Boolean aBoolean) throws Exception { + public void run() throws Exception { if (consumer != null) { consumer.accept(true); } + + getShowLoadingDialogLiveData().setValue(false); } }); + } - public void removeAllUploadTask(Consumer consumer) { - getRefreshLiveData().setValue(true); + public void restartSpecialStatusTask(TransferAction transferAction, TransferStatus transferStatus, Consumer consumer) { + getShowLoadingDialogLiveData().setValue(true); Account account = SupportAccountManager.getInstance().getCurrentAccount(); - List features = CollectionUtils.newArrayList(TransferDataSource.FILE_BACKUP, TransferDataSource.FOLDER_BACKUP); - Single> single = AppDatabase.getInstance().fileTransferDAO().getListByFeatAsync(account.getSignature(), features); + if (account == null) { + getShowLoadingDialogLiveData().setValue(false); + return; + } + Single> single = AppDatabase.getInstance().fileTransferDAO().getByActionAndStatusAsync(account.getSignature(), transferAction, transferStatus); Single single1 = single.flatMap(new Function, SingleSource>() { @Override - public SingleSource apply(List entities) throws Exception { + public SingleSource apply(List list) throws Exception { + + if (CollectionUtils.isEmpty(list)) { + return Single.just(false); + } + return Single.create(new SingleOnSubscribe() { @Override public void subscribe(SingleEmitter emitter) throws Exception { - AppDatabase.getInstance().fileTransferDAO().cancelAllWithFileBackup(); - for (FileTransferEntity entity : entities) { - if (entity.transfer_action == TransferAction.DOWNLOAD) { + for (FileTransferEntity entity : list) { + if (transferAction == TransferAction.DOWNLOAD) { FileUtils.delete(entity.target_path); SLogs.d("deleted : " + entity.target_path); } - } + entity.transfer_status = TransferStatus.WAITING; + entity.transfer_result = TransferResult.NO_RESULT; + entity.transferred_size = 0; + entity.action_end_at = 0; + + AppDatabase.getInstance().fileTransferDAO().update(entity); + } emitter.onSuccess(true); } }); @@ -310,12 +339,14 @@ public void subscribe(SingleEmitter emitter) throws Exception { addSingleDisposable(single1, new Consumer() { @Override - public void accept(Boolean b) throws Exception { + public void accept(Boolean aBoolean) throws Exception { if (consumer != null) { - consumer.accept(true); + consumer.accept(aBoolean); } + getShowLoadingDialogLiveData().setValue(false); } }); + } public void restartUpload(List list, Consumer consumer) { diff --git a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/UploadListFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/UploadListFragment.java index b7e323623..a8961ecc3 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/UploadListFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/transfer_list/UploadListFragment.java @@ -14,6 +14,7 @@ import com.blankj.utilcode.util.ToastUtils; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.seafile.seadroid2.R; +import com.seafile.seadroid2.enums.TransferStatus; import com.seafile.seadroid2.framework.data.db.entities.FileTransferEntity; import com.seafile.seadroid2.enums.TransferAction; import com.seafile.seadroid2.enums.TransferDataSource; @@ -101,7 +102,7 @@ private void doWorkInfoLiveData(TransferDataSource dataSource, WorkInfo workInfo Data progressData = workInfo.getProgress(); String progressEvent = progressData.getString(TransferWorker.KEY_DATA_EVENT); - if (TransferEvent.EVENT_CANCEL_OUT_OF_QUOTA.equals(progressEvent)) { + if (TransferEvent.EVENT_CANCEL_WITH_OUT_OF_QUOTA.equals(progressEvent)) { refreshData(); } else if (TransferEvent.EVENT_TRANSFERRING.equals(progressEvent)) { @@ -113,7 +114,6 @@ private void doWorkInfoLiveData(TransferDataSource dataSource, WorkInfo workInfo SLogs.d("upload: " + fileName + ", percent:" + percent + ", total_size:" + totalSize + ", dataSource: " + dataSource); - if (TextUtils.equals(transferId, lastTransferId)) { notifyProgressById(transferId, transferredSize, percent, progressEvent); } else { @@ -149,7 +149,7 @@ public void deleteSelectedItems(List list) { @Override public void onClick(DialogInterface dialog, int which) { - BackgroundJobManagerImpl.getInstance().cancelAllFolderUploadWorker(); + BackgroundJobManagerImpl.getInstance().cancelFolderAutoUploadWorker(); getViewModel().removeSpecialUploadListTask(list, new Consumer() { @Override @@ -157,13 +157,18 @@ public void accept(Boolean aBoolean) throws Exception { //todo 检查此处逻辑 // BackgroundJobManagerImpl.getInstance().startFolderUploadWorker(); - ToastUtils.showLong(R.string.deleted); + //You never know which item a user will select, so we need to remove them one by one, and then resort. + for (FileTransferEntity fileTransferEntity : list) { + removeSpecialEntity(fileTransferEntity.uid); + } - dialog.dismiss(); + getViewModel().getShowLoadingDialogLiveData().setValue(false); - refreshData(); + ToastUtils.showLong(R.string.deleted); } }); + + dialog.dismiss(); } }); } @@ -176,8 +181,8 @@ public void restartSelectedItems(List list) { public void accept(Boolean aBoolean) throws Exception { //todo 检查此处逻辑 - BackgroundJobManagerImpl.getInstance().startFolderChainWorker(true); - BackgroundJobManagerImpl.getInstance().startMediaChainWorker(true); + BackgroundJobManagerImpl.getInstance().startFolderAutoBackupWorkerChain(true); + BackgroundJobManagerImpl.getInstance().startMediaWorkerChain(true); } }); } @@ -187,8 +192,8 @@ public void accept(Boolean aBoolean) throws Exception { */ public void cancelAllTasks() { - BackgroundJobManagerImpl.getInstance().cancelAllFolderUploadWorker(); - BackgroundJobManagerImpl.getInstance().cancelAllMediaWorker(); + BackgroundJobManagerImpl.getInstance().cancelFolderAutoUploadWorker(); + BackgroundJobManagerImpl.getInstance().cancelMediaWorker(); getViewModel().cancelAllUploadTask(new Consumer() { @Override @@ -205,12 +210,11 @@ public void accept(Boolean aBoolean) throws Exception { * remove all download tasks */ public void removeAllTasks() { - //todo showConfirmDialog(new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // - BackgroundJobManagerImpl.getInstance().cancelAllFolderUploadWorker(); + BackgroundJobManagerImpl.getInstance().cancelFolderAutoUploadWorker(); // getViewModel().removeAllUploadTask(new Consumer() { @@ -227,7 +231,7 @@ public void accept(Boolean aBoolean) throws Exception { } private void showConfirmDialog(DialogInterface.OnClickListener listener) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); builder.setTitle(R.string.delete); builder.setMessage(R.string.delete_records); builder.setPositiveButton(R.string.ok, listener); @@ -235,6 +239,5 @@ private void showConfirmDialog(DialogInterface.OnClickListener listener) { builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); builder.show(); } - } diff --git a/app/src/main/java/com/seafile/seadroid2/view/HideBottomBehavior.java b/app/src/main/java/com/seafile/seadroid2/view/HideBottomBehavior.java index c4d2e2efa..e06b65166 100644 --- a/app/src/main/java/com/seafile/seadroid2/view/HideBottomBehavior.java +++ b/app/src/main/java/com/seafile/seadroid2/view/HideBottomBehavior.java @@ -6,6 +6,7 @@ import android.widget.LinearLayout; import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.view.ViewCompat; diff --git a/app/src/main/java/com/seafile/seadroid2/view/NestedWebView.java b/app/src/main/java/com/seafile/seadroid2/view/NestedWebView.java index 1215cfe65..8c632d55c 100644 --- a/app/src/main/java/com/seafile/seadroid2/view/NestedWebView.java +++ b/app/src/main/java/com/seafile/seadroid2/view/NestedWebView.java @@ -17,13 +17,15 @@ import androidx.core.view.NestedScrollingChildHelper; import androidx.core.view.ViewCompat; +import com.github.lzyzsd.jsbridge.BridgeWebView; + import org.jetbrains.annotations.Nullable; /** * WebView compatible with CoordinatorLayout by snachmsm * The implementation based on NestedScrollView of design library androidx v1.0.1 */ -public class NestedWebView extends WebView implements NestedScrollingChild3 { +public class NestedWebView extends BridgeWebView implements NestedScrollingChild3 { private static final String TAG = "NestedWebView"; private static final int INVALID_POINTER = -1; diff --git a/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/GravitySnapHelper.java b/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/GravitySnapHelper.java new file mode 100644 index 000000000..c5e8af2c6 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/GravitySnapHelper.java @@ -0,0 +1,679 @@ +package com.seafile.seadroid2.view.snap_recyclerview; + +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.widget.Scroller; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; +import androidx.core.text.TextUtilsCompat; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.LinearSnapHelper; +import androidx.recyclerview.widget.OrientationHelper; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Locale; + +/** + * A {@link LinearSnapHelper} that allows snapping to an edge or to the center. + *

      + * Possible snap positions: + * {@link Gravity#START}, {@link Gravity#TOP}, {@link Gravity#END}, {@link Gravity#BOTTOM}, + * {@link Gravity#CENTER}. + *

      + * To customize the scroll duration, use {@link GravitySnapHelper#setScrollMsPerInch(float)}. + *

      + * To customize the maximum scroll distance during flings, + * use {@link GravitySnapHelper#setMaxFlingSizeFraction(float)} + * or {@link GravitySnapHelper#setMaxFlingDistance(int)} + */ +public class GravitySnapHelper extends LinearSnapHelper { + + public static final int FLING_DISTANCE_DISABLE = -1; + public static final float FLING_SIZE_FRACTION_DISABLE = -1f; + private int gravity; + private boolean isRtl; + private boolean snapLastItem; + private int nextSnapPosition; + private boolean isScrolling = false; + private boolean snapToPadding = false; + private float scrollMsPerInch = 100f; + private int maxFlingDistance = FLING_DISTANCE_DISABLE; + private float maxFlingSizeFraction = FLING_SIZE_FRACTION_DISABLE; + private OrientationHelper verticalHelper; + private OrientationHelper horizontalHelper; + private SnapListener listener; + private RecyclerView recyclerView; + private final RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + GravitySnapHelper.this.onScrollStateChanged(newState); + } + }; + + public GravitySnapHelper(int gravity) { + this(gravity, false, null); + } + + public GravitySnapHelper(int gravity, @NonNull SnapListener snapListener) { + this(gravity, false, snapListener); + } + + public GravitySnapHelper(int gravity, boolean enableSnapLastItem) { + this(gravity, enableSnapLastItem, null); + } + + public GravitySnapHelper(int gravity, boolean enableSnapLastItem, + @Nullable SnapListener snapListener) { + if (gravity != Gravity.START + && gravity != Gravity.END + && gravity != Gravity.BOTTOM + && gravity != Gravity.TOP + && gravity != Gravity.CENTER) { + throw new IllegalArgumentException("Invalid gravity value. Use START " + + "| END | BOTTOM | TOP | CENTER constants"); + } + this.snapLastItem = enableSnapLastItem; + this.gravity = gravity; + this.listener = snapListener; + } + + @Override + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) + throws IllegalStateException { + if (this.recyclerView != null) { + this.recyclerView.removeOnScrollListener(scrollListener); + } + if (recyclerView != null) { + recyclerView.setOnFlingListener(null); + if (gravity == Gravity.START || gravity == Gravity.END) { + isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) + == ViewCompat.LAYOUT_DIRECTION_RTL; + } + recyclerView.addOnScrollListener(scrollListener); + this.recyclerView = recyclerView; + } else { + this.recyclerView = null; + } + super.attachToRecyclerView(recyclerView); + } + + @Override + @Nullable + public View findSnapView(@NonNull RecyclerView.LayoutManager lm) { + return findSnapView(lm, true); + } + + @Nullable + public View findSnapView(@NonNull RecyclerView.LayoutManager lm, boolean checkEdgeOfList) { + View snapView = null; + + switch (gravity) { + case Gravity.START: + snapView = findView(lm, getHorizontalHelper(lm), Gravity.START, checkEdgeOfList); + break; + case Gravity.END: + snapView = findView(lm, getHorizontalHelper(lm), Gravity.END, checkEdgeOfList); + break; + case Gravity.TOP: + snapView = findView(lm, getVerticalHelper(lm), Gravity.START, checkEdgeOfList); + break; + case Gravity.BOTTOM: + snapView = findView(lm, getVerticalHelper(lm), Gravity.END, checkEdgeOfList); + break; + case Gravity.CENTER: + if (lm.canScrollHorizontally()) { + snapView = findView(lm, getHorizontalHelper(lm), Gravity.CENTER, + checkEdgeOfList); + } else { + snapView = findView(lm, getVerticalHelper(lm), Gravity.CENTER, + checkEdgeOfList); + } + break; + } + if (snapView != null) { + nextSnapPosition = recyclerView.getChildAdapterPosition(snapView); + } else { + nextSnapPosition = RecyclerView.NO_POSITION; + } + return snapView; + } + + @Override + @NonNull + public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, + @NonNull View targetView) { + if (gravity == Gravity.CENTER) { + //noinspection ConstantConditions + return super.calculateDistanceToFinalSnap(layoutManager, targetView); + } + + int[] out = new int[2]; + + if (!(layoutManager instanceof LinearLayoutManager)) { + return out; + } + + LinearLayoutManager lm = (LinearLayoutManager) layoutManager; + + if (lm.canScrollHorizontally()) { + if ((isRtl && gravity == Gravity.END) || (!isRtl && gravity == Gravity.START)) { + out[0] = getDistanceToStart(targetView, getHorizontalHelper(lm)); + } else { + out[0] = getDistanceToEnd(targetView, getHorizontalHelper(lm)); + } + } else if (lm.canScrollVertically()) { + if (gravity == Gravity.TOP) { + out[1] = getDistanceToStart(targetView, getVerticalHelper(lm)); + } else { + out[1] = getDistanceToEnd(targetView, getVerticalHelper(lm)); + } + } + return out; + } + + @Override + @NonNull + public int[] calculateScrollDistance(int velocityX, int velocityY) { + if (recyclerView == null + || (verticalHelper == null && horizontalHelper == null) + || (maxFlingDistance == FLING_DISTANCE_DISABLE + && maxFlingSizeFraction == FLING_SIZE_FRACTION_DISABLE)) { + return super.calculateScrollDistance(velocityX, velocityY); + } + final int[] out = new int[2]; + Scroller scroller = new Scroller(recyclerView.getContext(), + new DecelerateInterpolator()); + int maxDistance = getFlingDistance(); + scroller.fling(0, 0, velocityX, velocityY, + -maxDistance, maxDistance, + -maxDistance, maxDistance); + out[0] = scroller.getFinalX(); + out[1] = scroller.getFinalY(); + return out; + } + + @Nullable + @Override + public RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) { + if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider) + || recyclerView == null) { + return null; + } + return new LinearSmoothScroller(recyclerView.getContext()) { + @Override + protected void onTargetFound(View targetView, + RecyclerView.State state, + Action action) { + if (recyclerView == null || recyclerView.getLayoutManager() == null) { + // The associated RecyclerView has been removed so there is no action to take. + return; + } + + int[] snapDistances = calculateDistanceToFinalSnap(recyclerView.getLayoutManager(), targetView); + final int dx = snapDistances[0]; + final int dy = snapDistances[1]; + final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); + if (time > 0) { + action.update(dx, dy, time, mDecelerateInterpolator); + } + } + + @Override + protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { + return scrollMsPerInch / displayMetrics.densityDpi; + } + }; + } + + /** + * Sets a {@link SnapListener} to listen for snap events + * + * @param listener a {@link SnapListener} that'll receive snap events or null to clear it + */ + public void setSnapListener(@Nullable SnapListener listener) { + this.listener = listener; + } + + /** + * Changes the gravity of this {@link GravitySnapHelper} + * and dispatches a smooth scroll for the new snap position. + * + * @param newGravity one of the following: {@link Gravity#START}, {@link Gravity#TOP}, + * {@link Gravity#END}, {@link Gravity#BOTTOM}, {@link Gravity#CENTER} + * @param smooth true if we should smooth scroll to new edge, false otherwise + */ + public void setGravity(int newGravity, Boolean smooth) { + if (this.gravity != newGravity) { + this.gravity = newGravity; + updateSnap(smooth, false); + } + } + + /** + * Updates the current view to be snapped + * + * @param smooth true if we should smooth scroll, false otherwise + * @param checkEdgeOfList true if we should check if we're at an edge of the list + * and snap according to {@link GravitySnapHelper#getSnapLastItem()}, + * or false to force snapping to the nearest view + */ + public void updateSnap(Boolean smooth, Boolean checkEdgeOfList) { + if (recyclerView == null || recyclerView.getLayoutManager() == null) { + return; + } + final RecyclerView.LayoutManager lm = recyclerView.getLayoutManager(); + View snapView = findSnapView(lm, checkEdgeOfList); + if (snapView != null) { + int[] out = calculateDistanceToFinalSnap(lm, snapView); + if (smooth) { + recyclerView.smoothScrollBy(out[0], out[1]); + } else { + recyclerView.scrollBy(out[0], out[1]); + } + } + } + + /** + * This method will only work if there's a ViewHolder for the given position. + * + * @return true if scroll was successful, false otherwise + */ + public boolean scrollToPosition(int position) { + if (position == RecyclerView.NO_POSITION) { + return false; + } + return scrollTo(position, false); + } + + /** + * Unlike {@link GravitySnapHelper#scrollToPosition(int)}, + * this method will generally always find a snap view if the position is valid. + *

      + * The smooth scroller from {@link GravitySnapHelper#createScroller(RecyclerView.LayoutManager)} + * will be used, and so will {@link GravitySnapHelper#scrollMsPerInch} for the scroll velocity + * + * @return true if scroll was successful, false otherwise + */ + public boolean smoothScrollToPosition(int position) { + if (position == RecyclerView.NO_POSITION) { + return false; + } + return scrollTo(position, true); + } + + /** + * Get the current gravity being applied + * + * @return one of the following: {@link Gravity#START}, {@link Gravity#TOP}, {@link Gravity#END}, + * {@link Gravity#BOTTOM}, {@link Gravity#CENTER} + */ + public int getGravity() { + return this.gravity; + } + + /** + * Changes the gravity of this {@link GravitySnapHelper} + * and dispatches a smooth scroll for the new snap position. + * + * @param newGravity one of the following: {@link Gravity#START}, {@link Gravity#TOP}, + * {@link Gravity#END}, {@link Gravity#BOTTOM}, {@link Gravity#CENTER} + */ + public void setGravity(int newGravity) { + setGravity(newGravity, true); + } + + /** + * @return true if this SnapHelper should snap to the last item + */ + public boolean getSnapLastItem() { + return snapLastItem; + } + + /** + * Enable snapping of the last item that's snappable. + * The default value is false, because you can't see the last item completely + * if this is enabled. + * + * @param snap true if you want to enable snapping of the last snappable item + */ + public void setSnapLastItem(boolean snap) { + snapLastItem = snap; + } + + /** + * @return last distance set through {@link GravitySnapHelper#setMaxFlingDistance(int)} + * or {@link GravitySnapHelper#FLING_DISTANCE_DISABLE} if we're not limiting the fling distance + */ + public int getMaxFlingDistance() { + return maxFlingDistance; + } + + /** + * Changes the max fling distance in absolute values. + * + * @param distance max fling distance in pixels + * or {@link GravitySnapHelper#FLING_DISTANCE_DISABLE} + * to disable fling limits + */ + public void setMaxFlingDistance(@Px int distance) { + maxFlingDistance = distance; + maxFlingSizeFraction = FLING_SIZE_FRACTION_DISABLE; + } + + /** + * @return last distance set through {@link GravitySnapHelper#setMaxFlingSizeFraction(float)} + * or {@link GravitySnapHelper#FLING_SIZE_FRACTION_DISABLE} + * if we're not limiting the fling distance + */ + public float getMaxFlingSizeFraction() { + return maxFlingSizeFraction; + } + + /** + * Changes the max fling distance depending on the available size of the RecyclerView. + *

      + * Example: if you pass 0.5f and the RecyclerView measures 600dp, + * the max fling distance will be 300dp. + * + * @param fraction size fraction to be used for the max fling distance + * or {@link GravitySnapHelper#FLING_SIZE_FRACTION_DISABLE} + * to disable fling limits + */ + public void setMaxFlingSizeFraction(float fraction) { + maxFlingDistance = FLING_DISTANCE_DISABLE; + maxFlingSizeFraction = fraction; + } + + /** + * @return last scroll speed set through {@link GravitySnapHelper#setScrollMsPerInch(float)} + * or 100f + */ + public float getScrollMsPerInch() { + return scrollMsPerInch; + } + + /** + * Sets the scroll duration in ms per inch. + *

      + * Default value is 100.0f + *

      + * This value will be used in + * {@link GravitySnapHelper#createScroller(RecyclerView.LayoutManager)} + * + * @param ms scroll duration in ms per inch + */ + public void setScrollMsPerInch(float ms) { + scrollMsPerInch = ms; + } + + /** + * @return true if this SnapHelper should snap to the padding. Defaults to false. + */ + public boolean getSnapToPadding() { + return snapToPadding; + } + + /** + * If true, GravitySnapHelper will snap to the gravity edge + * plus any amount of padding that was set in the RecyclerView. + *

      + * The default value is false. + * + * @param snapToPadding true if you want to snap to the padding + */ + public void setSnapToPadding(boolean snapToPadding) { + this.snapToPadding = snapToPadding; + } + + /** + * @return the position of the current view that's snapped + * or {@link RecyclerView#NO_POSITION} in case there's none. + */ + public int getCurrentSnappedPosition() { + if (recyclerView != null && recyclerView.getLayoutManager() != null) { + View snappedView = findSnapView(recyclerView.getLayoutManager()); + if (snappedView != null) { + return recyclerView.getChildAdapterPosition(snappedView); + } + } + return RecyclerView.NO_POSITION; + } + + private int getFlingDistance() { + if (maxFlingSizeFraction != FLING_SIZE_FRACTION_DISABLE) { + if (verticalHelper != null) { + return (int) (recyclerView.getHeight() * maxFlingSizeFraction); + } else if (horizontalHelper != null) { + return (int) (recyclerView.getWidth() * maxFlingSizeFraction); + } else { + return Integer.MAX_VALUE; + } + } else if (maxFlingDistance != FLING_DISTANCE_DISABLE) { + return maxFlingDistance; + } else { + return Integer.MAX_VALUE; + } + } + + /** + * @return true if the scroll will snap to a view, false otherwise + */ + private boolean scrollTo(int position, boolean smooth) { + if (recyclerView.getLayoutManager() != null) { + if (smooth) { + RecyclerView.SmoothScroller smoothScroller = createScroller(recyclerView.getLayoutManager()); + if (smoothScroller != null) { + smoothScroller.setTargetPosition(position); + recyclerView.getLayoutManager().startSmoothScroll(smoothScroller); + return true; + } + } else { + RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position); + if (viewHolder != null) { + int[] distances = calculateDistanceToFinalSnap( + recyclerView.getLayoutManager(), + viewHolder.itemView); + recyclerView.scrollBy(distances[0], distances[1]); + return true; + } + } + } + return false; + } + + private int getDistanceToStart(View targetView, @NonNull OrientationHelper helper) { + int distance; + // If we don't care about padding, just snap to the start of the view + if (!snapToPadding) { + int childStart = helper.getDecoratedStart(targetView); + if (childStart >= helper.getStartAfterPadding() / 2) { + distance = childStart - helper.getStartAfterPadding(); + } else { + distance = childStart; + } + } else { + distance = helper.getDecoratedStart(targetView) - helper.getStartAfterPadding(); + } + return distance; + } + + private int getDistanceToEnd(View targetView, @NonNull OrientationHelper helper) { + int distance; + + if (!snapToPadding) { + int childEnd = helper.getDecoratedEnd(targetView); + if (childEnd >= helper.getEnd() - (helper.getEnd() - helper.getEndAfterPadding()) / 2) { + distance = helper.getDecoratedEnd(targetView) - helper.getEnd(); + } else { + distance = childEnd - helper.getEndAfterPadding(); + } + } else { + distance = helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding(); + } + + return distance; + } + + /** + * Returns the first view that we should snap to. + * + * @param layoutManager the RecyclerView's LayoutManager + * @param helper orientation helper to calculate view sizes + * @param gravity gravity to find the closest view + * @return the first view in the LayoutManager to snap to, or null if we shouldn't snap to any + */ + @Nullable + private View findView(@NonNull RecyclerView.LayoutManager layoutManager, + @NonNull OrientationHelper helper, + int gravity, + boolean checkEdgeOfList) { + + if (layoutManager.getChildCount() == 0 || !(layoutManager instanceof LinearLayoutManager)) { + return null; + } + + final LinearLayoutManager lm = (LinearLayoutManager) layoutManager; + + // If we're at an edge of the list, we shouldn't snap + // to avoid having the last item not completely visible. + if (checkEdgeOfList && (isAtEdgeOfList(lm) && !snapLastItem)) { + return null; + } + + View edgeView = null; + int distanceToTarget = Integer.MAX_VALUE; + final int center; + if (layoutManager.getClipToPadding()) { + center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + } else { + center = helper.getEnd() / 2; + } + + final boolean snapToStart = (gravity == Gravity.START && !isRtl) + || (gravity == Gravity.END && isRtl); + + final boolean snapToEnd = (gravity == Gravity.START && isRtl) + || (gravity == Gravity.END && !isRtl); + + for (int i = 0; i < lm.getChildCount(); i++) { + View currentView = lm.getChildAt(i); + int currentViewDistance; + if (snapToStart) { + if (!snapToPadding) { + currentViewDistance = Math.abs(helper.getDecoratedStart(currentView)); + } else { + currentViewDistance = Math.abs(helper.getStartAfterPadding() + - helper.getDecoratedStart(currentView)); + } + } else if (snapToEnd) { + if (!snapToPadding) { + currentViewDistance = Math.abs(helper.getDecoratedEnd(currentView) + - helper.getEnd()); + } else { + currentViewDistance = Math.abs(helper.getEndAfterPadding() + - helper.getDecoratedEnd(currentView)); + } + } else { + int ds = helper.getDecoratedStart(currentView); + int dm = helper.getDecoratedMeasurement(currentView); + currentViewDistance = Math.abs(ds + (dm / 2) - center); + } + if (currentViewDistance < distanceToTarget) { + distanceToTarget = currentViewDistance; + edgeView = currentView; + } + } + return edgeView; + } + + private boolean isAtEdgeOfList(LinearLayoutManager lm) { + if ((!lm.getReverseLayout() && gravity == Gravity.START) + || (lm.getReverseLayout() && gravity == Gravity.END) + || (!lm.getReverseLayout() && gravity == Gravity.TOP) + || (lm.getReverseLayout() && gravity == Gravity.BOTTOM)) { + return lm.findLastCompletelyVisibleItemPosition() == lm.getItemCount() - 1; + } else if (gravity == Gravity.CENTER) { + return lm.findFirstCompletelyVisibleItemPosition() == 0 + || lm.findLastCompletelyVisibleItemPosition() == lm.getItemCount() - 1; + } else { + return lm.findFirstCompletelyVisibleItemPosition() == 0; + } + } + + /** + * Dispatches a {@link SnapListener#onSnap(int)} event if the snapped position + * is different than {@link RecyclerView#NO_POSITION}. + *

      + * When {@link GravitySnapHelper#findSnapView(RecyclerView.LayoutManager)} returns null, + * {@link GravitySnapHelper#dispatchSnapChangeWhenPositionIsUnknown()} is called + * + * @param newState the new RecyclerView scroll state + */ + private void onScrollStateChanged(int newState) { + if (newState == RecyclerView.SCROLL_STATE_IDLE && listener != null) { + if (isScrolling) { + if (nextSnapPosition != RecyclerView.NO_POSITION) { + listener.onSnap(nextSnapPosition); + } else { + dispatchSnapChangeWhenPositionIsUnknown(); + } + } + } + isScrolling = newState != RecyclerView.SCROLL_STATE_IDLE; + } + + /** + * Calls {@link GravitySnapHelper#findSnapView(RecyclerView.LayoutManager, boolean)} + * without the check for the edge of the list. + *

      + * This makes sure that a position is reported in {@link SnapListener#onSnap(int)} + */ + private void dispatchSnapChangeWhenPositionIsUnknown() { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager == null) { + return; + } + View snapView = findSnapView(layoutManager, false); + if (snapView == null) { + return; + } + int snapPosition = recyclerView.getChildAdapterPosition(snapView); + if (snapPosition != RecyclerView.NO_POSITION) { + listener.onSnap(snapPosition); + } + } + + private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) { + if (verticalHelper == null || verticalHelper.getLayoutManager() != layoutManager) { + verticalHelper = OrientationHelper.createVerticalHelper(layoutManager); + } + return verticalHelper; + } + + private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) { + if (horizontalHelper == null || horizontalHelper.getLayoutManager() != layoutManager) { + horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager); + } + return horizontalHelper; + } + + /** + * A listener that's called when the {@link RecyclerView} used by {@link GravitySnapHelper} + * changes its scroll state to {@link RecyclerView#SCROLL_STATE_IDLE} + * and there's a valid snap position. + */ + public interface SnapListener { + /** + * @param position last position snapped to + */ + void onSnap(int position); + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/GravitySnapRecyclerView.java b/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/GravitySnapRecyclerView.java new file mode 100644 index 000000000..84dd154d1 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/GravitySnapRecyclerView.java @@ -0,0 +1,150 @@ +package com.seafile.seadroid2.view.snap_recyclerview; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.seafile.seadroid2.R; + +/** + * An {@link OrientationAwareRecyclerView} that uses a default {@link GravitySnapHelper} + */ +public class GravitySnapRecyclerView extends OrientationAwareRecyclerView { + + @NonNull + final private GravitySnapHelper snapHelper; + + private boolean isSnappingEnabled = false; + + public GravitySnapRecyclerView(@NonNull Context context) { + this(context, null); + } + + public GravitySnapRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public GravitySnapRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypedArray typedArray = context.obtainStyledAttributes(attrs, + R.styleable.GravitySnapRecyclerView, defStyleAttr, 0); + int snapGravity = typedArray.getInt( + R.styleable.GravitySnapRecyclerView_snapGravity, 0); + switch (snapGravity) { + case 0: + snapHelper = new GravitySnapHelper(Gravity.START); + break; + case 1: + snapHelper = new GravitySnapHelper(Gravity.TOP); + break; + case 2: + snapHelper = new GravitySnapHelper(Gravity.END); + break; + case 3: + snapHelper = new GravitySnapHelper(Gravity.BOTTOM); + break; + case 4: + snapHelper = new GravitySnapHelper(Gravity.CENTER); + break; + default: + throw new IllegalArgumentException("Invalid gravity value. Use START " + + "| END | BOTTOM | TOP | CENTER constants"); + } + + snapHelper.setSnapToPadding(typedArray.getBoolean( + R.styleable.GravitySnapRecyclerView_snapToPadding, false)); + + snapHelper.setSnapLastItem(typedArray.getBoolean( + R.styleable.GravitySnapRecyclerView_snapLastItem, false)); + + snapHelper.setMaxFlingSizeFraction(typedArray.getFloat( + R.styleable.GravitySnapRecyclerView_snapMaxFlingSizeFraction, + GravitySnapHelper.FLING_SIZE_FRACTION_DISABLE)); + + snapHelper.setScrollMsPerInch(typedArray.getFloat( + R.styleable.GravitySnapRecyclerView_snapScrollMsPerInch, 100f)); + + enableSnapping(typedArray.getBoolean( + R.styleable.GravitySnapRecyclerView_snapEnabled, true)); + + typedArray.recycle(); + } + + @Override + public void smoothScrollToPosition(int position) { + if (!isSnappingEnabled || !snapHelper.smoothScrollToPosition(position)) { + super.smoothScrollToPosition(position); + } + } + + @Override + public void scrollToPosition(int position) { + if (!isSnappingEnabled || !snapHelper.scrollToPosition(position)) { + super.scrollToPosition(position); + } + } + + @NonNull + public GravitySnapHelper getSnapHelper() { + return snapHelper; + } + + public void enableSnapping(Boolean enable) { + if (enable) { + snapHelper.attachToRecyclerView(this); + } else { + snapHelper.attachToRecyclerView(null); + } + isSnappingEnabled = enable; + } + + public boolean isSnappingEnabled() { + return isSnappingEnabled; + } + + public int getCurrentSnappedPosition() { + return snapHelper.getCurrentSnappedPosition(); + } + + public void snapToNextPosition(Boolean smooth) { + snapTo(true, smooth); + } + + public void snapToPreviousPosition(Boolean smooth) { + snapTo(false, smooth); + } + + public void setSnapListener(@Nullable GravitySnapHelper.SnapListener listener) { + snapHelper.setSnapListener(listener); + } + + private void snapTo(Boolean next, Boolean smooth) { + final RecyclerView.LayoutManager lm = getLayoutManager(); + if (lm != null) { + final View snapView = snapHelper.findSnapView(lm, false); + if (snapView != null) { + final int pos = getChildAdapterPosition(snapView); + if (next) { + if (smooth) { + smoothScrollToPosition(pos + 1); + } else { + scrollToPosition(pos + 1); + } + } else if (pos > 0) { + if (smooth) { + smoothScrollToPosition(pos - 1); + } else { + scrollToPosition(pos - 1); + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/OrientationAwareRecyclerView.java b/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/OrientationAwareRecyclerView.java new file mode 100644 index 000000000..ba993d26b --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/snap_recyclerview/OrientationAwareRecyclerView.java @@ -0,0 +1,81 @@ +package com.seafile.seadroid2.view.snap_recyclerview; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * A RecyclerView that only handles scroll events with the same orientation of its LayoutManager. + * Avoids situations where nested recyclerviews don't receive touch events properly: + */ +public class OrientationAwareRecyclerView extends RecyclerView { + + private float lastX = 0.0f; + private float lastY = 0.0f; + private boolean scrolling = false; + + public OrientationAwareRecyclerView(@NonNull Context context) { + this(context, null); + } + + public OrientationAwareRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public OrientationAwareRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + addOnScrollListener(new OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + scrolling = newState != RecyclerView.SCROLL_STATE_IDLE; + } + }); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent e) { + final LayoutManager lm = getLayoutManager(); + if (lm == null) { + return super.onInterceptTouchEvent(e); + } + + boolean allowScroll = true; + + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + lastX = e.getX(); + lastY = e.getY(); + // If we were scrolling, stop now by faking a touch release + if (scrolling) { + MotionEvent newEvent = MotionEvent.obtain(e); + newEvent.setAction(MotionEvent.ACTION_UP); + return super.onInterceptTouchEvent(newEvent); + } + break; + } + case MotionEvent.ACTION_MOVE: { + // We're moving, so check if we're trying + // to scroll vertically or horizontally so we don't intercept the wrong event. + float currentX = e.getX(); + float currentY = e.getY(); + float dx = Math.abs(currentX - lastX); + float dy = Math.abs(currentY - lastY); + allowScroll = dy > dx ? lm.canScrollVertically() : lm.canScrollHorizontally(); + break; + } + } + + if (!allowScroll) { + return false; + } + + return super.onInterceptTouchEvent(e); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/IWebViewActionStrategy.java b/app/src/main/java/com/seafile/seadroid2/view/webview/IWebViewActionStrategy.java new file mode 100644 index 000000000..654104780 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/IWebViewActionStrategy.java @@ -0,0 +1,9 @@ +package com.seafile.seadroid2.view.webview; + +public interface IWebViewActionStrategy { + + /** + * return null means does not callback + */ + String route(String paramsStr); +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/OnWebPageListener.java b/app/src/main/java/com/seafile/seadroid2/view/webview/OnWebPageListener.java new file mode 100644 index 000000000..5348815f7 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/OnWebPageListener.java @@ -0,0 +1,7 @@ +package com.seafile.seadroid2.view.webview; + +import android.webkit.WebView; + +public interface OnWebPageListener { + void onPageFinished(WebView view, String url); +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/SeaWebView.java b/app/src/main/java/com/seafile/seadroid2/view/webview/SeaWebView.java index cc877c532..55bccf3db 100644 --- a/app/src/main/java/com/seafile/seadroid2/view/webview/SeaWebView.java +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/SeaWebView.java @@ -2,6 +2,7 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.text.TextUtils; import android.util.AttributeSet; import android.webkit.CookieManager; import android.webkit.WebSettings; @@ -10,15 +11,29 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.blankj.utilcode.util.GsonUtils; +import com.github.lzyzsd.jsbridge.BridgeHandler; +import com.github.lzyzsd.jsbridge.BridgeWebView; +import com.github.lzyzsd.jsbridge.CallBackFunction; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; +import com.seafile.seadroid2.config.WebViewActionConstant; +import com.seafile.seadroid2.framework.data.model.WebRouteModel; +import com.seafile.seadroid2.framework.util.SLogs; import com.seafile.seadroid2.view.NestedWebView; +import com.seafile.seadroid2.view.webview.strategy.AppShowToastStrategy; +import com.seafile.seadroid2.view.webview.strategy.AppVersionGetStrategy; +import com.seafile.seadroid2.view.webview.strategy.PageFinishStrategy; +import com.seafile.seadroid2.view.webview.strategy.PageStatusHeightGetStrategy; +import com.seafile.seadroid2.view.webview.strategy.UnsupportedStrategy; + +import java.util.Locale; public class SeaWebView extends NestedWebView { public static final String PATH_ACCOUNT_LOGIN = "accounts/login/"; public static String URL_LOGIN = null; - private final SeaWebViewClient mWebViewClient = new SeaWebViewClient(); + private final SeaWebViewClient mWebViewClient = new SeaWebViewClient(this); public SeaWebView(@NonNull Context context) { super(context); @@ -53,8 +68,6 @@ private void init() { webSettings.setDomStorageEnabled(true); webSettings.setDatabaseEnabled(true); webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); -// webSettings.setAppCacheEnabled(true); -// webSettings.setAppCachePath(getContext().getCacheDir().getAbsolutePath() + "sea_web_cache/"); webSettings.setLoadWithOverviewMode(true); webSettings.setUseWideViewPort(true); webSettings.setBlockNetworkImage(false); @@ -66,6 +79,7 @@ private void init() { webSettings.setJavaScriptCanOpenWindowsAutomatically(false); webSettings.setSupportZoom(false); webSettings.setBuiltInZoomControls(false); + webSettings.setDatabaseEnabled(true); webSettings.setDisplayZoomControls(false); webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); webSettings.setDefaultTextEncodingName("UTF-8"); @@ -75,6 +89,13 @@ private void init() { cookieManager.setAcceptCookie(true); this.setWebViewClient(mWebViewClient); + + registerCommonHandler(); + } + + public void setOnWebPageListener(OnWebPageListener onWebPageListener) { + mWebViewClient.setOnWebPageListener(onWebPageListener); + this.setWebViewClient(mWebViewClient); } public void load(String targetUrl) { @@ -84,4 +105,72 @@ public void load(String targetUrl) { public void loadDirectly(String targetUrl) { mWebViewClient.loadWithoutToken(targetUrl, this); } + + private final String jsCallMethodName = "callAndroidFunction"; + + private void registerCommonHandler() { + // + this.registerHandler(jsCallMethodName, new BridgeHandler() { + @Override + public void handler(String data, CallBackFunction function) { + callAndroidFunction(data, function); + } + }); + } + + private void callAndroidFunction(String data, CallBackFunction function) { + if (TextUtils.isEmpty(data) + || data.toLowerCase(Locale.getDefault()).equals("null") + || data.toLowerCase(Locale.getDefault()).equals("undefined")) { + return; + } + + try { + WebRouteModel model = GsonUtils.fromJson(data, WebRouteModel.class); + String result = getString(model); + if (!TextUtils.isEmpty(result)) { + function.onCallBack(result); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + private String getString(WebRouteModel model) { + IWebViewActionStrategy strategy = switch (model.action) { + case WebViewActionConstant.PAGE_FINISH -> new PageFinishStrategy(getContext()); + case WebViewActionConstant.APP_VERSION_GET -> new AppVersionGetStrategy(); + case WebViewActionConstant.APP_TOAST_SHOW -> new AppShowToastStrategy(); + case WebViewActionConstant.PAGE_STATUS_HEIGHT_GET -> new PageStatusHeightGetStrategy(); +// case WebViewActionConstant.PAGE_STATUS_COLOR_SET: +// strategy = new PageStatusColorSetStrategy(getContext()); +// break; + default -> new UnsupportedStrategy(); + }; + + return strategy.route(model.data); + } + + public void callJsFunction(String action, String data) { + callJsFunction(action, data, null); + } + + public void callJsFunction(String action, String data, CallBackFunction callback) { + WebRouteModel model = new WebRouteModel(); + model.action = action; + model.data = data; + model.v = 2; + String g = GsonUtils.toJson(model); + SLogs.d("callJsFunction param => " + g); + callHandler("callJsFunction", g, new CallBackFunction() { + @Override + public void onCallBack(String data) { + SLogs.d("callJsFunction callback data => " + data); + if (callback != null) { + callback.onCallBack(data); + } + } + }); + } } diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/SeaWebViewClient.java b/app/src/main/java/com/seafile/seadroid2/view/webview/SeaWebViewClient.java index b29d1799c..fe7ae53bf 100644 --- a/app/src/main/java/com/seafile/seadroid2/view/webview/SeaWebViewClient.java +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/SeaWebViewClient.java @@ -8,6 +8,8 @@ import android.webkit.WebView; import android.webkit.WebViewClient; +import com.github.lzyzsd.jsbridge.BridgeWebView; +import com.github.lzyzsd.jsbridge.BridgeWebViewClient; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.SupportAccountManager; import com.seafile.seadroid2.framework.util.SLogs; @@ -16,7 +18,26 @@ import java.util.HashMap; import java.util.Map; -public class SeaWebViewClient extends WebViewClient { +public class SeaWebViewClient extends BridgeWebViewClient { + + private OnWebPageListener onWebPageListener; + + public void setOnWebPageListener(OnWebPageListener onWebPageListener) { + this.onWebPageListener = onWebPageListener; + } + + public SeaWebViewClient(BridgeWebView webView) { + super(webView); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + if (onWebPageListener != null) { + onWebPageListener.onPageFinished(view, url); + } + } + @Override public boolean shouldOverrideUrlLoading(WebView wb, WebResourceRequest request) { String url = request.getUrl().toString(); @@ -71,8 +92,6 @@ public void loadWithoutToken(String targetUrl, WebView wb) { } public void loadWithToken(String targetUrl, WebView wb, boolean isRedirect) { - SLogs.d("targetUrl: " + targetUrl); - mOriginTargetUrl = targetUrl; if (isRedirect) { @@ -83,9 +102,12 @@ public void loadWithToken(String targetUrl, WebView wb, boolean isRedirect) { map.put("Authorization", "Token " + account.token); } - wb.loadUrl(buildUrl(mOriginTargetUrl), map); + String rUrl = buildUrl(mOriginTargetUrl); + SLogs.d("targetUrl: " + rUrl); + wb.loadUrl(rUrl, map); } else { wb.loadUrl(mOriginTargetUrl); + SLogs.d("targetUrl: " + mOriginTargetUrl); } } @@ -95,19 +117,6 @@ private String buildUrl(String url) { } return Token2SessionConverts.buildUrl(url); - -// // Optimise the code here: -// // The expiry time of each cookie field is not consistent, -// // and it is possible to be redirected to the login page when opening the link -// String cookieStr = SupportCookieManager.getCookie(url); -// String u = url; -// if (TextUtils.isEmpty(cookieStr)) { -// u = Token2SessionConverts.buildUrl(url); -// Log.d(getClass().getSimpleName(), "link redirect to -> " + u); -// } else { -// Log.d(getClass().getSimpleName(), "link to -> " + u); -// } -// return u; } } diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/AppShowToastStrategy.java b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/AppShowToastStrategy.java new file mode 100644 index 000000000..50e68df2c --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/AppShowToastStrategy.java @@ -0,0 +1,13 @@ +package com.seafile.seadroid2.view.webview.strategy; + +import com.blankj.utilcode.util.ToastUtils; +import com.seafile.seadroid2.view.webview.IWebViewActionStrategy; + + +public class AppShowToastStrategy implements IWebViewActionStrategy { + @Override + public String route(String paramsStr) { + ToastUtils.showLong(paramsStr); + return null; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/AppVersionGetStrategy.java b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/AppVersionGetStrategy.java new file mode 100644 index 000000000..53eb6ce24 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/AppVersionGetStrategy.java @@ -0,0 +1,11 @@ +package com.seafile.seadroid2.view.webview.strategy; + +import com.blankj.utilcode.util.AppUtils; +import com.seafile.seadroid2.view.webview.IWebViewActionStrategy; + +public class AppVersionGetStrategy implements IWebViewActionStrategy { + @Override + public String route(String paramsStr) { + return AppUtils.getAppVersionName(); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/BaseStrategyModel.java b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/BaseStrategyModel.java new file mode 100644 index 000000000..2c314c19c --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/BaseStrategyModel.java @@ -0,0 +1,5 @@ +package com.seafile.seadroid2.view.webview.strategy; + +public class BaseStrategyModel { + public String action; +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageFinishStrategy.java b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageFinishStrategy.java new file mode 100644 index 000000000..10816d68f --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageFinishStrategy.java @@ -0,0 +1,28 @@ +package com.seafile.seadroid2.view.webview.strategy; + +import android.app.Activity; +import android.content.Context; +import android.content.MutableContextWrapper; + +import com.seafile.seadroid2.view.webview.IWebViewActionStrategy; + +public class PageFinishStrategy implements IWebViewActionStrategy { + private final Context context; + + public PageFinishStrategy(Context context) { + this.context = context; + } + + @Override + public String route(String paramsStr) { + if (context != null) { + MutableContextWrapper c = (MutableContextWrapper) context; + if (c.getBaseContext() instanceof Activity activity) { + activity.finish(); + } else { + throw new IllegalArgumentException("Context is not activity"); + } + } + return null; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageStatusColorSetStrategy.java b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageStatusColorSetStrategy.java new file mode 100644 index 000000000..14d64ba8d --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageStatusColorSetStrategy.java @@ -0,0 +1,37 @@ +package com.seafile.seadroid2.view.webview.strategy; + +import android.app.Activity; +import android.content.Context; +import android.content.MutableContextWrapper; + +import androidx.appcompat.app.AppCompatActivity; + +import com.seafile.seadroid2.annotation.Todo; +import com.seafile.seadroid2.annotation.Unstable; +import com.seafile.seadroid2.view.webview.IWebViewActionStrategy; + + +@Unstable +@Todo +public class PageStatusColorSetStrategy implements IWebViewActionStrategy { + private Context context; + + public PageStatusColorSetStrategy(Context context) { + this.context = context; + } + + @Override + public String route(String paramsStr) { + if (context != null) { + MutableContextWrapper c = (MutableContextWrapper) context; + if (c.getBaseContext() instanceof Activity) { + AppCompatActivity a = (AppCompatActivity) c.getBaseContext(); + //todo + } else { + throw new IllegalArgumentException("Context is not activity"); + } + } + + return null; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageStatusHeightGetStrategy.java b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageStatusHeightGetStrategy.java new file mode 100644 index 000000000..316b7f7f4 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/PageStatusHeightGetStrategy.java @@ -0,0 +1,12 @@ +package com.seafile.seadroid2.view.webview.strategy; + +import com.blankj.utilcode.util.BarUtils; +import com.seafile.seadroid2.view.webview.IWebViewActionStrategy; + +public class PageStatusHeightGetStrategy implements IWebViewActionStrategy { + @Override + public String route(String paramsStr) { + //page.status.height.get + return String.valueOf(BarUtils.getStatusBarHeight()); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/UnsupportedStrategy.java b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/UnsupportedStrategy.java new file mode 100644 index 000000000..12855f130 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/view/webview/strategy/UnsupportedStrategy.java @@ -0,0 +1,15 @@ +package com.seafile.seadroid2.view.webview.strategy; + + +import com.blankj.utilcode.util.ToastUtils; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.view.webview.IWebViewActionStrategy; + + +public class UnsupportedStrategy implements IWebViewActionStrategy { + @Override + public String route(String paramsStr) { + ToastUtils.showLong(R.string.not_supported); + return null; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/SimpleMarkdownParser.java b/app/src/main/java/com/seafile/seadroid2/widget/SimpleMarkdownParser.java new file mode 100644 index 000000000..e9eae5e9c --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/SimpleMarkdownParser.java @@ -0,0 +1,130 @@ +package com.seafile.seadroid2.widget; + +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.BulletSpan; +import android.text.style.ImageSpan; +import android.text.style.StyleSpan; +import android.text.style.URLSpan; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SimpleMarkdownParser { + public static void setMarkdown(TextView textView, String markdown) { + SpannableStringBuilder spannable = parseMarkdown(textView, markdown); + textView.setText(spannable); + } + + private static SpannableStringBuilder parseMarkdown(TextView textView, String markdown) { + SpannableStringBuilder builder = new SpannableStringBuilder(); + + // 1. 解析粗斜体:***富文本*** + applyPattern(builder, "\\*\\*\\*(.*?)\\*\\*\\*", match -> { + builder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), + match.start(1), match.end(1), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + }); + + // 2. 解析引用:> ***引用*** + applyPattern(builder, "^>\\s?(.*)", match -> { + builder.setSpan(new StyleSpan(Typeface.ITALIC), + match.start(1), match.end(1), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + }); + + // 3. 解析无序列表:* 1 + applyPattern(builder, "^\\*\\s(.*)", match -> { + builder.setSpan(new BulletSpan(20), + match.start(1), match.end(1), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + }); + + // 4. 解析有序列表:1. 1 + applyPattern(builder, "^\\d+\\.\\s(.*)", match -> { + builder.setSpan(new StyleSpan(Typeface.BOLD), + match.start(1), match.end(1), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + }); + + // 5. 解析链接:[https://xxx](xxx) + applyPattern(builder, "\\[(.*?)\\]\\((.*?)\\)", match -> { + builder.setSpan(new URLSpan(match.group(2)), + match.start(1), match.end(1), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.replace(match.start(2), match.end(2), ""); // 删除链接部分 + }); + + // 6. 解析图片:![1.jpg](https://xxx) + applyPattern(builder, "!\\[(.*?)\\]\\((.*?)\\)", match -> { + String imageUrl = match.group(2); + builder.replace(match.start(), match.end(), match.group(1)); // 替换为占位符文字 + int start = match.start(); + int end = start + match.group(1).length(); + + // 异步加载图片 + loadImageAsync(textView, imageUrl, drawable -> { + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); + builder.setSpan(imageSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + textView.setText(builder); // 更新 TextView + }); + }); + return builder; + } + + private static void applyPattern(SpannableStringBuilder stringBuilder, String regex, MatchHandler handler) { + Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE); + Matcher matcher = pattern.matcher(stringBuilder); + while (matcher.find()) { + handler.handleMatch(matcher); + } + } + + private static void loadImageAsync(TextView textView, String imageUrl, ImageLoaderCallback callback) { + +// Glide.with(textView.getContext()) +// .load(imageUrl) +// .into(new CustomTarget() { +// @Override +// public void onResourceReady(@NonNull Drawable resource, Transition transition) { +// // 设置图片尺寸,适配 TextView +// int maxWidth = textView.getWidth() - textView.getPaddingLeft() - textView.getPaddingRight(); +// int width = Math.min(resource.getIntrinsicWidth(), maxWidth); +// int height = width * resource.getIntrinsicHeight() / resource.getIntrinsicWidth(); +// resource.setBounds(0, 0, width, height); +// +// // 替换文字为图片 +// ImageSpan imageSpan = new ImageSpan(resource, ImageSpan.ALIGN_BOTTOM); +// builder.setSpan(imageSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); +// +// // 更新 TextView 内容 +// textView.setText(builder); +// } +// +// @Override +// public void onLoadCleared(Drawable placeholder) { +// // 可选:当图片加载被取消时的处理 +// } +// }); + } + + interface MatchHandler { + void handleMatch(Matcher match); + } + + interface ImageLoaderCallback { + void onImageLoaded(@NonNull Drawable drawable); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/ButtonPreference.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/ButtonPreference.java deleted file mode 100644 index 40a73c3e3..000000000 --- a/app/src/main/java/com/seafile/seadroid2/widget/prefs/ButtonPreference.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.seafile.seadroid2.widget.prefs; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; - -import com.google.android.material.button.MaterialButton; -import com.seafile.seadroid2.R; - -public class ButtonPreference extends Preference { - public ButtonPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(); - } - - public ButtonPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public ButtonPreference(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(); - } - - public ButtonPreference(@NonNull Context context) { - super(context); - init(); - } - - private void init() { - setLayoutResource(R.layout.layout_logout_view); - } - - public MaterialButton getButton() { - return button; - } - - MaterialButton button; - - @Override - protected void onClick() { - super.onClick(); - } - - @Override - public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - - button = (MaterialButton) holder.findViewById(android.R.id.title); - button.setText(getTitle()); - } -} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/CardPreferenceCategory.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/CardPreferenceCategory.java index c6b18af94..6dbd0be94 100644 --- a/app/src/main/java/com/seafile/seadroid2/widget/prefs/CardPreferenceCategory.java +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/CardPreferenceCategory.java @@ -1,39 +1,56 @@ package com.seafile.seadroid2.widget.prefs; import android.content.Context; +import android.content.res.TypedArray; import android.util.AttributeSet; +import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.content.ContextCompat; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceViewHolder; +import androidx.recyclerview.widget.RecyclerView; import com.seafile.seadroid2.R; public class CardPreferenceCategory extends PreferenceCategory { public CardPreferenceCategory(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - init(); + init(context, attrs, defStyleAttr); } public CardPreferenceCategory(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(); + init(context, attrs, defStyleAttr); } public CardPreferenceCategory(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); - init(); + init(context, attrs, 0); } public CardPreferenceCategory(@NonNull Context context) { super(context); - init(); + init(context, null, 0); } - private void init() { - setLayoutResource(R.layout.preference_cardbox); +// private int marginBottom = 0; + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { +// if (context != null && attrs != null) { +// TypedArray typedArray = context.obtainStyledAttributes(attrs, +// R.styleable.PrefCategoryShape, defStyleAttr, 0); +// +// marginBottom = typedArray.getDimensionPixelSize(R.styleable.PrefCategoryShape_marginBottom, 0); +// +// typedArray.recycle(); +// } + + + setLayoutResource(R.layout.layout_pref_category); } @Override @@ -41,9 +58,7 @@ public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { super.onBindViewHolder(holder); TextView titleView = (TextView) holder.findViewById(android.R.id.title); - TextView summaryView = (TextView) holder.findViewById(android.R.id.summary); titleView.setText(getTitle()); - summaryView.setText(getSummary()); } } diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/PrefBackgrundRadiusShapeEnum.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/PrefBackgrundRadiusShapeEnum.java new file mode 100644 index 000000000..dff5fa539 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/PrefBackgrundRadiusShapeEnum.java @@ -0,0 +1,5 @@ +package com.seafile.seadroid2.widget.prefs; + +public enum PrefBackgrundRadiusShapeEnum { + TOP, NONE, BOTTOM +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/SimpleMenuPreference.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/SimpleMenuPreference.java new file mode 100644 index 000000000..6742ad821 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/SimpleMenuPreference.java @@ -0,0 +1,127 @@ +package com.seafile.seadroid2.widget.prefs; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.ContextThemeWrapper; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.PreferenceViewHolder; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.widget.prefs.background_pref.BackgroundListPreference; +import com.seafile.seadroid2.widget.prefs.simplemenu.SimpleMenuPopupWindow; + + +/** + * A version of {@link ListPreference} that use Simple Menus in Material Design 1 as drop down. + *

      + * On pre-Lollipop, it will fallback to {@link ListPreference}. + */ + +public class SimpleMenuPreference extends BackgroundListPreference { + + private static boolean sLightFixEnabled = false; + + public static boolean isLightFixEnabled() { + return sLightFixEnabled; + } + + public static void setLightFixEnabled(boolean lightFixEnabled) { + sLightFixEnabled = lightFixEnabled; + } + + private View mAnchor; + private View mItemView; + private SimpleMenuPopupWindow mPopupWindow; + + public SimpleMenuPreference(Context context) { + this(context, null); + } + + @Override + public int getLayoutId() { + return R.layout.layout_pref_title_summary_empty; + } + + public SimpleMenuPreference(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.simpleMenuPreferenceStyle); + } + + public SimpleMenuPreference(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, R.style.Preference_SimpleMenuPreference); + } + + public SimpleMenuPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SimpleMenuPreference, defStyleAttr, defStyleRes); + + int popupStyle = a.getResourceId(R.styleable.SimpleMenuPreference_android_popupMenuStyle, R.style.Widget_Preference_SimpleMenuPreference_PopupMenu); + int popupTheme = a.getResourceId(R.styleable.SimpleMenuPreference_android_popupTheme, R.style.ThemeOverlay_Preference_SimpleMenuPreference_PopupMenu); + Context popupContext; + if (popupTheme != 0) { + popupContext = new ContextThemeWrapper(context, popupTheme); + } else { + popupContext = context; + } + + mPopupWindow = new SimpleMenuPopupWindow(popupContext, attrs, R.styleable.SimpleMenuPreference_android_popupMenuStyle, popupStyle); + mPopupWindow.setOnItemClickListener(i -> { + String value = getEntryValues()[i].toString(); + if (callChangeListener(value)) { + setValue(value); + } + }); + + a.recycle(); + } + + @Override + protected void onClick() { + + if (getEntries() == null || getEntries().length == 0) { + return; + } + + if (mPopupWindow == null) { + return; + } + + mPopupWindow.setEntries(getEntries()); + mPopupWindow.setSelectedIndex(findIndexOfValue(getValue())); + + View container = (View) mItemView // itemView + .getParent(); // -> list (RecyclerView) + + mPopupWindow.show(mItemView, container, (int) mAnchor.getX()); + } + + @Override + public void setEntries(@NonNull CharSequence[] entries) { + super.setEntries(entries); + + mPopupWindow.requestMeasure(); + } + + @Override + public void setValue(String value) { + super.setValue(value); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); + + mItemView = view.itemView; + mAnchor = view.itemView.findViewById(android.R.id.empty); + + if (mAnchor == null) { + throw new IllegalStateException("SimpleMenuPreference item layout must contain" + + "a view id is android.R.id.empty to support iconSpaceReserved"); + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextMorePreference.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextMorePreference.java new file mode 100644 index 000000000..4172bd987 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextMorePreference.java @@ -0,0 +1,54 @@ +package com.seafile.seadroid2.widget.prefs; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceViewHolder; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.widget.prefs.background_pref.BackgroundShapePreference; + +public class TextMorePreference extends BackgroundShapePreference { + public TextMorePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public TextMorePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public TextMorePreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public TextMorePreference(@NonNull Context context) { + super(context); + } + + @Override + public int getLayoutId() { + return R.layout.layout_pref_text_and_more; + } + + public TextView getTitleTextView() { + return titleTextView; + } + + TextView titleTextView; + + @Override + protected void onClick() { + super.onClick(); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + titleTextView = (TextView) holder.findViewById(android.R.id.title); + titleTextView.setText(getTitle()); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextSwitchPreference.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextSwitchPreference.java new file mode 100644 index 000000000..b13eac75c --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextSwitchPreference.java @@ -0,0 +1,53 @@ +package com.seafile.seadroid2.widget.prefs; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceViewHolder; + +import com.google.android.material.materialswitch.MaterialSwitch; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.widget.prefs.background_pref.BackgroundSwitchPreference; + +public class TextSwitchPreference extends BackgroundSwitchPreference { + public TextSwitchPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public TextSwitchPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public TextSwitchPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public TextSwitchPreference(@NonNull Context context) { + super(context); + } + + @Override + public int getLayoutId() { + return R.layout.layout_pref_title_switch; + } + + private MaterialSwitch materialSwitch; + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + materialSwitch = (MaterialSwitch) holder.findViewById(android.R.id.switch_widget); + materialSwitch.setClickable(false); + } + + public void setChecked(boolean checked) { + super.setChecked(checked); + + if (materialSwitch != null) { + materialSwitch.setChecked(checked); + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextTitleSummaryPreference.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextTitleSummaryPreference.java new file mode 100644 index 000000000..80e3cab1d --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/TextTitleSummaryPreference.java @@ -0,0 +1,54 @@ +package com.seafile.seadroid2.widget.prefs; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceViewHolder; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.widget.prefs.background_pref.BackgroundShapePreference; + +public class TextTitleSummaryPreference extends BackgroundShapePreference { + public TextTitleSummaryPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public TextTitleSummaryPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public TextTitleSummaryPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public TextTitleSummaryPreference(@NonNull Context context) { + super(context); + } + + @Override + public int getLayoutId() { + return R.layout.layout_pref_title_summary; + } + + public TextView getTitleTextView() { + return titleTextView; + } + + TextView titleTextView; + + @Override + protected void onClick() { + super.onClick(); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + titleTextView = (TextView) holder.findViewById(android.R.id.title); + titleTextView.setText(getTitle()); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundListPreference.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundListPreference.java new file mode 100644 index 000000000..49342520f --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundListPreference.java @@ -0,0 +1,126 @@ +package com.seafile.seadroid2.widget.prefs.background_pref; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.preference.ListPreference; +import androidx.preference.PreferenceViewHolder; + +import com.google.android.material.divider.MaterialDivider; +import com.seafile.seadroid2.R; + +public abstract class BackgroundListPreference extends ListPreference { + public BackgroundListPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + public BackgroundListPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + public BackgroundListPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public BackgroundListPreference(@NonNull Context context) { + super(context); + init(context, null, 0); + } + + // + public abstract int getLayoutId(); + + private int titleTextColor = 0; + //default all + private int radiusPosition = 0; + private int backgroundColor = 0; + private int backgroundRadius = 0; + private int dividerPosition = 0; + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + + if (context != null && attrs != null) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, + R.styleable.PrefShape, defStyleAttr, 0); + + titleTextColor = typedArray.getColor(R.styleable.PrefShape_titleTextColor, ContextCompat.getColor(getContext(), R.color.item_title_color)); + radiusPosition = typedArray.getInt(R.styleable.PrefShape_radiusPosition, 0); + + backgroundColor = typedArray.getColor(R.styleable.PrefShape_backgroundColor, ContextCompat.getColor(getContext(), R.color.bar_background_color)); + backgroundRadius = typedArray.getDimensionPixelSize(R.styleable.PrefShape_backgroundRadius, 8); + + dividerPosition = typedArray.getInt(R.styleable.PrefShape_dividerPosition, 0); + + typedArray.recycle(); + } + + setLayoutResource(getLayoutId()); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + Drawable drawable = BackgroundShapeUtils.genBackgroundDrawable(radiusPosition, backgroundColor, backgroundRadius); + holder.itemView.setBackground(drawable); + + TextView titleTextView = (TextView) holder.findViewById(android.R.id.title); + if (titleTextView != null) { + titleTextView.setTextColor(titleTextColor); + } + + MaterialDivider topDivider = (MaterialDivider) holder.findViewById(R.id.top_divider); + MaterialDivider bottomDivider = (MaterialDivider) holder.findViewById(R.id.bottom_divider); + if (dividerPosition == 0) { + //none + topDivider.setVisibility(View.GONE); + bottomDivider.setVisibility(View.GONE); + } else if (dividerPosition == 1) { + //top + topDivider.setVisibility(View.VISIBLE); + bottomDivider.setVisibility(View.GONE); + } else if (dividerPosition == 2) { + //bottom + topDivider.setVisibility(View.GONE); + bottomDivider.setVisibility(View.VISIBLE); + } else if (dividerPosition == 3) { + //top and bottom + topDivider.setVisibility(View.VISIBLE); + bottomDivider.setVisibility(View.VISIBLE); + } + } + + public void setBackgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + notifyChanged(); + } + + public void setBackgroundRadius(int backgroundRadius) { + this.backgroundRadius = backgroundRadius; + notifyChanged(); + } + + public void setRadiusPosition(int radiusPosition) { + this.radiusPosition = radiusPosition; + notifyChanged(); + } + + public void setDividerPosition(int dividerPosition) { + this.dividerPosition = dividerPosition; + notifyChanged(); + } + public void setTitleTextColor(int titleTextColor) { + this.titleTextColor = titleTextColor; + notifyChanged(); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundShapePreference.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundShapePreference.java new file mode 100644 index 000000000..55df06d42 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundShapePreference.java @@ -0,0 +1,145 @@ +package com.seafile.seadroid2.widget.prefs.background_pref; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.google.android.material.divider.MaterialDivider; +import com.seafile.seadroid2.R; + +public abstract class BackgroundShapePreference extends Preference { + + public BackgroundShapePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + public BackgroundShapePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + public BackgroundShapePreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public BackgroundShapePreference(@NonNull Context context) { + super(context); + init(context, null, 0); + } + + // + public abstract int getLayoutId(); + + private int titleTextColor = 0; + private int summaryTextColor = 0; + //default all + private int radiusPosition = 0; + private int backgroundColor = 0; + private int backgroundRadius = 0; + private int dividerPosition = 0; + private int dividerColor = 0; + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + + if (context != null && attrs != null) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, + R.styleable.PrefShape, defStyleAttr, 0); + + titleTextColor = typedArray.getColor(R.styleable.PrefShape_titleTextColor, ContextCompat.getColor(getContext(), R.color.item_title_color)); + summaryTextColor = typedArray.getColor(R.styleable.PrefShape_summaryTextColor, ContextCompat.getColor(getContext(), R.color.item_subtitle_color)); + radiusPosition = typedArray.getInt(R.styleable.PrefShape_radiusPosition, 0); + + backgroundColor = typedArray.getColor(R.styleable.PrefShape_backgroundColor, ContextCompat.getColor(getContext(), R.color.bar_background_color)); + backgroundRadius = typedArray.getDimensionPixelSize(R.styleable.PrefShape_backgroundRadius, 8); + + dividerPosition = typedArray.getInt(R.styleable.PrefShape_dividerPosition, 0); + dividerColor = typedArray.getColor(R.styleable.PrefShape_dividerColor, ContextCompat.getColor(getContext(), R.color.divider_color)); + + typedArray.recycle(); + } + + setLayoutResource(getLayoutId()); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + Drawable drawable = BackgroundShapeUtils.genBackgroundDrawable(radiusPosition, backgroundColor, backgroundRadius); + holder.itemView.setBackground(drawable); + + TextView titleTextView = (TextView) holder.findViewById(android.R.id.title); + if (titleTextView != null) { + titleTextView.setTextColor(titleTextColor); + } + + TextView summaryTextView = (TextView) holder.findViewById(android.R.id.summary); + if (summaryTextView != null) { + summaryTextView.setTextColor(summaryTextColor); + } + + + MaterialDivider topDivider = (MaterialDivider) holder.findViewById(R.id.top_divider); + MaterialDivider bottomDivider = (MaterialDivider) holder.findViewById(R.id.bottom_divider); + topDivider.setDividerColor(dividerColor); + bottomDivider.setDividerColor(dividerColor); + + if (dividerPosition == 0) { + //none + topDivider.setVisibility(View.GONE); + bottomDivider.setVisibility(View.GONE); + } else if (dividerPosition == 1) { + //top + topDivider.setVisibility(View.VISIBLE); + bottomDivider.setVisibility(View.GONE); + } else if (dividerPosition == 2) { + //bottom + topDivider.setVisibility(View.GONE); + bottomDivider.setVisibility(View.VISIBLE); + } else if (dividerPosition == 3) { + //top and bottom + topDivider.setVisibility(View.VISIBLE); + bottomDivider.setVisibility(View.VISIBLE); + } + } + + public void setBackgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + notifyChanged(); + } + + public void setBackgroundRadius(int backgroundRadius) { + this.backgroundRadius = backgroundRadius; + notifyChanged(); + } + + public void setRadiusPosition(int radiusPosition) { + this.radiusPosition = radiusPosition; + notifyChanged(); + } + + public void setDividerPosition(int dividerPosition) { + this.dividerPosition = dividerPosition; + notifyChanged(); + } + + public void setTitleTextColor(int titleTextColor) { + this.titleTextColor = titleTextColor; + notifyChanged(); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundShapeUtils.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundShapeUtils.java new file mode 100644 index 000000000..79e06f233 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundShapeUtils.java @@ -0,0 +1,58 @@ +package com.seafile.seadroid2.widget.prefs.background_pref; + +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; + +public class BackgroundShapeUtils { + + + public static LayerDrawable genBackgroundDrawable(int radiusPosition, int backgroundColor, int backgroundRadius) { + // background shape + GradientDrawable backgroundShape = new GradientDrawable(); + backgroundShape.setShape(GradientDrawable.RECTANGLE); + backgroundShape.setColor(backgroundColor); + + float[] radii = getRadii(radiusPosition, backgroundRadius); + backgroundShape.setCornerRadii(radii); + + Drawable[] layers = {backgroundShape}; + return new LayerDrawable(layers); + } + + private static float[] getRadii(int radiusPosition, int backgroundRadius) { + if (backgroundRadius == 0) { + return new float[]{0, 0, 0, 0, 0, 0, 0, 0}; + } + + float[] radii; + if (radiusPosition == 0) { + //none + radii = new float[]{0, 0, 0, 0, 0, 0, 0, 0}; + } else if (radiusPosition == 1) { + //all + radii = new float[]{ + backgroundRadius, backgroundRadius, // left-top + backgroundRadius, backgroundRadius, // right-top + backgroundRadius, backgroundRadius, // right-bottom + backgroundRadius, backgroundRadius // left-bottom + }; + } else if (radiusPosition == 2) { + //top + radii = new float[]{ + backgroundRadius, backgroundRadius, + backgroundRadius, backgroundRadius, + 0, 0, 0, 0}; + } else if (radiusPosition == 3) { + //bottom + radii = new float[]{ + 0, 0, 0, 0, + backgroundRadius, backgroundRadius, + backgroundRadius, backgroundRadius + }; + } else { + radii = new float[]{0, 0, 0, 0, 0, 0, 0, 0}; + } + return radii; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundSwitchPreference.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundSwitchPreference.java new file mode 100644 index 000000000..75f844d80 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/background_pref/BackgroundSwitchPreference.java @@ -0,0 +1,136 @@ +package com.seafile.seadroid2.widget.prefs.background_pref; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceViewHolder; +import androidx.preference.SwitchPreferenceCompat; + +import com.google.android.material.divider.MaterialDivider; +import com.seafile.seadroid2.R; + +public abstract class BackgroundSwitchPreference extends SwitchPreferenceCompat { + public BackgroundSwitchPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + public BackgroundSwitchPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + public BackgroundSwitchPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public BackgroundSwitchPreference(@NonNull Context context) { + super(context); + init(context, null, 0); + } + + // + public abstract int getLayoutId(); + + private int titleTextColor = 0; + //default all + private int radiusPosition = 0; + private int backgroundColor = 0; + private int backgroundRadius = 0; + private int dividerPosition = 0; + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + + if (context != null && attrs != null) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, + R.styleable.PrefShape, defStyleAttr, 0); + + titleTextColor = typedArray.getColor(R.styleable.PrefShape_titleTextColor, ContextCompat.getColor(getContext(), R.color.item_title_color)); + radiusPosition = typedArray.getInt(R.styleable.PrefShape_radiusPosition, 0); + + backgroundColor = typedArray.getColor(R.styleable.PrefShape_backgroundColor, ContextCompat.getColor(getContext(), R.color.bar_background_color)); + backgroundRadius = typedArray.getDimensionPixelSize(R.styleable.PrefShape_backgroundRadius, 8); + + dividerPosition = typedArray.getInt(R.styleable.PrefShape_dividerPosition, 0); + + typedArray.recycle(); + } + + setLayoutResource(getLayoutId()); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + Drawable drawable = BackgroundShapeUtils.genBackgroundDrawable(radiusPosition, backgroundColor, backgroundRadius); + holder.itemView.setBackground(drawable); + + TextView titleTextView = (TextView) holder.findViewById(android.R.id.title); + if (titleTextView != null) { + titleTextView.setTextColor(titleTextColor); + } + + MaterialDivider topDivider = (MaterialDivider) holder.findViewById(R.id.top_divider); + MaterialDivider bottomDivider = (MaterialDivider) holder.findViewById(R.id.bottom_divider); + if (dividerPosition == 0) { + //none + topDivider.setVisibility(View.GONE); + bottomDivider.setVisibility(View.GONE); + } else if (dividerPosition == 1) { + //top + topDivider.setVisibility(View.VISIBLE); + bottomDivider.setVisibility(View.GONE); + } else if (dividerPosition == 2) { + //bottom + topDivider.setVisibility(View.GONE); + bottomDivider.setVisibility(View.VISIBLE); + } else if (dividerPosition == 3) { + //top and bottom + topDivider.setVisibility(View.VISIBLE); + bottomDivider.setVisibility(View.VISIBLE); + } + } + + public void setBackgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + notifyChanged(); + } + + /** + * dp + */ + public void setBackgroundRadius(int backgroundRadius) { + this.backgroundRadius = backgroundRadius; + notifyChanged(); + } + + /** + * @param radiusPosition 0: none, 1: all, 2: top, 3: bottom + */ + public void setRadiusPosition(int radiusPosition) { + this.radiusPosition = radiusPosition; + notifyChanged(); + } + + /** + * @param dividerPosition 0: none, 1: top, 2: bottom, 3: top and bottom + */ + public void setDividerPosition(int dividerPosition) { + this.dividerPosition = dividerPosition; + notifyChanged(); + } + + public void setTitleTextColor(int titleTextColor) { + this.titleTextColor = titleTextColor; + notifyChanged(); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/CustomBoundsDrawable.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/CustomBoundsDrawable.java new file mode 100644 index 000000000..bdbdac7ce --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/CustomBoundsDrawable.java @@ -0,0 +1,40 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import androidx.annotation.NonNull; + +/** + * A wrapped {@link Drawable} that force use its own bounds to draw. + *

      + * It maybe a little dirty. But if we don't do that, during the expanding animation, there will be + * one or two frame using wrong bounds because of parent view sets bounds. + */ + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +class CustomBoundsDrawable extends DrawableWrapper { + + public CustomBoundsDrawable(Drawable wrappedDrawable) { + super(wrappedDrawable); + } + + public void setCustomBounds(@NonNull Rect bounds) { + setCustomBounds(bounds.left, bounds.top, bounds.right, bounds.bottom); + } + + public void setCustomBounds(int left, int top, int right, int bottom) { + setBounds(left, top, right, bottom); + getWrappedDrawable().setBounds(left, top, right, bottom); + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + } + + @Override + public void setBounds(@NonNull Rect bounds) { + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/DrawableWrapper.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/DrawableWrapper.java new file mode 100644 index 000000000..a8a16b1ef --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/DrawableWrapper.java @@ -0,0 +1,218 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Outline; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.graphics.drawable.DrawableCompat; + +class DrawableWrapper extends Drawable implements Drawable.Callback { + + private Drawable mDrawable; + + public DrawableWrapper(Drawable drawable) { + setWrappedDrawable(drawable); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void getOutline(@NonNull Outline outline) { + mDrawable.getOutline(outline); + } + + @Override + public void draw(@NonNull Canvas canvas) { + mDrawable.draw(canvas); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mDrawable.setBounds(bounds); + } + + @Override + public void setChangingConfigurations(int configs) { + mDrawable.setChangingConfigurations(configs); + } + + @Override + public int getChangingConfigurations() { + return mDrawable.getChangingConfigurations(); + } + + @SuppressWarnings("deprecation") + @Override + public void setDither(boolean dither) { + mDrawable.setDither(dither); + } + + @Override + public void setFilterBitmap(boolean filter) { + mDrawable.setFilterBitmap(filter); + } + + @Override + public void setAlpha(int alpha) { + mDrawable.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mDrawable.setColorFilter(cf); + } + + @Override + public boolean isStateful() { + return mDrawable.isStateful(); + } + + @Override + public boolean setState(@NonNull final int[] stateSet) { + return mDrawable.setState(stateSet); + } + + @NonNull + @Override + public int[] getState() { + return mDrawable.getState(); + } + + @Override + public void jumpToCurrentState() { + mDrawable.jumpToCurrentState(); + } + + @NonNull + @Override + public Drawable getCurrent() { + return mDrawable.getCurrent(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + return super.setVisible(visible, restart) || mDrawable.setVisible(visible, restart); + } + + @Override + public int getOpacity() { + return mDrawable.getOpacity(); + } + + @Override + public Region getTransparentRegion() { + return mDrawable.getTransparentRegion(); + } + + @Override + public int getIntrinsicWidth() { + return mDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mDrawable.getIntrinsicHeight(); + } + + @Override + public int getMinimumWidth() { + return mDrawable.getMinimumWidth(); + } + + @Override + public int getMinimumHeight() { + return mDrawable.getMinimumHeight(); + } + + @Override + public boolean getPadding(@NonNull Rect padding) { + return mDrawable.getPadding(padding); + } + + /** + * {@inheritDoc} + */ + @Override + public void invalidateDrawable(@NonNull Drawable who) { + invalidateSelf(); + } + + /** + * {@inheritDoc} + */ + @Override + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { + scheduleSelf(what, when); + } + + /** + * {@inheritDoc} + */ + @Override + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + unscheduleSelf(what); + } + + @Override + protected boolean onLevelChange(int level) { + return mDrawable.setLevel(level); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + DrawableCompat.setAutoMirrored(mDrawable, mirrored); + } + + @Override + public boolean isAutoMirrored() { + return DrawableCompat.isAutoMirrored(mDrawable); + } + + @Override + public void setTint(int tint) { + DrawableCompat.setTint(mDrawable, tint); + } + + @Override + public void setTintList(ColorStateList tint) { + DrawableCompat.setTintList(mDrawable, tint); + } + + @Override + public void setTintMode(PorterDuff.Mode tintMode) { + DrawableCompat.setTintMode(mDrawable, tintMode); + } + + @Override + public void setHotspot(float x, float y) { + DrawableCompat.setHotspot(mDrawable, x, y); + } + + @Override + public void setHotspotBounds(int left, int top, int right, int bottom) { + DrawableCompat.setHotspotBounds(mDrawable, left, top, right, bottom); + } + + public Drawable getWrappedDrawable() { + return mDrawable; + } + + public void setWrappedDrawable(Drawable drawable) { + if (mDrawable != null) { + mDrawable.setCallback(null); + } + + mDrawable = drawable; + + if (drawable != null) { + drawable.setCallback(this); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/ForegroundCheckTextView.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/ForegroundCheckTextView.java new file mode 100644 index 000000000..8d72f396f --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/ForegroundCheckTextView.java @@ -0,0 +1,223 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.Gravity; +import android.widget.CheckedTextView; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.appcompat.widget.AppCompatCheckedTextView; + +import com.seafile.seadroid2.R; + +/** + * Extension of {@link CheckedTextView} that adds a Foreground drawable. + */ +public class ForegroundCheckTextView extends AppCompatCheckedTextView { + + private Drawable mForeground; + + private final Rect mSelfBounds = new Rect(); + + private final Rect mOverlayBounds = new Rect(); + + private int mForegroundGravity = Gravity.FILL; + + protected boolean mForegroundInPadding = true; + + boolean mForegroundBoundsChanged = false; + + public ForegroundCheckTextView(Context context) { + this(context, null); + } + + public ForegroundCheckTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ForegroundCheckTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundCheckTextView, + defStyleAttr, 0); + + mForegroundGravity = a.getInt(R.styleable.ForegroundCheckTextView_android_foregroundGravity, mForegroundGravity); + + final Drawable d = a.getDrawable(R.styleable.ForegroundCheckTextView_android_foreground); + if (d != null) { + setForeground(d); + } + + mForegroundInPadding = a.getBoolean(R.styleable.ForegroundCheckTextView_foregroundInsidePadding, true); + + a.recycle(); + } + + /** + * Describes how the foreground is positioned. + * + * @return foreground gravity. + * @see #setForegroundGravity(int) + */ + public int getForegroundGravity() { + return mForegroundGravity; + } + + /** + * Describes how the foreground is positioned. Defaults to START and TOP. + * + * @param foregroundGravity See {@link Gravity} + * @see #getForegroundGravity() + */ + public void setForegroundGravity(int foregroundGravity) { + if (mForegroundGravity != foregroundGravity) { + if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + foregroundGravity |= Gravity.START; + } + + if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { + foregroundGravity |= Gravity.TOP; + } + + mForegroundGravity = foregroundGravity; + + if (mForegroundGravity == Gravity.FILL && mForeground != null) { + Rect padding = new Rect(); + mForeground.getPadding(padding); + } + + requestLayout(); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || (who == mForeground); + } + + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + if (mForeground != null) { + mForeground.jumpToCurrentState(); + } + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mForeground != null && mForeground.isStateful()) { + mForeground.setState(getDrawableState()); + } + } + + /** + * Supply a Drawable that is to be rendered on top of all of the child + * views in the frame layout. Any padding in the Drawable will be taken + * into account by ensuring that the children are inset to be placed + * inside of the padding area. + * + * @param drawable The Drawable to be drawn on top of the children. + */ + public void setForeground(Drawable drawable) { + if (mForeground != drawable) { + if (mForeground != null) { + mForeground.setCallback(null); + unscheduleDrawable(mForeground); + } + + mForeground = drawable; + + if (drawable != null) { + setWillNotDraw(false); + drawable.setCallback(this); + if (drawable.isStateful()) { + drawable.setState(getDrawableState()); + } + if (mForegroundGravity == Gravity.FILL) { + Rect padding = new Rect(); + drawable.getPadding(padding); + } + } else { + setWillNotDraw(true); + } + requestLayout(); + invalidate(); + } + } + + /** + * Returns the drawable used as the foreground of this FrameLayout. The + * foreground drawable, if non-null, is always drawn on top of the children. + * + * @return A Drawable or null if no foreground was set. + */ + public Drawable getForeground() { + return mForeground; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + mForegroundBoundsChanged |= changed; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mForegroundBoundsChanged = true; + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + if (mForeground != null) { + final Drawable foreground = mForeground; + + if (mForegroundBoundsChanged) { + mForegroundBoundsChanged = false; + final Rect selfBounds = mSelfBounds; + final Rect overlayBounds = mOverlayBounds; + + final int w = getRight() - getLeft(); + final int h = getBottom() - getTop(); + + if (mForegroundInPadding) { + selfBounds.set(0, 0, w, h); + } else { + selfBounds.set(getPaddingLeft(), getPaddingTop(), + w - getPaddingRight(), h - getPaddingBottom()); + } + + Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), + foreground.getIntrinsicHeight(), selfBounds, overlayBounds); + foreground.setBounds(overlayBounds); + } + + foreground.draw(canvas); + } + } + + @Override + public void drawableHotspotChanged(float x, float y) { + super.drawableHotspotChanged(x, y); + if (mForeground != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mForeground.setHotspot(x, y); + } + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/Light.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/Light.java new file mode 100644 index 000000000..2f4505e94 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/Light.java @@ -0,0 +1,94 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import android.annotation.SuppressLint; +import android.graphics.HardwareRenderer; +import android.graphics.Point; +import android.os.Build; +import android.view.Display; +import android.view.View; +import android.widget.PopupWindow; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +@SuppressWarnings({"JavaReflectionMemberAccess", "DiscouragedPrivateApi"}) +@SuppressLint("PrivateApi") +class Light { + + /** + * Android uses displaySize.x / 2 - windowLeft as the x-coordinate of light source (source code). + *
      If the window is on the left of the screen, the light source will be at the right to the window + * causing shadow on the left side. This make our PopupWindow looks weird. + *

      This method reset the x-coordinate of light source to windowLeft + 56dp by using multiply reflections. + * + * @param window PopupWindow + */ + static void resetLightCenterForPopupWindow(PopupWindow window) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + try { + Class threadedRendererClass = Class.forName("android.view.ThreadedRenderer"); + Class attachInfoClass = Class.forName("android.view.View$AttachInfo"); + + View view = window.getContentView().getRootView(); + Method getThreadedRendererMethod = View.class.getDeclaredMethod("getThreadedRenderer"); + getThreadedRendererMethod.setAccessible(true); + Object threadedRenderer = getThreadedRendererMethod.invoke(view); + + Field attachInfoField = View.class.getDeclaredField("mAttachInfo"); + attachInfoField.setAccessible(true); + Object attachInfo = attachInfoField.get(view); + + Field pointField = attachInfoClass.getDeclaredField("mPoint"); + pointField.setAccessible(true); + Point displaySize = (Point) pointField.get(attachInfo); + + Field displayField = attachInfoClass.getDeclaredField("mDisplay"); + displayField.setAccessible(true); + Display display = (Display) displayField.get(attachInfo); + + display.getRealSize(displaySize); + + Field windowLeftField = attachInfoClass.getDeclaredField("mWindowLeft"); + windowLeftField.setAccessible(true); + int mWindowLeft = windowLeftField.getInt(attachInfo); + + Field windowTopField = attachInfoClass.getDeclaredField("mWindowTop"); + windowTopField.setAccessible(true); + int mWindowTop = windowTopField.getInt(attachInfo); + + Field lightYField = threadedRendererClass.getDeclaredField("mLightY"); + lightYField.setAccessible(true); + float mLightY = lightYField.getFloat(threadedRenderer); + + Field lightZField = threadedRendererClass.getDeclaredField("mLightZ"); + lightZField.setAccessible(true); + float mLightZ = lightZField.getFloat(threadedRenderer); + + Field lightRadiusField = threadedRendererClass.getDeclaredField("mLightRadius"); + lightRadiusField.setAccessible(true); + float mLightRadius = lightRadiusField.getFloat(threadedRenderer); + + final float lightX = mWindowLeft; + final float lightY = mLightY - mWindowTop; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ((HardwareRenderer) threadedRenderer).setLightSourceGeometry( + lightX, lightY, mLightZ, mLightRadius + ); + } else { + Field nativeProxyField = threadedRendererClass.getDeclaredField("mNativeProxy"); + nativeProxyField.setAccessible(true); + long mNativeProxy = nativeProxyField.getLong(threadedRenderer); + + Method nSetLightCenterMethod = threadedRendererClass.getDeclaredMethod("nSetLightCenter", long.class, float.class, float.class, float.class); + nSetLightCenterMethod.setAccessible(true); + nSetLightCenterMethod.invoke(null, mNativeProxy, lightX, lightY, mLightZ); + } + } catch (Throwable tr) { + tr.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/PropertyHolder.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/PropertyHolder.java new file mode 100644 index 000000000..fb1e5df5a --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/PropertyHolder.java @@ -0,0 +1,40 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import android.graphics.Rect; +import android.os.Build; +import android.view.View; + +import androidx.annotation.RequiresApi; + + +/** + * Holder class holds background drawable and content view. + */ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +class PropertyHolder { + + private final CustomBoundsDrawable mBackground; + private final View mContentView; + + public PropertyHolder(CustomBoundsDrawable background, View contentView) { + mBackground = background; + mContentView = contentView; + } + + private CustomBoundsDrawable getBackground() { + return mBackground; + } + + public View getContentView() { + return mContentView; + } + + public Rect getBounds() { + return getBackground().getBounds(); + } + + public void setBounds(Rect value) { + getBackground().setCustomBounds(value); + getContentView().invalidateOutline(); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/RectEvaluator.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/RectEvaluator.java new file mode 100644 index 000000000..1e04899c1 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/RectEvaluator.java @@ -0,0 +1,30 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import android.animation.TypeEvaluator; +import android.annotation.SuppressLint; +import android.graphics.Rect; + +/** + * This evaluator can be used to perform type interpolation between {@link Rect}. + */ + +class RectEvaluator implements TypeEvaluator { + + private final Rect mMax; + private final Rect mTemp = new Rect(); + + public RectEvaluator(Rect max) { + mMax = max; + } + + @SuppressLint("CheckResult") + @Override + public Rect evaluate(float fraction, Rect startValue, Rect endValue) { + mTemp.left = startValue.left + (int) ((endValue.left - startValue.left) * fraction); + mTemp.top = startValue.top + (int) ((endValue.top - startValue.top) * fraction); + mTemp.right = startValue.right + (int) ((endValue.right - startValue.right) * fraction); + mTemp.bottom = startValue.bottom + (int) ((endValue.bottom - startValue.bottom) * fraction); + mTemp.setIntersect(mMax, mTemp); + return mTemp; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuAnimation.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuAnimation.java new file mode 100644 index 000000000..fd2b91b5d --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuAnimation.java @@ -0,0 +1,111 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.FloatEvaluator; +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; + +/** + * Helper class to create and start animation of Simple Menu. + *

      + * TODO let params styleable + */ + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +class SimpleMenuAnimation { + + public static void startEnterAnimation(final CustomBoundsDrawable background, final View view, int width, int height, + int centerX, int centerY, Rect start, + int itemHeight, int elevation, int selectedIndex) { + PropertyHolder holder = new PropertyHolder(background, view); + Animator backgroundAnimator = createBoundsAnimator( + holder, width, height, centerX, centerY, start); + Animator elevationAnimator = createElevationAnimator(view, elevation); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(backgroundAnimator, elevationAnimator); + animatorSet.setDuration(backgroundAnimator.getDuration()); + animatorSet.start(); + + long delay = 0; + + if (view instanceof ViewGroup) { + for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) { + int offset = selectedIndex - i; + startChild(((ViewGroup) view).getChildAt(i), delay + 30 * Math.abs(offset), + offset == 0 ? 0 : (int) (itemHeight * 0.2) * (offset < 0 ? -1 : 1)); + } + } + } + + private static void startChild(View child, long delay, int translationY) { + child.setAlpha(0); + + Animator alphaAnimator = ObjectAnimator.ofFloat(child, "alpha", 0.0f, 1.0f); + alphaAnimator.setDuration(200); + alphaAnimator.setInterpolator(new AccelerateInterpolator()); + + Animator translationAnimator = ObjectAnimator.ofFloat(child, "translationY", translationY, 0); + translationAnimator.setDuration(275); + translationAnimator.setInterpolator(new DecelerateInterpolator()); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(alphaAnimator, translationAnimator); + animatorSet.setStartDelay(delay); + animatorSet.start(); + } + + private static Rect[] getBounds( + int width, int height, int centerX, int centerY) { + int endWidth = Math.max(centerX, width - centerX); + int endHeight = Math.max(centerY, height - centerY); + + int endLeft = centerX - endWidth; + int endRight = centerX + endWidth; + int endTop = centerY - endHeight; + int endBottom = centerY + endHeight; + + Rect end = new Rect(endLeft, endTop, endRight, endBottom); + Rect max = new Rect(0, 0, width, height); + + return new Rect[]{end, max}; + } + + private static Animator createBoundsAnimator(PropertyHolder holder, + int width, int height, int centerX, int centerY, Rect start) { + int speed = 4096; + + int endWidth = Math.max(centerX, width - centerX); + int endHeight = Math.max(centerY, height - centerY); + + Rect[] rect = getBounds(width, height, centerX, centerY); + Rect end = rect[0]; + Rect max = rect[1]; + + long duration = (long) ((float) Math.max(endWidth, endHeight) / speed * 1000); + duration = Math.max(duration, 150); + duration = Math.min(duration, 300); + + Animator animator = ObjectAnimator.ofObject(holder, SimpleMenuBoundsProperty.BOUNDS, new RectEvaluator(max), start, end); + animator.setInterpolator(new DecelerateInterpolator()); + animator.setDuration(duration); + return animator; + } + + @SuppressWarnings("unchecked") + private static Animator createElevationAnimator(View view, float elevation) { + Animator animator = ObjectAnimator.ofObject(view, View.TRANSLATION_Z, (TypeEvaluator) new FloatEvaluator(), -elevation, 0f); + animator.setInterpolator(new FastOutSlowInInterpolator()); + return animator; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuBoundsProperty.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuBoundsProperty.java new file mode 100644 index 000000000..76ca4ee8d --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuBoundsProperty.java @@ -0,0 +1,34 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.os.Build; +import android.util.Property; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +class SimpleMenuBoundsProperty extends Property { + + public static final Property BOUNDS; + + static { + BOUNDS = new SimpleMenuBoundsProperty("bounds"); + } + + @Override + public Rect get(PropertyHolder holder) { + return holder.getBounds(); + } + + @Override + public void set(PropertyHolder holder, Rect value) { + holder.setBounds(value); + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { + holder.getContentView().invalidate(); + } + } + + public SimpleMenuBoundsProperty(String name) { + super(Rect.class, name); + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuListAdapter.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuListAdapter.java new file mode 100644 index 000000000..10303ab48 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuListAdapter.java @@ -0,0 +1,79 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import static com.seafile.seadroid2.widget.prefs.simplemenu.SimpleMenuPopupWindow.DIALOG; +import static com.seafile.seadroid2.widget.prefs.simplemenu.SimpleMenuPopupWindow.HORIZONTAL; + +import android.os.Build; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckedTextView; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.recyclerview.widget.RecyclerView; + +import com.seafile.seadroid2.R; + +class SimpleMenuListAdapter extends RecyclerView.Adapter { + + private final SimpleMenuPopupWindow mWindow; + + public SimpleMenuListAdapter(SimpleMenuPopupWindow window) { + super(); + + mWindow = window; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.simple_menu_item, parent, false)); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + holder.bind(mWindow, position); + } + + @Override + public int getItemCount() { + return mWindow.getEntries() == null ? 0 : mWindow.getEntries().length; + } + + static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + + public CheckedTextView mCheckedTextView; + + private SimpleMenuPopupWindow mWindow; + + public ViewHolder(View itemView) { + super(itemView); + + mCheckedTextView = itemView.findViewById(android.R.id.text1); + itemView.setOnClickListener(this); + } + + public void bind(SimpleMenuPopupWindow window, int position) { + mWindow = window; + mCheckedTextView.setText(mWindow.getEntries()[position]); + mCheckedTextView.setChecked(position == mWindow.getSelectedIndex()); + mCheckedTextView.setMaxLines(mWindow.getMode() == DIALOG ? Integer.MAX_VALUE : 1); + + int padding = mWindow.listPadding[mWindow.getMode()][HORIZONTAL]; + int paddingVertical = mCheckedTextView.getPaddingTop(); + mCheckedTextView.setPadding(padding, paddingVertical, padding, paddingVertical); + } + + @Override + public void onClick(View view) { + if (mWindow.getOnItemClickListener() != null) { + mWindow.getOnItemClickListener().onClick(getAdapterPosition()); + } + + if (mWindow.isShowing()) { + mWindow.dismiss(); + } + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuPopupWindow.java b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuPopupWindow.java new file mode 100644 index 000000000..36c7dab50 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/widget/prefs/simplemenu/SimpleMenuPopupWindow.java @@ -0,0 +1,445 @@ +package com.seafile.seadroid2.widget.prefs.simplemenu; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.PopupWindow; +import android.widget.TextView; + +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Arrays; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.widget.prefs.SimpleMenuPreference; + +/** + * Extension of {@link PopupWindow} that implements Simple Menus in Material Design 1. + */ +@RestrictTo(LIBRARY_GROUP_PREFIX) +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class SimpleMenuPopupWindow extends PopupWindow { + + public static final int POPUP_MENU = 0; + public static final int DIALOG = 1; + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + public interface OnItemClickListener { + void onClick(int i); + } + + protected final int[] elevation = new int[2]; + protected final int[][] margin = new int[2][2]; + protected final int[][] listPadding = new int[2][2]; + protected final int itemHeight; + protected final int dialogMaxWidth; + protected final int unit; + protected final int maxUnits; + + private int mMode = POPUP_MENU; + + private boolean mRequestMeasure = true; + + private final RecyclerView mList; + private final SimpleMenuListAdapter mAdapter; + + private OnItemClickListener mOnItemClickListener; + private CharSequence[] mEntries; + private int mSelectedIndex; + + private int mMeasuredWidth; + + public SimpleMenuPopupWindow(Context context) { + this(context, null); + } + + public SimpleMenuPopupWindow(Context context, AttributeSet attrs) { + this(context, attrs, R.styleable.SimpleMenuPreference_android_popupMenuStyle); + } + + public SimpleMenuPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, R.style.Widget_Preference_SimpleMenuPreference_PopupMenu); + } + + @SuppressLint("InflateParams") + public SimpleMenuPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + setFocusable(true); + setOutsideTouchable(false); + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.SimpleMenuPopup, defStyleAttr, defStyleRes); + + elevation[POPUP_MENU] = (int) a.getDimension(R.styleable.SimpleMenuPopup_listElevation, 4f); + elevation[DIALOG] = (int) a.getDimension(R.styleable.SimpleMenuPopup_dialogElevation, 48f); + margin[POPUP_MENU][HORIZONTAL] = (int) a.getDimension(R.styleable.SimpleMenuPopup_listMarginHorizontal, 0); + margin[POPUP_MENU][VERTICAL] = (int) a.getDimension(R.styleable.SimpleMenuPopup_listMarginVertical, 0); + margin[DIALOG][HORIZONTAL] = (int) a.getDimension(R.styleable.SimpleMenuPopup_dialogMarginHorizontal, 0); + margin[DIALOG][VERTICAL] = (int) a.getDimension(R.styleable.SimpleMenuPopup_dialogMarginVertical, 0); + listPadding[POPUP_MENU][HORIZONTAL] = (int) a.getDimension(R.styleable.SimpleMenuPopup_listItemPadding, 0); + listPadding[DIALOG][HORIZONTAL] = (int) a.getDimension(R.styleable.SimpleMenuPopup_dialogItemPadding, 0); + dialogMaxWidth = (int) a.getDimension(R.styleable.SimpleMenuPopup_dialogMaxWidth, 0); + unit = (int) a.getDimension(R.styleable.SimpleMenuPopup_unit, 0); + maxUnits = a.getInteger(R.styleable.SimpleMenuPopup_maxUnits, 0); + + mList = (RecyclerView) LayoutInflater.from(context).inflate(R.layout.simple_menu_list, null); + mList.setFocusable(true); + mList.setLayoutManager(new LinearLayoutManager(context)); + mList.setItemAnimator(null); + mList.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + getBackground().getOutline(outline); + } + }); + mList.setClipToOutline(true); + + setContentView(mList); + + mAdapter = new SimpleMenuListAdapter(this); + mList.setAdapter(mAdapter); + + a.recycle(); + + // TODO do not hardcode + itemHeight = Math.round(context.getResources().getDisplayMetrics().density * 48); + listPadding[POPUP_MENU][VERTICAL] = listPadding[DIALOG][VERTICAL] = Math.round(context.getResources().getDisplayMetrics().density * 8); + } + + public OnItemClickListener getOnItemClickListener() { + return mOnItemClickListener; + } + + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + mOnItemClickListener = onItemClickListener; + } + + protected int getMode() { + return mMode; + } + + private void setMode(int mode) { + mMode = mode; + } + + protected CharSequence[] getEntries() { + return mEntries; + } + + public void setEntries(CharSequence[] entries) { + mEntries = entries; + } + + protected int getSelectedIndex() { + return mSelectedIndex; + } + + public void setSelectedIndex(int selectedIndex) { + mSelectedIndex = selectedIndex; + } + + @Override + public RecyclerView getContentView() { + return (RecyclerView) super.getContentView(); + } + + @Override + public CustomBoundsDrawable getBackground() { + Drawable background = super.getBackground(); + if (background != null + && !(background instanceof CustomBoundsDrawable)) { + setBackgroundDrawable(background); + } + return (CustomBoundsDrawable) super.getBackground(); + } + + @Override + public void setBackgroundDrawable(Drawable background) { + if (background == null) { + throw new IllegalStateException("SimpleMenuPopupWindow must have a background"); + } + + if (!(background instanceof CustomBoundsDrawable)) { + background = new CustomBoundsDrawable(background); + } + super.setBackgroundDrawable(background); + } + + /** + * Show the PopupWindow + * + * @param anchor View that will be used to calc the position of windows + * @param container View that will be used to calc the position of windows + * @param extraMargin extra margin start + */ + public void show(View anchor, View container, int extraMargin) { + int maxMaxWidth = container.getWidth() - margin[POPUP_MENU][HORIZONTAL] * 2; + int measuredWidth = measureWidth(maxMaxWidth, mEntries); + if (measuredWidth == -1) { + setMode(DIALOG); + } else if (measuredWidth != 0) { + setMode(POPUP_MENU); + + mMeasuredWidth = measuredWidth; + } + + mAdapter.notifyDataSetChanged(); + + // clear last bounds + Rect zeroRect = new Rect(); + getBackground().setCustomBounds(zeroRect); + getContentView().invalidateOutline(); + + if (mMode == POPUP_MENU) { + showPopupMenu(anchor, container, mMeasuredWidth, extraMargin); + + if (SimpleMenuPreference.isLightFixEnabled()) { + mList.post(() -> Light.resetLightCenterForPopupWindow(SimpleMenuPopupWindow.this)); + } + } else { + showDialog(anchor, container); + } + } + + /** + * Show popup window in dialog mode + * + * @param parent a parent view to get the {@link View#getWindowToken()} token from + * @param container Container view that holds preference list, also used to calc width + */ + private void showDialog(View parent, View container) { + final int index = Math.max(0, mSelectedIndex); + final int count = mEntries.length; + + getContentView().setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS); + getContentView().scrollToPosition(index); + + setWidth(Math.min(dialogMaxWidth, container.getWidth() - margin[DIALOG][HORIZONTAL] * 2)); + setHeight(WRAP_CONTENT); + setAnimationStyle(R.style.Animation_Preference_SimpleMenuCenter); + setElevation(elevation[DIALOG]); + + super.showAtLocation(parent, Gravity.CENTER_VERTICAL, 0, 0); + + getContentView().post(() -> { + // disable over scroll when no scroll + LinearLayoutManager lm = (LinearLayoutManager) getContentView().getLayoutManager(); + if (lm.findFirstCompletelyVisibleItemPosition() == 0 + && lm.findLastCompletelyVisibleItemPosition() == count - 1) { + getContentView().setOverScrollMode(View.OVER_SCROLL_NEVER); + } + + int width = getContentView().getWidth(); + int height = getContentView().getHeight(); + Rect start = new Rect(width / 2, height / 2, width / 2, height / 2); + + SimpleMenuAnimation.startEnterAnimation(getBackground(), getContentView(), + width, height, width / 2, height / 2, start, itemHeight, elevation[DIALOG] / 4, index); + }); + } + + /** + * Show popup window in popup mode + * + * @param anchor View that will be used to calc the position of the window + * @param container Container view that holds preference list, also used to calc width + * @param width Measured width of this window + */ + private void showPopupMenu(View anchor, View container, int width, int extraMargin) { + final boolean rtl = container.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + + final int index = Math.max(0, mSelectedIndex); + final int count = mEntries.length; + + final int anchorTop = anchor.getTop() - container.getPaddingTop(); + final int anchorHeight = anchor.getHeight(); + final int measuredHeight = itemHeight * count + listPadding[POPUP_MENU][VERTICAL] * 2; + + int[] location = new int[2]; + container.getLocationInWindow(location); + + final int containerTopInWindow = location[1] + container.getPaddingTop(); + final int containerHeight = container.getHeight() - container.getPaddingTop() - container.getPaddingBottom(); + + int y; + + int height; + int elevation = this.elevation[POPUP_MENU]; + int centerX = rtl + ? location[0] + extraMargin - width + listPadding[POPUP_MENU][HORIZONTAL] + : location[0] + extraMargin + listPadding[POPUP_MENU][HORIZONTAL]; + int centerY; + int animItemHeight = itemHeight + listPadding[POPUP_MENU][VERTICAL] * 2; + int animIndex = index; + Rect animStartRect; + + if (measuredHeight > containerHeight) { + // too high, use scroll + y = containerTopInWindow + margin[POPUP_MENU][VERTICAL]; + + // scroll to select item + final int scroll = itemHeight * index + - anchorTop + listPadding[POPUP_MENU][VERTICAL] + margin[POPUP_MENU][VERTICAL] + - anchorHeight / 2 + itemHeight / 2; + + getContentView().post(() -> { + getContentView().scrollBy(0, -measuredHeight); // to top + getContentView().scrollBy(0, scroll); + }); + getContentView().setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS); + + height = containerHeight - margin[POPUP_MENU][VERTICAL] * 2; + + centerY = itemHeight * index; + } else { + // calc align to selected + y = containerTopInWindow + anchorTop + anchorHeight / 2 - itemHeight / 2 + - listPadding[POPUP_MENU][VERTICAL] - index * itemHeight; + + // make sure window is in parent view + int maxY = containerTopInWindow + containerHeight + - measuredHeight - margin[POPUP_MENU][VERTICAL]; + y = Math.min(y, maxY); + + int minY = containerTopInWindow + margin[POPUP_MENU][VERTICAL]; + y = Math.max(y, minY); + + getContentView().setOverScrollMode(View.OVER_SCROLL_NEVER); + + height = measuredHeight; + + // center of selected item + centerY = (int) (listPadding[POPUP_MENU][VERTICAL] + index * itemHeight + itemHeight * 0.5); + } + + setWidth(width); + setHeight(height); + setElevation(elevation); + setAnimationStyle(R.style.Animation_Preference_SimpleMenuCenter); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setEnterTransition(null); + setExitTransition(null); + } + + super.showAtLocation(anchor, Gravity.NO_GRAVITY, centerX, y); + + int startTop = centerY - (int) (itemHeight * 0.2); + int startBottom = centerY + (int) (itemHeight * 0.2); + int startLeft; + int startRight; + + if (!rtl) { + startLeft = centerX; + startRight = centerX + unit; + } else { + startLeft = centerX + width - unit; + startRight = centerX + width; + } + + animStartRect = new Rect(startLeft, startTop, startRight, startBottom); + + int animElevation = (int) Math.round(elevation * 0.25); + + getContentView().post(() -> SimpleMenuAnimation.startEnterAnimation(getBackground(), getContentView(), + width, height, centerX, centerY, animStartRect, animItemHeight, animElevation, animIndex)); + } + + /** + * Request a measurement before next show, call this when entries changed. + */ + public void requestMeasure() { + mRequestMeasure = true; + } + + /** + * Measure window width + * + * @param maxWidth max width for popup + * @param entries Entries of preference hold this window + * @return 0: skip + * -1: use dialog + * other: measuredWidth + */ + private int measureWidth(int maxWidth, CharSequence[] entries) { + // skip if should not measure + if (!mRequestMeasure) { + return 0; + } + + mRequestMeasure = false; + + entries = Arrays.copyOf(entries, entries.length); + + Arrays.sort(entries, (o1, o2) -> o2.length() - o1.length()); + + Context context = getContentView().getContext(); + int width = 0; + + maxWidth = Math.min(unit * maxUnits, maxWidth); + + Rect bounds = new Rect(); + + TextView view = LayoutInflater.from(context).inflate(R.layout.simple_menu_item, null, false).findViewById(android.R.id.text1); + Paint textPaint = view.getPaint(); + + for (CharSequence chs : entries) { + textPaint.getTextBounds(chs.toString(), 0, chs.toString().length(), bounds); + + width = Math.max(width, bounds.right + 1 + Math.round(listPadding[POPUP_MENU][HORIZONTAL] * 2 + 1)); + + // more than one line should use dialog + if (width > maxWidth + || chs.toString().contains("\n")) { + return -1; + } + } + + // width is a multiple of a unit + int w = 0; + while (width > w) { + w += unit; + } + + return w; + } + + @Override + public void showAtLocation(View parent, int gravity, int x, int y) { + throw new UnsupportedOperationException("use show(anchor) to show the window"); + } + + @Override + public void showAsDropDown(View anchor) { + throw new UnsupportedOperationException("use show(anchor) to show the window"); + } + + @Override + public void showAsDropDown(View anchor, int xoff, int yoff) { + throw new UnsupportedOperationException("use show(anchor) to show the window"); + } + + @Override + public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { + throw new UnsupportedOperationException("use show(anchor) to show the window"); + } +} diff --git a/app/src/main/res/anim/grow_fade_in.xml b/app/src/main/res/anim/grow_fade_in.xml new file mode 100644 index 000000000..ba658277b --- /dev/null +++ b/app/src/main/res/anim/grow_fade_in.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/app/src/main/res/anim/grow_fade_in_center.xml b/app/src/main/res/anim/grow_fade_in_center.xml new file mode 100644 index 000000000..bc0c00882 --- /dev/null +++ b/app/src/main/res/anim/grow_fade_in_center.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/app/src/main/res/anim/grow_fade_in_from_bottom.xml b/app/src/main/res/anim/grow_fade_in_from_bottom.xml new file mode 100644 index 000000000..97d29ef4f --- /dev/null +++ b/app/src/main/res/anim/grow_fade_in_from_bottom.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/app/src/main/res/anim/shrink_fade_out.xml b/app/src/main/res/anim/shrink_fade_out.xml new file mode 100644 index 000000000..049453e06 --- /dev/null +++ b/app/src/main/res/anim/shrink_fade_out.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/shrink_fade_out_center.xml b/app/src/main/res/anim/shrink_fade_out_center.xml new file mode 100644 index 000000000..35f4fe46f --- /dev/null +++ b/app/src/main/res/anim/shrink_fade_out_center.xml @@ -0,0 +1,37 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/shrink_fade_out_from_bottom.xml b/app/src/main/res/anim/shrink_fade_out_from_bottom.xml new file mode 100644 index 000000000..752447aaf --- /dev/null +++ b/app/src/main/res/anim/shrink_fade_out_from_bottom.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/error_img.png b/app/src/main/res/drawable-xxhdpi/error_img.png new file mode 100644 index 000000000..07f9535eb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/error_img.png differ diff --git a/app/src/main/res/drawable/baseline_close_24.xml b/app/src/main/res/drawable/baseline_close_24.xml new file mode 100644 index 000000000..3d89fb7ce --- /dev/null +++ b/app/src/main/res/drawable/baseline_close_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_comment_24.xml b/app/src/main/res/drawable/baseline_comment_24.xml new file mode 100644 index 000000000..52191e667 --- /dev/null +++ b/app/src/main/res/drawable/baseline_comment_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_delete_24.xml b/app/src/main/res/drawable/baseline_delete_24.xml new file mode 100644 index 000000000..af9a48e10 --- /dev/null +++ b/app/src/main/res/drawable/baseline_delete_24.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/baseline_download_24.xml b/app/src/main/res/drawable/baseline_download_24.xml new file mode 100644 index 000000000..c9ca2f102 --- /dev/null +++ b/app/src/main/res/drawable/baseline_download_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_info_24.xml b/app/src/main/res/drawable/baseline_info_24.xml new file mode 100644 index 000000000..4ae16d020 --- /dev/null +++ b/app/src/main/res/drawable/baseline_info_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_mark_32.xml b/app/src/main/res/drawable/baseline_mark_32.xml new file mode 100644 index 000000000..b24b5151c --- /dev/null +++ b/app/src/main/res/drawable/baseline_mark_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_outline_24.xml b/app/src/main/res/drawable/baseline_outline_24.xml new file mode 100644 index 000000000..e06100424 --- /dev/null +++ b/app/src/main/res/drawable/baseline_outline_24.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/baseline_sdoc_mark_as_resolved_32.xml b/app/src/main/res/drawable/baseline_sdoc_mark_as_resolved_32.xml new file mode 100644 index 000000000..5291f7c0e --- /dev/null +++ b/app/src/main/res/drawable/baseline_sdoc_mark_as_resolved_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_share_24.xml b/app/src/main/res/drawable/baseline_share_24.xml new file mode 100644 index 000000000..a3390defe --- /dev/null +++ b/app/src/main/res/drawable/baseline_share_24.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/baseline_star_outline_24.xml b/app/src/main/res/drawable/baseline_star_outline_24.xml deleted file mode 100644 index 0735fb743..000000000 --- a/app/src/main/res/drawable/baseline_star_outline_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_starred_32.xml b/app/src/main/res/drawable/baseline_starred_32.xml deleted file mode 100644 index 6153d4543..000000000 --- a/app/src/main/res/drawable/baseline_starred_32.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_starred_filled_24.xml b/app/src/main/res/drawable/baseline_starred_filled_24.xml new file mode 100644 index 000000000..45b89e79b --- /dev/null +++ b/app/src/main/res/drawable/baseline_starred_filled_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_starred_filled_32.xml b/app/src/main/res/drawable/baseline_starred_filled_32.xml new file mode 100644 index 000000000..9efb9bdec --- /dev/null +++ b/app/src/main/res/drawable/baseline_starred_filled_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_starred_outline_24.xml b/app/src/main/res/drawable/baseline_starred_outline_24.xml new file mode 100644 index 000000000..b3d1e96ed --- /dev/null +++ b/app/src/main/res/drawable/baseline_starred_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 000000000..60bd629b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/loading_anim.xml b/app/src/main/res/drawable/loading_anim.xml deleted file mode 100644 index 46e8eb78b..000000000 --- a/app/src/main/res/drawable/loading_anim.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ripple_icon_large.xml b/app/src/main/res/drawable/ripple_icon_large.xml new file mode 100644 index 000000000..877420f2e --- /dev/null +++ b/app/src/main/res/drawable/ripple_icon_large.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ripple_icon_little.xml b/app/src/main/res/drawable/ripple_icon_little.xml new file mode 100644 index 000000000..b19423458 --- /dev/null +++ b/app/src/main/res/drawable/ripple_icon_little.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ripple_icon_middle.xml b/app/src/main/res/drawable/ripple_icon_middle.xml new file mode 100644 index 000000000..2cf503772 --- /dev/null +++ b/app/src/main/res/drawable/ripple_icon_middle.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_item_background_oval.xml b/app/src/main/res/drawable/selectable_item_background_oval.xml deleted file mode 100644 index 07e053ad5..000000000 --- a/app/src/main/res/drawable/selectable_item_background_oval.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/selection_control_ripple.xml b/app/src/main/res/drawable/selection_control_ripple.xml deleted file mode 100644 index 6e36e0e6c..000000000 --- a/app/src/main/res/drawable/selection_control_ripple.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/shape_solid_grey103_r2.xml b/app/src/main/res/drawable/shape_activity_item_action_type.xml similarity index 72% rename from app/src/main/res/drawable/shape_solid_grey103_r2.xml rename to app/src/main/res/drawable/shape_activity_item_action_type.xml index 6dc53c8b2..5fc91947f 100644 --- a/app/src/main/res/drawable/shape_solid_grey103_r2.xml +++ b/app/src/main/res/drawable/shape_activity_item_action_type.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_solid_ff_radius_8.xml b/app/src/main/res/drawable/shape_solid_ff_radius_8.xml index f5b590238..7dd98821a 100644 --- a/app/src/main/res/drawable/shape_solid_ff_radius_8.xml +++ b/app/src/main/res/drawable/shape_solid_ff_radius_8.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_radius_solid_orange.xml b/app/src/main/res/drawable/shape_solid_grey100_radius_4.xml similarity index 55% rename from app/src/main/res/drawable/shape_radius_solid_orange.xml rename to app/src/main/res/drawable/shape_solid_grey100_radius_4.xml index 89226f692..ccd3eb776 100644 --- a/app/src/main/res/drawable/shape_radius_solid_orange.xml +++ b/app/src/main/res/drawable/shape_solid_grey100_radius_4.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_stroke1_radius8_solid_grey.xml b/app/src/main/res/drawable/shape_stroke1_radius8_solid_grey.xml deleted file mode 100644 index 2213cd04b..000000000 --- a/app/src/main/res/drawable/shape_stroke1_radius8_solid_grey.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_stroke_radius_solid_gray.xml b/app/src/main/res/drawable/shape_stroke_radius_solid_gray.xml deleted file mode 100644 index dbd3d9327..000000000 --- a/app/src/main/res/drawable/shape_stroke_radius_solid_gray.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/simple_menu_background.xml b/app/src/main/res/drawable/simple_menu_background.xml new file mode 100644 index 000000000..6e0d2ff80 --- /dev/null +++ b/app/src/main/res/drawable/simple_menu_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/simple_menu_item_background.xml b/app/src/main/res/drawable/simple_menu_item_background.xml new file mode 100644 index 000000000..4ccfc9e60 --- /dev/null +++ b/app/src/main/res/drawable/simple_menu_item_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_carousel_image_preview.xml b/app/src/main/res/layout/activity_carousel_image_preview.xml new file mode 100644 index 000000000..4cbede298 --- /dev/null +++ b/app/src/main/res/layout/activity_carousel_image_preview.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_sdoc_comment.xml b/app/src/main/res/layout/activity_doc_comment.xml similarity index 80% rename from app/src/main/res/layout/activity_sdoc_comment.xml rename to app/src/main/res/layout/activity_doc_comment.xml index 5a4010868..314503033 100644 --- a/app/src/main/res/layout/activity_sdoc_comment.xml +++ b/app/src/main/res/layout/activity_doc_comment.xml @@ -4,11 +4,12 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="@color/window_background_color" android:orientation="vertical"> + layout="@layout/toolbar_actionbar" /> + android:paddingBottom="32dp" + tools:listitem="@layout/item_file_comment" /> @@ -33,7 +32,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:visibility="gone"> + android:visibility="visible"> @@ -90,19 +89,12 @@ android:id="@+id/submit" android:layout_width="wrap_content" android:layout_height="48dp" - android:layout_marginStart="12dp" - android:layout_marginEnd="8dp" - android:foreground="?selectableItemBackground" + android:background="@drawable/ripple_icon_middle" android:gravity="center" - android:paddingHorizontal="4dp" + android:paddingHorizontal="16dp" android:text="@string/confirm" android:textColor="@color/fancy_orange" android:textSize="16sp" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_image_preview.xml b/app/src/main/res/layout/activity_image_preview.xml index c1f5d7f55..53a7db8c7 100644 --- a/app/src/main/res/layout/activity_image_preview.xml +++ b/app/src/main/res/layout/activity_image_preview.xml @@ -28,7 +28,7 @@ android:background="@drawable/shape_stroke_radius_solid_translucent" android:foreground="?selectableItemBackground" android:scaleType="centerInside" - android:src="@drawable/action_download" + android:src="@drawable/baseline_download_24" app:shapeAppearance="@style/ShapeCircleStyle" app:tint="@color/white" /> @@ -40,7 +40,7 @@ android:background="@drawable/shape_stroke_radius_solid_translucent" android:foreground="?selectableItemBackground" android:scaleType="centerInside" - android:src="@drawable/action_delete" + android:src="@drawable/baseline_delete_24" app:shapeAppearance="@style/ShapeCircleStyle" app:tint="@color/white" /> @@ -52,7 +52,7 @@ android:background="@drawable/shape_stroke_radius_solid_translucent" android:foreground="?selectableItemBackground" android:scaleType="centerInside" - android:src="@drawable/baseline_star_outline_24" + android:src="@drawable/baseline_starred_outline_24" app:shapeAppearance="@style/ShapeCircleStyle" app:tint="@color/white" /> @@ -63,7 +63,7 @@ android:background="@drawable/shape_stroke_radius_solid_translucent" android:foreground="?selectableItemBackground" android:scaleType="centerInside" - android:src="@drawable/action_share" + android:src="@drawable/baseline_share_24" app:shapeAppearance="@style/ShapeCircleStyle" app:tint="@color/white" /> diff --git a/app/src/main/res/layout/activity_only_image_preview.xml b/app/src/main/res/layout/activity_only_image_preview.xml new file mode 100644 index 000000000..6e9572578 --- /dev/null +++ b/app/src/main/res/layout/activity_only_image_preview.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_sea_webview_pro.xml b/app/src/main/res/layout/activity_sea_webview_pro.xml index 6820b1abd..567780849 100644 --- a/app/src/main/res/layout/activity_sea_webview_pro.xml +++ b/app/src/main/res/layout/activity_sea_webview_pro.xml @@ -36,55 +36,57 @@ - - - - - - - - - - - - + android:layout_height="match_parent" + android:orientation="horizontal"> - + - + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:foreground="@drawable/ripple_icon_middle" + android:scaleType="centerInside" + android:src="@drawable/baseline_info_24" + app:tint="@color/bottom_bar_icon_tint_color" /> - - + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:foreground="@drawable/ripple_icon_middle" + android:scaleType="centerInside" + android:src="@drawable/baseline_comment_24" + app:tint="@color/bottom_bar_icon_tint_color" /> + + diff --git a/app/src/main/res/layout/dialog_sdoc_profile.xml b/app/src/main/res/layout/dialog_file_profile.xml similarity index 79% rename from app/src/main/res/layout/dialog_sdoc_profile.xml rename to app/src/main/res/layout/dialog_file_profile.xml index 1d72c36c2..f508990ee 100644 --- a/app/src/main/res/layout/dialog_sdoc_profile.xml +++ b/app/src/main/res/layout/dialog_file_profile.xml @@ -9,6 +9,16 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + + diff --git a/app/src/main/res/layout/dialog_sdoc_directory.xml b/app/src/main/res/layout/dialog_sdoc_directory.xml index 52f52f3ab..983488696 100644 --- a/app/src/main/res/layout/dialog_sdoc_directory.xml +++ b/app/src/main/res/layout/dialog_sdoc_directory.xml @@ -19,6 +19,6 @@ style="@style/Widget.FastRecyclerView.Style" android:layout_width="match_parent" android:layout_height="match_parent" - tools:listitem="@layout/item_sdoc_directory" /> + tools:listitem="@layout/item_sdoc_outline" /> diff --git a/app/src/main/res/layout/fragment_photo_view.xml b/app/src/main/res/layout/fragment_photo_view.xml index d21477f9e..f13d52226 100644 --- a/app/src/main/res/layout/fragment_photo_view.xml +++ b/app/src/main/res/layout/fragment_photo_view.xml @@ -1,5 +1,6 @@ @@ -12,5 +13,28 @@ android:id="@+id/progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center" /> + android:layout_gravity="center" + android:visibility="gone" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/gallery_activity_layout.xml b/app/src/main/res/layout/gallery_activity_layout.xml deleted file mode 100644 index a5a3f937f..000000000 --- a/app/src/main/res/layout/gallery_activity_layout.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_activity.xml b/app/src/main/res/layout/item_activity.xml index 271b26792..8c97791ba 100644 --- a/app/src/main/res/layout/item_activity.xml +++ b/app/src/main/res/layout/item_activity.xml @@ -37,7 +37,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="4dp" - android:background="@drawable/shape_solid_grey103_r2" + android:background="@drawable/shape_activity_item_action_type" android:paddingHorizontal="4dp" android:paddingVertical="2dp" android:textColor="@color/material_grey_666" diff --git a/app/src/main/res/layout/item_carousel_item_vertical.xml b/app/src/main/res/layout/item_carousel_item_vertical.xml index 9b3620380..d0f9c774a 100644 --- a/app/src/main/res/layout/item_carousel_item_vertical.xml +++ b/app/src/main/res/layout/item_carousel_item_vertical.xml @@ -1,33 +1,22 @@ - - - - - + diff --git a/app/src/main/res/layout/item_dirent.xml b/app/src/main/res/layout/item_dirent.xml index d636ad75d..3d93030cd 100644 --- a/app/src/main/res/layout/item_dirent.xml +++ b/app/src/main/res/layout/item_dirent.xml @@ -50,19 +50,11 @@ android:maxLines="1" android:orientation="horizontal"> - - + android:visibility="gone" /> + + @@ -92,7 +93,7 @@ android:layout_height="@dimen/lv_multi_select_height" android:layout_gravity="center" android:contentDescription="@string/file_action_more" - android:foreground="@drawable/selection_control_ripple" + android:foreground="@drawable/ripple_icon_little" android:padding="8dp" android:src="@drawable/baseline_more_vert_24" android:visibility="visible" @@ -106,7 +107,8 @@ android:layout_gravity="center" android:padding="6dp" android:src="@drawable/ic_checkbox_checked" - android:visibility="visible" /> + android:visibility="visible" + app:tint="@color/fancy_orange" /> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_repo.xml b/app/src/main/res/layout/item_repo.xml index e0c7ee235..c84522443 100644 --- a/app/src/main/res/layout/item_repo.xml +++ b/app/src/main/res/layout/item_repo.xml @@ -61,7 +61,7 @@ android:layout_width="@dimen/lv_multi_select_width" android:layout_height="@dimen/lv_multi_select_height" android:contentDescription="@string/file_action_more" - android:foreground="@drawable/selection_control_ripple" + android:foreground="@drawable/ripple_icon_little" android:padding="8dp" android:src="@drawable/baseline_more_vert_24" android:visibility="visible" diff --git a/app/src/main/res/layout/item_sdoc_directory.xml b/app/src/main/res/layout/item_sdoc_outline.xml similarity index 85% rename from app/src/main/res/layout/item_sdoc_directory.xml rename to app/src/main/res/layout/item_sdoc_outline.xml index 814086dc7..82e25e870 100644 --- a/app/src/main/res/layout/item_sdoc_directory.xml +++ b/app/src/main/res/layout/item_sdoc_outline.xml @@ -1,8 +1,9 @@ diff --git a/app/src/main/res/layout/item_starred.xml b/app/src/main/res/layout/item_starred.xml index 2b4d8b9f8..c9867e8d5 100644 --- a/app/src/main/res/layout/item_starred.xml +++ b/app/src/main/res/layout/item_starred.xml @@ -54,7 +54,7 @@ android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" android:contentDescription="@string/file_action_more" - android:foreground="@drawable/selection_control_ripple" + android:foreground="@drawable/ripple_icon_little" android:padding="8dp" android:scaleType="center" android:src="@drawable/baseline_more_vert_24" diff --git a/app/src/main/res/layout/item_transfer_list.xml b/app/src/main/res/layout/item_transfer_list.xml index a6cf03648..0efa6ae2f 100644 --- a/app/src/main/res/layout/item_transfer_list.xml +++ b/app/src/main/res/layout/item_transfer_list.xml @@ -130,7 +130,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/file_action_more" - android:foreground="@drawable/selection_control_ripple" + android:foreground="@drawable/ripple_icon_little" android:padding="8dp" android:src="@drawable/baseline_more_vert_24" android:visibility="visible" diff --git a/app/src/main/res/layout/layout_dialog_button_positive_negative_loading.xml b/app/src/main/res/layout/layout_dialog_button_positive_negative_loading.xml index aaebe067d..bd461d17e 100644 --- a/app/src/main/res/layout/layout_dialog_button_positive_negative_loading.xml +++ b/app/src/main/res/layout/layout_dialog_button_positive_negative_loading.xml @@ -2,15 +2,15 @@ + android:text="@string/empty_data" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_image.xml b/app/src/main/res/layout/layout_image.xml index 3ae1608ab..9ce235ec1 100644 --- a/app/src/main/res/layout/layout_image.xml +++ b/app/src/main/res/layout/layout_image.xml @@ -1,13 +1,15 @@ + android:layout_height="match_parent"> - + android:scaleType="centerCrop" + android:src="@drawable/shape_solid_grey100_radius_8" + app:shapeAppearance="@style/ShapeCorner4Style" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_logout_view.xml b/app/src/main/res/layout/layout_logout_view.xml deleted file mode 100644 index 23df0393a..000000000 --- a/app/src/main/res/layout/layout_logout_view.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pref_category.xml b/app/src/main/res/layout/layout_pref_category.xml new file mode 100644 index 000000000..c18fe2031 --- /dev/null +++ b/app/src/main/res/layout/layout_pref_category.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pref_text_and_more.xml b/app/src/main/res/layout/layout_pref_text_and_more.xml new file mode 100644 index 000000000..79dc3f107 --- /dev/null +++ b/app/src/main/res/layout/layout_pref_text_and_more.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pref_title_summary.xml b/app/src/main/res/layout/layout_pref_title_summary.xml new file mode 100644 index 000000000..19ffbef92 --- /dev/null +++ b/app/src/main/res/layout/layout_pref_title_summary.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pref_title_summary_empty.xml b/app/src/main/res/layout/layout_pref_title_summary_empty.xml new file mode 100644 index 000000000..47a58aab4 --- /dev/null +++ b/app/src/main/res/layout/layout_pref_title_summary_empty.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pref_title_switch.xml b/app/src/main/res/layout/layout_pref_title_switch.xml new file mode 100644 index 000000000..c64850092 --- /dev/null +++ b/app/src/main/res/layout/layout_pref_title_switch.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_cardbox.xml b/app/src/main/res/layout/preference_cardbox.xml deleted file mode 100644 index 2e8c4d8bb..000000000 --- a/app/src/main/res/layout/preference_cardbox.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/preference_simplemenu.xml b/app/src/main/res/layout/preference_simplemenu.xml new file mode 100644 index 000000000..1ef8bec02 --- /dev/null +++ b/app/src/main/res/layout/preference_simplemenu.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/simple_menu_item.xml b/app/src/main/res/layout/simple_menu_item.xml new file mode 100644 index 000000000..4fcd83080 --- /dev/null +++ b/app/src/main/res/layout/simple_menu_item.xml @@ -0,0 +1,32 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/simple_menu_list.xml b/app/src/main/res/layout/simple_menu_list.xml new file mode 100644 index 000000000..5abf520e4 --- /dev/null +++ b/app/src/main/res/layout/simple_menu_list.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_dialog_new_repo.xml b/app/src/main/res/layout/view_dialog_new_repo.xml index dd07483aa..519112b9d 100644 --- a/app/src/main/res/layout/view_dialog_new_repo.xml +++ b/app/src/main/res/layout/view_dialog_new_repo.xml @@ -17,7 +17,7 @@ android:layout_height="wrap_content" /> - +

      + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_sheet_op_dirent.xml b/app/src/main/res/menu/bottom_sheet_op_dirent.xml index 88213bf53..4e90d16ff 100644 --- a/app/src/main/res/menu/bottom_sheet_op_dirent.xml +++ b/app/src/main/res/menu/bottom_sheet_op_dirent.xml @@ -25,27 +25,31 @@ android:icon="@drawable/action_open" android:title="@string/file_action_open" /> - + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_sheet_op_transfer_list.xml b/app/src/main/res/menu/bottom_sheet_op_transfer_list.xml index 3c1f4f8a8..c56eba6bb 100644 --- a/app/src/main/res/menu/bottom_sheet_op_transfer_list.xml +++ b/app/src/main/res/menu/bottom_sheet_op_transfer_list.xml @@ -6,17 +6,12 @@ android:title="@string/delete" /> - - \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_sheet_unstarred.xml b/app/src/main/res/menu/bottom_sheet_unstarred.xml index a2c840bcb..82a8e0699 100644 --- a/app/src/main/res/menu/bottom_sheet_unstarred.xml +++ b/app/src/main/res/menu/bottom_sheet_unstarred.xml @@ -11,7 +11,7 @@ \ No newline at end of file diff --git a/app/src/main/res/menu/menu_comment_mark_delete.xml b/app/src/main/res/menu/menu_comment_mark_delete.xml new file mode 100644 index 000000000..e64d5eaf5 --- /dev/null +++ b/app/src/main/res/menu/menu_comment_mark_delete.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_image_list_preview.xml b/app/src/main/res/menu/menu_image_list_preview.xml new file mode 100644 index 000000000..043a5241c --- /dev/null +++ b/app/src/main/res/menu/menu_image_list_preview.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/transfer_list_menu.xml b/app/src/main/res/menu/transfer_list_menu.xml index 4896017d7..933d9c493 100644 --- a/app/src/main/res/menu/transfer_list_menu.xml +++ b/app/src/main/res/menu/transfer_list_menu.xml @@ -4,13 +4,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/transfer_list_multi_choice_menu.xml b/app/src/main/res/menu/transfer_list_multi_choice_menu.xml index 83667981d..69e15b1bc 100644 --- a/app/src/main/res/menu/transfer_list_multi_choice_menu.xml +++ b/app/src/main/res/menu/transfer_list_multi_choice_menu.xml @@ -10,7 +10,7 @@ diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 2313886bd..37707588d 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -58,13 +58,14 @@ @color/material_grey_911 @color/material_grey_200 @color/material_grey_200 + @color/material_grey_700 @color/material_grey_599 @color/bar_title_color - @color/material_grey_700 + @color/material_grey_666 @color/material_grey_700 @@ -91,4 +92,10 @@ #5e5e5e #2c2c2c + + @color/material_grey_919 + + + @color/material_grey_919 + @color/material_grey_900 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cdabb0d21..ecb58f7e4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -3,6 +3,7 @@ Добро пожаловать в Seafile Чтобы начать, выберите аккаунт Редактировать + Игнорировать Удалить Удалить Удалено @@ -22,6 +23,7 @@ Переместить папку Удалить папку Имя + Сервер Адрес сервера Эл. почта или логин Пароль @@ -327,6 +329,8 @@ Далее Пропустить + + Режим отображения Резервное копирование контактов Включить резервное копирование контактов @@ -585,6 +589,8 @@ Загрузка Сеть недоступна Ошибка сети + Загрузка не удалась + нажмите, чтобы повторить попытку Необходим пароль. Пароль слишком слабый. @@ -641,6 +647,7 @@ Включено Как в системе Пустой + Пустые данные Установить разрешение Предварительный просмотр и загрузка @@ -657,15 +664,21 @@ Последнее изменение Восходящий Сначала папки + Подробно Размер Последний модификатор Время последнего изменения + Рецензент Соавторы файлов Статус файла Выполняется На рассмотрении Завершено Устаревший + Теги + Отметить как решенное + Вы уверены, что хотите удалить этот элемент? + Отменено diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d689bcc15..513603c66 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -3,6 +3,7 @@ 欢迎使用Seafile 请选择一个账户开始使用 编辑帐户 + 忽略 删除帐户 删除 已删除 @@ -22,6 +23,7 @@ 移动文件夹 删除文件夹 名称 + 服务器 云盘网址 邮箱或用户名 密码 @@ -315,6 +317,8 @@ 下一个 跳过 + + 显示模式 通讯录备份 启用通讯录备份 @@ -441,11 +445,13 @@ Email: support@seafile.com
      网络连接错误,请稍后重试! 取消所有任务 + 取消所有正在进行的文件传输任务;单击重新启动按钮时,它们将被恢复以继续传输文件。 重新开始 重试失败的任务 重试取消的任务 清除失败的任务 清除所有任务 + 删除所有传输记录;如果是下载,它将删除所有下载记录(可选:本地文件也是)。如果是上传,它只会删除上传记录,但以后不会重新上传相应的目标文件。 清除完成的任务 下载列表 上传列表 @@ -559,6 +565,8 @@ Email: support@seafile.com
      上传中 网络不可用 网络异常 + 加载失败 + 点击重试 需要密码 密码太弱 @@ -615,6 +623,7 @@ Email: support@seafile.com
      开启 跟随系统 暂无 + 暂无数据 设置权限 预览和下载 @@ -631,15 +640,21 @@ Email: support@seafile.com
      最后修改 升序 文件夹优先 + 详情 大小 修改者 修改时间 + 文件审核人 文件协作人 文件状态 进行中 审核中 - 完成 + 已完成 已过期 + 标签 + 标记为已解决 + 你确定想要删除此条内容? + 已取消 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index b436f4ac2..165f4bc2f 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -71,4 +71,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 605a4e8c4..be8f929aa 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -27,6 +27,7 @@ #FFC783 #ea8201 #CF7300 + #44ea8201 @@ -60,6 +61,8 @@ @color/material_grey_100 @color/material_grey_900 @color/material_grey_900 + @color/material_grey_100_translucent + @color/material_grey_100 @color/material_grey_599 @@ -78,6 +81,7 @@ #F1F1F1 + #212529 @color/material_grey_599 @@ -95,4 +99,11 @@ #88000000 + + @color/material_grey_50 + + + @color/white + @color/material_grey_200 + diff --git a/app/src/main/res/values/colors_material.xml b/app/src/main/res/values/colors_material.xml index 1651cea50..872410610 100644 --- a/app/src/main/res/values/colors_material.xml +++ b/app/src/main/res/values/colors_material.xml @@ -261,6 +261,7 @@ #FAFAFA #F8F8F8 #F5F5F5 + #88F5F5F5 #F3F3F3 #EEEEEE #E0E0E0 @@ -275,6 +276,7 @@ #222222 #191919 + #00191919 #111111 #ECEFF1 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 4104e264e..476dcf67e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -190,4 +190,8 @@ 22sp 292.6dp + + 20dp + 40dp + 2dp \ No newline at end of file diff --git a/app/src/main/res/values/donottranslate_prefs.xml b/app/src/main/res/values/donottranslate_prefs.xml index ece347282..116393ed0 100644 --- a/app/src/main/res/values/donottranslate_prefs.xml +++ b/app/src/main/res/values/donottranslate_prefs.xml @@ -16,6 +16,7 @@ key_user_info key_space_info + key_user_server key_sign_out diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index cc9727188..8f4cae6eb 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -13,4 +13,6 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed9ae39c8..aefc08de7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ Welcome to Seafile Choose an account to start Edit + Ignore Delete Delete Deleted @@ -24,6 +25,7 @@ Move folder Delete folder Name + Server Server address Email or Username Password @@ -334,6 +336,10 @@ Next Skip + + + Display mode + Contacts backup Enable contacts backup @@ -460,11 +466,13 @@ Network connection error, please try again later! Cancel all tasks + Cancel all ongoing file transfer tasks; they will be restored to continue transferring files when the restart button is clicked. Restart Retry failed tasks Restart cancelled tasks Clear failed tasks Clear all tasks + Remove all transfer records; if it is a download, it will delete all download records (optionally: local files as well). If it is an upload, it will only delete the upload records, but the corresponding target files will not be re-uploaded in the future. Clear finished tasks Download List Upload List @@ -588,6 +596,8 @@ Uploading Network unavailable Network error + Load failed + click to retry Password is required. Password is too weak. @@ -647,6 +657,7 @@ On Follow system Empty + Empty data Set permission Preview and download @@ -664,6 +675,7 @@ Last modified Ascending Folders first + Detail Size @@ -676,8 +688,12 @@ In review Done Outdated + Tags + Mark as resolved + Are you sure you want to delete this item? + Canceled diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 87a53e5fc..cfa29a76f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -57,7 +57,7 @@ @null @color/selector_color_primary @color/selector_color_primary - @color/fancy_orange_light + @color/fancy_orange_translucent labeled ?actionBarSize 8dp @@ -72,6 +72,7 @@ @color/bar_background_color @color/bar_background_color + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/prefs_settings.xml b/app/src/main/res/xml/prefs_settings.xml index e22842324..d67a0f306 100644 --- a/app/src/main/res/xml/prefs_settings.xml +++ b/app/src/main/res/xml/prefs_settings.xml @@ -27,9 +27,10 @@ android:summaryOn="@string/gesture_lock_on" android:title="@string/gesture_lock" app:iconSpaceReserved="false" + app:isPreferenceVisible="false" app:useSimpleSummaryProvider="true" /> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/prefs_settings_camera_backup_advance_2.xml b/app/src/main/res/xml/prefs_settings_camera_backup_advance_2.xml new file mode 100644 index 000000000..a8748ab98 --- /dev/null +++ b/app/src/main/res/xml/prefs_settings_camera_backup_advance_2.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build_count.txt b/build_count.txt index d99e90eb9..dc7b54ad0 100644 --- a/build_count.txt +++ b/build_count.txt @@ -1 +1 @@ -29 \ No newline at end of file +33 \ No newline at end of file