管理端界面优化
This commit is contained in:
commit
0049afbf5c
|
|
@ -0,0 +1,314 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.R;
|
||||
|
||||
/**
|
||||
* 通话界面
|
||||
*/
|
||||
public class CallActivity extends AppCompatActivity implements CallManager.CallStateListener {
|
||||
|
||||
private ImageView ivBackgroundAvatar;
|
||||
private ImageView ivAvatar;
|
||||
private TextView tvUserName;
|
||||
private TextView tvCallStatus;
|
||||
private TextView tvCallDuration;
|
||||
private TextView tvCallType;
|
||||
private ImageButton btnMinimize;
|
||||
private ImageButton btnMute;
|
||||
private ImageButton btnSpeaker;
|
||||
private ImageButton btnVideo;
|
||||
private ImageButton btnHangup;
|
||||
private ImageButton btnSwitchCamera;
|
||||
private LinearLayout layoutCallControls;
|
||||
private LinearLayout layoutVideoToggle;
|
||||
|
||||
private CallManager callManager;
|
||||
private AudioManager audioManager;
|
||||
private Handler handler;
|
||||
|
||||
private String callId;
|
||||
private String callType;
|
||||
private boolean isCaller;
|
||||
private int otherUserId;
|
||||
private String otherUserName;
|
||||
private String otherUserAvatar;
|
||||
|
||||
private boolean isMuted = false;
|
||||
private boolean isSpeakerOn = false;
|
||||
private boolean isVideoEnabled = true;
|
||||
private boolean isConnected = false;
|
||||
private long callStartTime = 0;
|
||||
|
||||
private Runnable durationRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isConnected && callStartTime > 0) {
|
||||
long duration = (System.currentTimeMillis() - callStartTime) / 1000;
|
||||
int minutes = (int) (duration / 60);
|
||||
int seconds = (int) (duration % 60);
|
||||
tvCallDuration.setText(String.format("%02d:%02d", minutes, seconds));
|
||||
handler.postDelayed(this, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// 保持屏幕常亮,显示在锁屏上方
|
||||
getWindow().addFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||
);
|
||||
|
||||
setContentView(R.layout.activity_call);
|
||||
|
||||
initViews();
|
||||
initData();
|
||||
initCallManager();
|
||||
setupListeners();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
ivBackgroundAvatar = findViewById(R.id.ivBackgroundAvatar);
|
||||
ivAvatar = findViewById(R.id.ivAvatar);
|
||||
tvUserName = findViewById(R.id.tvUserName);
|
||||
tvCallStatus = findViewById(R.id.tvCallStatus);
|
||||
tvCallDuration = findViewById(R.id.tvCallDuration);
|
||||
tvCallType = findViewById(R.id.tvCallType);
|
||||
btnMinimize = findViewById(R.id.btnMinimize);
|
||||
btnMute = findViewById(R.id.btnMute);
|
||||
btnSpeaker = findViewById(R.id.btnSpeaker);
|
||||
btnVideo = findViewById(R.id.btnVideo);
|
||||
btnHangup = findViewById(R.id.btnHangup);
|
||||
btnSwitchCamera = findViewById(R.id.btnSwitchCamera);
|
||||
layoutCallControls = findViewById(R.id.layoutCallControls);
|
||||
layoutVideoToggle = findViewById(R.id.layoutVideoToggle);
|
||||
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
callId = getIntent().getStringExtra("callId");
|
||||
callType = getIntent().getStringExtra("callType");
|
||||
isCaller = getIntent().getBooleanExtra("isCaller", true);
|
||||
otherUserId = getIntent().getIntExtra("otherUserId", 0);
|
||||
otherUserName = getIntent().getStringExtra("otherUserName");
|
||||
otherUserAvatar = getIntent().getStringExtra("otherUserAvatar");
|
||||
|
||||
// 如果是被叫方接听,直接进入通话状态
|
||||
boolean alreadyConnected = getIntent().getBooleanExtra("isConnected", false);
|
||||
if (alreadyConnected) {
|
||||
android.util.Log.d("CallActivity", "被叫方接听,直接进入通话状态");
|
||||
isConnected = true;
|
||||
callStartTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private void initCallManager() {
|
||||
callManager = CallManager.getInstance(this);
|
||||
callManager.setStateListener(this);
|
||||
|
||||
// 确保WebSocket已连接(主叫方需要接收接听/拒绝通知)
|
||||
String userId = com.example.livestreaming.net.AuthStore.getUserId(this);
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
try {
|
||||
int uid = (int) Double.parseDouble(userId);
|
||||
if (uid > 0) {
|
||||
android.util.Log.d("CallActivity", "确保WebSocket连接,userId: " + uid);
|
||||
callManager.connect(uid);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
android.util.Log.e("CallActivity", "解析用户ID失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
btnMinimize.setOnClickListener(v -> {
|
||||
// 最小化通话(后台运行)
|
||||
moveTaskToBack(true);
|
||||
});
|
||||
|
||||
btnMute.setOnClickListener(v -> toggleMute());
|
||||
btnSpeaker.setOnClickListener(v -> toggleSpeaker());
|
||||
btnVideo.setOnClickListener(v -> toggleVideo());
|
||||
btnSwitchCamera.setOnClickListener(v -> switchCamera());
|
||||
|
||||
btnHangup.setOnClickListener(v -> {
|
||||
if (isConnected) {
|
||||
callManager.endCall(callId, "user_hangup");
|
||||
} else if (isCaller) {
|
||||
callManager.cancelCall(callId);
|
||||
} else {
|
||||
callManager.rejectCall(callId);
|
||||
}
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
// 设置用户信息
|
||||
tvUserName.setText(otherUserName != null ? otherUserName : "用户" + otherUserId);
|
||||
|
||||
if (otherUserAvatar != null && !otherUserAvatar.isEmpty()) {
|
||||
Glide.with(this).load(otherUserAvatar).into(ivAvatar);
|
||||
Glide.with(this).load(otherUserAvatar).into(ivBackgroundAvatar);
|
||||
}
|
||||
|
||||
// 设置通话类型
|
||||
boolean isVideo = "video".equals(callType);
|
||||
tvCallType.setText(isVideo ? "视频通话" : "语音通话");
|
||||
layoutVideoToggle.setVisibility(isVideo ? View.VISIBLE : View.GONE);
|
||||
btnSwitchCamera.setVisibility(isVideo ? View.VISIBLE : View.GONE);
|
||||
|
||||
// 根据连接状态设置界面
|
||||
if (isConnected) {
|
||||
// 已接通,显示通话中界面
|
||||
tvCallStatus.setVisibility(View.GONE);
|
||||
tvCallDuration.setVisibility(View.VISIBLE);
|
||||
layoutCallControls.setVisibility(View.VISIBLE);
|
||||
handler.post(durationRunnable);
|
||||
android.util.Log.d("CallActivity", "updateUI: 已接通状态,显示计时器");
|
||||
} else {
|
||||
// 未接通,显示等待状态
|
||||
if (isCaller) {
|
||||
tvCallStatus.setText("正在呼叫...");
|
||||
} else {
|
||||
tvCallStatus.setText("正在连接...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleMute() {
|
||||
isMuted = !isMuted;
|
||||
btnMute.setImageResource(isMuted ? R.drawable.ic_mic_off : R.drawable.ic_mic);
|
||||
btnMute.setBackgroundResource(isMuted ? R.drawable.bg_call_button_active : R.drawable.bg_call_button);
|
||||
// TODO: 实际静音控制
|
||||
Toast.makeText(this, isMuted ? "已静音" : "已取消静音", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void toggleSpeaker() {
|
||||
isSpeakerOn = !isSpeakerOn;
|
||||
audioManager.setSpeakerphoneOn(isSpeakerOn);
|
||||
btnSpeaker.setBackgroundResource(isSpeakerOn ? R.drawable.bg_call_button_active : R.drawable.bg_call_button);
|
||||
Toast.makeText(this, isSpeakerOn ? "已开启免提" : "已关闭免提", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void toggleVideo() {
|
||||
isVideoEnabled = !isVideoEnabled;
|
||||
btnVideo.setBackgroundResource(isVideoEnabled ? R.drawable.bg_call_button : R.drawable.bg_call_button_active);
|
||||
// TODO: 实际视频控制
|
||||
Toast.makeText(this, isVideoEnabled ? "已开启摄像头" : "已关闭摄像头", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void switchCamera() {
|
||||
// TODO: 切换前后摄像头
|
||||
Toast.makeText(this, "切换摄像头", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void onCallConnected() {
|
||||
android.util.Log.d("CallActivity", "onCallConnected() 开始执行");
|
||||
isConnected = true;
|
||||
callStartTime = System.currentTimeMillis();
|
||||
|
||||
tvCallStatus.setVisibility(View.GONE);
|
||||
tvCallDuration.setVisibility(View.VISIBLE);
|
||||
layoutCallControls.setVisibility(View.VISIBLE);
|
||||
|
||||
handler.post(durationRunnable);
|
||||
android.util.Log.d("CallActivity", "onCallConnected() 执行完成,通话已连接");
|
||||
Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// CallStateListener 实现
|
||||
@Override
|
||||
public void onCallStateChanged(String state, String callId) {
|
||||
// 状态变化处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncomingCall(String callId, int callerId, String callerName, String callerAvatar, String callType) {
|
||||
// 来电处理(在IncomingCallActivity中处理)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallConnected(String callId) {
|
||||
android.util.Log.d("CallActivity", "========== onCallConnected 被调用 ==========");
|
||||
android.util.Log.d("CallActivity", "callId: " + callId);
|
||||
android.util.Log.d("CallActivity", "this.callId: " + this.callId);
|
||||
runOnUiThread(() -> {
|
||||
android.util.Log.d("CallActivity", "执行 onCallConnected UI更新");
|
||||
onCallConnected();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallEnded(String callId, String reason) {
|
||||
runOnUiThread(() -> {
|
||||
String message;
|
||||
switch (reason) {
|
||||
case "rejected":
|
||||
message = "对方已拒绝";
|
||||
break;
|
||||
case "cancelled":
|
||||
message = "通话已取消";
|
||||
break;
|
||||
case "timeout":
|
||||
message = "对方无应答";
|
||||
break;
|
||||
case "busy":
|
||||
message = "对方忙线中";
|
||||
break;
|
||||
case "peer_disconnect":
|
||||
message = "对方已断开";
|
||||
break;
|
||||
default:
|
||||
message = "通话已结束";
|
||||
}
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(this, "通话错误: " + error, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
handler.removeCallbacks(durationRunnable);
|
||||
if (callManager != null) {
|
||||
callManager.setStateListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// 禁止返回键退出,需要点击挂断
|
||||
moveTaskToBack(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
/**
|
||||
* 通话API响应包装类
|
||||
*/
|
||||
public class CallApiResponse<T> {
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public boolean isSuccess() {
|
||||
return code == 200;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.*;
|
||||
|
||||
/**
|
||||
* 通话相关API接口
|
||||
*/
|
||||
public interface CallApiService {
|
||||
|
||||
/**
|
||||
* 发起通话
|
||||
*/
|
||||
@POST("api/front/call/initiate")
|
||||
Call<CallApiResponse<InitiateCallResponse>> initiateCall(@Body InitiateCallRequest request);
|
||||
|
||||
/**
|
||||
* 接听通话
|
||||
*/
|
||||
@POST("api/front/call/accept/{callId}")
|
||||
Call<CallApiResponse<Boolean>> acceptCall(@Path("callId") String callId);
|
||||
|
||||
/**
|
||||
* 拒绝通话
|
||||
*/
|
||||
@POST("api/front/call/reject/{callId}")
|
||||
Call<CallApiResponse<Boolean>> rejectCall(@Path("callId") String callId);
|
||||
|
||||
/**
|
||||
* 取消通话
|
||||
*/
|
||||
@POST("api/front/call/cancel/{callId}")
|
||||
Call<CallApiResponse<Boolean>> cancelCall(@Path("callId") String callId);
|
||||
|
||||
/**
|
||||
* 结束通话
|
||||
*/
|
||||
@POST("api/front/call/end/{callId}")
|
||||
Call<CallApiResponse<Boolean>> endCall(@Path("callId") String callId, @Query("endReason") String endReason);
|
||||
|
||||
/**
|
||||
* 获取通话记录
|
||||
*/
|
||||
@GET("api/front/call/history")
|
||||
Call<CallApiResponse<CallHistoryResponse>> getCallHistory(
|
||||
@Query("page") int page,
|
||||
@Query("limit") int limit
|
||||
);
|
||||
|
||||
/**
|
||||
* 删除通话记录
|
||||
*/
|
||||
@DELETE("api/front/call/record/{recordId}")
|
||||
Call<CallApiResponse<Boolean>> deleteCallRecord(@Path("recordId") long recordId);
|
||||
|
||||
/**
|
||||
* 获取未接来电数量
|
||||
*/
|
||||
@GET("api/front/call/missed/count")
|
||||
Call<CallApiResponse<Integer>> getMissedCallCount();
|
||||
|
||||
/**
|
||||
* 获取通话状态
|
||||
*/
|
||||
@GET("api/front/call/status")
|
||||
Call<CallApiResponse<Map<String, Object>>> getCallStatus();
|
||||
|
||||
/**
|
||||
* 获取通话详情
|
||||
*/
|
||||
@GET("api/front/call/detail/{callId}")
|
||||
Call<CallApiResponse<CallRecordResponse>> getCallDetail(@Path("callId") String callId);
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.example.livestreaming.R;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
/**
|
||||
* 通话记录列表界面
|
||||
*/
|
||||
public class CallHistoryActivity extends AppCompatActivity {
|
||||
|
||||
private ImageButton btnBack;
|
||||
private TextView tvMissedCount;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private RecyclerView rvCallHistory;
|
||||
|
||||
private CallHistoryAdapter adapter;
|
||||
private CallApiService apiService;
|
||||
private CallManager callManager;
|
||||
|
||||
private int currentPage = 1;
|
||||
private boolean isLoading = false;
|
||||
private boolean hasMore = true;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_call_history);
|
||||
|
||||
initViews();
|
||||
initApiService();
|
||||
initCallManager();
|
||||
setupListeners();
|
||||
loadData();
|
||||
loadMissedCount();
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
btnBack = findViewById(R.id.btnBack);
|
||||
tvMissedCount = findViewById(R.id.tvMissedCount);
|
||||
swipeRefresh = findViewById(R.id.swipeRefresh);
|
||||
rvCallHistory = findViewById(R.id.rvCallHistory);
|
||||
|
||||
adapter = new CallHistoryAdapter();
|
||||
rvCallHistory.setLayoutManager(new LinearLayoutManager(this));
|
||||
rvCallHistory.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void initApiService() {
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(this);
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(chain -> {
|
||||
String token = AuthStore.getToken(CallHistoryActivity.this);
|
||||
okhttp3.Request.Builder req = chain.request().newBuilder();
|
||||
if (token != null && !token.isEmpty()) {
|
||||
req.header("Authori-zation", token);
|
||||
req.header("Authorization", token);
|
||||
}
|
||||
return chain.proceed(req.build());
|
||||
})
|
||||
.build();
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build();
|
||||
|
||||
apiService = retrofit.create(CallApiService.class);
|
||||
}
|
||||
|
||||
private void initCallManager() {
|
||||
callManager = CallManager.getInstance(this);
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
btnBack.setOnClickListener(v -> finish());
|
||||
|
||||
swipeRefresh.setOnRefreshListener(() -> {
|
||||
currentPage = 1;
|
||||
hasMore = true;
|
||||
loadData();
|
||||
});
|
||||
|
||||
adapter.setOnItemClickListener(new CallHistoryAdapter.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(CallRecordResponse record) {
|
||||
// 点击查看详情或回拨
|
||||
showCallOptions(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallClick(CallRecordResponse record) {
|
||||
// 回拨
|
||||
makeCall(record);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载更多
|
||||
rvCallHistory.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
|
||||
if (layoutManager != null) {
|
||||
int totalItemCount = layoutManager.getItemCount();
|
||||
int lastVisibleItem = layoutManager.findLastVisibleItemPosition();
|
||||
if (!isLoading && hasMore && lastVisibleItem >= totalItemCount - 3) {
|
||||
loadMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
isLoading = true;
|
||||
apiService.getCallHistory(currentPage, 20).enqueue(new Callback<CallApiResponse<CallHistoryResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<CallApiResponse<CallHistoryResponse>> call,
|
||||
Response<CallApiResponse<CallHistoryResponse>> response) {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
isLoading = false;
|
||||
|
||||
if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) {
|
||||
CallHistoryResponse data = response.body().getData();
|
||||
if (data != null && data.getList() != null) {
|
||||
if (currentPage == 1) {
|
||||
adapter.setRecords(data.getList());
|
||||
} else {
|
||||
adapter.addRecords(data.getList());
|
||||
}
|
||||
hasMore = data.getList().size() >= 20;
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(CallHistoryActivity.this, "加载失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<CallApiResponse<CallHistoryResponse>> call, Throwable t) {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
isLoading = false;
|
||||
Toast.makeText(CallHistoryActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadMore() {
|
||||
currentPage++;
|
||||
loadData();
|
||||
}
|
||||
|
||||
private void loadMissedCount() {
|
||||
apiService.getMissedCallCount().enqueue(new Callback<CallApiResponse<Integer>>() {
|
||||
@Override
|
||||
public void onResponse(Call<CallApiResponse<Integer>> call,
|
||||
Response<CallApiResponse<Integer>> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) {
|
||||
Integer count = response.body().getData();
|
||||
if (count != null && count > 0) {
|
||||
tvMissedCount.setText(count + "未接");
|
||||
tvMissedCount.setVisibility(android.view.View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<CallApiResponse<Integer>> call, Throwable t) {
|
||||
// 忽略错误
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showCallOptions(CallRecordResponse record) {
|
||||
// 可以显示底部弹窗,提供回拨、删除等选项
|
||||
// 这里简化处理,直接回拨
|
||||
makeCall(record);
|
||||
}
|
||||
|
||||
private void makeCall(CallRecordResponse record) {
|
||||
String callType = record.getCallType();
|
||||
int otherUserId = record.getOtherUserId();
|
||||
|
||||
callManager.initiateCall(otherUserId, callType, new CallManager.CallCallback() {
|
||||
@Override
|
||||
public void onSuccess(InitiateCallResponse response) {
|
||||
// 通话界面会自动启动
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Toast.makeText(CallHistoryActivity.this, "呼叫失败: " + error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.R;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
/**
|
||||
* 通话记录列表适配器
|
||||
*/
|
||||
public class CallHistoryAdapter extends RecyclerView.Adapter<CallHistoryAdapter.ViewHolder> {
|
||||
|
||||
private List<CallRecordResponse> records = new ArrayList<>();
|
||||
private OnItemClickListener listener;
|
||||
private SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||
private SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd", Locale.getDefault());
|
||||
|
||||
public interface OnItemClickListener {
|
||||
void onItemClick(CallRecordResponse record);
|
||||
void onCallClick(CallRecordResponse record);
|
||||
}
|
||||
|
||||
public void setOnItemClickListener(OnItemClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setRecords(List<CallRecordResponse> records) {
|
||||
this.records = records != null ? records : new ArrayList<>();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addRecords(List<CallRecordResponse> newRecords) {
|
||||
if (newRecords != null && !newRecords.isEmpty()) {
|
||||
int start = records.size();
|
||||
records.addAll(newRecords);
|
||||
notifyItemRangeInserted(start, newRecords.size());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_call_record, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
CallRecordResponse record = records.get(position);
|
||||
holder.bind(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return records.size();
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
CircleImageView ivAvatar;
|
||||
TextView tvUserName;
|
||||
ImageView ivCallType;
|
||||
ImageView ivDirection;
|
||||
TextView tvStatus;
|
||||
TextView tvTime;
|
||||
ImageButton btnCall;
|
||||
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
ivAvatar = itemView.findViewById(R.id.ivAvatar);
|
||||
tvUserName = itemView.findViewById(R.id.tvUserName);
|
||||
ivCallType = itemView.findViewById(R.id.ivCallType);
|
||||
ivDirection = itemView.findViewById(R.id.ivDirection);
|
||||
tvStatus = itemView.findViewById(R.id.tvStatus);
|
||||
tvTime = itemView.findViewById(R.id.tvTime);
|
||||
btnCall = itemView.findViewById(R.id.btnCall);
|
||||
}
|
||||
|
||||
void bind(CallRecordResponse record) {
|
||||
// 用户名和头像
|
||||
tvUserName.setText(record.getOtherUserName() != null ?
|
||||
record.getOtherUserName() : "用户" + record.getOtherUserId());
|
||||
|
||||
if (record.getOtherUserAvatar() != null && !record.getOtherUserAvatar().isEmpty()) {
|
||||
Glide.with(itemView.getContext())
|
||||
.load(record.getOtherUserAvatar())
|
||||
.into(ivAvatar);
|
||||
} else {
|
||||
ivAvatar.setImageResource(R.drawable.ic_default_avatar);
|
||||
}
|
||||
|
||||
// 通话类型图标
|
||||
ivCallType.setImageResource("video".equals(record.getCallType()) ?
|
||||
R.drawable.ic_videocam : R.drawable.ic_call);
|
||||
|
||||
// 呼入/呼出方向和状态
|
||||
String status = record.getStatus();
|
||||
boolean isOutgoing = record.isOutgoing();
|
||||
|
||||
if ("missed".equals(status)) {
|
||||
ivDirection.setImageResource(R.drawable.ic_call_missed);
|
||||
tvStatus.setText("未接听");
|
||||
tvStatus.setTextColor(itemView.getContext().getColor(R.color.red));
|
||||
} else if ("rejected".equals(status)) {
|
||||
ivDirection.setImageResource(isOutgoing ? R.drawable.ic_call_made : R.drawable.ic_call_received);
|
||||
tvStatus.setText(isOutgoing ? "对方已拒绝" : "已拒绝");
|
||||
tvStatus.setTextColor(itemView.getContext().getColor(R.color.text_secondary));
|
||||
} else if ("cancelled".equals(status)) {
|
||||
ivDirection.setImageResource(R.drawable.ic_call_made);
|
||||
tvStatus.setText("已取消");
|
||||
tvStatus.setTextColor(itemView.getContext().getColor(R.color.text_secondary));
|
||||
} else if ("ended".equals(status)) {
|
||||
ivDirection.setImageResource(isOutgoing ? R.drawable.ic_call_made : R.drawable.ic_call_received);
|
||||
String durationText = record.getDurationText();
|
||||
tvStatus.setText(durationText.isEmpty() ? "已结束" : "通话 " + durationText);
|
||||
tvStatus.setTextColor(itemView.getContext().getColor(R.color.text_secondary));
|
||||
} else {
|
||||
ivDirection.setImageResource(isOutgoing ? R.drawable.ic_call_made : R.drawable.ic_call_received);
|
||||
tvStatus.setText(isOutgoing ? "呼出" : "呼入");
|
||||
tvStatus.setTextColor(itemView.getContext().getColor(R.color.text_secondary));
|
||||
}
|
||||
|
||||
// 时间
|
||||
if (record.getCallTime() != null) {
|
||||
Date callDate = new Date(record.getCallTime());
|
||||
Date today = new Date();
|
||||
if (isSameDay(callDate, today)) {
|
||||
tvTime.setText(timeFormat.format(callDate));
|
||||
} else {
|
||||
tvTime.setText(dateFormat.format(callDate));
|
||||
}
|
||||
}
|
||||
|
||||
// 点击事件
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onItemClick(record);
|
||||
});
|
||||
|
||||
btnCall.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onCallClick(record);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isSameDay(Date date1, Date date2) {
|
||||
SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd", Locale.getDefault());
|
||||
return fmt.format(date1).equals(fmt.format(date2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通话历史响应
|
||||
*/
|
||||
public class CallHistoryResponse {
|
||||
private List<CallRecordResponse> list;
|
||||
private int total;
|
||||
private int pageNum;
|
||||
private int pageSize;
|
||||
|
||||
public List<CallRecordResponse> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setList(List<CallRecordResponse> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public int getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(int total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public int getPageNum() {
|
||||
return pageNum;
|
||||
}
|
||||
|
||||
public void setPageNum(int pageNum) {
|
||||
this.pageNum = pageNum;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
/**
|
||||
* 通话管理器 - 单例模式
|
||||
*/
|
||||
public class CallManager implements CallSignalingClient.SignalingListener {
|
||||
private static final String TAG = "CallManager";
|
||||
private static CallManager instance;
|
||||
|
||||
private Context context;
|
||||
private CallSignalingClient signalingClient;
|
||||
private CallApiService apiService;
|
||||
private CallStateListener stateListener;
|
||||
|
||||
private String currentCallId;
|
||||
private String currentCallType;
|
||||
private boolean isCaller;
|
||||
private int otherUserId;
|
||||
private String otherUserName;
|
||||
private String otherUserAvatar;
|
||||
|
||||
public interface CallStateListener {
|
||||
void onCallStateChanged(String state, String callId);
|
||||
void onIncomingCall(String callId, int callerId, String callerName, String callerAvatar, String callType);
|
||||
void onCallConnected(String callId);
|
||||
void onCallEnded(String callId, String reason);
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
private CallManager(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
initApiService();
|
||||
}
|
||||
|
||||
public static synchronized CallManager getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
instance = new CallManager(context);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void initApiService() {
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(context);
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(ApiClient.getService(context).hashCode() > 0 ?
|
||||
new okhttp3.OkHttpClient.Builder()
|
||||
.addInterceptor(chain -> {
|
||||
String token = AuthStore.getToken(context);
|
||||
okhttp3.Request.Builder req = chain.request().newBuilder();
|
||||
if (token != null && !token.isEmpty()) {
|
||||
req.header("Authori-zation", token);
|
||||
req.header("Authorization", token);
|
||||
}
|
||||
return chain.proceed(req.build());
|
||||
})
|
||||
.build() : new okhttp3.OkHttpClient())
|
||||
.build();
|
||||
apiService = retrofit.create(CallApiService.class);
|
||||
}
|
||||
|
||||
public void setStateListener(CallStateListener listener) {
|
||||
this.stateListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接信令服务器
|
||||
*/
|
||||
public void connect(int userId) {
|
||||
Log.d(TAG, "connect() called, userId: " + userId);
|
||||
if (signalingClient != null && signalingClient.isConnected()) {
|
||||
Log.d(TAG, "已经连接,跳过");
|
||||
return;
|
||||
}
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(context);
|
||||
Log.d(TAG, "连接信令服务器,baseUrl: " + baseUrl);
|
||||
signalingClient = new CallSignalingClient(baseUrl, userId);
|
||||
signalingClient.setListener(this);
|
||||
signalingClient.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开信令服务器
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (signalingClient != null) {
|
||||
signalingClient.disconnect();
|
||||
signalingClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起通话
|
||||
*/
|
||||
public void initiateCall(int calleeId, String callType, CallCallback callback) {
|
||||
// 确保WebSocket已连接,这样才能收到对方的接听/拒绝通知
|
||||
String userId = com.example.livestreaming.net.AuthStore.getUserId(context);
|
||||
int uid = 0;
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
try {
|
||||
uid = (int) Double.parseDouble(userId);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "解析用户ID失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
final int finalUid = uid;
|
||||
|
||||
// 如果WebSocket未连接,先连接再发起通话
|
||||
if (signalingClient == null || !signalingClient.isConnected()) {
|
||||
Log.d(TAG, "发起通话前连接WebSocket, userId=" + finalUid);
|
||||
connect(finalUid);
|
||||
// 延迟发起通话,等待WebSocket连接
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
||||
Log.d(TAG, "WebSocket连接状态: " + (signalingClient != null && signalingClient.isConnected()));
|
||||
doInitiateCall(calleeId, callType, callback);
|
||||
}, 1000);
|
||||
} else {
|
||||
Log.d(TAG, "WebSocket已连接,直接发起通话");
|
||||
doInitiateCall(calleeId, callType, callback);
|
||||
}
|
||||
}
|
||||
|
||||
private void doInitiateCall(int calleeId, String callType, CallCallback callback) {
|
||||
InitiateCallRequest request = new InitiateCallRequest(calleeId, callType);
|
||||
apiService.initiateCall(request).enqueue(new Callback<CallApiResponse<InitiateCallResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<CallApiResponse<InitiateCallResponse>> call,
|
||||
Response<CallApiResponse<InitiateCallResponse>> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) {
|
||||
InitiateCallResponse data = response.body().getData();
|
||||
currentCallId = data.getCallId();
|
||||
currentCallType = data.getCallType();
|
||||
isCaller = true;
|
||||
otherUserId = data.getCalleeId();
|
||||
otherUserName = data.getCalleeName();
|
||||
otherUserAvatar = data.getCalleeAvatar();
|
||||
|
||||
Log.d(TAG, "通话创建成功: callId=" + currentCallId + ", WebSocket连接=" +
|
||||
(signalingClient != null && signalingClient.isConnected()));
|
||||
|
||||
// 启动通话界面
|
||||
startCallActivity(true);
|
||||
|
||||
if (callback != null) callback.onSuccess(data);
|
||||
} else {
|
||||
String msg = response.body() != null ? response.body().getMessage() : "发起通话失败";
|
||||
if (callback != null) callback.onError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<CallApiResponse<InitiateCallResponse>> call, Throwable t) {
|
||||
Log.e(TAG, "Initiate call failed", t);
|
||||
if (callback != null) callback.onError(t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 接听通话
|
||||
*/
|
||||
public void acceptCall(String callId) {
|
||||
Log.d(TAG, "acceptCall: callId=" + callId);
|
||||
|
||||
// 确保WebSocket已连接
|
||||
if (signalingClient == null || !signalingClient.isConnected()) {
|
||||
Log.w(TAG, "WebSocket未连接,尝试重新连接");
|
||||
// 尝试获取当前用户ID并连接
|
||||
String userId = com.example.livestreaming.net.AuthStore.getUserId(context);
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
try {
|
||||
int uid = (int) Double.parseDouble(userId);
|
||||
connect(uid);
|
||||
// 延迟发送接听消息,等待连接建立
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
||||
if (signalingClient != null && signalingClient.isConnected()) {
|
||||
signalingClient.sendCallAccept(callId);
|
||||
Log.d(TAG, "延迟发送接听消息成功");
|
||||
}
|
||||
}, 500);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "解析用户ID失败", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
signalingClient.sendCallAccept(callId);
|
||||
Log.d(TAG, "发送接听消息");
|
||||
}
|
||||
|
||||
apiService.acceptCall(callId).enqueue(new Callback<CallApiResponse<Boolean>>() {
|
||||
@Override
|
||||
public void onResponse(Call<CallApiResponse<Boolean>> call, Response<CallApiResponse<Boolean>> response) {
|
||||
Log.d(TAG, "Accept call API response: success=" + response.isSuccessful() +
|
||||
", code=" + response.code() +
|
||||
", body=" + (response.body() != null ? response.body().getMessage() : "null"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<CallApiResponse<Boolean>> call, Throwable t) {
|
||||
Log.e(TAG, "Accept call API failed: " + t.getMessage(), t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝通话
|
||||
*/
|
||||
public void rejectCall(String callId) {
|
||||
if (signalingClient != null) {
|
||||
signalingClient.sendCallReject(callId);
|
||||
}
|
||||
apiService.rejectCall(callId).enqueue(new Callback<CallApiResponse<Boolean>>() {
|
||||
@Override
|
||||
public void onResponse(Call<CallApiResponse<Boolean>> call, Response<CallApiResponse<Boolean>> response) {
|
||||
Log.d(TAG, "Reject call response: " + (response.isSuccessful()));
|
||||
clearCallState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<CallApiResponse<Boolean>> call, Throwable t) {
|
||||
Log.e(TAG, "Reject call failed", t);
|
||||
clearCallState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消通话
|
||||
*/
|
||||
public void cancelCall(String callId) {
|
||||
if (signalingClient != null) {
|
||||
signalingClient.sendCallCancel(callId);
|
||||
}
|
||||
apiService.cancelCall(callId).enqueue(new Callback<CallApiResponse<Boolean>>() {
|
||||
@Override
|
||||
public void onResponse(Call<CallApiResponse<Boolean>> call, Response<CallApiResponse<Boolean>> response) {
|
||||
Log.d(TAG, "Cancel call response: " + (response.isSuccessful()));
|
||||
clearCallState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<CallApiResponse<Boolean>> call, Throwable t) {
|
||||
Log.e(TAG, "Cancel call failed", t);
|
||||
clearCallState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束通话
|
||||
*/
|
||||
public void endCall(String callId, String reason) {
|
||||
if (signalingClient != null) {
|
||||
signalingClient.sendCallEnd(callId, reason);
|
||||
}
|
||||
apiService.endCall(callId, reason).enqueue(new Callback<CallApiResponse<Boolean>>() {
|
||||
@Override
|
||||
public void onResponse(Call<CallApiResponse<Boolean>> call, Response<CallApiResponse<Boolean>> response) {
|
||||
Log.d(TAG, "End call response: " + (response.isSuccessful()));
|
||||
clearCallState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<CallApiResponse<Boolean>> call, Throwable t) {
|
||||
Log.e(TAG, "End call failed", t);
|
||||
clearCallState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startCallActivity(boolean isCaller) {
|
||||
Intent intent = new Intent(context, CallActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("callId", currentCallId);
|
||||
intent.putExtra("callType", currentCallType);
|
||||
intent.putExtra("isCaller", isCaller);
|
||||
intent.putExtra("otherUserId", otherUserId);
|
||||
intent.putExtra("otherUserName", otherUserName);
|
||||
intent.putExtra("otherUserAvatar", otherUserAvatar);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private void clearCallState() {
|
||||
currentCallId = null;
|
||||
currentCallType = null;
|
||||
isCaller = false;
|
||||
otherUserId = 0;
|
||||
otherUserName = null;
|
||||
otherUserAvatar = null;
|
||||
}
|
||||
|
||||
public String getCurrentCallId() {
|
||||
return currentCallId;
|
||||
}
|
||||
|
||||
public boolean isInCall() {
|
||||
return currentCallId != null;
|
||||
}
|
||||
|
||||
// SignalingListener 实现
|
||||
@Override
|
||||
public void onConnected() {
|
||||
Log.d(TAG, "Signaling connected - WebSocket连接成功并已注册");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
Log.d(TAG, "Signaling disconnected - WebSocket断开连接");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "Signaling error: " + error);
|
||||
if (stateListener != null) {
|
||||
stateListener.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncomingCall(String callId, int callerId, String callerName, String callerAvatar, String callType) {
|
||||
Log.d(TAG, "========== 收到来电通知 ==========");
|
||||
Log.d(TAG, "callId: " + callId);
|
||||
Log.d(TAG, "callerId: " + callerId);
|
||||
Log.d(TAG, "callerName: " + callerName);
|
||||
Log.d(TAG, "callType: " + callType);
|
||||
|
||||
currentCallId = callId;
|
||||
currentCallType = callType;
|
||||
isCaller = false;
|
||||
otherUserId = callerId;
|
||||
otherUserName = callerName;
|
||||
otherUserAvatar = callerAvatar;
|
||||
|
||||
// 启动来电界面
|
||||
Log.d(TAG, "启动来电界面 IncomingCallActivity");
|
||||
Intent intent = new Intent(context, IncomingCallActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("callId", callId);
|
||||
intent.putExtra("callType", callType);
|
||||
intent.putExtra("callerId", callerId);
|
||||
intent.putExtra("callerName", callerName);
|
||||
intent.putExtra("callerAvatar", callerAvatar);
|
||||
context.startActivity(intent);
|
||||
|
||||
if (stateListener != null) {
|
||||
stateListener.onIncomingCall(callId, callerId, callerName, callerAvatar, callType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallAccepted(String callId) {
|
||||
Log.d(TAG, "========== 收到通话接听通知 ==========");
|
||||
Log.d(TAG, "callId: " + callId);
|
||||
Log.d(TAG, "currentCallId: " + currentCallId);
|
||||
Log.d(TAG, "stateListener: " + (stateListener != null ? stateListener.getClass().getSimpleName() : "null"));
|
||||
|
||||
if (stateListener != null) {
|
||||
Log.d(TAG, "调用 stateListener.onCallConnected");
|
||||
stateListener.onCallConnected(callId);
|
||||
} else {
|
||||
Log.w(TAG, "stateListener 为空,无法通知界面");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallRejected(String callId) {
|
||||
Log.d(TAG, "Call rejected: " + callId);
|
||||
if (stateListener != null) {
|
||||
stateListener.onCallEnded(callId, "rejected");
|
||||
}
|
||||
clearCallState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallCancelled(String callId) {
|
||||
Log.d(TAG, "Call cancelled: " + callId);
|
||||
if (stateListener != null) {
|
||||
stateListener.onCallEnded(callId, "cancelled");
|
||||
}
|
||||
clearCallState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallEnded(String callId, String reason) {
|
||||
Log.d(TAG, "Call ended: " + callId + ", reason: " + reason);
|
||||
if (stateListener != null) {
|
||||
stateListener.onCallEnded(callId, reason);
|
||||
}
|
||||
clearCallState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallTimeout(String callId) {
|
||||
Log.d(TAG, "Call timeout: " + callId);
|
||||
if (stateListener != null) {
|
||||
stateListener.onCallEnded(callId, "timeout");
|
||||
}
|
||||
clearCallState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOffer(String callId, String sdp) {
|
||||
// WebRTC offer处理 - 后续实现
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnswer(String callId, String sdp) {
|
||||
// WebRTC answer处理 - 后续实现
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(String callId, JSONObject candidate) {
|
||||
// WebRTC ICE candidate处理 - 后续实现
|
||||
}
|
||||
|
||||
public interface CallCallback {
|
||||
void onSuccess(InitiateCallResponse response);
|
||||
void onError(String error);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
/**
|
||||
* 通话记录响应
|
||||
*/
|
||||
public class CallRecordResponse {
|
||||
private long id;
|
||||
private String callId;
|
||||
private int callerId;
|
||||
private int calleeId;
|
||||
private String callType;
|
||||
private String status;
|
||||
private Long callTime;
|
||||
private Long connectTime;
|
||||
private Long endTime;
|
||||
private int duration;
|
||||
private boolean isOutgoing;
|
||||
private String callerName;
|
||||
private String callerAvatar;
|
||||
private String calleeName;
|
||||
private String calleeAvatar;
|
||||
private int otherUserId;
|
||||
private String otherUserName;
|
||||
private String otherUserAvatar;
|
||||
|
||||
// Getters and Setters
|
||||
public long getId() { return id; }
|
||||
public void setId(long id) { this.id = id; }
|
||||
|
||||
public String getCallId() { return callId; }
|
||||
public void setCallId(String callId) { this.callId = callId; }
|
||||
|
||||
public int getCallerId() { return callerId; }
|
||||
public void setCallerId(int callerId) { this.callerId = callerId; }
|
||||
|
||||
public int getCalleeId() { return calleeId; }
|
||||
public void setCalleeId(int calleeId) { this.calleeId = calleeId; }
|
||||
|
||||
public String getCallType() { return callType; }
|
||||
public void setCallType(String callType) { this.callType = callType; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public Long getCallTime() { return callTime; }
|
||||
public void setCallTime(Long callTime) { this.callTime = callTime; }
|
||||
|
||||
public Long getConnectTime() { return connectTime; }
|
||||
public void setConnectTime(Long connectTime) { this.connectTime = connectTime; }
|
||||
|
||||
public Long getEndTime() { return endTime; }
|
||||
public void setEndTime(Long endTime) { this.endTime = endTime; }
|
||||
|
||||
public int getDuration() { return duration; }
|
||||
public void setDuration(int duration) { this.duration = duration; }
|
||||
|
||||
public boolean isOutgoing() { return isOutgoing; }
|
||||
public void setOutgoing(boolean outgoing) { isOutgoing = outgoing; }
|
||||
|
||||
public String getCallerName() { return callerName; }
|
||||
public void setCallerName(String callerName) { this.callerName = callerName; }
|
||||
|
||||
public String getCallerAvatar() { return callerAvatar; }
|
||||
public void setCallerAvatar(String callerAvatar) { this.callerAvatar = callerAvatar; }
|
||||
|
||||
public String getCalleeName() { return calleeName; }
|
||||
public void setCalleeName(String calleeName) { this.calleeName = calleeName; }
|
||||
|
||||
public String getCalleeAvatar() { return calleeAvatar; }
|
||||
public void setCalleeAvatar(String calleeAvatar) { this.calleeAvatar = calleeAvatar; }
|
||||
|
||||
public int getOtherUserId() { return otherUserId; }
|
||||
public void setOtherUserId(int otherUserId) { this.otherUserId = otherUserId; }
|
||||
|
||||
public String getOtherUserName() { return otherUserName; }
|
||||
public void setOtherUserName(String otherUserName) { this.otherUserName = otherUserName; }
|
||||
|
||||
public String getOtherUserAvatar() { return otherUserAvatar; }
|
||||
public void setOtherUserAvatar(String otherUserAvatar) { this.otherUserAvatar = otherUserAvatar; }
|
||||
|
||||
/**
|
||||
* 获取状态显示文本
|
||||
*/
|
||||
public String getStatusText() {
|
||||
if (status == null) return "";
|
||||
switch (status) {
|
||||
case "ended": return "已结束";
|
||||
case "missed": return "未接听";
|
||||
case "rejected": return "已拒绝";
|
||||
case "cancelled": return "已取消";
|
||||
case "busy": return "对方忙";
|
||||
default: return status;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通话时长显示文本
|
||||
*/
|
||||
public String getDurationText() {
|
||||
if (duration <= 0) return "";
|
||||
int minutes = duration / 60;
|
||||
int seconds = duration % 60;
|
||||
if (minutes > 0) {
|
||||
return String.format("%d分%d秒", minutes, seconds);
|
||||
}
|
||||
return String.format("%d秒", seconds);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
/**
|
||||
* 通话信令WebSocket客户端
|
||||
*/
|
||||
public class CallSignalingClient {
|
||||
private static final String TAG = "CallSignalingClient";
|
||||
|
||||
private WebSocket webSocket;
|
||||
private OkHttpClient client;
|
||||
private SignalingListener listener;
|
||||
private Handler mainHandler;
|
||||
private String baseUrl;
|
||||
private int userId;
|
||||
private boolean isConnected = false;
|
||||
|
||||
public interface SignalingListener {
|
||||
void onConnected();
|
||||
void onDisconnected();
|
||||
void onError(String error);
|
||||
void onIncomingCall(String callId, int callerId, String callerName, String callerAvatar, String callType);
|
||||
void onCallAccepted(String callId);
|
||||
void onCallRejected(String callId);
|
||||
void onCallCancelled(String callId);
|
||||
void onCallEnded(String callId, String reason);
|
||||
void onCallTimeout(String callId);
|
||||
void onOffer(String callId, String sdp);
|
||||
void onAnswer(String callId, String sdp);
|
||||
void onIceCandidate(String callId, JSONObject candidate);
|
||||
}
|
||||
|
||||
public CallSignalingClient(String baseUrl, int userId) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.userId = userId;
|
||||
this.client = new OkHttpClient();
|
||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
public void setListener(SignalingListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
String wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://");
|
||||
if (!wsUrl.endsWith("/")) wsUrl += "/";
|
||||
wsUrl += "ws/call";
|
||||
|
||||
Log.d(TAG, "Connecting to: " + wsUrl);
|
||||
|
||||
Request request = new Request.Builder().url(wsUrl).build();
|
||||
webSocket = client.newWebSocket(request, new WebSocketListener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
Log.d(TAG, "WebSocket connected");
|
||||
isConnected = true;
|
||||
register();
|
||||
notifyConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
Log.d(TAG, "Received: " + text);
|
||||
handleMessage(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosing(WebSocket webSocket, int code, String reason) {
|
||||
Log.d(TAG, "WebSocket closing: " + reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||
Log.d(TAG, "WebSocket closed: " + reason);
|
||||
isConnected = false;
|
||||
notifyDisconnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||
Log.e(TAG, "WebSocket error", t);
|
||||
isConnected = false;
|
||||
notifyError(t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
if (webSocket != null) {
|
||||
webSocket.close(1000, "User disconnect");
|
||||
webSocket = null;
|
||||
}
|
||||
isConnected = false;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
private void register() {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "register");
|
||||
msg.put("userId", userId);
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Register error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendCallRequest(int calleeId, String callType) {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "call_request");
|
||||
msg.put("calleeId", calleeId);
|
||||
msg.put("callType", callType);
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Send call request error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendCallAccept(String callId) {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "call_accept");
|
||||
msg.put("callId", callId);
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Send call accept error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendCallReject(String callId) {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "call_reject");
|
||||
msg.put("callId", callId);
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Send call reject error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendCallCancel(String callId) {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "call_cancel");
|
||||
msg.put("callId", callId);
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Send call cancel error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendCallEnd(String callId, String reason) {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "call_end");
|
||||
msg.put("callId", callId);
|
||||
msg.put("reason", reason != null ? reason : "normal");
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Send call end error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendOffer(String callId, String sdp) {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "offer");
|
||||
msg.put("callId", callId);
|
||||
msg.put("sdp", sdp);
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Send offer error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendAnswer(String callId, String sdp) {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "answer");
|
||||
msg.put("callId", callId);
|
||||
msg.put("sdp", sdp);
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Send answer error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendIceCandidate(String callId, JSONObject candidate) {
|
||||
try {
|
||||
JSONObject msg = new JSONObject();
|
||||
msg.put("type", "ice-candidate");
|
||||
msg.put("callId", callId);
|
||||
msg.put("candidate", candidate);
|
||||
send(msg);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Send ICE candidate error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void send(JSONObject msg) {
|
||||
if (webSocket != null && isConnected) {
|
||||
String text = msg.toString();
|
||||
Log.d(TAG, "Sending: " + text);
|
||||
webSocket.send(text);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessage(String text) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(text);
|
||||
String type = json.optString("type", "");
|
||||
|
||||
switch (type) {
|
||||
case "registered":
|
||||
Log.d(TAG, "Registered successfully");
|
||||
break;
|
||||
case "incoming_call":
|
||||
handleIncomingCall(json);
|
||||
break;
|
||||
case "call_created":
|
||||
// 通话创建成功,等待对方接听
|
||||
break;
|
||||
case "call_accepted":
|
||||
handleCallAccepted(json);
|
||||
break;
|
||||
case "call_rejected":
|
||||
handleCallRejected(json);
|
||||
break;
|
||||
case "call_cancelled":
|
||||
handleCallCancelled(json);
|
||||
break;
|
||||
case "call_ended":
|
||||
handleCallEnded(json);
|
||||
break;
|
||||
case "call_timeout":
|
||||
handleCallTimeout(json);
|
||||
break;
|
||||
case "offer":
|
||||
handleOffer(json);
|
||||
break;
|
||||
case "answer":
|
||||
handleAnswer(json);
|
||||
break;
|
||||
case "ice-candidate":
|
||||
handleIceCandidate(json);
|
||||
break;
|
||||
case "error":
|
||||
notifyError(json.optString("message", "Unknown error"));
|
||||
break;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Parse message error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleIncomingCall(JSONObject json) {
|
||||
String callId = json.optString("callId");
|
||||
int callerId = json.optInt("callerId");
|
||||
String callerName = json.optString("callerName");
|
||||
String callerAvatar = json.optString("callerAvatar");
|
||||
String callType = json.optString("callType");
|
||||
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onIncomingCall(callId, callerId, callerName, callerAvatar, callType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleCallAccepted(JSONObject json) {
|
||||
String callId = json.optString("callId");
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onCallAccepted(callId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleCallRejected(JSONObject json) {
|
||||
String callId = json.optString("callId");
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onCallRejected(callId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleCallCancelled(JSONObject json) {
|
||||
String callId = json.optString("callId");
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onCallCancelled(callId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleCallEnded(JSONObject json) {
|
||||
String callId = json.optString("callId");
|
||||
String reason = json.optString("reason", "normal");
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onCallEnded(callId, reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleCallTimeout(JSONObject json) {
|
||||
String callId = json.optString("callId");
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onCallTimeout(callId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleOffer(JSONObject json) throws JSONException {
|
||||
String callId = json.optString("callId");
|
||||
String sdp = json.optString("sdp");
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onOffer(callId, sdp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleAnswer(JSONObject json) throws JSONException {
|
||||
String callId = json.optString("callId");
|
||||
String sdp = json.optString("sdp");
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onAnswer(callId, sdp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleIceCandidate(JSONObject json) throws JSONException {
|
||||
String callId = json.optString("callId");
|
||||
JSONObject candidate = json.optJSONObject("candidate");
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) {
|
||||
listener.onIceCandidate(callId, candidate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyConnected() {
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) listener.onConnected();
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyDisconnected() {
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) listener.onDisconnected();
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyError(String error) {
|
||||
mainHandler.post(() -> {
|
||||
if (listener != null) listener.onError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.R;
|
||||
|
||||
/**
|
||||
* 来电界面
|
||||
*/
|
||||
public class IncomingCallActivity extends AppCompatActivity implements CallManager.CallStateListener {
|
||||
|
||||
private ImageView ivBackgroundAvatar;
|
||||
private ImageView ivAvatar;
|
||||
private TextView tvUserName;
|
||||
private TextView tvCallType;
|
||||
private ImageButton btnReject;
|
||||
private ImageButton btnAccept;
|
||||
|
||||
private CallManager callManager;
|
||||
private Ringtone ringtone;
|
||||
private Vibrator vibrator;
|
||||
|
||||
private String callId;
|
||||
private String callType;
|
||||
private int callerId;
|
||||
private String callerName;
|
||||
private String callerAvatar;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// 显示在锁屏上方,保持屏幕常亮
|
||||
getWindow().addFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||
);
|
||||
|
||||
setContentView(R.layout.activity_incoming_call);
|
||||
|
||||
initViews();
|
||||
initData();
|
||||
initCallManager();
|
||||
setupListeners();
|
||||
updateUI();
|
||||
startRinging();
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
ivBackgroundAvatar = findViewById(R.id.ivBackgroundAvatar);
|
||||
ivAvatar = findViewById(R.id.ivAvatar);
|
||||
tvUserName = findViewById(R.id.tvUserName);
|
||||
tvCallType = findViewById(R.id.tvCallType);
|
||||
btnReject = findViewById(R.id.btnReject);
|
||||
btnAccept = findViewById(R.id.btnAccept);
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
callId = getIntent().getStringExtra("callId");
|
||||
callType = getIntent().getStringExtra("callType");
|
||||
callerId = getIntent().getIntExtra("callerId", 0);
|
||||
callerName = getIntent().getStringExtra("callerName");
|
||||
callerAvatar = getIntent().getStringExtra("callerAvatar");
|
||||
}
|
||||
|
||||
private void initCallManager() {
|
||||
callManager = CallManager.getInstance(this);
|
||||
callManager.setStateListener(this);
|
||||
|
||||
// 确保WebSocket已连接(被叫方需要发送接听/拒绝消息)
|
||||
String userId = com.example.livestreaming.net.AuthStore.getUserId(this);
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
try {
|
||||
int uid = (int) Double.parseDouble(userId);
|
||||
if (uid > 0) {
|
||||
android.util.Log.d("IncomingCallActivity", "确保WebSocket连接,userId: " + uid);
|
||||
callManager.connect(uid);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
android.util.Log.e("IncomingCallActivity", "解析用户ID失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
btnReject.setOnClickListener(v -> {
|
||||
stopRinging();
|
||||
callManager.rejectCall(callId);
|
||||
finish();
|
||||
});
|
||||
|
||||
btnAccept.setOnClickListener(v -> {
|
||||
stopRinging();
|
||||
callManager.acceptCall(callId);
|
||||
|
||||
// 跳转到通话界面,并标记为已接通
|
||||
Intent intent = new Intent(this, CallActivity.class);
|
||||
intent.putExtra("callId", callId);
|
||||
intent.putExtra("callType", callType);
|
||||
intent.putExtra("isCaller", false);
|
||||
intent.putExtra("otherUserId", callerId);
|
||||
intent.putExtra("otherUserName", callerName);
|
||||
intent.putExtra("otherUserAvatar", callerAvatar);
|
||||
intent.putExtra("isConnected", true); // 被叫方接听后直接进入通话状态
|
||||
startActivity(intent);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
tvUserName.setText(callerName != null ? callerName : "用户" + callerId);
|
||||
tvCallType.setText("video".equals(callType) ? "视频来电" : "语音来电");
|
||||
|
||||
if (callerAvatar != null && !callerAvatar.isEmpty()) {
|
||||
Glide.with(this).load(callerAvatar).into(ivAvatar);
|
||||
Glide.with(this).load(callerAvatar).into(ivBackgroundAvatar);
|
||||
}
|
||||
}
|
||||
|
||||
private void startRinging() {
|
||||
// 播放铃声
|
||||
try {
|
||||
Uri ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
|
||||
ringtone = RingtoneManager.getRingtone(this, ringtoneUri);
|
||||
if (ringtone != null) {
|
||||
ringtone.play();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// 震动
|
||||
try {
|
||||
vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
|
||||
if (vibrator != null && vibrator.hasVibrator()) {
|
||||
long[] pattern = {0, 1000, 1000};
|
||||
vibrator.vibrate(pattern, 0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopRinging() {
|
||||
if (ringtone != null && ringtone.isPlaying()) {
|
||||
ringtone.stop();
|
||||
}
|
||||
if (vibrator != null) {
|
||||
vibrator.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// CallStateListener 实现
|
||||
@Override
|
||||
public void onCallStateChanged(String state, String callId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncomingCall(String callId, int callerId, String callerName, String callerAvatar, String callType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallConnected(String callId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallEnded(String callId, String reason) {
|
||||
runOnUiThread(() -> {
|
||||
stopRinging();
|
||||
if ("cancelled".equals(reason)) {
|
||||
Toast.makeText(this, "对方已取消", Toast.LENGTH_SHORT).show();
|
||||
} else if ("timeout".equals(reason)) {
|
||||
Toast.makeText(this, "来电已超时", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
runOnUiThread(() -> {
|
||||
stopRinging();
|
||||
Toast.makeText(this, "错误: " + error, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopRinging();
|
||||
if (callManager != null) {
|
||||
callManager.setStateListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// 禁止返回键,必须接听或拒绝
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
/**
|
||||
* 发起通话请求
|
||||
*/
|
||||
public class InitiateCallRequest {
|
||||
private int calleeId;
|
||||
private String callType; // "voice" 或 "video"
|
||||
|
||||
public InitiateCallRequest(int calleeId, String callType) {
|
||||
this.calleeId = calleeId;
|
||||
this.callType = callType;
|
||||
}
|
||||
|
||||
public int getCalleeId() {
|
||||
return calleeId;
|
||||
}
|
||||
|
||||
public void setCalleeId(int calleeId) {
|
||||
this.calleeId = calleeId;
|
||||
}
|
||||
|
||||
public String getCallType() {
|
||||
return callType;
|
||||
}
|
||||
|
||||
public void setCallType(String callType) {
|
||||
this.callType = callType;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
/**
|
||||
* 发起通话响应
|
||||
*/
|
||||
public class InitiateCallResponse {
|
||||
private String callId;
|
||||
private String callType;
|
||||
private int calleeId;
|
||||
private String calleeName;
|
||||
private String calleeAvatar;
|
||||
private String status;
|
||||
private String signalingUrl;
|
||||
|
||||
public String getCallId() {
|
||||
return callId;
|
||||
}
|
||||
|
||||
public void setCallId(String callId) {
|
||||
this.callId = callId;
|
||||
}
|
||||
|
||||
public String getCallType() {
|
||||
return callType;
|
||||
}
|
||||
|
||||
public void setCallType(String callType) {
|
||||
this.callType = callType;
|
||||
}
|
||||
|
||||
public int getCalleeId() {
|
||||
return calleeId;
|
||||
}
|
||||
|
||||
public void setCalleeId(int calleeId) {
|
||||
this.calleeId = calleeId;
|
||||
}
|
||||
|
||||
public String getCalleeName() {
|
||||
return calleeName;
|
||||
}
|
||||
|
||||
public void setCalleeName(String calleeName) {
|
||||
this.calleeName = calleeName;
|
||||
}
|
||||
|
||||
public String getCalleeAvatar() {
|
||||
return calleeAvatar;
|
||||
}
|
||||
|
||||
public void setCalleeAvatar(String calleeAvatar) {
|
||||
this.calleeAvatar = calleeAvatar;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getSignalingUrl() {
|
||||
return signalingUrl;
|
||||
}
|
||||
|
||||
public void setSignalingUrl(String signalingUrl) {
|
||||
this.signalingUrl = signalingUrl;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class ConversationResponse {
|
||||
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
|
||||
@SerializedName("title")
|
||||
private String title;
|
||||
|
||||
@SerializedName("lastMessage")
|
||||
private String lastMessage;
|
||||
|
||||
@SerializedName("timeText")
|
||||
private String timeText;
|
||||
|
||||
@SerializedName("unreadCount")
|
||||
private Integer unreadCount;
|
||||
|
||||
@SerializedName("muted")
|
||||
private Boolean muted;
|
||||
|
||||
@SerializedName("avatarUrl")
|
||||
private String avatarUrl;
|
||||
|
||||
@SerializedName("otherUserId")
|
||||
private Integer otherUserId;
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getTitle() { return title; }
|
||||
public String getLastMessage() { return lastMessage; }
|
||||
public String getTimeText() { return timeText; }
|
||||
public Integer getUnreadCount() { return unreadCount; }
|
||||
public Boolean getMuted() { return muted; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
public Integer getOtherUserId() { return otherUserId; }
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CreateRechargeRequest {
|
||||
|
||||
@SerializedName("optionId")
|
||||
private Integer optionId;
|
||||
|
||||
@SerializedName("coinAmount")
|
||||
private BigDecimal coinAmount;
|
||||
|
||||
@SerializedName("price")
|
||||
private BigDecimal price;
|
||||
|
||||
public CreateRechargeRequest(Integer optionId, BigDecimal coinAmount, BigDecimal price) {
|
||||
this.optionId = optionId;
|
||||
this.coinAmount = coinAmount;
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public Integer getOptionId() { return optionId; }
|
||||
public BigDecimal getCoinAmount() { return coinAmount; }
|
||||
public BigDecimal getPrice() { return price; }
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class CreateRechargeResponse {
|
||||
|
||||
@SerializedName("orderId")
|
||||
private String orderId;
|
||||
|
||||
@SerializedName("paymentUrl")
|
||||
private String paymentUrl;
|
||||
|
||||
public String getOrderId() { return orderId; }
|
||||
public String getPaymentUrl() { return paymentUrl; }
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class FileUploadResponse {
|
||||
|
||||
@SerializedName("url")
|
||||
private String url;
|
||||
|
||||
@SerializedName("fileName")
|
||||
private String fileName;
|
||||
|
||||
@SerializedName("type")
|
||||
private String type;
|
||||
|
||||
public String getUrl() { return url; }
|
||||
public String getFileName() { return fileName; }
|
||||
public String getType() { return type; }
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class FriendRequestResponse {
|
||||
|
||||
@SerializedName("id")
|
||||
private Long id;
|
||||
|
||||
@SerializedName("from_user_id")
|
||||
private Integer fromUserId;
|
||||
|
||||
@SerializedName("nickname")
|
||||
private String nickname;
|
||||
|
||||
@SerializedName("avatarUrl")
|
||||
private String avatarUrl;
|
||||
|
||||
@SerializedName("phone")
|
||||
private String phone;
|
||||
|
||||
@SerializedName("message")
|
||||
private String message;
|
||||
|
||||
@SerializedName("create_time")
|
||||
private String createTime;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Integer getFromUserId() { return fromUserId; }
|
||||
public String getNickname() { return nickname; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
public String getPhone() { return phone; }
|
||||
public String getMessage() { return message; }
|
||||
public String getCreateTime() { return createTime; }
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class FriendResponse {
|
||||
|
||||
@SerializedName("id")
|
||||
private Integer id;
|
||||
|
||||
@SerializedName("name")
|
||||
private String name;
|
||||
|
||||
@SerializedName("avatarUrl")
|
||||
private String avatarUrl;
|
||||
|
||||
@SerializedName("phone")
|
||||
private String phone;
|
||||
|
||||
@SerializedName("isOnline")
|
||||
private Integer isOnline;
|
||||
|
||||
@SerializedName("lastOnlineTime")
|
||||
private String lastOnlineTime;
|
||||
|
||||
public Integer getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
public String getPhone() { return phone; }
|
||||
public boolean isOnline() { return isOnline != null && isOnline == 1; }
|
||||
public String getLastOnlineTime() { return lastOnlineTime; }
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class GiftResponse {
|
||||
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
|
||||
@SerializedName("name")
|
||||
private String name;
|
||||
|
||||
@SerializedName("price")
|
||||
private BigDecimal price;
|
||||
|
||||
@SerializedName("iconUrl")
|
||||
private String iconUrl;
|
||||
|
||||
@SerializedName("description")
|
||||
private String description;
|
||||
|
||||
@SerializedName("level")
|
||||
private Integer level;
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public BigDecimal getPrice() { return price; }
|
||||
public String getIconUrl() { return iconUrl; }
|
||||
public String getDescription() { return description; }
|
||||
public Integer getLevel() { return level; }
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.util.List;
|
||||
|
||||
public class PageResponse<T> {
|
||||
|
||||
@SerializedName("list")
|
||||
private List<T> list;
|
||||
|
||||
@SerializedName("total")
|
||||
private Long total;
|
||||
|
||||
@SerializedName("page")
|
||||
private Integer page;
|
||||
|
||||
@SerializedName("limit")
|
||||
private Integer limit;
|
||||
|
||||
@SerializedName("totalPage")
|
||||
private Integer totalPage;
|
||||
|
||||
public List<T> getList() { return list; }
|
||||
public Long getTotal() { return total; }
|
||||
public Integer getPage() { return page; }
|
||||
public Integer getLimit() { return limit; }
|
||||
public Integer getTotalPage() { return totalPage; }
|
||||
|
||||
public boolean hasMore() {
|
||||
return page != null && totalPage != null && page < totalPage;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class PrivateMessageResponse {
|
||||
|
||||
@SerializedName("messageId")
|
||||
private String messageId;
|
||||
|
||||
@SerializedName("userId")
|
||||
private Integer userId;
|
||||
|
||||
@SerializedName("username")
|
||||
private String username;
|
||||
|
||||
@SerializedName("avatarUrl")
|
||||
private String avatarUrl;
|
||||
|
||||
@SerializedName("message")
|
||||
private String message;
|
||||
|
||||
@SerializedName("timestamp")
|
||||
private Long timestamp;
|
||||
|
||||
@SerializedName("status")
|
||||
private String status;
|
||||
|
||||
@SerializedName("isSystemMessage")
|
||||
private Boolean isSystemMessage;
|
||||
|
||||
public String getMessageId() { return messageId; }
|
||||
public Integer getUserId() { return userId; }
|
||||
public String getUsername() { return username; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
public String getMessage() { return message; }
|
||||
public Long getTimestamp() { return timestamp; }
|
||||
public String getStatus() { return status; }
|
||||
public Boolean getIsSystemMessage() { return isSystemMessage; }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class RechargeOptionResponse {
|
||||
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
|
||||
@SerializedName("coinAmount")
|
||||
private BigDecimal coinAmount;
|
||||
|
||||
@SerializedName("price")
|
||||
private BigDecimal price;
|
||||
|
||||
@SerializedName("discountLabel")
|
||||
private String discountLabel;
|
||||
|
||||
public String getId() { return id; }
|
||||
public BigDecimal getCoinAmount() { return coinAmount; }
|
||||
public BigDecimal getPrice() { return price; }
|
||||
public String getDiscountLabel() { return discountLabel; }
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class SearchUserResponse {
|
||||
|
||||
@SerializedName("id")
|
||||
private Integer id;
|
||||
|
||||
@SerializedName("nickname")
|
||||
private String nickname;
|
||||
|
||||
@SerializedName("phone")
|
||||
private String phone;
|
||||
|
||||
@SerializedName("avatarUrl")
|
||||
private String avatarUrl;
|
||||
|
||||
@SerializedName("friendStatus")
|
||||
private Integer friendStatus; // 0=未添加, 1=已是好友, 2=已申请待审核
|
||||
|
||||
public Integer getId() { return id; }
|
||||
public String getNickname() { return nickname; }
|
||||
public String getPhone() { return phone; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
public Integer getFriendStatus() { return friendStatus; }
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class SendGiftRequest {
|
||||
|
||||
@SerializedName("giftId")
|
||||
private Integer giftId;
|
||||
|
||||
@SerializedName("streamerId")
|
||||
private Integer streamerId;
|
||||
|
||||
@SerializedName("count")
|
||||
private Integer count;
|
||||
|
||||
public SendGiftRequest(Integer giftId, Integer streamerId, Integer count) {
|
||||
this.giftId = giftId;
|
||||
this.streamerId = streamerId;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public Integer getGiftId() { return giftId; }
|
||||
public Integer getStreamerId() { return streamerId; }
|
||||
public Integer getCount() { return count; }
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class SendGiftResponse {
|
||||
|
||||
@SerializedName("success")
|
||||
private Boolean success;
|
||||
|
||||
@SerializedName("newBalance")
|
||||
private BigDecimal newBalance;
|
||||
|
||||
@SerializedName("message")
|
||||
private String message;
|
||||
|
||||
public Boolean getSuccess() { return success; }
|
||||
public BigDecimal getNewBalance() { return newBalance; }
|
||||
public String getMessage() { return message; }
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class SendMessageRequest {
|
||||
|
||||
@SerializedName("message")
|
||||
private String message;
|
||||
|
||||
@SerializedName("messageType")
|
||||
private String messageType;
|
||||
|
||||
public SendMessageRequest(String message) {
|
||||
this.message = message;
|
||||
this.messageType = "text";
|
||||
}
|
||||
|
||||
public SendMessageRequest(String message, String messageType) {
|
||||
this.message = message;
|
||||
this.messageType = messageType;
|
||||
}
|
||||
|
||||
public String getMessage() { return message; }
|
||||
public String getMessageType() { return messageType; }
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class UserBalanceResponse {
|
||||
|
||||
@SerializedName("coinBalance")
|
||||
private BigDecimal coinBalance;
|
||||
|
||||
public BigDecimal getCoinBalance() { return coinBalance; }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class UserEditRequest {
|
||||
|
||||
@SerializedName("nickname")
|
||||
private String nickname;
|
||||
|
||||
@SerializedName("avatar")
|
||||
private String avatar;
|
||||
|
||||
public UserEditRequest() {}
|
||||
|
||||
public UserEditRequest(String nickname, String avatar) {
|
||||
this.nickname = nickname;
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||
public String getNickname() { return nickname; }
|
||||
public String getAvatar() { return avatar; }
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class UserInfoResponse {
|
||||
|
||||
@SerializedName("nickname")
|
||||
private String nickname;
|
||||
|
||||
@SerializedName("avatar")
|
||||
private String avatar;
|
||||
|
||||
@SerializedName("phone")
|
||||
private String phone;
|
||||
|
||||
@SerializedName("nowMoney")
|
||||
private BigDecimal nowMoney;
|
||||
|
||||
@SerializedName("integral")
|
||||
private Integer integral;
|
||||
|
||||
@SerializedName("experience")
|
||||
private Integer experience;
|
||||
|
||||
@SerializedName("brokeragePrice")
|
||||
private BigDecimal brokeragePrice;
|
||||
|
||||
@SerializedName("level")
|
||||
private Integer level;
|
||||
|
||||
@SerializedName("isPromoter")
|
||||
private Boolean isPromoter;
|
||||
|
||||
@SerializedName("couponCount")
|
||||
private Integer couponCount;
|
||||
|
||||
@SerializedName("vip")
|
||||
private Boolean vip;
|
||||
|
||||
@SerializedName("vipIcon")
|
||||
private String vipIcon;
|
||||
|
||||
@SerializedName("vipName")
|
||||
private String vipName;
|
||||
|
||||
@SerializedName("rechargeSwitch")
|
||||
private Boolean rechargeSwitch;
|
||||
|
||||
@SerializedName("collectCount")
|
||||
private Integer collectCount;
|
||||
|
||||
public String getNickname() { return nickname; }
|
||||
public String getAvatar() { return avatar; }
|
||||
public String getPhone() { return phone; }
|
||||
public BigDecimal getNowMoney() { return nowMoney; }
|
||||
public Integer getIntegral() { return integral; }
|
||||
public Integer getExperience() { return experience; }
|
||||
public BigDecimal getBrokeragePrice() { return brokeragePrice; }
|
||||
public Integer getLevel() { return level; }
|
||||
public Boolean getIsPromoter() { return isPromoter; }
|
||||
public Integer getCouponCount() { return couponCount; }
|
||||
public Boolean getVip() { return vip; }
|
||||
public String getVipIcon() { return vipIcon; }
|
||||
public String getVipName() { return vipName; }
|
||||
public Boolean getRechargeSwitch() { return rechargeSwitch; }
|
||||
public Integer getCollectCount() { return collectCount; }
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#34C759" />
|
||||
</shape>
|
||||
5
android-app/app/src/main/res/drawable/bg_call_button.xml
Normal file
5
android-app/app/src/main/res/drawable/bg_call_button.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#33FFFFFF" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#FFFFFF" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#FF3B30" />
|
||||
</shape>
|
||||
10
android-app/app/src/main/res/drawable/ic_arrow_back.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_arrow_back.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_call.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_call.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_call_end.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_call_end.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.87,1.12 -2.66,1.85 -0.18,0.18 -0.43,0.28 -0.7,0.28 -0.28,0 -0.53,-0.11 -0.71,-0.29L0.29,13.08c-0.18,-0.17 -0.29,-0.42 -0.29,-0.7 0,-0.28 0.11,-0.53 0.29,-0.71C3.34,8.78 7.46,7 12,7s8.66,1.78 11.71,4.67c0.18,0.18 0.29,0.43 0.29,0.71 0,0.28 -0.11,0.53 -0.29,0.71l-2.48,2.48c-0.18,0.18 -0.43,0.29 -0.71,0.29 -0.27,0 -0.52,-0.11 -0.7,-0.28 -0.79,-0.74 -1.69,-1.36 -2.67,-1.85 -0.33,-0.16 -0.56,-0.5 -0.56,-0.9v-3.1C15.15,9.25 13.6,9 12,9z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_call_made.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_call_made.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M9,5v2h6.59L4,18.59 5.41,20 17,8.41V15h2V5z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_call_missed.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_call_missed.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#F44336"
|
||||
android:pathData="M19.59,7L12,14.59 6.41,9H11V7H3v8h2v-4.59l7,7 9,-9z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_call_received.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_call_received.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#2196F3"
|
||||
android:pathData="M20,5.41L18.59,4 7,15.59V9H5v10h10v-2H8.41z"/>
|
||||
</vector>
|
||||
13
android-app/app/src/main/res/drawable/ic_default_avatar.xml
Normal file
13
android-app/app/src/main/res/drawable/ic_default_avatar.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#E0E0E0"
|
||||
android:pathData="M24,24m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
|
||||
<path
|
||||
android:fillColor="#9E9E9E"
|
||||
android:pathData="M24,12c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6 2.69,-6 6,-6zM24,30c6.63,0 12,2.69 12,6v2H12v-2c0,-3.31 5.37,-6 12,-6z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_mic.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_mic.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_mic_off.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_mic_off.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,11h-1.7c0,0.74 -0.16,1.43 -0.43,2.05l1.23,1.23c0.56,-0.98 0.9,-2.09 0.9,-3.28zM14.98,11.17c0,-0.06 0.02,-0.11 0.02,-0.17V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v0.18l5.98,5.99zM4.27,3L3,4.27l6.01,6.01V11c0,1.66 1.33,3 2.99,3 0.22,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9L19.73,21 21,19.73 4.27,3z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_speaker.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_speaker.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,9v6h4l5,5V4L7,9H3zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_switch_camera.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_switch_camera.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M9,12c0,1.66 1.34,3 3,3s3,-1.34 3,-3 -1.34,-3 -3,-3 -3,1.34 -3,3zM17,8.54V4h-2v2H9V4H7v4.54l-4,4.46h2l2,-2.23V20h2v-4h6v4h2v-9.23l2,2.23h2l-4,-4.46z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_videocam.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_videocam.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
|
||||
</vector>
|
||||
239
android-app/app/src/main/res/layout/activity_call.xml
Normal file
239
android-app/app/src/main/res/layout/activity_call.xml
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#1A1A2E">
|
||||
|
||||
<!-- 背景模糊头像 -->
|
||||
<ImageView
|
||||
android:id="@+id/ivBackgroundAvatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:alpha="0.3" />
|
||||
|
||||
<!-- 视频通话时的远程视频 -->
|
||||
<SurfaceView
|
||||
android:id="@+id/remoteVideoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 视频通话时的本地视频(小窗口) -->
|
||||
<SurfaceView
|
||||
android:id="@+id/localVideoView"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginTop="80dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<!-- 顶部状态栏 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp"
|
||||
android:layout_marginTop="24dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnMinimize"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_arrow_back"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCallType"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="语音通话"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSwitchCamera"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_switch_camera"
|
||||
android:visibility="gone"
|
||||
app:tint="@android:color/white" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 用户信息区域 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutUserInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="60dp">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/ivAvatar"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:src="@drawable/ic_default_avatar"
|
||||
app:civ_border_color="#FFFFFF"
|
||||
app:civ_border_width="3dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUserName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="用户名"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCallStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="正在呼叫..."
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCallDuration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="00:00"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 底部控制按钮 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="60dp">
|
||||
|
||||
<!-- 通话中的控制按钮 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutCallControls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- 静音按钮 -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginHorizontal="20dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnMute"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:background="@drawable/bg_call_button"
|
||||
android:src="@drawable/ic_mic"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="静音"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 免提按钮 -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginHorizontal="20dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSpeaker"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:background="@drawable/bg_call_button"
|
||||
android:src="@drawable/ic_speaker"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="免提"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 视频按钮(仅视频通话显示) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutVideoToggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnVideo"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:background="@drawable/bg_call_button"
|
||||
android:src="@drawable/ic_videocam"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="摄像头"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 挂断按钮 -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnHangup"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:background="@drawable/bg_hangup_button"
|
||||
android:src="@drawable/ic_call_end"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="挂断"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/background_color">
|
||||
|
||||
<!-- 顶部标题栏 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="@color/white">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_arrow_back"
|
||||
app:tint="@color/text_primary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="通话记录"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvMissedCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0未接"
|
||||
android:textColor="@color/red"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#E0E0E0" />
|
||||
|
||||
<!-- 通话记录列表 -->
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvCallHistory"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingVertical="8dp" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</LinearLayout>
|
||||
126
android-app/app/src/main/res/layout/activity_incoming_call.xml
Normal file
126
android-app/app/src/main/res/layout/activity_incoming_call.xml
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#1A1A2E">
|
||||
|
||||
<!-- 背景模糊头像 -->
|
||||
<ImageView
|
||||
android:id="@+id/ivBackgroundAvatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:alpha="0.3" />
|
||||
|
||||
<!-- 主内容 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<!-- 顶部提示 -->
|
||||
<TextView
|
||||
android:id="@+id/tvCallType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="100dp"
|
||||
android:text="语音来电"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/ivAvatar"
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_marginTop="40dp"
|
||||
android:src="@drawable/ic_default_avatar"
|
||||
app:civ_border_color="#FFFFFF"
|
||||
app:civ_border_width="3dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUserName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="用户名"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="邀请你进行通话"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<!-- 占位 -->
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="80dp">
|
||||
|
||||
<!-- 拒绝按钮 -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginHorizontal="40dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnReject"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:background="@drawable/bg_hangup_button"
|
||||
android:src="@drawable/ic_call_end"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="拒绝"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 接听按钮 -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginHorizontal="40dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnAccept"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:background="@drawable/bg_accept_button"
|
||||
android:src="@drawable/ic_call"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="接听"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
104
android-app/app/src/main/res/layout/item_call_record.xml
Normal file
104
android-app/app/src/main/res/layout/item_call_record.xml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<!-- 头像 -->
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/ivAvatar"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_default_avatar" />
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUserName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="用户名"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<!-- 通话类型图标 -->
|
||||
<ImageView
|
||||
android:id="@+id/ivCallType"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:src="@drawable/ic_call"
|
||||
app:tint="@color/text_secondary" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<!-- 呼入/呼出图标 -->
|
||||
<ImageView
|
||||
android:id="@+id/ivDirection"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:src="@drawable/ic_call_made"
|
||||
app:tint="@color/text_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="已接通 · 5分钟"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 时间和操作 -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="10:30"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnCall"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_call"
|
||||
app:tint="@color/primary" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
305
android-app/接口文档.md
Normal file
305
android-app/接口文档.md
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
# 直播APP接口文档
|
||||
|
||||
> 后端服务地址: `http://localhost:8081`
|
||||
> 所有接口需要在Header中携带 `Authori-zation: {token}` (登录后获取)
|
||||
|
||||
---
|
||||
|
||||
## 一、用户认证
|
||||
|
||||
### 1.1 账号密码登录
|
||||
- **POST** `/api/front/login`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"account": "手机号",
|
||||
"password": "密码"
|
||||
}
|
||||
```
|
||||
- **响应**: `token`, `uid`, `nikeName`, `phone`
|
||||
|
||||
### 1.2 用户注册
|
||||
- **POST** `/api/front/register`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"phone": "手机号",
|
||||
"password": "密码",
|
||||
"verificationCode": "验证码",
|
||||
"nickname": "昵称"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 发送验证码
|
||||
- **POST** `/api/front/sendCode`
|
||||
- **参数**: `phone` (表单)
|
||||
|
||||
### 1.4 退出登录
|
||||
- **GET** `/api/front/logout`
|
||||
|
||||
---
|
||||
|
||||
## 二、用户信息
|
||||
|
||||
### 2.1 获取用户信息
|
||||
- **GET** `/api/front/user`
|
||||
- **响应**: `nickname`, `avatar`, `phone`, `nowMoney`, `integral`, `level`, `vip` 等
|
||||
|
||||
### 2.2 修改用户资料
|
||||
- **POST** `/api/front/user/edit`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"nickname": "昵称",
|
||||
"avatar": "头像URL"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、直播间
|
||||
|
||||
### 3.1 获取直播间列表
|
||||
- **GET** `/api/front/live/public/rooms`
|
||||
- **响应**: 直播中的房间列表
|
||||
|
||||
### 3.2 创建直播间
|
||||
- **POST** `/api/front/live/rooms`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"title": "直播标题",
|
||||
"streamerName": "主播名称"
|
||||
}
|
||||
```
|
||||
- **响应**: 房间信息 + 推流地址
|
||||
|
||||
### 3.3 获取直播间详情
|
||||
- **GET** `/api/front/live/public/rooms/{id}`
|
||||
|
||||
### 3.4 删除直播间
|
||||
- **DELETE** `/api/front/live/rooms/{id}`
|
||||
|
||||
### 3.5 获取观看人数
|
||||
- **GET** `/api/front/live/public/rooms/{roomId}/viewers/count`
|
||||
|
||||
### 3.6 关注/取消关注主播
|
||||
- **POST** `/api/front/live/follow`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"streamerId": 123,
|
||||
"action": "follow"
|
||||
}
|
||||
```
|
||||
> action 可选值: "follow" 或 "unfollow"
|
||||
|
||||
---
|
||||
|
||||
## 四、直播弹幕
|
||||
|
||||
### 4.1 获取弹幕消息
|
||||
- **GET** `/api/front/live/public/rooms/{roomId}/messages`
|
||||
- **参数**: `limit` (数量限制)
|
||||
|
||||
### 4.2 发送弹幕
|
||||
- **POST** `/api/front/live/public/rooms/{roomId}/messages`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"message": "弹幕内容",
|
||||
"visitorId": "访客ID",
|
||||
"nickname": "昵称"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、礼物打赏
|
||||
|
||||
### 5.1 获取礼物列表
|
||||
- **GET** `/api/front/gift/list`
|
||||
- **响应**: `id`, `name`, `price`, `iconUrl`, `description`, `level`
|
||||
|
||||
### 5.2 获取用户余额
|
||||
- **GET** `/api/front/gift/balance`
|
||||
- **响应**: `coinBalance`
|
||||
|
||||
### 5.3 赠送礼物
|
||||
- **POST** `/api/front/gift/send`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"giftId": 1,
|
||||
"streamerId": 123,
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
- **响应**: `success`, `newBalance`, `message`
|
||||
|
||||
### 5.4 获取充值选项
|
||||
- **GET** `/api/front/gift/recharge/options`
|
||||
- **响应**: `id`, `coinAmount`, `price`, `discountLabel`
|
||||
|
||||
### 5.5 创建充值订单
|
||||
- **POST** `/api/front/gift/recharge/create`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"optionId": 1,
|
||||
"coinAmount": 100,
|
||||
"price": 10.00
|
||||
}
|
||||
```
|
||||
- **响应**: `orderId`, `paymentUrl`
|
||||
|
||||
---
|
||||
|
||||
## 六、私聊会话
|
||||
|
||||
### 6.1 获取会话列表
|
||||
- **GET** `/api/front/conversations`
|
||||
- **响应**: `id`, `title`, `lastMessage`, `timeText`, `unreadCount`, `avatarUrl`, `otherUserId`
|
||||
|
||||
### 6.2 搜索会话
|
||||
- **GET** `/api/front/conversations/search`
|
||||
- **参数**: `keyword`
|
||||
|
||||
### 6.3 获取/创建会话
|
||||
- **POST** `/api/front/conversations/with/{otherUserId}`
|
||||
- **响应**: `conversationId`
|
||||
|
||||
### 6.4 标记会话已读
|
||||
- **POST** `/api/front/conversations/{id}/read`
|
||||
|
||||
### 6.5 删除会话
|
||||
- **DELETE** `/api/front/conversations/{id}`
|
||||
|
||||
### 6.6 获取消息列表
|
||||
- **GET** `/api/front/conversations/{id}/messages`
|
||||
- **参数**: `page`, `pageSize`
|
||||
- **响应**: `messageId`, `userId`, `username`, `avatarUrl`, `message`, `timestamp`, `status`
|
||||
|
||||
### 6.7 发送私信
|
||||
- **POST** `/api/front/conversations/{id}/messages`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"message": "消息内容",
|
||||
"messageType": "text"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 删除消息
|
||||
- **DELETE** `/api/front/conversations/messages/{id}`
|
||||
|
||||
---
|
||||
|
||||
## 七、好友管理
|
||||
|
||||
### 7.1 获取好友列表
|
||||
- **GET** `/api/front/friends`
|
||||
- **参数**: `page`, `pageSize`
|
||||
- **响应**: `id`, `name`, `avatarUrl`, `phone`, `isOnline`, `lastOnlineTime`
|
||||
|
||||
### 7.2 删除好友
|
||||
- **DELETE** `/api/front/friends/{friendId}`
|
||||
|
||||
### 7.3 搜索用户
|
||||
- **GET** `/api/front/users/search`
|
||||
- **参数**: `keyword`, `page`, `pageSize`
|
||||
- **响应**: `id`, `nickname`, `phone`, `avatarUrl`, `friendStatus` (0=未添加, 1=已是好友, 2=已申请)
|
||||
|
||||
### 7.4 发送好友请求
|
||||
- **POST** `/api/front/friends/request`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"targetUserId": 123,
|
||||
"message": "请求消息"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.5 获取好友请求列表
|
||||
- **GET** `/api/front/friends/requests`
|
||||
- **参数**: `page`, `pageSize`
|
||||
|
||||
### 7.6 处理好友请求
|
||||
- **POST** `/api/front/friends/requests/{requestId}/handle`
|
||||
- **请求体**:
|
||||
```json
|
||||
{
|
||||
"accept": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、文件上传
|
||||
|
||||
### 8.1 上传图片
|
||||
- **POST** `/api/front/user/upload/image`
|
||||
- **参数**:
|
||||
- `multipart` - 图片文件
|
||||
- `model` - 模块 (user/product/wechat/news)
|
||||
- `pid` - 分类ID (7=前台用户头像)
|
||||
- **响应**: `url`, `fileName`, `type`
|
||||
|
||||
---
|
||||
|
||||
## 九、在线状态
|
||||
|
||||
### 9.1 检查用户在线状态
|
||||
- **GET** `/api/front/online/status/{userId}`
|
||||
- **响应**: `userId`, `online`, `lastActiveTime`
|
||||
|
||||
### 9.2 批量检查在线状态
|
||||
- **POST** `/api/front/online/status/batch`
|
||||
- **请求体**: `[userId1, userId2, ...]`
|
||||
- **响应**: `total`, `onlineCount`, `onlineUsers`
|
||||
|
||||
### 9.3 获取直播间在线人数
|
||||
- **GET** `/api/front/online/room/{roomId}/count`
|
||||
- **响应**: `roomId`, `count`
|
||||
|
||||
### 9.4 获取直播间在线用户列表
|
||||
- **GET** `/api/front/online/room/{roomId}/users`
|
||||
- **响应**: `roomId`, `count`, `users`
|
||||
|
||||
### 9.5 获取WebSocket连接统计
|
||||
- **GET** `/api/front/online/stats`
|
||||
- **响应**: `activeConnections`, `timestamp`
|
||||
|
||||
---
|
||||
|
||||
## 十、离线消息
|
||||
|
||||
### 10.1 获取离线消息数量
|
||||
- **GET** `/api/front/online/offline/count/{userId}`
|
||||
- **响应**: `userId`, `count`
|
||||
|
||||
### 10.2 获取离线消息列表
|
||||
- **GET** `/api/front/online/offline/messages/{userId}`
|
||||
- **参数**: `limit` (默认50)
|
||||
- **响应**: `userId`, `messages`, `count`, `totalCount`
|
||||
|
||||
### 10.3 清除离线消息
|
||||
- **DELETE** `/api/front/online/offline/messages/{userId}`
|
||||
|
||||
---
|
||||
|
||||
## 接口统计
|
||||
|
||||
| 模块 | 接口数量 |
|
||||
|------|----------|
|
||||
| 用户认证 | 4 |
|
||||
| 用户信息 | 2 |
|
||||
| 直播间 | 6 |
|
||||
| 直播弹幕 | 2 |
|
||||
| 礼物打赏 | 5 |
|
||||
| 私聊会话 | 8 |
|
||||
| 好友管理 | 6 |
|
||||
| 文件上传 | 1 |
|
||||
| 在线状态 | 5 |
|
||||
| 离线消息 | 3 |
|
||||
| **总计** | **42** |
|
||||
113
工作日志-接口对接.md
Normal file
113
工作日志-接口对接.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# 工作日志 - 前后端接口对接
|
||||
|
||||
> 日期: 2024年12月26日
|
||||
|
||||
---
|
||||
|
||||
## 一、完成的工作
|
||||
|
||||
### 1. Android端接口扩展
|
||||
|
||||
将 `ApiService.java` 从原来的 6 个接口扩展到 42 个接口,覆盖以下模块:
|
||||
|
||||
- 用户认证(4个)
|
||||
- 用户信息(2个)
|
||||
- 直播间(6个)
|
||||
- 直播弹幕(2个)
|
||||
- 礼物打赏(5个)
|
||||
- 私聊会话(8个)
|
||||
- 好友管理(6个)
|
||||
- 文件上传(1个)
|
||||
- 在线状态(5个)
|
||||
- 离线消息(3个)
|
||||
|
||||
### 2. 新增数据模型类(17个)
|
||||
|
||||
| 文件名 | 说明 |
|
||||
|--------|------|
|
||||
| UserInfoResponse.java | 用户信息响应 |
|
||||
| UserEditRequest.java | 修改用户资料请求 |
|
||||
| GiftResponse.java | 礼物信息 |
|
||||
| UserBalanceResponse.java | 用户余额 |
|
||||
| SendGiftRequest.java | 赠送礼物请求 |
|
||||
| SendGiftResponse.java | 赠送礼物响应 |
|
||||
| RechargeOptionResponse.java | 充值选项 |
|
||||
| CreateRechargeRequest.java | 创建充值订单请求 |
|
||||
| CreateRechargeResponse.java | 创建充值订单响应 |
|
||||
| ConversationResponse.java | 会话信息 |
|
||||
| PrivateMessageResponse.java | 私信消息 |
|
||||
| SendMessageRequest.java | 发送消息请求 |
|
||||
| FriendResponse.java | 好友信息 |
|
||||
| FriendRequestResponse.java | 好友请求 |
|
||||
| SearchUserResponse.java | 搜索用户结果 |
|
||||
| PageResponse.java | 分页响应 |
|
||||
| FileUploadResponse.java | 文件上传响应 |
|
||||
|
||||
### 3. 字段匹配修复
|
||||
|
||||
检查并修复了 Android 端数据模型与后端返回字段的匹配问题:
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| ConversationResponse | 字段改为 id, title, lastMessage, timeText, unreadCount, muted, avatarUrl, otherUserId |
|
||||
| PrivateMessageResponse | 字段改为 messageId, userId, username, avatarUrl, message, timestamp, status, isSystemMessage |
|
||||
| SendMessageRequest | 字段改为 message, messageType |
|
||||
| CreateRechargeRequest | 字段改为 optionId, coinAmount, price |
|
||||
| UserInfoResponse | 匹配后端 UserCenterResponse |
|
||||
| UserEditRequest | 简化为 nickname, avatar |
|
||||
|
||||
### 4. Git配置修改
|
||||
|
||||
修改 `.gitignore`,注释掉 `**/*.jks` 和 `**/*.keystore`,允许签名文件上传到公司仓库。
|
||||
|
||||
### 5. 文档编写
|
||||
|
||||
- `zhibo/android-app/接口文档.md` - 完整的接口说明文档
|
||||
- `zhibo/接口对接状态.md` - 三端接口对接状态表格
|
||||
|
||||
---
|
||||
|
||||
## 二、接口对接状态
|
||||
|
||||
| 端 | 已打通 | 未打通 | 完成度 |
|
||||
|----|--------|--------|--------|
|
||||
| Android ↔ 后端 | 42 | 0 | 100% |
|
||||
| Vue Admin ↔ 后端 | 20+ | 0 | 100% |
|
||||
|
||||
---
|
||||
|
||||
## 三、待完成事项
|
||||
|
||||
1. 支付SDK集成(微信/支付宝)
|
||||
|
||||
---
|
||||
|
||||
## 四、修改的文件清单
|
||||
|
||||
```
|
||||
zhibo/
|
||||
├── .gitignore # 修改:注释jks规则
|
||||
├── 接口对接状态.md # 新增
|
||||
├── 工作日志-接口对接.md # 新增
|
||||
└── android-app/
|
||||
├── 接口文档.md # 新增
|
||||
└── app/src/main/java/.../net/
|
||||
├── ApiService.java # 修改:扩展接口
|
||||
├── ConversationResponse.java # 修改:字段匹配
|
||||
├── PrivateMessageResponse.java # 修改:字段匹配
|
||||
├── SendMessageRequest.java # 修改:字段匹配
|
||||
├── CreateRechargeRequest.java # 修改:字段匹配
|
||||
├── UserInfoResponse.java # 修改:字段匹配
|
||||
├── UserEditRequest.java # 修改:字段匹配
|
||||
├── GiftResponse.java # 新增
|
||||
├── UserBalanceResponse.java # 新增
|
||||
├── SendGiftRequest.java # 新增
|
||||
├── SendGiftResponse.java # 新增
|
||||
├── RechargeOptionResponse.java # 新增
|
||||
├── CreateRechargeResponse.java # 新增
|
||||
├── FriendResponse.java # 新增
|
||||
├── FriendRequestResponse.java # 新增
|
||||
├── SearchUserResponse.java # 新增
|
||||
├── PageResponse.java # 新增
|
||||
└── FileUploadResponse.java # 新增
|
||||
```
|
||||
132
接口对接状态.md
Normal file
132
接口对接状态.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# 接口对接状态总览
|
||||
|
||||
> 更新时间: 2024年
|
||||
|
||||
## 端口配置
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| crmeb-front | 8081 | Android/H5 API |
|
||||
| crmeb-admin | 30001 | Vue Admin API |
|
||||
| Vue Admin | 9527 | 管理后台前端 |
|
||||
| SRS RTMP | 25002 | 推流端口 |
|
||||
| SRS HTTP | 25003 | 播放端口 |
|
||||
|
||||
---
|
||||
|
||||
## 一、Android端 ↔ 后端 (crmeb-front)
|
||||
|
||||
### ✅ 已打通 (42个)
|
||||
|
||||
| 模块 | 接口 | Android | 后端 | 状态 |
|
||||
|------|------|---------|------|------|
|
||||
| **用户认证** | 登录 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 注册 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 发送验证码 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 退出登录 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **用户信息** | 获取用户信息 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 修改用户资料 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **直播间** | 获取直播间列表 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 创建直播间 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 获取直播间详情 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 删除直播间 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 获取观看人数 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 关注主播 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **直播弹幕** | 获取弹幕消息 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 发送弹幕 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **礼物打赏** | 获取礼物列表 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 获取用户余额 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 赠送礼物 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 获取充值选项 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 创建充值订单 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **私聊会话** | 获取会话列表 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 搜索会话 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 获取/创建会话 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 标记已读 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 删除会话 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 获取消息列表 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 发送私信 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 删除消息 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **好友管理** | 获取好友列表 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 删除好友 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 搜索用户 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 发送好友请求 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 获取好友请求 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 处理好友请求 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **文件上传** | 上传图片 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **在线状态** | 检查用户在线 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 批量检查在线 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 直播间在线人数 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 直播间在线用户 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | WebSocket统计 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **离线消息** | 获取离线消息数量 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 获取离线消息 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 清除离线消息 | ✅ | ✅ | ✅ 已打通 |
|
||||
|
||||
### ⏳ 未打通 (后端有,Android未定义)
|
||||
|
||||
| 模块 | 接口 | Android | 后端 | 说明 |
|
||||
|------|------|---------|------|------|
|
||||
| 无 | - | - | - | 全部已打通 |
|
||||
|
||||
---
|
||||
|
||||
## 二、Vue Admin前端 ↔ 后端 (crmeb-admin)
|
||||
|
||||
### ✅ 已打通
|
||||
|
||||
| 模块 | 功能 | Vue Admin | 后端 | 状态 |
|
||||
|------|------|-----------|------|------|
|
||||
| **用户管理** | 用户列表 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 用户详情 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 用户编辑 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 用户等级 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 用户分组 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 用户标签 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **直播间管理** | 直播间列表 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 创建直播间 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 开播/停播 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 弹幕记录 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 房间类型 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 房间背景 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **礼物管理** | 礼物列表 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 打赏记录 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **财务管理** | 充值记录 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 提现管理 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 账单明细 | ✅ | ✅ | ✅ 已打通 |
|
||||
| **系统设置** | 管理员 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 角色权限 | ✅ | ✅ | ✅ 已打通 |
|
||||
| | 系统配置 | ✅ | ✅ | ✅ 已打通 |
|
||||
|
||||
---
|
||||
|
||||
## 三、数据流向
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Android │────▶│ crmeb-front │────▶│ │
|
||||
│ APP │ │ :8081 │ │ │
|
||||
└─────────────┘ └─────────────┘ │ │
|
||||
│ MySQL │
|
||||
┌─────────────┐ ┌─────────────┐ │ 数据库 │
|
||||
│ Vue Admin │────▶│ crmeb-admin │────▶│ │
|
||||
│ :9527 │ │ :30001 │ │ │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、统计
|
||||
|
||||
| 端 | 已打通 | 未打通 | 完成度 |
|
||||
|----|--------|--------|--------|
|
||||
| Android ↔ 后端 | 42 | 0 | 100% |
|
||||
| Vue Admin ↔ 后端 | 20+ | 0 | 100% |
|
||||
|
||||
---
|
||||
|
||||
## 五、待完成事项
|
||||
|
||||
1. ~~图片上传接口(用户头像)~~ ✅ 已完成
|
||||
2. ~~在线状态接口~~ ✅ 已完成
|
||||
3. 支付SDK集成(微信/支付宝)
|
||||
Loading…
Reference in New Issue
Block a user