From 546cf3cc5df3fd55e10672e474c246582b947689 Mon Sep 17 00:00:00 2001 From: Runt <qingingrunt2010@qq.com> Date: Tue, 05 Aug 2025 02:30:23 +0000 Subject: [PATCH] 网络请求加密 网络请求优化 弹窗优化 --- libmvi/src/main/java/com/runt/open/mvi/views/PublicViews.kt | 14 libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/EncryptInterceptor.java | 20 + libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/HttpLoggingInterceptor.java | 42 -- libmvi/build.gradle.kts | 4 libmvi/src/main/java/com/runt/open/mvi/data/PhoneDevice.java | 4 libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RetrofitUtils.java | 175 ++++++++-- /dev/null | 41 -- libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/GsonConverterFactory.java | 2 libmvi/src/main/java/com/runt/open/mvi/utils/DeviceIdUtils.java | 2 libmvi/src/main/java/com/runt/open/mvi/utils/DeviceUtil.java | 52 --- libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/DecryptGsonResponseBodyConverter.java | 98 ++++++ libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/LogEncryptInterceptor.java | 19 + libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/OpenInterceptor.java | 117 +++++++ libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/EncryptGsonRequestBodyConverter.java | 43 ++ libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/DecryptGsonConverterFactory.java | 58 +++ libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RSAUtils.java | 180 ++++++++++++ 16 files changed, 697 insertions(+), 174 deletions(-) diff --git a/libmvi/build.gradle.kts b/libmvi/build.gradle.kts index 2ff9876..7283dbf 100644 --- a/libmvi/build.gradle.kts +++ b/libmvi/build.gradle.kts @@ -17,12 +17,12 @@ debug { isMinifyEnabled = false - buildConfigField("String","HOST_IP_ADDR","\"http://192.168.28.9:8080/\"") + buildConfigField("String","HOST_IP_ADDR","\"http://192.168.28.89:8080/\"") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt") , "proguard-rules.pro") } release { isMinifyEnabled = false - buildConfigField("String","HOST_IP_ADDR","\"http://192.168.28.9:8080/\"") + buildConfigField("String","HOST_IP_ADDR","\"http://192.168.28.175:8888/advert/\"") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt") , "proguard-rules.pro") } } diff --git a/libmvi/src/main/java/com/runt/open/mvi/data/PhoneDevice.java b/libmvi/src/main/java/com/runt/open/mvi/data/PhoneDevice.java index 456b6fa..01ebede 100644 --- a/libmvi/src/main/java/com/runt/open/mvi/data/PhoneDevice.java +++ b/libmvi/src/main/java/com/runt/open/mvi/data/PhoneDevice.java @@ -4,7 +4,7 @@ import android.os.Build; import com.runt.open.mvi.retrofit.utils.NetWorkUtils; -import com.runt.open.mvi.utils.DeviceUtil; +import com.runt.open.mvi.utils.DeviceIdUtils; /** @@ -20,7 +20,7 @@ static PhoneDevice device; public static void setDevice(Context context) { - device = new PhoneDevice(Build.BRAND,Build.MODEL,Build.VERSION.SDK_INT+"",Build.VERSION.RELEASE, DeviceUtil.getSerialNumber(context), NetWorkUtils.getNetIp()); + device = new PhoneDevice(Build.BRAND,Build.MODEL,Build.VERSION.SDK_INT+"",Build.VERSION.RELEASE, DeviceIdUtils.getDeviceId(context), NetWorkUtils.getNetIp()); } public static PhoneDevice getDevice() { diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/AddHeadersInterceptor.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/AddHeadersInterceptor.java deleted file mode 100644 index c958403..0000000 --- a/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/AddHeadersInterceptor.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.runt.open.mvi.retrofit.Interceptor; - - -import com.google.gson.Gson; -import com.runt.open.mvi.OpenApplication; -import com.runt.open.mvi.data.PhoneDevice; -import com.runt.open.mvi.utils.DeviceUtil; - -import java.io.IOException; -import java.nio.charset.Charset; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -/** - * My father is Object, ites purpose of 添加header拦截器 - * - * @purpose Created by Runt (qingingrunt2010@qq.com) on 2021-10-8. - */ -public class AddHeadersInterceptor implements Interceptor { - - protected final Charset UTF8 = Charset.forName("UTF-8"); - - @Override - public Response intercept(Chain chain) throws IOException { - return chain.proceed(addHeaders(chain.request())); - } - - protected Request addHeaders(Request request){ - Request.Builder requestBuild = request.newBuilder() - .addHeader("device", new Gson().toJson(PhoneDevice.getDevice())) - .addHeader("appVersion", DeviceUtil.getAppVersionName(OpenApplication.Companion.getApplication())) - .addHeader("os", DeviceUtil.isHarmonyOS()? "harmony" : "android"); - /*if(UserBean.getUser() != null){ - requestBuild.addHeader("token", UserBean.getUser().getToken()); - }*/ - return requestBuild.build().newBuilder().build(); - } - -} diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/EncryptInterceptor.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/EncryptInterceptor.java new file mode 100644 index 0000000..57def63 --- /dev/null +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/EncryptInterceptor.java @@ -0,0 +1,20 @@ +package com.runt.open.mvi.retrofit.Interceptor; + +import java.io.IOException; + +import okhttp3.Response; + +/** + * @purpose 加密拦截器 + * @author Runt (qingingrunt2010@qq.com) + * @date 2021-10-8. + */ + +public class EncryptInterceptor extends OpenInterceptor { + + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(encryptRequest(chain.request())); + } + +} diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/HttpLoggingInterceptor.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/HttpLoggingInterceptor.java index 20bb448..72d9512 100644 --- a/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/HttpLoggingInterceptor.java +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/HttpLoggingInterceptor.java @@ -2,13 +2,9 @@ import android.util.Log; -import com.google.gson.Gson; -import com.runt.open.mvi.OpenApplication; -import com.runt.open.mvi.data.PhoneDevice; import com.runt.open.mvi.retrofit.net.NetWorkCost; import com.runt.open.mvi.retrofit.net.NetWorkListener; import com.runt.open.mvi.retrofit.utils.HttpPrintUtils; -import com.runt.open.mvi.utils.DeviceUtil; import org.json.JSONArray; import org.json.JSONException; @@ -40,31 +36,17 @@ * @purpose Created by Runt (qingingrunt2010@qq.com) on 2020-10-21. */ -public class HttpLoggingInterceptor extends AddHeadersInterceptor { +public class HttpLoggingInterceptor extends OpenInterceptor { final String TAG = "HttpLogging"; - - private boolean printLog ; - - public HttpLoggingInterceptor(){ - this(true); - } - public HttpLoggingInterceptor(boolean printLog) { - this.printLog = printLog; - } @Override public Response intercept(Chain chain) throws IOException { Request requestTemp = chain.request(); int hashCode = requestTemp.hashCode(); - if(printLog) { - Log.d(TAG, "hashcode:" + hashCode); - } - Request.Builder requestBuild = requestTemp.newBuilder() - .addHeader("device", new Gson().toJson(PhoneDevice.getDevice())) - .addHeader("appVersion", DeviceUtil.getAppVersionName(OpenApplication.Companion.getApplication())) - .addHeader("os", DeviceUtil.isHarmonyOS()? "harmony" : "android"); + Log.d(TAG, "hashcode:" + hashCode); + Request.Builder requestBuild = addHeaders(requestTemp); /*if(UserBean.getUser() != null){ requestBuild.addHeader("token",UserBean.getUser().getToken()); }*/ @@ -74,7 +56,7 @@ try { logArrays.addAll(getRequestLog(request)); int position = logArrays.size() +2; - response = chain.proceed(request); + response = proceed(chain,request); logArrays.addAll(getResponseLog(response)); NetWorkCost netWorkCost = NetWorkListener.workCostMap.get(hashCode); if(netWorkCost != null) { @@ -84,9 +66,7 @@ new Thread(){ @Override public void run() { - if(printLog) { - HttpPrintUtils.getInstance().printLog(logArrays, true);//线程安全方法,需在新线程执行,避免阻塞当前线程,导致程序无响应 - } + HttpPrintUtils.getInstance().printLog(logArrays, true);//线程安全方法,需在新线程执行,避免阻塞当前线程,导致程序无响应 } }.start(); } catch (JSONException e) { @@ -107,9 +87,7 @@ new Thread(){ @Override public void run() { - if(printLog) { - HttpPrintUtils.getInstance().printLog(logArrays, false);//线程安全方法,需在新线程执行,避免阻塞当前线程,导致程序无响应 - } + HttpPrintUtils.getInstance().printLog(logArrays, false);//线程安全方法,需在新线程执行,避免阻塞当前线程,导致程序无响应 } }.start(); throw e;//抛出异常,用于请求接收信息 @@ -117,12 +95,16 @@ return response; } + protected Response proceed(Chain chain,Request request) throws IOException { + return chain.proceed(request); + } + /** * 请求数据信息 * @param request * @return */ - private ArrayList<String> getRequestLog(Request request) throws IOException, JSONException { + protected ArrayList<String> getRequestLog(Request request) throws IOException, JSONException { RequestBody requestBody = request.body(); ArrayList<String> logArrays = new ArrayList<>(); String requestStartMessage = "--> " + request.method() + ' ' + URLDecoder.decode(request.url().toString() ,"UTF-8")+ ' ' ; @@ -195,7 +177,7 @@ * @throws IOException * @throws JSONException */ - private ArrayList<String> getResponseLog(Response response) throws IOException, JSONException { + protected ArrayList<String> getResponseLog(Response response) throws IOException, JSONException { ArrayList<String> logArrays = new ArrayList<>(); ResponseBody responseBody = response.body(); long contentLength = responseBody.contentLength(); diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/LogEncryptInterceptor.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/LogEncryptInterceptor.java new file mode 100644 index 0000000..589e696 --- /dev/null +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/LogEncryptInterceptor.java @@ -0,0 +1,19 @@ +package com.runt.open.mvi.retrofit.Interceptor; + +import java.io.IOException; + +import okhttp3.Request; +import okhttp3.Response; + +/** + * @author Runt(qingingrunt2010 @ qq.com) + * @purpose + * @date 8/4/25 + */ +public class LogEncryptInterceptor extends HttpLoggingInterceptor{ + + @Override + protected Response proceed(Chain chain, Request request) throws IOException { + return super.proceed(chain, encryptRequest(request)); + } +} diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/OpenInterceptor.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/OpenInterceptor.java new file mode 100644 index 0000000..3af2d03 --- /dev/null +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/Interceptor/OpenInterceptor.java @@ -0,0 +1,117 @@ +package com.runt.open.mvi.retrofit.Interceptor; + + +import com.google.gson.Gson; +import com.runt.open.mvi.OpenApplication; +import com.runt.open.mvi.data.PhoneDevice; +import com.runt.open.mvi.retrofit.utils.RSAUtils; +import com.runt.open.mvi.utils.DeviceUtil; + +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import okhttp3.FormBody; +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okio.Buffer; + +/** + * My father is Object, ites purpose of 添加header拦截器 + * + * @purpose Created by Runt (qingingrunt2010@qq.com) on 2021-10-8. + */ +public class OpenInterceptor implements Interceptor { + + protected final Charset UTF8 = Charset.forName("UTF-8"); + protected final String ENCRYPT = "encrypt"; + + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(addHeaders(chain.request()).build().newBuilder().build()); + } + + protected Request.Builder addHeaders(Request request){ + Request.Builder requestBuild = request.newBuilder() + .addHeader("device", new Gson().toJson(PhoneDevice.getDevice())) + .addHeader("appVersion", DeviceUtil.getAppVersionName(OpenApplication.Companion.getApplication())) + .addHeader("os", DeviceUtil.isHarmonyOS()? "harmony" : "android"); + /*if(UserBean.getUser() != null){ + requestBuild.addHeader("token", UserBean.getUser().getToken()); + }*/ + return requestBuild; + } + + //加密 + protected Request encryptRequest(Request request) throws IOException { + Headers headers = request.headers(); + RequestBody requestBody = request.body(); + Request.Builder builder = addHeaders(request);; + for(int i = 0 ; i < headers.size() ; i ++){ + builder.addHeader(headers.name(i),headers.value(i)); + } + if(requestBody != null){ + Charset charset = UTF8; + MediaType contentType = requestBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + HashMap param = new HashMap(); + if(requestBody instanceof MultipartBody){ + MultipartBody body = (MultipartBody) requestBody; + for(MultipartBody.Part part:body.parts()){ + Buffer buffer1 = new Buffer(); + part.body().writeTo(buffer1); + String str=buffer1.readString(charset).replaceAll("%(?![0-9a-fA-F]{2})","%25"); + param.put(part.headers().get(part.headers().name(0)), URLDecoder.decode(str, "UTF-8")); + } + MultipartBody.Builder mbuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + mbuilder.addFormDataPart(ENCRYPT,encryptParam(param)); + builder.post(mbuilder.build()); + }else if(requestBody instanceof FormBody){ + FormBody body = (FormBody) requestBody; + for(int i = 0 ; i < body.size() ; i ++ ){ + param.put(body.name(i),body.value(i)); + } + FormBody.Builder formBuild = new FormBody.Builder(); + formBuild.add(ENCRYPT,encryptParam(param)); + builder.post(formBuild.build()); + }else{ + Buffer buffer = new Buffer(); + requestBody.writeTo(buffer); + String str = buffer.readString(charset); + String encrypt = encryptJson(str); + param.put(ENCRYPT,encrypt); + builder.post(RequestBody.create(MediaType.parse("application/json;charset=utf-8"), new JSONObject(param).toString())); + } + } + return builder.build(); + } + + /** + * 加密传递的参数 + * @param params + * @return + */ + protected String encryptParam(Map<String, Object> params){ + return encryptJson(new JSONObject(params).toString()); + } + + protected String encryptJson(String json){ + try { + return RSAUtils.encrypt(json,RSAUtils.getPublicKey(RSAUtils.PUBLIC_KEY)); + }catch (Exception e){ + e.printStackTrace(); + return e.getMessage(); + } + } +} diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/DecryptGsonConverterFactory.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/DecryptGsonConverterFactory.java new file mode 100644 index 0000000..2897d0e --- /dev/null +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/DecryptGsonConverterFactory.java @@ -0,0 +1,58 @@ +package com.runt.open.mvi.retrofit.converter; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; + +/** + * @purpose 解密gson转换 + * @author Runt (qingingrunt2010@qq.com) + * @date 2021-7-22. + */ +public class DecryptGsonConverterFactory extends Converter.Factory { + + public static DecryptGsonConverterFactory create() { + return create(new Gson(),false); + } + + public static DecryptGsonConverterFactory create(boolean transHump) { + return create(new Gson(),transHump); + } + + + public static DecryptGsonConverterFactory create(Gson gson,boolean transHump) { + return new DecryptGsonConverterFactory(gson,transHump); + } + + private final Gson gson; + + private final boolean transHump; + + public DecryptGsonConverterFactory(Gson gson, boolean transHump) { + if (gson == null) throw new NullPointerException("gson == null"); + this.gson = gson; + this.transHump = transHump; + } + + @Override + public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, + Retrofit retrofit) { + TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type)); + return new DecryptGsonResponseBodyConverter<>(gson, adapter,transHump); + } + + @Override + public Converter<?, RequestBody> requestBodyConverter(Type type, + Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { + TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type)); + return new EncryptGsonRequestBodyConverter<>(gson, adapter); + } +} diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/DecryptGsonResponseBodyConverter.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/DecryptGsonResponseBodyConverter.java new file mode 100644 index 0000000..44e36e4 --- /dev/null +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/DecryptGsonResponseBodyConverter.java @@ -0,0 +1,98 @@ +package com.runt.open.mvi.retrofit.converter; + +import android.text.TextUtils; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.runt.open.mvi.BuildConfig; +import com.runt.open.mvi.retrofit.utils.RSAUtils; +import com.runt.open.mvi.utils.GsonUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import okhttp3.ResponseBody; +import retrofit2.Converter; + +/** + * @purpose 解密gson转换器 + * @author Runt (qingingrunt2010@qq.com) + * @date 2021-7-22. + */ +public class DecryptGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> { + private final Gson gson; + private final TypeAdapter<T> adapter; + private final Charset UTF_8 = Charset.forName("UTF-8"); + private final boolean transHump;//驼峰转换 + private final String ENCRYPT = "encrypt"; + + public DecryptGsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter, boolean transHump) { + this.gson = gson; + this.adapter = adapter; + this.transHump = transHump; + } + + @Override + public T convert(ResponseBody value) throws IOException { + String response = null; + try { + String val = new String(value.bytes(),UTF_8); + response = decryptJsonStr(val);//解密 + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + response = "{\"code\":412,\"message\":\""+"解密数据出错"+e.getMessage()+"\"}"; + } catch (JSONException e) { + e.printStackTrace(); + response = "{\"code\":414,\"message\":\"非标准json\"}"; + }catch (Exception e){ + e.printStackTrace(); + JsonReader jsonReader = gson.newJsonReader(value.charStream()); + return adapter.read(jsonReader); + } finally { + if(response==null){ + return null; + } + InputStream inputStream = new ByteArrayInputStream(response.getBytes()); + JsonReader jsonReader = gson.newJsonReader(new InputStreamReader(inputStream, UTF_8)); + T result = adapter.read(jsonReader); + if (jsonReader.peek() != JsonToken.END_DOCUMENT) { + throw new JsonIOException("JSON document was not fully consumed."); + } + value.close(); + return result; + } + } + + /** + * 解密json + * @param body + * @return + * @throws Exception + */ + protected String decryptJsonStr(String body) throws Exception { + if(BuildConfig.DEBUG) { + Log.e("Converter", "decryptJsonStr body:" + body); + } + if(TextUtils.isEmpty(body)){ + + }else if(body.indexOf("{") == 0) { + JSONObject json = new JSONObject(body); + body = RSAUtils.decrypt(json.getString(ENCRYPT), RSAUtils.getPublicKey(RSAUtils.PUBLIC_KEY));// + //Log.e("Converter", "decryptJsonStr body:" + body); + } + return transHump? GsonUtils.toHumpJson(body):body; + } + +} diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/EncryptGsonRequestBodyConverter.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/EncryptGsonRequestBodyConverter.java new file mode 100644 index 0000000..03f8b85 --- /dev/null +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/EncryptGsonRequestBodyConverter.java @@ -0,0 +1,43 @@ +package com.runt.open.mvi.retrofit.converter; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.Buffer; +import retrofit2.Converter; + +/** + * @author Runt(qingingrunt2010 @ qq.com) + * @purpose 加密gson转换器 + * @date 8/4/25 + */ +public class EncryptGsonRequestBodyConverter<T> implements Converter<T, RequestBody> { + private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8"); + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private final Gson gson; + private final TypeAdapter<T> adapter; + + EncryptGsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) { + this.gson = gson; + this.adapter = adapter; + } + + @Override + public RequestBody convert(T value) throws IOException { + Buffer buffer = new Buffer(); + Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8); + JsonWriter jsonWriter = gson.newJsonWriter(writer); + adapter.write(jsonWriter, value); + jsonWriter.close(); + return RequestBody.create(MEDIA_TYPE, buffer.readByteString()); + } +} diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/GsonConverterFactory.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/GsonConverterFactory.java index af95142..5097e1f 100644 --- a/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/GsonConverterFactory.java +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/converter/GsonConverterFactory.java @@ -15,7 +15,7 @@ import retrofit2.Retrofit; /** - * My father is Object, ites purpose of 解密gson转换 + * My father is Object, ites purpose of gson转换 * * @purpose Created by Runt (qingingrunt2010@qq.com) on 2021-7-22. */ diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RSAUtils.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RSAUtils.java new file mode 100644 index 0000000..5bbd89d --- /dev/null +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RSAUtils.java @@ -0,0 +1,180 @@ +package com.runt.open.mvi.retrofit.utils; + +import android.util.Base64; + +import java.io.ByteArrayOutputStream; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.Cipher; + + +public class RSAUtils { + + //客户端私钥 + public static final String PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALBGei0scHoOjTLImPHvASaGqYNrdLie0ckWp74Nkqv7FVeXPOvWEG8_jRJCVjJ1grr8SGd9sVY2sxn5XIz7fUEBfx7Vm8m0DaCNBWJpFLGw9xiaVZ2AUKoNyTD4NgZobbwZbt6ZNB6_fggPrGF18pq6GPyCndX1JW8ZiZKj33VBAgMBAAECgYB0q-EX3y7_CnyYXT8l-mxHhJ_T9R6HR89QimcyGqe2nvRMSjSvX7r29xg3OqL0uORzQKHnpcDncELw8SQ5yAbpENeIsD0dvdFlkoyFYU4ljeUbJ46binwwg20TNARjTbpNos9zbhTPh8qixdblxppXA1WC18HtXhixgca5bNG9lQJBAPQfNdpNdDL9l8Tw4hYVuDMszcFuZYbHbm0S4xcwqj-dXNWBztNf5W_K92-N5GIoHbOypkGzjlBjSZi_oKA0HusCQQC42irhw682CG44mKdP6YRDxy6OaauVX4yE9WnsbO8JFSSc9ZCKMMD0F3NGtytDrVMAJxG1iPWXa4ptEdtgwCmDAkAUW1npR1YuPllekdu4jb0bf1v1ClirAYxiyhVnxKYdweiQ4U827yM5zEoP4lwuFzxK1NXqWqe-alkjxK8HTPFbAkAviQLf_adP2MknSrIzzZQSreTeAHR8PA7xnf54KucpScOZjVh3AOSNoH4nYDEC_U5LysA2E5s8Lg5xz9a_QYsrAkEAwV6gNED7_SYDsYyEWimQ6znUb_QSY-sSChnSCY-ILG1wpynBHw_t1Oi3ljl6gL_cYKG1O3uwOtvZtb-Vr1bNkQ"; + + //服务器公钥 + public static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVj4BkEYtlv8z4quUUrkRvW4xuWQvXegMuLKPQZky8LObbxfFZniSvQ4gllFlyFuCjeeInjyQFPC3ARdbihV3P88drBsB2gCG9lwlCkgMjZfSc/hxC4VirsHbGGSIN5oPyCZMQNAUnIojpKBRlE0TJmHvP+FpAe46Yb+oPs8R5DQIDAQAB"; + + + /** + * RSA最大加密明文大小 + */ + private static final int MAX_ENCRYPT_BLOCK = 117; + + /** + * RSA最大解密密文大小 + */ + private static final int MAX_DECRYPT_BLOCK = 128; + + static final String KEY_RSA = "RSA"; //android标准 “RSA/ECB/PKCS1Padding” 服务端标准 “RSA” + + /** + * 获取密钥对 + * + * @return 密钥对 + */ + public static KeyPair getKeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_RSA); + generator.initialize(1024); + return generator.generateKeyPair(); + } + + /** + * 获取私钥 + * + * @param privateKey 私钥字符串 + * @return + */ + public static PrivateKey getPrivateKey(String privateKey) throws Exception { + KeyFactory keyFactory = KeyFactory.getInstance(KEY_RSA); + byte[] decodedKey = Base64.decode(privateKey.getBytes(), Base64.URL_SAFE); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); + return keyFactory.generatePrivate(keySpec); + } + + /** + * 获取公钥 + * + * @param publicKey 公钥字符串 + * @return + */ + public static PublicKey getPublicKey(String publicKey) throws Exception { + KeyFactory keyFactory = KeyFactory.getInstance(KEY_RSA); + byte[] decodedKey = Base64.decode(publicKey.getBytes(), Base64.DEFAULT); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey); + return keyFactory.generatePublic(keySpec); + } + + /** + * RSA加密 + * + * @param data 待加密数据 + * @param key 密钥 + * @return + */ + public static String encrypt(String data, Key key) throws Exception { + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, key); + int inputLen = data.getBytes().length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offset = 0; + byte[] cache; + int i = 0; + // 对数据分段加密 + while (inputLen - offset > 0) { + if (inputLen - offset > MAX_ENCRYPT_BLOCK) { + cache = cipher.doFinal(data.getBytes(), offset, MAX_ENCRYPT_BLOCK); + } else { + cache = cipher.doFinal(data.getBytes(), offset, inputLen - offset); + } + out.write(cache, 0, cache.length); + i++; + offset = i * MAX_ENCRYPT_BLOCK; + } + byte[] encryptedData = out.toByteArray(); + out.close(); + // 获取加密内容使用base64进行编码,并以UTF-8为标准转化成字符串 + // 加密后的字符串 + return Base64.encodeToString(encryptedData,Base64.URL_SAFE | Base64.NO_WRAP); + } + + /** + * RSA解密 + * + * @param data 待解密数据 + * @param key 密钥 + * @return + */ + public static String decrypt(String data, Key key) throws Exception { + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.DECRYPT_MODE, key); + byte[] dataBytes = Base64.decode(data.getBytes(), Base64.URL_SAFE); + int inputLen = dataBytes.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offset = 0; + byte[] cache; + int i = 0; + // 对数据分段解密 + while (inputLen - offset > 0) { + if (inputLen - offset > MAX_DECRYPT_BLOCK) { + cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK); + } else { + cache = cipher.doFinal(dataBytes, offset, inputLen - offset); + } + out.write(cache, 0, cache.length); + i++; + offset = i * MAX_DECRYPT_BLOCK; + } + byte[] decryptedData = out.toByteArray(); + out.close(); + // 解密后的内容 + return new String(decryptedData, "UTF-8"); + } + + /** + * 签名 + * + * @param data 待签名数据 + * @param privateKey 私钥 + * @return 签名 + */ + public static String sign(String data, PrivateKey privateKey) throws Exception { + byte[] keyBytes = privateKey.getEncoded(); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_RSA); + PrivateKey key = keyFactory.generatePrivate(keySpec); + Signature signature = Signature.getInstance("MD5withRSA"); + signature.initSign(key); + signature.update(data.getBytes()); + return Base64.encodeToString(signature.sign(),Base64.DEFAULT); + } + + /** + * 验签 + * + * @param srcData 原始字符串 + * @param publicKey 公钥 + * @param sign 签名 + * @return 是否验签通过 + */ + public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception { + byte[] keyBytes = publicKey.getEncoded(); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_RSA); + PublicKey key = keyFactory.generatePublic(keySpec); + Signature signature = Signature.getInstance("MD5withRSA"); + signature.initVerify(key); + signature.update(srcData.getBytes()); + return signature.verify(Base64.decode(sign.getBytes(),Base64.DEFAULT)); + } + +} diff --git a/libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RetrofitUtils.java b/libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RetrofitUtils.java index b5b0988..fa21c1a 100644 --- a/libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RetrofitUtils.java +++ b/libmvi/src/main/java/com/runt/open/mvi/retrofit/utils/RetrofitUtils.java @@ -2,14 +2,18 @@ import com.runt.open.mvi.BuildConfig; -import com.runt.open.mvi.retrofit.Interceptor.AddHeadersInterceptor; +import com.runt.open.mvi.retrofit.Interceptor.LogEncryptInterceptor; +import com.runt.open.mvi.retrofit.Interceptor.OpenInterceptor; +import com.runt.open.mvi.retrofit.Interceptor.EncryptInterceptor; import com.runt.open.mvi.retrofit.Interceptor.HttpLoggingInterceptor; +import com.runt.open.mvi.retrofit.converter.DecryptGsonConverterFactory; import com.runt.open.mvi.retrofit.converter.GsonConverterFactory; import com.runt.open.mvi.retrofit.net.NetWorkListener; import java.util.Collections; import java.util.concurrent.TimeUnit; +import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Protocol; import retrofit2.Retrofit; @@ -23,13 +27,11 @@ public class RetrofitUtils { static RetrofitUtils instance; - Retrofit retrofit/*log输出,驼峰转换*/,unHumpRetrofit/*log输出,不强制驼峰转换*/, - unLogRetrofit/*log不输出,驼峰转换*/,unLogHumpRetorfit/*log不输出,不强制驼峰转换*/; + Retrofit retrofit/*log输出*/,humpRetrofit/*log输出,强制驼峰转换*/, + unLogRetrofit/*log不输出*/,unLogHumpRetrofit/*log不输出,强制驼峰转换*/, + encryptRetrofit/*加密log输出*/,humpEncryptRetrofit/*加密log输出,强制驼峰转换*/, + unLogEncryptRetrofit/*加密log不输出*/,unLogHumpEncryptRetrofit/*加密log不输出,强制驼峰转换*/; - OkHttpClient.Builder builder = new OkHttpClient.Builder() - .addInterceptor(new AddHeadersInterceptor()); - OkHttpClient.Builder logBuilder = new OkHttpClient.Builder() - .addInterceptor(new HttpLoggingInterceptor());//log打印拦截器 public static RetrofitUtils getInstance() { if(instance == null){ @@ -47,38 +49,22 @@ .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); } + + public <T> T getTempRetrofit(Class<T> clas,String url) { + Interceptor interceptor = BuildConfig.DEBUG ? new HttpLoggingInterceptor() : new OpenInterceptor(); + return getRetrofit(getOkHttpClient(new OkHttpClient.Builder().addInterceptor(interceptor)), + new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(true)),url).create(clas); + } + /** - * log输出,gson驼峰转换 + * log输出 * @return */ public <T> T getRetrofit(Class<T> clas) { - if(retrofit == null){ - retrofit = getRetrofit(getOkHttpClient(logBuilder), - new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(true))) ; - } if(!BuildConfig.DEBUG){//正式版 不打印log return getUnLogRetrofit(clas); } - return retrofit.create(clas); - } - - public <T> T getTempRetrofit(Class<T> clas,String url) { - return getRetrofit(getOkHttpClient(new OkHttpClient.Builder().addInterceptor(new HttpLoggingInterceptor(BuildConfig.DEBUG))), - new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(true)),url).create(clas); - } - /** - * log输出,gson不转换驼峰 - * @return - */ - public <T> T getUnHumpRetrofit(Class<T> clas) { - if(unHumpRetrofit == null){ - unHumpRetrofit = getRetrofit(getOkHttpClient(logBuilder), - new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create())) ; - } - if(!BuildConfig.DEBUG){//正式版 不打印log - return getUnLogHumpRetorfit(clas); - } - return unHumpRetrofit.create(clas); + return getRetrofit(clas,false,false,true); } /** @@ -86,23 +72,128 @@ * @return */ public <T> T getUnLogRetrofit(Class<T> clas) { - if(unLogRetrofit == null){ - unLogRetrofit = getRetrofit(getOkHttpClient(builder), - new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(true))) ; - } - return unLogRetrofit.create(clas); + return getRetrofit(clas,false,false,false); } /** - * log不输出,gson不转换驼峰 + * log输出,gson转换驼峰 + * @return + */ + public <T> T getHumpRetrofit(Class<T> clas) { + if(!BuildConfig.DEBUG){//正式版 不打印log + return getUnLogHumpRetorfit(clas); + } + return getRetrofit(clas,true,false,true); + } + + /** + * log不输出,gson转换驼峰 * @return */ public <T> T getUnLogHumpRetorfit(Class<T> clas) { - if(unLogHumpRetorfit == null){ - unLogHumpRetorfit = getRetrofit(getOkHttpClient(builder), - new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create())) ; + return getRetrofit(clas,true,false,false); + } + + /** + * 加密log输出 + * @return + */ + public <T> T getEncrptRetrofit(Class<T> clas) { + if(!BuildConfig.DEBUG){//正式版 不打印log + return getUnLogEncryptRetrofit(clas); } - return unLogHumpRetorfit.create(clas); + return getRetrofit(clas,false,true,true); + } + + /** + * 加密log不输出 + * @return + */ + public <T> T getUnLogEncryptRetrofit(Class<T> clas) { + return getRetrofit(clas,false,true,false); + } + + /** + * 加密log输出 gson转换驼峰 + * @return + */ + public <T> T getHumpEncryptRetrofit(Class<T> clas) { + if(!BuildConfig.DEBUG){//正式版 不打印log + return getUnLogHumpEncryptRetrofit(clas); + } + return getRetrofit(clas,true,true,true); + } + + /** + * 加密log不输出 gson转换驼峰 + * @return + */ + public <T> T getUnLogHumpEncryptRetrofit(Class<T> clas) { + return getRetrofit(clas,true,true,false); + } + + private <T> T getRetrofit(Class<T> clzz,boolean transHump,boolean encrypt,boolean log){ + Retrofit temp = null; + if(transHump && encrypt && log){ + if(humpEncryptRetrofit == null){ + humpEncryptRetrofit = initRetrofit(transHump,encrypt,log); + } + temp = humpEncryptRetrofit; + }else if(transHump && encrypt && !log ){ + if(unLogHumpEncryptRetrofit == null){ + unLogHumpEncryptRetrofit = initRetrofit(transHump,encrypt,log); + } + temp = unLogHumpEncryptRetrofit; + }else if(transHump && !encrypt && !log ){ + if(unLogHumpRetrofit == null){ + unLogHumpRetrofit = initRetrofit(transHump,encrypt,log); + } + temp = unLogHumpRetrofit; + }else if(!transHump && !encrypt && !log ){ + if(unLogRetrofit == null){ + unLogRetrofit = initRetrofit(transHump,encrypt,log); + } + temp = unLogRetrofit; + }else if(!transHump && !encrypt && log ){ + if(retrofit == null){ + retrofit = initRetrofit(transHump,encrypt,log); + } + temp = retrofit; + }else if(!transHump && encrypt && log ){ + if(encryptRetrofit == null){ + encryptRetrofit = initRetrofit(transHump,encrypt,log); + } + temp = encryptRetrofit; + }else if(!transHump && encrypt && !log ){ + if(unLogEncryptRetrofit == null){ + unLogEncryptRetrofit = initRetrofit(transHump,encrypt,log); + } + temp = unLogEncryptRetrofit; + }else if(transHump && !encrypt && log ){ + if(humpRetrofit == null){ + humpRetrofit = initRetrofit(transHump,encrypt,log); + } + temp = humpRetrofit; + } + return temp.create(clzz); + } + + private Retrofit initRetrofit(boolean transHump,boolean encrypt,boolean log){ + Retrofit.Builder builder = new Retrofit.Builder(); + builder.addConverterFactory(encrypt ? DecryptGsonConverterFactory.create(transHump) : GsonConverterFactory.create(transHump)); + + OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder(); + if(log && encrypt){ + httpBuilder.addInterceptor(new LogEncryptInterceptor());//log打印加密拦截器 + } else if (log && !encrypt) { + httpBuilder.addInterceptor(new HttpLoggingInterceptor());//log打印拦截器 + } else if (!log && encrypt) { + httpBuilder.addInterceptor(new EncryptInterceptor());//加密拦截器 + } else if (!log && !encrypt) { + httpBuilder.addInterceptor(new OpenInterceptor());//拦截器 + } + OkHttpClient httpClient = getOkHttpClient(httpBuilder); + return getRetrofit(httpClient,builder); } private OkHttpClient getOkHttpClient(OkHttpClient.Builder builder){ diff --git a/libmvi/src/main/java/com/runt/open/mvi/utils/DeviceIdUtils.java b/libmvi/src/main/java/com/runt/open/mvi/utils/DeviceIdUtils.java index 69a6a94..e9bea59 100644 --- a/libmvi/src/main/java/com/runt/open/mvi/utils/DeviceIdUtils.java +++ b/libmvi/src/main/java/com/runt/open/mvi/utils/DeviceIdUtils.java @@ -51,7 +51,7 @@ TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); return tm.getDeviceId(); } catch (Exception e) { - //e.printStackTrace(); + e.printStackTrace(); } return ""; } diff --git a/libmvi/src/main/java/com/runt/open/mvi/utils/DeviceUtil.java b/libmvi/src/main/java/com/runt/open/mvi/utils/DeviceUtil.java index e8c0234..5586242 100644 --- a/libmvi/src/main/java/com/runt/open/mvi/utils/DeviceUtil.java +++ b/libmvi/src/main/java/com/runt/open/mvi/utils/DeviceUtil.java @@ -1,21 +1,16 @@ package com.runt.open.mvi.utils; -import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Point; import android.os.Build; -import android.provider.Settings; -import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.WindowManager; - -import androidx.core.app.ActivityCompat; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -43,8 +38,7 @@ sb.append("设备厂商:");//设备型号 sb.append(getDeviceBrand() + "\t");//设备型号 sb.append("程序版本号:" + getAppVersionCode(context) + " " + getAppVersionName(context) + "\t");//程序版本号 - sb.append("设备唯一标识符:" + getSerialNumber(context)); - sb.append("\n设备imei:" + getIMEI(context)); + sb.append("设备唯一标识符:" + DeviceIdUtils.getDeviceId(context)); String str = sb.toString() + " \n"; str += getDisplayInfomation(context) + " \n"; str += getDensity(context) + " \n"; @@ -291,50 +285,6 @@ } return 0; - } - - - - public static String getSerialNumber(Context context) { - String serial = ""; - try { - if (Build.VERSION.SDK_INT >= 28) {//9.0+ - if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { - Log.i(TAG, "getMEID meid: READ_PHONE_STATE" ); - ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.READ_PHONE_STATE}, 1567); - } else { - serial = Build.getSerial(); - } - } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+ - serial = Build.SERIAL; - } else {//8.0- - Class<?> c = Class.forName("android.os.SystemProperties"); - Method get = c.getMethod("get", String.class); - serial = (String) get.invoke(c, "ro.serialno"); - } - } catch (Exception e) { - Log.e("e", "读取设备序列号异常:" + e.toString()); - } - return serial; - } - - public static String getIMEI(Context context) { - String deviceId = ""; - TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (null != tm) { - if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { - Log.i(TAG, "getMEID meid: READ_PHONE_STATE" ); - ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.READ_PHONE_STATE}, 1567); - } else { - if (tm.getDeviceId() != null) { - deviceId = tm.getDeviceId(); - } else { - deviceId = Settings.Secure.getString(context.getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID); - } - } - Log.d("deviceId--->", deviceId); - } - return ""; } public static String getMEID() { diff --git a/libmvi/src/main/java/com/runt/open/mvi/views/PublicViews.kt b/libmvi/src/main/java/com/runt/open/mvi/views/PublicViews.kt index f13f6dc..6cf1b26 100644 --- a/libmvi/src/main/java/com/runt/open/mvi/views/PublicViews.kt +++ b/libmvi/src/main/java/com/runt/open/mvi/views/PublicViews.kt @@ -1,5 +1,6 @@ package com.runt.open.mvi.views +import android.util.Log import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -72,10 +73,13 @@ fun MessageDialog(message : MessageState){ if(message.isVisible){ Dialog(onDismissRequest = { - if(message.cancelDissmiss){ + //系统响应 + Log.i("PublicViews" , "MessageDialog: onDismiss") + if(message.touchOutside){ message.setDismiss.invoke() + message.onCancelRequest.invoke() + message.onDismissRequest.invoke() } - message.onDismissRequest.invoke() }) { Card( modifier = Modifier @@ -104,10 +108,11 @@ if(!message.cancelText.equals("")){ Spacer(modifier = Modifier.weight(1f)) Button(onClick = { + message.onCancelRequest.invoke() if(message.cancelDissmiss){ message.setDismiss.invoke() + message.onDismissRequest.invoke() } - message.onCancelRequest.invoke() }, colors = ButtonDefaults.buttonColors( containerColor = Color.Gray, // 背景色 @@ -119,10 +124,11 @@ } Spacer(modifier = Modifier.weight(1f)) Button(onClick = { + message.onConfirmRequest.invoke() if(message.confirmDissmiss){ message.setDismiss.invoke() + message.onDismissRequest.invoke() } - message.onConfirmRequest.invoke() }) { Text(text = message.confirmText) } -- Gitblit v1.9.1