feat: 增加文件分片上传功能

This commit is contained in:
xiaoyan 2022-10-21 15:49:19 +08:00
parent 1b6024d0cb
commit 8706bb1243
15 changed files with 1220 additions and 103 deletions

View File

@ -1,4 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 30
@ -83,6 +86,7 @@ dependencies {
implementation 'com.lzy.net:okgo:3.0.4'
implementation 'com.lzy.net:okrx2:2.0.2'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.readystatesoftware.chuck:library:1.0.4'
//retrofit+rxJava
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
@ -128,7 +132,8 @@ dependencies {
def room_version = "2.2.0-alpha01"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
implementation "androidx.room:room-guava:$room_version"
testImplementation "androidx.room:room-testing:$room_version"

View File

@ -245,6 +245,8 @@ public class Constant {
public static Set<String> submitIdSet = new HashSet<>();
public static final String SUBMIT_TOAST_MSG= "当前POI已经在提交列表中无需重复提交";
public static final long DEFAULT_CUT_SIZE = 5*1024*1024; // 文件切分默认大小为5MB
public static final int DEFAULT_TIME_OUT = 300; // 默认http超时时间设置为300秒
public static void clearLoginInfo() {
ACCESS_TOKEN = null;

View File

@ -0,0 +1,276 @@
package com.navinfo.outdoor.bean;
import android.app.Activity;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
import com.lzy.okgo.OkGo;
import com.lzy.okgo.cache.CacheEntity;
import com.lzy.okgo.cache.CacheMode;
import com.lzy.okgo.convert.Converter;
import com.lzy.okgo.convert.StringConvert;
import com.lzy.okgo.model.HttpHeaders;
import com.lzy.okgo.model.HttpParams;
import com.lzy.okgo.model.Response;
import com.lzy.okgo.request.GetRequest;
import com.lzy.okgo.request.PostRequest;
import com.lzy.okgo.request.base.Request;
import com.navinfo.outdoor.api.Constant;
import com.navinfo.outdoor.api.UserApplication;
import com.navinfo.outdoor.http.Callback;
import com.navinfo.outdoor.http.JsonCallback;
import com.navinfo.outdoor.util.FlushTokenUtil;
import com.navinfo.outdoor.util.Md5Util;
import com.navinfo.outdoor.util.NetWorkUtils;
import com.navinfo.outdoor.util.ToastUtils;
import com.umeng.umcrash.UMCrash;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
public class CommonRequestSend<T extends CommonResponseBase> {
public void getMethodCommon(Activity mContext, String url, HttpParams params, int timeOut, Callback<CommonResponse<T>> callback, Class<T> tClass) {
try {
Response<CommonResponse<T>> response = ((GetRequest<CommonResponse<T>>) obitainRequest(mContext, url, params, timeOut, 0, tClass))
.adapt()
.execute();
if (response.code() == 200) {
if (response.body().getCode() == 200) {
callback.onSuccess(response.body(), 1);
} else if (response.body().getCode() == 230){
FlushTokenUtil.flushToken(mContext);
Toast.makeText(mContext, "token过期请重新登录后再试...", Toast.LENGTH_LONG).show();
Throwable throwable = new Throwable("token过期请重新登录后再试...");
callback.onError(throwable, -1);
/*
* 友盟+
* 使用自定义错误查看时请在错误列表页面选择自定义异常
*/
UMCrash.generateCustomLog("网络请求报错-位置OKGOBuilder" + throwable, "UmengException");
} else {
Toast.makeText(mContext, response.message(), Toast.LENGTH_LONG).show();
Throwable throwable = new Throwable(response.message());
callback.onError(throwable, -2);
/*
* 友盟+
* 使用自定义错误查看时请在错误列表页面选择自定义异常
*/
UMCrash.generateCustomLog("网络请求报错-位置OKGOBuilder" + throwable, "UmengException");
}
} else {
Toast.makeText(mContext, response.message(), Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
e.printStackTrace();
Throwable throwable = e;
if (throwable != null) {
throwable.printStackTrace();
callback.onError(throwable, -3);
}
/*
* 友盟+
* 使用自定义错误查看时请在错误列表页面选择自定义异常
*/
UMCrash.generateCustomLog("网络请求报错-位置OKGOBuilder" + throwable, "UmengException");
}
}
public CommonResponse<T> getMethodCommonSync(Activity mContext, String url, HttpParams params, int timeOut, Class<T> tClass) {
try {
Response<CommonResponse<T>> response = ((GetRequest<CommonResponse<T>>) obitainRequest(mContext, url, params, timeOut, 0, tClass))
.adapt()
.execute();
if (response.code() == 200) {
if (response.body().getCode() == 230){
FlushTokenUtil.flushToken(mContext);
}
return response.body();
} else {
return new CommonResponse<T>(response.code(), response.message(), null);
}
} catch (Exception e) {
e.printStackTrace();
String message = e.getMessage();
assert message != null;
if (message.equals("timeout") || message.equals("Read time out")) {
ToastUtils.Message(mContext, "请求超时");
} else {
ToastUtils.Message(mContext, message);
}
Log.d("TAG", "onError: " + e.getMessage());
return new CommonResponse<T>(e.hashCode(), e.getMessage(), null);
}
}
public void postMethodCommon(Activity mContext, String url, HttpParams params, int timeOut, Callback<CommonResponse<T>> callback, Class<T> tClass) {
try {
Response<CommonResponse<T>> response = ((GetRequest<CommonResponse<T>>) obitainRequest(mContext, url, params, timeOut, 1, tClass))
.adapt()
.execute();
if (response.code() == 200) {
if (response.body().getCode() == 200) {
callback.onSuccess(response.body(), 1);
} else if (response.body().getCode() == 230){
FlushTokenUtil.flushToken(mContext);
Toast.makeText(mContext, "token过期请重新登录后再试...", Toast.LENGTH_LONG).show();
Throwable throwable = new Throwable("token过期请重新登录后再试...");
callback.onError(throwable, -1);
/*
* 友盟+
* 使用自定义错误查看时请在错误列表页面选择自定义异常
*/
UMCrash.generateCustomLog("网络请求报错-位置OKGOBuilder" + throwable, "UmengException");
} else {
Toast.makeText(mContext, response.message(), Toast.LENGTH_LONG).show();
Throwable throwable = new Throwable(response.message());
callback.onError(throwable, -2);
/*
* 友盟+
* 使用自定义错误查看时请在错误列表页面选择自定义异常
*/
UMCrash.generateCustomLog("网络请求报错-位置OKGOBuilder" + throwable, "UmengException");
}
} else {
Toast.makeText(mContext, response.message(), Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
e.printStackTrace();
Throwable throwable = e;
if (throwable != null) {
throwable.printStackTrace();
callback.onError(throwable, -3);
}
/*
* 友盟+
* 使用自定义错误查看时请在错误列表页面选择自定义异常
*/
UMCrash.generateCustomLog("网络请求报错-位置OKGOBuilder" + throwable, "UmengException");
}
}
public CommonResponse<T> postMethodCommonSync(Activity mContext, String url, HttpParams params, int timeOut, List<File> files, Class<T> tClass) {
try {
PostRequest<CommonResponse<T>> postRequest = (PostRequest<CommonResponse<T>>) obitainRequest(mContext, url, params, timeOut, 1, tClass);
if (files!=null) {
postRequest.addFileParams("file", files);
}
Response<CommonResponse<T>> response = postRequest
.adapt()
.execute();
if (response.code() == 200) {
if (response.body().getCode() == 230){
FlushTokenUtil.flushToken(mContext);
}
return response.body();
} else {
return new CommonResponse<T>(response.code(), response.message(), null);
}
} catch (Exception e) {
e.printStackTrace();
String message = e.getMessage();
assert message != null;
if (message.equals("timeout") || message.equals("Read time out")) {
ToastUtils.Message(mContext, "请求超时");
} else {
ToastUtils.Message(mContext, message);
}
Log.d("TAG", "onError: " + e.getMessage());
return new CommonResponse<T>(e.hashCode(), e.getMessage(), null);
}
}
/**
* 生成通用请求
* @param requestType 0-默认为get请求非0-post请求
* */
private Request obitainRequest(Activity mContext, String url, HttpParams params, int timeOut, int requestType, Class<T> clazz) throws Exception{
if (!NetWorkUtils.iConnected(UserApplication.userApplication)) { // 当前网络不可用
throw new Exception("网络不可用");
}
initTimeOut(timeOut);
long time = System.currentTimeMillis();
params.put("datetime", time);
Request request = null;
if (requestType != 0) {
request= OkGo
// 请求方式和请求url
.<CommonResponse<T>>post(url);
} else {
request= OkGo
// 请求方式和请求url
.<CommonResponse<T>>get(url);
}
return request
.headers(getHeader(params))
.params(params)
.retryCount(3)
.converter(new MyJsonCallback(clazz) {
@Override
public void onSuccess(Response response) {
}
})
// 请求的 tag, 主要用于取消对应的请求
.tag(this);
}
private void initTimeOut(int time) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.readTimeout(time, TimeUnit.SECONDS);
//全局的写入超时时间
builder.writeTimeout(time, TimeUnit.SECONDS);
//全局的连接超时时间
builder.connectTimeout(time, TimeUnit.SECONDS);
builder.callTimeout(time, TimeUnit.SECONDS);
OkGo.getInstance().init(UserApplication.getUserApplication()).setOkHttpClient(builder.build())
//全局统一缓存模式默认不使用缓存可以不传
.setCacheMode(CacheMode.NO_CACHE)
//全局统一缓存时间默认永不过期可以不传
.setCacheTime(CacheEntity.CACHE_NEVER_EXPIRE)
.setRetryCount(3);
}
private HttpHeaders getHeader(HttpParams params) {
HttpHeaders headers = new HttpHeaders();
try {
if (Constant.ACCESS_TOKEN == null) {
headers.put("Authorization", "Basic YXBwOmFwcHNlY3JldA==");
} else {
headers.put("Authorization", "bearer " + Constant.ACCESS_TOKEN);
}
StringBuilder util = new StringBuilder();//k1=v1&k2=v2&k3=v3
if (params != null && params.urlParamsMap != null) {
for (Map.Entry<String, List<String>> entry : params.urlParamsMap.entrySet()) {
if (!"file".equals(entry.getKey())) {
util.append(entry.getKey()).append("=").append(entry.getValue().get(0).toString()).append("&");
}
}
if (!util.toString().equals("")) {
util = new StringBuilder(util.substring(0, util.length() - 1));
}
}
headers.put("key", Md5Util.toMD5("dtxb_2021_navinfo" + util));
headers.put("Accept-Encoding", "identity");
return headers;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -0,0 +1,22 @@
package com.navinfo.outdoor.bean;
public class CommonResponse<T> extends CommonResponseBase{
public CommonResponse(int code, String message, T body) {
this.code = code;
this.message = message;
this.body = body;
}
public CommonResponse() {
}
protected T body;
public T getBody() {
return body;
}
public void setBody(T body) {
this.body = body;
}
}

View File

@ -0,0 +1,29 @@
package com.navinfo.outdoor.bean;
public class CommonResponseBase {
protected Integer code;
protected String message;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public CommonResponse toLzyResponse() {
CommonResponse lzyResponse = new CommonResponse();
lzyResponse.code = code;
lzyResponse.message = message;
return lzyResponse;
}
}

View File

@ -0,0 +1,72 @@
package com.navinfo.outdoor.bean;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.stream.JsonReader;
import com.readystatesoftware.chuck.internal.support.JsonConvertor;
import java.io.Reader;
import java.lang.reflect.Type;
public class Convert {
private static Gson create() {
return Convert.GsonHolder.gson;
}
private static class GsonHolder {
private static Gson gson = new Gson();
}
public static <T> T fromJson(String json, Class<T> type) throws JsonIOException, JsonSyntaxException {
return create().fromJson(json, type);
}
public static <T> T fromJson(String json, Type type) {
return create().fromJson(json, type);
}
public static <T> T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException {
return create().fromJson(reader, typeOfT);
}
public static <T> T fromJson(Reader json, Class<T> classOfT) throws JsonSyntaxException, JsonIOException {
return create().fromJson(json, classOfT);
}
public static <T> T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyntaxException {
return create().fromJson(json, typeOfT);
}
public static String toJson(Object src) {
return create().toJson(src);
}
public static String toJson(Object src, Type typeOfSrc) {
return create().toJson(src, typeOfSrc);
}
public static String formatJson(String json) {
try {
JsonParser jp = new JsonParser();
JsonElement je = jp.parse(json);
return JsonConvertor.getInstance().toJson(je);
} catch (Exception e) {
return json;
}
}
public static String formatJson(Object src) {
try {
JsonParser jp = new JsonParser();
JsonElement je = jp.parse(toJson(src));
return JsonConvertor.getInstance().toJson(je);
} catch (Exception e) {
return e.getMessage();
}
}
}

View File

@ -0,0 +1,128 @@
package com.navinfo.outdoor.bean;
import com.google.gson.stream.JsonReader;
import com.lzy.okgo.callback.AbsCallback;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import okhttp3.Response;
import okhttp3.ResponseBody;
public abstract class MyJsonCallback<T> extends AbsCallback<T> {
private Type type;
private Class<T> clazz;
public MyJsonCallback() {
}
public MyJsonCallback(Type type) {
this.type = type;
}
public MyJsonCallback(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public T convertResponse(Response response) throws Throwable {
if (type == null) {
if (clazz == null) {
// 如果没有通过构造函数传进来就自动解析父类泛型的真实类型有局限性继承后就无法解析到
Type genType = getClass().getGenericSuperclass();
type = ((ParameterizedType) genType).getActualTypeArguments()[0];
} else {
return parseClass(response, clazz);
}
}
if (type instanceof ParameterizedType) {
return parseParameterizedType(response, (ParameterizedType) type);
} else if (type instanceof Class) {
return parseClass(response, (Class<?>) type);
} else {
return parseType(response, type);
}
}
private T parseClass(Response response, Class<?> rawType) throws Exception {
if (rawType == null) return null;
ResponseBody body = response.body();
if (body == null) return null;
JsonReader jsonReader = new JsonReader(body.charStream());
if (rawType == String.class) {
//noinspection unchecked
return (T) body.string();
} else if (rawType == JSONObject.class) {
//noinspection unchecked
return (T) new JSONObject(body.string());
} else if (rawType == JSONArray.class) {
//noinspection unchecked
return (T) new JSONArray(body.string());
} else {
T t = Convert.fromJson(jsonReader, rawType);
response.close();
return t;
}
}
private T parseType(Response response, Type type) throws Exception {
if (type == null) return null;
ResponseBody body = response.body();
if (body == null) return null;
JsonReader jsonReader = new JsonReader(body.charStream());
// 泛型格式如下 new JsonCallback<任意JavaBean>(this)
T t = Convert.fromJson(jsonReader, type);
response.close();
return t;
}
private T parseParameterizedType(Response response, ParameterizedType type) throws Exception {
if (type == null) return null;
ResponseBody body = response.body();
if (body == null) return null;
JsonReader jsonReader = new JsonReader(body.charStream());
Type rawType = type.getRawType(); // 泛型的实际类型
Type typeArgument = type.getActualTypeArguments()[0]; // 泛型的参数
if (rawType != CommonResponse.class) {
// 泛型格式如下 new JsonCallback<外层BaseBean<内层JavaBean>>(this)
T t = Convert.fromJson(jsonReader, type);
response.close();
return t;
} else {
if (typeArgument == Void.class) {
// 泛型格式如下 new JsonCallback<LzyResponse<Void>>(this)
CommonResponseBase simpleResponse = Convert.fromJson(jsonReader, CommonResponseBase.class);
response.close();
//noinspection unchecked
return (T) simpleResponse.toLzyResponse();
} else {
// 泛型格式如下 new JsonCallback<LzyResponse<内层JavaBean>>(this)
CommonResponse lzyResponse = Convert.fromJson(jsonReader, type);
response.close();
int code = lzyResponse.code;
// 一般来说服务器会和客户端约定一个数表示成功如200其余的表示失败如400,300等这里根据实际情况罗列并抛出
if (code == 0) {
//noinspection unchecked
return (T) lzyResponse;
} else if (code == 230) {
throw new IllegalStateException("Token已过期");
} /*else if (code == 300) {
throw new IllegalStateException("用户名密码错误");
} */else {
// 直接将服务端的错误信息抛出onError中可以获取
throw new IllegalStateException("错误代码:" + code + ",错误信息:" + lzyResponse.message);
}
}
}
}
}

View File

@ -52,6 +52,7 @@ import com.navinfo.outdoor.room.PoiDao;
import com.navinfo.outdoor.room.PoiDatabase;
import com.navinfo.outdoor.room.PoiEntity;
import com.navinfo.outdoor.util.AWMp4ParserHelper;
import com.navinfo.outdoor.util.DataSaveUtils;
import com.navinfo.outdoor.util.FlushTokenUtil;
import com.navinfo.outdoor.util.Geohash;
import com.navinfo.outdoor.util.GeometryTools;
@ -507,42 +508,62 @@ public class PoiVideoFragment extends BaseDrawerFragment implements View.OnClick
ToastUtils.Message(getActivity(), "本地不存在照片文件,无法上传数据,请确认!");
return;
}
fileZip = new File(Constant.PICTURE_FOLDER, "files" + ".zip");
new Thread(new Runnable() {
DataSaveUtils.getInstance().uploadFiles(getActivity(), showPoiEntity, videoFileList, new DataSaveUtils.UploadCallback() {
@Override
public void run() {
ZipUtil.zipFiles(videoFileList, fileZip, null);
long zipTrueSize = ZipUtils.getZipTrueSize(fileZip.getAbsolutePath());
if (zipTrueSize > 0) {
if (getActivity() != null) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (showPoiEntity == null) {
showPoiEntity = new PoiEntity();
}
if (showPoiEntity.getTaskStatus() == 0 || showPoiEntity.getTaskStatus() == 1 || showPoiEntity.getTaskStatus() == 2 || showPoiEntity.getTaskStatus() == 5) {
initPoiSaveLocal(true);
} else {
poiVideoUpload(showPoiEntity.getBodyId(), fileZip);
Constant.isPresent = false;
}
}
});
}
} else {
if (getActivity() != null) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
fileZip.delete();
ToastUtils.Message(getActivity(), "压缩文件失败,请重新提交");
}
});
}
}
public void onStart() {
dismissLoadingDialog();
Constant.isPresent = false;
getActivity().getSupportFragmentManager().popBackStack();//回退
WaitDialog.show((AppCompatActivity) getActivity(), "任务正在后台上传中,请稍候...");
WaitDialog.dismiss(3000);
}
}).start();
@Override
public void onFinish() {
Constant.isPresent = true;
}
@Override
public void onError() {
}
});
// fileZip = new File(Constant.PICTURE_FOLDER, "files" + ".zip");
// new Thread(new Runnable() {
// @Override
// public void run() {
// ZipUtil.zipFiles(videoFileList, fileZip, null);
// long zipTrueSize = ZipUtils.getZipTrueSize(fileZip.getAbsolutePath());
// if (zipTrueSize > 0) {
// if (getActivity() != null) {
// getActivity().runOnUiThread(new Runnable() {
// @Override
// public void run() {
// if (showPoiEntity == null) {
// showPoiEntity = new PoiEntity();
// }
// if (showPoiEntity.getTaskStatus() == 0 || showPoiEntity.getTaskStatus() == 1 || showPoiEntity.getTaskStatus() == 2 || showPoiEntity.getTaskStatus() == 5) {
// initPoiSaveLocal(true);
// } else {
// poiVideoUpload(showPoiEntity.getBodyId(), fileZip);
// Constant.isPresent = false;
// }
// }
// });
// }
// } else {
// if (getActivity() != null) {
// getActivity().runOnUiThread(new Runnable() {
// @Override
// public void run() {
// fileZip.delete();
// ToastUtils.Message(getActivity(), "压缩文件失败,请重新提交");
// }
// });
// }
// }
// }
// }).start();
} else {
dismissLoadingDialog();
ToastUtils.Message(getActivity(), "请录像");

View File

@ -44,6 +44,7 @@ import com.navinfo.outdoor.activity.FragmentManagement;
import com.navinfo.outdoor.activity.PicturesActivity;
import com.navinfo.outdoor.api.Constant;
import com.navinfo.outdoor.base.BaseDrawerFragment;
import com.navinfo.outdoor.util.DataSaveUtils;
import com.navinfo.outdoor.util.FlushTokenUtil;
import com.navinfo.outdoor.util.PoiSaveUtils;
import com.navinfo.outdoor.util.PreserveUtils;
@ -584,42 +585,62 @@ public class RoadFragment extends BaseDrawerFragment implements View.OnClickList
ToastUtils.Message(getActivity(), "本地不存在照片文件,无法上传数据,请确认!");
return;
}
fileZip = new File(Constant.PICTURE_FOLDER, "files" + ".zip");
new Thread(new Runnable() {
DataSaveUtils.getInstance().uploadFiles(getActivity(), showPoiEntity, videoFileList, new DataSaveUtils.UploadCallback() {
@Override
public void run() {
ZipUtil.zipFiles(videoFileList, fileZip, null);//压缩
long zipTrueSize = ZipUtils.getZipTrueSize(fileZip.getAbsolutePath());
if (zipTrueSize > 0) {
if (getActivity() != null) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (showPoiEntity == null) {
showPoiEntity = new PoiEntity();
}
if (showPoiEntity.getTaskStatus() == 1 || showPoiEntity.getTaskStatus() == 2 || showPoiEntity.getTaskStatus() == 0 || showPoiEntity.getTaskStatus() == 5) {
initPoiSaveLocal(true);
} else {
poiVideoUpload(showPoiEntity.getBodyId(), fileZip);
Constant.isPresent = false;
}
}
});
}
} else {
if (getActivity() != null) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
fileZip.delete();
ToastUtils.Message(getActivity(), "压缩文件失败,请重新提交");
}
});
}
}
public void onStart() {
dismissLoadingDialog();
Constant.isPresent = false;
getActivity().getSupportFragmentManager().popBackStack();//回退
WaitDialog.show((AppCompatActivity) getActivity(), "任务正在后台上传中,请稍候...");
WaitDialog.dismiss(3000);
}
}).start();
@Override
public void onFinish() {
Constant.isPresent = true;
}
@Override
public void onError() {
}
});
// fileZip = new File(Constant.PICTURE_FOLDER, "files" + ".zip");
// new Thread(new Runnable() {
// @Override
// public void run() {
// ZipUtil.zipFiles(videoFileList, fileZip, null);//压缩
// long zipTrueSize = ZipUtils.getZipTrueSize(fileZip.getAbsolutePath());
// if (zipTrueSize > 0) {
// if (getActivity() != null) {
// getActivity().runOnUiThread(new Runnable() {
// @Override
// public void run() {
// if (showPoiEntity == null) {
// showPoiEntity = new PoiEntity();
// }
// if (showPoiEntity.getTaskStatus() == 1 || showPoiEntity.getTaskStatus() == 2 || showPoiEntity.getTaskStatus() == 0 || showPoiEntity.getTaskStatus() == 5) {
// initPoiSaveLocal(true);
// } else {
// poiVideoUpload(showPoiEntity.getBodyId(), fileZip);
// Constant.isPresent = false;
// }
// }
// });
// }
// } else {
// if (getActivity() != null) {
// getActivity().runOnUiThread(new Runnable() {
// @Override
// public void run() {
// fileZip.delete();
// ToastUtils.Message(getActivity(), "压缩文件失败,请重新提交");
// }
// });
// }
// }
// }
// }).start();
} else {
dismissLoadingDialog();
ToastUtils.Message(getActivity(), "请录像");

View File

@ -12,6 +12,10 @@ public abstract class DialogCallback<T> extends JsonCallback<T> {
@Override
public void onSuccess(Response<T> response) { }
public DialogCallback() {
super();
}
public DialogCallback( Class<T> tClass) {
super(tClass);
}

View File

@ -1,8 +1,9 @@
package com.navinfo.outdoor.http;
public class HttpInterface {
public static final String IP = "http://172.23.138.133:9999/m4";//测试接口
public static final String IP2 = "http://dtxbmaps.navinfo.com/dtxb/dev/m4";//测试接口-外网
// public static final String IP = "http://172.23.138.133:9999/m4";//测试接口-IP
public static final String IPm = "http://dtxbmaps.navinfo.com/dtxb/dev/m4";//开发接口-外网
public static final String IP = "http://dtxbmaps.navinfo.com/dtxb/test/m4";//测试接口-外网
public static final String IP1 = "http://dtxbmaps.navinfo.com/dtxb/m4";//正式接口
public static final String USER_PATH = "/user/";//我的
public static final String MSG_LIST_PATH = "/msgList/";//发现
@ -136,6 +137,9 @@ public class HttpInterface {
//dtxbmaps.navinfo.com/dtxb_test/m4/msgList/InfoPush/28/push?type=0
public static String MESSAGE_INFO_PUSH = null;//消息通知
public static String UPDATE_PHONE_NUM_URL = null;//消息通知
public static String CREATE_UPLOAD_TASK = null;//创建断点续传任务
public static String UPLOAD_SPLITE_TASK = null;//执行断点续传
public static String UPLOAD_TASK_FINISH = null;//断点续传完成
public static String CONTACT_US = "";//联系我们
public static String ABOUT_MAP = "";//关于 -关于地图寻宝
@ -237,5 +241,8 @@ public class HttpInterface {
COMPLETE = IP + TASK_PATH + "polygonTask/" + userId + "/complete";//面状任务结束领取
SUBMIT_POLYGON_TASK = IP + TASK_PATH + "polygonTask/" + userId + "/submitPolygontask";//面状任务开始采集
UPDATE_PHONE_NUM_URL = IP + UPDATE_PHONE_NUM_PATH.replace("{userId}", userId);// 修改手机号
CREATE_UPLOAD_TASK = IP + TASK_PATH + "task/"+ userId+"/createUploadTask";// 创建断点续传任务
UPLOAD_SPLITE_TASK = IP + TASK_PATH + "task/"+ userId+"/uploadSplitTask";// 执行断点续传
UPLOAD_TASK_FINISH = IP + TASK_PATH + "task/"+ userId+"/uploadTaskFinish";// 断点续传完成
}
}

View File

@ -0,0 +1,339 @@
package com.navinfo.outdoor.util;
import android.app.Activity;
import android.content.Context;
import android.os.Message;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import com.github.lazylibrary.util.FileUtils;
import com.github.lazylibrary.util.MD5;
import com.github.lazylibrary.util.ZipUtil;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import com.kongzue.dialog.v3.TipDialog;
import com.kongzue.dialog.v3.WaitDialog;
import com.lzy.okgo.OkGo;
import com.lzy.okgo.model.HttpParams;
import com.lzy.okgo.model.Response;
import com.navinfo.outdoor.api.Constant;
import com.navinfo.outdoor.api.UserApplication;
import com.navinfo.outdoor.bean.CommonRequestSend;
import com.navinfo.outdoor.bean.CommonResponse;
import com.navinfo.outdoor.bean.PoiSaveBean;
import com.navinfo.outdoor.http.Callback;
import com.navinfo.outdoor.http.DialogCallback;
import com.navinfo.outdoor.http.HttpInterface;
import com.navinfo.outdoor.http.OkGoBuilder;
import com.navinfo.outdoor.room.InsertAndUpdateUtils;
import com.navinfo.outdoor.room.PoiDatabase;
import com.navinfo.outdoor.room.PoiEntity;
import com.umeng.commonsdk.debug.UMLog;
import com.umeng.umcrash.UMCrash;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.ObservableSource;
import io.reactivex.Observer;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
public class DataSaveUtils {
private static DataSaveUtils instance;
public static DataSaveUtils getInstance() {
if (instance == null) {
instance = new DataSaveUtils();
}
return instance;
}
public interface UploadCallback {
public void onStart();
public void onFinish();
public void onError();
}
// 批量上传文件
public void uploadFiles(Activity mContext, PoiEntity poiEntity, List<File> poiPicList, UploadCallback callback) {
int auditId = poiEntity.getBodyId();
File zipFile = new File(poiPicList.get(0).getParentFile(), auditId+".zip");
Observable.create(new ObservableOnSubscribe<File>() {
@Override
public void subscribe(ObservableEmitter<File> emitter) throws Exception {
if (!zipFile.exists()) {
// 开始压缩文件
ZipUtil.zipFiles(poiPicList, zipFile, "", null);
}
emitter.onNext(zipFile);
emitter.onComplete();
}
})
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
// 切分数据发送开始上传请求
.map(new Function<File, Map<Integer, File>>() {
@Override
public Map<Integer, File> apply(File file) throws Exception {
// 每次执行上传都会切分数据因为都会执行第一步向服务器请求需要上传的分包数据
List<File> splitFiles = FileSpliteMergeUtils.splitFile(file, Constant.DEFAULT_CUT_SIZE);
List<Long> chunkSizeList = new ArrayList<>();
for (File f: splitFiles) {
chunkSizeList.add(f.length());
}
CommonResponse<String> response = createUploadTask(mContext, auditId, file.length(), chunkSizeList);
if (response!=null) {
// 请求成功获取需要上传的分包index
String body = response.getBody();
if (response.getCode() == 213) { // 该数据已经上传成功自动切换该数据为已上传状态
poiEntity.setTaskStatus(100);
PoiDatabase.getInstance(mContext).getPoiDao().updatePoiEntity(poiEntity);
ToastUtils.Message(mContext, "数据:"+poiEntity.getName()+"已成功上传!");
}
if (body==null||body.isEmpty()) {
throw new Exception(response.getMessage());
}
String[] needUploadStrIndex = body.split(",");
// 批量转换String为int
int[] needUploadIndex = new int[needUploadStrIndex.length];
for (int i = 0; i < needUploadStrIndex.length; i++) {
needUploadIndex[i] = Integer.parseInt(needUploadStrIndex[i]);
}
// 根据返回的index将需要上传的切分包数据流转到下一个流程
Map<Integer, File> needUploadFileMap = new HashMap<>();
for (int index: needUploadIndex) {
if (index<splitFiles.size()) {
needUploadFileMap.put(index, splitFiles.get(index));
}
}
// 遍历已存在的切割文件如果不需要上传则删除该分包数据
for (int i = 0; i < splitFiles.size(); i++) {
if (!needUploadFileMap.keySet().contains(i)) {
splitFiles.get(i).delete();
}
}
return needUploadFileMap;
} else {
return new HashMap<>();
}
}
})
.flatMap(new Function<Map<Integer, File>, ObservableSource<Map.Entry<Integer, File>>>() {
@Override
public ObservableSource<Map.Entry<Integer, File>> apply(Map<Integer, File> filesMap) throws Exception {
// 将每个切分的文件file作为发送者重新发送出去
return Observable.fromIterable(filesMap.entrySet());
}
})
.doOnNext(new Consumer<Map.Entry<Integer, File>>() {
@Override
public void accept(Map.Entry<Integer, File> fileEntry) throws Exception {
// 执行上传流程
CommonResponse response = uploadSplitTask(mContext, auditId, fileEntry.getKey(), fileEntry.getValue());
if (response.getCode() != 200) {
throw new Exception(response.getMessage());
} else {
UMLog.mutlInfo(3, "已上传分包"+fileEntry.getKey());
// 上传成功则删除该分包数据
if (fileEntry.getValue().exists()) {
fileEntry.getValue().delete();
}
}
}
})
.observeOn(AndroidSchedulers.mainThread())
.toList()
.observeOn(Schedulers.io())
// 调用执行成功的接口
.doOnSuccess(new Consumer<List<Map.Entry<Integer, File>>>() {
@Override
public void accept(List<Map.Entry<Integer, File>> entries) throws Exception {
// 最终成功调用finish接口
CommonResponse response = uploadTaskFinish(mContext, auditId);
Message obtain1 = Message.obtain();
obtain1.what = Constant.NEST_WORD_SUBMIT;
if (response.getCode() == 200) { // 更新成功再次更新本地数据库
ToastUtils.Message(mContext, "分包数据上传完成!");
poiEntity.setTaskStatus(100);
PoiDatabase.getInstance(mContext).getPoiDao().updatePoiEntity(poiEntity);
// 提醒用户数据上传完成
obtain1.obj = "数据:" + poiEntity.getName() + " 上传成功";
// 同时删除关联的照片文件和压缩包文件
if (zipFile.exists()) {
zipFile.delete();
}
if (poiPicList!=null&&!poiPicList.isEmpty()) {
for (File picFile: poiPicList
) {
picFile.delete();
}
}
} else {
obtain1.obj = "数据:" + poiEntity.getName() + " 上传失败";
}
EventBus.getDefault().post(obtain1);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<List<Map.Entry<Integer, File>>>() {
@Override
public void onSubscribe(Disposable d) {
callback.onStart();
}
@Override
public void onSuccess(List<Map.Entry<Integer, File>> entries) {
callback.onFinish();
}
@Override
public void onError(Throwable e) {
ToastUtils.Message(mContext, e.getMessage());
callback.onError();
callback.onFinish();
}
});
}
/**
* 创建分包上传任务
* */
private CommonResponse createUploadTask(Activity mContext, int auditId, long fileSize, List<Long> chunkSize) {
HttpParams httpParams = new HttpParams();
httpParams.put("auditId", auditId);
httpParams.put("fileSize", fileSize);
httpParams.put("chunkSize", list2Str(chunkSize));
CommonRequestSend commonRequestSend = new CommonRequestSend<CommonResponse<String>>();
CommonResponse response = commonRequestSend.getMethodCommonSync(mContext, HttpInterface.CREATE_UPLOAD_TASK, httpParams, Constant.DEFAULT_TIME_OUT, new CommonResponse<String>().getClass());
if (response!=null) {
if (response.getCode() == 200) {
return response;
} else {
ToastUtils.Message(mContext, response.getMessage());
}
return response;
}
return null;
}
/**
* 上传分包数据
* */
private CommonResponse uploadSplitTask(Activity mContext, int auditId, int chunkIndex, File splitFile) {
HttpParams httpParams = new HttpParams();
httpParams.put("auditId", auditId);
httpParams.put("chunkIndex", chunkIndex);
httpParams.put("md5", MD5.md5sum(splitFile.getAbsolutePath()).toLowerCase());
httpParams.put("file", splitFile);
// List<File> fileList = new ArrayList<>();
// fileList.add(splitFile);
CommonRequestSend commonRequestSend = new CommonRequestSend<CommonResponse<String>>();
CommonResponse response = commonRequestSend.postMethodCommonSync(mContext, HttpInterface.UPLOAD_SPLITE_TASK, httpParams, Constant.DEFAULT_TIME_OUT, null, new CommonResponse<String>().getClass());
if (response!=null) {
if (response.getCode() == 200) {
return response;
} else {
ToastUtils.Message(mContext, response.getMessage());
}
return response;
}
return null;
}
/**
* 分包数据上传完成
* */
private CommonResponse uploadTaskFinish(Activity mContext, int auditId) {
HttpParams httpParams = new HttpParams();
httpParams.put("auditId", auditId);
CommonRequestSend commonRequestSend = new CommonRequestSend<CommonResponse<String>>();
CommonResponse response = commonRequestSend.getMethodCommonSync(mContext, HttpInterface.UPLOAD_TASK_FINISH, httpParams, Constant.DEFAULT_TIME_OUT, new CommonResponse<String>().getClass());
if (response!=null) {
if (response.getCode() == 200) {
return response;
} else {
ToastUtils.Message(mContext, response.getMessage());
}
return response;
}
return null;
}
public Observable savePoiEntity(Context mContext, PoiEntity poiEntity) {
return getPermission(mContext)
.subscribeOn(Schedulers.io())
.map(new Function() {
@Override
public PoiEntity apply(Object o) {
return poiEntity;
}
})
// 判断当前POI状态根据状态处理随后的流程
.map(new Function<PoiEntity, PoiEntity>() {
@Override
public PoiEntity apply(PoiEntity entity) {
return null;
}
});
}
public Observable getPermission(Context mContext) {
return Observable.create(
new ObservableOnSubscribe<Object>() {
@Override
public void subscribe(ObservableEmitter<Object> emitter) throws Exception {
XXPermissions.with(mContext)
//读写权限
.permission(Permission.MANAGE_EXTERNAL_STORAGE)
.request(new OnPermissionCallback() {
@Override
public void onGranted(List<String> permissions, boolean all) {
if (all) {
emitter.onNext("permission Ok");
} else {
emitter.onError(new Throwable("未获取文件访问权限,请确认!"));
}
}
@Override
public void onDenied(List<String> permissions, boolean never) {
if (never) {
emitter.onError(new Throwable("未获取文件访问权限,请确认!"));
}
}
});
}
}
);
}
private String list2Str(List chunkSize) {
StringBuilder result = new StringBuilder("");
for (Object chunk: chunkSize) {
result.append(chunk).append(",");
}
if (result.length()>0) {
return result.substring(0, result.length()-1);
}
return result.toString();
}
}

View File

@ -0,0 +1,150 @@
package com.navinfo.outdoor.util
import android.util.Log
import com.navinfo.outdoor.api.Constant
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.RandomAccessFile
import java.lang.Exception
/**
* @Author: LiaoZhongKai
* @Date: 2021/7/28 19:42
* @Description:
*/
object FileSpliteMergeUtils {
const val TAG = "FileUtils"
//默认切割文件的大小
// private const val DEFAULT_CUT_SIZE: Long = 5*1024*1024//5MB
private const val DEFAULT_CUT_SIZE: Long = Constant.DEFAULT_CUT_SIZE//5MB
/**
* 分割文件
* [sourceFile] 需要分割的源文件
* [cutSize] 每个文件的大小
* @return 分段文件集合
*/
@JvmStatic
fun splitFile(sourceFile: File,cutSize: Long = DEFAULT_CUT_SIZE): List<File>{
//分段片文件集合
val singleFileList = mutableListOf<File>()
val totalLength = sourceFile.length()
//分段数量
val count = if(totalLength % cutSize == 0L) totalLength/cutSize else totalLength/cutSize +1
Log.d(TAG,"split file count:$count")
val mRandomAccFile = RandomAccessFile(sourceFile,"r")
try {
// val length = mRandomAccFile.length()
// val singleSize = length/count//源文件分割后每个文件的大小
var offSet = 0L
for (i in 0 until count){//最后一个文件单独处理因为它的大小可能不等于singleSize
val begin = offSet
// 如果当前index对应的数据没有超过文件大小则使用cutSize否则使用文件实际剩余大小
var end = begin + cutSize
if (totalLength<end) {
end = totalLength;
}
val file = createSingleFile(sourceFile,i)
offSet = writeFile(mRandomAccFile,file,begin,end)
singleFileList.add(file)
}
// if (length - offSet > 0){//最后一个文件
// val file = createSingleFile(sourceFile,count-1)
// writeFile(mRandomAccFile,file,offSet,length)
// singleFileList.add(file)
// }
}catch (e: FileNotFoundException){
e.printStackTrace()
}catch (e: IOException){
e.printStackTrace()
}finally {
try {
mRandomAccFile.close()
}catch (e: IOException){
e.printStackTrace()
}
}
return singleFileList
}
private fun createSingleFile(sourceFile: File,index: Long): File{
val path = sourceFile.absolutePath.substringBeforeLast(".")
// val suffix = sourceFile.name.substringAfterLast(".")
val suffix = "tmp";
val file = File("${path}_${index}.${suffix}")
if (file.exists()){
file.delete()
}
Log.d(TAG,"sourceFilePath:${sourceFile.absolutePath}")
file.createNewFile()
Log.d(TAG,"single file path:${file.absolutePath}")
return file
}
private fun writeFile(inFile: RandomAccessFile,single: File,begin: Long,end: Long): Long{
var endPointer = 0L
val out = RandomAccessFile(single,"rw")
try {
val byte = ByteArray(1024)
var index = 0
inFile.seek(begin)
while (inFile.read(byte).also { index = it } != -1 && inFile.filePointer <= end){
out.write(byte,0,index)
}
endPointer = inFile.filePointer
}catch (e: Exception){
e.printStackTrace()
}finally {
out.close()
}
return endPointer - 1024//减1024是为了避免分割文件时丢包
}
/**
* 合并文件
* [files] 需要合并的文件集合
* [outputFilePath] 输出文件的路径 eg:D:/test
* [outputFileName] 输出文件的名称 eg:crawler.json
*/
@JvmStatic
fun mergeFile(files: List<File>,outputFilePath: String,outputFileName: String){
val parentFile = File(outputFilePath)
val targetFile = File(outputFilePath+File.separator+outputFileName)
if (!parentFile.exists()){
parentFile.mkdirs()
}
if (!targetFile.exists()){
targetFile.createNewFile()
}
val outRaf = RandomAccessFile(targetFile,"rw")
try {
files.forEach { file ->
val reader = RandomAccessFile(file,"r")
val byte = ByteArray(1024)
var index = 0
while (reader.read(byte).also { index = it } != -1){
outRaf.write(byte,0,index)
}
}
files.forEach {
if (it.exists()){
it.delete()
}
}
Log.d(TAG,"merge output file path:${targetFile.absolutePath}")
}catch (e: IOException){
e.printStackTrace()
}catch (e: FileNotFoundException){
e.printStackTrace()
}
finally {
outRaf.close()
}
}
}

View File

@ -21,6 +21,7 @@ import com.navinfo.outdoor.room.ChargingPileEntity;
import com.navinfo.outdoor.room.InsertAndUpdateUtils;
import com.navinfo.outdoor.room.PoiDatabase;
import com.navinfo.outdoor.room.PoiEntity;
import com.umeng.commonsdk.debug.UMLog;
import org.greenrobot.eventbus.EventBus;
@ -256,24 +257,42 @@ public class PoiSaveUtils {
bInt++;
return;
}
File fileZip = new File(Constant.PICTURE_FOLDER, "files" + ".zip");
ZipUtil.zipFiles(videoFileList, fileZip, null);
photoFile.add(fileZip);
// 需要上传的文件都存在开始调用分片上传接口上传附件数据
DataSaveUtils.getInstance().uploadFiles(mContext, poiEntity, videoFileList, new DataSaveUtils.UploadCallback() {
@Override
public void onStart() {
}
@Override
public void onFinish() {
anInt++;
}
@Override
public void onError() {
// 上传失败
bInt++;
}
});
// File fileZip = new File(Constant.PICTURE_FOLDER, "files" + ".zip");
// ZipUtil.zipFiles(videoFileList, fileZip, null);
// photoFile.add(fileZip);
} else {
bInt++;
return;
}
if (photoFile.size() > 0) {
long zipTrueSize = ZipUtils.getZipTrueSize(photoFile.get(0).getAbsolutePath());
if (zipTrueSize > 0) {
initList(HttpInterface.POI_VIDEO_UPLOAD_PIC, photoFile, poiEntity);
} else {
for (int i = 0; i < photoFile.size(); i++) {
photoFile.get(i).delete();
}
bInt++;
}
}
// if (photoFile.size() > 0) {
// long zipTrueSize = ZipUtils.getZipTrueSize(photoFile.get(0).getAbsolutePath());
// if (zipTrueSize > 0) {
// initList(HttpInterface.POI_VIDEO_UPLOAD_PIC, photoFile, poiEntity);
// } else {
// for (int i = 0; i < photoFile.size(); i++) {
// photoFile.get(i).delete();
// }
// bInt++;
// }
// }
} else if (poiEntity.getType() == 4) {
List<File> videoFileList = AWMp4ParserHelper.getInstance().getFileListByUUID(poiEntity.getId());
if (videoFileList != null && !videoFileList.isEmpty()) {
@ -282,27 +301,48 @@ public class PoiSaveUtils {
bInt++;
return;
}
File fileZip = new File(Constant.PICTURE_FOLDER, "files" + ".zip");
ZipUtil.zipFiles(videoFileList, fileZip, null);
photoFile.add(fileZip);
// 需要上传的文件都存在开始调用分片上传接口上传附件数据
DataSaveUtils.getInstance().uploadFiles(mContext, poiEntity, videoFileList, new DataSaveUtils.UploadCallback() {
@Override
public void onStart() {
}
@Override
public void onFinish() {
anInt++;
Log.d("TAGSS", "uploadPoiNet: 成功" + anInt);
}
@Override
public void onError() {
// 上传失败
bInt++;
Log.d("TAGSS", poiEntity.getBodyId()+"uploadPoiNet: 失败" + bInt);
UMLog.aq(1, poiEntity.getBodyId()+"uploadPoiNet: 失败", "文件上传失败");
}
});
// File fileZip = new File(Constant.PICTURE_FOLDER, "files" + ".zip");
// ZipUtil.zipFiles(videoFileList, fileZip, null);
// photoFile.add(fileZip);
} else {
Log.d("TAGSS", "videoFileList: 失败" + bInt);
bInt++;
return;
}
if (photoFile.size() > 0) {
long zipTrueSize = ZipUtils.getZipTrueSize(photoFile.get(0).getAbsolutePath());
if (zipTrueSize > 0) {
initList(HttpInterface.ROAD_TASK_UPLOAD_PIC, photoFile, poiEntity);
} else {
Log.d("TAGSS", "photoFile: 失败" + bInt);
for (int i = 0; i < photoFile.size(); i++) {
photoFile.get(i).delete();
}
bInt++;
return;
}
}
// if (photoFile.size() > 0) {
// long zipTrueSize = ZipUtils.getZipTrueSize(photoFile.get(0).getAbsolutePath());
// if (zipTrueSize > 0) {
// initList(HttpInterface.ROAD_TASK_UPLOAD_PIC, photoFile, poiEntity);
// } else {
// Log.d("TAGSS", "photoFile: 失败" + bInt);
// for (int i = 0; i < photoFile.size(); i++) {
// photoFile.get(i).delete();
// }
// bInt++;
// return;
// }
// }
} else if (poiEntity.getType() == 5) {
initList(HttpInterface.OTHER_TASK_UPLOAD_PIC, photoFile, poiEntity);
}

View File

@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.anko_version = '0.10.1'//
ext.kotlin_version = '1.5.10'
repositories {
google()
jcenter()
@ -19,8 +20,8 @@ buildscript {
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.0"
//kotlin支持
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files