#
简介智汀云盘(安卓版) 是一款在 Android Studio 中开发的 安卓云盘APP。该应用一直在积极升级以采用 Android 和 java 语言的最新功能。此应用配合智汀家庭云这款智能家居APP授权使用。
#
1. 快速上手#
1.1 开发工具开发平台:win 10
开发工具:Android Studio 4.2
#
1.2 源码地址Git Hub
名称 URL 描述 zhiting-nas-android https://github.com/zhiting-tech/zhiting-nas-android Android源码 gitee
名称 URL 描述 zhiting-nas-android https://gitee.com/zhiting-tech/zhiting-nas-android Android源码
#
1.3 构建版本克隆存储库
bash $ git clone https://xxxx/zhiting-nas-android.git
在Android Studio打开拉取的项目,在项目加载完相关依赖并没有报错的情况下就可以运行项目了。
#
2. 开发指南#
2.1 组织架构项目组织架构如下图
#
2.2 网络架构在讲网络框架之前,我们先来说说开发模式。智汀云盘使用的开发模式是MVP(Model-View-Presenter)模式:
- Model提供数据
- View负责显示
- Controller/Presenter负责逻辑的处理
MVP是从MVC演变而来的,但它与MVC有着一个重大的区别:
- 在MVP中View并不直接使用Model,它们是通过Presenter(MVC中的Controller)来进行通信的,所有的交互都发生再Presenter内容
- 而MVC中View读取数据不是通过Controller而是直接从Model中读取。
MVP有以下三个优势:
- View与Model完全隔离;
- Presenter与View的具体实现技术无关;
- 可以进行View的模拟测试。
接着我们继续来看网络请求框架,智汀云盘使用的网络请求框架是:Retrofit + RxJava + OkHttp。
- Retrofit是Square公司基于
OkHttp
封装的Android网络请求框架; OkHttp
是一个网络请求库,也是Square开源的;RxJava
在GitHub
上的描述是:a library for composing asynchronous and event-based programs by using observable sequences(使用可观察序列编写异步和基于事件的程序的库),这使得我们切换线程的操作变得更加简单。
Retrofit + RxJava + OkHttp是当下Android用Java语言开发最流行的网络请求方式。
下面是网络框架集成的步骤:
1) 导入相关的库
api rootProject.ext.dependencies["gson"]api rootProject.ext.dependencies["okhttp"]api rootProject.ext.dependencies["loggingInterceptor"]api rootProject.ext.dependencies["retrofit"]api rootProject.ext.dependencies["converterGson"]api rootProject.ext.dependencies["rxjavaAdapter"]api rootProject.ext.dependencies["rxandroid"]api rootProject.ext.dependencies["rxjava"]
2) 创建Retrofit实例<
public class RetrofitManager { ... private RetrofitManager(String baserUrl) { retrofit = new Retrofit.Builder() .baseUrl(baserUrl) .client(getOkHttpClient()) .addConverterFactory(GsonConverterFactory.create(GsonConverter.getGson())) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) .build(); } ...}
3) 添加OkHttp配置
public class RetrofitManager { ... private OkHttpClient getOkHttpClient() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.addInterceptor(getHttpLoginInterceptor()) .connectTimeout(HttpConfig.connectTimeout, TimeUnit.SECONDS) .readTimeout(HttpConfig.readTimeOut, TimeUnit.SECONDS) .writeTimeout(HttpConfig.writeTimeOut, TimeUnit.SECONDS) .retryOnConnectionFailure(true); return builder.build(); } ...}
4)创建一个接口
public interface ApiService { ...}
注:ApiService的方法必须是Observable<BaseResponseEntity<T>>
类型
5) 用Retrofit创建接口实例ApiService
public class RetrofitManager { ... public <T> T create(Class<T> service) { return retrofit.create(service); } ...}
6) 配合RxJava使用并封装
public abstract class BasePresenter<M extends IModel, V extends IView> implements IPresenter<V> { ... /** * 网络请求 * * @param observable * @param callback * @param <T> */ public <T> void executeObservable(Observable<BaseResponseEntity<T>> observable, RequestDataCallback<T> callback) { observable.subscribeOn(Schedulers.io()) .unsubscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<BaseResponseEntity<T>>() { @Override public void onSubscribe(@NonNull Disposable d) { mModel.addDisposable(d); if (mView != null && callback != null && callback.isLoading()) mView.showLoading(); }
@Override public void onNext(@NonNull BaseResponseEntity<T> response) { if (callback != null) { if (response.getStatus() == 0) { // 成功 callback.onSuccess(response.getData()); } else { // 失败 if (mView != null) { if (callback != null && callback.isLoading()) mView.hideLoading(); if (response.getStatus() != ErrorConstant.INVALID_AUTH) // 如果不是无效的授权才需要土司提示 showError(response.getReason()); // 提示错误信息 mView.showError(response.getStatus(), response.getReason()); // 提示错误信息 } callback.onFailed(response.getStatus(), response.getReason()); } } }
@Override public void onError(@NonNull Throwable e) { e.printStackTrace(); String error = ""; if (e instanceof ConnectException || e instanceof UnknownHostException) { error = "网络异常,请检查网络"; } else if (e instanceof TimeoutException || e instanceof SocketTimeoutException) { error = "网络不畅,请稍后再试!"; } else if (e instanceof JsonSyntaxException) { error = "数据解析异常"; } else { error = "服务端错误"; } if (mView != null) { if (callback != null && callback.isLoading()) mView.hideLoading(); showError(error); } }
@Override public void onComplete() { if (mView != null && callback != null && callback.isLoading()) mView.hideLoading(); } }); } ...}
附:
- Retrofit: https://github.com/square/retrofit
- RxJava:https://github.com/ReactiveX/RxJava
- OkHttp:https://github.com/square/okhttp
#
2.3 授权登录授权登录功能的实现主要是通过智汀云盘App发起一个携带要获取权限参数的意图去启动智汀App,智汀App根据权限参数获取相应的授权信息(包括家庭信息、用户信息和登录凭证)并通过广播发送消息,智汀云盘接收到广播消息之后进行保存授权信息和登录操作,当然进入智汀App之后的那个家庭,必须是有绑定SA的家庭。
1) 智汀云盘主要代码实现:
/** * 登录界面 */public class LoginActivity extends BaseMVPDBActivity<ActivityLoginBinding, LoginContract.View, LoginPresenter> implements LoginContract.View {
private String mUri = "zt://com.yctc.zhiting/sign?type=1&user_package_name=com.yctc.zhiting"; // 启动智汀App地址 private MyBroadcastReceiver mReceiver; // 授权登录广播
...
@Override protected void initData() { super.initData(); //注册广播接受者,接收授权成功返回广播信息 mReceiver = new MyBroadcastReceiver(); IntentFilter intentFilter = new IntentFilter(); //zt.com.yctc.zhiting.sign 自行定义action 即可 intentFilter.addAction("zt.com.yctc.zhiting.sign"); registerReceiver(mReceiver, intentFilter); if (!TextUtils.isEmpty(SpUtil.getString("loginInfo"))){ toMain(SpUtil.getString("loginInfo"), false); } } ... /** * 点击事件 */ public class OnClickHandler{ public void onClick(View view){ int viewId = view.getId(); if (viewId == R.id.tvLogin){ // 登录 if (AppUtil.isMobile_spExist(CDApplication.getContext(), "com.yctc.zhiting")) { // 已安装智汀,执行授权登录 LogUtil.d("=================登录============"); Intent intent = new Intent(); intent.setData(Uri.parse(mUri));//参数拼接在URI后面 type=1是授权页面,user_package_name使用者包名,后续参数可自行添加 intent.putExtra("needPermissions", "user,area");//这里Intent也可传递参数,但是一般情况下都会放到上面的URL中进行传递 intent.putExtra("appName", UiUtil.getString(R.string.to_third_party_name)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }else { // 未安装智汀,提示用户安装 ToastUtil.show(getResources().getString(R.string.main_please_install_zhiting)); } } } }
/** * 接收授权登录广播 */ private class MyBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // 授权成功之后返回的信息 String backInfo = intent.getStringExtra("backInfo"); SpUtil.put("loginInfo", backInfo); toMain(backInfo, true); } }
/** * 去主界面 * @param json */ private void toMain(String json, boolean delay){ Constant.authBackBean = GsonConverter.getGson().fromJson(json, AuthBackBean.class); // 授权信息 if (Constant.authBackBean!=null) { HomeCompanyBean homeCompanyBean = Constant.authBackBean.getHomeCompanyBean(); // 家庭 if (homeCompanyBean!=null) { String url = homeCompanyBean.getSa_lan_address(); if (!TextUtils.isEmpty(url)) { HttpConfig.baseUrl = homeCompanyBean.getSa_lan_address(); HttpConfig.baseTestUrl = homeCompanyBean.getSa_lan_address(); HttpConfig.uploadFileUrl = HttpConfig.baseTestUrl+HttpConfig.uploadFileUrl; HttpConfig.downLoadFileUrl = HttpConfig.baseTestUrl+HttpConfig.downLoadFileUrl; HttpConfig.downLoadFolderUrl1 = HttpConfig.baseTestUrl+HttpConfig.downLoadFolderUrl1; HttpConfig.downLoadFolderUrl2 = HttpConfig.baseTestUrl+HttpConfig.downLoadFolderUrl2; } Constant.HOME_NAME = homeCompanyBean.getName(); } Constant.cookies = Constant.authBackBean.getCookies(); Constant.scope_token = Constant.authBackBean.getStBean().getToken(); // scopeToken Constant.USER_ID = Constant.authBackBean.getUserId(); // 用户 id Constant.userName = Constant.authBackBean.getUserName(); // 用户名称 if (delay) { // 是否需要延时, 主要解决oppo没有跳转到主界面,直接到桌面的问题 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } switchToActivity(MainActivity.class); finish(); } }}
2) 智汀主要代码实现:
/** * 启动页 */ public class SplashActivity extends BaseActivity { ... /** * 1 授权登录 */ private String type; /** * 第三方应用需要的权限 */ private String needPermissions; /** * 第三方app的名称 */ private String appName; ... @Override protected void initIntent(Intent intent) { super.initIntent(intent); Uri uri = intent.getData(); if (uri != null) { type = uri.getQueryParameter("type"); } needPermissions = intent.getStringExtra("needPermissions"); appName = intent.getStringExtra("appName"); if (type!=null && type.equals("1") && CurrentHome!=null){ // 如果是授权过来且当前家庭不为空,直接调整授权界面 Bundle bundle = new Bundle(); bundle.putString(IntentConstant.NEED_PERMISSION, needPermissions); bundle.putString(IntentConstant.APP_NAME, appName); // 如果type不为空且为1的情况下到授权界面,否则直接到主界面 switchToActivity(AuthorizeActivity.class, bundle); finish(); }else { // 否则,正常流程 checkPermissionTask(); } } ... /** * 去到主界面/授权界面 */ private void toMain() { UiUtil.starThread(() -> { List<HomeCompanyBean> homeList = dbManager.queryHomeCompanyList(); if (CollectionUtil.isNotEmpty(homeList)){ CurrentHome = homeList.get(0); UiUtil.runInMainThread(() -> { if (wifiInfo != null) { for (HomeCompanyBean home : homeList) { if (HomeFragment.homeLocalId > 0){ // 之前打开过,没退出,按Home键之前的那个家庭 if (home.getLocalId() == HomeFragment.homeLocalId){ CurrentHome = home; break; } } if (home.getMac_address() != null && home.getMac_address(). equalsIgnoreCase(wifiInfo.getBSSID()) && home.isIs_bind_sa()) { // 当前sa环境 CurrentHome = home; break; } } } UiUtil.postDelayed(() -> { Bundle bundle = new Bundle(); bundle.putString(IntentConstant.TYPE, type); bundle.putString(IntentConstant.NEED_PERMISSION, needPermissions); bundle.putString(IntentConstant.APP_NAME, appName); // 如果type不为空且为1的情况下到授权界面,否则直接到主界面 switchToActivity(type!=null && type.equals("1") ? AuthorizeActivity.class : MainActivity.class, bundle); finish(); }, 1500); }); } }); } ...
/*** 授权界面*/public class AuthorizeActivity extends MVPBaseActivity<AuthorizeContract.View, AuthorizePresenter> implements AuthorizeContract.View {
@BindView(R.id.tvName) TextView tvName; @BindView(R.id.tvJoin) TextView tvJoin; @BindView(R.id.rvScopes) RecyclerView rvScopes; @BindView(R.id.tvTips) TextView tvTips;
/** * 第三方应用需要的权限 */ private String needPermissions; /** * 第三方app的名称 */ private String appName;
private ScopesAdapter scopesAdapter;
private Handler mainThreadHandler; private DBManager dbManager; private WeakReference<Context> mContext; private int userId; private String userName; // 用户名称 private String[] permissions; // 同意授权的信息 ...
@Override protected void initData() { super.initData(); mContext = new WeakReference<>(getActivity()); dbManager = DBManager.getInstance(mContext.get()); mainThreadHandler = new Handler(Looper.getMainLooper()); getUserInfo();
permissions = needPermissions.split(",");
// 同意授权的信息数据 List<ScopesBean.ScopeBean> data = new ArrayList<>(); if (permissions.length>0){ for (int i=0; i<permissions.length; i++){ if (permissions[i].equals(Constant.USER)){ data.add(new ScopesBean.ScopeBean(Constant.USER, getResources().getString(R.string.main_get_login_status))); }else if (permissions[i].equals(Constant.AREA)){ data.add(new ScopesBean.ScopeBean(Constant.AREA, getResources().getString(R.string.main_get_family_info))); } } } scopesAdapter.setNewData(data); }
...
@OnClick(R.id.tvConfirm) void onClickConfirm(){ if (TextUtils.isEmpty(Constant.CurrentHome.getSa_user_token())){ // 当前家庭没有绑定SA ToastUtil.show(UiUtil.getString(R.string.main_home_is_not_bind_with_sa)); }else { // 当前家庭绑了SA // 获取token的请求参数 List<String> scopes = new ArrayList<>(); for (String permission : permissions){ scopes.add(permission); } ScopeTokenRequest scopeTokenRequest = new ScopeTokenRequest(scopes); // 获取token接口 mPresenter.getScopeToken(scopeTokenRequest.toString()); } }
/** * 获取 SCOPE 列表成功 * @param scopesBean */ @Override public void getScopesSuccess(ScopesBean scopesBean) { if (scopesBean!=null){ if (CollectionUtil.isNotEmpty(scopesBean.getScopes())){ List<ScopesBean.ScopeBean> data = new ArrayList<>(); if (!TextUtils.isEmpty(needPermissions)) {
for (ScopesBean.ScopeBean scopeBean : scopesBean.getScopes()) { for (String permission : permissions){ if (scopeBean.getName().equals(permission)){ data.add(scopeBean); } } } scopesAdapter.setNewData(data); } } } }
@Override public void getScopesFail(int errorCode, String msg) { ToastUtil.show(msg); }
/** * 获取 SCOPE Token成功 * @param scopeTokenBean */ @Override public void getScopeTokenSuccess(ScopeTokenBean scopeTokenBean) { if (scopeTokenBean!=null){ ScopeTokenBean.STBean stBean = scopeTokenBean.getScope_token(); if (stBean!=null){ Intent intent = new Intent(); AuthBackBean authBackBean = new AuthBackBean(Constant.CurrentHome.getUser_id(), userName, Constant.CurrentHome, stBean); if (UserUtils.isLogin()){ authBackBean.setCookies(PersistentCookieStore.getInstance().get(HttpUrl.parse(HttpUrlConfig.getLogin()))); }else { authBackBean.setCookies(new ArrayList<>()); } intent.setAction("zt.com.yctc.zhiting.sign"); intent.putExtra("backInfo", authBackBean.toString()); sendBroadcast(intent); LibLoader.finishAllActivity(); }else { ToastUtil.show(UiUtil.getString(R.string.main_login_fail)); } }else { ToastUtil.show(UiUtil.getString(R.string.main_login_fail)); } }
@Override public void getScopeTokenFail(int errorCode, String msg) { ToastUtil.show(msg); }
...}
3) 还有最重要的一点是需要在智汀的清单文件为其他App/浏览器提供入口:
<activity android:name=".activity.SplashActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <data android:host="com.yctc.zhiting" android:path="/sign" android:scheme="zt" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
4) 授权信息类:
/** * 授权信息类 */public class AuthBackBean extends Request {
private int userId; // 用户id private String userName; // 用户昵称 private HomeCompanyBean homeCompanyBean; // 家庭信息 private ScopeTokenBean.STBean stBean; // 授权token和过期时间 private List<Cookie> cookies; // 登录SC时的cookie
...}
public class HomeCompanyBean {
private int id;//云端家庭id private int roomAreaCount;//房间个数 private int location_count; // 房间数量 private int role_count; // 角色数量 private int user_count; // 成员数量 @SerializedName("sa_user_id") private int user_id; // sa用户id private boolean is_bind_sa; //是否绑定sa private boolean selected; // 标识是否被选中 private String name;//家庭名称 private String sa_lan_address; // sa地址 private String sa_user_token; // sa token private String ss_id;//wifi id private String mac_address;//wifi地址 ...}
public class ScopeTokenBean {
/** * scope_token : {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgyNDc2NzgsInNhX2lkIjoidGVzdC1zYS10ZXN0Iiwic2NvcGVzIjoidXNlcixhcmVhIiwidWlkIjozfQ.HM_pLMTYw_Yzz4kWQIERWU9FnmP6SM_ejV1M0GMXbAc","expires_in":2592000} */
private STBean scope_token;
...
public static class STBean { /** * token : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgyNDc2NzgsInNhX2lkIjoidGVzdC1zYS10ZXN0Iiwic2NvcGVzIjoidXNlcixhcmVhIiwidWlkIjozfQ.HM_pLMTYw_Yzz4kWQIERWU9FnmP6SM_ejV1M0GMXbAc * expires_in : 2592000 */
private String token; private int expires_in; // 有效期,单位为秒 ... }}
#
2.4 文件列表及权限说明#
2.4.1 文件类型说明文件夹分为私人文件和共享文件夹,私人文件夹只有在文件夹管理创建文件夹时,选择可访问者的那个人可以访问;
共享文件夹只有在文件夹管理创建文件夹时,选择可访问者的那些人可以访问。
私人文件夹又可分为加密文件夹,未加密文件夹:
- 如果是加密文件夹的话,访问时需要输入正确密码才能访问;
- 未加密文件夹可以直接访问。
共享文件夹又分为在创建文件夹时的共享文件夹和别人共享给我的文件夹
- 如果是在主界面的情况下,创建文件夹时的共享文件夹是不能做任何操作,只能查看文件夹详情
- 别人共享给我的文件夹,是可以进行共享、下载、复制、重新命名操作
注:上述操作,必须要有相应的操作权限,包括查看文件夹详情。下面是对文件夹类型的进一步说明。
在说明之前,我们可以先看看文件的实体类(有说明字段定义的字段之外,其它字段都是接口返回):
/** * 文件实体类 */public class FileBean implements Serializable {
/** * name : config.yaml * size : 1474 * mod_time : 1622010079 * type : 1 * path : /1/demo-plugin/config.yaml * from_user : */
private String name; // 名称 private long size; // 大小(b) private long mod_time; // 修改时间 private int type;//0是文件夹 1文件 private String path; // 路径 private String from_user; // 共享者名称 private boolean selected; // 是否选中该条数据,自己定义的字段
private int is_encrypt; // 是否加密文件夹;文件夹有效:1加密,0不需要加密 private int read; // 是否可读:1/0 private int write; //是否可写:1/0 private int deleted; // 是否可删:1/0
private boolean enabled = true; // 是否可操作,自己定义的字段 ...}
如果是私人文件,我们访问的接口是:GET: /plugin/wangpan/resources/:path(path参数为空),在我们获取到私人文件夹之后,我们可以根据
is_encrypt
(1 加密,0 未加密)这个字段去判断改文件夹是否加密如果是共享文件夹,我们访问的接口是GET: /plugin/wangpan/shares,在我们获取到共享文件夹之后,我们可以根据
from_user
这个字段判断是在文件夹创建的共享文件夹还是别人共享给我的文件夹,如果from_user
未空的话,则是在文件夹管理创建的文件夹,否则的话是别人共享给我的文件夹,而from_user
的值就是共享给我的用户。文件夹详情,不管是私人文件夹还是共享文件夹,我们访问的接口都是GET: /plugin/wangpan/resources/:path(path:一级目录为空)
1) 主界面的文件获取列表代码实现
public class HomePresenter extends BasePresenter<HomeModel, HomeContract.View> implements HomeContract.Presenter { ... /** * 文件 * @param scopeToken 凭证 * @param path * @param map */ @Override public void getFiles(String scopeToken, String path, Map<String, String> map, boolean showLoading) { executeObservable(mModel.getFiles(scopeToken, path, map), new RequestDataCallback<FileListBean>(showLoading) { @Override public void onSuccess(FileListBean response) { super.onSuccess(response); mView.getFilesSuccess(response); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); } }); } ...}
2) 主界面的共享文件夹获取列表数据代码实现
public class SharePresenter extends BasePresenter<ShareModel, ShareContract.View> implements ShareContract.Presenter {
@Override public ShareModel createModel() { return new ShareModel(); }
/** * 共享文件夹列表 * @param scopeToken 凭证 * @param showLoading 是否显示加载弹窗 */ @Override public void getShareFolders(String scopeToken, boolean showLoading) { executeObservable(mModel.getShareFolders(scopeToken), new RequestDataCallback<FileListBean>(showLoading) { @Override public void onSuccess(FileListBean response) { super.onSuccess(response); mView.getFilesSuccess(response); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); } }); }}
3) 获取文件夹详情数据代码实现
public class FileDetailPresenter extends BasePresenter<FileDetailModel, FileDetailContract.View> implements FileDetailContract.Presenter { ... /** * 文件列表 有密码参数 * @param scopeToken 凭证 * @param pwd 密码,加密文件的话传对应的密码,没有则传空串就行 * @param path 路径 * @param map 分页参数 * @param showLoading 是否显示加载弹窗 */ @Override public void getFiles(String scopeToken, String pwd, String path, Map<String, String> map, boolean showLoading) { executeObservable(mModel.getFiles(scopeToken, pwd, path, map), new RequestDataCallback<FileListBean>(showLoading) { @Override public void onSuccess(FileListBean response) { super.onSuccess(response); if (mView!=null) mView.getFilesSuccess(response); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null) mView.getFilesFail(errorCode, errorMessage); } }); } ...}
#
2.4.2 权限说明1) 这里的权限主要是指对文件/文件夹的读、写和删的操作权限。在详细说明之前,我们先看下面这个表格。
权限 | 备注 |
---|---|
读 | 1、查看该文件夹及其下面所有文件/文件夹的权限; 2、有权限才展示对应文件夹的入口; |
写 | 1、包括新建文件夹、上传、重命名、共享、下载的权限; 2、有权限才展示对应操作的入口; |
删 | 1、包括移动、删除的权限; 2、有权限才展示对应的操作的入口; 3、有移动、复制的权限,不代表能成功移动复制,需要看是否有移入文件夹的【写】权限,有才能操作。 |
注:复制功能不受权限控制
从表中我们可以看出读权限对应文件夹/文件的查看操作:
- 写权限对应新建文件、上传、重命名、共享和下载的操作
- 删权限对应移动和删除的操作。 我们在移动和复制的时候不一定能够移动和复制成功,因为我们在移动到目标文件时是否成功,还得看目标文件夹是否又写权限。 这些权限的设置是从文件夹管理创建文件夹时或共享文件时设置给对应访问者设置。
注:共享只能共享文件夹,不能共享文件
2) 接下来是对共享、移动和复制操作的进一步说明,为了更加直观的阐述,我们直接用表格来表达。
- 根据文件类型来决定是否可共享
文件夹的根目录文件类型 | 是否可共享 |
---|---|
私人文件夹(未加密) | 可共享 |
私人文件夹(已加密) | 不可共享 |
共享文件夹 | 可共享 |
别人共享给我的文件夹 | 可共享 |
- 根据文件/文件夹类型来决定可移动到的目标路径
文件/文件夹的根目录文件类型操作 | 可移动到位置 |
---|---|
私人文件夹(未加密) | 任何位置 |
私人文件夹(已加密) | 该已加密文件/文件夹所属根目录内 |
共享文件夹 | 任何位置 |
别人共享给我的文件夹 | 该共享文件夹所属根目录内 |
- 根据文件/文件夹的类型来决定可复制的目标路径
文件/文件夹的根目录文件类型 | 可复制到位置 |
---|---|
私人文件夹(未加密) | 任何位置 |
私人文件夹(已加密) | 该已加密文件/文件夹所属根目录内 |
共享文件夹 | 任何位置 |
别人共享给我的文件夹 | 任何位置 |
3) 下面部分是部分代码实现
/** * 文件详情 */public class FileDetailActivity extends BaseMVPDBActivity<ActivityFileDetailBinding, FileDetailContract.View, FileDetailPresenter> implements FileDetailContract.View { ... /** * 初始化文件列表 */ private void initRv() { fileDetailAdapter = new FileDetailAdapter(0); ... fileDetailAdapter.setOnItemChildClickListener((adapter, view, position) -> { if (view.getId() == R.id.ivSelected) { FileBean fileBean = fileDetailAdapter.getItem(position); fileBean.setSelected(!fileBean.isSelected()); fileDetailAdapter.notifyItemChanged(position); List<FileBean> selectedData = fileDetailAdapter.getSelectedData(); if (CollectionUtil.isNotEmpty(selectedData)) { if (isEncrypt()) { // 如果是加密文件,不能共享 mOperateData.get(0).setEnabled(false); } else { mOperateData.get(0).setEnabled(fileDetailAdapter.isOnlyFolder() && FileUtil.hasWritePermission(selectedData)); // 是文件夹且有写权限才能共享 } mOperateData.get(1).setEnabled(FileUtil.hasWritePermission(selectedData)); // 有写权限才能下载 mOperateData.get(2).setEnabled(FileUtil.hasDelPermission(selectedData)); // 有写权限才能移动 mOperateData.get(3).setEnabled(true); // 复制不受权限影响 mOperateData.get(4).setEnabled(selectedData.size() == 1 && FileUtil.hasWritePermission(selectedData)); // 有写权限才能重命名 mOperateData.get(5).setEnabled(FileUtil.hasDelPermission(selectedData)); // 有删权限才能删除 fileOperateAdapter.notifyDataSetChanged(); } else { setAllEnabled(); } setOperateVisible(selectedData.size() > 0); // 设置底部文件操作是否可见 binding.rrv.setRefreshAndLoadMore(selectedData.size() <= 0); // 设置列表是否可刷新和加载更多 } }); } ... /** * 文件的操作 */ private void initRvOperateFile() { binding.rvOperate.setLayoutManager(new GridLayoutManager(this, 6)); fileOperateAdapter = new FileOperateAdapter(); binding.rvOperate.setAdapter(fileOperateAdapter); mOperateData.add(new FileOperateBean(R.drawable.icon_share, UiUtil.getString(R.string.home_share), true)); mOperateData.add(new FileOperateBean(R.drawable.icon_download, UiUtil.getString(R.string.home_download), true)); mOperateData.add(new FileOperateBean(R.drawable.icon_move, UiUtil.getString(R.string.home_move), true)); mOperateData.add(new FileOperateBean(R.drawable.icon_copy, UiUtil.getString(R.string.home_copy), true)); mOperateData.add(new FileOperateBean(R.drawable.icon_rename, UiUtil.getString(R.string.home_rename), true)); mOperateData.add(new FileOperateBean(R.drawable.icon_remove, UiUtil.getString(R.string.home_remove), true)); fileOperateAdapter.setNewData(mOperateData); fileOperateAdapter.setOnItemClickListener((adapter, view, position) -> { FileOperateBean fileOperateBean = fileOperateAdapter.getItem(position); if (fileOperateBean.isEnabled()) { switch (position) { case 0: // 共享 Bundle bundle = new Bundle(); bundle.putSerializable("folder", (Serializable) fileDetailAdapter.getSelectedData()); bundle.putBoolean("originalWrite", FileUtil.hasWritePermission(fileDetailAdapter.getSelectedData())); bundle.putBoolean("originalDel", FileUtil.hasDelPermission(fileDetailAdapter.getSelectedData())); switchToActivity(ShareFolderActivity.class, bundle); break;
case 1: // 下载 downloadFiles(fileDetailAdapter.getSelectedData()); break;
case 2: // 移动 toMoveCopyActivity(0, fileDetailAdapter.getSelectedPath()); break;
case 3: // 复制到 toMoveCopyActivity(1, fileDetailAdapter.getSelectedPath()); break;
case 4: // 重命名 FileBean fileBean = fileDetailAdapter.getSingleSelectedData(); int drawableRes = R.drawable.icon_file_big; if (fileBean.getType() == 0) { drawableRes = R.drawable.icon_file_big; } else { /** * 1. word * 2. excel * 3. ppt * 4. 压缩包 * 5. 图片 * 6. 音频 * 7. 视频 * 8. 文本 * */ int fileType = FileTypeUtil.fileType(fileBean.getName()); drawableRes = FileTypeUtil.getFileBigLogo(fileType); } showCreateFileDialog(1, drawableRes, fileBean); break;
case 5: // 删除 showRemoveFileTips(fileDetailAdapter.getSelectedPath()); break; } } }); } ...}
/** * 文件夹详情列表适配器 */public class FileOperateAdapter extends BaseQuickAdapter<FileOperateBean, BaseViewHolder> { ... @Override protected void convert(BaseViewHolder helper, FileOperateBean item) { LinearLayout llParent = helper.getView(R.id.llParent); llParent.setAlpha(item.isEnabled() ? 1 : 0.5f); ImageView ivLogo = helper.getView(R.id.ivLogo); ivLogo.setImageResource(item.getDrawable()); TextView tvName = helper.getView(R.id.tvName); tvName.setText(item.getName()); }}
#
2.5 文件夹解密逻辑#
2.5.1 文件夹解密说明只有加密的文件夹才需要解密,同一用户在查看同一加密文件夹或者是里面的文件/文件夹,每次成功校验密码后,再次查看同一加密文件夹或里面的文件/文件夹,不用输入密码,72个小时后查看才需要输入密码;(除非有修改密码,如有修改密码,则需要重新输入,修改后第一次成功验证密码后再开始计算72小时)。
注:校验是否72小时是在客户端实现
#
2.5.2 文件夹解密过程1) 解密过程说明
我们在访问加密的文件时:
- 先去根据scopeToken和path去本地数据库(sqlite)查找该文件先前是否有保存解密密码
- 如果存在的话,则会校验该条数据是否超过72小时,在没有超过72小时的情况下就拿这条数据的密码访问接口Get:/api/plugin/wangpan/folders/:path 校验密码是否正确:
- 正确的话进入文件夹详情,并更新该文件夹本地保存的密码和修改时间;
- 错误需要重新输入密码并访问接口校验该密码是否正确;
- 校验数据是超过72小时的情况下,需要输入密码并访问接口校验该密码是否正确:
- 在正确的情况下更新该文件夹本地保存的密码和修改时间,并插入该文件的密码缓存信息到数据库;
- 否则提示用户密码错误,重新输入;
- 如果存在的话,则会校验该条数据是否超过72小时,在没有超过72小时的情况下就拿这条数据的密码访问接口Get:/api/plugin/wangpan/folders/:path 校验密码是否正确:
2) 主要代码实现
/** * 文件 */public class HomeFragment extends BaseMVPDBFragment<FragmentHomeBinding, HomeContract.View, HomePresenter> implements HomeContract.View { ... /** * 初始化列表 */ private void initRv() { homeFileAdapter = new HomeFileAdapter(1, true); binding.rrv.setAdapter(homeFileAdapter) .setOnRefreshAndLoadMoreListener(refreshLoadMoreListener); homeFileAdapter.setOnItemClickListener((adapter, view, position) -> { mFileBean = homeFileAdapter.getItem(position); if (mFileBean.getType() == 0 && homeFileAdapter.getSelectedSize() <= 0) { // 如果是文件夹,且没有处于编辑状态 if (mFileBean.getRead() == 1) { // 有读权限 if (mFileBean.getIs_encrypt() == 1) { // 加密文件 mPresenter.getFolderPwdByScopeTokenAndPath(Constant.scope_token, mFileBean.getPath()); } else { // 非加密文件 filePwd = ""; toFolderDetail(false); } } else { // 没有读权限 ToastUtil.show(UiUtil.getString(R.string.mine_without_read_permission)); } } });
... } ... /** * 解密文件成功 */ @Override public void decryptPwdSuccess() { if (inputPwdDialog != null && inputPwdDialog.isShowing()) { inputPwdDialog.dismiss(); } if (mFolderPwd == null) { mFolderPwd = new FolderPassword(Constant.USER_ID, mFileBean.getPath(), filePwd, Constant.scope_token, TimeUtil.getCurrentTimeMillis()); mPresenter.insertFolderPwd(mFolderPwd); } else { updateFolderPwd(); } toFolderDetail(true); }
/** * 更新文件夹密码 */ private void updateFolderPwd(){ mFolderPwd.setPassword(filePwd); mFolderPwd.setModifyTime(TimeUtil.getCurrentTimeMillis()); mPresenter.updateFolderPwd(mFolderPwd); }
/** * 解密文件失败 * * @param errorCode * @param msg */ @Override public void decryptPwdFail(int errorCode, String msg) { if (errorCode == ErrorConstant.PWD_ERROR) { // 文件夹密码错误 if (inputPwdDialog != null && !inputPwdDialog.isShowing()) { filePwd = ""; updateFolderPwd(); inputPwdDialog.show(this); } } } /** * 获取密码成功 * * @param folderPassword */ @Override public void getFolderPwdByScopeTokenAndPathSuccess(FolderPassword folderPassword) { LogUtil.e("查询文件夹密码成功"); if (folderPassword != null) { filePwd = folderPassword.getPassword(); long modifyTime = folderPassword.getModifyTime(); long distinct = TimeUtil.getCurrentTimeMillis() - modifyTime; mFolderPwd = folderPassword; if (TimeUtil.over72hour(distinct) || TextUtils.isEmpty(filePwd)) { // 超过72小时 showInputPwdDialog(); } else { checkFilePwd(); }
} else { showInputPwdDialog(); } }
/** * 获取密码失败 */ @Override public void getFolderPwdByScopeTokenAndPathFail() { LogUtil.e("查询文件夹密码失败"); showInputPwdDialog(); }}
/** * 文件的presenter层 */public class HomePresenter extends BasePresenter<HomeModel, HomeContract.View> implements HomeContract.Presenter { ... /** * 解密文件夹 * @param scopeToken * @param path * @param checkPwdRequest */ @Override public void decryptFile(String scopeToken, String path, CheckPwdRequest checkPwdRequest) { executeObservable(mModel.decryptFile(scopeToken, path, checkPwdRequest), new RequestDataCallback<Object>(false) { @Override public void onSuccess(Object response) { super.onSuccess(response); if (mView!=null){ mView.decryptPwdSuccess(); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null){ mView.decryptPwdFail(errorCode, errorMessage); } } }); }
/** * 获取本地保存的文件夹密码 * @param scopeToken * @param path */ @Override public void getFolderPwdByScopeTokenAndPath(String scopeToken, String path) { executeDBObservable(mModel.getFolderPwdByScopeTokenAndPath(scopeToken, path), new RequestDataCallback<FolderPassword>() { @Override public void onSuccess(FolderPassword response) { super.onSuccess(response); if (mView!=null){ mView.getFolderPwdByScopeTokenAndPathSuccess(response); } }
@Override public void onFailed() { super.onFailed(); if (mView!=null){ mView.getFolderPwdByScopeTokenAndPathFail(); } } }); }
/** * 插入文件夹密码 * @param folderPassword */ @Override public void insertFolderPwd(FolderPassword folderPassword) { executeDBObservable(mModel.insertFolderPwd(folderPassword), new RequestDataCallback<Boolean>() { @Override public void onSuccess(Boolean response) { super.onSuccess(response); if (mView!=null){ if (response) { mView.insertFolderPwdSuccess(true); }else { mView.insertFolderFail(); } }
}
@Override public void onFailed() { super.onFailed(); if (mView!=null){ mView.insertFolderFail(); } } }); ... /** * 校验文件夹密码是否正确 */ private void checkFilePwd() { if (mFileBean != null) { if (TextUtils.isEmpty(filePwd)){ // 如果密码为空,则输入 inputPwdDialog.show(this); }else { // 密码不为空,校验密码 CheckPwdRequest checkPwdRequest = new CheckPwdRequest(filePwd); mPresenter.decryptFile(Constant.scope_token, mFileBean.getPath(), checkPwdRequest); } } } }
/** * 修改文件夹密码 * @param folderPassword */ @Override public void updateFolderPwd(FolderPassword folderPassword) { executeDBObservable(mModel.updateFolderPwd(folderPassword), new RequestDataCallback<Boolean>() { @Override public void onSuccess(Boolean response) { super.onSuccess(response); if (mView!=null) { if (response) { mView.updateFolderPwdSuccess(); } else { mView.updateFolderPwdFail(); } } }
@Override public void onFailed() { super.onFailed(); if (mView!=null){ mView.updateFolderPwdFail(); } } }); }}
3) 数据库
智汀云盘存储文件夹密码相关信息的方式是使用Android本地数据库SqLite,使用的数据库框架是Greendao(greendao的使用,请参照官方文档,这里不再赘述greendao的使用),在查找文件夹密码时是通过文件夹的path和凭证查找的。下面是文件夹密码相关信息表的设计。
表名 | 描述 |
---|---|
id | 主键id |
userId | 用户id |
path | 文件夹路径 |
password | 文件夹密码 |
scopeToken | 凭证 |
modifyTime | 创建/修改时间 |
以下是文件夹密码相关信息表的model类:
@Entitypublic class FolderPassword {
@Id(autoincrement = true) private Long id; // 主键id private int userId; // 用户id private String path; // 文件夹路径 private String password; // 文件夹密码 private String scopeToken; // scopeToken private Long modifyTime; // 创建/修改时间 ... }
#
2.6 文件夹共享#
2.6.1.1 说明文件夹共享分为两种情况:
- 一种是在创建文件夹时选择文件夹类型为共享文件夹,另外别人共享给我的文件夹。
- 第一种情况是没有共享者的,根目录不能共享,相当于可以有多个访问成员的非共享文件夹;
第二种情况是有共享者的,在共享文件列表可以再共享给别人。
以第一种方式共享文件夹时,可以给访问者设置读、写和删权限;
以第二种方式共享文件夹,读、写和删的权限受制于共享者自己是否有对应的权限。
在设置权限时,如果用户去掉了读权限,那么写和删权限也会随之去掉;如果用户选择了写或删权限,那么会自动把读权限也选上。
#
2.6.1.2 主要代码实现/** * 共享文件夹 */public class ShareFolderActivity extends BaseMVPDBActivity<ActivityShareFolderBinding, ShareFolderContract.View, ShareFolderPresenter> implements ShareFolderContract.View { ... /** * 编辑成员弹窗 */ private void initOperatePermissionDialog(){ operatePermissionDialog = OperatePermissionDialog.getInstance(); operatePermissionDialog.setConfirmListener(new OperatePermissionDialog.OnConfirmListener() { @Override public void onConfirm(int read, int write, int del) { List<String> paths = new ArrayList<>(); for (FileBean fileBean : folders){ // 要共享的文件夹 paths.add(fileBean.getPath()); } List<Integer> to_users = new ArrayList<>(); for (MemberBean.UsersBean usersBean : memberAdapter.getSelectedUsers()){ // 选择的成员 to_users.add(usersBean.getUser_id()); } ShareRequest shareRequest = new ShareRequest(to_users, paths, read, write, del, Constant.userName); mPresenter.share(Constant.scope_token, shareRequest); } }); } ... public class OnClickHandler { public void onClick(View view) { int viewId = view.getId(); if (viewId == R.id.ivBack){ // 返回 ... }else if (viewId == R.id.tvAll){ // 全选 ... }else if (viewId == R.id.tvConfirm){ // 确定 Bundle bundle = new Bundle(); bundle.putInt("read", 1); // 读权限 bundle.putBoolean("originalWrite", originalWrite); // 写权限 bundle.putBoolean("originalDel", originalDel); // 删权限 bundle.putBoolean("checkSaveEnabled", true); List<PermissionUserBean> users = new ArrayList<>(); for (MemberBean.UsersBean usersBean : memberAdapter.getSelectedUsers()) { PermissionUserBean permissionUserBean = new PermissionUserBean(usersBean.getNickname(), ""); users.add(permissionUserBean); } bundle.putSerializable("users", (Serializable) users); operatePermissionDialog.setArguments(bundle); operatePermissionDialog.show(ShareFolderActivity.this); } } }}
/** * 用户操作权限弹窗 */public class OperatePermissionDialog extends CommonBaseDialog {
...
@Override protected void initArgs(Bundle arguments) { read = arguments.getInt("read", 0); write = arguments.getInt("write", 0); del = arguments.getInt("del", 0); originalWrite = arguments.getBoolean("originalWrite"); originalDel = arguments.getBoolean("originalDel"); checkSaveEnabled = arguments.getBoolean("checkSaveEnabled"); users = (List<PermissionUserBean>) arguments.getSerializable("users"); } ...
/** * 初始化列表 */ private void initRv(){ ... operatePermissionAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { @Override public void onItemClick(BaseQuickAdapter adapter, View view, int position) { OperatePermissionBean operatePermissionBean = operatePermissionAdapter.getItem(position); boolean selected = operatePermissionBean.isSelected(); operatePermissionBean.setSelected(!selected); List<OperatePermissionBean> permissionData = operatePermissionAdapter.getData(); switch (position){ case 0: read = selected ? 0 : 1; if (read == 0){ // 如果没有读权限,也就没有写删权限 for (OperatePermissionBean opb : permissionData){ opb.setSelected(false); } write = 0; del = 0; } break;
case 1: write = selected ? 0 : 1; if (write == 1){ // 如果选择了写权限,读权限也要一并选择 permissionData.get(0).setSelected(true); read = 1; } break;
case 2: del = selected ? 0 : 1; if (del == 1){ // 如果选择了删权限,读权限也要一并选择 permissionData.get(0).setSelected(true); read = 1; } break; } operatePermissionAdapter.notifyDataSetChanged(); setSaveStatus(); } }); }
...}
#
2.7 文件/文件夹上传下载:gomobile#
2.7.1 为什么要使用gomobile作为上传下载插件?高性能-协程
golang可以实现跨平台编译,开发出的插件Android/OS平台都可以使用,研发人员能少写代码,运维方便维护部署。golang 源码级别支持协程,实现简单。协程使用,当底层遇到阻塞会自动切换,也就是逻辑层通过同步方式实现异步,充分利用了系统资源,同时避免了异步状态机的反人类异步回调,实现方式更为直观简单。golang 协程是通过多线程维护,所以避免不了锁的使用,但也极大解决了研发效率问题。
生态
有谷歌做背书,生态丰富,可以轻松获得各种高质量轮子。这样用户可以专注于业务逻辑,避免重复造轮子。
部署
部署简单,源码编译成执行文件后,可以直接运行,减少了对其它插件依赖。不像其它语言,执行文件依赖各种插件,各种库,研发机器运行正常,放到生产环境上,死活跑不起来,需要各种安装和版本匹配。自己的
GC
,有defer
功能,函数可以返回多个参数等等。
#
2.7.2 gomobile前期工作需要把gomobile使用到的数据库mobile.db 复制到Android项目下assets文件夹目录下。
把gomobile插件gonet.aar复制到app->libs目录下。
在build.gradle(:app)目录下添加依赖 :implementation (name:'gonet',ext:'aar')。
在android添加如下代码:
android {.......repositories { flatDir { dirs 'libs' }}......}
需要在使用gomobile插件前把在assets文件下的数据库mobile.db复制到手机sd卡
private void copyAssetFileToSD() { String fileName = GonetUtil.dbName; String fileRoot = GonetUtil.dbPath; String filePath = fileRoot + File.separator + fileName; File file = new File(filePath); if ((file.exists() && file.length() == 0) || !file.exists()) { LogUtil.e("fileRoot=path2=" + filePath); BaseFileUtil.copyAssetData(fileName, fileRoot); }}
/** * 拷贝assets目录下的文件 * * @param assetsFileName assets目录下的文件名 * @param targetPath 目标文件夹路径 */public static void copyAssetData(String assetsFileName, String targetPath) { new Thread(() -> { try { AssetManager am = UiUtil.getContext().getAssets(); //得到数据库输入流,就是去读数据库 InputStream is = am.open(assetsFileName); //用输出流写到SDcard上 FileOutputStream fos = new FileOutputStream(new File(targetPath, assetsFileName)); //创建byte数据,1KB写一次 byte[] buffer = new byte[1024]; int count = 0; while ((count = is.read(buffer)) > 0) { fos.write(buffer, 0, count); } //关闭 fos.flush(); close(fos); close(is); } catch (IOException e) { e.printStackTrace(); } }).start();}
#
2.7.3 gomobile上传文件#
2.7.3.1 初始化上传管理管理器/** * 初始化上传管理类 */public static void initUploadManager() { if (mUploadManager == null && !TextUtils.isEmpty(Constant.scope_token)) { UiUtil.starThread(() -> { String headerStr = "{\"scope-token\":\"" + Constant.scope_token + "\"}"; mUploadManager = Gonet.newUploadManager(httpUrl, dbPath, headerStr); mUploadManager.run(headerStr); }); }}
newUploadManager请求参数解析:httpUrl:上传服务器地址dbPath:数据库地址headerStr:请求头文件
#
2.7.3.2 上传文件创建对象public synchronized static void uploadFile(String filePath, String folderPath, String pwd, UpOrDownloadListener upOrDownloadListener) { if (mUploadManager != null) { UiUtil.starThread(() -> { String fileName = getFileName(filePath); String url = HttpConfig.uploadFileUrl + folderPath + "/" + fileName; String headerStr = "{\"scope-token\":\"" + Constant.scope_token + "\"}"; mUploadManager.createFileUploader(url, filePath, fileName, headerStr, pwd); }); }}
createFileUploader请求参数解析:url:上传服务器地址filePath:本地文件路径fileName:文件名称(包括文件后缀)headerStr:请求头文件pwd:上传文件需要的密码
#
2.7.3.3 开始上传文件public static void startUpload(long id) { if (mUploadManager != null) { mUploadManager.start(id); }}
参数解析id:任务id
#
2.7.3.4 暂停上传文件public static void stopUpload(long id) { if (mUploadManager != null) { mUploadManager.stop(id); }}
参数解析id:任务id
#
2.7.3.5 获取上传文件列表public static void getUploadList(OnGetFilesListener listener) { UiUtil.starThread(() -> { String jsonData = Gonet.getUploadList(dbPath); Log.e("getUploadList=返回列表json字符串="+jsonData); });}
#
2.7.4 gomobile下载文件#
2.7.4.1 初始化下载文件管理器public static void initDownloadManager() { LogUtil.e(TAG + "初始化"); if (mDownloadManager == null && !TextUtils.isEmpty(Constant.scope_token)) { UiUtil.starThread(() -> { String headerStr = "{\"scope-token\":\"" + Constant.scope_token + "\"}"; mDownloadManager = Gonet.newDownloadManager(apiUrl, dbPath, headerStr); mDownloadManager.run(headerStr); }) }}
newDownloadManager解析apiUrl:应用服务器api基地址dbPath:数据库文件夹地址headerStr:请求头文件
#
2.7.4.2 创建下载文件对象public synchronized static void downloadFile(String filePath, String pwd, UpOrDownloadListener upOrDownloadListener) { if (mDownloadManager != null) { UiUtil.starThread(() -> { String url = HttpConfig.downLoadFileUrl + filePath; String headerStr = "{\"scope-token\":\"" + Constant.scope_token + "\"}"; mDownloadManager.createFileDownloader(url, headerStr, pwd); }); }}
createFileDownloader解析url:"/plugin/wangpan/resources"headerStr:请求头文件pwd:文件密码
#
2.7.4.3 创建下载文件夹对象public static void downloadFolder(String filePath, String pwd, UpOrDownloadListener startDownListener) { if (mDownloadManager != null) { UiUtil.starThread(() -> { String url1 = HttpConfig.downLoadFolderUrl1;//网络请求地址1 String url2 = HttpConfig.downLoadFolderUrl2;//网络请求地址2 String headerStr = "{\"scope-token\":\"" + Constant.scope_token + "\"}"; mDownloadManager.createDirDownloader(url1, url2, filePath, headerStr, pwd) }); }}
createDirDownloader解析url1:"/plugin/wangpan/resources/*path"url2:"/plugin/wangpan/download/*path"filePath:下载文件夹路径headerStr:请求头文件pwd:文件夹密码
#
2.7.4.4 开始下载任务public static void startDownload(long id) { if (mDownloadManager != null) { mDownloadManager.start(id); }}
#
2.7.4.5 停止下载任务public static void stopDownload(long id) { if (mDownloadManager != null) { mDownloadManager.stop(id); }}
#
2.7.4.6 删除下载任务public static void deleteDownload(long id) { if (mDownloadManager != null) { mDownloadManager.delete(id); }}
#
2.7.5 使用插件注意事项#
2.7.5.1 退出APP需要调用public static void exitApp() { if (mUploadManager != null) { mUploadManager.quitAPP(); } if (mDownloadManager != null) { mDownloadManager.quitAPP(); }}
#
2.7.5.2 监听网络变化通知public static void netWorkNotice() { if (mUploadManager != null) { mUploadManager.networkNil(); } if (mDownloadManager != null) { mDownloadManager.networkNil(); }}
#
2.7.5.3 上传域名的更变public static void changeHost() { String baseUrl = getBaseUrl(); if (mUploadManager != null) { mUploadManager.changeHost(newHost); } if (mDownloadManager != null) { mDownloadManager.changeHost(newHost); }}
#
2.8 存储池#
2.8.1 说明存储池是存储池分区及文件操作的根基,只有存储池存在,才能操作存储池分区和文件夹,是数据存储的地方。
只要有闲置的存储池(硬盘),我们就可以把它添加到现有的存储池或者是新的存储池,然后回到存储池列表就可以看到刷新的数据。
除了添加和查看存储池之外,我们还可以修改存储池的名称、删除存储和添加存储池分区、查看存储池分区、辑存储池分区及删除存储池分区(存储池分区的操作将会在下一章节进行说明)。
由于删除存储池需要一定的时间,所以存在存储池删除中的状态;删除存储池不一定能删除成功,所以存在删除存储池失败的状态。
注:只有家庭的拥有者才能操作存储池
#
2.8.2 主要代码实现#
2.8.2.1 存储池列表和存储详情的model类/** * 存储池列表 */public class StoragePoolListBean {
private List<StoragePoolDetailBean> list; // tem为object,存储池 private PagerBean pager; // 分页数据
...}
/** * 存储池详情 */public class StoragePoolDetailBean implements MultiItemEntity, Serializable {
public static final int POOL = 1; // 自己定义字段添加存储池时是存储 public static final int ADD = 2; // 自己定义字段添加存储池时是最后的加号图片
private int itemType; // 自己定义字段,用于区分添加存储池时是存储还是最后的加号图片
private boolean selected; // 自己定义字段,用于表示存储添加存储池时是否选择
private String id; private String name; // 名称 private double capacity; // 容量 private double use_capacity; //已用容量
/** * // 异步任务状态, 为空则没有异步任务, * TaskDelPool_0删除存储池失败,TaskDelPool_1删除存储池中, * TaskAddPool_0添加存储池失败,TaskAddPool_1添加存储池中, * TaskUpdatePool_0修改存储池失败,TaskUpdatePool_1修改存储池中, */ private String status; private String task_id; //异步任务ID
private List<DiskBean> pv; // 物理分区:就是所谓硬盘 private List<DiskBean> lv; //逻辑分区:实际存储池分区
...}
/** * 硬盘和存储池分区 */public class DiskBean implements Serializable {
/** * id : 4255 * name : 安但风张义 * capacity : 91.2 */
private String id; private String name; private long capacity; private long use_capacity; // 已用容量 private boolean selected;
/** * 为空则没有任务状态, * TaskAddPartition_1添加存储池分区中,TaskAddPartition_0添加存储池分区失败, * TaskUpdatePartition_1修改存储池分区中,TaskUpdatePartition_0修改存储池分区失败, * TaskDelPartition_1删除存储池分区中,TaskDelPartition_0删除存储池分区失败 */ private String status; private String task_id; // 异步任务id
...}
#
2.8.2.2 存储池列表及闲置硬盘/** * 存储管理(存储池列表) */public class StoragePoolListActivity extends BaseMVPDBActivity<ActivityStoragePoolListBinding, StoragePoolListContract.View, StoragePoolListPresenter> implements StoragePoolListContract.View { ... /** * 闲置硬盘 */ private void initRvDisk(){ LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setOrientation(RecyclerView.HORIZONTAL); binding.rvDisk.setLayoutManager(layoutManager); diskAdapter = new DiskAdapter(); binding.rvDisk.setAdapter(diskAdapter);
diskAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() { @Override public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) { DiskBean diskBean = diskAdapter.getItem(position); if (view.getId() == R.id.tvAdd){ Bundle bundle = new Bundle(); bundle.putString("diskName", diskBean.getName()); bundle.putLong("capacity", diskBean.getCapacity()); switchToActivity(AddToStoragePoolActivity.class, bundle); } } }); }
/** * 存储池 */ private void initRvPool(){ ... storagePoolAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { @Override public void onItemClick(BaseQuickAdapter adapter, View view, int position) { StoragePoolDetailBean storagePoolDetailBean = storagePoolAdapter.getItem(position); if (TextUtils.isEmpty(storagePoolDetailBean.getStatus())) { // 没有异步状态才可跳转 Bundle bundle = new Bundle(); bundle.putString("name", storagePoolDetailBean.getName()); switchToActivity(StoragePoolDetailActivity.class, bundle); } } });
storagePoolAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() { @Override public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) { StoragePoolDetailBean storagePoolDetailBean = storagePoolAdapter.getItem(position); int viewId = view.getId(); if (viewId == R.id.ivDot){ // 显示物理硬盘个数 showHardDiskDialog(storagePoolAdapter.getItem(position).getPv()); }else if (viewId == R.id.tvRetry){ // 重试 String status = storagePoolDetailBean.getStatus(); if (status!=null){ if (status.equals(Constant.STORAGE_POOL_DELETE_FAIL)){ // 删除失败 mPresenter.restartTask(Constant.scope_token, storagePoolDetailBean.getTask_id()); } } } } }); } ...}
public class StoragePoolListPresenter extends BasePresenter<StoragePoolListModel, StoragePoolListContract.View> implements StoragePoolListContract.Presenter {
... /** * 获取存储池列表 * @param scopeToken * @param map * @param showLoading */ @Override public void getStoragePools(String scopeToken, Map<String, String> map, boolean showLoading) { executeObservable(mModel.getStoragePools(scopeToken, map), new RequestDataCallback<StoragePoolListBean>(showLoading) { @Override public void onSuccess(StoragePoolListBean response) { super.onSuccess(response); if (mView!=null){ mView.getStoragePoolsSuccess(response); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null){ mView.getStoragePoolsFail(errorCode, errorMessage); } } }); }
/** * 闲置硬盘列表 * @param scopeToken */ @Override public void getDisks(String scopeToken) { executeObservable(mModel.getDisks(scopeToken), new RequestDataCallback<DiskListBean>(false) { @Override public void onSuccess(DiskListBean response) { super.onSuccess(response); if (mView!=null){ mView.getDisksSuccess(response); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null){ mView.getDisksFail(errorCode, errorMessage); } } }); } ...}
#
2.8.2.3 添加到存储池/** * 添加到存储池 */public class AddToStoragePoolActivity extends BaseMVPDBActivity<ActivityAddToStoragePoolBinding, AddToStoragePoolContract.View, AddToStoragePoolPresenter> implements AddToStoragePoolContract.View { ... /** * 初始化列表 */ private void initRvPool(){ ...
storagePoolMulAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { @Override public void onItemClick(BaseQuickAdapter adapter, View view, int position) { StoragePoolDetailBean storagePoolMulBean = storagePoolMulAdapter.getItem(position); if (storagePoolMulBean.getItemType() == 1){ // 列表数据 for (StoragePoolDetailBean storagePoolDetailBean : storagePoolMulAdapter.getData()){ storagePoolDetailBean.setSelected(false); } storagePoolMulBean.setSelected(true); createName = storagePoolMulBean.getName(); storagePoolMulAdapter.notifyDataSetChanged(); }else { // 添加 if (addStoragePoolDialog!=null && !addStoragePoolDialog.isShowing()) addStoragePoolDialog.show(AddToStoragePoolActivity.this); } } }); } ... /** * 初始 添加到新的存储池 弹窗 */ private void initStoragePoolDialog(){ addStoragePoolDialog = AddStoragePoolDialog.getInstance(0, ""); addStoragePoolDialog.setCompleteListener(new AddStoragePoolDialog.OnCompleteListener() { @Override public void onComplete(int type, String poolName) { createName = poolName; CreateStoragePoolRequest createStoragePoolRequest = new CreateStoragePoolRequest(poolName, diskName); mPresenter.createStoragePool(Constant.scope_token, createStoragePoolRequest); } });
} ... /** * 点击事件 */ public class OnClickHandler { public void onClick(View view) { int viewId = view.getId(); if (viewId == R.id.ivBack) { // 返回 finish(); }else if (viewId == R.id.tvSave){ // 保存 StoragePoolDetailBean storagePoolDetailBean = storagePoolMulAdapter.getSelectedData(); if (storagePoolDetailBean == null){ // 未选择 ToastUtil.show(UiUtil.getString(R.string.mine_please_choose_storage_pool)); return; } if (storagePoolDetailBean.getUse_capacity()>0){ // 已有数据 showSaveDialog(storagePoolDetailBean.getName()); // 弹窗提示 }else { // 直接添加 addToStoragePool(storagePoolDetailBean.getName()); }
} } }}
public class AddToStoragePoolPresenter extends BasePresenter<AddToStoragePoolModel, AddToStoragePoolContract.View> implements AddToStoragePoolContract.Presenter {
...
/** * 创建存储池 * @param scopeToken * @param createStoragePoolRequest */ @Override public void createStoragePool(String scopeToken, CreateStoragePoolRequest createStoragePoolRequest) { executeObservable(mModel.createStoragePool(scopeToken, createStoragePoolRequest), new RequestDataCallback<Object>() { @Override public void onSuccess(Object response) { super.onSuccess(response); if (mView!=null){ mView.createStoragePoolSuccess(); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null){ mView.createStoragePoolFail(errorCode, errorMessage); } } }); }
/** * 添加硬盘到存储池 * @param scopeToken * @param addStoragePoolRequest */ @Override public void addToStoragePool(String scopeToken, AddStoragePoolRequest addStoragePoolRequest) { executeObservable(mModel.addToStoragePool(scopeToken, addStoragePoolRequest), new RequestDataCallback<Object>() { @Override public void onSuccess(Object response) { super.onSuccess(response); if (mView!=null){ mView.addToStoragePoolSuccess(); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null){ mView.addToStoragePoolFail(errorCode, errorMessage); } } }); }}
#
2.8.2.4 存储池详情/** * 存储池详情 */public class StoragePoolDetailActivity extends BaseMVPDBActivity<ActivityStoragePoolDetailBinding, StoragePoolDetailContract.View, StoragePoolDetailPresenter> implements StoragePoolDetailContract.View { ... /** * 获取存储池详情成功 * @param storagePoolDetailBean */ @Override public void getStoragePoolDetailSuccess(StoragePoolDetailBean storagePoolDetailBean) { binding.refreshLayout.finishRefresh(); if (storagePoolDetailBean!=null){ mStoragePoolDetailBean = storagePoolDetailBean; poolId = storagePoolDetailBean.getId(); binding.tvName.setText(storagePoolDetailBean.getName()); double usedCapacity = storagePoolDetailBean.getUse_capacity(); double allCapacity = storagePoolDetailBean.getCapacity(); double availableCapacity = allCapacity - usedCapacity; binding.tvSeparable.setText(FileUtil.getReadableFileSize(availableCapacity)+UiUtil.getString(R.string.mine_separable_capacity)); binding.tvAll.setText(FileUtil.getReadableFileSize(allCapacity)+UiUtil.getString(R.string.mine_all_capacity)); List<DiskBean> hardDisk = storagePoolDetailBean.getPv(); // 硬盘列表
int hardDiskCount = CollectionUtil.isNotEmpty(hardDisk) ? hardDisk.size() : 0; // 硬盘个数 binding.tvCount.setText(StringUtil.getStringFormat(UiUtil.getString(R.string.mine_hard_disk_count), hardDiskCount)); int progress = (int) ((usedCapacity/allCapacity)*100); // 容量进度百分比 isSystemPool = storagePoolDetailBean.getName().equals(Constant.SYSTEM_POOL); // 是否是系统存储池 binding.tvDel.setVisibility(isSystemPool ? View.GONE : View.VISIBLE); // 是系统存储池可删除 binding.tvName.setCompoundDrawablesWithIntrinsicBounds(0, 0, isSystemPool ? 0 : R.drawable.icon_white_edit, 0); // 是系统存储池不可编辑 binding.tvCount.setCompoundDrawablesWithIntrinsicBounds(0, 0, isSystemPool ? 0 : R.drawable.icon_white_right_arrow, 0); // 是系统存储池查看磁盘不可操作 binding.tvCount.setEnabled(!isSystemPool);// 是系统存储池查看磁盘不可操作 binding.ivAdd.setVisibility(isSystemPool ? View.GONE : View.VISIBLE); // 是系统存储池不可添加存储分区 binding.rb.setProgress(progress); storagePoolDetailAdapter.setNewData(storagePoolDetailBean.getLv()); setNullView(CollectionUtil.isEmpty(storagePoolDetailBean.getLv())); binding.coordinatorLayout.setVisibility(View.VISIBLE); } } ...}
#
2.9 存储池分区#
2.9.1 说明存储池分区是在某个存储池建立的分区,是创建文件的根基之一,因为文件夹的创建是建立在存储池和存储池分区的基础上,可以说存储池和存储池分区时文件夹/文件的载体。
处理系统存储池下的存储池分区之外,我们都可以对存储池分区进行增删改操作和不一定可以操作成功。
同样,添加、编辑和删除存储池需要一定时间,所以存储池分区存在添加中、修改中、删除中、添加失败、修改失败和删除失败的状态。
如果添加或者修改失败,我们可以重试或者取消添加和修改;
如果删除失败的话,我们只能进行重试操作。
添加/编辑存储池分区时,容量单位只能选择MB、GB和TB,在单位选择MB时,容量必须时4的倍数。
注:由于存储池分区在存储池的子级,所以存储池分区也是只有家庭拥有者才能操作
#
2.9.2 主要代码实现#
2.9.2.1 存储池分区实体类/** * 硬盘和存储池分区 */public class DiskBean implements Serializable {
/** * id : 4255 * name : 安但风张义 * capacity : 91.2 */
private String id; private String name; private long capacity; private long use_capacity; // 已用容量 private boolean selected;
/** * 为空则没有任务状态, * TaskAddPartition_1添加存储池分区中,TaskAddPartition_0添加存储池分区失败, * TaskUpdatePartition_1修改存储池分区中,TaskUpdatePartition_0修改存储池分区失败, * TaskDelPartition_1删除存储池分区中,TaskDelPartition_0删除存储池分区失败 */ private String status; private String task_id; // 异步任务id ...}
#
2.9.2.2 存储池分区列表适配器/** * 存储池详情的存储池分区列表 */public class StoragePoolDetailAdapter extends BaseQuickAdapter<DiskBean, BaseViewHolder> {
public StoragePoolDetailAdapter() { super(R.layout.item_storage_pool_detail); }
@Override protected void convert(BaseViewHolder helper, DiskBean item) {
helper.addOnClickListener(R.id.tvRetry); helper.addOnClickListener( R.id.tvCancel); String capacity = FileUtil.getReadableFileSize(item.getUse_capacity())+"/"+FileUtil.getReadableFileSize(item.getCapacity()); String allCapacity = UiUtil.getString(R.string.mine_all_size)+FileUtil.getReadableFileSizeSaveTow(item.getCapacity()); String availableCapacity = UiUtil.getString(R.string.mine_available_capacity)+FileUtil.getReadableFileSizeSaveTow(item.getCapacity() - item.getUse_capacity()); helper.setText(R.id.tvName, item.getName()) .setText(R.id.tvAllSize, allCapacity) .setText(R.id.tvAvailable, availableCapacity);
ProgressBar rb = helper.getView(R.id.rb); int progress = item.getCapacity() == 0 ? 100 : (int) (StringUtil.long2Double(item.getUse_capacity())/StringUtil.long2Double(item.getCapacity())*100); rb.setProgress(progress); rb.setVisibility(View.VISIBLE);
ImageView ivDot = helper.getView(R.id.ivDot); LinearLayout llResult = helper.getView(R.id.llResult); TextView tvRetry= helper.getView(R.id.tvRetry); TextView tvCancel = helper.getView(R.id.tvCancel); TextView tvTips = helper.getView(R.id.tvTips); ImageView ivStatus = helper.getView(R.id.ivStatus); llResult.setVisibility(View.GONE); ivStatus.setVisibility(View.GONE); tvCancel.setVisibility(View.GONE); String status = item.getStatus(); ivDot.setVisibility(TextUtils.isEmpty(status) ? View.VISIBLE : View.GONE); if (!TextUtils.isEmpty(status)){ switch (status){ case Constant.PARTITION_ADDING: // 添加中 case Constant.PARTITION_UPDATING: // 修改中 case Constant.PARTITION_DELETING: // 删除中 ivStatus.setVisibility(View.VISIBLE); llResult.setVisibility(View.GONE); if (status.equals(Constant.PARTITION_DELETING)) { // 删除中 ivStatus.setImageResource(R.drawable.icon_folder_deleting); }else if (status.equals(Constant.PARTITION_ADDING)) { // 添加中 ivStatus.setImageResource(R.drawable.icon_adding); }else if (status.equals(Constant.PARTITION_UPDATING)) { // 修改中 ivStatus.setImageResource(R.drawable.icon_folder_updating); }
break;
case Constant.PARTITION_ADD_FAIL: // 添加失败 case Constant.PARTITION_UPDATE_FAIL: // 修改失败 case Constant.PARTITION_DELETE_FAIL: // 删除失败 ivStatus.setVisibility(View.GONE); tvRetry.setVisibility(View.VISIBLE); llResult.setVisibility(View.VISIBLE); if (status.equals(Constant.PARTITION_DELETE_FAIL)){ // 删除状态 tvCancel.setVisibility(View.GONE); tvTips.setText(UiUtil.getString(R.string.mine_partition_del_fail)); }else { tvCancel.setVisibility(View.VISIBLE); rb.setVisibility(View.GONE); if (status.equals(Constant.PARTITION_ADD_FAIL)){ // 添加失败 tvCancel.setText(UiUtil.getString(R.string.mine_cancel_add)); tvTips.setText(UiUtil.getString(R.string.mine_partition_add_fail)); }else if (status.equals(Constant.PARTITION_UPDATE_FAIL)){ // 修改失败 tvCancel.setText(UiUtil.getString(R.string.mine_cancel_update)); tvTips.setText(UiUtil.getString(R.string.mine_partition_update_fail)); } }
break;
} } }}
#
2.9.2.3 添加、编辑和存储池分区/** * 添加/编辑分区 */public class AddPartitionActivity extends BaseMVPDBActivity<ActivityAddPartitionBinding, AddPartitionContract.View, AddPartitionPresenter> implements AddPartitionContract.View{ ... /** * 删除确认弹窗 */ private void showRemoveDialog(){ CenterAlertDialog centerAlertDialog = CenterAlertDialog.getInstance(UiUtil.getString(R.string.mine_remove_confirm), UiUtil.getString(R.string.mine_remove_partition_content), UiUtil.getString(R.string.mine_remove_tips), R.color.color_ff0000, "", UiUtil.getString(R.string.mine_sure_remove)); centerAlertDialog.setConfirmListener(new CenterAlertDialog.OnConfirmListener() { @Override public void onConfirm() { PoolNameRequest poolNameRequest = new PoolNameRequest(poolName); mPresenter.removePartition(Constant.scope_token, diskBean.getName(), poolNameRequest); centerAlertDialog.dismiss(); } }); centerAlertDialog.show(this); } ... /** * 点击事件 */ public class OnClickHandler { public void onClick(View view) { int viewId = view.getId(); if (viewId == R.id.ivBack) { // 返回 finish(); }else if (viewId == R.id.tvDel){ // 删除 showRemoveDialog(); }else if (viewId == R.id.tvUnit){ // 单位 if (bottomListDialog!=null && !bottomListDialog.isShowing()){ bottomListDialog.show(AddPartitionActivity.this); } }else if (viewId == R.id.tvSave){ // 保存 capacityStr = unitIsMB() ? binding.tvResult.getText().toString().trim() : binding.etCapacity.getText().toString().trim(); long capacity = Long.parseLong(capacityStr); if (diskBean == null) { // 添加 AddPartitionRequest addPartitionRequest = new AddPartitionRequest(binding.etName.getText().toString(), capacity, defaultUnit, poolName); mPresenter.addPartition(Constant.scope_token, addPartitionRequest); }else { // 更新 ModifyPartitionRequest modifyPartitionRequest = new ModifyPartitionRequest(binding.etName.getText().toString(), capacity, defaultUnit, poolName); mPresenter.modifyPartition(Constant.scope_token, diskBean.getName(), modifyPartitionRequest); } } } }}
/** * 添加分区 */public class AddPartitionPresenter extends BasePresenter<AddPartitionModel, AddPartitionContract.View> implements AddPartitionContract.Presenter {
@Override public AddPartitionModel createModel() { return new AddPartitionModel(); }
/** * 添加分区 * @param scopeToken * @param addPartitionRequest */ @Override public void addPartition(String scopeToken, AddPartitionRequest addPartitionRequest) { executeObservable(mModel.addPartition(scopeToken, addPartitionRequest), new RequestDataCallback<Object>() { @Override public void onSuccess(Object response) { super.onSuccess(response); if (mView!=null){ mView.addPartitionSuccess(); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null){ mView.addPartitionFail(errorCode, errorMessage); } } }); }
/** * 编辑 存储池分区 * @param scopeToken * @param name * @param modifyPartitionRequest */ @Override public void modifyPartition(String scopeToken, String name, ModifyPartitionRequest modifyPartitionRequest) { executeObservable(mModel.modifyPartition(scopeToken, name, modifyPartitionRequest), new RequestDataCallback<Object>() { @Override public void onSuccess(Object response) { super.onSuccess(response); if (mView!=null){ mView.modifyPartitionSuccess(); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null){ mView.modifyPartitionFail(errorCode, errorMessage); } } }); }
/** * 删除 存储池分区 * @param scopeToken * @param name */ @Override public void removePartition(String scopeToken, String name, PoolNameRequest poolNameRequest) { executeObservable(mModel.removePartition(scopeToken, name, poolNameRequest), new RequestDataCallback<Object>() { @Override public void onSuccess(Object response) { super.onSuccess(response); if (mView!=null){ mView.removePartitionSuccess(); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView!=null){ mView.removePartitionFail(errorCode, errorMessage); } } }); }}
#
2.10 文件夹管理#
2.10.1 说明在文件夹管理这个模块中,我们可以新增文件夹、编辑文件夹和删除文件夹。
我们在新增文件夹时需要选择时私人文件夹还是共享文件夹
- 如果我们选择的时私人文件夹,那么我们选哟选择是否需要加密且可访问成员我们只能选择一位;
- 如果我们选择的时共享文件夹,则不需要选择是否加密且可访问成员可以选择多个。
我们在编辑文件夹时,如果把存储分区修改,则会提示需要一些处理时间,所以文件夹会有修改中的状态,而修改文件夹存储不一定成功,所以存在修改失败的状态。
同样,删除文件夹也需要一定时间且不一定成功,因此文件夹也存在删除中和删除失败的状态。
当然,如果时加密文件夹,我们可以修改文件夹的密码,在文件夹管理的文件列表点击要修改文件项的三个点就可以了。
除此之外,我们还可以在设置中设置有新成员加入时,系统自动为该成员创建私人文件夹的位置和设置成员退出或被移除时是否自动删除该成员的私人文件。
其中,修改失败后,点击确定(相当于取消修改)之后可以进入文件夹详情;
删除失败之后只能重试,不能做其它操作。
当然,我们也可以在新增/编辑文件夹时设置文件夹访问成员的权限。
我们编辑文件夹时:
- 如果在创建文件的时候文件夹类型为共享文件夹,则文件类型可更改;
- 如果在创建文件的时候文件夹类型为私人文件夹的情况,则要判断文件夹是否加密,未加密的话,文件夹类型可更改,否则不可更改。
#
2.10.2 主要代码实现#
2.10.2.1 文件夹实体类public class FolderBean implements Serializable {
private long id; private String name; //文件夹名称 private int is_encrypt; // 是否需要加密1需要0步需要 private int mode; // 类型:1个人文件夹2分享文件夹 private String persons; // 可访问成员,字符串输出 private String pool_name; // 存储分区名称 private String task_id; // 异步任务ID
// 枚举: 为空则代表没有异步状态,TaskMovingFolder_1 修改中,TaskMovingFolder_0 修改失败,TaskDelFolder_1 删除中,TaskDelFolder_0 删除失败 private String status; ...}
#
2.10.2.2 文件夹列表/** * 文件夹 */public class FolderActivity extends BaseMVPDBActivity<ActivityFolderBinding, FolderContract.View, FolderPresenter> implements FolderContract.View { ... /** * 初始化列表 */ private void initRv(){ ... mineFolderAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() { @Override public void onItemClick(BaseQuickAdapter adapter, View view, int position) { FolderBean folderBean = mineFolderAdapter.getItem(position); if (TextUtils.isEmpty(folderBean.getStatus())) { // 没有任务状态时,才能进入文件夹详情 Bundle bundle = new Bundle(); bundle.putSerializable("folder", folderBean); switchToActivityForResult(CreateFolderActivity.class, bundle, 100); } } });
mineFolderAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() { @Override public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) { int viewId = view.getId(); FolderBean folderBean = mineFolderAdapter.getItem(position); folderId = folderBean.getId(); if (viewId == R.id.ivThreeDot){ // 三个点(修改密码弹窗) if (folderBean.getIs_encrypt() == 1) { // 加密情况下才弹窗 settingPopupWindow.showAsDropDown(view, -settingPopupWindow.getWidth() + view.getWidth() * 2 - UiUtil.getDimens(R.dimen.dp_10), 0); } }else if (viewId == R.id.tvOperate){ // 重试/确定 String status = folderBean.getStatus(); if (status!=null) { if (status.equals(Constant.FOLDER_UPDATE_FAIL)) { // 修改失败时确定 mPresenter.removeTask(Constant.scope_token, folderBean.getTask_id()); } else if (status.equals(Constant.FOLDER_DELETE_FAIL)) { // 删除失败时重试 mPresenter.restartTask(Constant.scope_token, folderBean.getTask_id()); } } } } }); } ... }
/** * 文件夹列表适配器 */public class MineFolderAdapter extends BaseQuickAdapter<FolderBean, BaseViewHolder> {
public MineFolderAdapter() { super(R.layout.item_folder); }
@Override protected void convert(BaseViewHolder helper, FolderBean item) { helper.addOnClickListener(R.id.ivThreeDot); helper.addOnClickListener(R.id.tvOperate); helper.setText(R.id.tvName, item.getName()) .setText(R.id.tvDistrict, item.getPool_name()) .setText(R.id.tvMember, item.getPersons()); LinearLayout llResult = helper.getView(R.id.llResult); ImageView ivEncrypt = helper.getView(R.id.ivEncrypt); ImageView ivStatus = helper.getView(R.id.ivStatus); ImageView ivThreeDot = helper.getView(R.id.ivThreeDot); TextView tvTips = helper.getView(R.id.tvTips); TextView tvOperate = helper.getView(R.id.tvOperate); int is_encrypt = item.getIs_encrypt(); ivEncrypt.setVisibility(is_encrypt == 1 ? View.VISIBLE : View.GONE); ivThreeDot.setSelected(is_encrypt == 1); llResult.setVisibility(View.GONE); ivStatus.setVisibility(View.GONE); ivThreeDot.setVisibility(View.GONE); String status = item.getStatus(); if (TextUtils.isEmpty(status)){ // 空则代表没有异步状态 ivThreeDot.setVisibility(View.VISIBLE); }else { String name = item.getName(); name = name.length()<4 ? name : (name.substring(0, 2)+"..."); switch (status){ case Constant .FOLDER_UPDATING: // 修改中 ivStatus.setVisibility(View.VISIBLE); ivStatus.setImageResource(R.drawable.icon_folder_updating); break;
case Constant .FOLDER_UPDATE_FAIL: // 修改失败 llResult.setVisibility(View.VISIBLE); tvTips.setText(StringUtil.getStringFormat(UiUtil.getString(R.string.mine_update_fail), name)); tvOperate.setText(UiUtil.getString(R.string.common_confirm)); break;
case Constant .FOLDER_DELETING: // 删除中 ivStatus.setVisibility(View.VISIBLE); ivStatus.setImageResource(R.drawable.icon_folder_deleting); break;
case Constant .FOLDER_DELETE_FAIL: // 删除失败 llResult.setVisibility(View.VISIBLE); tvTips.setText(StringUtil.getStringFormat(UiUtil.getString(R.string.mine_del_fail), name)); tvOperate.setText(UiUtil.getString(R.string.mine_retry)); break; } } }}
#
2.10.2.3 新增、编辑和删除文件夹/** * 新增/修改文件夹 */public class CreateFolderActivity extends BaseMVPDBActivity<ActivityCreateFolderBinding, CreateFolderContract.View, CreateFolderPresenter> implements CreateFolderContract.View { ... /** * 编辑成员弹窗 */ private void initOperatePermissionDialog(){ operatePermissionDialog = OperatePermissionDialog.getInstance(); operatePermissionDialog.setConfirmListener(new OperatePermissionDialog.OnConfirmListener() { @Override public void onConfirm(int read, int write, int del) { AccessibleMemberBean accessibleMemberBean = folderMemberAdapter.getItem(memberPos); accessibleMemberBean.setRead(read); accessibleMemberBean.setWrite(write); accessibleMemberBean.setDeleted(del); folderMemberAdapter.notifyItemChanged(memberPos); operatePermissionDialog.dismiss(); } }); } ... /** * 初始化成员列表 */ private void initRv(){ binding.rvMember.setLayoutManager(new LinearLayoutManager(this)); folderMemberAdapter = new FolderMemberAdapter(); binding.rvMember.setAdapter(folderMemberAdapter); folderMemberAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() { @Override public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) { memberPos = position; AccessibleMemberBean accessibleMemberBean = folderMemberAdapter.getItem(position); int viewId = view.getId(); if (viewId == R.id.ivEdit){ // 编辑 Bundle bundle = new Bundle(); bundle.putInt("read", accessibleMemberBean.getRead()); bundle.putInt("write", accessibleMemberBean.getWrite()); bundle.putInt("del", accessibleMemberBean.getDeleted()); PermissionUserBean permissionUserBean = new PermissionUserBean(accessibleMemberBean.getNickname(), accessibleMemberBean.getFace()); List<PermissionUserBean> users = new ArrayList<>(); users.add(permissionUserBean); bundle.putSerializable("users", (Serializable) users); bundle.putBoolean("originalWrite", true); bundle.putBoolean("originalDel", true); operatePermissionDialog.setArguments(bundle); operatePermissionDialog.show(CreateFolderActivity.this); }else if (viewId == R.id.ivDel){ // 删除 members.remove(position); folderMemberAdapter.notifyItemRemoved(position); if (CollectionUtil.isNotEmpty(members)){ binding.tvMember.setText(StringUtil.getStringFormat(UiUtil.getString(R.string.mine_accessible_member_count), members.size())); binding.tvPrivate.setEnabled(members.size()<2); }else { setNoAvailableMember(); } checkSaveEnabled(); } } }); } ... /** * 是否加密 */ private void whetherEncrypt(boolean encrypt){ binding.tvYes.setSelected(encrypt); binding.tvNo.setSelected(!encrypt); binding.viewLineEncrypt.setVisibility(encrypt ? View.VISIBLE : View.INVISIBLE); binding.clPwd.setVisibility(encrypt ? View.VISIBLE : View.GONE); binding.clConfirmPwd.setVisibility(encrypt ? View.VISIBLE : View.GONE); } ... /** * 设置文件类型和是否加密状态 */ private void setTypeStatus(){ binding.tvPrivate.setSelected(mode == 1); binding.tvShare.setSelected(mode == 2); binding.clEncrypt.setVisibility(View.VISIBLE); binding.tvYes.setSelected(is_encrypt == 1); binding.tvNo.setSelected(is_encrypt == 0); if (mode == 1){ // 私人文件夹 if (is_encrypt == 1){ // 是加密 setPrivateDisabled(); }
} setEncryptDisabled();
} /** * 设置私人文件夹不可操作 */ private void setPrivateDisabled(){ binding.tvPrivate.setEnabled(false); binding.tvPrivate.setAlpha(0.5f); binding.tvShare.setVisibility(View.GONE); }
/** * 设置加密类型不可操作 */ private void setEncryptDisabled(){ binding.tvYes.setEnabled(false); binding.tvYes.setAlpha(0.5f); binding.tvNo.setEnabled(false); binding.tvNo.setAlpha(0.5f); binding.tvYes.setVisibility(is_encrypt == 1 ? View.VISIBLE : View.GONE); binding.tvNo.setVisibility(is_encrypt == 0 ? View.VISIBLE : View.GONE); } ... /** * 文件夹详情成功 * @param folderDetailBean */ @Override public void getFolderDetailSuccess(FolderDetailBean folderDetailBean) { if (folderDetailBean!=null){ folderDetail = folderDetailBean; binding.tvDel.setVisibility(View.VISIBLE); originalPoolName = folderDetailBean.getPool_name(); mPoolName = folderDetailBean.getPool_name(); originalPartitionName = folderDetailBean.getPartition_name(); mPartitionName = folderDetailBean.getPartition_name(); binding.etName.setText(folderDetailBean.getName() ); setPoolName(); is_encrypt = folderDetailBean.getIs_encrypt(); mode = folderDetailBean.getMode(); setTypeStatus(); members = folderDetailBean.getAuth(); if (CollectionUtil.isEmpty(members)){ setNoAvailableMember(); }else { setHasMulMember(); } folderMemberAdapter.setNewData(members); binding.nsv.setVisibility(View.VISIBLE); setSaveEnabled(true); } } ... /** * 保存 */ private void save(){ String folderName = binding.etName.getText().toString().trim(); if (TextUtils.isEmpty(folderName)){ ToastUtil.show(UiUtil.getString(R.string.mine_please_input_folder_name)); return; } if (TextUtils.isEmpty(binding.tvSelPartition.getText().toString().trim())){ ToastUtil.show(UiUtil.getString(R.string.mine_please_choose_save_partition)); return; } // 如果是添加文件 String pwd = binding.etPwd.getText().toString().trim(); String confirmPwd = binding.etConfirmPwd.getText().toString().trim(); if (folderBean == null){
// 如果没有选择文件类型 if (mode == 0){ ToastUtil.show(UiUtil.getString(R.string.mine_please_choose_type)); return; }
// 如果是加密文件 if (is_encrypt == 1){ // 判断密码是为空 if (TextUtils.isEmpty(pwd)){ ToastUtil.show(UiUtil.getString(R.string.mine_please_input_pwd)); return; } // 判断确认密码是为空 if (TextUtils.isEmpty(confirmPwd)){ ToastUtil.show(UiUtil.getString(R.string.mine_please_input_confirm_pwd)); return; }
// 密码是否一致 if (!pwd.equals(confirmPwd)){ ToastUtil.show(UiUtil.getString(R.string.mine_pwd_is_inconsistent_with)); return; } } } if (CollectionUtil.isEmpty(members)){ ToastUtil.show(UiUtil.getString(R.string.mine_at_least_choose_one_member)); return; } if (folderBean == null){ // 如果是添加文件夹 FolderDetailBean folderPost = new FolderDetailBean(); folderPost.setName(folderName); folderPost.setPool_name(mPoolName); folderPost.setPartition_name(mPartitionName); folderPost.setMode(mode); folderPost.setIs_encrypt(mode == 1 ? is_encrypt : 0); folderPost.setAuth(members); // 如果是加密文件 if (is_encrypt == 1) { // 设置密码 folderPost.setPwd(pwd); folderPost.setConfirm_pwd(confirmPwd); } mPresenter.addFolder(Constant.scope_token, folderPost); }else { // 编辑文件夹 if (poolPartitionChanged()) { // 存储池更改 Bundle bundle = new Bundle(); bundle.putString("title", UiUtil.getString(R.string.mine_partition_transfer)); bundle.putString("content", StringUtil.getStringFormat(UiUtil.getString(R.string.mine_partition_change), folderDetail.getName(), originalPoolName + "-" + originalPartitionName, mPoolName + "-" + mPartitionName)); bundle.putString("tips", UiUtil.getString(R.string.mine_partition_change_time)); bundle.putString("leftText", UiUtil.getString(R.string.common_cancel)); bundle.putString("rightText", UiUtil.getString(R.string.common_confirm)); bundle.putInt("tipsTextColor", R.color.color_ff0000); partitionChangeTipsDialog.setArguments(bundle); partitionChangeTipsDialog.show(CreateFolderActivity.this); }else { // 存储池没更改 updateFolder(); } } }
... /** * 点击事件 */ public class OnClickHandler { public void onClick(View view) { int viewId = view.getId(); if (viewId == R.id.ivBack) { // 返回 finish(); }else if (viewId == R.id.tvPrivate){ // 私有文件 if (CollectionUtil.isNotEmpty(members)){ if (members.size()>1){ ToastUtil.show(UiUtil.getString(R.string.mine_only_one)); return; } } mode = 1; privateFolder(true); checkSaveEnabled(); }else if (viewId == R.id.tvShare){ // 共享文件 mode = 2; privateFolder(false); checkSaveEnabled(); }else if (viewId == R.id.tvSelPartition){ // 选择分区 if (choosePoolPartitionDialog == null){ // 如果选择存储池-存储分区弹窗为空 mPresenter.getStoragePools(Constant.scope_token); }else { if (!choosePoolPartitionDialog.isShowing()) choosePoolPartitionDialog.show(CreateFolderActivity.this); }
}else if (viewId == R.id.ivAdd || viewId == R.id.tvAdd){ // 添加成员 if (mode == 1 && CollectionUtil.isNotEmpty(members)){ // 如果时私人文件夹且已有成员 ToastUtil.show(UiUtil.getString(R.string.mine_only_one)); return; } Bundle bundle = new Bundle(); bundle.putInt("mode", mode); bundle.putSerializable("members", (Serializable) members); switchToActivityForResult(AddMemberActivity.class, bundle, 100); }else if (viewId == R.id.tvYes){ // 是加密 is_encrypt = 1; whetherEncrypt(true); checkSaveEnabled(); }else if (viewId == R.id.tvNo){ // 否加密 is_encrypt = 0; whetherEncrypt(false); checkSaveEnabled(); }else if (viewId == R.id.tvSave){ // 保存 save(); }else if (viewId == R.id.tvDel){ // 删除 if (removeAlertDialog!=null && !removeAlertDialog.isShowing()){ removeAlertDialog.show(CreateFolderActivity.this); } } } }}