AndroidApp/RecordingService.java

431 lines
15 KiB
Java
Raw Permalink Normal View History

2025-07-11 18:31:33 +08:00
package com.example.myapplication.Service;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
2025-07-25 14:29:27 +08:00
import android.location.Location;
2025-07-17 10:53:35 +08:00
import android.media.MediaRecorder;
2025-07-11 18:31:33 +08:00
import android.os.Build;
2025-07-17 10:53:35 +08:00
import android.os.Handler;
2025-07-11 18:31:33 +08:00
import android.os.IBinder;
2025-07-17 10:53:35 +08:00
import android.os.Looper;
import android.util.Log;
2025-07-11 18:31:33 +08:00
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
2025-07-17 10:53:35 +08:00
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.room.Room;
import com.example.myapplication.DataBase.AppDatabase;
2025-07-11 18:31:33 +08:00
import com.example.myapplication.R;
2025-07-17 10:53:35 +08:00
import com.example.myapplication.Tool.BackgroundToast;
import com.example.myapplication.model.AudioEntity;
import com.example.myapplication.model.SharedDataManager;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
2025-07-11 18:31:33 +08:00
2025-07-17 10:53:35 +08:00
import io.reactivex.disposables.Disposable;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class RecordingService extends Service implements SharedDataManager.DataChangeListener {
2025-07-11 18:31:33 +08:00
private static final String CHANNEL_ID = "recording_channel";
2025-07-17 10:53:35 +08:00
private static final int NOTIFICATION_ID = 2;
private MediaRecorder mediaRecorder;
private String currentAudioPath;
private Handler handler;
private Runnable updateTimerRunnable;
private AppDatabase db;
private AppDatabase db2;
private final ExecutorService executor = Executors.newFixedThreadPool(4);
2025-07-25 14:29:27 +08:00
private String ImageId="-1";
2025-07-17 10:53:35 +08:00
private String currentToken="-1";
private SharedDataManager dataManager;
@Override
public IBinder onBind(Intent intent) {
return null;
}
2025-07-11 18:31:33 +08:00
2025-07-17 10:53:35 +08:00
private final OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) // 连接超时
.readTimeout(60, TimeUnit.SECONDS) // 读取超时
.writeTimeout(60, TimeUnit.SECONDS) // 写入超时
.build();
private int recordingSeconds = 0;
2025-07-11 18:31:33 +08:00
@RequiresApi(api = Build.VERSION_CODES.R)
@Override
public void onCreate() {
super.onCreate();
try {
createNotificationChannel();
// 启动前台服务必须5秒内调用startForeground
Notification notification = buildNotification("录音服务运行中");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ 需要指定类型
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else {
// Android 8.0-9.0
startForeground(NOTIFICATION_ID, notification);
}
} catch (Exception e) {
Toast.makeText(this,"错误:"+e,Toast.LENGTH_LONG).show();
stopSelf();
}
2025-07-17 10:53:35 +08:00
handler = new Handler(Looper.getMainLooper());
db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "image-database").build();
db2 = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "image-database2").build();
createNotificationChannel();
// 在 onCreate() 中改进 ViewModel 初始化
// 观察LiveData变化
// 在类成员变量中添加
dataManager=SharedDataManager.getInstance();
dataManager.addListener(this);
2025-07-25 14:29:27 +08:00
2025-07-17 10:53:35 +08:00
currentToken = dataManager.getToken();
2025-07-25 14:29:27 +08:00
ImageId=dataManager.getImageId();
2025-07-17 10:53:35 +08:00
// 在onCreate()中改为使用成员变量观察
2025-07-11 18:31:33 +08:00
}
2025-07-25 14:29:27 +08:00
2025-07-17 10:53:35 +08:00
@Override
2025-07-25 14:29:27 +08:00
public void onImageIdChanged(String newId) {
ImageId=newId;
2025-07-17 10:53:35 +08:00
}
2025-07-11 18:31:33 +08:00
@Override
2025-07-17 10:53:35 +08:00
public void onTokenChanged(String newToken) {
currentToken = newToken;
Log.d("Service", "Token updated: " + newToken);
}
@Override
public void onUserChanged(String newUser) {
2025-07-11 18:31:33 +08:00
}
@Override
2025-07-17 10:53:35 +08:00
public void onAudioPathChanged(String newAudioPath) {
}
@Override
public void onProjectIdChanged(String newProjectId) {
}
@Override
public void onUnitChanged(String newUnit) {
}
@Override
public void onBladeChanged(int newBlade) {
}
@Override
public void onPartidChanged(String newPartid) {
}
@Override
public void onChooseImageSourceChanged(String newChooseImageSource) {
}
@Override
public void onUnitNameChanged(String newUnitName) {
}
@Override
public void onIsRecordingChanged(boolean newisRecording) {
}
@Override
public void onUploadingStatusChanged(boolean isUploading) {
2025-07-25 14:29:27 +08:00
}
@Override
public void onLocationChanged(Location newLocation) {
}
@Override
public void onOtherMsgChanged(String msg) {
2025-07-17 10:53:35 +08:00
}
// 获取当前数据(可选,如果 Activity 需要读取)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && "start_recording".equals(intent.getAction())) {
startRecording();
} else if (intent != null && "stop_recording".equals(intent.getAction())) {
stopRecording();
}
return START_REDELIVER_INTENT;
}
private void startRecording() {
2025-07-25 14:29:27 +08:00
2025-07-17 10:53:35 +08:00
try {
2025-07-25 14:29:27 +08:00
dataManager.setIsRecording(true);
2025-07-17 10:53:35 +08:00
// 1. 设置录音文件路径
currentAudioPath = getExternalCacheDir().getAbsolutePath()
+ "/" + System.currentTimeMillis() + ".3gp";
dataManager.setAudioPath(currentAudioPath);
// 2. 初始化MediaRecorder
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mediaRecorder.setOutputFile(currentAudioPath);
mediaRecorder.prepare();
mediaRecorder.start();
// 3. 启动前台服务
startForegroundService();
// 4. 开始计时更新通知
startUpdatingTimer();
} catch (Exception e) {
Toast.makeText(this, "录音失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
stopSelf();
}
2025-07-11 18:31:33 +08:00
}
2025-07-17 10:53:35 +08:00
private void startForegroundService() {
Notification notification = buildNotification("录音中... 00:00");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else {
startForeground(NOTIFICATION_ID, notification);
}
}
private void startUpdatingTimer() {
updateTimerRunnable = new Runnable() {
@Override
public void run() {
recordingSeconds++;
String time = String.format("%02d:%02d",
recordingSeconds / 60, recordingSeconds % 60);
updateNotification("录音中... " + time);
handler.postDelayed(this, 1000);
}
};
handler.post(updateTimerRunnable);
}
private void uploadAudio(String imageId, String audioPath, String token) {
// 1. 参数校验(在调用线程执行,避免不必要的后台任务)
if (audioPath == null || audioPath.equals("0")) {
BackgroundToast.show(this, "文件路径为空");
return;
}
File audioFile = new File(audioPath);
if (!audioFile.exists()) {
BackgroundToast.show(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()) {
// 上传成功
2025-07-25 14:29:27 +08:00
executor.execute(()->{ db.AudioDao().deleteByPath(audioPath);
db2.AudioDao().insert(new AudioEntity(audioPath, imageId, currentTime,true));});
2025-07-17 10:53:35 +08:00
BackgroundToast.show(this, "录音上传成功: " + imageId);
stopSelf();
} else {
// 上传失败HTTP 错误)
2025-07-25 14:29:27 +08:00
executor.execute(()->{ db.AudioDao().insert(new AudioEntity(audioPath, imageId, currentTime,false));});
2025-07-17 10:53:35 +08:00
BackgroundToast.show(this,
"录音上传失败: " + response.code() + " " + response.message());
stopSelf();
}
} catch (IOException e) {
// 网络异常
String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
.format(new Date());
2025-07-25 14:29:27 +08:00
executor.execute(() -> {
db.AudioDao().insert(new AudioEntity(currentAudioPath, ImageId, currentToken, false));
});
2025-07-17 10:53:35 +08:00
BackgroundToast.show(this, "录音上传失败: " + e.getMessage());
Log.e("录音上传失败:",e.toString());
stopSelf();
}
});
}
private void stopRecording() {
try {
// 1. 停止录音
if (mediaRecorder != null) {
mediaRecorder.stop();
mediaRecorder.release();
mediaRecorder = null;
}
2025-07-25 14:29:27 +08:00
dataManager.setIsRecording(false);
2025-07-17 10:53:35 +08:00
// 2. 停止计时器
if (handler != null && updateTimerRunnable != null) {
handler.removeCallbacks(updateTimerRunnable);
}
// 3. 更新通知
updateNotification("录音已保存");
// 4. 上传录音文件(如果有需要)
2025-07-25 14:29:27 +08:00
if (currentAudioPath != null && ImageId != "-1") {
BackgroundToast.show(this,"开始上传录音了");
uploadAudio(ImageId, currentAudioPath, currentToken);
} else
{
BackgroundToast.show(this,"Imageid不存在上传录音失败");
executor.execute(() -> {
db.AudioDao().insert(new AudioEntity(currentAudioPath, ImageId, currentToken, false));
});
2025-07-17 10:53:35 +08:00
}
// 5. 不立即停止服务,等待上传完成
// 可以通过其他机制如广播或LiveData在上传完成后通知服务停止
} catch (Exception e) {
Toast.makeText(this, "停止录音失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
2025-07-25 14:29:27 +08:00
dataManager.setIsRecording(false);
2025-07-17 10:53:35 +08:00
stopSelf();
}
}
2025-07-11 18:31:33 +08:00
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);
if (manager != null) {
manager.createNotificationChannel(channel);
}
}
}
2025-07-17 10:53:35 +08:00
public Notification buildNotification(String text) {
2025-07-11 18:31:33 +08:00
// 确保图标有效R.drawable.ic_mic_on必须存在
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_mic_on)
.setContentTitle("录音服务")
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build();
}
public void updateNotification(String text) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ActivityCompat.checkSelfPermission(this,
android.Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
// 无权限时不显示通知(避免崩溃)
return;
}
}
NotificationManagerCompat.from(this)
.notify(NOTIFICATION_ID, buildNotification(text));
}
2025-07-17 10:53:35 +08:00
@Override
public void onDestroy() {
super.onDestroy();
// 2. 停止并释放MediaRecorder
if (mediaRecorder != null) {
try {
mediaRecorder.stop();
mediaRecorder.release();
} catch (IllegalStateException e) {
Log.e("Service", "MediaRecorder释放异常", e);
}
mediaRecorder = null;
}
// 3. 移除Handler回调
if (handler != null && updateTimerRunnable != null) {
handler.removeCallbacks(updateTimerRunnable);
}
dataManager.removeListener(this);
// 5. 关闭数据库如果Room支持
if (db != null) {
db.close();
}
if (db2 != null) {
db2.close();
}
}
2025-07-11 18:31:33 +08:00
}