431 lines
15 KiB
Java
431 lines
15 KiB
Java
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();
|
||
}
|
||
}
|
||
} |