From f37ce20fcb6bb2523ed2bec7dbd578ed720cca76 Mon Sep 17 00:00:00 2001 From: pppscn <35696959@qq.com> Date: Wed, 26 Jan 2022 00:53:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E5=85=8B=E9=9A=86=E6=9C=BA=E5=88=B6=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E9=AB=98=E6=88=90=E5=8A=9F=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../idormy/sms/forwarder/CloneActivity.java | 292 +++++++++++------- .../idormy/sms/forwarder/CrashHandler.java | 2 +- .../sms/forwarder/model/vo/CloneInfoVo.java | 28 ++ .../sms/forwarder/receiver/BaseServlet.java | 69 ++++- .../sms/forwarder/sender/HttpServer.java | 7 +- .../sms/forwarder/utils/BackupDbTask.java | 118 +++++++ .../idormy/sms/forwarder/utils/Define.java | 2 + .../sms/forwarder/utils/DownloadUtil.java | 103 ++++++ .../idormy/sms/forwarder/utils/NetUtil.java | 1 - .../sms/forwarder/utils/SettingUtil.java | 13 + .../idormy/sms/forwarder/utils/TimeUtil.java | 9 + .../idormy/sms/forwarder/utils/ZipUtils.java | 210 +++++++++++++ .../idormy/sms/forwarder/view/StepBar.java | 2 - app/src/main/res/layout/activity_setting.xml | 8 +- app/src/main/res/values-en/strings.xml | 6 +- app/src/main/res/values/strings.xml | 12 +- 16 files changed, 748 insertions(+), 134 deletions(-) create mode 100644 app/src/main/java/com/idormy/sms/forwarder/model/vo/CloneInfoVo.java create mode 100644 app/src/main/java/com/idormy/sms/forwarder/utils/BackupDbTask.java create mode 100644 app/src/main/java/com/idormy/sms/forwarder/utils/DownloadUtil.java create mode 100644 app/src/main/java/com/idormy/sms/forwarder/utils/ZipUtils.java diff --git a/app/src/main/java/com/idormy/sms/forwarder/CloneActivity.java b/app/src/main/java/com/idormy/sms/forwarder/CloneActivity.java index 6d2d2ac2..aa6a6282 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/CloneActivity.java +++ b/app/src/main/java/com/idormy/sms/forwarder/CloneActivity.java @@ -1,8 +1,12 @@ package com.idormy.sms.forwarder; import android.annotation.SuppressLint; +import android.app.ProgressDialog; import android.content.Context; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.TextUtils; import android.util.Log; import android.widget.Button; import android.widget.TextView; @@ -11,26 +15,32 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import com.alibaba.fastjson.JSON; +import com.idormy.sms.forwarder.model.vo.CloneInfoVo; +import com.idormy.sms.forwarder.receiver.BaseServlet; import com.idormy.sms.forwarder.receiver.RebootBroadcastReceiver; import com.idormy.sms.forwarder.sender.HttpServer; -import com.idormy.sms.forwarder.utils.LogUtil; +import com.idormy.sms.forwarder.utils.BackupDbTask; +import com.idormy.sms.forwarder.utils.Define; +import com.idormy.sms.forwarder.utils.DownloadUtil; +import com.idormy.sms.forwarder.utils.HttpUtil; import com.idormy.sms.forwarder.utils.NetUtil; import com.idormy.sms.forwarder.utils.SettingUtil; import com.idormy.sms.forwarder.view.IPEditText; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.TimeUnit; import okhttp3.Call; import okhttp3.Callback; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; public class CloneActivity extends AppCompatActivity { @@ -38,6 +48,28 @@ public class CloneActivity extends AppCompatActivity { private Context context; private String serverIp; public static final String DATABASE_NAME = "sms_forwarder.db"; + private IPEditText textServerIp; + private TextView sendTxt; + private TextView receiveTxt; + private Button sendBtn; + public static final int TOAST = 0x9731994; + public static final int DOWNLOAD = 0x9731995; + + //消息处理者,创建一个Handler的子类对象,目的是重写Handler的处理消息的方法(handleMessage()) + @SuppressWarnings("deprecation") + @SuppressLint("HandlerLeak") + private final Handler handError = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == TOAST) { + Toast.makeText(CloneActivity.this, msg.getData().getString("DATA"), Toast.LENGTH_LONG).show(); + } else if (msg.what == DOWNLOAD) { + String savePath = context.getCacheDir().getPath() + File.separator + BackupDbTask.BACKUP_FILE; + Log.d(TAG, savePath); + downloadFile(msg.getData().getString("URL"), context.getCacheDir().getPath(), BackupDbTask.BACKUP_FILE); + } + } + }; @Override public void onCreate(Bundle savedInstanceState) { @@ -49,24 +81,27 @@ public class CloneActivity extends AppCompatActivity { setContentView(R.layout.activity_clone); Log.d(TAG, "onCreate: " + RebootBroadcastReceiver.class.getName()); + HttpUtil.init(this); + HttpServer.init(this); } + @SuppressWarnings({"rawtypes", "unchecked", "deprecation"}) @SuppressLint("SetTextI18n") @Override protected void onStart() { super.onStart(); Log.d(TAG, "onStart"); - IPEditText textServerIp = findViewById(R.id.textServerIp); - - - TextView sendTxt = findViewById(R.id.sendTxt); - TextView receiveTxt = findViewById(R.id.receiveTxt); + sendBtn = findViewById(R.id.sendBtn); + sendTxt = findViewById(R.id.sendTxt); + TextView ipText = findViewById(R.id.ipText); + textServerIp = findViewById(R.id.textServerIp); + receiveTxt = findViewById(R.id.receiveTxt); + Button receiveBtn = findViewById(R.id.receiveBtn); - Button sendBtn = findViewById(R.id.sendBtn); serverIp = NetUtil.getLocalIp(CloneActivity.this); - TextView ipText = findViewById(R.id.ipText); ipText.setText(serverIp); + if (HttpServer.asRunning()) { sendBtn.setText(R.string.stop); sendTxt.setText(R.string.server_has_started); @@ -77,9 +112,15 @@ public class CloneActivity extends AppCompatActivity { } sendBtn.setOnClickListener(v -> { if (!HttpServer.asRunning() && NetUtil.NETWORK_WIFI != NetUtil.getNetWorkStatus()) { - Toast.makeText(CloneActivity.this, R.string.no_wifi_network, Toast.LENGTH_SHORT).show(); + Toast(handError, TAG, getString(R.string.no_wifi_network)); return; } + + //备份文件 + BackupDbTask task = new BackupDbTask(this); + String backup_version = task.doInBackground(BackupDbTask.COMMAND_BACKUP); + Log.d(TAG, "backup_version = " + backup_version); + SettingUtil.switchEnableHttpServer(!SettingUtil.getSwitchEnableHttpServer()); if (!HttpServer.update()) { SettingUtil.switchEnableHttpServer(!SettingUtil.getSwitchEnableHttpServer()); @@ -96,128 +137,106 @@ public class CloneActivity extends AppCompatActivity { } }); - Button receiveBtn = findViewById(R.id.receiveBtn); receiveBtn.setOnClickListener(v -> { if (HttpServer.asRunning()) { receiveTxt.setText(R.string.sender_cannot_receive); - Toast.makeText(CloneActivity.this, R.string.sender_cannot_receive, Toast.LENGTH_SHORT).show(); + Toast(handError, TAG, getString(R.string.sender_cannot_receive)); return; } if (NetUtil.NETWORK_WIFI != NetUtil.getNetWorkStatus()) { receiveTxt.setText(R.string.no_wifi_network); - Toast.makeText(CloneActivity.this, R.string.no_wifi_network, Toast.LENGTH_SHORT).show(); + Toast(handError, TAG, getString(R.string.no_wifi_network)); return; } serverIp = textServerIp.getIP(); if (serverIp == null || serverIp.isEmpty()) { receiveTxt.setText(R.string.invalid_server_ip); - Toast.makeText(CloneActivity.this, R.string.invalid_server_ip, Toast.LENGTH_SHORT).show(); + Toast(handError, TAG, getString(R.string.invalid_server_ip)); return; } - //下载连接 - final String url = "http://" + serverIp + ":5000/"; - Log.d(TAG, url); - //保存路径 - final String savePath = context.getCacheDir().getPath() + File.separator + DATABASE_NAME; - Log.d(TAG, savePath); - final long startTime = System.currentTimeMillis(); - Log.i(TAG, "startTime=" + startTime); - OkHttpClient okHttpClient = new OkHttpClient(); - Request request = new Request.Builder().url(url).addHeader("Connection", "close").build(); - okHttpClient.newCall(request).enqueue(new Callback() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + //设置读取超时时间 + OkHttpClient client = builder + .readTimeout(Define.REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(Define.REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .connectTimeout(Define.REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .build(); + + Map msgMap = new HashMap(); + msgMap.put("versionCode", SettingUtil.getVersionCode()); + msgMap.put("versionName", SettingUtil.getVersionName()); + + String requestMsg = JSON.toJSONString(msgMap); + Log.i(TAG, "requestMsg:" + requestMsg); + RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), requestMsg); + + //请求链接:post 获取版本信息,get 下载备份文件 + final String requestUrl = "http://" + serverIp + ":" + Define.HTTP_SERVER_PORT + BaseServlet.CLONE_PATH + "?" + System.currentTimeMillis(); + Log.i(TAG, "requestUrl:" + requestUrl); + + //获取版本信息 + final Request request = new Request.Builder() + .url(requestUrl) + .addHeader("Content-Type", "application/json; charset=utf-8") + .post(requestBody) + .build(); + client.newCall(request).enqueue(new Callback() { @Override - public void onFailure(@NonNull Call call, @NonNull IOException e) { - e.printStackTrace(); - //Toast.makeText(CloneActivity.this, R.string.download_failed + e.getMessage(), Toast.LENGTH_SHORT).show(); + public void onFailure(@NonNull Call call, @NonNull final IOException e) { + Toast(handError, TAG, "从发送端获取一键克隆信息失败"); } @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - InputStream is = null; - byte[] buf = new byte[2048]; - int len; - FileOutputStream fos = null; + public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + final String responseStr = Objects.requireNonNull(response.body()).string(); + Log.d(TAG, "Response:" + response.code() + "," + responseStr); - try { - is = Objects.requireNonNull(response.body()).byteStream(); - long total = Objects.requireNonNull(response.body()).contentLength(); - File file = new File(savePath, url.substring(url.lastIndexOf("/") + 1)); - fos = new FileOutputStream(file); - long sum = 0; - while ((len = is.read(buf)) != -1) { - fos.write(buf, 0, len); - sum += len; - int progress = (int) (sum * 1.0f / total * 100); - Log.e(TAG, "download progress : " + progress); - } - fos.flush(); - Log.e(TAG, "download success"); - Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime)); - //Toast.makeText(CloneActivity.this, R.string.download_success, Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - e.printStackTrace(); - //Toast.makeText(CloneActivity.this, R.string.download_failed + e.getMessage(), Toast.LENGTH_SHORT).show(); - } finally { - try { - if (is != null) is.close(); - } catch (IOException ignored) { - } - try { - if (fos != null) fos.close(); - } catch (IOException ignored) { - } + if (TextUtils.isEmpty(responseStr)) { + Toast(handError, TAG, "从发送端获取一键克隆信息失败"); + return; } - } - }); - //TODO:替换sqlite - File dbFile = new File(savePath); - FileInputStream fis; - try { - fis = new FileInputStream(dbFile); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return; - } + try { + CloneInfoVo cloneInfoVo = JSON.parseObject(responseStr, CloneInfoVo.class); + if (SettingUtil.getVersionCode() != cloneInfoVo.getVersionCode()) { + Toast(handError, TAG, "发送端与接收端的APP版本不一致,无法一键克隆!"); + return; + } - String outFileName = context.getDatabasePath(DATABASE_NAME).getAbsolutePath(); - Log.d(TAG, outFileName); + //应用配置 + SettingUtil.switchEnableSms(cloneInfoVo.isEnableSms()); + SettingUtil.switchEnablePhone(cloneInfoVo.isEnablePhone()); + SettingUtil.switchCallType1(cloneInfoVo.isCallType1()); + SettingUtil.switchCallType2(cloneInfoVo.isCallType2()); + SettingUtil.switchCallType3(cloneInfoVo.isCallType3()); + SettingUtil.switchEnableAppNotify(cloneInfoVo.isEnableAppNotify()); + SettingUtil.switchCancelAppNotify(cloneInfoVo.isCancelAppNotify()); + SettingUtil.smsHubApiUrl(cloneInfoVo.getSmsHubApiUrl()); + SettingUtil.setBatteryLevelAlarmMin(cloneInfoVo.getBatteryLevelAlarmMin()); + SettingUtil.setBatteryLevelAlarmMax(cloneInfoVo.getBatteryLevelAlarmMax()); + SettingUtil.switchBatteryLevelAlarmOnce(cloneInfoVo.isBatteryLevelAlarmOnce()); + SettingUtil.setRetryTimes(cloneInfoVo.getRetryTimes()); + SettingUtil.setDelayTime(cloneInfoVo.getDelayTime()); + SettingUtil.switchSmsTemplate(cloneInfoVo.isEnableSmsTemplate()); + SettingUtil.setSmsTemplate(cloneInfoVo.getSmsTemplate()); - // Open the empty db as the output stream - OutputStream output; - try { - output = new FileOutputStream(outFileName); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return; - } + //下载备份文件 + Message msg = new Message(); + msg.what = DOWNLOAD; + Bundle bundle = new Bundle(); + bundle.putString("URL", requestUrl); + msg.setData(bundle); + handError.sendMessage(msg); - // Transfer bytes from the input file to the output file - byte[] buffer = new byte[1024]; - int length; - while (true) { - try { - if (!((length = fis.read(buffer)) > 0)) break; - output.write(buffer, 0, length); - } catch (IOException e) { - e.printStackTrace(); + } catch (Exception e) { + Toast(handError, TAG, "一键克隆失败:" + e.getMessage()); + } } - } - - // Close the streams - try { - output.flush(); - output.close(); - fis.close(); - } catch (IOException e) { - e.printStackTrace(); - } - LogUtil.delLog(null, null); + }); - receiveTxt.setText(R.string.download_success); }); } @@ -231,4 +250,63 @@ public class CloneActivity extends AppCompatActivity { TextView ipText = findViewById(R.id.ipText); ipText.setText(getString(R.string.local_ip) + serverIp); } + + /** + * 文件下载 + * + * @param url 下载链接 + */ + public void downloadFile(String url, final String destFileDir, final String destFileName) { + ProgressDialog progressDialog = new ProgressDialog(context); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setTitle("正在下载"); + progressDialog.setMessage("请稍后..."); + progressDialog.setProgress(0); + progressDialog.setMax(100); + progressDialog.show(); + progressDialog.setCancelable(false); + DownloadUtil.get().download(url, destFileDir, destFileName, new DownloadUtil.OnDownloadListener() { + @Override + public void onDownloadSuccess(File file) { + if (progressDialog.isShowing()) { + Toast(handError, TAG, "下载完成,正准备还原数据..."); + progressDialog.dismiss(); + } + //下载完成进行相关逻辑操作 + Log.d(TAG, file.getPath()); + + //还原数据库 + BackupDbTask task = new BackupDbTask(context); + String backup_version = task.doInBackground(BackupDbTask.COMMAND_RESTORE); + Log.d(TAG, "backup_version = " + backup_version); + + Toast(handError, TAG, "一键克隆操作成功!请进入通用设置检查各项开关是否已开启!"); + } + + @Override + public void onDownloading(int progress) { + progressDialog.setProgress(progress); + } + + @SuppressLint("SetTextI18n") + @Override + public void onDownloadFailed(Exception e) { + //下载异常进行相关提示操作 + Log.e(TAG, "下载失败:" + e.getMessage()); + Toast(handError, TAG, "下载失败:" + e.getMessage()); + } + }); + } + + public static void Toast(Handler handError, String Tag, String data) { + Log.i(Tag, data); + if (handError != null) { + Message msg = new Message(); + msg.what = TOAST; + Bundle bundle = new Bundle(); + bundle.putString("DATA", data); + msg.setData(bundle); + handError.sendMessage(msg); + } + } } diff --git a/app/src/main/java/com/idormy/sms/forwarder/CrashHandler.java b/app/src/main/java/com/idormy/sms/forwarder/CrashHandler.java index 08d81c52..0f8c2edb 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/CrashHandler.java +++ b/app/src/main/java/com/idormy/sms/forwarder/CrashHandler.java @@ -29,7 +29,7 @@ import java.util.Objects; * UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告 */ public class CrashHandler implements UncaughtExceptionHandler { - public static final String TAG = "CrashHandler"; + private static final String TAG = "CrashHandler"; //系统默认的UncaughtException处理类 private Thread.UncaughtExceptionHandler mDefaultHandler; //CrashHandler实例 diff --git a/app/src/main/java/com/idormy/sms/forwarder/model/vo/CloneInfoVo.java b/app/src/main/java/com/idormy/sms/forwarder/model/vo/CloneInfoVo.java new file mode 100644 index 00000000..846fc508 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/model/vo/CloneInfoVo.java @@ -0,0 +1,28 @@ +package com.idormy.sms.forwarder.model.vo; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class CloneInfoVo implements Serializable { + + private int versionCode; + private String versionName; + private boolean enableSms; + private boolean enablePhone; + private boolean callType1; + private boolean callType2; + private boolean callType3; + private boolean enableAppNotify; + private boolean cancelAppNotify; + private String smsHubApiUrl; + private int batteryLevelAlarmMin; + private int batteryLevelAlarmMax; + private boolean batteryLevelAlarmOnce; + private int retryTimes; + private int delayTime; + private boolean enableSmsTemplate; + private String smsTemplate; + +} diff --git a/app/src/main/java/com/idormy/sms/forwarder/receiver/BaseServlet.java b/app/src/main/java/com/idormy/sms/forwarder/receiver/BaseServlet.java index 3158defe..477c1170 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/receiver/BaseServlet.java +++ b/app/src/main/java/com/idormy/sms/forwarder/receiver/BaseServlet.java @@ -6,8 +6,9 @@ import android.util.Log; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.util.IOUtils; -import com.idormy.sms.forwarder.CloneActivity; import com.idormy.sms.forwarder.model.vo.SmsHubVo; +import com.idormy.sms.forwarder.utils.BackupDbTask; +import com.idormy.sms.forwarder.utils.SettingUtil; import com.idormy.sms.forwarder.utils.SmsHubActionHandler; import org.eclipse.jetty.server.Server; @@ -21,7 +22,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.Reader; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.servlet.MultipartConfigElement; import javax.servlet.ServletOutputStream; @@ -36,7 +39,7 @@ import javax.servlet.http.HttpServletResponse; public class BaseServlet extends HttpServlet { public static final int BUFFER_SIZE = 1 << 12; - public static final String CLONE_PATH = "/"; + public static final String CLONE_PATH = "/clone"; public static final String SMSHUB_PATH = "/send_api"; private static final long serialVersionUID = 1L; private static final String TAG = "BaseServlet"; @@ -87,12 +90,27 @@ public class BaseServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String msg = "HTTP method POST is not supported by this URL"; if (CLONE_PATH.equals(path)) { - clone(req, resp); + clone_api(req, resp); } else if (SMSHUB_PATH.equals(path)) { send_api(req, resp); + } else if ("1.1".endsWith(req.getProtocol())) { + resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); + } else { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String msg = "HTTP method GET is not supported by this URL"; + if (CLONE_PATH.equals(path)) { + clone(req, resp); + } else if ("1.1".endsWith(req.getProtocol())) { + resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); } else { - notFound(req, resp); + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); } } @@ -155,9 +173,48 @@ public class BaseServlet extends HttpServlet { writer.println(text); } + //一键克隆——查询接口 + @SuppressWarnings({"unchecked", "rawtypes"}) + private void clone_api(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setCharacterEncoding("utf-8"); + PrintWriter writer = resp.getWriter(); + BufferedReader reader = req.getReader(); + try { + Map msgMap = new HashMap(); + msgMap.put("versionCode", SettingUtil.getVersionCode()); + msgMap.put("versionName", SettingUtil.getVersionName()); + msgMap.put("enableSms", SettingUtil.getSwitchEnableSms()); + msgMap.put("enablePhone", SettingUtil.getSwitchEnablePhone()); + msgMap.put("callType1", SettingUtil.getSwitchCallType1()); + msgMap.put("callType2", SettingUtil.getSwitchCallType2()); + msgMap.put("callType3", SettingUtil.getSwitchCallType3()); + msgMap.put("enableAppNotify", SettingUtil.getSwitchEnableAppNotify()); + msgMap.put("cancelAppNotify", SettingUtil.getSwitchCancelAppNotify()); + msgMap.put("smsHubApiUrl", SettingUtil.getSmsHubApiUrl()); + msgMap.put("batteryLevelAlarmMin", SettingUtil.getBatteryLevelAlarmMin()); + msgMap.put("batteryLevelAlarmMax", SettingUtil.getBatteryLevelAlarmMax()); + msgMap.put("batteryLevelAlarmOnce", SettingUtil.getBatteryLevelAlarmOnce()); + msgMap.put("retryTimes", SettingUtil.getRetryTimes()); + msgMap.put("delayTime", SettingUtil.getDelayTime()); + msgMap.put("enableSmsTemplate", SettingUtil.getSwitchSmsTemplate()); + msgMap.put("smsTemplate", SettingUtil.getSmsTemplate()); + + resp.setContentType("application/json;charset=utf-8"); + String text = JSON.toJSONString(msgMap); + writer.println(text); + } catch (Exception e) { + e.printStackTrace(); + printErrMsg(resp, writer, e); + } finally { + IOUtils.close(reader); + IOUtils.close(writer); + } + } + + //一键克隆——下载接口 private void clone(HttpServletRequest req, HttpServletResponse resp) throws IOException { - File file = context.getDatabasePath(CloneActivity.DATABASE_NAME); - resp.addHeader("Content-Disposition", "attachment;filename=" + CloneActivity.DATABASE_NAME); + File file = new File(context.getCacheDir().getPath() + File.separator + BackupDbTask.BACKUP_FILE); + resp.addHeader("Content-Disposition", "attachment;filename=" + BackupDbTask.BACKUP_FILE); ServletOutputStream outputStream = resp.getOutputStream(); InputStream inputStream = new FileInputStream(file); try { diff --git a/app/src/main/java/com/idormy/sms/forwarder/sender/HttpServer.java b/app/src/main/java/com/idormy/sms/forwarder/sender/HttpServer.java index 21dbc0ea..cb20ecc0 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/sender/HttpServer.java +++ b/app/src/main/java/com/idormy/sms/forwarder/sender/HttpServer.java @@ -8,6 +8,7 @@ import android.widget.Toast; import com.idormy.sms.forwarder.R; import com.idormy.sms.forwarder.model.vo.SmsHubVo; import com.idormy.sms.forwarder.receiver.BaseServlet; +import com.idormy.sms.forwarder.utils.Define; import com.idormy.sms.forwarder.utils.NetUtil; import com.idormy.sms.forwarder.utils.SettingUtil; import com.idormy.sms.forwarder.utils.SmsHubActionHandler; @@ -16,7 +17,6 @@ import org.eclipse.jetty.server.Server; public class HttpServer { - private static final int port = 5000; private static Boolean hasInit = false; private static Server jettyServer; @SuppressLint("StaticFieldLeak") @@ -33,7 +33,7 @@ public class HttpServer { hasInit = true; HttpServer.context = context; SmsHubActionHandler.init(context); - jettyServer = new Server(port); + jettyServer = new Server(Define.HTTP_SERVER_PORT); BaseServlet.addServlet(jettyServer, context); } } @@ -102,13 +102,12 @@ public class HttpServer { //}).start(); } - private static void stop() { if (Boolean.FALSE.equals(asStopp())) { try { if (jettyServer != null) { jettyServer.stop(); - // jettyServer = new Server(port); + //jettyServer = new Server(port); } } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/BackupDbTask.java b/app/src/main/java/com/idormy/sms/forwarder/utils/BackupDbTask.java new file mode 100644 index 00000000..7cfd9262 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/BackupDbTask.java @@ -0,0 +1,118 @@ +package com.idormy.sms.forwarder.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.channels.FileChannel; + +@SuppressWarnings("ResultOfMethodCallIgnored") +public class BackupDbTask { + private static final String TAG = "BackupDbTask"; + public static final String COMMAND_BACKUP = "backupDatabase"; + public static final String COMMAND_RESTORE = "restoreDatabase"; + public final static String BACKUP_FOLDER = "SmsForwarder"; + public final static String BACKUP_FILE = "SmsForwarder.zip"; + public String backup_version; + @SuppressLint("StaticFieldLeak") + private static Context mContext; + + public BackupDbTask(Context context) { + mContext = context; + } + + private static File getExternalStoragePublicDir() { + // /sdcard/SmsForwarder/ + String path = Environment.getExternalStorageDirectory() + File.separator + BACKUP_FOLDER + File.separator; + File dir = new File(path); + if (!dir.exists()) dir.mkdirs(); + return dir; + } + + public String doInBackground(String command) { + File dbFile = mContext.getDatabasePath(DbHelper.DATABASE_NAME);// 默认路径是 /data/data/(包名)/databases/* + File dbFile_shm = mContext.getDatabasePath(DbHelper.DATABASE_NAME + "-journal");// 默认路径是 /data/data/(包名)/databases/* + //File dbFile_wal = mContext.getDatabasePath("event_database-wal");// 默认路径是 /data/data/(包名)/databases/* + + String bakFolder = mContext.getCacheDir().getPath() + File.separator + BACKUP_FOLDER; + String zipFile = mContext.getCacheDir().getPath() + File.separator + BACKUP_FILE; + Log.d(TAG, "备份目录名:" + bakFolder + ",备份文件名:" + zipFile); + + File exportDir = new File(mContext.getCacheDir().getPath(), BACKUP_FOLDER);//直接丢在 cache 目录,可以在在关于目录下清除缓存 + if (!exportDir.exists()) exportDir.mkdirs(); + + File backup = new File(bakFolder, dbFile.getName());//备份文件与原数据库文件名一致 + File backup_shm = new File(bakFolder, dbFile_shm.getName());//备份文件与原数据库文件名一致 + //File backup_wal = new File(bakFolder, dbFile_wal.getName());//备份文件与原数据库文件名一致 + if (command.equals(COMMAND_BACKUP)) { + try { + //备份文件 + backup.createNewFile(); + backup_shm.createNewFile(); + //backup_wal.createNewFile(); + fileCopy(dbFile, backup);//数据库文件拷贝至备份文件 + fileCopy(dbFile_shm, backup_shm);//数据库文件拷贝至备份文件 + //fileCopy(dbFile_wal, backup_wal);//数据库文件拷贝至备份文件 + //backup.setLastModified(MyTimeUtils.getTimeLong()); + + backup_version = TimeUtil.getTimeString("yyyy.MM.dd_HH:mm:ss"); + Log.d(TAG, "backup ok! 备份目录:" + backup.getName() + "\t" + backup_version); + + //打包文件 + ZipUtils.ZipFolder(bakFolder, zipFile); + Log.d(TAG, "备份Zip包:" + zipFile); + + return backup_version; + } catch (Exception e) { + e.printStackTrace(); + Log.d(TAG, "backup fail! 备份文件名:" + backup.getName()); + return null; + } + } else if (command.equals(COMMAND_RESTORE)) { + try { + //解压文件 + ZipUtils.UnZipFolder(zipFile, bakFolder); + Log.d(TAG, "解压Zip包:" + zipFile); + + //还原文件 + fileCopy(backup, dbFile);//备份文件拷贝至数据库文件 + fileCopy(backup_shm, dbFile_shm);//备份文件拷贝至数据库文件 + //fileCopy(backup_wal, dbFile_wal);//备份文件拷贝至数据库文件 + backup_version = TimeUtil.getTimeString(backup.lastModified(), "yyyy.MM.dd_HH:mm:ss"); + Log.d(TAG, "restore success! 数据库文件名:" + dbFile.getName() + "\t" + backup_version); + + try { + //noinspection BusyWait + Thread.sleep(1000); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException(e.getMessage()); + } + + LogUtil.delLog(null, null); + + return backup_version; + } catch (Exception e) { + e.printStackTrace(); + Log.d(TAG, "restore fail! 数据库文件名:" + dbFile.getName()); + return null; + } + } else { + return null; + } + } + + private void fileCopy(File dbFile, File backup) { + try (FileChannel inChannel = new FileInputStream(dbFile).getChannel(); FileChannel outChannel = new FileOutputStream(backup).getChannel()) { + inChannel.transferTo(0, inChannel.size(), outChannel); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/Define.java b/app/src/main/java/com/idormy/sms/forwarder/utils/Define.java index b3f031cc..0fbbd9d8 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/Define.java +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/Define.java @@ -36,4 +36,6 @@ public class Define { //OkHttp 请求超时时间 public static final int REQUEST_TIMEOUT_SECONDS = 5; + //HttpServer 服务端口 + public static final int HTTP_SERVER_PORT = 5000; } diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/DownloadUtil.java b/app/src/main/java/com/idormy/sms/forwarder/utils/DownloadUtil.java new file mode 100644 index 00000000..7fe46fc2 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/DownloadUtil.java @@ -0,0 +1,103 @@ +package com.idormy.sms.forwarder.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class DownloadUtil { + private static DownloadUtil downloadUtil; + private final OkHttpClient okHttpClient; + + public static DownloadUtil get() { + if (downloadUtil == null) { + downloadUtil = new DownloadUtil(); + } + return downloadUtil; + } + + private DownloadUtil() { + okHttpClient = new OkHttpClient(); + } + + /** + * @param url 下载连接 + * @param destFileDir 下载的文件储存目录 + * @param destFileName 下载文件名称 + * @param listener 下载监听 + */ + public void download(final String url, final String destFileDir, final String destFileName, final OnDownloadListener listener) { + + Request request = new Request.Builder().url(url).addHeader("Connection", "close").build(); + okHttpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + // 下载失败监听回调 + listener.onDownloadFailed(e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + InputStream is = null; + byte[] buf = new byte[2048]; + int len = 0; + FileOutputStream fos = null; + + // 储存下载文件的目录 + File dir = new File(destFileDir); + if (!dir.exists()) dir.mkdirs(); + + File file = new File(dir, destFileName); + if (file.exists()) file.delete(); + + try { + is = response.body().byteStream(); + long total = response.body().contentLength(); + fos = new FileOutputStream(file); + long sum = 0; + while ((len = is.read(buf)) != -1) { + fos.write(buf, 0, len); + sum += len; + int progress = (int) (sum * 1.0f / total * 100); + // 下载中更新进度条 + listener.onDownloading(progress); + } + fos.flush(); + // 下载完成 + listener.onDownloadSuccess(file); + } catch (Exception e) { + listener.onDownloadFailed(e); + } finally { + try { + if (is != null) is.close(); + if (fos != null) fos.close(); + } catch (IOException e) { + } + } + } + }); + } + + public interface OnDownloadListener { + /** + * @param file 下载成功后的文件 + */ + void onDownloadSuccess(File file); + + /** + * @param progress 下载进度 + */ + void onDownloading(int progress); + + /** + * @param e 下载异常信息 + */ + void onDownloadFailed(Exception e); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/NetUtil.java b/app/src/main/java/com/idormy/sms/forwarder/utils/NetUtil.java index 087bd49b..0d3d7d75 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/NetUtil.java +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/NetUtil.java @@ -22,7 +22,6 @@ public class NetUtil { @SuppressLint("StaticFieldLeak") static Context context; - public static void init(Context context1) { //noinspection SynchronizeOnNonFinalField synchronized (hasInit) { diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtil.java b/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtil.java index df7b44c4..9fe9315a 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtil.java +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtil.java @@ -281,6 +281,19 @@ public class SettingUtil { } } + //获取应用的版本号 + public static int getVersionCode() { + PackageManager manager = context.getPackageManager(); + int code = 0; + try { + PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); + code = info.versionCode; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return code; + } + private static String getString(int resId) { return MyApplication.getContext().getString(resId); } diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/TimeUtil.java b/app/src/main/java/com/idormy/sms/forwarder/utils/TimeUtil.java index 373b803c..b91ce92f 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/TimeUtil.java +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/TimeUtil.java @@ -77,4 +77,13 @@ public class TimeUtil { return localFormatter.format(utcDate.getTime()); } + public static String getTimeString(String pattern) { + return new SimpleDateFormat(pattern, Locale.CHINESE).format(new Date()); + } + + public static String getTimeString(long time, String pattern) { + SimpleDateFormat df = new SimpleDateFormat(pattern, Locale.CHINESE); + return df.format(new Date(time)); + } + } diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/ZipUtils.java b/app/src/main/java/com/idormy/sms/forwarder/utils/ZipUtils.java new file mode 100644 index 00000000..673accb0 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/ZipUtils.java @@ -0,0 +1,210 @@ +package com.idormy.sms.forwarder.utils; + +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * Created by YuShuangPing on 2018/11/11. + */ +@SuppressWarnings("ResultOfMethodCallIgnored") +public class ZipUtils { + public static final String TAG = "ZIP"; + + public ZipUtils() { + + } + + /** + * 解压zip到指定的路径 + * + * @param zipFileString ZIP的名称 + * @param outPathString 要解压缩路径 + * @throws Exception 异常抛出 + */ + public static void UnZipFolder(String zipFileString, String outPathString) throws Exception { + ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFileString)); + ZipEntry zipEntry; + String szName; + while ((zipEntry = inZip.getNextEntry()) != null) { + szName = zipEntry.getName(); + if (zipEntry.isDirectory()) { + //获取部件的文件夹名 + szName = szName.substring(0, szName.length() - 1); + File folder = new File(outPathString + File.separator + szName); + folder.mkdirs(); + } else { + Log.e(TAG, outPathString + File.separator + szName); + File file = new File(outPathString + File.separator + szName); + if (!file.exists()) { + Log.e(TAG, "Create the file:" + outPathString + File.separator + szName); + Objects.requireNonNull(file.getParentFile()).mkdirs(); + file.createNewFile(); + } + // 获取文件的输出流 + FileOutputStream out = new FileOutputStream(file); + int len; + byte[] buffer = new byte[1024]; + // 读取(字节)字节到缓冲区 + while ((len = inZip.read(buffer)) != -1) { + // 从缓冲区(0)位置写入(字节)字节 + out.write(buffer, 0, len); + out.flush(); + } + out.close(); + } + } + inZip.close(); + } + + public static void UnZipFolder(String zipFileString, String outPathString, String szName) throws Exception { + ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFileString)); + ZipEntry zipEntry; + while ((zipEntry = inZip.getNextEntry()) != null) { + //szName = zipEntry.getName(); + if (zipEntry.isDirectory()) { + //获取部件的文件夹名 + szName = szName.substring(0, szName.length() - 1); + File folder = new File(outPathString + File.separator + szName); + folder.mkdirs(); + } else { + Log.e(TAG, outPathString + File.separator + szName); + File file = new File(outPathString + File.separator + szName); + if (!file.exists()) { + Log.e(TAG, "Create the file:" + outPathString + File.separator + szName); + Objects.requireNonNull(file.getParentFile()).mkdirs(); + file.createNewFile(); + } + // 获取文件的输出流 + FileOutputStream out = new FileOutputStream(file); + int len; + byte[] buffer = new byte[1024]; + // 读取(字节)字节到缓冲区 + while ((len = inZip.read(buffer)) != -1) { + // 从缓冲区(0)位置写入(字节)字节 + out.write(buffer, 0, len); + out.flush(); + } + out.close(); + } + } + inZip.close(); + } + + /** + * 压缩文件和文件夹 + * + * @param srcFileString 要压缩的文件或文件夹 + * @param zipFileString 解压完成的Zip路径 + * @throws Exception 异常抛出 + */ + public static void ZipFolder(String srcFileString, String zipFileString) throws Exception { + //创建ZIP + ZipOutputStream outZip = new ZipOutputStream(new FileOutputStream(zipFileString)); + //创建文件 + File file = new File(srcFileString); + //压缩 + Log.d(TAG, "---->" + file.getParent() + "===" + file.getAbsolutePath()); + ZipFiles(file.getParent() + File.separator, file.getName(), outZip); + //完成和关闭 + outZip.finish(); + outZip.close(); + } + + /** + * 压缩文件 + * + * @param folderString 文件夹 + * @param fileString 文件 + * @param zipOutputSteam zip输出流 + * @throws Exception 异常抛出 + */ + private static void ZipFiles(String folderString, String fileString, ZipOutputStream zipOutputSteam) throws Exception { + Log.d(TAG, "folderString:" + folderString + "\n" + + "fileString:" + fileString + "\n=========================="); + if (zipOutputSteam == null) + return; + File file = new File(folderString + fileString); + if (file.isFile()) { + ZipEntry zipEntry = new ZipEntry(fileString); + FileInputStream inputStream = new FileInputStream(file); + zipOutputSteam.putNextEntry(zipEntry); + int len; + byte[] buffer = new byte[4096]; + while ((len = inputStream.read(buffer)) != -1) { + zipOutputSteam.write(buffer, 0, len); + } + zipOutputSteam.closeEntry(); + } else { + //文件夹 + String[] fileList = file.list(); + //没有子文件和压缩 + if (Objects.requireNonNull(fileList).length <= 0) { + ZipEntry zipEntry = new ZipEntry(fileString + File.separator); + zipOutputSteam.putNextEntry(zipEntry); + zipOutputSteam.closeEntry(); + } + //子文件和递归 + for (String s : fileList) { + ZipFiles(folderString + fileString + "/", s, zipOutputSteam); + } + } + } + + /** + * 返回zip的文件输入流 + * + * @param zipFileString zip的名称 + * @param fileString ZIP的文件名 + * @return InputStream 输出流 + * @throws Exception 异常抛出 + */ + public static InputStream UpZip(String zipFileString, String fileString) throws Exception { + ZipFile zipFile = new ZipFile(zipFileString); + ZipEntry zipEntry = zipFile.getEntry(fileString); + return zipFile.getInputStream(zipEntry); + } + + /** + * 返回ZIP中的文件列表(文件和文件夹) + * + * @param zipFileString ZIP的名称 + * @param bContainFolder 是否包含文件夹 + * @param bContainFile 是否包含文件 + * @throws Exception 异常抛出 + */ + public static List GetFileList(String zipFileString, boolean bContainFolder, boolean bContainFile) throws Exception { + List fileList = new ArrayList<>(); + ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFileString)); + ZipEntry zipEntry; + String szName; + while ((zipEntry = inZip.getNextEntry()) != null) { + szName = zipEntry.getName(); + if (zipEntry.isDirectory()) { + // 获取部件的文件夹名 + szName = szName.substring(0, szName.length() - 1); + File folder = new File(szName); + if (bContainFolder) { + fileList.add(folder); + } + } else { + File file = new File(szName); + if (bContainFile) { + fileList.add(file); + } + } + } + inZip.close(); + return fileList; + } +} diff --git a/app/src/main/java/com/idormy/sms/forwarder/view/StepBar.java b/app/src/main/java/com/idormy/sms/forwarder/view/StepBar.java index 61c58b51..c88a9718 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/view/StepBar.java +++ b/app/src/main/java/com/idormy/sms/forwarder/view/StepBar.java @@ -73,9 +73,7 @@ public class StepBar extends LinearLayout { mTypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.StepBar); if (mTypedArray != null) { current_step = mTypedArray.getString(R.styleable.StepBar_current_step); - System.out.println("current_step = " + current_step); help_tip = mTypedArray.getString(R.styleable.StepBar_help_tip); - System.out.println("help_tip = " + help_tip); mTypedArray.recycle(); } } diff --git a/app/src/main/res/layout/activity_setting.xml b/app/src/main/res/layout/activity_setting.xml index 83777241..a75679f8 100644 --- a/app/src/main/res/layout/activity_setting.xml +++ b/app/src/main/res/layout/activity_setting.xml @@ -132,7 +132,7 @@ android:id="@+id/cbCallType3" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="-5dp" + android:layout_marginStart="-6dp" android:scaleX="0.7" android:scaleY="0.7" android:text="@string/missed_call" /> @@ -141,7 +141,7 @@ android:id="@+id/cbCallType1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="-15dp" + android:layout_marginStart="-16dp" android:scaleX="0.7" android:scaleY="0.7" android:text="@string/received_call" /> @@ -150,7 +150,7 @@ android:id="@+id/cbCallType2" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="-15dp" + android:layout_marginStart="-16dp" android:scaleX="0.7" android:scaleY="0.7" android:text="@string/local_outgoing_call" /> @@ -217,7 +217,7 @@ android:id="@+id/checkbox_cancel_app_notify" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="-5dp" + android:layout_marginStart="-6dp" android:scaleX="0.7" android:scaleY="0.7" android:text="@string/cancel_app_notify" /> diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index cf6e856b..01caad72 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -218,9 +218,9 @@ GET Local IP: - Instructions: \n1. Please keep the SOURCE and DESTINATION phones in the same Wi-Fi network, and do not turn on isolation. \n2. Tap "Send" on SOURCE mobile phone, and get "server IP" \n3. After filling in "Server IP" on DESTINATION phone, tap "Receive". \n [NOTE:] sender(s), forwarding rule(s) and log(s) will be overwritten after cloning! - Start Server - Stop Server + Instructions: \n[Note] The APP version of the sender and receiver must be the same!\n1. Please keep the SOURCE and DESTINATION phones in the same Wi-Fi network, and do not turn on isolation. \n2. Tap "Send" on SOURCE mobile phone, and get "server IP" \n3. After filling in "Server IP" on DESTINATION phone, tap "Receive". \n [NOTE:] sender(s), forwarding rule(s) and log(s) will be overwritten after cloning! + Start + Stop I\'m the SCOURCE phone Receive I\'m the DESTINATION phone diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60366d1d..6487d4ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ 转发规则 发送通道 应用列表 - 提示:首次使用请按照以下步骤顺序设置,该步骤点亮表示已设置! + 提示:首次使用按照1234步骤顺序设置,数字点亮即步骤已设置 提示:置顶下拉刷新,长按删除单条,选项卡切换日志类型 提示:新建规则点击“添加”,长按删除/克隆,点击编辑已有 提示:新建发送通道点击“添加”,长按删除/克隆,点击编辑已有 @@ -217,9 +217,9 @@ POST GET 本机IP: - 操作说明:\n1.新旧手机连接同一个WiFi网络(禁用AP隔离)\n2.旧手机直接点【发送】按钮,获取到【服务端IP】\n3.新手机填写【服务端IP】后,点【接收】按钮\n【注意】新手机接收后,发送通道、转发规则将完全被覆盖,清空历史记录! - 启动服务 - 停止服务 + 操作说明:\n【注意】发送方与接收方的APP版本号必须一致!\n1.新旧手机连接同一个WiFi网络(禁用AP隔离)\n2.旧手机直接点【发送】按钮,获取到【服务端IP】\n3.新手机填写【服务端IP】后,点【接收】按钮\n【注意】新手机接收后,发送通道、转发规则将完全被覆盖,清空历史记录! + 启动 + 停止 我是旧手机 接收 我是新手机 @@ -229,7 +229,7 @@ 服务端已启动 服务端已停止 本手机是发送端,不可接收文件,请先停止服务端! - 未接入Wifi网络,不可使用 HttpServer! + 未接入Wifi网络,不可使用 HttpServer 功能! 请输入服务端IP 下载成功 当前处于无线网络 @@ -280,7 +280,7 @@ 隐私政策 同意 不同意 -   SmsForwarder-短信转发器(下称本软件) 100% 免费开源,Github 在线编译发版,绝不会收集您的任何隐私数据! \n\n以下情形会上报本软件的版本信息: \n  1、启动本软件时,发送版本信息发送到《友盟+·U-App移动统计》,用于分析本软件的用户版本留存与软件奔溃统计; \n  2、手动检查更新时,发送版本号用于检查新版本; \n  除此之外,没有任何数据!!! \n\n  本软件会遵循《隐私政策》收集、使用版本信息,但不会因为您同意了《隐私政策》而进行强制捆绑式的信息收集。 +     SmsForwarder-短信转发器(下称本软件) 100% 免费开源,Github 在线编译发版,绝不会收集您的任何隐私数据! \n\n以下情形会上报本软件的版本信息: \n    1、启动本软件时,发送版本信息发送到《友盟+·U-App移动统计》,用于分析本软件的用户版本留存与软件奔溃统计; \n    2、手动检查更新时,发送版本号用于检查新版本; \n    除此之外,没有任何数据!!! \n\n    本软件会遵循《隐私政策》收集、使用版本信息,但不会因为您同意了《隐私政策》而进行强制捆绑式的信息收集。\n\n    提示:首次使用请按照1234步骤顺序设置,数字点亮表示该步骤已设置! WebServer ]]> 标题模板