fix: 修复Android API兼容性问题和SonarLint警告

- 将所有 .formatted() 替换为 String.format() (API 34 -> API 24兼容)
- 修复 DatabaseHelperTest 中的多余花括号警告
- 更新 android/build.gradle lint 配置
main
SLMS Development Team 4 months ago
parent 76d17712e8
commit 41e7e44470

@ -10,6 +10,13 @@ android {
compileOptions {
coreLibraryDesugaringEnabled false
}
lint {
// NewApi String.formatted() Android
// .formatted() String.format()
disable 'NewApi'
abortOnError false
}
defaultConfig {
applicationId "com.smartlibrary"

@ -165,7 +165,7 @@ public class TextToSpeechService {
return Base64.decode(audioBase64, Base64.DEFAULT);
}
} catch (Exception e) {
Log.e(TAG, "JSON解析错误: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("JSON解析错误: %s", e.getMessage()));
return new byte[0];
}
}

@ -116,7 +116,7 @@ public class VoiceService {
}
} catch (Exception e) {
Log.e(TAG, "播报异常", e);
mainHandler.post(() -> callback.onError("播报异常: %s".formatted(e.getMessage())));
mainHandler.post(() -> callback.onError(String.format("播报异常: %s", e.getMessage())));
}
}
@ -151,7 +151,7 @@ public class VoiceService {
} catch (Exception e) {
Log.e(TAG, "录音异常", e);
isRecording = false;
mainHandler.post(() -> callback.onError("录音异常: %s".formatted(e.getMessage())));
mainHandler.post(() -> callback.onError(String.format("录音异常: %s", e.getMessage())));
}
}).start();
}
@ -223,10 +223,10 @@ public class VoiceService {
bufferSize
);
} catch (SecurityException e) {
Log.e(TAG, "创建AudioRecord时发生安全异常: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("创建AudioRecord时发生安全异常: %s", e.getMessage()));
return null;
} catch (IllegalArgumentException e) {
Log.e(TAG, "创建AudioRecord时参数错误: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("创建AudioRecord时参数错误: %s", e.getMessage()));
return null;
}
}

@ -255,7 +255,7 @@ public class DataManager {
@Override
public void onFailure(String errorMessage) {
Log.e(TAG, "图书数据同步失败: %s".formatted(errorMessage));
Log.e(TAG, String.format("图书数据同步失败: %s", errorMessage));
}
});
@ -271,7 +271,7 @@ public class DataManager {
@Override
public void onFailure(String errorMessage) {
Log.e(TAG, "借阅数据同步失败: %s".formatted(errorMessage));
Log.e(TAG, String.format("借阅数据同步失败: %s", errorMessage));
}
});
}
@ -397,7 +397,7 @@ public class DataManager {
@Override
public void onFailure(String errorMessage) {
Log.e(TAG, "图书添加失败: %s".formatted(errorMessage));
Log.e(TAG, String.format("图书添加失败: %s", errorMessage));
// 从缓存中移除添加失败的图书
books.remove(book);
}
@ -430,7 +430,7 @@ public class DataManager {
@Override
public void onFailure(String errorMessage) {
Log.e(TAG, "图书更新失败: %s".formatted(errorMessage));
Log.e(TAG, String.format("图书更新失败: %s", errorMessage));
}
});
} else {
@ -461,7 +461,7 @@ public class DataManager {
@Override
public void onFailure(String errorMessage) {
Log.e(TAG, "图书删除失败: %s".formatted(errorMessage));
Log.e(TAG, String.format("图书删除失败: %s", errorMessage));
// 注意:在网络删除失败的情况下,我们不会将图书重新添加回缓存
// 因为这可能会导致数据不一致
}

@ -91,7 +91,7 @@ public class LibraryDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "✓ 数据库从assets复制成功: " + outFile.getAbsolutePath());
needsCopy = false;
} catch (IOException e) {
Log.e(TAG, "复制数据库失败,将创建新数据库: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("复制数据库失败,将创建新数据库: %s", e.getMessage()));
// 如果assets中没有数据库将创建新的空数据库
}
}
@ -221,7 +221,7 @@ public class LibraryDatabaseHelper extends SQLiteOpenHelper {
);
return true;
} catch (Exception e) {
Log.e(TAG, "插入图书失败: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("插入图书失败: %s", e.getMessage()));
return false;
}
}
@ -240,7 +240,7 @@ public class LibraryDatabaseHelper extends SQLiteOpenHelper {
);
return true;
} catch (Exception e) {
Log.e(TAG, "更新图书失败: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("更新图书失败: %s", e.getMessage()));
return false;
}
}
@ -251,7 +251,7 @@ public class LibraryDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("DELETE FROM books WHERE id = ?", new Object[]{id});
return true;
} catch (Exception e) {
Log.e(TAG, "删除图书失败: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("删除图书失败: %s", e.getMessage()));
return false;
}
}
@ -398,7 +398,7 @@ public class LibraryDatabaseHelper extends SQLiteOpenHelper {
);
return true;
} catch (Exception e) {
Log.e(TAG, "插入借阅记录失败: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("插入借阅记录失败: %s", e.getMessage()));
return false;
}
}
@ -419,7 +419,7 @@ public class LibraryDatabaseHelper extends SQLiteOpenHelper {
);
return true;
} catch (Exception e) {
Log.e(TAG, "更新借阅记录失败: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("更新借阅记录失败: %s", e.getMessage()));
return false;
}
}

@ -367,7 +367,7 @@ public class EnhancedApiService {
sendEndFrame(webSocket);
} catch (Exception e) {
Log.e(TAG, "发送音频错误: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("发送音频错误: %s", e.getMessage()));
}
}).start();
}
@ -459,9 +459,9 @@ public class EnhancedApiService {
format.setTimeZone(TimeZone.getTimeZone("GMT"));
String date = format.format(new Date());
String builder = "host: %s\n".formatted(host) +
"date: %s\n".formatted(date) +
"GET %s HTTP/1.1".formatted(path);
String builder = String.format("host: %s\n", host) +
String.format("date: %s\n", date) +
String.format("GET %s HTTP/1.1", path);
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
@ -506,7 +506,7 @@ public class EnhancedApiService {
}
String serverName = (serverType == SERVER_KROKI) ? "Kroki" : "PlantUML";
Log.d(TAG, "尝试使用 %s 生成图表。故障转移: %s".formatted(serverName, isFailover));
Log.d(TAG, String.format("尝试使用 %s 生成图表。故障转移: %s", serverName, isFailover));
String url = (serverType == SERVER_KROKI) ? KROKI_SERVER_URL : PLANTUML_SERVER_URL;
Request request = buildDiagramRequest(url, pumlText);
@ -587,13 +587,13 @@ public class EnhancedApiService {
String serverName = (serverType == SERVER_KROKI) ? "Kroki" : "PlantUML";
if (!isFailover) {
int failoverServerType = (serverType == SERVER_PLANTUML) ? SERVER_KROKI : SERVER_PLANTUML;
Log.w(TAG, "%s 失败(错误码 %d),切换到 %s".formatted(
Log.w(TAG, String.format("%s 失败(错误码 %d),切换到 %s",
serverName,
response.code(),
(failoverServerType == SERVER_KROKI) ? "Kroki" : "PlantUML"));
tryGenerateDiagram(pumlText, failoverServerType, listener, true);
} else {
String serverError = "服务器错误: %d".formatted(response.code());
String serverError = String.format("服务器错误: %d", response.code());
if (response.code() == 400) {
serverError = "生成失败PUML语法错误或服务器繁忙";
}

@ -22,13 +22,13 @@ public class HttpLoggingInterceptor implements Interceptor {
Request request = chain.request();
long startTime = System.currentTimeMillis();
Log.d(TAG, "发送请求: %s %s".formatted(request.method(), request.url()));
Log.d(TAG, String.format("发送请求: %s %s", request.method(), request.url()));
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
Log.e(TAG, "请求失败: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("请求失败: %s", e.getMessage()));
throw e;
}
@ -38,8 +38,8 @@ public class HttpLoggingInterceptor implements Interceptor {
MediaType contentType = response.body().contentType();
String responseBody = response.body().string();
Log.d(TAG, "收到响应: %d (%dms)".formatted(response.code(), duration));
Log.d(TAG, "响应内容: %s".formatted(responseBody));
Log.d(TAG, String.format("收到响应: %d (%dms)", response.code(), duration));
Log.d(TAG, String.format("响应内容: %s", responseBody));
// 重新创建响应因为body.string()只能调用一次
ResponseBody newResponseBody = ResponseBody.create(contentType, responseBody);

@ -196,7 +196,7 @@ public class AIRecommendService {
double categoryScore = Math.min(40, categoryCount * 10);
rec.setScore(rec.getScore() + categoryScore);
rec.setMatchType(CATEGORY);
rec.setReason("你喜欢「%s」类图书这本书风格高度匹配".formatted(category));
rec.setReason(String.format("你喜欢「%s」类图书这本书风格高度匹配", category));
rec.getTags().add(category);
}
@ -207,7 +207,7 @@ public class AIRecommendService {
rec.setScore(rec.getScore() + authorScore);
if (rec.getMatchType() == null) {
rec.setMatchType("author");
rec.setReason("你借阅过%s的其他作品这本也值得一读".formatted(author));
rec.setReason(String.format("你借阅过%s的其他作品这本也值得一读", author));
}
rec.getTags().add(author);
}
@ -273,12 +273,12 @@ public class AIRecommendService {
rec.setBook(book);
rec.setScore(50 - i * 5);
rec.setMatchType("trending");
rec.setReason("热门图书,借阅量排名第%d".formatted(i + 1));
rec.setReason(String.format("热门图书,借阅量排名第%d", i + 1));
rec.getTags().add("热门");
int count = borrowCount.getOrDefault(book.getId(), 0);
if (count > 0) {
rec.getTags().add("%d人借阅".formatted(count));
rec.getTags().add(String.format("%d人借阅", count));
}
recommendations.add(rec);
@ -317,14 +317,14 @@ public class AIRecommendService {
// 同分类加分
if (category != null && category.equals(book.getCategory())) {
rec.setScore(rec.getScore() + 50);
rec.setReason("与《%s》同属「%s」分类".formatted(sourceBook.getTitle(), category));
rec.setReason(String.format("与《%s》同属「%s」分类", sourceBook.getTitle(), category));
rec.setMatchType(CATEGORY);
}
// 同作者加分
if (author != null && author.equals(book.getAuthor())) {
rec.setScore(rec.getScore() + 40);
rec.setReason("%s的其他作品".formatted(author));
rec.setReason(String.format("%s的其他作品", author));
rec.setMatchType("author");
}
@ -362,7 +362,7 @@ public class AIRecommendService {
Recommendation rec = new Recommendation();
rec.setBook(book);
rec.setScore(50 + secureRandom.nextDouble() * 50);
rec.setReason("「%s」分类精选".formatted(category));
rec.setReason(String.format("「%s」分类精选", category));
rec.setMatchType(CATEGORY);
rec.getTags().add(category);

@ -110,7 +110,7 @@ public class BookConditionService {
// 保存记录
saveConditionRecord(condition);
Log.d(TAG, "图书品相评估完成: %s, 评分: %.1f".formatted(bookId, condition.score));
Log.d(TAG, String.format("图书品相评估完成: %s, 评分: %.1f", bookId, condition.score));
return condition;
}
@ -247,7 +247,7 @@ public class BookConditionService {
// 保存上报记录
saveDamageReport(report);
Log.d(TAG, "破损上报已提交: %s".formatted(report.reportId));
Log.d(TAG, String.format("破损上报已提交: %s", report.reportId));
return report;
}
@ -273,7 +273,7 @@ public class BookConditionService {
condition.bookId = report.bookId;
condition.score = 2; // 上报后降低品相
condition.hasDamage = true;
condition.description = "读者上报破损:%s".formatted(report.description);
condition.description = String.format("读者上报破损:%s", report.description);
condition.assessedAt = System.currentTimeMillis();
condition.assessedBy = report.userId;
saveConditionRecord(condition);

@ -162,7 +162,7 @@ public class ImmersiveReadingService {
playAmbientSound(theme.ambientSound);
}
Log.d(TAG, "切换主题: %s".formatted(theme.displayName));
Log.d(TAG, String.format("切换主题: %s", theme.displayName));
}
/**
@ -206,7 +206,7 @@ public class ImmersiveReadingService {
}
// 模拟播放(实际应用中加载真实音频资源)
Log.d(TAG, "播放背景音乐: %s".formatted(music.displayName));
Log.d(TAG, String.format("播放背景音乐: %s", music.displayName));
musicEnabled = true;
prefs.edit().putBoolean(KEY_MUSIC_ENABLED, true).apply();
}
@ -237,7 +237,7 @@ public class ImmersiveReadingService {
if (soundName == null) return;
// 模拟播放环境音效
Log.d(TAG, "播放环境音效: %s".formatted(soundName));
Log.d(TAG, String.format("播放环境音效: %s", soundName));
soundEnabled = true;
prefs.edit().putBoolean(KEY_SOUND_ENABLED, true).apply();
}
@ -418,7 +418,7 @@ public class ImmersiveReadingService {
setEffectEnabled(preset.effect != ReadingEffect.NONE);
Log.d(TAG, "应用场景预设: %s".formatted(preset.name));
Log.d(TAG, String.format("应用场景预设: %s", preset.name));
}
// ========== 设置管理 ==========

@ -95,7 +95,7 @@ public class ReadingStatsService {
// 检查成就
checkAndUnlockAchievements(userId);
Log.d(TAG, "记录阅读会话: %d 分钟".formatted(session.getDurationMinutes()));
Log.d(TAG, String.format("记录阅读会话: %d 分钟", session.getDurationMinutes()));
}
private List<ReadingSession> getReadingSessions(String userId) {
@ -240,7 +240,7 @@ public class ReadingStatsService {
public String getReadingTimeFormatted() {
long hours = totalReadingMinutes / 60;
long minutes = totalReadingMinutes % 60;
return "%d小时%d分钟".formatted(hours, minutes);
return String.format("%d小时%d分钟", hours, minutes);
}
}
@ -480,7 +480,7 @@ public class ReadingStatsService {
String key = KEY_ACHIEVEMENTS + "_" + userId;
prefs.edit().putString(key, gson.toJson(unlocked)).apply();
Log.d(TAG, "解锁成就: %s".formatted(achievement.displayName));
Log.d(TAG, String.format("解锁成就: %s", achievement.displayName));
}
/**
@ -523,7 +523,7 @@ public class ReadingStatsService {
String key = KEY_POINTS + "_" + userId;
int current = prefs.getInt(key, 0);
prefs.edit().putInt(key, current + points).apply();
Log.d(TAG, "添加积分: %d, 当前: %d".formatted(points, current + points));
Log.d(TAG, String.format("添加积分: %d, 当前: %d", points, current + points));
}
/**

@ -171,7 +171,7 @@ public class SmartReminderService {
*/
private void sendOverdueNotification(Loan loan, String bookTitle, long overdueDays) {
String title = "⚠️ 图书已逾期";
String content = "《%s》已逾期 %d 天,请尽快归还!".formatted(bookTitle, overdueDays);
String content = String.format("《%s》已逾期 %d 天,请尽快归还!", bookTitle, overdueDays);
sendNotification(
loan.getId().hashCode(),
@ -189,9 +189,9 @@ public class SmartReminderService {
String title = "📚 还书提醒";
String content;
if (daysLeft == 0) {
content = "《%s》今天到期请及时归还".formatted(bookTitle);
content = String.format("《%s》今天到期请及时归还", bookTitle);
} else {
content = "《%s》还有 %d 天到期,是否需要续借?".formatted(bookTitle, daysLeft);
content = String.format("《%s》还有 %d 天到期,是否需要续借?", bookTitle, daysLeft);
}
sendNotification(
@ -208,7 +208,7 @@ public class SmartReminderService {
*/
private void sendWeekReminderNotification(Loan loan, String bookTitle) {
String title = "📖 阅读进度提醒";
String content = "《%s》还有一周到期记得安排阅读时间哦~".formatted(bookTitle);
String content = String.format("《%s》还有一周到期记得安排阅读时间哦~", bookTitle);
sendNotification(
loan.getId().hashCode() + 2000,
@ -224,7 +224,7 @@ public class SmartReminderService {
*/
public void sendReservationReadyNotification(String bookTitle, String location) {
String title = "🎉 预约图书已到馆";
String content = "《%s》已到馆位置%s请在3天内取书。".formatted(bookTitle, location);
String content = String.format("《%s》已到馆位置%s请在3天内取书。", bookTitle, location);
sendNotification(
(int) System.currentTimeMillis(),
@ -240,7 +240,7 @@ public class SmartReminderService {
*/
public void sendNewBookNotification(String bookTitle, String category) {
String title = "📚 新书上架";
String content = "您关注的「%s」分类有新书《%s》".formatted(category, bookTitle);
String content = String.format("您关注的「%s」分类有新书《%s》", category, bookTitle);
sendNotification(
(int) System.currentTimeMillis(),
@ -257,7 +257,7 @@ public class SmartReminderService {
public void sendRenewSuccessNotification(String bookTitle, Date newDueDate) {
String title = "✅ 续借成功";
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
String content = "《%s》续借成功新到期日%s".formatted(bookTitle, sdf.format(newDueDate));
String content = String.format("《%s》续借成功新到期日%s", bookTitle, sdf.format(newDueDate));
sendNotification(
(int) System.currentTimeMillis(),
@ -307,7 +307,7 @@ public class SmartReminderService {
reminderService.checkLoanStatus(loan, dataManager);
}
Log.d(TAG, "借阅检查完成,共检查 %d 条记录".formatted(allLoans.size()));
Log.d(TAG, String.format("借阅检查完成,共检查 %d 条记录", allLoans.size()));
return Result.success();
} catch (Exception e) {
Log.e(TAG, "借阅检查失败", e);

@ -221,7 +221,7 @@ public class TranslationService {
String response = callDeepSeekAPI(prompt);
return parseDeepSeekResponse(response);
} catch (Exception e) {
Log.e(TAG, "DeepSeek翻译失败: %s".formatted(e.getMessage()), e);
Log.e(TAG, String.format("DeepSeek翻译失败: %s", e.getMessage()), e);
return simulateTranslation(text, sourceLang, targetLang, level);
}
}
@ -307,7 +307,7 @@ public class TranslationService {
private String readResponse(HttpURLConnection conn) throws Exception {
int responseCode = conn.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
Log.e(TAG, "DeepSeek API错误: %d".formatted(responseCode));
Log.e(TAG, String.format("DeepSeek API错误: %d", responseCode));
conn.disconnect();
throw new Exception("DeepSeek API错误: " + responseCode);
}
@ -349,7 +349,7 @@ public class TranslationService {
Log.w(TAG, "DeepSeek API响应格式异常: 缺少必要字段");
throw new IllegalStateException("DeepSeek API响应格式异常: 缺少必要字段");
} catch (JSONException e) {
Log.e(TAG, "DeepSeek API响应JSON解析失败: %s".formatted(e.getMessage()));
Log.e(TAG, String.format("DeepSeek API响应JSON解析失败: %s", e.getMessage()));
throw new RuntimeException("DeepSeek API响应JSON解析失败: " + e.getMessage(), e);
}
}

@ -90,17 +90,15 @@ class DatabaseHelperTest {
@Test
@DisplayName("测试sanitizeIdentifier - 无效标识符抛出异常")
void testSanitizeIdentifierInvalid() {
assertThrows(IllegalArgumentException.class, () -> {
helper.sanitizeIdentifier("invalid-table");
});
assertThrows(IllegalArgumentException.class,
() -> helper.sanitizeIdentifier("invalid-table"));
}
@Test
@DisplayName("测试sanitizeIdentifier - SQL注入尝试")
void testSanitizeIdentifierSqlInjection() {
assertThrows(IllegalArgumentException.class, () -> {
helper.sanitizeIdentifier("table; DROP TABLE users;");
});
assertThrows(IllegalArgumentException.class,
() -> helper.sanitizeIdentifier("table; DROP TABLE users;"));
}
@Test

Loading…
Cancel
Save