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; import android.location.Location; import android.media.MediaRecorder; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.util.Log; import android.widget.Toast; import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.room.Room; import com.example.myapplication.DataBase.AppDatabase; import com.example.myapplication.R; 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; 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 { private static final String CHANNEL_ID = "recording_channel"; 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); private String ImageId="-1"; private String currentToken="-1"; private SharedDataManager dataManager; @Override public IBinder onBind(Intent intent) { return null; } private final OkHttpClient httpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) // 连接超时 .readTimeout(60, TimeUnit.SECONDS) // 读取超时 .writeTimeout(60, TimeUnit.SECONDS) // 写入超时 .build(); private int recordingSeconds = 0; @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(); } 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); currentToken = dataManager.getToken(); ImageId=dataManager.getImageId(); // 在onCreate()中改为使用成员变量观察 } @Override public void onImageIdChanged(String newId) { ImageId=newId; } @Override public void onTokenChanged(String newToken) { currentToken = newToken; Log.d("Service", "Token updated: " + newToken); } @Override public void onUserChanged(String newUser) { } @Override 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) { } @Override public void onLocationChanged(Location newLocation) { } @Override public void onOtherMsgChanged(String msg) { } // 获取当前数据(可选,如果 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() { try { dataManager.setIsRecording(true); // 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(); } } 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()) { // 上传成功 executor.execute(()->{ db.AudioDao().deleteByPath(audioPath); db2.AudioDao().insert(new AudioEntity(audioPath, imageId, currentTime,true));}); BackgroundToast.show(this, "录音上传成功: " + imageId); stopSelf(); } else { // 上传失败(HTTP 错误) executor.execute(()->{ db.AudioDao().insert(new AudioEntity(audioPath, imageId, currentTime,false));}); 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()); executor.execute(() -> { db.AudioDao().insert(new AudioEntity(currentAudioPath, ImageId, currentToken, false)); }); BackgroundToast.show(this, "录音上传失败: " + e.getMessage()); Log.e("录音上传失败:",e.toString()); stopSelf(); } }); } private void stopRecording() { try { // 1. 停止录音 if (mediaRecorder != null) { mediaRecorder.stop(); mediaRecorder.release(); mediaRecorder = null; } dataManager.setIsRecording(false); // 2. 停止计时器 if (handler != null && updateTimerRunnable != null) { handler.removeCallbacks(updateTimerRunnable); } // 3. 更新通知 updateNotification("录音已保存"); // 4. 上传录音文件(如果有需要) 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)); }); } // 5. 不立即停止服务,等待上传完成 // 可以通过其他机制(如广播或LiveData)在上传完成后通知服务停止 } catch (Exception e) { Toast.makeText(this, "停止录音失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); dataManager.setIsRecording(false); stopSelf(); } } 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); } } } public Notification buildNotification(String text) { // 确保图标有效(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)); } @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(); } } }