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(); } }