修复了便签背景缺陷 #20

Merged
pr9ixgmc2 merged 1 commits from cuijiaxiang_branch into master 4 weeks ago

@ -0,0 +1,409 @@
package net.micode.notes.tool;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Base64;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* AIService - AI
* <p>
* AIAPI
* </p>
*/
public class AIService {
private static final String TAG = "AIService";
private static final String DOUBAO_API_URL = "https://ark.cn-beijing.volces.com/api/v3/responses";
private static final String API_KEY = "ee5fb4c7-ea14-4481-ac23-4b0e82907850";
private static final String SECRET_ACCESS_KEY = "";
/**
*
* @param bitmap bitmap
* @param callback
*/
public static void extractImageContent(final Bitmap bitmap, final ExtractImageContentCallback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.d(TAG, "Starting image content extraction...");
// 检查bitmap
if (bitmap == null) {
Log.e(TAG, "Bitmap is null");
callback.onFailure("Bitmap is null");
return;
}
Log.d(TAG, "Bitmap width: " + bitmap.getWidth() + ", height: " + bitmap.getHeight());
// 将bitmap转换为Base64
Log.d(TAG, "Converting bitmap to base64...");
String base64Image = bitmapToBase64(bitmap);
if (base64Image == null) {
Log.e(TAG, "Failed to convert bitmap to base64");
callback.onFailure("Failed to convert bitmap to base64");
return;
}
Log.d(TAG, "Base64 conversion successful, length: " + base64Image.length());
// 构建请求体
Log.d(TAG, "Building request body...");
JSONObject requestBody = new JSONObject();
requestBody.put("model", "ep-20260127214554-frsrr"); // 新的推理接入点ID
// 创建input数组
org.json.JSONArray input = new org.json.JSONArray();
// 创建user input
JSONObject userInput = new JSONObject();
userInput.put("role", "user");
// 创建content数组
org.json.JSONArray contentArray = new org.json.JSONArray();
// 添加图片部分
JSONObject imageContent = new JSONObject();
imageContent.put("type", "input_image");
imageContent.put("image_url", "data:image/jpeg;base64," + base64Image);
contentArray.put(imageContent);
// 添加文本部分
JSONObject textContent = new JSONObject();
textContent.put("type", "input_text");
textContent.put("text", "请提取这张图片中的所有文字和结构化数据,包括表格、列表等信息,清晰准确地格式化提取的内容。");
contentArray.put(textContent);
userInput.put("content", contentArray);
input.put(userInput);
requestBody.put("input", input);
// 发送请求
String requestBodyString = requestBody.toString();
Log.d(TAG, "Request body length: " + requestBodyString.length());
Log.d(TAG, "Request body (first 1000 chars): " + (requestBodyString.length() > 1000 ? requestBodyString.substring(0, 1000) + "..." : requestBodyString));
Log.d(TAG, "Sending POST request to: " + DOUBAO_API_URL);
String response = sendPostRequest(DOUBAO_API_URL, requestBodyString);
if (response == null) {
Log.e(TAG, "Failed to get response from Doubao API");
callback.onFailure("Failed to get response from Doubao API");
return;
}
Log.d(TAG, "Got response from Doubao API, length: " + response.length());
Log.d(TAG, "Response content: " + response);
// 解析响应
Log.d(TAG, "Parsing response...");
JSONObject responseJson = new JSONObject(response);
// 检查响应格式
if (responseJson.has("output")) {
Log.d(TAG, "Response has output field");
try {
// 尝试作为数组处理(新格式)
org.json.JSONArray outputArray = responseJson.getJSONArray("output");
Log.d(TAG, "Output is an array, length: " + outputArray.length());
// 遍历数组找到包含文本的message
String extractedText = "";
for (int i = 0; i < outputArray.length(); i++) {
JSONObject item = outputArray.getJSONObject(i);
Log.d(TAG, "Output item " + i + ": " + item.toString());
// 检查是否是message类型
if (item.has("type") && "message".equals(item.getString("type"))) {
Log.d(TAG, "Found message item");
if (item.has("content")) {
org.json.JSONArray messageContentArray = item.getJSONArray("content");
for (int j = 0; j < messageContentArray.length(); j++) {
JSONObject contentItem = messageContentArray.getJSONObject(j);
if (contentItem.has("type") && "output_text".equals(contentItem.getString("type"))) {
extractedText = contentItem.getString("text");
Log.d(TAG, "Got text from response: " + extractedText);
callback.onSuccess(extractedText);
return;
}
}
}
}
// 检查是否有role字段为assistant
else if (item.has("role") && "assistant".equals(item.getString("role"))) {
Log.d(TAG, "Found assistant item");
if (item.has("content")) {
org.json.JSONArray assistantContentArray = item.getJSONArray("content");
for (int j = 0; j < assistantContentArray.length(); j++) {
JSONObject contentItem = assistantContentArray.getJSONObject(j);
if (contentItem.has("type") && "output_text".equals(contentItem.getString("type"))) {
extractedText = contentItem.getString("text");
Log.d(TAG, "Got text from assistant response: " + extractedText);
callback.onSuccess(extractedText);
return;
}
}
}
}
}
// 如果没有找到文本,尝试其他方式
if (extractedText.isEmpty()) {
Log.e(TAG, "No text found in output array");
callback.onFailure("No text found in output array");
}
} catch (JSONException e) {
// 如果不是数组,尝试作为对象处理(旧格式)
Log.d(TAG, "Output is not an array, trying as object: " + e.getMessage());
try {
JSONObject outputObj = responseJson.getJSONObject("output");
if (outputObj.has("text")) {
String content = outputObj.getString("text");
Log.d(TAG, "Got text from response object: " + content);
callback.onSuccess(content);
} else if (outputObj.has("content")) {
String content = outputObj.getString("content");
Log.d(TAG, "Got content from response object: " + content);
callback.onSuccess(content);
} else {
Log.e(TAG, "No text or content in response object: " + outputObj.toString());
callback.onFailure("No text or content in response");
}
} catch (JSONException ex) {
Log.e(TAG, "Error parsing output: " + ex.getMessage());
callback.onFailure("Error parsing output: " + ex.getMessage());
}
}
} else if (responseJson.has("choices")) {
// 兼容旧格式
Log.d(TAG, "Response has choices field");
org.json.JSONArray choices = responseJson.getJSONArray("choices");
if (choices.length() > 0) {
JSONObject choice = choices.getJSONObject(0);
if (choice.has("message")) {
JSONObject message = choice.getJSONObject("message");
if (message.has("content")) {
String content = message.getString("content");
Log.d(TAG, "Got content from choices: " + content);
callback.onSuccess(content);
} else {
Log.e(TAG, "No content in message: " + message.toString());
callback.onFailure("No content in message");
}
} else {
Log.e(TAG, "No message in choice: " + choice.toString());
callback.onFailure("No message in choice");
}
} else {
Log.e(TAG, "No choices in response");
callback.onFailure("No choices in response");
}
} else if (responseJson.has("error")) {
// 处理错误响应
Log.e(TAG, "API returned error: " + responseJson.toString());
JSONObject error = responseJson.getJSONObject("error");
String errorMessage = error.getString("message");
callback.onFailure("API error: " + errorMessage);
} else {
Log.e(TAG, "Unexpected response format: " + responseJson.toString());
callback.onFailure("Unexpected response format: " + responseJson.toString());
}
} catch (JSONException e) {
Log.e(TAG, "JSONException: " + e.getMessage());
e.printStackTrace();
callback.onFailure("JSON error: " + e.getMessage());
} catch (Exception e) {
Log.e(TAG, "Exception: " + e.getMessage());
e.printStackTrace();
callback.onFailure("Error: " + e.getMessage());
}
}
}).start();
}
/**
* BitmapBase64
* @param bitmap bitmap
* @return Base64
*/
private static String bitmapToBase64(Bitmap bitmap) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();
try {
byteArrayOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeToString(byteArray, Base64.NO_WRAP);
}
/**
* POST
* @param urlString URL
* @param requestBody
* @return
*/
private static String sendPostRequest(String urlString, String requestBody) {
try {
Log.d(TAG, "Sending POST request to: " + urlString);
Log.d(TAG, "Request body length: " + requestBody.length());
Log.d(TAG, "Request body (first 500 chars): " + (requestBody.length() > 500 ? requestBody.substring(0, 500) + "..." : requestBody));
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
// 设置请求头
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Authorization", "Bearer " + API_KEY);
connection.setRequestProperty("X-TT-LOGID", System.currentTimeMillis() + "");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
connection.setDoOutput(true);
connection.setConnectTimeout(30000); // 设置连接超时为30秒
connection.setReadTimeout(30000); // 设置读取超时为30秒
// 写入请求体
Log.d(TAG, "Writing request body...");
OutputStream outputStream = connection.getOutputStream();
outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
Log.d(TAG, "Request body written successfully");
// 读取响应
Log.d(TAG, "Reading response...");
int responseCode = connection.getResponseCode();
Log.d(TAG, "HTTP response code: " + responseCode);
// 读取所有响应头
Log.d(TAG, "Response headers:");
java.util.Map<String, java.util.List<String>> headers = connection.getHeaderFields();
for (String key : headers.keySet()) {
if (key != null) {
Log.d(TAG, key + ": " + headers.get(key));
}
}
if (responseCode == HttpURLConnection.HTTP_OK) {
Log.d(TAG, "HTTP OK, reading response body...");
InputStream inputStream = connection.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead;
ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
while ((bytesRead = inputStream.read(buffer)) != -1) {
responseStream.write(buffer, 0, bytesRead);
}
String responseString = responseStream.toString(StandardCharsets.UTF_8.name());
responseStream.close();
inputStream.close();
connection.disconnect();
Log.d(TAG, "API response length: " + responseString.length());
Log.d(TAG, "API response (first 500 chars): " + (responseString.length() > 500 ? responseString.substring(0, 500) + "..." : responseString));
return responseString;
} else {
// 读取错误响应
Log.e(TAG, "HTTP error, reading error response...");
InputStream errorStream = connection.getErrorStream();
if (errorStream != null) {
byte[] buffer = new byte[1024];
int bytesRead;
ByteArrayOutputStream errorResponseStream = new ByteArrayOutputStream();
while ((bytesRead = errorStream.read(buffer)) != -1) {
errorResponseStream.write(buffer, 0, bytesRead);
}
String errorResponse = errorResponseStream.toString(StandardCharsets.UTF_8.name());
errorResponseStream.close();
errorStream.close();
Log.e(TAG, "HTTP error: " + responseCode + ", Error response: " + errorResponse);
} else {
Log.e(TAG, "HTTP error code: " + responseCode + ", No error stream available");
}
connection.disconnect();
return null;
}
} catch (java.net.SocketTimeoutException e) {
Log.e(TAG, "Socket timeout error: " + e.getMessage());
e.printStackTrace();
return null;
} catch (java.net.ConnectException e) {
Log.e(TAG, "Connection error: " + e.getMessage());
e.printStackTrace();
return null;
} catch (java.io.IOException e) {
Log.e(TAG, "IO error: " + e.getMessage());
e.printStackTrace();
return null;
} catch (Exception e) {
Log.e(TAG, "Error sending POST request: " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
*
*/
public interface ExtractImageContentCallback {
void onSuccess(String extractedContent);
void onFailure(String errorMessage);
}
/**
* API
*/
public static void testApiConnection(final ExtractImageContentCallback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 构建测试请求体
JSONObject requestBody = new JSONObject();
requestBody.put("model", "ep-20260127214554-frsrr");
org.json.JSONArray input = new org.json.JSONArray();
JSONObject userInput = new JSONObject();
userInput.put("role", "user");
org.json.JSONArray contentArray = new org.json.JSONArray();
JSONObject textContent = new JSONObject();
textContent.put("type", "input_text");
textContent.put("text", "Hello, test connection");
contentArray.put(textContent);
userInput.put("content", contentArray);
input.put(userInput);
requestBody.put("input", input);
Log.d(TAG, "Testing API connection...");
String response = sendPostRequest(DOUBAO_API_URL, requestBody.toString());
if (response != null) {
Log.d(TAG, "API connection test successful: " + response);
callback.onSuccess("API connection test successful");
} else {
Log.e(TAG, "API connection test failed");
callback.onFailure("API connection test failed");
}
} catch (Exception e) {
Log.e(TAG, "Error testing API connection: " + e.getMessage());
callback.onFailure("Error testing API connection: " + e.getMessage());
}
}
}).start();
}
}

@ -119,6 +119,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
public ImageButton ibRedo; // 重做按钮
public ImageView ibSetBgColor; // 设置背景色按钮
public ImageButton ibInsertImage; // 插入图片按钮
public ImageButton ibExtractImage; // 提取图片内容按钮
public TextView tvTitleHint; // 标题提示文字
public EditText etTitle; // 标题输入框
public TextView tvTitleCount; // 字符数提示
@ -594,6 +595,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteHeaderHolder.ibInsertImage = (ImageButton) findViewById(R.id.add_img_btn);
mNoteHeaderHolder.ibInsertImage.setOnClickListener(this);
mNoteHeaderHolder.ibExtractImage = (ImageButton) findViewById(R.id.extract_img_btn);
mNoteHeaderHolder.ibExtractImage.setOnClickListener(this);
mNoteHeaderHolder.tvTitleHint = (TextView) findViewById(R.id.tv_title_hint);
mNoteHeaderHolder.etTitle = (EditText) findViewById(R.id.et_title);
mNoteHeaderHolder.etTitle.addTextChangedListener(new TextWatcher() {
@ -795,6 +798,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mFontSizeSelector.setVisibility(View.GONE);
} else if (id == R.id.add_img_btn) {
insertImage();
} else if (id == R.id.extract_img_btn) {
extractImageContent();
} else if (id == R.id.btn_bold) {
// 处理加粗按钮点击
mNoteEditor.toggleBold();
@ -1981,4 +1986,143 @@ public class NoteEditActivity extends Activity implements OnClickListener,
showToast(R.string.error_out_of_memory);
}
}
/**
*
*/
private void extractImageContent() {
// 检查当前笔记中是否有图片
String content = mNoteEditor.getText().toString();
if (!content.contains("[IMAGE:")) {
showToast(R.string.error_no_image_in_note);
return;
}
// 提取所有图片路径
final java.util.List<String> imagePaths = new java.util.ArrayList<>();
int startIndex = 0;
while (true) {
int imageStart = content.indexOf("[IMAGE:", startIndex);
if (imageStart == -1) {
break;
}
int imageEnd = content.indexOf("]", imageStart);
if (imageEnd == -1) {
break;
}
String imagePath = content.substring(imageStart + 7, imageEnd);
imagePaths.add(imagePath);
startIndex = imageEnd + 1;
}
if (imagePaths.isEmpty()) {
showToast(R.string.error_no_image_in_note);
return;
}
// 如果只有一张图片,直接提取
if (imagePaths.size() == 1) {
extractImageContentFromPath(imagePaths.get(0));
return;
}
// 如果有多张图片,让用户选择
final String[] imageOptions = new String[imagePaths.size()];
for (int i = 0; i < imagePaths.size(); i++) {
imageOptions[i] = "Image " + (i + 1);
}
new android.app.AlertDialog.Builder(this)
.setTitle("Select Image")
.setItems(imageOptions, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String selectedImagePath = imagePaths.get(which);
extractImageContentFromPath(selectedImagePath);
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
}
/**
*
*/
private void extractImageContentFromPath(final String imagePath) {
// 显示加载提示
final android.app.AlertDialog loadingDialog = new android.app.AlertDialog.Builder(this)
.setTitle("Loading")
.setMessage("Extracting image content...")
.setCancelable(false)
.create();
loadingDialog.show();
// 加载图片
ImageHelper imageHelper = new ImageHelper(this);
final Bitmap bitmap = imageHelper.loadImage(imagePath);
if (bitmap == null) {
loadingDialog.dismiss();
showToast(R.string.error_failed_to_load_image);
return;
}
// 调用AI服务提取图片内容
net.micode.notes.tool.AIService.extractImageContent(bitmap, new net.micode.notes.tool.AIService.ExtractImageContentCallback() {
@Override
public void onSuccess(final String extractedContent) {
runOnUiThread(new Runnable() {
@Override
public void run() {
loadingDialog.dismiss();
// 显示提取结果对话框
new android.app.AlertDialog.Builder(NoteEditActivity.this)
.setTitle(R.string.dialog_title_extracted_content)
.setMessage(extractedContent)
.setPositiveButton(R.string.dialog_button_insert, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 将提取的内容插入到笔记中
int cursorPosition = mNoteEditor.getSelectionStart();
Editable editable = mNoteEditor.getEditableText();
editable.insert(cursorPosition, "\n" + extractedContent + "\n");
mNoteEditor.setSelection(cursorPosition + extractedContent.length() + 2);
// 更新WorkingNote的内容
getWorkingText();
updateWordCount();
showToast(R.string.info_content_inserted);
}
})
.setNegativeButton(R.string.dialog_button_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
}
});
}
@Override
public void onFailure(final String errorMessage) {
runOnUiThread(new Runnable() {
@Override
public void run() {
loadingDialog.dismiss();
showToast(R.string.error_extract_image_content_failed);
Log.e(TAG, "Extract image content failed: " + errorMessage);
}
});
}
});
}
}

@ -28,6 +28,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
@ -83,6 +84,8 @@ import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -218,13 +221,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
// 处理从相册选择图片的结果
if (data != null && data.getData() != null) {
try {
// 保存图片路径
mCurrentBackgroundType = BACKGROUND_TYPE_ALBUM;
mCurrentBackgroundPath = data.getData().toString();
// 更新背景
updateBackground();
// 保存设置
saveBackgroundSetting();
// 将相册图片复制到应用内部存储
String internalPath = saveImageToInternalStorage(data.getData());
if (!TextUtils.isEmpty(internalPath)) {
// 保存内部存储路径
mCurrentBackgroundType = BACKGROUND_TYPE_ALBUM;
mCurrentBackgroundPath = internalPath;
// 更新背景
updateBackground();
// 保存设置
saveBackgroundSetting();
} else {
Toast.makeText(this, "图片保存失败", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "图片加载失败", Toast.LENGTH_SHORT).show();
@ -1640,8 +1649,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
} else if (BACKGROUND_TYPE_ALBUM.equals(mCurrentBackgroundType) && !TextUtils.isEmpty(mCurrentBackgroundPath)) {
// 使用相册图片作为背景
try {
android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(
getContentResolver().openInputStream(android.net.Uri.parse(mCurrentBackgroundPath)));
android.graphics.Bitmap bitmap = null;
// 检查路径类型
if (mCurrentBackgroundPath.startsWith("content://")) {
// 如果是URI字符串
bitmap = android.graphics.BitmapFactory.decodeStream(
getContentResolver().openInputStream(android.net.Uri.parse(mCurrentBackgroundPath)));
} else {
// 如果是内部存储路径
bitmap = android.graphics.BitmapFactory.decodeFile(mCurrentBackgroundPath);
}
if (bitmap != null) {
// 计算屏幕尺寸
android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
@ -1750,4 +1768,42 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
});
}
/**
*
* @param uri URI
* @return
*/
private String saveImageToInternalStorage(Uri uri) {
try {
// 创建内部存储目录
File directory = new File(getFilesDir(), "backgrounds");
if (!directory.exists()) {
directory.mkdirs();
}
// 创建输出文件
String fileName = "background_" + System.currentTimeMillis() + ".jpg";
File outputFile = new File(directory, fileName);
// 复制图片
InputStream inputStream = getContentResolver().openInputStream(uri);
FileOutputStream outputStream = new FileOutputStream(outputFile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
inputStream.close();
outputStream.close();
// 返回文件路径
return outputFile.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

@ -94,6 +94,16 @@
android:background="@drawable/bg_btn_insert_image"
android:contentDescription="@string/menu_insert_image"
android:padding="12dp" />
<ImageButton
android:id="@+id/extract_img_btn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:layout_marginLeft="8dp"
android:background="@drawable/bg_btn_insert_image"
android:contentDescription="Extract image content"
android:padding="12dp"
android:src="@drawable/ic_insert_image" />
</LinearLayout>
<LinearLayout

@ -233,4 +233,15 @@
<string name="word_count_warning">Word count large</string>
<string name="word_count_large">Word count too large</string>
<!-- Image extraction strings -->
<string name="error_no_image_in_note">No image found in the note</string>
<string name="loading_title">Loading</string>
<string name="loading_extracting_image_content">Extracting image content...</string>
<string name="error_failed_to_load_image">Failed to load image</string>
<string name="dialog_title_extracted_content">Extracted Content</string>
<string name="dialog_button_insert">Insert</string>
<string name="dialog_button_cancel">Cancel</string>
<string name="info_content_inserted">Content inserted successfully</string>
<string name="error_extract_image_content_failed">Failed to extract image content</string>
</resources>

Loading…
Cancel
Save