diff --git a/app/build.gradle b/app/build.gradle index 4d75a39..b369eba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,6 +105,8 @@ android { dependencies { // implementation fileTree(dir: 'libs', include: ['*.jar']) compileOnly files('libs/framework.jar') + implementation project(path: ':niceimageview') + implementation project(path: ':verification-view') implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' @@ -120,7 +122,7 @@ dependencies { annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3' //okhttp - implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' //Retrofit implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b4245..63b1d23 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -2,7 +2,7 @@ # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # -# For more details, see +# For icon_more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e5fe698..05f05b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,6 +79,25 @@ + + + + + + + + + + + + + + + + implements Serializable { + + private static final long serialVersionUID = 5468533687801294972L; + + public int code; + public String msg; + public T data; + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/uiuipad/find/dialog/CustomDialog.java b/app/src/main/java/com/uiuipad/find/dialog/CustomDialog.java new file mode 100644 index 0000000..b10b5b2 --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/dialog/CustomDialog.java @@ -0,0 +1,254 @@ +package com.uiuipad.find.dialog; + + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; + +import com.uiuipad.find.R; + + +/** + * description:自定义dialog + */ + +public class CustomDialog extends AlertDialog { + /** + * 显示的图片 + */ + private ImageView imageIv; + + /** + * 显示的标题 + */ + private TextView titleTv; + + /** + * 显示的消息 + */ + private TextView messageTv; + + /** + * 确认和取消按钮 + */ + private TextView negtiveBn, positiveBn; + + /** + * 按钮之间的分割线 + */ +// private View columnLineView; + + private Context mContext; + + public CustomDialog(Context context) { + super(context, R.style.CustomDialog); + this.mContext = context; + } + + /** + * 都是内容数据 + */ + private String message; + private String title; + private String positive, negtive; + private int imageResId = -1; + + /** + * 底部是否只有一个按钮 + */ + private boolean isSingle = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.dialog_custom); + //按空白处不能取消动画 + setCanceledOnTouchOutside(false); + //初始化界面控件 + initView(); + //初始化界面数据 + refreshView(); + //初始化界面控件的事件 + initEvent(); + } + + /** + * 初始化界面的确定和取消监听器 + */ + private void initEvent() { + //设置确定按钮被点击后,向外界提供监听 + positiveBn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onClickBottomListener != null) { + onClickBottomListener.onPositiveClick(); + } + } + }); + //设置取消按钮被点击后,向外界提供监听 + negtiveBn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onClickBottomListener != null) { + onClickBottomListener.onNegtiveClick(); + } + } + }); + } + + /** + * 初始化界面控件的显示数据 + */ + private void refreshView() { + //如果用户自定了title和message + if (!TextUtils.isEmpty(title)) { + titleTv.setText(title); + titleTv.setVisibility(View.VISIBLE); + } else { + titleTv.setVisibility(View.GONE); + } + if (!TextUtils.isEmpty(message)) { + messageTv.setText(message); + } + //如果设置按钮的文字 + if (!TextUtils.isEmpty(positive)) { + positiveBn.setText(positive); + } else { + positiveBn.setText("确定"); + } + if (!TextUtils.isEmpty(negtive)) { + negtiveBn.setText(negtive); + } else { + negtiveBn.setText("取消"); + } + + if (imageResId != -1) { + imageIv.setImageResource(imageResId); + imageIv.setVisibility(View.VISIBLE); + } else { + imageIv.setVisibility(View.GONE); + } + /** + * 只显示一个按钮的时候隐藏取消按钮,回掉只执行确定的事件 + */ + if (isSingle) { +// columnLineView.setVisibility(View.GONE); + negtiveBn.setVisibility(View.GONE); + } else { + negtiveBn.setVisibility(View.VISIBLE); +// columnLineView.setVisibility(View.VISIBLE); + } + } + + @Override + public void show() { + super.show(); + refreshView(); + } + + /** + * 初始化界面控件 + */ + private void initView() { + negtiveBn = findViewById(R.id.negtive); + positiveBn = findViewById(R.id.positive); + titleTv = findViewById(R.id.title); + messageTv = findViewById(R.id.message); + imageIv = findViewById(R.id.image); +// columnLineView = findViewById(R.id.column_line); + } + + /** + * 设置确定取消按钮的回调 + */ + private OnClickBottomListener onClickBottomListener; + + public void setOnClickBottomListener(OnClickBottomListener onClickBottomListener) { + this.onClickBottomListener = onClickBottomListener; + } + + public interface OnClickBottomListener { + /** + * 点击确定按钮事件 + */ + void onPositiveClick(); + + /** + * 点击取消按钮事件 + */ + void onNegtiveClick(); + } + + public String getMessage() { + return message; + } + + public CustomDialog setMessage(String message) { + this.message = message; + return this; + } + + public String getTitle() { + return title; + } + + public CustomDialog setTitle(String title) { + this.title = title; + return this; + } + + public String getPositive() { + return positive; + } + + public CustomDialog setPositive(String positive) { + this.positive = positive; + return this; + } + + public String getNegtive() { + return negtive; + } + + public CustomDialog setNegtive(String negtive) { + this.negtive = negtive; + return this; + } + + public CustomDialog setNegtiveText(String negtive) { + negtiveBn.setText(negtive); + return this; + } + + public int getImageResId() { + return imageResId; + } + + public boolean isSingle() { + return isSingle; + } + + public CustomDialog setSingle(boolean single) { + isSingle = single; + return this; + } + + public CustomDialog setImageResId(int imageResId) { + this.imageResId = imageResId; + return this; + } + + @Override + public void dismiss() { + super.dismiss(); +// Intent intent = new Intent(mContext, MainActivity.class); +// intent.setAction(MainActivity.REFRESHACTION); +// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); +// mContext.startActivity(intent); + } +} diff --git a/app/src/main/java/com/uiuipad/find/network/NetInterfaceManager.java b/app/src/main/java/com/uiuipad/find/network/NetInterfaceManager.java new file mode 100644 index 0000000..93e9f13 --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/network/NetInterfaceManager.java @@ -0,0 +1,217 @@ +package com.uiuipad.find.network; + +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.Context; +import android.os.Environment; +import android.util.Log; + +import com.tencent.mmkv.MMKV; +import com.uiuipad.find.comm.CommonConfig; +import com.uiuipad.find.network.api.BindDevices; +import com.uiuipad.find.network.interceptor.MD5Util; +import com.uiuipad.find.network.interceptor.RepeatRequestInterceptor; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import okhttp3.Cache; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + +public class NetInterfaceManager { + private static final String TAG = NetInterfaceManager.class.getSimpleName(); + + @SuppressLint("StaticFieldLeak") + private static NetInterfaceManager INSTANCE; + private Context mContext; + private ContentResolver mCrv; + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + private Retrofit mRetrofit; + private OkHttpClient mOkHttpClient; + private Retrofit mJxwRetrofit; + private OkHttpClient mJxwOkHttpClient; +// private CacheHelper mCacheHelper; + + private final ConcurrentHashMap requestIdsMap = new ConcurrentHashMap<>(); + + //超时时间 + private static final int timeOut = 5; + // 缓存文件最大限制大小20M + private static final long cacheSize = 1024 * 1024 * 64; + public static final String CUSTOM_REPEAT_REQ_PROTOCOL = "MY_CUSTOM_REPEAT_REQ_PROTOCOL"; + + private NetInterfaceManager(Context context) { + if (context == null) { + throw new RuntimeException("Context is NULL"); + } + this.mContext = context; + this.mCrv = mContext.getContentResolver(); +// this.mCacheHelper = new CacheHelper(mContext); + + if (mRetrofit == null) { + if (mOkHttpClient == null) { + Interceptor myHttpInterceptor = new Interceptor() { + @NotNull + @Override + public Response intercept(@NotNull Chain chain) throws IOException { + Request request = chain.request(); + String requestKey = MD5Util.getUpperMD5Str(request.method() + request.url().toString()); + Response response = chain.proceed(request); //准备返回Response + synchronized (requestIdsMap) { + requestIdsMap.remove(requestKey); //在这里移除正常的请求登记 + Log.e("REPEAT-REQUEST", "移除请求2:" + requestKey + " --- " + Thread.currentThread().getName() + " URL = " + request.url()); + } + return response; + } + }; + Interceptor mRequestInterceptor = new Interceptor() { + @NotNull + @Override + public Response intercept(@NotNull Chain chain) throws IOException { + Request request = chain.request(); + //拦截处理重复的HTTP 请求,类似 防止快速点击按钮去重 可以不去处理了,全局统一处理 + String requestKey = MD5Util.getUpperMD5Str(request.method() + request.url().toString()); + synchronized (requestIdsMap) { + if (requestIdsMap.get(requestKey) == null) { +// Log.e("REPEAT-REQUEST", "intercept: " + requestIdsMap); + requestIdsMap.put(requestKey, System.currentTimeMillis()); + Log.e("REPEAT-REQUEST", "注册请求:" + requestKey + " --- " + Thread.currentThread().getName() + " URL = " + request.url()); + } else { + //如果是重复的请求,抛出一个自定义的错误,这个错误大家根据自己的业务定义吧 + Log.e("REPEAT-REQUEST", "重复请求:" + requestKey + " --- " + Thread.currentThread().getName() + " URL = " + request.url()); + return new Response.Builder() + .protocol(Protocol.get(CUSTOM_REPEAT_REQ_PROTOCOL)) + .request(request) //multi thread + .build(); + } + } + Response originalResponse = chain.proceed(request); + return originalResponse.newBuilder().build(); + } + }; + + //如果无法生存缓存文件目录,检测权限使用已经加上,检测手机是否把文件读写权限禁止了 + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + builder.connectTimeout(timeOut, TimeUnit.SECONDS); // 设置连接超时时间 + builder.writeTimeout(timeOut, TimeUnit.SECONDS);// 设置写入超时时间 + builder.readTimeout(timeOut, TimeUnit.SECONDS);// 设置读取数据超时时间 + builder.retryOnConnectionFailure(true);// 设置进行连接失败重试 + builder.addInterceptor(new RepeatRequestInterceptor()); + +// builder.addInterceptor(myHttpInterceptor); +// builder.addNetworkInterceptor(mRequestInterceptor); + + // 设置缓存文件路径 + String cacheDirectory = getCacheDir() + "/OkHttpCache"; + Cache cache = new Cache(new File(cacheDirectory), cacheSize); + builder.cache(cache);// 设置缓存 + mOkHttpClient = builder.build(); + } + + mRetrofit = new Retrofit.Builder() + .client(mOkHttpClient) + .baseUrl(UrlAddress.ROOT_URL) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) + .build(); + } + + if (mJxwRetrofit == null) { + if (mJxwOkHttpClient == null) { + //如果无法生存缓存文件目录,检测权限使用已经加上,检测手机是否把文件读写权限禁止了 + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + builder.connectTimeout(timeOut, TimeUnit.SECONDS); // 设置连接超时时间 + builder.writeTimeout(timeOut, TimeUnit.SECONDS);// 设置写入超时时间 + builder.readTimeout(timeOut, TimeUnit.SECONDS);// 设置读取数据超时时间 + builder.retryOnConnectionFailure(true);// 设置进行连接失败重试 + builder.addInterceptor(new RepeatRequestInterceptor()); + // 设置缓存文件路径 + String cacheDirectory = getCacheDir() + "/OkHttpCache"; + Cache cache = new Cache(new File(cacheDirectory), cacheSize); + builder.cache(cache);// 设置缓存 + mJxwOkHttpClient = builder.build(); + } + mJxwRetrofit = new Retrofit.Builder() + .client(mJxwOkHttpClient) + .baseUrl(UrlAddress.JXW_ROOT_URL) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) + .build(); + } + + } + + private String getCacheDir() { + String cachePath; + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) + || !Environment.isExternalStorageRemovable()) { + if (mContext.getExternalCacheDir() != null) { + cachePath = mContext.getExternalCacheDir().getPath(); + } else if (mContext.getExternalFilesDir("cache") != null) { + cachePath = mContext.getExternalFilesDir("cache").getPath(); + } else { + cachePath = mContext.getCacheDir().getPath(); + } + } else { + cachePath = mContext.getCacheDir().getPath(); + } + return cachePath; + } + + public static void init(Context context) { + if (context == null) { + throw new RuntimeException("context is NULL"); + } + if (INSTANCE == null) { + INSTANCE = new NetInterfaceManager(context); + } + } + + public static NetInterfaceManager getInstance() { + if (INSTANCE == null) { + throw new IllegalStateException("You must be init NetworkManager first"); + } + return INSTANCE; + } + + public OkHttpClient getOkHttpClient() { + return mOkHttpClient; + } + + /* + * + * Observable + * + * */ + + + + /* + * + * API + * + * */ + + public BindDevices getbindDevicesControl() { + return mRetrofit.create(BindDevices.class); + } + + /* + * + * execution + * + * */ + + +} diff --git a/app/src/main/java/com/uiuipad/find/network/UrlAddress.java b/app/src/main/java/com/uiuipad/find/network/UrlAddress.java new file mode 100644 index 0000000..9b9a020 --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/network/UrlAddress.java @@ -0,0 +1,82 @@ +package com.uiuipad.find.network; + +public class UrlAddress { + /*主页接口*/ + public static final String ROOT_URL = "https://led.aolelearn.com/android/"; + + /*绑定设备消息*/ + public final static String BIND_DEVICES = "sn/bindSn"; + /*发送绑定验证码*/ + public static final String SEND_BIND_VER_CODE = "Sn/sendBindVerCode"; + /*手动绑定设备*/ + public static final String EQUIPMENT_BIND = "Sn/equipmentBind"; + + /*设备激活*/ + public static final String SN_ACTIVATION = "sn/snActivation"; + /*获取设备是否激活*/ + public static final String GET_SN_IS_ACTIVATION = "sn/getSnIsActivation"; + /*获取设备激活二维码链接*/ + public static final String ACTIVATION_QRCODE = "pay/getActivationQrcode"; + + + /*获取文件*/ + public static final String GET_FILES = "file/getFiles"; + /*获取操作指南*/ + public static final String GET_OPERATION_GUIDE = "file/getFiles"; + + + /*上传屏幕截图*/ + public final static String UPLOAD_SCREEN_SNAPSHOT = "sn/uploadScreenshot"; + /*浏览器网址管控*/ + public final static String SET_BROWSER_URL = "control/getBrowser"; + /*浏览器书签管控*/ + public final static String SET_BROWSER_LABEL = "control/getLabel"; + /*上传控制面版截图*/ + public static final String UPLOAD_CONTROL_SCREENSHOT = "sn/uploadControlScreenshot"; + + /*设备信息接口*/ + public static final String SNINFO = "sn/getSnInfo"; + /*获取用户头像和信息*/ + public static final String GET_USER_AVATAR_INFO = "sn/getUserAvatarInfo"; + /*获取设备类型*/ + public static final String GET_SN_TYPE = "sn/getSnType"; + /*获取正在运行的app*/ + public static final String RUN_NEW_APP = "app/runNewApp"; + /*获取所有应用*/ + public final static String GET_ALL_PACKAGE = "app/queryAllApp"; + //获取管理员所有应用 + public final static String GET_ADMIN_APP = "getAdminApp"; + + /*获取系统设置*/ + public final static String GET_SETTINGS = "control/getSetting"; + /*获取强制下载*/ + public final static String GET_FORCE_INSTALL = "app/getForceDownload"; + /*发送卸载或者安装信息*/ + public final static String SEND_INSTALLEDORREMOVED = "app/addAppInstall"; + /*发送设备基本信息*/ + public final static String UPDATE_SNINFO = "sn/updateAdminSn"; + /*根据包名获取更新*/ + public final static String GET_NEWESTAPPUPDATE = "app/newestAppUpdate"; + /*获取禁用包名*/ + public static final String GET_APP_ICON = "getAppIcon"; + /*获取灰度更新*/ + public static final String GET_TEST_APP_INFO = "app/getTestAppInfo"; + /*获取wifi*/ + public static final String GET_WIFI_ALIAS_PW = "getWifi"; + + /*获取屏幕管控*/ + public final static String GET_SCREEN_LOCK = "sn/getScreenshot"; + /*获取锁屏密码*/ + public static final String LOCK_SCREEN_PWD = "sn/getLockScreenPwd"; + /*解除锁屏*/ + public static final String UPDATE_LOCK_SCREEN = "sn/updateLockScreen"; + + /*通过ip获取信息*/ + public static final String PCONLINE_WHOIS = "http://whois.pconline.com.cn/"; + public static final String WHOIS = "ipJson.jsp"; + + /*九学王测试服务器*/ + public static final String JXW_ROOT_URL = "http://api.jxwxxkj.com/"; + /*激活接口*/ + public static final String ADD_BY_AUTHORIZED = "api/thddevice/series/addByAuthorized"; +} diff --git a/app/src/main/java/com/uiuipad/find/network/api/BindDevices.java b/app/src/main/java/com/uiuipad/find/network/api/BindDevices.java new file mode 100644 index 0000000..8bcc433 --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/network/api/BindDevices.java @@ -0,0 +1,17 @@ +package com.uiuipad.find.network.api; + +import com.uiuipad.find.bean.BaseResponse; +import com.uiuipad.find.network.UrlAddress; + +import io.reactivex.rxjava3.core.Observable; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface BindDevices { + @GET(UrlAddress.BIND_DEVICES) + Observable getBindDevices( + @Query("sn") String sn, + @Query("id") String id, + @Query("type") int type + ); +} diff --git a/app/src/main/java/com/uiuipad/find/network/interceptor/MD5Util.java b/app/src/main/java/com/uiuipad/find/network/interceptor/MD5Util.java new file mode 100644 index 0000000..4861b5b --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/network/interceptor/MD5Util.java @@ -0,0 +1,112 @@ +package com.uiuipad.find.network.interceptor; + +import android.annotation.SuppressLint; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5Util { + + public static String packetMD5(String str) { + MessageDigest messageDigest = null; + try { + messageDigest = MessageDigest.getInstance("MD5"); + + messageDigest.reset(); + + messageDigest.update(str.getBytes("UTF-8")); + } catch (NoSuchAlgorithmException e) { + System.out.println("NoSuchAlgorithmException caught!"); + System.exit(-1); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + byte[] byteArray = messageDigest.digest(); + + StringBuffer md5StrBuff = new StringBuffer(); + + for (int i = 0; i < byteArray.length; i++) { + if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) + md5StrBuff.append("0").append( + Integer.toHexString(0xFF & byteArray[i])); + else + md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i])); + } + + return md5StrBuff.toString(); + } + + @SuppressLint("DefaultLocale") + public static String getUpperMD5Str(String str) { + MessageDigest messageDigest = null; + + try { + messageDigest = MessageDigest.getInstance("MD5"); + + messageDigest.reset(); + + messageDigest.update(str.getBytes("UTF-8")); + } catch (NoSuchAlgorithmException e) { + System.out.println("NoSuchAlgorithmException caught!"); + System.exit(-1); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + byte[] byteArray = messageDigest.digest(); + + StringBuffer md5StrBuff = new StringBuffer(); + + for (int i = 0; i < byteArray.length; i++) { + if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) + md5StrBuff.append("0").append( + Integer.toHexString(0xFF & byteArray[i])); + else + md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i])); + } + + return md5StrBuff.toString().toUpperCase(); + } + + + /** + * 获取16位的MD5 值得 + * + * @param str + * @return + */ + @SuppressLint("DefaultLocale") + public static String getUpperMD5Str16(String str) { + MessageDigest messageDigest = null; + + try { + messageDigest = MessageDigest.getInstance("MD5"); + + messageDigest.reset(); + + messageDigest.update(str.getBytes("UTF-8")); + } catch (NoSuchAlgorithmException e) { + System.out.println("NoSuchAlgorithmException caught!"); + System.exit(-1); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + byte[] byteArray = messageDigest.digest(); + + StringBuffer md5StrBuff = new StringBuffer(); + + for (int i = 0; i < byteArray.length; i++) { + if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) + md5StrBuff.append("0").append( + Integer.toHexString(0xFF & byteArray[i])); + else + md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i])); + } + + return md5StrBuff.toString().toUpperCase().substring(8, 24); + } + +} diff --git a/app/src/main/java/com/uiuipad/find/network/interceptor/RepeatRequestInterceptor.java b/app/src/main/java/com/uiuipad/find/network/interceptor/RepeatRequestInterceptor.java new file mode 100644 index 0000000..5e6e38b --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/network/interceptor/RepeatRequestInterceptor.java @@ -0,0 +1,106 @@ +package com.uiuipad.find.network.interceptor; + +import android.util.Log; + +import com.uiuipad.find.BuildConfig; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +import okhttp3.Interceptor; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; + +/** + * v1.0 2022-07-15 16:16:52 + */ +public class RepeatRequestInterceptor implements Interceptor { + private static final String TAG = RepeatRequestInterceptor.class.getSimpleName(); + + private final ConcurrentHashMap requestIdsMap = new ConcurrentHashMap<>(); + public static final String REPEAT_REQUEST_PROTOCOL = "OKHTTP_REPEAT_REQUEST_PROTOCOL"; + + @NotNull + @Override + public Response intercept(@NotNull Chain chain) throws IOException { + Request request = chain.request(); + Response response = chain.proceed(request); + ResponseBody responseBody = response.body(); + + //会消费请求,导致请求多次 + String content = responseBody.string(); +// Response copy = response.newBuilder().body(responseBody).build(); + ResponseBody copy = ResponseBody.create(responseBody.contentType(), content); + if (BuildConfig.DEBUG) { + Log.e(TAG, "请求体返回:| Response: " + request.url() + "\t body: " + content); + } + //相同的请求 + String requestKey = MD5Util.getUpperMD5Str(request.method() + request.url().toString() + requestBodyToString(request.body())); + long time = System.currentTimeMillis();//请求时间 + try { + if (requestIdsMap.size() > 0 && requestIdsMap.containsKey(requestKey)) { + log("重复请求:", requestKey, request); + //下面这行写了不会抛出onerror +// chain.call().cancel(); + return new Response.Builder() + .protocol(Protocol.get(REPEAT_REQUEST_PROTOCOL)) + .request(request) //multi thread + .build(); + } + requestIdsMap.put(requestKey, time); + log("注册请求:", requestKey, request); +// RepeatRequestInterceptor.Builder builder = request.newBuilder(); +// builder.addHeader("header", jsonObject.toString()); + return response.newBuilder().body(copy).build(); + } catch (IOException e) { + Log.e(TAG, "intercept: " + e.getMessage()); + throw e; + } finally { + if (requestIdsMap.containsKey(requestKey) && requestIdsMap.containsValue(time)) {//请求任务完成删除map中的数据 + requestIdsMap.remove(requestKey); + log("移除请求:", requestKey, request); + } + } + } + + private void log(String action, String requestKey, Request request) { + if (BuildConfig.DEBUG) { + Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method() + " --- " + " URL = " + request.url().encodedPath() + "\t" + bodyToString(request)); + } else { + Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method()); + } + } + + private static String bodyToString(final Request request) { + try { + final Request copy = request.newBuilder().build(); + final Buffer buffer = new Buffer(); + copy.body().writeTo(buffer); + if (buffer.size() > 4096) { + return "-too long"; + } + return buffer.readUtf8(); + } catch (Exception e) { + return "-"; + } + } + + private static String requestBodyToString(RequestBody body) { + try { + final Buffer buffer = new Buffer(); + body.writeTo(buffer); + if (buffer.size() > 4096) { + return "-too long"; + } + return buffer.readUtf8(); + } catch (Exception e) { + return "-"; + } + } +} diff --git a/app/src/main/java/com/uiuipad/find/push/PushManager.java b/app/src/main/java/com/uiuipad/find/push/PushManager.java index 0f6541a..bc9998e 100644 --- a/app/src/main/java/com/uiuipad/find/push/PushManager.java +++ b/app/src/main/java/com/uiuipad/find/push/PushManager.java @@ -3,19 +3,48 @@ package com.uiuipad.find.push; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.Gravity; +import android.view.WindowManager; +import com.google.gson.JsonObject; import com.tencent.mmkv.MMKV; +import com.uiuipad.find.activity.main.MainActivity; +import com.uiuipad.find.bean.BaseResponse; import com.uiuipad.find.comm.CommonConfig; +import com.uiuipad.find.dialog.CustomDialog; +import com.uiuipad.find.gson.GsonUtils; +import com.uiuipad.find.network.NetInterfaceManager; +import com.uiuipad.find.service.main.MainService; +import com.uiuipad.find.util.ToastUtil; +import com.uiuipad.find.util.Utils; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.Schedulers; public class PushManager { private static final String TAG = PushManager.class.getSimpleName(); + public static final String ACTION_REFRESH_BINDING_STATUS = "RefreshBindingStatus"; + + @SuppressLint("StaticFieldLeak") private static PushManager sInstance; private Context mContext; private ContentResolver mResolver; private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + // 1.绑定设备 + private static final String BIND_DEVIVES = "1"; + private PushManager(Context context) { if (context == null) { throw new RuntimeException("Context is NULL"); @@ -40,10 +69,120 @@ public class PushManager { public void setPushContent(String title, String extras) { switch (title) { - + case BIND_DEVIVES: + ToastUtil.debugShow("收到推送消息: 绑定设备"); + bindService(extras); + break; default: } } + private Disposable subscribe; + private long cutdownTime = 30; + private CustomDialog dialog; + + void bindService(final String jsonString) { + ToastUtil.debugShow("收到绑定设备请求"); + JsonObject object = GsonUtils.getJsonObject(jsonString); + String userName = object.get("member_name").getAsString(); + final String id = object.get("id").getAsString(); + String phoneNum = object.get("member_phone").getAsString(); + dialog = new CustomDialog(mContext); + subscribe = Observable.interval(1, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(Long aLong) throws Exception { + if (aLong < cutdownTime && (subscribe != null && !subscribe.isDisposed())) { + dialog.setNegtiveText("拒绝" + "(" + (cutdownTime - aLong) + ")"); + } else { + bind(id, 0); + dialog.dismiss(); + if (subscribe != null) + subscribe.dispose(); + subscribe = null; + } + } + }); + dialog.setMessage(phoneNum + "请求绑定你的设备") + .setTitle("设备绑定请求") + .setPositive("允许") + .setNegtive("拒绝") + .setOnClickBottomListener(new CustomDialog.OnClickBottomListener() { + @Override + public void onPositiveClick() { + bind(id, 1); + dialog.dismiss(); + if (subscribe != null) + subscribe.dispose(); + subscribe = null; + } + + @Override + public void onNegtiveClick() { + bind(id, 0); + ToastUtil.show("设备取消绑定"); + dialog.dismiss(); + if (subscribe != null) + subscribe.dispose(); + subscribe = null; + } + }); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.show(); + dialog.getWindow().setGravity(Gravity.CENTER); + dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } + + private void bind(final String id, int type) { + NetInterfaceManager.getInstance() + .getbindDevicesControl() + .getBindDevices(Utils.getSerial(), id, type) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Log.e("bind", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse baseResponse) { + int code = baseResponse.code; + Log.e("bind", "onNext: " + baseResponse); + String msg = baseResponse.msg; + if (code == 200) { + ToastUtil.show("绑定成功"); + sendZyosRefreshIntent(); + } else if (code == 301) { + ToastUtil.show(msg); + } + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("bind", "onError: " + e.getMessage()); + onComplete(); + } + + @Override + public void onComplete() { + Log.e("bind", "onComplete: "); + Intent serviceIntent = new Intent(MainService.RefreshInfoReceiver.REFRESH_RECEIVER_ACTION); + mContext.sendBroadcast(serviceIntent); + Intent activityIntent = new Intent(MainActivity.REFRESHACTION); + mContext.sendBroadcast(activityIntent); + } + }); + } + + private void sendZyosRefreshIntent() { + Intent intent = new Intent(ACTION_REFRESH_BINDING_STATUS); + intent.setPackage("com.uiui.zyos"); + mContext.sendBroadcast(intent); + } + } diff --git a/app/src/main/java/com/uiuipad/find/receiver/BootReceiver.java b/app/src/main/java/com/uiuipad/find/receiver/BootReceiver.java new file mode 100644 index 0000000..eb6b981 --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/receiver/BootReceiver.java @@ -0,0 +1,33 @@ +package com.uiuipad.find.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.uiuipad.find.service.main.MainService; + +public class BootReceiver extends BroadcastReceiver { + private static String TAG = BootReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.e("SNBootReceiver", action); + if (Intent.ACTION_BOOT_COMPLETED.equals(action) + || Intent.ACTION_BATTERY_CHANGED.equals(action) + || Intent.ACTION_BATTERY_LEVEL_CHANGED.equals(action) + || Intent.ACTION_BATTERY_LOW.equals(action) + || Intent.ACTION_BATTERY_OKAY.equals(action) + || Intent.ACTION_POWER_CONNECTED.equals(action) + || Intent.ACTION_POWER_DISCONNECTED.equals(action) + || Intent.ACTION_DATE_CHANGED.equals(action) + || Intent.ACTION_TIME_TICK.equals(action) + || Intent.ACTION_USER_PRESENT.equals(action) + || Intent.ACTION_SCREEN_ON.equals(action) + || Intent.ACTION_SCREEN_OFF.equals(action) + ) { + context.startService(new Intent(context, MainService.class)); + } + } +} diff --git a/app/src/main/java/com/uiuipad/find/service/main/MainSContact.java b/app/src/main/java/com/uiuipad/find/service/main/MainSContact.java new file mode 100644 index 0000000..b4ae1c9 --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/service/main/MainSContact.java @@ -0,0 +1,15 @@ +package com.uiuipad.find.service.main; + + +import com.uiuipad.find.base.BasePresenter; +import com.uiuipad.find.base.BaseView; + +public class MainSContact { + interface Presenter extends BasePresenter { + + } + + public interface MainView extends BaseView { + + } +} diff --git a/app/src/main/java/com/uiuipad/find/service/main/MainSPresenter.java b/app/src/main/java/com/uiuipad/find/service/main/MainSPresenter.java new file mode 100644 index 0000000..e4aed5a --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/service/main/MainSPresenter.java @@ -0,0 +1,47 @@ +package com.uiuipad.find.service.main; + +import android.content.Context; + +import com.tencent.mmkv.MMKV; +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.uiuipad.find.comm.CommonConfig; + +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +/** + * @author fanhuitong + */ +public class MainSPresenter implements MainSContact.Presenter { + private static final String TAG = MainSPresenter.class.getSimpleName(); + + private MainSContact.MainView mView; + private Context mContext; + + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + + public MainSPresenter(Context context) { + this.mContext = context; + } + + private BehaviorSubject lifecycle; + + public void setLifecycle(BehaviorSubject lifecycle) { + this.lifecycle = lifecycle; + } + + public BehaviorSubject getLifecycle() { + return lifecycle; + } + + @Override + public void attachView(MainSContact.MainView view) { + this.mView = view; + } + + @Override + public void detachView() { + this.mView = null; + } + + +} diff --git a/app/src/main/java/com/uiuipad/find/service/main/MainService.java b/app/src/main/java/com/uiuipad/find/service/main/MainService.java new file mode 100644 index 0000000..fe8acab --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/service/main/MainService.java @@ -0,0 +1,190 @@ +package com.uiuipad.find.service.main; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.IBinder; +import android.text.TextUtils; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.blankj.utilcode.util.NetworkUtils; +import com.tencent.mmkv.MMKV; +import com.trello.rxlifecycle4.LifecycleProvider; +import com.trello.rxlifecycle4.LifecycleTransformer; +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.trello.rxlifecycle4.android.RxLifecycleAndroid; +import com.uiuipad.find.R; +import com.uiuipad.find.activity.main.MainActivity; +import com.uiuipad.find.comm.CommonConfig; + +import org.jetbrains.annotations.NotNull; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +/** + * @author fanhuitong + */ +public class MainService extends Service implements MainSContact.MainView, NetworkUtils.OnNetworkStatusChangedListener, LifecycleProvider { + private static final String TAG = MainService.class.getSimpleName(); + + public MainSPresenter mPresenter; + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + + //执行所有请求的时间 + long runningTime; + + private final BehaviorSubject lifecycleSubject = BehaviorSubject.create(); + + @NotNull + @Override + public Observable lifecycle() { + return lifecycleSubject.hide(); + } + + @NotNull + @Override + public LifecycleTransformer bindUntilEvent(@NotNull ActivityEvent event) { + return RxLifecycle.bindUntilEvent(lifecycleSubject, event); + } + + @NotNull + @Override + public LifecycleTransformer bindToLifecycle() { + return RxLifecycleAndroid.bindActivity(lifecycleSubject); + } + + @Override + public void onDisconnected() { + Log.e(TAG, "网络未连接"); + } + + @Override + public void onConnected(NetworkUtils.NetworkType networkType) { + + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + lifecycleSubject.onNext(ActivityEvent.CREATE); + mPresenter = new MainSPresenter(this); + mPresenter.setLifecycle(lifecycleSubject); + mPresenter.attachView(this); + + registerReceivers(); + + } + + private void registerReceivers() { +// registerWiFiReceiver(); + registerRefreshReceiver(); +// registerPasswordReceiver(); + } + + private static final String CHANNEL_ID = "CHANNEL_ID"; + private static final String channel_name = "系统通知"; + private static final String channel_description = "我的设备系统通知"; + + private void createNotificationChannel() { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = channel_name; + String description = channel_description; + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + private NotificationManagerCompat mNotificationManagerCompat; + private int NotificationID = 1; + + private void sendSimpleNotification() { + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "CHANNEL_ID") + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("设备管控中") +// .setContentText("测试内容") + .setAutoCancel(false) + .setShowWhen(false) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_MAX); + // notificationId is a unique int for each notification that you must define + mNotificationManagerCompat.notify(NotificationID, builder.build()); + } + + + private RefreshInfoReceiver mRefreshInfoReceiver; + + private void registerRefreshReceiver() { + if (mRefreshInfoReceiver == null) { + mRefreshInfoReceiver = new RefreshInfoReceiver(); + } + IntentFilter filter = new IntentFilter(); + filter.addAction(RefreshInfoReceiver.REFRESH_RECEIVER_ACTION); + registerReceiver(mRefreshInfoReceiver, filter); + } + + + public class RefreshInfoReceiver extends BroadcastReceiver { + public static final String REFRESH_RECEIVER_ACTION = "Receiver_Refresh_Action"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.e("RefreshInfoReceiver", "onReceive: " + action); + if (!TextUtils.isEmpty(action)) { + + } + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.e(TAG, "onStartCommand: " + System.currentTimeMillis()); + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + lifecycleSubject.onNext(ActivityEvent.DESTROY); + NetworkUtils.unregisterNetworkStatusChangedListener(this); + mPresenter.detachView(); + +// if (mWifiReceiver != null) { +// unregisterReceiver(mRefreshInfoReceiver); +// } + if (mRefreshInfoReceiver != null) { + unregisterReceiver(mRefreshInfoReceiver); + } +// if (mPasswdReceiver != null) { +// unregisterReceiver(mPasswdReceiver); +// } + } + +} diff --git a/app/src/main/java/com/uiuipad/find/util/ToastUtil.java b/app/src/main/java/com/uiuipad/find/util/ToastUtil.java new file mode 100644 index 0000000..e26072f --- /dev/null +++ b/app/src/main/java/com/uiuipad/find/util/ToastUtil.java @@ -0,0 +1,46 @@ +package com.uiuipad.find.util; + +import android.graphics.Color; +import android.util.Log; +import android.view.Gravity; + +import com.blankj.utilcode.util.ColorUtils; +import com.blankj.utilcode.util.ToastUtils; +import com.uiuipad.find.BuildConfig; +import com.uiuipad.find.R; + +public class ToastUtil { + private static String TAG = ToastUtil.class.getSimpleName(); + + public static void show(final String msg) { + ToastUtils.make() + .setBgColor(ColorUtils.getColor(R.color.toast_color)) + .setTextColor(Color.WHITE) + .setGravity(Gravity.CENTER, 0, 0) + .setNotUseSystemToast() + .show(msg); + } + + public static void debugShow(final String msg) { + if (BuildConfig.DEBUG) { + ToastUtils.make() + .setBgColor(ColorUtils.getColor(R.color.toast_color)) + .setTextColor(Color.RED) + .setGravity(Gravity.CENTER, 0, 0) + .setNotUseSystemToast() + .setDurationIsLong(true) + .show(msg); + } else { + Log.e(TAG, "debugShow: " + msg); + } + } + + public static void showCenter(final String msg) { + ToastUtils.make() + .setBgColor(ColorUtils.getColor(R.color.toast_color)) + .setTextColor(Color.WHITE) + .setGravity(Gravity.CENTER, 0, 0) + .setNotUseSystemToast() + .show(msg); + } +} diff --git a/app/src/main/res/drawable-hdpi/bt_return.png b/app/src/main/res/drawable-hdpi/bt_return.png new file mode 100644 index 0000000..af5d346 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/bt_return.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_more.png b/app/src/main/res/drawable-hdpi/icon_more.png new file mode 100644 index 0000000..230f521 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_more.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_more_white.png b/app/src/main/res/drawable-hdpi/icon_more_white.png new file mode 100644 index 0000000..f5869af Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_more_white.png differ diff --git a/app/src/main/res/drawable/bg_dialog.xml b/app/src/main/res/drawable/bg_dialog.xml new file mode 100644 index 0000000..64427cc --- /dev/null +++ b/app/src/main/res/drawable/bg_dialog.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app/src/main/res/drawable/bt_default_normnl.xml b/app/src/main/res/drawable/bt_default_normnl.xml new file mode 100644 index 0000000..20499a5 --- /dev/null +++ b/app/src/main/res/drawable/bt_default_normnl.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bt_default_pressed.xml b/app/src/main/res/drawable/bt_default_pressed.xml new file mode 100644 index 0000000..328d667 --- /dev/null +++ b/app/src/main/res/drawable/bt_default_pressed.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bt_default_selector.xml b/app/src/main/res/drawable/bt_default_selector.xml new file mode 100644 index 0000000..1ea6ea8 --- /dev/null +++ b/app/src/main/res/drawable/bt_default_selector.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/card_background.xml b/app/src/main/res/drawable/card_background.xml new file mode 100644 index 0000000..039758e --- /dev/null +++ b/app/src/main/res/drawable/card_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_button_bg.xml b/app/src/main/res/drawable/default_button_bg.xml new file mode 100644 index 0000000..45b8ea0 --- /dev/null +++ b/app/src/main/res/drawable/default_button_bg.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_button_cancel_bg.xml b/app/src/main/res/drawable/default_button_cancel_bg.xml new file mode 100644 index 0000000..c9d6327 --- /dev/null +++ b/app/src/main/res/drawable/default_button_cancel_bg.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/join_background.xml b/app/src/main/res/drawable/join_background.xml new file mode 100644 index 0000000..521aed6 --- /dev/null +++ b/app/src/main/res/drawable/join_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/joined_background.xml b/app/src/main/res/drawable/joined_background.xml new file mode 100644 index 0000000..5e209a7 --- /dev/null +++ b/app/src/main/res/drawable/joined_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 34528b9..6e21235 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -4,8 +4,838 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".activity.main.MainActivity"> + + + + + + + + +