diff --git a/AdminActivity.java b/AdminActivity.java new file mode 100644 index 0000000..3189775 --- /dev/null +++ b/AdminActivity.java @@ -0,0 +1,96 @@ +package com.example.myapplication; + +import android.annotation.SuppressLint; +import android.database.Cursor; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ListView; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.myapplication.DataBase.DatabaseHelper; + +public class AdminActivity extends AppCompatActivity { + private ListView lvUsers; + private DatabaseHelper dbHelper; + private SimpleCursorAdapter adapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_admin); + + dbHelper = new DatabaseHelper(this); + lvUsers = findViewById(R.id.lvUsers); + Button btnBack = findViewById(R.id.btnBack); + + loadUsers(); + + btnBack.setOnClickListener(v -> finish()); + } + + private void loadUsers() { + Cursor cursor = dbHelper.getAllUsers(); + + String[] from = new String[]{ + DatabaseHelper.COLUMN_USERNAME, + DatabaseHelper.COLUMN_PASSWORD, + DatabaseHelper.COLUMN_USER_PROJECT_NAME, + DatabaseHelper.COLUMN_USER_PROJECT_ID + }; + + int[] to = new int[]{ + R.id.tvUsername, + R.id.tvPassword, + R.id.tvProjectName, + R.id.tvProjectId + }; + + adapter = new SimpleCursorAdapter(this, R.layout.user_item, + cursor, from, to, 0) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + + // 获取当前行的数据 + Cursor cursor = (Cursor) getItem(position); + @SuppressLint("Range") String username = cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_USERNAME)); + + // 设置删除按钮点击事件 + Button btnDelete = view.findViewById(R.id.btnDelete); + btnDelete.setOnClickListener(v -> { + if (dbHelper.deleteUser(username)) { + Toast.makeText(AdminActivity.this, "用户已删除", Toast.LENGTH_SHORT).show(); + // 重新加载数据 + Cursor newCursor = dbHelper.getAllUsers(); + changeCursor(newCursor); + } else { + Toast.makeText(AdminActivity.this, "删除失败", Toast.LENGTH_SHORT).show(); + } + }); + TextView tvProjectName = view.findViewById(R.id.tvProjectName); + tvProjectName.setOnClickListener(v -> { + if (tvProjectName.getMaxLines() == 1) { + tvProjectName.setMaxLines(Integer.MAX_VALUE); // 展开 + } else { + tvProjectName.setMaxLines(1); // 收起 + } + }); + return view; + } + }; + + lvUsers.setAdapter(adapter); + } + + @Override + protected void onDestroy() { + dbHelper.close(); + super.onDestroy(); + } +} \ No newline at end of file diff --git a/LoginActivity.java b/LoginActivity.java new file mode 100644 index 0000000..c544690 --- /dev/null +++ b/LoginActivity.java @@ -0,0 +1,853 @@ +package com.example.myapplication; + +import static android.content.ContentValues.TAG; + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Base64; +import android.util.Log; +import android.util.Pair; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Filter; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import com.example.myapplication.DataBase.DatabaseHelper; +import com.example.myapplication.Tool.AuthInterceptor; +import com.example.myapplication.Tool.RetrofitClient; +import com.example.myapplication.Tool.ShowError; +import com.example.myapplication.api.AuthApi; +import com.example.myapplication.api.ProjectApi; +import com.example.myapplication.api.UserApi; +import com.example.myapplication.model.ApiResponse; +import com.example.myapplication.model.LoginEntity; +import com.example.myapplication.model.LoginRequest; +import com.example.myapplication.model.PageResult; +import com.example.myapplication.model.Project; +import com.example.myapplication.model.UserInfo; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.http.Body; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.POST; +import retrofit2.http.PUT; + + +public class LoginActivity extends AppCompatActivity { + private EditText etUsername, etPassword; + private CheckBox cbRemember; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private final ExecutorService executor = Executors.newFixedThreadPool(7); + + private Button btnClearProject; + private Button btnLogin; + + private DatabaseHelper dbHelper; + private SharedPreferences sharedPreferences; + private final String AUTH_TOKEN_KEY = "auth_token"; + private static final String Name = "name"; + private static final String BASE_URL = "http://pms.dtyx.net:9158/"; + private String authToken = ""; + private static final String PREFS_NAME = "LoginPrefs"; + private static final String PREF_USERNAME = "username"; + private static final String PREF_PASSWORD = "password"; + private static final String PREF_REMEMBER = "remember"; + private static final String PREF_PROJECT = "project"; // 新增项目键名 + private AutoCompleteTextView actvProject; + private ArrayAdapter projectAdapter; + private TextView tvSelectedProjectName; + private TextView tvSelectedProjectId; + private List projectList = new ArrayList<>(); + ShowError errorShower = new ShowError(this); + + + + @SuppressLint("SuspiciousIndentation") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + dbHelper = new DatabaseHelper(this); + // 使用 Context.MODE_PRIVATE 替代 MODE_PRIVATE 常量 + sharedPreferences = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + + initViews(); + setupListeners(); + loadSavedPreferences(); + loadProjects(); + } + + private void initViews() { + etUsername = findViewById(R.id.etUsername); + etPassword = findViewById(R.id.etPassword); + actvProject = findViewById(R.id.etProject); + cbRemember = findViewById(R.id.cbRemember); + btnLogin = findViewById(R.id.btnLogin); + btnClearProject = findViewById(R.id.btnClearProject); + tvSelectedProjectName = findViewById(R.id.tvSelectedProjectName); + tvSelectedProjectId = findViewById(R.id.tvSelectedProjectId); + + // 初始化项目下拉框 + initProjectDropdown(); + } + + private void setupListeners() { + // 清空项目按钮点击事件 + btnClearProject.setOnClickListener(v -> { + actvProject.setText(""); + tvSelectedProjectName.setText("项目名称:未选择"); + tvSelectedProjectId.setText("项目ID:未选择"); + loadProjects(); + projectAdapter.getFilter().filter(""); + actvProject.showDropDown(); + }); + + btnLogin.setOnClickListener(v -> attemptLogin()); + } + private boolean isNetworkAvailable() { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) return false; + + NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork()); + return capabilities != null && + (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)); + } + private void loadSavedPreferences() { + boolean remember = sharedPreferences.getBoolean(PREF_REMEMBER, false); + if (remember) { + String savedUsername = sharedPreferences.getString(PREF_USERNAME, ""); + String savedPassword = sharedPreferences.getString(PREF_PASSWORD, ""); + String savedProject = sharedPreferences.getString(PREF_PROJECT, ""); + + etUsername.setText(savedUsername); + etPassword.setText(savedPassword); + actvProject.setText(savedProject); + + if (!savedProject.isEmpty()) { + try { + int lastBracketIndex = savedProject.lastIndexOf("("); + int closingBracketIndex = savedProject.lastIndexOf(")"); + + if (lastBracketIndex != -1 && closingBracketIndex != -1 && closingBracketIndex > lastBracketIndex) { + String projectName = savedProject.substring(0, lastBracketIndex).trim(); + String projectId = savedProject.substring(lastBracketIndex + 1, closingBracketIndex).trim(); + + tvSelectedProjectName.setText("项目名称:" + projectName); + tvSelectedProjectId.setText("项目ID:" + projectId); + } + } catch (Exception e) { + e.printStackTrace(); + tvSelectedProjectName.setText("解析错误"); + tvSelectedProjectId.setText(""); + } + } + cbRemember.setChecked(true); + } + } + + private void attemptLogin() { + String username = etUsername.getText().toString().trim(); + String password = etPassword.getText().toString().trim(); + String projectInput = actvProject.getText().toString().trim(); + + if (username.isEmpty() || password.isEmpty() || projectInput.isEmpty()) { + Toast.makeText(LoginActivity.this, "请填写所有字段", Toast.LENGTH_SHORT).show(); + return; + } + + // 在后台线程执行数据库操作 + // 先进行认证 + if(isNetworkAvailable()) + authenticateUser(username, password, projectInput); + else continueLoginProcess(username,password,projectInput); + } + private void authenticateUser(String username, String password, String projectInput) { + + executor.execute(() -> { + try { + // 加密密码 + String encryptedPassword2 = encryptPassword(username, password); + + + // 创建认证请求体 + LoginRequest loginRequest = new LoginRequest(username, encryptedPassword2); + String token = sharedPreferences.getString(AUTH_TOKEN_KEY, ""); + // 初始化Retrofit + Retrofit retrofit = RetrofitClient.getClient(token); + + // 创建认证API服务 + AuthApi authApi = retrofit.create(AuthApi.class); + + // 执行认证请求 + Call> call = authApi.login(loginRequest); + Response> response = call.execute(); + + if (response.isSuccessful() && response.body() != null) { + + if (response.body().getCode() == 500200) { + //修改密码操作 + runOnUiThread(() -> showChangePasswordDialog(username)); + } else if (response.body().getCode() == 200) { + + authToken = response.body().getData().getTokenValue(); + Log.e("令牌:",authToken); + sharedPreferences.edit().putString(AUTH_TOKEN_KEY, authToken).apply(); + Retrofit retrofit2 = RetrofitClient.getClient(authToken); + // 创建UserApi服务 + UserApi userApi = retrofit2.create(UserApi.class); + + // 执行查询请求,只根据account查询 + Call> userCall = userApi.getUserList( + authToken, // 注意token前缀,根据你的API要求调整 + username, // account参数 + null, // deptId + null, // mobile + null, // name + null, // userCode + null, // userStatus + null // userType + ); + + try { + + Response> userResponse = userCall.execute(); + if (userResponse.isSuccessful() && userResponse.body() != null + && userResponse.body().getCode() == 200 + && !userResponse.body().getRows().isEmpty()) { + // 获取第一个匹配用户的name + String name = userResponse.body().getRows().get(0).getName(); + sharedPreferences.edit().putString(Name, name).apply(); + executor.execute(()-> { + + continueLoginProcess(username, password, projectInput); + }); + + } else { + // 查询失败或没有结果,使用username作为name + errorShower.showErrorDialog("查询姓名失败" , userResponse.body().getMsg()); + + sharedPreferences.edit().putString(Name, "").apply(); + executor.execute(()-> { + + continueLoginProcess(username, password, projectInput); + }); + } + } catch (IOException e) { + e.printStackTrace(); + // 发生异常,使用username作为name + errorShower.showErrorDialog("出现异常:", e.getMessage()); + executor.execute(()-> { + + continueLoginProcess(username, password, projectInput); + }); + } + + + } + else { + showToast(password+"密码出错"+response.body().toString()); + } + } else { + runOnUiThread(() -> { + String errorMsg = "认证失败: "; + if (response.errorBody() != null) { + try { + errorMsg += response.errorBody().string(); + } catch (IOException e) { + errorMsg += "无法读取错误详情"; + } + } + showToast(errorMsg); + }); + } + } catch (Exception e) { + showToast("认证出错:"+e.getMessage()); + } + }); + } + + + // 封装 Toast 显示 + private void showToast(String message) { + mainHandler.post(() -> Toast.makeText(LoginActivity.this, message, Toast.LENGTH_SHORT).show()); + } + + private void showChangePasswordDialog(String username) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("请修改密码"); + + View view = getLayoutInflater().inflate(R.layout.dialog_change_password, null); + EditText etOldPassword = view.findViewById(R.id.etOldPassword); + EditText etNewPassword = view.findViewById(R.id.etNewPassword); + EditText etConfirmPassword = view.findViewById(R.id.etConfirmPassword); + + builder.setView(view); + builder.setPositiveButton("确认", (dialog, which) -> { + String oldPassword = etOldPassword.getText().toString().trim(); + String newPassword = etNewPassword.getText().toString().trim(); + String confirmPassword = etConfirmPassword.getText().toString().trim(); + + if (newPassword.equals(confirmPassword)) { + changePassword(username, oldPassword, newPassword); + } else { + Toast.makeText(this, "新密码与确认密码不匹配", Toast.LENGTH_SHORT).show(); + } + }); + builder.setNegativeButton("取消", null); + builder.setCancelable(false); + builder.show(); + } + + private void changePassword(String username, String oldPassword, String newPassword) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + try { + // 加密旧密码和新密码 + String encryptedOldPassword = encryptPassword(username, oldPassword); + String encryptedNewPassword = encryptPassword(username, newPassword); + + // 创建修改密码请求体 + ChangePasswordRequest request = new ChangePasswordRequest( + username, + encryptedNewPassword, + encryptedOldPassword + ); + + // 获取token + String token = sharedPreferences.getString(AUTH_TOKEN_KEY, ""); + + // 初始化Retrofit + Retrofit retrofit = RetrofitClient.getClient(token); + + // 创建API服务 + AuthApi authApi = retrofit.create(AuthApi.class); + + // 执行修改密码请求 + Call> call = authApi.modifyPassword(request); + Response> response = call.execute(); + + runOnUiThread(() -> { + if (response.isSuccessful() && response.body() != null) { + if (response.body().isSuccess()) { + Toast.makeText(LoginActivity.this, "密码修改成功,请重新登录", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(LoginActivity.this, "密码修改失败: " + response.body().toString(), Toast.LENGTH_SHORT).show(); + } + } else { + String errorMsg = "修改密码失败: "; + if (response.errorBody() != null) { + try { + errorMsg += response.errorBody().string(); + } catch (IOException e) { + errorMsg += "无法读取错误详情"; + } + } + Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_SHORT).show(); + } + }); + } catch (Exception e) { + runOnUiThread(() -> + Toast.makeText(LoginActivity.this, "修改密码出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); + } + }); + } + + // 新增修改密码请求体 + public static class ChangePasswordRequest { + private String account; + private String newPassword; + private String oldPassword; + + public ChangePasswordRequest(String account, String newPassword, String oldPassword) { + this.account = account; + this.newPassword = newPassword; + this.oldPassword = oldPassword; + } + + // getters and setters + public String getAccount() { + return account; + } + + public void setAccount(String account) { + this.account = account; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + + public String getOldPassword() { + return oldPassword; + } + + public void setOldPassword(String oldPassword) { + this.oldPassword = oldPassword; + } + } + private void continueLoginProcess(String username, String password, String projectInput) { + // 在后台线程执行数据库操作 + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + try { + // 解析项目ID和名称 + Pair projectInfo = parseProjectInfo(projectInput); + String projectId = projectInfo.first; + String projectName = projectInfo.second; + + // 保存登录信息 + saveLoginPreferences(username, password, projectName, projectId); + + // 检查用户是否存在 + boolean userExists = dbHelper.userExists(username); + + runOnUiThread(() -> { + if (userExists) { + // 用户存在,验证密码 + if (dbHelper.checkUser(username, password)) { + handleLoginSuccess(username, projectId, projectName,password); + } else { + Toast.makeText(LoginActivity.this, "密码错误", Toast.LENGTH_SHORT).show(); + } + } else { + // 用户不存在,自动注册 + if (dbHelper.addUser(username, password, projectId, projectName)) { + handleLoginSuccess(username, projectId, projectName,password); + } else { + Toast.makeText(LoginActivity.this, "注册失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } catch (Exception e) { + runOnUiThread(() -> + Toast.makeText(LoginActivity.this, "登录出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); + } + }); + } + + private String encryptPassword(String username, String password) { + try { + // 1. 对账号做MD5计算 + String md5Hash = md5(username); + + // 2. 取8-24位作为密钥 + String key = md5Hash.substring(8, 24); // 8-24位(索引从0开始) + Log.e("key:",key); + // 3. AES加密 + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // PKCS5Padding等同于PKCS7Padding在Java中 + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] encryptedBytes = cipher.doFinal(password.getBytes()); + return Base64.encodeToString(encryptedBytes, Base64.DEFAULT).trim(); + } catch (Exception e) { + e.printStackTrace(); + showToast("加密密码错误:"+e.getMessage()); + return ""; + } + } + + private String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(input.getBytes()); + + StringBuilder hexString = new StringBuilder(); + for (byte b : messageDigest) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + + + private Pair parseProjectInfo(String projectInput) { + String projectId = ""; + String projectName = ""; + + Matcher matcher = Pattern.compile(".*\\((.*)\\)").matcher(projectInput); + if (matcher.find()) { + projectId = matcher.group(1).trim(); + projectName = projectInput.replace("(" + projectId + ")", "").trim(); + } else { + for (Project project : projectList) { + if (project.getProjectName().equals(projectInput) || + project.getProjectId().equals(projectInput)) { + projectId = project.getProjectId(); + projectName = project.getProjectName(); + break; + } + } + + if (projectId.isEmpty()) { + projectName = projectInput; + } + } + return new Pair<>(projectId, projectName); + } + + private void saveLoginPreferences(String username, String password, String projectName, String projectId) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + if (cbRemember.isChecked()) { + editor.putString(PREF_USERNAME, username); + editor.putString(PREF_PASSWORD, password); + String projectDisplayText = projectId.isEmpty() ? projectName : + (projectName + " (" + projectId + ")"); + editor.putString(PREF_PROJECT, projectDisplayText); + editor.putBoolean(PREF_REMEMBER, true); + } else { + editor.clear(); + } + editor.apply(); + } + + private void handleLoginSuccess(String username, String projectId, String projectName,String password) { + dbHelper.updateUserProject(username, projectId, projectName); + Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + intent.putExtra("username", username); + intent.putExtra("password",password); + intent.putExtra("projectId", projectId); + intent.putExtra("projectName", projectName); + startActivity(intent); + finish(); + } + + private void initProjectDropdown() { + projectAdapter = new ArrayAdapter(this, + R.layout.custom_dropdown_item, + projectList) { + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + TextView textView = view.findViewById(android.R.id.text1); + Project project = getItem(position); + if (project != null) { + textView.setText(project.getProjectName() + " (" + project.getProjectId() + ")"); + textView.setSingleLine(false); + textView.setMaxLines(3); + textView.setEllipsize(null); + } + return view; + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + return getView(position, convertView, parent); + } + + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + FilterResults results = new FilterResults(); + List filteredList = new ArrayList<>(); + + if (constraint != null && constraint.length() > 0) { + String filterPattern = constraint.toString().toLowerCase().trim(); + DatabaseHelper dbHelper = new DatabaseHelper(LoginActivity.this); + filteredList = dbHelper.searchProjects(filterPattern); + } else { + filteredList.addAll(projectList); + } + + results.values = filteredList; + results.count = filteredList.size(); + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + clear(); + if (results.values != null) { + addAll((List) results.values); + } + notifyDataSetChanged(); + } + }; + } + }; + + actvProject.setAdapter(projectAdapter); + actvProject.setThreshold(0); + actvProject.setDropDownBackgroundResource(android.R.color.white); + + actvProject.setOnItemClickListener((parent, view, position, id) -> { + Object item = parent.getItemAtPosition(position); + if (item instanceof Project) { + Project selectedProject = (Project) item; + tvSelectedProjectName.setText("项目名称:" + selectedProject.getProjectName()); + tvSelectedProjectId.setText("项目ID:" + selectedProject.getProjectId()); + actvProject.setText(selectedProject.getProjectName() + " (" + selectedProject.getProjectId() + ")"); + } else { + Log.e("TYPE_ERROR", "Expected Project but got: " + item.getClass()); + Toast.makeText(this, "数据格式错误", Toast.LENGTH_SHORT).show(); + } + }); + } + + + private void loadProjects() { + + + try { + // 1. 验证接口定义 + if (!validateApiDefinition()) { + return; + } + OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS); + okHttpClientBuilder.addInterceptor(new AuthInterceptor(authToken)); + // 2. 初始化Retrofit + Retrofit retrofit = RetrofitClient.getClient(authToken); + + // 3. 创建API服务 + ProjectApi projectApi = retrofit.create(ProjectApi.class); + + // 4. 执行网络请求 + executeProjectRequest(projectApi); + + } catch (Exception e) { + showErrorOnUI("初始化失败", getErrorMessage(e)); + logErrorToFile(e); + } + } + + private boolean validateApiDefinition() { + try { + Method method = ProjectApi.class.getMethod("getProjectList"); + Type returnType = method.getGenericReturnType(); + + // 预期的完整类型签名 + String expectedType = "retrofit2.Call>>"; + + if (!returnType.toString().equals(expectedType)) { + String errorMsg = "接口定义错误!\n\n当前定义:\n" + returnType + + "\n\n应修改为:\n@GET(\"project/list\")\nCall>> getProjectList();"; + + new Handler(Looper.getMainLooper()).post(() -> { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("API接口定义不合法") + .setMessage(errorMsg) + .setPositiveButton("复制定义", (d, w) -> copyToClipboard(errorMsg)) + .setNegativeButton("关闭", null) + .show(); + }); + return false; + } + return true; + } catch (Exception e) { + showErrorOnUI("接口验证失败", e.getMessage()); + return false; + } + } + + private void executeProjectRequest(ProjectApi projectApi) { + Call>> call = projectApi.getProjectList(); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, Response>> response) { + if (response.isSuccessful() && response.body() != null) { + handleSuccessResponse(response.body().getData()); + } else { + handleApiError(response); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + handleNetworkError(t); + } + }); + } + + // === 错误处理方法 === + private void handleSuccessResponse(List projects) { + runOnUiThread(() -> { + try { + dbHelper.saveProjects(projects); + projectList.clear(); + projectList.addAll(projects); + projectAdapter.notifyDataSetChanged(); + } catch (Exception e) { + showErrorOnUI("数据处理错误", e.getMessage()); + } + }); + } + + private void handleApiError(Response response) { + String errorMsg = "服务器响应错误: " + response.code(); + try { + if (response.errorBody() != null) { + errorMsg += "\n" + response.errorBody().string(); + } + } catch (IOException e) { + errorMsg += "\n无法读取错误详情"; + } + + showErrorOnUI("API请求失败", errorMsg); + loadCachedProjects(); + } + + private void handleNetworkError(Throwable t) { + String errorMsg = "网络错误: " + t.getMessage(); + if (t instanceof SocketTimeoutException) { + errorMsg = "连接超时,请检查网络"; + } else if (t instanceof UnknownHostException) { + errorMsg = "无法解析主机,请检查URL"; + } + + showErrorOnUI("网络连接失败", errorMsg); + loadCachedProjects(); + } + + // === 工具方法 === + private void showErrorOnUI(String title, String message) { + runOnUiThread(() -> { + // 在界面底部显示错误面板 + ViewGroup rootView = findViewById(android.R.id.content); + LinearLayout errorPanel = new LinearLayout(this); + errorPanel.setOrientation(LinearLayout.VERTICAL); + errorPanel.setBackgroundColor(0x33FF0000); + errorPanel.setPadding(32, 16, 32, 16); + + TextView tvError = new TextView(this); + tvError.setTextColor(Color.RED); + tvError.setText(title); + + TextView tvMsg = new TextView(this); + tvMsg.setText(message); + + errorPanel.addView(tvError); + errorPanel.addView(tvMsg); + rootView.addView(errorPanel); + + // 5秒后自动消失 + new Handler().postDelayed(() -> rootView.removeView(errorPanel), 5000); + }); + } + + private void copyToClipboard(String text) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("错误定义", text)); + Toast.makeText(this, "定义已复制", Toast.LENGTH_SHORT).show(); + } + + private String getErrorMessage(Exception e) { + if (e instanceof IllegalArgumentException) { + return "参数错误: " + e.getMessage(); + } else if (e instanceof NullPointerException) { + return "空指针异常: " + e.getMessage(); + } else { + return "未知错误: " + e.getClass().getSimpleName(); + } + } + + private void logErrorToFile(Exception e) { + String log = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + + "\nError: " + e.getClass().getName() + + "\nMessage: " + e.getMessage() + + "\nStack Trace:\n" + Log.getStackTraceString(e); + + try { + File file = new File(getExternalFilesDir(null), "error_log.txt"); + FileWriter writer = new FileWriter(file, true); + writer.append(log).append("\n\n"); + writer.close(); + } catch (IOException ioException) { + Log.e("FileLog", "无法写入错误日志", ioException); + } + } + + private void loadCachedProjects() { + new Thread(() -> { + List cachedProjects = dbHelper.getAllProjects(); + runOnUiThread(() -> { + projectList.clear(); + projectList.addAll(cachedProjects); + projectAdapter.notifyDataSetChanged(); + Toast.makeText(this, "已加载本地缓存数据", Toast.LENGTH_LONG).show(); + }); + }).start(); + } + +} \ No newline at end of file diff --git a/MainActivity.java b/MainActivity.java new file mode 100644 index 0000000..b304adc --- /dev/null +++ b/MainActivity.java @@ -0,0 +1,4020 @@ +package com.example.myapplication; + + + + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.DatePickerDialog; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.TimePickerDialog; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.database.ContentObserver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.media.ExifInterface; +import android.media.MediaRecorder; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import androidx.core.util.Pair; +import android.os.Looper; +import android.provider.MediaStore; +import android.provider.Settings; +import android.util.Base64; +import android.util.JsonReader; +import android.util.Log; + +import android.util.SparseBooleanArray; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.DatePicker; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ScrollView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.TimePicker; +import android.widget.Toast; +import android.Manifest; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; + +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.room.Room; + + +import com.chaquo.python.PyException; +import com.chaquo.python.PyObject; +import com.chaquo.python.Python; +import com.example.myapplication.DataBase.AppDatabase; +import com.example.myapplication.DataBase.DatabaseHelper; +import com.example.myapplication.Service.FloatingWindowService; +import com.example.myapplication.Service.RecordingService; +import com.example.myapplication.Tool.BackgroundToast; +import com.example.myapplication.Tool.CommonImageSourceFetcher; +import com.example.myapplication.Tool.DynamicDataFetcher; +import com.example.myapplication.Tool.PartListFetcher; +import com.example.myapplication.Tool.PermissionUtils; +import com.example.myapplication.Tool.ReportGeneratorHelper; +import com.example.myapplication.Tool.RetrofitClient; +import com.example.myapplication.api.AuthApi; +import com.example.myapplication.api.PartService; +import com.example.myapplication.model.ApiResponse; +import com.example.myapplication.model.AudioEntity; +import com.example.myapplication.model.ImageEntity; +import com.example.myapplication.model.ImageInfo; +import com.example.myapplication.model.ImageSourceItem; +import com.example.myapplication.model.PartResponse; +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.android.material.datepicker.MaterialDatePicker; +import com.google.android.material.switchmaterial.SwitchMaterial; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import okhttp3.*; +import retrofit2.Retrofit; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +// 在MainActivity类中添加以下变量 + + + +public class MainActivity extends AppCompatActivity { + private DynamicDataFetcher fetcher; + private Spinner spinnerImageSource; + private List> imageSources = new ArrayList<>(); + private PartListFetcher partListFetcher; + private List cachedPartList; + private AlertDialog reportcurrentDialog; // 添加成员变量来保存当前对话框 + private static String Temperator="0"; + private static String Humidity="0"; + private static String Weather="0"; + private static final int NOTIFICATION_PERMISSION_REQUEST_CODE = 102; + private DatabaseHelper dbHelper; + private long startTimestamp = 0; + private long endTimestamp = 0; + private String startDateStr = ""; + private String endDateStr = ""; + private TextView tvUser; + private TextView tvProject; + private TextView tvProjectId; + private Button btnAdmin; + // 在类变量声明部分添加 + private Button btnUploadVideos; + private Button btnUploadVideos2; + + private FloatingActionButton floatingActionButton; + private static boolean isRecording = false; + private MediaRecorder mediaRecorder; + private String currentAudioPath; + + private final OkHttpClient httpClient = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) // 连接超时 + .readTimeout(60, TimeUnit.SECONDS) // 读取超时 + .writeTimeout(60, TimeUnit.SECONDS) // 写入超时 + .build(); + private static final String TAG = "MainActivity"; + private static String outputPath = "report" + ".json"; + + + private TextView tvStatus; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private ReportGeneratorHelper reportGenerator; + private Button selectTimeButton; + + + private TextView startTimeText; + private TextView endTimeText; + private long startTime = 0; + private long endTime = 0; + + private TextView statusText; + private TextView TempText; + private TextView WeatText; + private TextView HumText; + + private ContentObserver contentObserver; + private long lastPhotoId = -1; + private boolean isMonitoring = false; + private SwitchMaterial switchMonitor; + + private static final int REQUEST_CODE_SELECT_PHOTOS = 1001; + + + private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + private AppDatabase db; + private static final int PERMISSION_REQUEST_CODE = 100; + private static final int PERMISSIONS_REQUEST_CODE = 109; + + private AppDatabase db2; + private Button btnHistory; + private Button btnFailedAudios; + private Button btnFailedAudios2; + private Button btnHistory2; + private AlertDialog currentDialog; + private AlertDialog currentDialog2; + private static final int THUMBNAIL_SIZE = 200; + private static final int DECODE_SAMPLE_SIZE = 4; + + + private final ExecutorService executor = Executors.newFixedThreadPool(20); + // 添加位置相关变量 + + private LocationManager locationManager; + private LocationListener locationListener; + private Location lastKnownLocation; + + private static final int LOCATION_PERMISSION_REQUEST_CODE = 1002; + private TextView locationText; + private Button btnSetUser ; + private static final int AUDIO_PERMISSION_REQUEST_CODE = 1030; + private EditText etUserInfo; + private Button btnUploadQueue; + private Button btnStopUpload; + private volatile boolean isUploading = false; + private final Object uploadLock = new Object(); + private final Object monitoringLock = new Object(); + private EditText etUploadInterval; + private Button btnSetInterval; + private Button btnStartTimedUpload; + private Button btnStopTimedUpload; + private Handler timedUploadHandler; + private Runnable timedUploadRunnable; + private static final int OVERLAY_PERMISSION_REQUEST_CODE = 1003; + private int uploadIntervalMinutes = 0; // 默认上传间隔(分钟) + private boolean isTimedUploadRunning = false; + private static final int REQUEST_VIDEO_PERMISSION = 1005; + private static final int REQUEST_CODE_SELECT_VIDEOS = 1004; + // 在类变量声明部分添加 + private ActivityResultLauncher videoPickerLauncher; + private InputMethodManager inputMethodManager; + + private ActivityResultLauncher requestPermissionLauncher; + private static final int REQUEST_CODE_OVERLAY = 101; + private TextView tvUnit, tvBlade,tvUnitName; + private static final String ACTION_UPDATE_VALUES = "com.example.myapplication.UPDATE_VALUES"; + private BroadcastReceiver updateReceiver; + private String currentUsername; + private String projectName ="0"; + private String projectId="0"; + private String unit = "0"; + private String unitName = "0"; + private String imgpath ="0"; + private String password="-1"; + private String name="-1"; + private String token="-1"; + private int blade =0; + private Button changepassword; + private Map, String> nameToIdMap; + private String ChooseImageSource ="-1"; + private ActivityResultLauncher overlayPermissionLauncher; + @SuppressLint("UnspecifiedRegisterReceiverFlag") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + tvStatus = findViewById(R.id.tv_status); + reportGenerator = new ReportGeneratorHelper(this); + TempText=findViewById(R.id.tem_text); + HumText=findViewById(R.id.hum_text); + WeatText=findViewById(R.id.weather_text); + initializeViews(); + initializeDatabase(); + initializeReceivers(); + initializePermissionLaunchers(); + initialProject(); + requestInitialPermissions(); + initFloatingActionButton(); + setupButtonListeners(); + setupTimedUpload(); + SharedPreferences sharedPreferences = getSharedPreferences("LoginPrefs", Context.MODE_PRIVATE); + token=sharedPreferences.getString("auth_token", "-1"); + name=sharedPreferences.getString("name","-1"); + + initApp(); + + changepassword=findViewById(R.id.changePassword); + changepassword.setOnClickListener(v -> { + showChangePasswordDialog(currentUsername); + }); + + partListFetcher=new PartListFetcher(this,token,projectId); + + new Thread(() -> { + try { + // 在子线程执行网络请求 + cachedPartList = partListFetcher.fetchPartListSync(); + + // 初始化 nameToIdMap + nameToIdMap = new HashMap<>(); + for (PartResponse part : cachedPartList) { + Pairkey=new Pair<>(part.getTurbineId(),part.getPartName()); + nameToIdMap.put(key, part.getPartId()); + } + + + } catch (IOException e) { + runOnUiThread(() -> showToast("网络错误: " + e.getMessage())); + } catch (PartListFetcher.ApiException e) { + runOnUiThread(() -> showToast("API 错误: " + e.getMessage())); + } + }).start(); + spinnerImageSource = findViewById(R.id.spinner_image_source); +// 使用 DynamicDataFetcher 替代 CommonImageSourceFetcher,指定数据类型为 "image_sources" + fetcher = new DynamicDataFetcher(token, this, "image_sources"); + loadImageSources(); + +// 设置下拉框选择监听 + spinnerImageSource.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position >= 0 && position < imageSources.size()) { + Map selectedItem = imageSources.get(position); + String value = selectedItem.get("value"); + + String key = selectedItem.get("key"); + ChooseImageSource=key; + Toast.makeText(MainActivity.this, + "选择了: " + value + " (Key: " + key + ")", + Toast.LENGTH_SHORT).show(); + Log.d("ImageSource", "Selected: " + key + " - " + value); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + // 未选择任何项 + } + }); + } + private void loadImageSources() { + fetcher.fetchDynamicDataAsync(new DynamicDataFetcher.DynamicDataCallback() { + @Override + public void onSuccess(List> sources) { + runOnUiThread(() -> { + + // 过滤掉null值 + imageSources = new ArrayList<>(); + for (Map item : sources) { + if (item != null && !item.isEmpty()) { + // 获取第一个也是唯一的entry(因为每个map只有一个键值对) + Map.Entry entry = item.entrySet().iterator().next(); + String key = entry.getKey(); + String value = entry.getValue(); + + if (value != null) { + // 创建一个新的map,统一结构为{"key":key, "value":value} + Map normalizedItem = new HashMap<>(); + normalizedItem.put("key", key); + normalizedItem.put("value", value); + imageSources.add(normalizedItem); + } + } + } + + if (imageSources.isEmpty()) { + showToast("图像数据为空"); + } else { + updateSpinner(imageSources); + } + }); + } + + @Override + public void onFailure(Throwable t) { + runOnUiThread(() -> { + Toast.makeText(MainActivity.this, + "加载失败: " + t.getMessage(), + Toast.LENGTH_SHORT).show(); + }); + } + }); + } + + private void updateSpinner(List> sources) { + // 提取显示文本并确保不为null + List displayValues = new ArrayList<>(); + for (Map item : sources) { + if (item != null && item.get("value") != null) { + displayValues.add(item.get("value")); + } + } + + // 确保列表不为空 + if (displayValues.isEmpty()) { + displayValues.add("没有可用的选项"); + } + + // 创建适配器 + ArrayAdapter adapter = new ArrayAdapter<>( + this, + android.R.layout.simple_spinner_item, + displayValues + ); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + // 设置适配器 + spinnerImageSource.setAdapter(adapter); + } + public String getPartIdByNameFast(String tid,String partName) { + Pairkey=new Pair<>(tid,partName); + return nameToIdMap.get(key); + } + + private void showChangePasswordDialog(String username) { + hideKeyboard(); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("请修改密码"); + + View view = getLayoutInflater().inflate(R.layout.dialog_change_password, null); + EditText etOldPassword = view.findViewById(R.id.etOldPassword); + EditText etNewPassword = view.findViewById(R.id.etNewPassword); + EditText etConfirmPassword = view.findViewById(R.id.etConfirmPassword); + + builder.setView(view); + builder.setPositiveButton("确认", (dialog, which) -> { + String oldPassword = etOldPassword.getText().toString().trim(); + String newPassword = etNewPassword.getText().toString().trim(); + String confirmPassword = etConfirmPassword.getText().toString().trim(); + + if (newPassword.equals(confirmPassword)) { + changePassword(username, oldPassword, newPassword); + } else { + Toast.makeText(this, "新密码与确认密码不匹配", Toast.LENGTH_SHORT).show(); + } + }); + builder.setNegativeButton("取消", null); + builder.setCancelable(false); + builder.show(); + } + private String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(input.getBytes()); + + StringBuilder hexString = new StringBuilder(); + for (byte b : messageDigest) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + private String encryptPassword(String username, String password) { + try { + // 1. 对账号做MD5计算 + String md5Hash = md5(username); + + // 2. 取8-24位作为密钥 + String key = md5Hash.substring(8, 24); // 8-24位(索引从0开始) + Log.e("key:",key); + // 3. AES加密 + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // PKCS5Padding等同于PKCS7Padding在Java中 + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] encryptedBytes = cipher.doFinal(password.getBytes()); + return Base64.encodeToString(encryptedBytes, Base64.DEFAULT).trim(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + private void changePassword(String username, String oldPassword, String newPassword) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + try { + // 加密旧密码和新密码 + String encryptedOldPassword = encryptPassword(username, oldPassword); + String encryptedNewPassword = encryptPassword(username, newPassword); + + // 创建修改密码请求体 + LoginActivity.ChangePasswordRequest request = new LoginActivity.ChangePasswordRequest( + username, + encryptedNewPassword, + encryptedOldPassword + ); + + + + // 初始化Retrofit + Retrofit retrofit = RetrofitClient.getClient(token); + + // 创建API服务 + AuthApi authApi = retrofit.create(AuthApi.class); + + // 执行修改密码请求 + retrofit2.Call> call = authApi.modifyPassword(request); + retrofit2.Response> response = call.execute(); + + runOnUiThread(() -> { + if (response.isSuccessful() && response.body() != null) { + if (response.body().isSuccess()) { + Toast.makeText(this, "密码修改成功,请重新登录", Toast.LENGTH_SHORT).show(); + dbHelper.deleteUser(currentUsername); + } else { + Toast.makeText(this, "密码修改失败: " + response.body().toString(), Toast.LENGTH_SHORT).show(); + } + } else { + String errorMsg = "修改密码失败: "; + if (response.errorBody() != null) { + try { + errorMsg += response.errorBody().string(); + } catch (IOException e) { + errorMsg += "无法读取错误详情"; + } + } + Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show(); + } + }); + } catch (Exception e) { + runOnUiThread(() -> + Toast.makeText(this, "修改密码出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); + } + }); + } + + + + private void generateReportJson(String inputpath, List imageDataList, String outputPath, String starttime, String endtime) { + String templateContent; + File outputFile = new File(this.getExternalFilesDir(null), outputPath); + if (!outputFile.getParentFile().exists()) { + outputFile.getParentFile().mkdirs(); + } + Map> imagesByDay = new HashMap<>(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + for (ImageEntity image : imageDataList) { + String day = sdf.format(new Date(image.time)); + if (!imagesByDay.containsKey(day)) { + imagesByDay.put(day, new ArrayList<>()); + } + imagesByDay.get(day).add(image); + } + Set unitSet = new HashSet<>(); + for (ImageEntity image : imageDataList) { + if (image.unitName != null) { + unitSet.add(image.unitName); + } + } + List unitList = new ArrayList<>(unitSet); + int n = unitList.size(); + for (Map.Entry> dayEntry : imagesByDay.entrySet()) { + String day = dayEntry.getKey(); + List dayImages = dayEntry.getValue(); + + for (int i = 0; i < n; i++) { + + String unitName = unitList.get(i); + + List unitImages = new ArrayList<>(); + for (ImageEntity image : dayImages) { + if (unitName.equals(image.unitName)) { + unitImages.add(image); + } + } + if (unitImages.isEmpty()) { + continue; + } + List imageAndResistanceList = new ArrayList<>(); + for (ImageEntity image : unitImages) { + imageAndResistanceList.add(new ReportGeneratorHelper.ImageData(image.path, "0.6")); + } + ImageEntity firstImage = unitImages.get(0); + String weather = firstImage.weather != null ? firstImage.weather :"0"; + String temperature = firstImage.temperature != null ? firstImage.temperature :"0"; + String humidity = firstImage.humidity != null ? firstImage.humidity : "0"; + if (i == 0) { + templateContent = readJsonFromAssets(this, inputpath); + } else { + templateContent = null; + } + reportGenerator.generateReport( + templateContent, + outputFile.getAbsolutePath(), + "#"+unitName, + day, + imageAndResistanceList, + weather, + temperature, + humidity, + starttime, + endtime, + new ReportGeneratorHelper.ReportGenerationCallback() { + @Override + public void onSuccess(String outputPath){ + runOnUiThread(() -> { + showToast("Json写入成功"); + }); + } + @Override + public void onError(String errorMessage) { + runOnUiThread(()->{ + showErrorDialog("Json写入失败",errorMessage); + }); + Log.e("生成失败", errorMessage); + } + } + ); + + } + // 在后台线程执行转换 + new Thread(() -> { + try { + // 转换文件 + File inputFile = new File(this.getExternalFilesDir(null), outputPath); + + String outputPath2 = "output" + day+ ".docx"; + File outputFile2 = convertJsonToDocx(this,inputFile , outputPath2); + + // 更新UI必须在主线程 + mainHandler.post(() -> { + updateStatus("转换完成!"); + showReportsList("output",".docx"); + }); + } catch (Exception e) { + Log.e(TAG, "Conversion failed", e); + mainHandler.post(() -> { + updateStatus("转换失败"); + showToast("转换失败: " + e.getMessage()); + }); + } + }).start(); + } + } + + + // 分享文件方法 + private void shareGeneratedFile(File file) { + Uri fileUri = FileProvider.getUriForFile(this, + getPackageName()+".provider", + file); + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + String mimeType = getMimeType(file.getAbsolutePath()); + shareIntent.setDataAndType(fileUri, mimeType); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + + startActivity(Intent.createChooser(shareIntent, "分享报告")); + } + + // 删除文件方法 + + // 添加方法列出所有生成的报告 + private List getGeneratedReports(String start, String end) { + File dir = this.getExternalFilesDir(null); + File[] files = dir.listFiles((d, name) -> (name.startsWith(start) && name.endsWith(end))); + + if (files == null) { + return Collections.emptyList(); + } + + List fileList = Arrays.asList(files); + + // 按日期从大到小排序 + fileList.sort((f1, f2) -> { + // 从文件名中提取日期部分 + String dateStr1 = f1.getName().substring(start.length(), start.length() + 10); + String dateStr2 = f2.getName().substring(start.length(), start.length() + 10); + + // 比较日期,返回逆序结果实现从大到小排序 + return dateStr2.compareTo(dateStr1); + }); + + return fileList; + } + + // 显示报告列表对话框 + + + private void showReportsList(String start, String end) { + if (reportcurrentDialog != null && reportcurrentDialog.isShowing()) { + reportcurrentDialog.dismiss(); + } + List reports = getGeneratedReports(start, end); + + if (reports.isEmpty()) { + Toast.makeText(this, "没有找到生成的报告", Toast.LENGTH_SHORT).show(); + if (reportcurrentDialog!= null && reportcurrentDialog.isShowing()) { + reportcurrentDialog.dismiss(); + } + return; + } + + // 创建自定义对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + LayoutInflater inflater = getLayoutInflater(); + View dialogView = inflater.inflate(R.layout.dialog_report_list, null); + builder.setView(dialogView); + + // 初始化视图 + ListView listView = dialogView.findViewById(R.id.report_list); + Button btnDeleteSelected = dialogView.findViewById(R.id.btn_delete_selected); + Button btnDeleteAll = dialogView.findViewById(R.id.btn_delete_all); + Button btnShare = dialogView.findViewById(R.id.btn_share); + Button btnCancel = dialogView.findViewById(R.id.btn_cancel); + + // 创建适配器 + ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item_report, reports) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = getLayoutInflater().inflate(R.layout.item_report, parent, false); + } + + File file = getItem(position); + CheckBox checkBox = view.findViewById(R.id.checkbox); + TextView fileName = view.findViewById(R.id.file_name); + + fileName.setText(file.getName()); + checkBox.setChecked(listView.isItemChecked(position)); + + return view; + } + }; + + listView.setAdapter(adapter); + + // 列表项点击事件 - 打开文件 + listView.setOnItemClickListener((parent, view, position, id) -> { + openGeneratedFile(reports.get(position)); + }); + + // 列表项长按事件 - 切换选中状态 + listView.setOnItemLongClickListener((parent, view, position, id) -> { + boolean newState = !listView.isItemChecked(position); + listView.setItemChecked(position, newState); + adapter.notifyDataSetChanged(); + return true; + }); + + // 删除选中按钮 + btnDeleteSelected.setOnClickListener(v -> { + SparseBooleanArray checked = listView.getCheckedItemPositions(); + List filesToDelete = new ArrayList<>(); + + for (int i = 0; i < checked.size(); i++) { + if (checked.valueAt(i)) { + filesToDelete.add(reports.get(checked.keyAt(i))); + } + } + + if (!filesToDelete.isEmpty()) { + new AlertDialog.Builder(this) + .setTitle("确认删除") + .setMessage("确定要删除选中的 " + filesToDelete.size() + " 个报告吗?") + .setPositiveButton("确定", (dialog1, which1) -> { + deleteSelectedReports(filesToDelete, start, end); + if (reportcurrentDialog != null && reportcurrentDialog.isShowing()) { + reportcurrentDialog.dismiss(); + } + }) + .setNegativeButton("取消", null) + .show(); + } else { + Toast.makeText(this, "请先选择要删除的报告", Toast.LENGTH_SHORT).show(); + } + }); + + // 删除全部按钮 + btnDeleteAll.setOnClickListener(v -> { + if (!reports.isEmpty()) { + new AlertDialog.Builder(this) + .setTitle("确认删除") + .setMessage("确定要删除全部 " + reports.size() + " 个报告吗?") + .setPositiveButton("确定", (dialog1, which1) -> { + deleteAllReports(reports, start, end); + if (reportcurrentDialog != null && reportcurrentDialog.isShowing()) { + reportcurrentDialog.dismiss(); + } + }) + .setNegativeButton("取消", null) + .show(); + } + }); + + // 分享按钮 + btnShare.setOnClickListener(v -> { + SparseBooleanArray checked = listView.getCheckedItemPositions(); + List filesToShare = new ArrayList<>(); + + for (int i = 0; i < checked.size(); i++) { + if (checked.valueAt(i)) { + filesToShare.add(reports.get(checked.keyAt(i))); + } + } + + if (!filesToShare.isEmpty()) { + shareSelectedFiles(filesToShare); + } else { + Toast.makeText(this, "请先选择要分享的报告", Toast.LENGTH_SHORT).show(); + } + }); + + // 返回按钮 + btnCancel.setOnClickListener(v -> { + if (reportcurrentDialog != null && reportcurrentDialog.isShowing()) { + reportcurrentDialog.dismiss(); + } + }); + + // 创建并显示对话框 + if (reportcurrentDialog != null && reportcurrentDialog.isShowing()) { + reportcurrentDialog.dismiss(); + } + reportcurrentDialog = builder.create(); + reportcurrentDialog.show(); + } + + // 删除选中报告的方法 + private void deleteSelectedReports(List filesToDelete, String start, String end) { + int successCount = 0; + for (File file : filesToDelete) { + if (file.delete()) { + successCount++; + } + } + + Toast.makeText(this, "已删除 " + successCount + " 个报告", Toast.LENGTH_SHORT).show(); + + // 删除后刷新列表 + showReportsList(start, end); + } + private void shareSelectedFiles(List filesToShare) { + if (filesToShare.size() == 1) { + // 单个文件直接分享 + shareGeneratedFile(filesToShare.get(0)); + } else { + // 多个文件需要先压缩或使用其他方式分享 + Toast.makeText(this, "正在准备分享多个文件...", Toast.LENGTH_SHORT).show(); + // 这里可以添加多文件分享逻辑 + } + } + // 删除全部报告的方法 + private void deleteAllReports(List reports, String start, String end) { + int successCount = 0; + for (File file : reports) { + if (file.delete()) { + successCount++; + } + } + + Toast.makeText(this, "已删除 " + successCount + " 个报告", Toast.LENGTH_SHORT).show(); + + // 删除后刷新列表 + showReportsList(start, end); + } + // 获取MIME类型 + + private void initApp() { + Button btnConvert = findViewById(R.id.btn_convert); + Button btnread=findViewById(R.id.btn_readdocx); + btnread.setOnClickListener(v->{ + showReportsList("output",".docx"); + }); + btnConvert.setOnClickListener(v -> { + if(PermissionUtils.checkAndRequestPermissions(this)) + { + // 显示处理中状态 + updateStatus("正在处理..."); + + showDateRangePicker();} + + }); + } + private void showDateRangePicker() { + // 创建日期选择对话框 + MaterialDatePicker> picker = MaterialDatePicker.Builder + .dateRangePicker() + .setTitleText("选择日期范围") + .setSelection(Pair.create( + MaterialDatePicker.thisMonthInUtcMilliseconds(), + MaterialDatePicker.todayInUtcMilliseconds() + )) + .build(); + + picker.addOnPositiveButtonClickListener(selection -> { + try { + // 保存原始时间戳 + startTimestamp = selection.first; + endTimestamp = selection.second; + + // 格式化为日期字符串 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + startDateStr = sdf.format(new Date(startTimestamp)); + endDateStr = sdf.format(new Date(endTimestamp)); + + // 查询数据库 + queryDatabasesAndGenerateReport(); + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "日期处理错误: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + + picker.show(getSupportFragmentManager(), "DATE_RANGE_PICKER"); + } + + private void queryDatabasesAndGenerateReport() { + updateStatus("正在查询数据库..."); + + try { + // 调整时间范围 + Calendar startCal = Calendar.getInstance(); + startCal.setTimeInMillis(startTimestamp); + startCal.set(Calendar.HOUR_OF_DAY, 0); + startCal.set(Calendar.MINUTE, 0); + startCal.set(Calendar.SECOND, 0); + startCal.set(Calendar.MILLISECOND, 0); + + Calendar endCal = Calendar.getInstance(); + endCal.setTimeInMillis(endTimestamp); + endCal.set(Calendar.HOUR_OF_DAY, 23); + endCal.set(Calendar.MINUTE, 59); + endCal.set(Calendar.SECOND, 59); + endCal.set(Calendar.MILLISECOND, 999); + + long adjustedStart = startCal.getTimeInMillis(); + long adjustedEnd = endCal.getTimeInMillis(); + + // 异步查询 + new Thread(() -> { + + try { + + + List listFromDb = db.imageDao().getImagesByTimeRange(adjustedStart, adjustedEnd); + List listFromDb2 = db2.imageDao().getImagesByTimeRange(adjustedStart, adjustedEnd); + + // 合并结果 + List combinedList = new ArrayList<>(); + combinedList.addAll(listFromDb); + combinedList.addAll(listFromDb2); + + // 回到主线程处理结果 + runOnUiThread(() -> { + if (combinedList.isEmpty()) { + updateStatus("所选时间段内没有数据"); + Toast.makeText(this, "所选时间段内没有数据", Toast.LENGTH_SHORT).show(); + } else { + updateStatus("正在生成报告..."); + generateReportJson("testjson/input.json", combinedList, + "output.json", startDateStr, endDateStr); + } + }); + } catch (Exception e) { + runOnUiThread(() -> { + updateStatus("查询失败: " + e.getMessage()); + Toast.makeText(this, "查询失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + }); + } finally { + if (db != null) db.close(); + if (db2 != null) db2.close(); + } + }).start(); + } catch (Exception e) { + updateStatus("处理日期错误: " + e.getMessage()); + Toast.makeText(this, "处理日期错误: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + private String readJsonFromOutputFile(File outputFile) { + try { + BufferedReader reader = new BufferedReader(new FileReader(outputFile)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + return sb.toString(); + } catch (IOException e) { + showToast("读取输出JSON文件错误: " + e.getMessage()); + return null; + } + } + + private File convertJsonToDocx(Context context, File outfile, String outputFileName) { + + String jsonData = readJsonFromOutputFile(outfile); + if (jsonData == null || jsonData.trim().isEmpty()) { + showErrorDialog("JSON错误", "空JSON数据或读取失败"); + return null; + } + + // 验证JSON格式 + + + File outputFile = new File(context.getExternalFilesDir(null), outputFileName); + + try { + Python py = Python.getInstance(); + PyObject result = py.getModule("test") + .callAttrThrows("json_to_docx", jsonData, outputFile.getAbsolutePath()); + + // 如果没有异常抛出,则认为成功 + showToast("DOCX生成成功: " + outputFile.getPath()); + return outputFile; + + } catch (PyException e) { + // 专门处理Python异常 + String pythonError = getPythonErrorDetails(e); + showErrorDialog("Python处理错误", pythonError); + return null; + } catch (Throwable e) { + // 处理其他Java异常 + String errorMsg = buildErrorMessage((Exception) e); + showErrorDialog("转换失败", errorMsg); + Log.e(TAG, errorMsg); + return null; + } + } + + // 构建完整的错误信息 + private String buildErrorMessage(Exception e) { + return "错误类型: " + e.getClass().getSimpleName() + "\n\n" + + "错误信息: " + e.getMessage() + "\n\n" + + "堆栈跟踪:\n" + Log.getStackTraceString(e); + } + + // 显示错误对话框(主线程安全) + private void showErrorDialog(String title, String message) { + runOnUiThread(() -> { + new AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton("确定", null) + .setNegativeButton("复制错误", (dialog, which) -> { + // 将错误复制到剪贴板 + android.content.ClipboardManager clipboard = + (android.content.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = + android.content.ClipData.newPlainText("错误信息", message); + clipboard.setPrimaryClip(clip); + showToast("已复制错误信息"); + }) + .show(); + }); + } + + // 获取Python错误详情(优化版) + private String getPythonErrorDetails(PyException e) { + try { + Python py = Python.getInstance(); + PyObject traceback = py.getModule("traceback"); + PyObject formatExc = traceback.callAttr("format_exc"); + + return "Python脚本错误:\n\n" + + formatExc.toString() + + "\n\n原始异常: " + e.getMessage(); + } catch (Exception tracebackError) { + return "无法解析Python错误详情: " + e.getMessage() + + "\n(解析traceback时出错: " + tracebackError.getMessage() + ")"; + } + } + private String readJsonFromAssets(Context context, String filename) { + try { + InputStream is = context.getAssets().open(filename); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + return sb.toString(); + } catch (IOException e) { + showToast("读取JSON文件错误: " + e.getMessage()); + return null; + } + } + + private void openGeneratedFile(File file) { + if (file == null || !file.exists()) { + showToast("文件不存在或创建失败"); + return; + } + + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri fileUri = FileProvider.getUriForFile( + this, + getPackageName() + ".provider", // 必须与 AndroidManifest.xml 中的 authorities 一致 + file); + + // 根据文件扩展名动态设置 MIME 类型 + String mimeType = getMimeType(file.getAbsolutePath()); + intent.setDataAndType(fileUri, mimeType); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + showToast("没有找到可以打开该文件的应用"); + // 提示用户安装支持的应用(如 WPS、Office) + showOfficeInstallDialog(); + } + } + + private void showOfficeInstallDialog() { + mainHandler.post(() -> { + new AlertDialog.Builder(this) + .setTitle("无法打开文件") + .setMessage("您的设备上没有安装可以打开Word文档的应用。是否要安装Microsoft Word?") + .setPositiveButton("安装", (d, w) -> { + try { + startActivity(new Intent(Intent.ACTION_VIEW, + Uri.parse("market://details?id=com.microsoft.office.word"))); + } catch (ActivityNotFoundException ex) { + startActivity(new Intent(Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=com.microsoft.office.word"))); + } + }) + .setNegativeButton("取消", null) + .show(); + }); + } + + // 辅助方法:在主线程更新状态 + private void updateStatus(String message) { + tvStatus.setText(message); + } + + // 辅助方法:在主线程显示Toast + private void showToast(String message) { + mainHandler.post(() -> Toast.makeText(this, message, Toast.LENGTH_SHORT).show()); + } + + private String getMimeType(String filePath) { + String extension = filePath.substring(filePath.lastIndexOf(".") + 1).toLowerCase(); + switch (extension) { + case "pdf": + return "application/pdf"; + case "doc": + case "docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case "txt": + return "text/plain"; + default: + return "*/*"; // 通用类型 + } + } + + + + private void showPermissionExplanationDialog2() { + mainHandler.post(() -> { + new AlertDialog.Builder(this) + .setTitle("需要权限") + .setMessage("此功能需要存储权限以保存生成的文件") + .setPositiveButton("去授权", (d, w) -> + PermissionUtils.checkAndRequestPermissions(this)) + .setNegativeButton("取消", null) + .show(); + }); + } + + + + + + + + + + + private void initializeViews() { + lastPhotoId = getLastPhotoId(); + selectTimeButton = findViewById(R.id.selectTimeButton); + startTimeText = findViewById(R.id.startTimeText); + endTimeText = findViewById(R.id.endTimeText); + btnUploadVideos2 = findViewById(R.id.btn_upload_videos2); + btnUploadVideos = findViewById(R.id.btn_upload_videos); + etUploadInterval = findViewById(R.id.et_upload_interval); + etUploadInterval.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + showKeyboard(v); + } else { + hideKeyboard(); + } + }); + btnFailedAudios = findViewById(R.id.btn_failed_audios); + btnFailedAudios2 = findViewById(R.id.btn_success_audios); + + btnSetInterval = findViewById(R.id.btn_set_interval); + btnStartTimedUpload = findViewById(R.id.btn_start_timed_upload); + btnStopTimedUpload = findViewById(R.id.btn_stop_timed_upload); + btnUploadQueue = findViewById(R.id.btn_upload_queue); + btnStopUpload = findViewById(R.id.btn_stop_upload); + btnHistory = findViewById(R.id.btn_history); + btnHistory2 = findViewById(R.id.btn_history2); + statusText = findViewById(R.id.status_text); + switchMonitor = findViewById(R.id.switch_monitor); + locationText = findViewById(R.id.location_text); + btnAdmin = findViewById(R.id.btnAdmin); + tvProject = findViewById(R.id.tv_project); + tvProjectId = findViewById(R.id.tv_projectId); + tvUser = findViewById(R.id.tv_user); + tvUnit = findViewById(R.id.tvUnit); + tvBlade = findViewById(R.id.tvBlade); + tvUnitName = findViewById(R.id.tvUnitName); + } + + private void initializeDatabase() { + db = Room.databaseBuilder(getApplicationContext(), + AppDatabase.class, "image-database").build(); + db2 = Room.databaseBuilder(getApplicationContext(), + AppDatabase.class, "image-database2").build(); + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private void initializeReceivers() { + updateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + unit = intent.getStringExtra("unit"); + blade = intent.getIntExtra("blade", 0); + unitName = intent.getStringExtra("unitName"); + password=intent.getStringExtra("password"); + updateValues(unit, blade, unitName); + } + }; + + IntentFilter filter = new IntentFilter(ACTION_UPDATE_VALUES); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(updateReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(updateReceiver, filter); + } + } + private void showKeyboard(View view) { + if (view.requestFocus()) { + view.postDelayed(() -> { + if (inputMethodManager == null) { + inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + } + if (inputMethodManager != null) { + inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + } + }, 100); + } + } + private void hideKeyboard() { + View view = this.getCurrentFocus(); + if (view != null) { + if (inputMethodManager == null) { + inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + } + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + // 清除当前焦点 + view.clearFocus(); + } + } + private void initializePermissionLaunchers() { + // Video picker launcher + videoPickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK && result.getData() != null) { + handleVideoSelection(result.getData()); + } + }); + + // Permission request launcher + requestPermissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestMultiplePermissions(), + permissions -> { + if (permissions.values().stream().allMatch(Boolean::booleanValue)) { + openVideoPicker(); + } else { + Toast.makeText(this, "需要权限才能选择视频", Toast.LENGTH_SHORT).show(); + } + }); + + // Overlay permission launcher + overlayPermissionLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Settings.canDrawOverlays(MainActivity.this)) { + startFloatingService(); + } else { + Toast.makeText(MainActivity.this, + "需要悬浮窗权限才能显示悬浮窗", Toast.LENGTH_SHORT).show(); + } + } + }, 500); + }); + } + private void initialProject() + { + Intent intent = getIntent(); + currentUsername = intent.getStringExtra("username"); + projectName = intent.getStringExtra("projectName"); + projectId=intent.getStringExtra("projectId"); + tvProject.setText(projectName); + tvProjectId.setText(projectId); + tvUser.setText(currentUsername); + inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + } + + private void requestInitialPermissions() { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + String[] permissions = { + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.POST_NOTIFICATIONS + }; + + if (checkSelfPermission(permissions)) { + + initLocationManager(); + } else { + + ActivityCompat.requestPermissions(this, permissions, PERMISSIONS_REQUEST_CODE); + } + } else { + + String[] permissions = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.ACCESS_FINE_LOCATION + }; + + if (checkSelfPermission(permissions)) { + initLocationManager(); + } else { + ActivityCompat.requestPermissions(this, permissions, PERMISSIONS_REQUEST_CODE); + } + } + + // Special handling for overlay permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) { + requestOverlayPermission(); + } + } + + private boolean checkSelfPermission(String[] permissions) { + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(this, permission) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; + } + + private void setupButtonListeners() { + // Set up all button click listeners here + btnUploadVideos2.setOnClickListener(v -> openVideoPicker()); + btnUploadVideos.setOnClickListener(v -> showVideoDateTimePicker()); + selectTimeButton.setOnClickListener(v -> showDateTimePicker()); + + btnSetInterval.setOnClickListener(v -> { + hideKeyboard(); // 先隐藏键盘 + setUploadInterval(); // 然后执行设置逻辑 + }); + btnStartTimedUpload.setOnClickListener(v -> startTimedUpload()); + btnStopTimedUpload.setOnClickListener(v -> stopTimedUpload()); + + btnUploadQueue.setOnClickListener(v -> startUploadQueue()); + btnStopUpload.setOnClickListener(v -> stopUploadQueue()); + btnFailedAudios.setOnClickListener(v -> showFailedAudios()); + btnFailedAudios2.setOnClickListener(v -> showSuccessAudios()); + btnHistory.setOnClickListener(v -> showHistory(db, "历史记录(点击查看大图)", "删除所有记录", "确定要删除所有历史记录吗?")); + btnHistory2.setOnClickListener(v -> showHistory(db2, "缓存队列(点击查看大图)", "删除所有缓存", "确定要删除所有缓存吗?")); + switchMonitor.setChecked(false); + switchMonitor.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + LOCATION_PERMISSION_REQUEST_CODE); + } else { + checkRecordPermission(); + } + } else { + stopMonitoring(); + } + }); + + findViewById(R.id.btnShowFloating).setOnClickListener(v -> { + if (checkOverlayPermission2()) { + startFloatingService(); + } + else { + BackgroundToast.show(MainActivity.this,"需要悬浮窗权限"); + } + }); + + btnAdmin.setOnClickListener(v -> { + startActivity(new Intent(MainActivity.this, AdminActivity.class)); + }); + } + private void showFailedAudios() { + new Thread(() -> { + List failedAudios = db.AudioDao().getAllAudios(); // Assuming you have getAll() in AudioDao + + runOnUiThread(() -> { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("上传失败的录音"); + + if (failedAudios == null || failedAudios.isEmpty()) { + builder.setMessage("没有上传失败的录音"); + builder.setPositiveButton("确定", null); + builder.show(); + return; + } + + List displayItems = new ArrayList<>(); + for (AudioEntity audio : failedAudios) { + displayItems.add(String.format("录音路径: %s\n图片路径: %s\n时间: %s", + audio.getAudioPath(), + audio.getImagePath(), + audio.getTime())); + } + + // Convert to array for the adapter + String[] items = displayItems.toArray(new String[0]); + + builder.setAdapter(new ArrayAdapter<>(this, + android.R.layout.simple_list_item_1, items), + (dialog, which) -> { + + }); + + // Add delete and retry buttons + builder.setNegativeButton("一键删除", (dialog, which) -> deleteAllFailedAudios(failedAudios)); + builder.setNeutralButton("一键上传", (dialog, which) -> retryAllFailedAudios(failedAudios)); + + builder.show(); + }); + }).start(); + } + private void showSuccessAudios() { + new Thread(() -> { + List successAudios = db2.AudioDao().getAllAudios(); // 假设 AudioDao 中有 getSuccessAudios() 方法 + + runOnUiThread(() -> { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("上传成功的录音"); + + if (successAudios == null || successAudios.isEmpty()) { + builder.setMessage("没有上传成功的录音"); + builder.setPositiveButton("确定", null); + builder.show(); + return; + } + + List displayItems = new ArrayList<>(); + for (AudioEntity audio : successAudios) { + displayItems.add(String.format("录音路径: %s\n图片路径: %s\n时间: %s", + audio.getAudioPath(), + audio.getImagePath(), + audio.getTime())); + } + + // Convert to array for the adapter + String[] items = displayItems.toArray(new String[0]); + + builder.setAdapter(new ArrayAdapter<>(this, + android.R.layout.simple_list_item_1, items), + (dialog, which) -> { + // 可以添加点击单个项目的操作 + }); + + // 添加删除按钮 + builder.setNegativeButton("一键删除", (dialog, which) -> deleteAllSuccessAudios(successAudios)); + + builder.show(); + }); + }).start(); + } + + private void setupTimedUpload() { + timedUploadHandler = new Handler(Looper.getMainLooper()); + SharedPreferences prefs = getSharedPreferences("UploadPrefs", MODE_PRIVATE); + uploadIntervalMinutes = prefs.getInt("upload_interval", 0); + if (uploadIntervalMinutes > 0) { + etUploadInterval.setText(String.valueOf(uploadIntervalMinutes)); + } + } + private void deleteAllSuccessAudios(List audios) { + new Thread(() -> { + for (AudioEntity audio : audios) { + + db2.AudioDao().delete(audio); + } + + runOnUiThread(() -> { + Toast.makeText(this, "已删除所有成功录音", Toast.LENGTH_SHORT).show(); + }); + }).start(); + } + private void deleteAllFailedAudios(List audios) { + new Thread(() -> { + for (AudioEntity audio : audios) { + + db.AudioDao().delete(audio); + } + + runOnUiThread(() -> { + Toast.makeText(this, "已删除所有失败录音", Toast.LENGTH_SHORT).show(); + }); + }).start(); + } + + private void retryAllFailedAudios(List audios) { + new Thread(() -> { + for (AudioEntity audio : audios) { +executor.execute(()->{ uploadAudio(audio.ImagePath,audio.AudioPath,audio.time); + db.AudioDao().deleteByPath(audio.AudioPath);}); + } + + BackgroundToast.show(MainActivity.this,"录音手动上传成功"); + }).start(); + } + + + + public void checkAndshowNotifation() + { + if (checkAndRequestNotificationPermission()) { + // 权限已授予,可以显示通知 + showNotification("准备录音..."); + } + } + + public void updateValues(String unit, int blade,String unitName) { + runOnUiThread(() -> { + tvUnit.setText( unit); + tvBlade.setText( String.valueOf(blade)); + tvUnitName.setText(unitName); + + }); + } + private boolean checkOverlayPermission2() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!Settings.canDrawOverlays(this)) { + requestOverlayPermission2(); + return false; + } + } + return true; + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private void requestOverlayPermission2() { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + intent.setData(Uri.parse("package:" + getPackageName())); + overlayPermissionLauncher.launch(intent); + } catch (Exception e) { + // 兼容厂商 ROM + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + } + } + + + private void startFloatingService() { + Intent serviceIntent = new Intent(this, FloatingWindowService.class); + serviceIntent.putExtra("PROJECT_ID", projectId); // 添加projectID到Intent + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent); + } else { + startService(serviceIntent); + } + + } + private void openVideoPicker() { + String[] requiredPermissions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+ + requiredPermissions = new String[]{ + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_IMAGES + }; + } + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requiredPermissions = new String[]{Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_IMAGES }; + } else { + requiredPermissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; + } + + if (checkPermissions(requiredPermissions)) { + launchVideoPicker(); + } else { + requestPermissionLauncher.launch(requiredPermissions); + } + } + + private boolean checkPermissions(String[] permissions) { + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(this, permission) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; + } + + private void launchVideoPicker() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("video/*"); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + videoPickerLauncher.launch(Intent.createChooser(intent, "选择视频")); + } + private void handleVideoSelection(Intent data) { + List videoUris = new ArrayList<>(); + + if (data.getClipData() != null) { + // 多选 + ClipData clipData = data.getClipData(); + for (int i = 0; i < clipData.getItemCount(); i++) { + videoUris.add(clipData.getItemAt(i).getUri()); + } + } else if (data.getData() != null) { + // 单选 + videoUris.add(data.getData()); + } + + // 显示确认对话框 + showQualificationDialog(videoUris); + } + private void showQualificationDialog(List videoUris) { + new AlertDialog.Builder(this) + .setTitle("机组合格确认") + .setMessage("这批上传的机组视频是否合格?") + .setPositiveButton("合格", (dialog, which) -> { + processVideosWithQualification(videoUris, 1); + }) + .setNegativeButton("不合格", (dialog, which) -> { + processVideosWithQualification(videoUris, 0); + }) + .setCancelable(false) + .show(); + } + private void processVideosWithQualification(List videoUris, int qualified) { + for (Uri videoUri : videoUris) { + processVideoUri(videoUri, qualified); + } + } + + private void processVideoUri(Uri videoUri, int qualified) { + String[] projection = { + MediaStore.Video.Media.DATA, + MediaStore.Video.Media.DATE_TAKEN, + MediaStore.Video.Media.DURATION + }; + + try (Cursor cursor = getContentResolver().query(videoUri, projection, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)); + long time = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_TAKEN)); + long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)); + + String user = getCurrentUser(); + uploadVideo(path, time, duration, lastKnownLocation, user, qualified); + } + } + } + + + private void showVideoEndDateTimePicker() { + final Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(startTime); + calendar.add(Calendar.HOUR, 1); + + DatePickerDialog datePickerDialog = new DatePickerDialog(this, + (view, year, month, day) -> { + calendar.set(year, month, day); + TimePickerDialog timePickerDialog = new TimePickerDialog(MainActivity.this, + (view1, hour, minute) -> { + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + endTime = calendar.getTimeInMillis(); + + if (endTime <= startTime) { + Toast.makeText(MainActivity.this, "结束时间必须晚于开始时间", Toast.LENGTH_SHORT).show(); + showVideoEndDateTimePicker(); + return; + } + + updateTimeDisplay(); + showVideoConfirmationDialog(); + }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true); + timePickerDialog.setTitle("选择视频结束时间"); + timePickerDialog.show(); + }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); + datePickerDialog.setTitle("选择视频结束日期"); + datePickerDialog.show(); + } + private void showVideoDateTimePicker() { + final Calendar calendar = Calendar.getInstance(); + Toast.makeText(this, "请选择视频开始时间", Toast.LENGTH_SHORT).show(); + + DatePickerDialog datePickerDialog = new DatePickerDialog(this, + (view, year, month, day) -> { + calendar.set(year, month, day); + TimePickerDialog timePickerDialog = new TimePickerDialog(MainActivity.this, + (view1, hour, minute) -> { + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + startTime = calendar.getTimeInMillis(); + updateTimeDisplay(); + Toast.makeText(MainActivity.this, "请选择视频结束时间", Toast.LENGTH_SHORT).show(); + showVideoEndDateTimePicker(); + }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true); + timePickerDialog.setTitle("选择视频开始时间"); + timePickerDialog.show(); + }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); + datePickerDialog.setTitle("选择视频开始日期"); + datePickerDialog.show(); + } + + private void showVideoConfirmationDialog() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + String message = "确认上传以下时间范围内的视频吗?\n\n" + + "开始时间: " + sdf.format(new Date(startTime)) + "\n" + + "结束时间: " + sdf.format(new Date(endTime)); + + new AlertDialog.Builder(this) + .setTitle("确认视频查询") + .setMessage(message) + .setPositiveButton("确认", (dialog, which) -> checkVideoPermissionAndQuery()) + .setNegativeButton("取消", (dialog, which) -> Toast.makeText(MainActivity.this, "已取消视频查询", Toast.LENGTH_SHORT).show()) + .setNeutralButton("重新选择", (dialog, which) -> showVideoDateTimePicker()) + .show(); + } + private void checkVideoPermissionAndQuery() { + String[] permissions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+ + permissions = new String[]{ + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_IMAGES + }; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13 + permissions = new String[]{Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_IMAGES }; + } else { // Android 12- + permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; + } + + if (checkAnyPermissionGranted(permissions)) { + queryVideos(); + } else { + ActivityCompat.requestPermissions(this, permissions, REQUEST_VIDEO_PERMISSION); + + } + } + + + + + private void queryVideos() { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { + List videoList = new ArrayList<>(); + ContentResolver contentResolver = getContentResolver(); + Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + + String[] projection = { + MediaStore.Video.Media._ID, + MediaStore.Video.Media.DATA, + MediaStore.Video.Media.DATE_TAKEN, + MediaStore.Video.Media.DURATION + }; + + String selection = MediaStore.Video.Media.DATE_TAKEN + " >= ? AND " + + MediaStore.Video.Media.DATE_TAKEN + " <= ?"; + String[] selectionArgs = {String.valueOf(startTime), String.valueOf(endTime)}; + + String sortOrder = MediaStore.Video.Media.DATE_TAKEN + " DESC"; + + try (Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)) { + if (cursor != null) { + while (cursor.moveToNext()) { + String path = cursor.getString(1); + long time = cursor.getLong(2); + long duration = cursor.getLong(3); + + videoList.add(new VideoInfo(path, time, duration)); + } + } + } catch (Exception e) { + Log.e("VideoQuery", "Error querying videos", e); + } finally { + executorService.shutdown(); + } + + final int count = videoList.size(); + runOnUiThread(() -> { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + String message = "找到 " + count + " 个视频 (" + + sdf.format(new Date(startTime)) + " 到 " + + sdf.format(new Date(endTime)) + ")"; + + if (count > 0) { + // 显示确认对话框 + new AlertDialog.Builder(MainActivity.this) + .setTitle("视频上传确认") + .setMessage(message + "\n\n这些视频是否合格?") + .setPositiveButton("合格", (dialog, which) -> { + uploadVideosWithQualification(videoList, 1); + }) + .setNegativeButton("不合格", (dialog, which) -> { + uploadVideosWithQualification(videoList, 0); + }) + .show(); + } else { + Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show(); + } + }); + }); + } + + // 辅助类,用于存储视频信息 + private static class VideoInfo { + String path; + long time; + long duration; + + VideoInfo(String path, long time, long duration) { + this.path = path; + this.time = time; + this.duration = duration; + } + } + + // 批量上传视频并设置合格标记 + private void uploadVideosWithQualification(List videoList, int qualified) { + String user = getCurrentUser(); + for (VideoInfo video : videoList) { + uploadVideo(video.path, video.time, video.duration, lastKnownLocation, user, qualified); + } + } + // 添加视频上传方法 + private void uploadVideo(String filePath, long time, long duration, Location location, String user,int q) { + File file = new File(filePath); + double latitude = location != null ? location.getLatitude() : 0; + double longitude = location != null ? location.getLongitude() : 0; + double altitude = location != null ? location.getAltitude() : 0; + String finalunit = unit==null?"0":unit; + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), + RequestBody.create(file, MediaType.parse("video/*"))) + .addFormDataPart("shootingTime", String.valueOf(time)) + .addFormDataPart("duration", String.valueOf(duration)) + .addFormDataPart("latitude", String.valueOf(latitude)) + .addFormDataPart("longitude", String.valueOf(longitude)) + .addFormDataPart("altitude", String.valueOf(altitude)) + .addFormDataPart("workerUserId", user) + .addFormDataPart("partId",finalunit) + .addFormDataPart("qualified", String.valueOf(q)) + .build(); + + Request request = new Request.Builder() + .url("http://pms.dtyx.net:9158/video-file-info/batch-upload") + .addHeader("Authorization",token)// 修改为您的视频上传URL + .post(requestBody) + .build(); + + httpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + BackgroundToast.show( + MainActivity.this, + "视频上传失败: " + e.getMessage() + ); + } + + @Override + public void onResponse(Call call, Response response) { + runOnUiThread(() -> { + if (response.isSuccessful()) { + BackgroundToast.show( + MainActivity.this, + "视频上传成功: " + response.code() + ); + } else { + BackgroundToast.show( + MainActivity.this, + "视频上传失败: " + response.code() + ); + } + }); + } + }); + } + + private void initFloatingActionButton() { + if (!checkOverlayPermission()) { + requestOverlayPermission(); + return; + } + + floatingActionButton = new FloatingActionButton(this); + floatingActionButton.setSize(FloatingActionButton.SIZE_MINI); + floatingActionButton.setImageResource(R.drawable.ic_mic_off); + floatingActionButton.setBackgroundTintList(ColorStateList.valueOf(Color.RED)); + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : + WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH| + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + + params.gravity = Gravity.TOP | Gravity.START; + params.x = 0; + params.y = 100; + + try { + WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + windowManager.addView(floatingActionButton, params); + } catch (Exception e) { + Toast.makeText(this, "需要悬浮窗权限才能显示悬浮球", Toast.LENGTH_SHORT).show(); + return; + } + + floatingActionButton.setOnTouchListener(new View.OnTouchListener() { + private int initialX; + private int initialY; + private float initialTouchX; + private float initialTouchY; + + @Override + public boolean onTouch(View v, MotionEvent event) { + WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + WindowManager.LayoutParams params = (WindowManager.LayoutParams) floatingActionButton.getLayoutParams(); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + initialX = params.x; + initialY = params.y; + initialTouchX = event.getRawX(); + initialTouchY = event.getRawY(); + return true; + + case MotionEvent.ACTION_MOVE: + params.x = initialX + (int) (event.getRawX() - initialTouchX); + params.y = initialY + (int) (event.getRawY() - initialTouchY); + windowManager.updateViewLayout(floatingActionButton, params); + return true; + + case MotionEvent.ACTION_UP: + if (Math.abs(event.getRawX() - initialTouchX) < 10 && + Math.abs(event.getRawY() - initialTouchY) < 10) { + floatingActionButton.performClick(); + } + return true; + } + return false; + } + }); + + floatingActionButton.setOnClickListener(v -> { + if (isRecording) { + stopVoiceRecording(); + floatingActionButton.setImageResource(R.drawable.ic_mic_off); + showNotification("录音已停止"); + } else { + if (checkRecordPermission1()) { + currentAudioPath = startVoiceRecording(); + if (currentAudioPath != null) { + + isRecording = true; + floatingActionButton.setImageResource(R.drawable.ic_mic_on); + showNotification("录音中..."); + Toast.makeText(this,"开始录音了"+currentAudioPath,Toast.LENGTH_SHORT).show(); + } + } else { + requestRecordPermission(); + } + } + }); + } + private boolean checkAndRequestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, + new String[]{Manifest.permission.POST_NOTIFICATIONS}, + NOTIFICATION_PERMISSION_REQUEST_CODE + ); + return false; + } + } + return true; + } + private void showNotification(String message) { + // 确保通知渠道已创建(Android 8.0+ 必须) + createNotificationChannel(); + + // 检查通知权限(Android 13+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "请授予通知权限以显示录音状态", Toast.LENGTH_SHORT).show(); + return; // 不显示通知,但不崩溃 + } + } + + try { + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "recording_channel") + .setSmallIcon(R.drawable.ic_mic_on) // 确保图标有效 + .setContentTitle("录音状态") + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_LOW); + + NotificationManagerCompat.from(this).notify(1, builder.build()); + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "通知显示失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + "recording_channel", + "Recording Service", + NotificationManager.IMPORTANCE_LOW + ); + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(channel); + } + } + private boolean checkOverlayPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return Settings.canDrawOverlays(this); + } + return true; + } + + private void requestOverlayPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + getPackageName())); + startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE); + } + } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE) { + if (checkOverlayPermission()) { + // Permission granted, initialize floating button + initFloatingActionButton(); + } else { + Toast.makeText(this, "悬浮窗权限被拒绝,无法显示悬浮球", Toast.LENGTH_SHORT).show(); + } + } + } + private boolean checkRecordPermission1() { + return ContextCompat.checkSelfPermission(this, + Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED; + } + + private void requestRecordPermission() { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, + AUDIO_PERMISSION_REQUEST_CODE); + } + private String startVoiceRecording() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + return null; + } + + // 启动前台服务 + Intent serviceIntent = new Intent(this, RecordingService.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + startForegroundService(serviceIntent); + } else { + startService(serviceIntent); + } + + String audioFilePath = Objects.requireNonNull(getExternalCacheDir()).getAbsolutePath() + + "/" + System.currentTimeMillis() + ".3gp"; + + try { + mediaRecorder = new MediaRecorder(); + mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + mediaRecorder.setOutputFile(audioFilePath); + // 添加异常捕获 + try { + mediaRecorder.prepare(); + } catch (IOException e) { + Toast.makeText(this,"AudioRecordprepare() failed: " + e.getMessage(),Toast.LENGTH_SHORT).show(); + return null; + } + mediaRecorder.start(); + return audioFilePath; + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "录音初始化失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + return null; + } + + } + + private void stopVoiceRecording() { + if (mediaRecorder != null) { + try { + mediaRecorder.stop(); + mediaRecorder.release(); + mediaRecorder = null; + isRecording = false; + // 停止服务 + stopService(new Intent(this, RecordingService.class)); + BackgroundToast.show(this, "录音已保存"+currentAudioPath); +executor.execute(()->{ uploadAudio(imgpath,currentAudioPath,currentUsername); + currentAudioPath=null;}); + + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + } + +private String getPartName(int b) +{ + return "叶片" +b; +} + private void queryPhotos() { + // 定义查询时间范围的条件(示例,可根据需要修改) + String selection = MediaStore.Images.Media.DATE_TAKEN + " BETWEEN ? AND ? AND"+ + "(" + MediaStore.Images.Media.DATA + " LIKE '%/DCIM/%' OR " + + MediaStore.Images.Media.DATA + " LIKE '%/Camera/%')" ;; + + // 使用Executor替代AsyncTask + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { + int count = 0; + ContentResolver contentResolver = getContentResolver(); + Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + + String[] projection = { + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.DATE_TAKEN, + MediaStore.Images.Media.LATITUDE, + MediaStore.Images.Media.LONGITUDE + }; + + String[] selectionArgs = {String.valueOf(startTime), String.valueOf(endTime)}; + String sortOrder = MediaStore.Images.Media.DATE_TAKEN + " DESC"; + + try (Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)) { + if (cursor != null) { + while (cursor.moveToNext()) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); + String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)); + long time = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN)); + + // 初始化位置信息 + double latitude = 0.0; + double longitude = 0.0; + double altitude = 0.0; + + // 从媒体库读取位置信息(如果有) + int latIndex = cursor.getColumnIndex(MediaStore.Images.Media.LATITUDE); + int lngIndex = cursor.getColumnIndex(MediaStore.Images.Media.LONGITUDE); + if (latIndex != -1 && lngIndex != -1) { + latitude = cursor.getDouble(latIndex); + longitude = cursor.getDouble(lngIndex); + } + + // 改进位置获取 + try { + if (locationManager != null && checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + Location lastLocation = locationManager.getLastKnownLocation( + LocationManager.GPS_PROVIDER); + if (lastLocation != null) { + lastKnownLocation = lastLocation; + runOnUiThread(() -> updateLocationText(lastLocation)); + } + } + } catch (SecurityException e) { + e.printStackTrace(); + } + + if (lastKnownLocation != null) { + latitude = lastKnownLocation.getLatitude(); + longitude = lastKnownLocation.getLongitude(); + altitude = lastKnownLocation.getAltitude(); + } + + // 改进EXIF高度读取 + try { + ExifInterface exif = new ExifInterface(path); + String altitudeStr = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE); + if (altitudeStr != null) { + String[] parts = altitudeStr.split("/"); + if (parts.length == 2) { + double alt = Double.parseDouble(parts[0]) / + Double.parseDouble(parts[1]); + altitude = alt; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + String user = getCurrentUser(); + Log.d("PhotoQuery", "Processing photo for user: " + user); + + // 使用更高效的方式生成缩略图 + Bitmap thumbnail = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, options); + + options.inSampleSize = calculateInSampleSize(options, THUMBNAIL_SIZE, THUMBNAIL_SIZE); + options.inJustDecodeBounds = false; + thumbnail = BitmapFactory.decodeFile(path, options); + } catch (Exception e) { + e.printStackTrace(); + } + + count++; + if (thumbnail != null) { + + + + + lastPhotoId = Math.max(lastPhotoId, id); + + + double finalAltitude = altitude; + double finalLongitude = longitude; + double finalLatitude = latitude; + String finalproject=getProject(); + String finalunit=getUnit(); + String partname=getPartName(blade); + + String partid=getPartIdByNameFast(unit,partname); + if(partid==null) + partid=partname; + + // 使用另一个线程执行上传或保存操作 + String finalPartid = partid; + Executors.newSingleThreadExecutor().execute(() -> { + if (isNetworkAvailable()) { + uploadPhoto(path, time, lastKnownLocation, user, "0",finalproject,finalunit,blade, finalPartid,ChooseImageSource,unitName); + + } else { + db2.imageDao().insert(new ImageEntity(path, time, + finalLatitude, finalLongitude, + finalAltitude, user, "0",finalproject,finalunit,blade,false,unitName,Temperator,Humidity,Weather,ChooseImageSource)); + } + }); + } + } + cursor.close(); + } + } catch (Exception e) { + Log.e("PhotoQuery", "Error querying photos", e); + } finally { + executorService.shutdown(); + } + + // 更新UI需要在主线程执行 + final int finalCount = count; + runOnUiThread(() -> { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + Toast.makeText(MainActivity.this, + "找到 " + finalCount + " 张照片 (" + + sdf.format(new Date(startTime)) + " 到 " + + sdf.format(new Date(endTime)) + ")上传中", + Toast.LENGTH_LONG).show(); + }); + }); + } + + // 辅助方法计算采样大小 + private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) >= reqHeight + && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + private void showDateTimePicker() { + hideKeyboard(); + final Calendar calendar = Calendar.getInstance(); + + // Show prompt for start time selection + Toast.makeText(this, "请选择开始时间", Toast.LENGTH_SHORT).show(); + + // 日期选择器 + DatePickerDialog datePickerDialog = new DatePickerDialog(this, + new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePicker view, int year, int month, int day) { + calendar.set(year, month, day); + + // 时间选择器 + TimePickerDialog timePickerDialog = new TimePickerDialog(MainActivity.this, + new TimePickerDialog.OnTimeSetListener() { + @Override + public void onTimeSet(TimePicker view, int hour, int minute) { + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + startTime = calendar.getTimeInMillis(); + + // Update start time display immediately + updateTimeDisplay(); + + // Show prompt for end time selection + Toast.makeText(MainActivity.this, "请选择结束时间", Toast.LENGTH_SHORT).show(); + + // 选择结束时间 + showEndDateTimePicker(); + } + }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true); + timePickerDialog.setTitle("选择开始时间"); + timePickerDialog.show(); + } + }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); + datePickerDialog.setOnShowListener(dialog -> { + // 确保日期选择器获得焦点 + datePickerDialog.getButton(DialogInterface.BUTTON_POSITIVE).requestFocus(); + }); + datePickerDialog.setTitle("选择开始日期"); + datePickerDialog.show(); + } + + private void showEndDateTimePicker() { + hideKeyboard(); + final Calendar calendar = Calendar.getInstance(); + // Set initial time to start time + 1 hour as a suggestion + calendar.setTimeInMillis(startTime); + calendar.add(Calendar.HOUR, 1); + + DatePickerDialog datePickerDialog = new DatePickerDialog(this, + new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePicker view, int year, int month, int day) { + calendar.set(year, month, day); + + TimePickerDialog timePickerDialog = new TimePickerDialog(MainActivity.this, + new TimePickerDialog.OnTimeSetListener() { + @Override + public void onTimeSet(TimePicker view, int hour, int minute) { + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + endTime = calendar.getTimeInMillis(); + + // 验证结束时间是否晚于开始时间 + if (endTime <= startTime) { + Toast.makeText(MainActivity.this, "结束时间必须晚于开始时间", Toast.LENGTH_SHORT).show(); + showEndDateTimePicker(); // 重新选择 + return; + } + + // 更新UI显示选择的时间 + updateTimeDisplay(); + + // 显示确认对话框 + showConfirmationDialog(); + } + }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true); + timePickerDialog.setTitle("选择结束时间"); + timePickerDialog.show(); + } + }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); + datePickerDialog.setOnShowListener(dialog -> { + // 确保日期选择器获得焦点 + datePickerDialog.getButton(DialogInterface.BUTTON_POSITIVE).requestFocus(); + }); + datePickerDialog.setTitle("选择结束日期"); + datePickerDialog.show(); + } + private void showConfirmationDialog() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + String message = "确认上传以下时间范围内的照片吗?\n\n" + + "开始时间: " + sdf.format(new Date(startTime)) + "\n" + + "结束时间: " + sdf.format(new Date(endTime)); + + new AlertDialog.Builder(this) + .setTitle("确认查询") + .setMessage(message) + .setPositiveButton("确认", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 用户确认,开始查询照片 + queryPhotos(); + } + }) + .setNegativeButton("取消", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 用户取消,可以重新选择时间或不做任何操作 + Toast.makeText(MainActivity.this, "已取消查询", Toast.LENGTH_SHORT).show(); + } + }) + .setNeutralButton("重新选择", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 用户想重新选择时间 + showDateTimePicker(); + } + }) + .show(); + } + private void updateTimeDisplay() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + + if (startTime > 0) { + startTimeText.setText("开始时间:" + sdf.format(new Date(startTime))); + } + + if (endTime > 0) { + endTimeText.setText("结束时间:" + sdf.format(new Date(endTime))); + } + } + + + + + + + + + // 设置上传间隔 + private void setUploadInterval() { + try { + hideKeyboard(); + int minutes = Integer.parseInt(etUploadInterval.getText().toString()); + if (minutes <= 0) { + showToast("请输入大于0的分钟数"); + return; + } + + uploadIntervalMinutes = minutes; + + // 保存到SharedPreferences + SharedPreferences.Editor editor = getSharedPreferences("UploadPrefs", MODE_PRIVATE).edit(); + editor.putInt("upload_interval", uploadIntervalMinutes); + editor.apply(); + + showToast( "上传间隔已设置为: " + minutes + "分钟"); + + // 如果定时上传正在运行,重新启动以应用新间隔 + if (isTimedUploadRunning) { + stopTimedUpload(); + startTimedUpload(); + } + } catch (NumberFormatException e) { + showToast("请输入有效的数字"); + } + } + + // 开始定时上传 + private void startTimedUpload() { + if (uploadIntervalMinutes <= 0) { + showToast("请先设置上传间隔"); + return; + } + + if (isTimedUploadRunning) { + showToast("定时上传已经在运行"); + return; + } + + isTimedUploadRunning = true; + btnStartTimedUpload.setEnabled(false); + btnStopTimedUpload.setEnabled(true); + etUploadInterval.setEnabled(false); + btnSetInterval.setEnabled(false); + + + + // 设置定时任务 + timedUploadRunnable = new Runnable() { + @Override + public void run() { + if (!isTimedUploadRunning) return; + + startUploadQueue(); + timedUploadHandler.postDelayed(this, uploadIntervalMinutes * 60 * 1000L); + } + }; + + timedUploadHandler.postDelayed(timedUploadRunnable, uploadIntervalMinutes * 60 * 1000L); + statusText.setText("定时上传已启动,间隔: " + uploadIntervalMinutes + "分钟"); + } + + // 停止定时上传 + private void stopTimedUpload() { + if (!isTimedUploadRunning) return; + + isTimedUploadRunning = false; + if (timedUploadRunnable != null) { + timedUploadHandler.removeCallbacks(timedUploadRunnable); + } + + btnStartTimedUpload.setEnabled(true); + btnStopTimedUpload.setEnabled(false); + etUploadInterval.setEnabled(true); + btnSetInterval.setEnabled(true); + + statusText.setText("定时上传已停止"); + } + + + private void uploadPhotoSync(String filePath, long time, Location location, String user, String audioPath,String project,String unit,int blade,String partId,String imageSource,String unitName) { + File file = new File(filePath); + + double latitude = location != null ? location.getLatitude() : 0; + double longitude = location != null ? location.getLongitude() : 0; + double altitude = location != null ? location.getAltitude() : 0; + + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), + RequestBody.create(file, MediaType.parse("image/*"))) + .addFormDataPart("time", String.valueOf(time)) + .addFormDataPart("latitude", String.valueOf(latitude)) + .addFormDataPart("longitude", String.valueOf(longitude)) + .addFormDataPart("altitude", String.valueOf(altitude)) + .addFormDataPart("uploadUser", user) + .addFormDataPart("projectId",project) + .addFormDataPart("turbineId",unit) + .addFormDataPart("partId",partId); + + // + + RequestBody requestBody = builder.build(); + + Request request = new Request.Builder() + .url("http://pms.dtyx.net:9158/common/upload-image/file") + .post(requestBody) + .addHeader("Authorization",token) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (response.isSuccessful()) { + BackgroundToast.show( + MainActivity.this, + "上传成功: " + response.code() + filePath + ); + executor.execute(() -> { + + db.imageDao().insert(new ImageEntity(filePath, time, + latitude, longitude, altitude, user, audioPath, project, unit, blade, true,unitName,Temperator,Humidity,Weather,imageSource));}); + } else { + BackgroundToast.show(MainActivity.this, + "上传失败: " + response.message() + ); + } + } catch (IOException e) { + if (isUploading) { // 只有主动上传时才重新加入队列 + db2.imageDao().insert(new ImageEntity(filePath, time, + latitude, longitude, altitude, user, audioPath, project, unit, blade, false,unitName,Temperator,Humidity,Weather,imageSource)); + } + BackgroundToast.show(MainActivity.this, + "上传失败: " + e.getMessage() + ); + + } + } + + + private void stopUploadQueue() { + isUploading = false; + // 中断当前上传 + synchronized (uploadLock) { + // 这里会中断当前上传 + uploadLock.notifyAll(); // 唤醒可能正在等待的上传线程 + } + btnStopUpload.setEnabled(false); + btnUploadQueue.setEnabled(true); + // 更新状态显示 + runOnUiThread(() -> { + statusText.setText(isTimedUploadRunning ? "定时上传已暂停" : "上传已停止"); + Toast.makeText(MainActivity.this, "上传队列已停止", Toast.LENGTH_SHORT).show(); + }); + } + private void checkRecordPermission() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + // 请求录音权限 + ActivityCompat.requestPermissions( + this, + new String[]{Manifest.permission.RECORD_AUDIO}, + AUDIO_PERMISSION_REQUEST_CODE); + }else { + // 已经有录音权限,继续检查图片权限 + checkPermissionAndPickImage(); + } + } + + + + private void initLocationManager() { + locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); + try { + Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (lastLocation != null) { + lastKnownLocation = lastLocation; + updateLocationText(lastLocation); + Log.d("LocationUpdate", "最新已知位置: " + locationToString(lastLocation)); + } + } catch (SecurityException e) { + e.printStackTrace(); + BackgroundToast.show( MainActivity.this, + " 获取位置失败" + ); + } + locationListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + lastKnownLocation = location; + Toast.makeText(MainActivity.this, + "位置更新: " + location.getLatitude() + ", " + location.getLongitude(), + Toast.LENGTH_SHORT).show(); + // 打印到Logcat + Log.d("LocationUpdate", "新位置: " + locationToString(location)); + updateLocationText(lastKnownLocation); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) {} + + @Override + public void onProviderEnabled(String provider) {} + + @Override + public void onProviderDisabled(String provider) {} + }; + + try { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this,"没有授予定位权限",Toast.LENGTH_SHORT).show(); + return; + } + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 60000, // 5秒更新间隔 + 10, // 10米距离变化 + locationListener); + } catch (Exception e) { + e.printStackTrace(); + } + } + private void updateLocationText(Location location) { + runOnUiThread(() -> { + if (location == null) { + locationText.setText("当前位置: 无法获取位置信息"); + return; + } + String locationInfo = String.format(Locale.getDefault(), + "当前位置:\n纬度: %.6f\n经度: %.6f\n海拔: %.2f米\n精度: %.2f米\n更新时间: %s", + location.getLatitude(), + location.getLongitude(), + location.getAltitude(), + location.getAccuracy(), + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date(location.getTime()))); + + locationText.setText(locationInfo); + }); + } + + // 将位置转换为字符串用于打印 + private String locationToString(Location location) { + return String.format(Locale.getDefault(), + "纬度: %.6f, 经度: %.6f, 海拔: %.2f, 精度: %.2f, 时间: %s", + location.getLatitude(), + location.getLongitude(), + location.getAltitude(), + location.getAccuracy(), + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date(location.getTime()))); + } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (requestCode == PERMISSIONS_REQUEST_CODE) { + // 检查所有权限是否被授予 + boolean allPermissionsGranted = true; + for (int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + allPermissionsGranted = false; + break; + } + } + + if (allPermissionsGranted) { + // 所有权限都已授予,继续初始化位置管理器 + initLocationManager(); + } else { + // 处理权限被拒绝的情况 + handlePermissionDenial(); + } + } + else if (requestCode == REQUEST_VIDEO_PERMISSION) { + boolean allGranted = true; + for (int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + allGranted = false; + break; + } + } + + if(allGranted) { + Toast.makeText(this, "媒体权限已授权", Toast.LENGTH_SHORT).show(); + + queryVideos(); + } + else { + Toast.makeText(this, "需要媒体权限才能查询视频", Toast.LENGTH_SHORT).show(); + } + + + } + else if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showNotification("通知权限已授予"); + initFloatingActionButton(); + } else { + Toast.makeText(this, "通知权限被拒绝,无法显示录音状态", Toast.LENGTH_SHORT).show(); + } + } + else if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // 位置权限已授予,继续检查图片权限 + checkRecordPermission(); + initLocationManager(); + Toast.makeText(this, "位置权限已授予", Toast.LENGTH_SHORT).show(); + } else { + // 位置权限被拒绝,关闭开关 + switchMonitor.setChecked(false); + Toast.makeText(this, "需要位置权限才能获取位置信息", Toast.LENGTH_SHORT).show(); + } + } + else if (requestCode == AUDIO_PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // 录音权限已授予,继续检查位置权限 + checkPermissionAndPickImage(); + Toast.makeText(this, "语音权限已授予", Toast.LENGTH_SHORT).show(); + } else { + // 录音权限被拒绝,关闭开关 + switchMonitor.setChecked(false); + Toast.makeText(this, "需要录音权限才能录制语音", Toast.LENGTH_SHORT).show(); + } + } + else if (requestCode == PERMISSION_REQUEST_CODE) { + + boolean allGranted=true; + for (int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + allGranted = false; + break; + } + } + + if (allGranted) { + Toast.makeText(this, "媒体权限已授与", Toast.LENGTH_SHORT).show(); + checkAndshowNotifation(); + lastPhotoId=getLastPhotoId(); + startMonitoring(); + } else { + switchMonitor.setChecked(false); + Toast.makeText(this, "需要媒体权限以选择图片", Toast.LENGTH_SHORT).show(); + + + if (shouldShowRequestPermissionRationale(permissions)) { + showPermissionExplanationDialog(() -> { + // 重新请求权限 + requestPermissions(permissions); + }); + } + } + } + + } + private void handlePermissionDenial() { + // 检查哪些关键权限被拒绝 + boolean showRationale = false; + + // 检查是否有权限被永久拒绝(用户勾选了"不再询问") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + for (String permission : new String[]{ + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.POST_NOTIFICATIONS}) { + if (!shouldShowRequestPermissionRationale(permission) + && ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + showRationale = true; + break; + } + } + } else { + for (String permission : new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.ACCESS_FINE_LOCATION}) { + if (!shouldShowRequestPermissionRationale(permission) + && ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + showRationale = true; + break; + } + } + } + + if (showRationale) { + // 显示解释为什么需要权限,并引导用户去设置页面 + showPermissionExplanationDialog(); + } else { + // 部分权限被拒绝,但用户没有选择"不再询问" + Toast.makeText(this, "部分功能可能无法正常使用", Toast.LENGTH_SHORT).show(); + // 仍然尝试初始化位置管理器(如果位置权限被授予) + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED) { + initLocationManager(); + } + } + } + + // 显示权限解释对话框 + private void showPermissionExplanationDialog() { + new AlertDialog.Builder(this) + .setTitle("需要权限") + .setMessage("某些功能需要权限才能正常工作。请前往设置授予权限。") + .setPositiveButton("去设置", (dialog, which) -> { + // 跳转到应用设置页面 + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + }) + .setNegativeButton("取消", null) + .show(); + } + + private void checkPermissionAndPickImage() { + String[] permissions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+ + permissions = new String[]{ + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO + }; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13 + permissions = new String[]{Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO}; + } else { // Android 12- + permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; + } + + if (checkAnyPermissionGranted(permissions)) { + startMonitoring(); + } else { + requestPermissions(permissions); + } + } + + private boolean checkAnyPermissionGranted(String[] permissions) { + for (String perm : permissions) { + if (ContextCompat.checkSelfPermission(this, perm) == PackageManager.PERMISSION_GRANTED) { + return true; + } + } + return false; + } + + private void requestPermissions(String[] permissions) { + if (shouldShowRequestPermissionRationale(permissions)) { + showPermissionExplanationDialog(() -> + ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE)); + } else { + ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE); + } + } + + private boolean shouldShowRequestPermissionRationale(String[] permissions) { + for (String perm : permissions) { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, perm)) { + return true; + } + } + return false; + } + + private void openAppSettings() { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", getPackageName(), null)); + startActivity(intent); + } + + private void showPermissionExplanationDialog(Runnable onContinue) { + new AlertDialog.Builder(this) + .setTitle("需要权限") + .setMessage("此功能需要访问图片权限以监控相册变化") + .setPositiveButton("继续", (dialog, which) -> onContinue.run()) + .setNegativeButton("取消", null) + .show(); + } + + private void showHistory(AppDatabase database, String title, String deleteBtnText, String deleteMessage) { + new Thread(() -> { + List history = database.imageDao().getAll(); + runOnUiThread(() -> { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(title); + + LinearLayout mainContainer = createDialogContainer(deleteBtnText, database, deleteMessage, history); + ScrollView scrollContainer = createScrollContainer(history, database); + + mainContainer.addView(scrollContainer); + builder.setView(mainContainer); + builder.setPositiveButton("关闭", null); + + AlertDialog dialog = builder.create(); + updateCurrentDialog(database, dialog); + dialog.show(); + }); + }).start(); + } + + + private LinearLayout createDialogContainer(String deleteBtnText, AppDatabase database, String deleteMessage, List history) { + LinearLayout mainContainer = new LinearLayout(this); + mainContainer.setOrientation(LinearLayout.VERTICAL); + + // 顶部按钮布局 + LinearLayout buttonLayout = new LinearLayout(this); + buttonLayout.setOrientation(LinearLayout.HORIZONTAL); + buttonLayout.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + + // 批量删除按钮 + Button batchDeleteButton = new Button(this); + batchDeleteButton.setText("批量删除"); + batchDeleteButton.setOnClickListener(v -> showBatchDeleteDialog(database, history)); + buttonLayout.addView(batchDeleteButton); + + // 时间段删除按钮 + Button timeRangeDeleteButton = new Button(this); + timeRangeDeleteButton.setText("时间段删除"); + timeRangeDeleteButton.setOnClickListener(v -> showTimeRangeDeleteDialog(database)); + buttonLayout.addView(timeRangeDeleteButton); + + // 删除所有按钮 + Button deleteAllButton = new Button(this); + deleteAllButton.setText(deleteBtnText); + deleteAllButton.setOnClickListener(v -> confirmDelete(database, deleteMessage)); + buttonLayout.addView(deleteAllButton); + + mainContainer.addView(buttonLayout); + + return mainContainer; + } + + + + + private ScrollView createScrollContainer(List history, AppDatabase database) { + ScrollView scrollContainer = new ScrollView(this); + LinearLayout contentContainer = new LinearLayout(this); + contentContainer.setOrientation(LinearLayout.VERTICAL); + + if (history.isEmpty()) { + addEmptyView(contentContainer, database); + } else { + addHistoryItems(contentContainer, history, database); + } + + scrollContainer.addView(contentContainer); + return scrollContainer; + } + + private void addEmptyView(LinearLayout contentContainer, AppDatabase database) { + TextView emptyText = new TextView(this); + emptyText.setText(database == db ? "暂无历史记录" : "暂无缓存记录"); + contentContainer.addView(emptyText); + } + + private void addHistoryItems(LinearLayout contentContainer, List history, AppDatabase database) { + for (ImageEntity item : history) { + View itemView = LayoutInflater.from(this).inflate(R.layout.item_history, contentContainer, false); + + // 添加复选框 + CheckBox checkBox = new CheckBox(this); + checkBox.setTag(item.id); // 使用记录的ID作为tag + + LinearLayout itemLayout = (LinearLayout) itemView.findViewById(R.id.item_layout); + if (itemLayout != null) { + itemLayout.addView(checkBox, 0); // 将复选框添加到最前面 + } + + setupItemView(itemView, item); + contentContainer.addView(itemView); + } + } + private void showBatchDeleteDialog(AppDatabase database, List history) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("批量删除"); + + // 获取所有复选框的选中状态 + List selectedIds = new ArrayList<>(); + ViewGroup container = (ViewGroup) ((ViewGroup) currentDialog.getWindow().getDecorView()).getChildAt(0); + findSelectedCheckBoxes(container, selectedIds); + + if (selectedIds.isEmpty()) { + builder.setMessage("请先勾选要删除的项"); + builder.setPositiveButton("确定", null); + } else { + builder.setMessage("确定要删除选中的 " + selectedIds.size() + " 项记录吗?"); + builder.setPositiveButton("删除", (dialog, which) -> { + deleteSelectedRecords(database, selectedIds); + }); + builder.setNegativeButton("取消", null); + } + + builder.show(); + } + // 查找选中的复选框 + private void findSelectedCheckBoxes(ViewGroup container, List selectedIds) { + for (int i = 0; i < container.getChildCount(); i++) { + View child = container.getChildAt(i); + if (child instanceof ViewGroup) { + findSelectedCheckBoxes((ViewGroup) child, selectedIds); + } else if (child instanceof CheckBox) { + CheckBox checkBox = (CheckBox) child; + if (checkBox.isChecked()) { + selectedIds.add((Integer) checkBox.getTag()); + } + } + } + } + + // 删除选中的记录 + private void deleteSelectedRecords(AppDatabase database, List selectedIds) { + executor.execute(() -> { + database.imageDao().deleteByIds(selectedIds); + runOnUiThread(() -> { + Toast.makeText(this, "已删除选中的 " + selectedIds.size() + " 项记录", Toast.LENGTH_SHORT).show(); + dismissCurrentDialog(database); + }); + }); + } + // 显示时间段删除对话框 + // 显示时间段删除对话框 - 优化后的版本 + private void showTimeRangeDeleteDialog(AppDatabase database) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("按时间段删除"); + + // 使用ScrollView确保内容可滚动 + ScrollView scrollView = new ScrollView(this); + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(32, 32, 32, 32); // 增加内边距 + + // 开始时间部分 + TextView startText = new TextView(this); + startText.setText("开始时间:"); + startText.setTextSize(18); + layout.addView(startText); + + // 开始日期和时间选择器 + LinearLayout startTimeLayout = new LinearLayout(this); + startTimeLayout.setOrientation(LinearLayout.VERTICAL); + + final DatePicker startDatePicker = new DatePicker(this); + startDatePicker.setCalendarViewShown(false); // 使用spinner模式更紧凑 + startTimeLayout.addView(startDatePicker); + + final TimePicker startTimePicker = new TimePicker(this); + startTimePicker.setIs24HourView(true); + startTimeLayout.addView(startTimePicker); + + layout.addView(startTimeLayout); + + // 添加分隔空间 + View space = new View(this); + space.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 32)); + layout.addView(space); + + // 结束时间部分 + TextView endText = new TextView(this); + endText.setText("结束时间:"); + endText.setTextSize(18); + layout.addView(endText); + + // 结束日期和时间选择器 + LinearLayout endTimeLayout = new LinearLayout(this); + endTimeLayout.setOrientation(LinearLayout.VERTICAL); + + final DatePicker endDatePicker = new DatePicker(this); + endDatePicker.setCalendarViewShown(false); + endTimeLayout.addView(endDatePicker); + + final TimePicker endTimePicker = new TimePicker(this); + endTimePicker.setIs24HourView(true); + endTimeLayout.addView(endTimePicker); + + layout.addView(endTimeLayout); + + scrollView.addView(layout); + builder.setView(scrollView); + + builder.setPositiveButton("删除", (dialog, which) -> { + // 获取开始时间 + Calendar startCal = Calendar.getInstance(); + startCal.set(startDatePicker.getYear(), + startDatePicker.getMonth(), + startDatePicker.getDayOfMonth(), + startTimePicker.getHour(), + startTimePicker.getMinute(), + 0); + long startTime = startCal.getTimeInMillis(); + + // 获取结束时间 + Calendar endCal = Calendar.getInstance(); + endCal.set(endDatePicker.getYear(), + endDatePicker.getMonth(), + endDatePicker.getDayOfMonth(), + endTimePicker.getHour(), + endTimePicker.getMinute(), + 59); + long endTime = endCal.getTimeInMillis(); + + if (startTime > endTime) { + Toast.makeText(this, "开始时间不能晚于结束时间", Toast.LENGTH_SHORT).show(); + return; + } + + confirmTimeRangeDelete(database, startTime, endTime); + }); + + builder.setNegativeButton("取消", null); + + // 创建并显示对话框 + AlertDialog dialog = builder.create(); + + // 设置对话框大小 + Window window = dialog.getWindow(); + if (window != null) { + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(window.getAttributes()); + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + window.setAttributes(lp); + } + + dialog.show(); + } + + // 确认时间段删除 + private void confirmTimeRangeDelete(AppDatabase database, long startTime, long endTime) { + new AlertDialog.Builder(this) + .setTitle("确认删除") + .setMessage(String.format("确定要删除 %s 至 %s 的所有记录吗?", + sdf.format(new Date(startTime)), + sdf.format(new Date(endTime)))) + .setPositiveButton("删除", (d, w) -> deleteRecordsByTimeRange(database, startTime, endTime)) + .setNegativeButton("取消", null) + .show(); + } + + // 删除时间段内的记录 + private void deleteRecordsByTimeRange(AppDatabase database, long startTime, long endTime) { + executor.execute(() -> { + int deletedCount = database.imageDao().deleteByTimeRange(startTime, endTime); + runOnUiThread(() -> { + Toast.makeText(this, "已删除 " + deletedCount + " 条记录", Toast.LENGTH_SHORT).show(); + dismissCurrentDialog(database); + }); + }); + } + private void setupItemView(View itemView, ImageEntity item) { + ImageView iv = itemView.findViewById(R.id.iv_thumbnail); + TextView tv = itemView.findViewById(R.id.tv_info); + + executor.execute(() -> loadThumbnail(iv, item.path)); + + tv.setText(String.format( + Locale.US, + "时间: %s\n" + + "路径: %s\n"+ + "纬度: %.6f\n" + // 保留6位小数 + "经度: %.6f\n" + + "高度: %.2f 米\n" + // 保留2位小数 + "参与人名: %s\n"+ + "项目名字:%s\n"+ + "机组号:%s\n"+ + "叶片号:%d\n", + + sdf.format(new Date(item.time)), // 时间 + item.path, // 路径 + item.latitude, // 纬度 + item.longitude, // 经度 + item.altitude, // 高度(单位:米) + item.user,// 用户信息 + projectName, + item.unitName, + item.blade + + )); + itemView.setOnClickListener(v -> openFullImage(item.path)); + } + + private void loadThumbnail(ImageView iv, String path) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = DECODE_SAMPLE_SIZE; + Bitmap src = BitmapFactory.decodeFile(path, options); + if (src != null) { + Bitmap thumbnail = Bitmap.createScaledBitmap(src, THUMBNAIL_SIZE, THUMBNAIL_SIZE, true); + runOnUiThread(() -> iv.setImageBitmap(thumbnail)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void confirmDelete(AppDatabase database, String message) { + + new AlertDialog.Builder(this) + .setTitle("确认删除") + .setMessage(message) + .setPositiveButton("删除", (d, w) -> deleteAllRecords(database)) + .setNegativeButton("取消", null) + .show(); + + } + + + private void deleteAllRecords(AppDatabase database) { + executor.execute(() -> { + database.imageDao().deleteAll(); + runOnUiThread(() -> { + Toast.makeText(this, "已删除所有记录", Toast.LENGTH_SHORT).show(); + dismissCurrentDialog(database); + }); + }); + } + + private void updateCurrentDialog(AppDatabase database, AlertDialog dialog) { + if (database == db) { + currentDialog = dialog; + } else { + currentDialog2 = dialog; + } + } + + private void dismissCurrentDialog(AppDatabase database) { + if (database == db && currentDialog != null && currentDialog.isShowing()) { + currentDialog.dismiss(); + } else if (database == db2 && currentDialog2 != null && currentDialog2.isShowing()) { + currentDialog2.dismiss(); + } + } + + + private void startMonitoring() { + synchronized (monitoringLock) { + if (isMonitoring) return; + + if (isNetworkAvailable()) { + executor.execute(() -> { + // 使用上传锁保护数据库操作 + // 在后台线程中执行数据库操作 + List history = db2.imageDao().getAll(); + + + if (history != null && !history.isEmpty()) { + synchronized (uploadLock) { + for (ImageEntity image : history) { + String partname=getPartName(image.blade); + String partid=getPartIdByNameFast(unit,partname); + if(partid==null) + partid=partname; + uploadPhoto(image.path, image.time, lastKnownLocation, image.user, image.audioPath,image.project,image.unit,image.blade,partid,image.imageSource,image.unitName); + } + } + } + + // 删除所有记录(仍在后台线程) + db2.imageDao().deleteAll(); + + }); + } + + // 创建内容观察者 + Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + + contentObserver = new ContentObserver(new Handler()) { + @SuppressLint("SuspiciousIndentation") + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + if(!isRecording) + checkNewPhotos(); + } + }; + + // 注册观察者 + getContentResolver().registerContentObserver(uri, true, contentObserver); + isMonitoring = true; + runOnUiThread(() -> statusText.setText("当前状态:正在监听相册变化")); + } + } + private boolean isNetworkAvailable() { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) return false; + + NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork()); + return capabilities != null && + (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)); + } + private void stopMonitoring() { + if (!isMonitoring) return; + + // 注销观察者 + if (contentObserver != null) { + getContentResolver().unregisterContentObserver(contentObserver); + contentObserver = null; + } + isMonitoring = false; + statusText.setText("当前状态:未监听"); + } + private void startUploadQueue() { + if (isUploading) { + Toast.makeText(this, "上传正在进行中", Toast.LENGTH_SHORT).show(); + return; + } + + executor.execute(() -> { + isUploading = true; + runOnUiThread(() -> { + btnUploadQueue.setEnabled(false); + btnStopUpload.setEnabled(true); + statusText.setText("正在上传缓存队列..." + (isTimedUploadRunning ? "(定时上传)" : "")); + }); + + try { + while (isUploading) { + List queue = db2.imageDao().getAll(); + if (queue.isEmpty()) { + break; + } + + for (ImageEntity image : queue) { + if (!isUploading) break; + String partname=getPartName(image.blade); + String partid=getPartIdByNameFast(unit,partname); + if(partid==null) + partid=partname; + synchronized (uploadLock) { + uploadPhotoSync(image.path, image.time, + new Location("") {{ + setLatitude(image.latitude); + setLongitude(image.longitude); + setAltitude(image.altitude); + }}, + image.user, image.audioPath,image.project,image.unit,image.blade,partid,image.imageSource,image.unitName); + } + + if (isUploading) { + db2.imageDao().delete(image); + } + } + } + } finally { + runOnUiThread(() -> { + btnUploadQueue.setEnabled(true); + btnStopUpload.setEnabled(false); + String status = isUploading ? + (isTimedUploadRunning ? "定时上传完成" : "手动上传完成") : + "上传已终止"; + statusText.setText(status); + }); + isUploading = false; + } + }); + } + @Override + protected void onDestroy() { + super.onDestroy(); + if (updateReceiver != null) { + unregisterReceiver(updateReceiver); + } + stopMonitoring(); + executor.shutdown(); + if (httpClient != null) { + httpClient.dispatcher().executorService().shutdown(); + httpClient.connectionPool().evictAll(); + } + stopTimedUpload(); + if (!executor.isTerminated()) { + executor.shutdownNow(); + } + // 移除悬浮球 + if (floatingActionButton != null) { + WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + windowManager.removeView(floatingActionButton); + } + if(isRecording) + stopVoiceRecording(); + + } + + + // + private void checkNewPhotos() { + + String[] projection = { + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.DATE_ADDED, + MediaStore.Images.Media.DATE_TAKEN, // 拍摄时间(相机照片才有) + MediaStore.Images.Media.LATITUDE, + MediaStore.Images.Media.LONGITUDE + }; + +// 查询条件:ID > lastPhotoId 且 DATE_TAKEN > 0(确保是相机拍摄的照片) + String selection = MediaStore.Images.Media._ID + " > ? AND " + + MediaStore.Images.Media.DATE_TAKEN + " > 0 AND " + + "(" + MediaStore.Images.Media.DATA + " LIKE '%/DCIM/%' OR " + + MediaStore.Images.Media.DATA + " LIKE '%/Camera/%')"; + String[] selectionArgs = new String[]{ + String.valueOf(lastPhotoId), + + }; + Cursor cursor = getContentResolver().query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, + selection, + selectionArgs, + MediaStore.Images.Media._ID + " DESC" + ); + + if (cursor != null) { + while (cursor.moveToNext()) { + long id = cursor.getLong(0); + String path = cursor.getString(1); + imgpath=path; + long time = cursor.getLong(2) * 1000; + double latitude = cursor.getDouble(3); + double longitude = cursor.getDouble(4); + double altitude = 0.0; // 默认值 + if(locationManager!=null) { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + + break; + } + else { + Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (lastLocation != null) { + lastKnownLocation = lastLocation; + updateLocationText(lastLocation); + } + } + + } + if (lastKnownLocation != null) { + latitude = lastKnownLocation.getLatitude(); + longitude = lastKnownLocation.getLongitude(); + altitude = lastKnownLocation.getAltitude(); + } + + // 从EXIF获取高度信息 + try { + ExifInterface exif = new ExifInterface(path); + String altitudeStr = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE); + if (altitudeStr != null) { + altitude = Double.parseDouble(altitudeStr); + } + } catch (Exception e) { + e.printStackTrace(); + } + + String user = getCurrentUser(); + Log.e("用户是", user); + + // 生成缩略图 + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeFile(path); + } catch (Exception e) { + e.printStackTrace(); + } + + if (bitmap != null) { + Bitmap thumbnail = Bitmap.createScaledBitmap(bitmap, THUMBNAIL_SIZE, THUMBNAIL_SIZE, true); + String audioPath = currentAudioPath ==null?"0":currentAudioPath; + ImageInfo imageInfo = new ImageInfo(path, time, thumbnail, latitude, longitude, altitude, user, audioPath); + + // 保护imageList和lastPhotoId的修改 + + lastPhotoId = Math.max(lastPhotoId, id); + + + double finalAltitude = altitude; + double finalLongitude = longitude; + double finalLatitude = latitude; + + if(isNetworkAvailable()&&!isRecording) { + + executor.execute(() -> { + List history = db2.imageDao().getAll(); + + if (history != null && !history.isEmpty()) { + + synchronized (uploadLock) { + for (ImageEntity image : history) { + String partname=getPartName(image.blade); + String partid=getPartIdByNameFast(unit,partname); + if(partid==null) + partid=partname; + uploadPhoto(image.path, image.time, lastKnownLocation, + image.user, image.audioPath, image.project, + image.unit, image.blade,partid,image.imageSource,image.unitName); + } + } + } + db2.imageDao().deleteAll(); + }); + executor.execute(() -> { + String partname=getPartName(blade); + String partid=getPartIdByNameFast(unit,partname); + if(partid==null) + partid=partname; + + uploadPhoto(path, time, lastKnownLocation, user, + audioPath, projectId, unit, blade,partid,ChooseImageSource,unitName); + }); + + } + else { + String partname=getPartName(blade); + String partid=getPartIdByNameFast(unit,partname); + if(partid==null) + partid=partname; + Log.e("partid:************",partid); + BackgroundToast.show(MainActivity.this,"没有网路"); + executor.execute(() -> { + db2.imageDao().insert(new ImageEntity(path, time,finalLatitude,finalLongitude,finalAltitude, user, audioPath,projectId,unit,blade,false,unitName,Temperator,Humidity,Weather,ChooseImageSource)); + + }); + } + + }; + + + currentAudioPath=null; + } + cursor.close(); + } + + + + } + + + private String getCurrentUser() { + String user = name; + // 如果用户没有输入信息,则使用设备型号作为默认值 + if (user=="-1") { + user = currentUsername; + } + return user; + } + private String getProject(){ + + return projectId.isEmpty()?"项目ID未填写":projectId; + } + private String getUnit() + { + return unit.isEmpty()?"机组未填写":unit; + } + private int getBlade() + { + return blade; + } + private void openFullImage(String imagePath) { + File file = new File(imagePath); + Uri uri = FileProvider.getUriForFile( + this, + getPackageName() + ".provider", + file + ); + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(uri, "image/*"); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(intent); + } + + + private void uploadPhoto(String filePath, long time, Location lastKnownLocation, String user, + String audioPath, String project, String unit, int blade, String partid,String imageSource,String unitName) { + synchronized (uploadLock) { + if (isUploading) { + // 如果正在上传,直接加入队列 + double latitude; + double longitude; + double altitude; + if (lastKnownLocation != null) { + latitude = lastKnownLocation.getLatitude(); + longitude = lastKnownLocation.getLongitude(); + altitude = lastKnownLocation.getAltitude(); + } else { + altitude = 0; + longitude = 0; + latitude = 0; + } + executor.execute(() -> { + db2.imageDao().insert(new ImageEntity(filePath, time, latitude, longitude, altitude, + user, audioPath, project, unit, blade, false, unitName, Temperator, Humidity, Weather,ChooseImageSource)); + }); + return; + } + } + + // 在后台线程执行上传逻辑 + executor.execute(() -> { + File imageFile = new File(filePath); + + double latitude; + double longitude; + double altitude; + if (lastKnownLocation != null) { + latitude = lastKnownLocation.getLatitude(); + longitude = lastKnownLocation.getLongitude(); + altitude = lastKnownLocation.getAltitude(); + } else { + altitude = 0; + longitude = 0; + latitude = 0; + } + + // 构建 Multipart 请求体 + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("image", imageFile.getName(), + RequestBody.create(imageFile, MediaType.parse("image/*"))) + .addFormDataPart("time", String.valueOf(time)) + .addFormDataPart("latitude", String.valueOf(latitude)) + .addFormDataPart("longitude", String.valueOf(longitude)) + .addFormDataPart("altitude", String.valueOf(altitude)) + .addFormDataPart("uploadUser", user) + .addFormDataPart("projectId", project) + .addFormDataPart("turbineId", unit) + .addFormDataPart("partId", partid) + .addFormDataPart("imageSource", imageSource); + + + RequestBody requestBody = builder.build(); + + // 构建请求 + Request request = new Request.Builder() + .url("http://pms.dtyx.net:9158/common/upload-image/file") + .post(requestBody) + .addHeader("Authorization", token) + .build(); + + // 使用 try-with-resources 确保 Response 关闭 + try (Response response = httpClient.newCall(request).execute()) { + if (response.isSuccessful()) { + // 上传成功 + db.imageDao().insert(new ImageEntity(filePath, time, latitude, longitude, altitude, + user, audioPath, project, unit, blade, true, unitName, Temperator, Humidity, Weather,ChooseImageSource)); + BackgroundToast.show(MainActivity.this, "上传成功: " + filePath); + } else { + // 上传失败(HTTP 状态码非 2xx) + db2.imageDao().insert(new ImageEntity(filePath, time, latitude, longitude, altitude, + user, audioPath, project, unit, blade, false, unitName, Temperator, Humidity, Weather,ChooseImageSource)); + BackgroundToast.show(MainActivity.this, "上传失败: " + response.code() + " " + response.message() + ",已存入缓冲队列"); + } + } catch (IOException e) { + // 网络异常 + db2.imageDao().insert(new ImageEntity(filePath, time, latitude, longitude, altitude, + user, audioPath, project, unit, blade, false, unitName, Temperator, Humidity, Weather,ChooseImageSource)); + BackgroundToast.show(MainActivity.this, "上传失败: " + e.getMessage() + ",已存入缓冲队列"); + } + }); + } +// private void uploadPhoto(String filePath, long time, Location lastKnownLocation, String user, String audioPath, String project, String unit, int blade,String partid) { +// synchronized (uploadLock) { +// if (isUploading) { +// // 如果正在执行队列上传,则直接加入队列不单独上传 +// double latitude = 0; +// double longitude = 0; +// double altitude = 0; +// if (lastKnownLocation != null) { +// latitude = lastKnownLocation.getLatitude(); +// longitude = lastKnownLocation.getLongitude(); +// altitude = lastKnownLocation.getAltitude(); +// } +// +// db2.imageDao().insert(new ImageEntity(filePath, time, latitude, longitude, altitude, user, audioPath, project, unit, blade, false,unitName,Temperator,Humidity,Weather)); +// return; +// } +// } +// +// File imageFile = new File(filePath); +// +// double latitude; +// double longitude; +// double altitude; +// if (lastKnownLocation != null) { +// latitude = lastKnownLocation.getLatitude(); +// longitude = lastKnownLocation.getLongitude(); +// altitude = lastKnownLocation.getAltitude(); +// } else { +// altitude = 0; +// longitude = 0; +// latitude = 0; +// } +// +// // 创建包含图片文件、录音文件和元数据的请求体 +// MultipartBody.Builder builder = new MultipartBody.Builder() +// .setType(MultipartBody.FORM) +// .addFormDataPart("image", imageFile.getName(), +// RequestBody.create(imageFile, MediaType.parse("image/*"))) +// .addFormDataPart("time", String.valueOf(time)) +// .addFormDataPart("latitude", String.valueOf(latitude)) +// .addFormDataPart("longitude", String.valueOf(longitude)) +// .addFormDataPart("altitude", String.valueOf(altitude)) +// .addFormDataPart("uploadUser", user) +// .addFormDataPart("projectId", project) +// .addFormDataPart("turbineId", unit) +// .addFormDataPart("partId", partid); +// +// +// +// +// RequestBody requestBody = builder.build(); +// +// // 构建请求 +// Request request = new Request.Builder() +// .url("http://pms.dtyx.net:9158/common/upload-image/file") // 修改为你的实际URL +// .post(requestBody) +// .addHeader("Authorization",token) +// .build(); +// +// httpClient.newCall(request).enqueue(new Callback() { +// @Override +// public void onFailure(Call call, IOException e) { +// +// BackgroundToast.show( MainActivity.this, +// "上传失败: " + e.getMessage() + "已存入缓冲队列" +// ); +// executor.execute(() -> { +// db2.imageDao().insert(new ImageEntity(filePath, time, latitude, longitude, altitude, user, audioPath, project, unit, blade, false,unitName,Temperator,Humidity,Weather)); +// }); +// } +// +// @Override +// public void onResponse(Call call, Response response) { +// runOnUiThread(() -> { +// if (response.isSuccessful()) { +// BackgroundToast.show( +// MainActivity.this, +// "上传成功: " + response.code() + filePath); +// executor.execute(() -> { +// db.imageDao().insert(new ImageEntity(filePath, time, latitude, longitude, altitude, user, audioPath, project, unit, blade, true,unitName,Temperator,Humidity,Weather)); +// +// +// }); +// +// +// +// } else { +// BackgroundToast.show( +// MainActivity.this, +// "上传失败: " + response.code() +response.message()+ "已存入缓冲队列" +// ); +// executor.execute(() -> { +// db2.imageDao().insert(new ImageEntity(filePath, time, latitude, longitude, altitude, user, audioPath, project, unit, blade, false,unitName,Temperator,Humidity,Weather)); +// }); +// } +// }); +// } +// }); +// } +// private void uploadAudio(String imageId, String audioPath, String token) { +// if (audioPath == null || audioPath.equals("0")) { +// BackgroundToast.show(MainActivity.this,"文件路径为空"); +// +// } +// +// File audioFile = new File(audioPath); +// if (!audioFile.exists()) { +// BackgroundToast.show(MainActivity.this,"文件不存在"); +// +// } +// +// RequestBody requestBody = new MultipartBody.Builder() +// .setType(MultipartBody.FORM) +// .addFormDataPart("file", audioFile.getName(), +// RequestBody.create(audioFile, MediaType.parse("audio/*"))) +// .build(); +// +// Request.Builder requestBuilder = new Request.Builder() +// .url( "http://pms.dtyx.net:9158/audio/upload/" + imageId) +// .addHeader("Authorization",token) +// .post(requestBody); +// +// if (token != null && !token.isEmpty()) { +// requestBuilder.addHeader("Authorization", token); +// } +// +// httpClient.newCall(requestBuilder.build()).enqueue(new Callback() { +// @Override +// public void onFailure(Call call, IOException e) { +// +// BackgroundToast.show( +// MainActivity.this, +// "录音上传失败: " + e.getMessage() +// ); +// String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) +// .format(new Date()); +// +// executor.execute(() -> { +// db.AudioDao().insert(new AudioEntity(audioPath,imageId,currentTime)); +// }); +// +// } +// +// @Override +// public void onResponse(Call call, Response response) { +// // Handle response +// if (!response.isSuccessful()) { +// // Handle upload failure +// +// BackgroundToast.show( +// MainActivity.this, +// "录音上传失败: " + response.code() + response.message() +// ); +// String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) +// .format(new Date()); +// +// executor.execute(() -> { +// db.AudioDao().insert(new AudioEntity(audioPath,imageId,currentTime)); +// }); +// } +// else { +// BackgroundToast.show( +// MainActivity.this, +// "录音上传成功: " + response.code() + imageId +// ); +// String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) +// .format(new Date()); +// executor.execute(() -> { +// db.AudioDao().delete(new AudioEntity(audioPath,imageId,currentTime)); +// }); +// executor.execute(() -> { +// db2.AudioDao().insert(new AudioEntity(audioPath,imageId,currentTime)); +// }); +// +// } +// response.close(); +// } +// }); +// } +private void uploadAudio(String imageId, String audioPath, String token) { + // 1. 参数校验(在调用线程执行,避免不必要的后台任务) + if (audioPath == null || audioPath.equals("0")) { + BackgroundToast.show(MainActivity.this, "文件路径为空"); + return; + } + + File audioFile = new File(audioPath); + if (!audioFile.exists()) { + BackgroundToast.show(MainActivity.this, "文件不存在"); + return; + } + + // 2. 在后台线程执行上传逻辑 + executor.execute(() -> { + // 3. 构建请求体(已在后台线程) + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", audioFile.getName(), + RequestBody.create(audioFile, MediaType.parse("audio/*"))) + .build(); + + // 4. 构建请求 + Request.Builder requestBuilder = new Request.Builder() + .url("http://pms.dtyx.net:9158/audio/upload/" + imageId) + .post(requestBody); + + if (token != null && !token.isEmpty()) { + requestBuilder.addHeader("Authorization", token); + } + + // 5. 同步执行请求(try-with-resources 自动关闭 Response) + try (Response response = httpClient.newCall(requestBuilder.build()).execute()) { + String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + .format(new Date()); + + if (response.isSuccessful()) { + // 上传成功 + db.AudioDao().delete(new AudioEntity(audioPath, imageId, currentTime)); + db2.AudioDao().insert(new AudioEntity(audioPath, imageId, currentTime)); + BackgroundToast.show(MainActivity.this, "录音上传成功: " + imageId); + } else { + // 上传失败(HTTP 错误) + db.AudioDao().insert(new AudioEntity(audioPath, imageId, currentTime)); + BackgroundToast.show(MainActivity.this, + "录音上传失败: " + response.code() + " " + response.message()); + } + } catch (IOException e) { + // 网络异常 + String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + .format(new Date()); + db.AudioDao().insert(new AudioEntity(audioPath, imageId, currentTime)); + BackgroundToast.show(MainActivity.this, "录音上传失败: " + e.getMessage()); + } + }); +} + private long getLastPhotoId() { + long id = -1; + Cursor cursor = getContentResolver().query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + new String[]{"MAX(" + MediaStore.Images.Media._ID + ")"}, + null, + null, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + id = cursor.getLong(0); // MAX(_ID) 的结果在索引 0 + cursor.close(); + } + return id; + } + + +} \ No newline at end of file