171
android-app/README.md
Archivo normal
171
android-app/README.md
Archivo normal
@@ -0,0 +1,171 @@
|
||||
# ChatRTC - Android WebRTC Chat Application
|
||||
|
||||
A modern Android chat application with WebRTC support for audio/video calling and emoji text messaging.
|
||||
|
||||
## Features
|
||||
|
||||
- **Nickname-based joining**: Users only need to enter their nickname to join the chat
|
||||
- **Text messaging with emoji support**: Full emoji keyboard and rendering support
|
||||
- **Audio/Video calling**: WebRTC-powered audio and video communication
|
||||
- **Modern UI**: Material Design components with beautiful chat bubbles
|
||||
- **Camera controls**: Toggle video, mute audio, and switch between front/back cameras
|
||||
- **Real-time communication**: Instant messaging and WebRTC peer-to-peer connections
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
chatrtc/
|
||||
├── app/ # Android application
|
||||
│ ├── src/main/java/com/chatrtc/app/
|
||||
│ │ ├── MainActivity.java # Nickname entry screen
|
||||
│ │ ├── ChatActivity.java # Main chat interface
|
||||
│ │ ├── adapter/
|
||||
│ │ │ └── ChatAdapter.java # RecyclerView adapter for messages
|
||||
│ │ ├── model/
|
||||
│ │ │ └── ChatMessage.java # Message data model
|
||||
│ │ └── webrtc/
|
||||
│ │ └── WebRTCManager.java # WebRTC connection management
|
||||
│ └── src/main/res/ # Android resources
|
||||
└── signaling-server/ # Node.js signaling server
|
||||
├── server.js # Socket.IO signaling server
|
||||
└── package.json # Server dependencies
|
||||
```
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Android App Setup
|
||||
|
||||
1. **Prerequisites**:
|
||||
- Android Studio Arctic Fox or later
|
||||
- JDK 8 or later
|
||||
- Android SDK API 24+ (Android 7.0)
|
||||
|
||||
2. **Open the project**:
|
||||
```bash
|
||||
cd chatrtc
|
||||
# Open in Android Studio
|
||||
```
|
||||
|
||||
3. **Configure signaling server URL**:
|
||||
- Open `app/src/main/java/com/chatrtc/app/webrtc/WebRTCManager.java`
|
||||
- Update the `SIGNALING_SERVER_URL` constant with your server URL:
|
||||
```java
|
||||
private static final String SIGNALING_SERVER_URL = "https://your-server.com";
|
||||
// For local development: "http://10.0.2.2:3000"
|
||||
```
|
||||
|
||||
4. **Build and run**:
|
||||
- Connect your Android device or start an emulator
|
||||
- Click "Run" in Android Studio
|
||||
|
||||
### 2. Signaling Server Setup
|
||||
|
||||
1. **Install Node.js**: Download from [nodejs.org](https://nodejs.org/)
|
||||
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
cd signaling-server
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start the server**:
|
||||
```bash
|
||||
npm start
|
||||
# or for development with auto-restart:
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. **Server will run on**: `http://localhost:3000`
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Start the signaling server** (see setup instructions above)
|
||||
2. **Install and run the Android app** on your device(s)
|
||||
3. **Enter your nickname** on the welcome screen
|
||||
4. **Grant permissions** for camera and microphone when prompted
|
||||
5. **Start chatting**:
|
||||
- Send text messages with emoji support
|
||||
- Toggle video/audio using the control buttons
|
||||
- Switch cameras using the camera switch button
|
||||
- Multiple users can join the same chat room
|
||||
|
||||
## Key Components
|
||||
|
||||
### Android App
|
||||
|
||||
- **MainActivity**: Handles nickname entry and permissions
|
||||
- **ChatActivity**: Main chat interface with video views and message list
|
||||
- **WebRTCManager**: Manages WebRTC connections, media streams, and signaling
|
||||
- **ChatAdapter**: Handles different message types (own, other, system)
|
||||
- **Emoji Support**: Uses Vanniktech emoji library for full emoji rendering
|
||||
|
||||
### WebRTC Features
|
||||
|
||||
- **Peer-to-peer connections**: Direct audio/video streams between users
|
||||
- **Data channels**: For text message transmission
|
||||
- **STUN servers**: Google's STUN servers for NAT traversal
|
||||
- **Camera management**: Front/back camera switching
|
||||
- **Media controls**: Toggle audio/video streams
|
||||
|
||||
### Signaling Server
|
||||
|
||||
- **Socket.IO based**: Real-time bidirectional communication
|
||||
- **Room management**: Users join a common chat room
|
||||
- **WebRTC signaling**: Handles offer/answer/ICE candidate exchange
|
||||
- **User management**: Tracks connected users and broadcasts join/leave events
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Android
|
||||
- **WebRTC**: `org.webrtc:google-webrtc:1.0.32006`
|
||||
- **Socket.IO**: `io.socket:socket.io-client:2.0.1`
|
||||
- **Emoji Support**: `com.vanniktech:emoji-google:0.16.0`
|
||||
- **Material Design**: `com.google.android.material:material:1.9.0`
|
||||
|
||||
### Server
|
||||
- **Express**: Web server framework
|
||||
- **Socket.IO**: Real-time communication
|
||||
- **CORS**: Cross-origin resource sharing
|
||||
|
||||
## Network Configuration
|
||||
|
||||
For local development, the app includes network security configuration to allow HTTP connections to localhost and common local IP addresses.
|
||||
|
||||
## Permissions
|
||||
|
||||
The app requires:
|
||||
- `CAMERA`: For video calling
|
||||
- `RECORD_AUDIO`: For audio calling
|
||||
- `MODIFY_AUDIO_SETTINGS`: For audio management
|
||||
- `INTERNET`: For signaling server connection
|
||||
- `ACCESS_NETWORK_STATE`: For network status
|
||||
|
||||
## Deployment
|
||||
|
||||
### Android App
|
||||
- Build APK: `./gradlew assembleDebug`
|
||||
- Install: `adb install app/build/outputs/apk/debug/app-debug.apk`
|
||||
|
||||
### Signaling Server
|
||||
- Deploy to any Node.js hosting service (Heroku, Railway, etc.)
|
||||
- Update the `SIGNALING_SERVER_URL` in the Android app
|
||||
- Ensure HTTPS is used for production (required by WebRTC)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Connection issues**: Check signaling server URL and network connectivity
|
||||
2. **Permission errors**: Ensure camera/microphone permissions are granted
|
||||
3. **Video not showing**: Check camera availability and permissions
|
||||
4. **Audio not working**: Verify microphone permissions and device audio settings
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is open source and available under the MIT License.
|
||||
61
android-app/app/build.gradle
Archivo normal
61
android-app/app/build.gradle
Archivo normal
@@ -0,0 +1,61 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.chatrtc.app'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.chatrtc.app"
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||
|
||||
// WebRTC
|
||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||
|
||||
// Socket.IO for signaling
|
||||
implementation 'io.socket:socket.io-client:2.0.1'
|
||||
|
||||
// JSON parsing
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
|
||||
// Permissions
|
||||
implementation 'androidx.activity:activity:1.7.2'
|
||||
implementation 'androidx.fragment:fragment:1.6.1'
|
||||
|
||||
// Emoji support
|
||||
implementation 'com.vanniktech:emoji-java:0.16.0'
|
||||
implementation 'com.vanniktech:emoji-google:0.16.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
21
android-app/app/proguard-rules.pro
vendido
Archivo normal
21
android-app/app/proguard-rules.pro
vendido
Archivo normal
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
45
android-app/app/src/main/AndroidManifest.xml
Archivo normal
45
android-app/app/src/main/AndroidManifest.xml
Archivo normal
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ChatRTC"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ChatActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
229
android-app/app/src/main/java/com/chatrtc/app/ChatActivity.java
Archivo normal
229
android-app/app/src/main/java/com/chatrtc/app/ChatActivity.java
Archivo normal
@@ -0,0 +1,229 @@
|
||||
package com.chatrtc.app;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.chatrtc.app.adapter.ChatAdapter;
|
||||
import com.chatrtc.app.model.ChatMessage;
|
||||
import com.chatrtc.app.webrtc.WebRTCManager;
|
||||
import com.vanniktech.emoji.EmojiManager;
|
||||
import com.vanniktech.emoji.EmojiPopup;
|
||||
import com.vanniktech.emoji.google.GoogleEmojiProvider;
|
||||
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class ChatActivity extends AppCompatActivity implements WebRTCManager.WebRTCListener {
|
||||
|
||||
private static final String TAG = "ChatActivity";
|
||||
|
||||
private RecyclerView rvChat;
|
||||
private EditText etMessage;
|
||||
private Button btnSend;
|
||||
private ImageButton btnEmoji;
|
||||
private Button btnToggleVideo;
|
||||
private Button btnToggleAudio;
|
||||
private Button btnSwitchCamera;
|
||||
private SurfaceViewRenderer localVideoView;
|
||||
private SurfaceViewRenderer remoteVideoView;
|
||||
|
||||
private ChatAdapter chatAdapter;
|
||||
private List<ChatMessage> chatMessages;
|
||||
private String nickname;
|
||||
private WebRTCManager webRTCManager;
|
||||
private EmojiPopup emojiPopup;
|
||||
|
||||
private boolean isVideoEnabled = true;
|
||||
private boolean isAudioEnabled = true;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Initialize emoji support
|
||||
EmojiManager.install(new GoogleEmojiProvider());
|
||||
|
||||
setContentView(R.layout.activity_chat);
|
||||
|
||||
// Get nickname from intent
|
||||
nickname = getIntent().getStringExtra("nickname");
|
||||
if (TextUtils.isEmpty(nickname)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
initViews();
|
||||
setupRecyclerView();
|
||||
setupClickListeners();
|
||||
setupEmojiPopup();
|
||||
|
||||
// Initialize WebRTC
|
||||
webRTCManager = new WebRTCManager(this, this);
|
||||
webRTCManager.initializeWebRTC(localVideoView, remoteVideoView);
|
||||
webRTCManager.connectToSignalingServer();
|
||||
|
||||
// Join chat room
|
||||
webRTCManager.joinRoom(nickname);
|
||||
|
||||
// Add welcome message
|
||||
addSystemMessage("Welcome to the chat, " + nickname + "!");
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
rvChat = findViewById(R.id.rv_chat);
|
||||
etMessage = findViewById(R.id.et_message);
|
||||
btnSend = findViewById(R.id.btn_send);
|
||||
btnEmoji = findViewById(R.id.btn_emoji);
|
||||
btnToggleVideo = findViewById(R.id.btn_toggle_video);
|
||||
btnToggleAudio = findViewById(R.id.btn_toggle_audio);
|
||||
btnSwitchCamera = findViewById(R.id.btn_switch_camera);
|
||||
localVideoView = findViewById(R.id.local_video_view);
|
||||
remoteVideoView = findViewById(R.id.remote_video_view);
|
||||
|
||||
setTitle("Chat - " + nickname);
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
chatMessages = new ArrayList<>();
|
||||
chatAdapter = new ChatAdapter(chatMessages);
|
||||
rvChat.setLayoutManager(new LinearLayoutManager(this));
|
||||
rvChat.setAdapter(chatAdapter);
|
||||
}
|
||||
|
||||
private void setupClickListeners() {
|
||||
btnSend.setOnClickListener(v -> sendMessage());
|
||||
|
||||
btnToggleVideo.setOnClickListener(v -> toggleVideo());
|
||||
|
||||
btnToggleAudio.setOnClickListener(v -> toggleAudio());
|
||||
|
||||
btnSwitchCamera.setOnClickListener(v -> webRTCManager.switchCamera());
|
||||
|
||||
etMessage.setOnEditorActionListener((v, actionId, event) -> {
|
||||
sendMessage();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void setupEmojiPopup() {
|
||||
emojiPopup = EmojiPopup.Builder.fromRootView(findViewById(android.R.id.content))
|
||||
.setOnEmojiPopupShownListener(() -> btnEmoji.setImageResource(R.drawable.ic_keyboard))
|
||||
.setOnEmojiPopupDismissListener(() -> btnEmoji.setImageResource(R.drawable.ic_emoji))
|
||||
.build(etMessage);
|
||||
|
||||
btnEmoji.setOnClickListener(v -> emojiPopup.toggle());
|
||||
}
|
||||
|
||||
private void sendMessage() {
|
||||
String message = etMessage.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMessage chatMessage = new ChatMessage(nickname, message, new Date(), true);
|
||||
addMessage(chatMessage);
|
||||
|
||||
// Send message through WebRTC data channel
|
||||
webRTCManager.sendMessage(message);
|
||||
|
||||
etMessage.setText("");
|
||||
}
|
||||
|
||||
private void addMessage(ChatMessage message) {
|
||||
chatMessages.add(message);
|
||||
chatAdapter.notifyItemInserted(chatMessages.size() - 1);
|
||||
rvChat.smoothScrollToPosition(chatMessages.size() - 1);
|
||||
}
|
||||
|
||||
private void addSystemMessage(String message) {
|
||||
ChatMessage systemMessage = new ChatMessage("System", message, new Date(), false);
|
||||
systemMessage.setSystemMessage(true);
|
||||
addMessage(systemMessage);
|
||||
}
|
||||
|
||||
private void toggleVideo() {
|
||||
isVideoEnabled = !isVideoEnabled;
|
||||
webRTCManager.toggleVideo(isVideoEnabled);
|
||||
btnToggleVideo.setText(isVideoEnabled ? "Video Off" : "Video On");
|
||||
btnToggleVideo.setBackgroundColor(getResources().getColor(
|
||||
isVideoEnabled ? R.color.button_enabled : R.color.button_disabled));
|
||||
}
|
||||
|
||||
private void toggleAudio() {
|
||||
isAudioEnabled = !isAudioEnabled;
|
||||
webRTCManager.toggleAudio(isAudioEnabled);
|
||||
btnToggleAudio.setText(isAudioEnabled ? "Mute" : "Unmute");
|
||||
btnToggleAudio.setBackgroundColor(getResources().getColor(
|
||||
isAudioEnabled ? R.color.button_enabled : R.color.button_disabled));
|
||||
}
|
||||
|
||||
// WebRTCListener implementation
|
||||
@Override
|
||||
public void onMessageReceived(String senderNickname, String message) {
|
||||
runOnUiThread(() -> {
|
||||
ChatMessage chatMessage = new ChatMessage(senderNickname, message, new Date(), false);
|
||||
addMessage(chatMessage);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserJoined(String userNickname) {
|
||||
runOnUiThread(() -> addSystemMessage(userNickname + " joined the chat"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserLeft(String userNickname) {
|
||||
runOnUiThread(() -> addSystemMessage(userNickname + " left the chat"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected() {
|
||||
runOnUiThread(() -> addSystemMessage("Connected to chat"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
runOnUiThread(() -> addSystemMessage("Disconnected from chat"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
runOnUiThread(() -> {
|
||||
Log.e(TAG, "WebRTC Error: " + error);
|
||||
Toast.makeText(this, "Error: " + error, Toast.LENGTH_LONG).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (webRTCManager != null) {
|
||||
webRTCManager.cleanup();
|
||||
}
|
||||
if (emojiPopup != null && emojiPopup.isShowing()) {
|
||||
emojiPopup.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (emojiPopup != null && emojiPopup.isShowing()) {
|
||||
emojiPopup.dismiss();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
99
android-app/app/src/main/java/com/chatrtc/app/MainActivity.java
Archivo normal
99
android-app/app/src/main/java/com/chatrtc/app/MainActivity.java
Archivo normal
@@ -0,0 +1,99 @@
|
||||
package com.chatrtc.app;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final int PERMISSIONS_REQUEST_CODE = 100;
|
||||
private static final String[] REQUIRED_PERMISSIONS = {
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
Manifest.permission.MODIFY_AUDIO_SETTINGS
|
||||
};
|
||||
|
||||
private EditText etNickname;
|
||||
private Button btnJoinChat;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
initViews();
|
||||
setupClickListeners();
|
||||
|
||||
// Check permissions
|
||||
if (!hasPermissions()) {
|
||||
requestPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
etNickname = findViewById(R.id.et_nickname);
|
||||
btnJoinChat = findViewById(R.id.btn_join_chat);
|
||||
}
|
||||
|
||||
private void setupClickListeners() {
|
||||
btnJoinChat.setOnClickListener(v -> {
|
||||
String nickname = etNickname.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(nickname)) {
|
||||
Toast.makeText(this, "Please enter your nickname", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasPermissions()) {
|
||||
requestPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start chat activity
|
||||
Intent intent = new Intent(this, ChatActivity.class);
|
||||
intent.putExtra("nickname", nickname);
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean hasPermissions() {
|
||||
for (String permission : REQUIRED_PERMISSIONS) {
|
||||
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void requestPermissions() {
|
||||
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, PERMISSIONS_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == PERMISSIONS_REQUEST_CODE) {
|
||||
boolean allGranted = true;
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
allGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allGranted) {
|
||||
Toast.makeText(this, "Permissions are required for audio/video chat", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
android-app/app/src/main/java/com/chatrtc/app/adapter/ChatAdapter.java
Archivo normal
127
android-app/app/src/main/java/com/chatrtc/app/adapter/ChatAdapter.java
Archivo normal
@@ -0,0 +1,127 @@
|
||||
package com.chatrtc.app.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.chatrtc.app.R;
|
||||
import com.chatrtc.app.model.ChatMessage;
|
||||
import com.vanniktech.emoji.EmojiTextView;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ChatAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int VIEW_TYPE_OWN_MESSAGE = 1;
|
||||
private static final int VIEW_TYPE_OTHER_MESSAGE = 2;
|
||||
private static final int VIEW_TYPE_SYSTEM_MESSAGE = 3;
|
||||
|
||||
private List<ChatMessage> messages;
|
||||
private SimpleDateFormat timeFormat;
|
||||
|
||||
public ChatAdapter(List<ChatMessage> messages) {
|
||||
this.messages = messages;
|
||||
this.timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
ChatMessage message = messages.get(position);
|
||||
if (message.isSystemMessage()) {
|
||||
return VIEW_TYPE_SYSTEM_MESSAGE;
|
||||
} else if (message.isOwnMessage()) {
|
||||
return VIEW_TYPE_OWN_MESSAGE;
|
||||
} else {
|
||||
return VIEW_TYPE_OTHER_MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_OWN_MESSAGE:
|
||||
return new OwnMessageViewHolder(inflater.inflate(R.layout.item_own_message, parent, false));
|
||||
case VIEW_TYPE_OTHER_MESSAGE:
|
||||
return new OtherMessageViewHolder(inflater.inflate(R.layout.item_other_message, parent, false));
|
||||
case VIEW_TYPE_SYSTEM_MESSAGE:
|
||||
return new SystemMessageViewHolder(inflater.inflate(R.layout.item_system_message, parent, false));
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown view type: " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
ChatMessage message = messages.get(position);
|
||||
|
||||
if (holder instanceof OwnMessageViewHolder) {
|
||||
((OwnMessageViewHolder) holder).bind(message);
|
||||
} else if (holder instanceof OtherMessageViewHolder) {
|
||||
((OtherMessageViewHolder) holder).bind(message);
|
||||
} else if (holder instanceof SystemMessageViewHolder) {
|
||||
((SystemMessageViewHolder) holder).bind(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return messages.size();
|
||||
}
|
||||
|
||||
class OwnMessageViewHolder extends RecyclerView.ViewHolder {
|
||||
private EmojiTextView tvMessage;
|
||||
private TextView tvTime;
|
||||
|
||||
public OwnMessageViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvMessage = itemView.findViewById(R.id.tv_message);
|
||||
tvTime = itemView.findViewById(R.id.tv_time);
|
||||
}
|
||||
|
||||
public void bind(ChatMessage message) {
|
||||
tvMessage.setText(message.getMessage());
|
||||
tvTime.setText(timeFormat.format(message.getTimestamp()));
|
||||
}
|
||||
}
|
||||
|
||||
class OtherMessageViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView tvNickname;
|
||||
private EmojiTextView tvMessage;
|
||||
private TextView tvTime;
|
||||
|
||||
public OtherMessageViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvNickname = itemView.findViewById(R.id.tv_nickname);
|
||||
tvMessage = itemView.findViewById(R.id.tv_message);
|
||||
tvTime = itemView.findViewById(R.id.tv_time);
|
||||
}
|
||||
|
||||
public void bind(ChatMessage message) {
|
||||
tvNickname.setText(message.getSenderNickname());
|
||||
tvMessage.setText(message.getMessage());
|
||||
tvTime.setText(timeFormat.format(message.getTimestamp()));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemMessageViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView tvMessage;
|
||||
|
||||
public SystemMessageViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvMessage = itemView.findViewById(R.id.tv_system_message);
|
||||
}
|
||||
|
||||
public void bind(ChatMessage message) {
|
||||
tvMessage.setText(message.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
59
android-app/app/src/main/java/com/chatrtc/app/model/ChatMessage.java
Archivo normal
59
android-app/app/src/main/java/com/chatrtc/app/model/ChatMessage.java
Archivo normal
@@ -0,0 +1,59 @@
|
||||
package com.chatrtc.app.model;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class ChatMessage {
|
||||
private String senderNickname;
|
||||
private String message;
|
||||
private Date timestamp;
|
||||
private boolean isOwnMessage;
|
||||
private boolean isSystemMessage;
|
||||
|
||||
public ChatMessage(String senderNickname, String message, Date timestamp, boolean isOwnMessage) {
|
||||
this.senderNickname = senderNickname;
|
||||
this.message = message;
|
||||
this.timestamp = timestamp;
|
||||
this.isOwnMessage = isOwnMessage;
|
||||
this.isSystemMessage = false;
|
||||
}
|
||||
|
||||
public String getSenderNickname() {
|
||||
return senderNickname;
|
||||
}
|
||||
|
||||
public void setSenderNickname(String senderNickname) {
|
||||
this.senderNickname = senderNickname;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Date timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public boolean isOwnMessage() {
|
||||
return isOwnMessage;
|
||||
}
|
||||
|
||||
public void setOwnMessage(boolean ownMessage) {
|
||||
isOwnMessage = ownMessage;
|
||||
}
|
||||
|
||||
public boolean isSystemMessage() {
|
||||
return isSystemMessage;
|
||||
}
|
||||
|
||||
public void setSystemMessage(boolean systemMessage) {
|
||||
isSystemMessage = systemMessage;
|
||||
}
|
||||
}
|
||||
490
android-app/app/src/main/java/com/chatrtc/app/webrtc/WebRTCManager.java
Archivo normal
490
android-app/app/src/main/java/com/chatrtc/app/webrtc/WebRTCManager.java
Archivo normal
@@ -0,0 +1,490 @@
|
||||
package com.chatrtc.app.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import org.webrtc.AudioSource;
|
||||
import org.webrtc.AudioTrack;
|
||||
import org.webrtc.Camera1Enumerator;
|
||||
import org.webrtc.Camera2Enumerator;
|
||||
import org.webrtc.CameraEnumerator;
|
||||
import org.webrtc.CameraVideoCapturer;
|
||||
import org.webrtc.DataChannel;
|
||||
import org.webrtc.DefaultVideoDecoderFactory;
|
||||
import org.webrtc.DefaultVideoEncoderFactory;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.MediaConstraints;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.SessionDescription;
|
||||
import org.webrtc.SurfaceTextureHelper;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoRenderer;
|
||||
import org.webrtc.VideoSource;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import io.socket.client.IO;
|
||||
import io.socket.client.Socket;
|
||||
|
||||
public class WebRTCManager {
|
||||
|
||||
private static final String TAG = "WebRTCManager";
|
||||
private static final String SIGNALING_SERVER_URL = "https://your-signaling-server.com"; // Replace with your server
|
||||
|
||||
public interface WebRTCListener {
|
||||
void onMessageReceived(String senderNickname, String message);
|
||||
void onUserJoined(String userNickname);
|
||||
void onUserLeft(String userNickname);
|
||||
void onConnected();
|
||||
void onDisconnected();
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
private Context context;
|
||||
private WebRTCListener listener;
|
||||
private Socket socket;
|
||||
private String nickname;
|
||||
|
||||
// WebRTC components
|
||||
private PeerConnectionFactory peerConnectionFactory;
|
||||
private PeerConnection peerConnection;
|
||||
private EglBase rootEglBase;
|
||||
private VideoCapturer videoCapturer;
|
||||
private VideoSource videoSource;
|
||||
private AudioSource audioSource;
|
||||
private VideoTrack localVideoTrack;
|
||||
private AudioTrack localAudioTrack;
|
||||
private DataChannel dataChannel;
|
||||
|
||||
// UI components
|
||||
private SurfaceViewRenderer localVideoView;
|
||||
private SurfaceViewRenderer remoteVideoView;
|
||||
|
||||
// State
|
||||
private boolean isVideoEnabled = true;
|
||||
private boolean isAudioEnabled = true;
|
||||
private boolean isFrontCamera = true;
|
||||
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public WebRTCManager(Context context, WebRTCListener listener) {
|
||||
this.context = context;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void initializeWebRTC(SurfaceViewRenderer localVideoView, SurfaceViewRenderer remoteVideoView) {
|
||||
this.localVideoView = localVideoView;
|
||||
this.remoteVideoView = remoteVideoView;
|
||||
|
||||
// Initialize EGL context
|
||||
rootEglBase = EglBase.create();
|
||||
|
||||
// Initialize video views
|
||||
localVideoView.init(rootEglBase.getEglBaseContext(), null);
|
||||
remoteVideoView.init(rootEglBase.getEglBaseContext(), null);
|
||||
|
||||
// Create peer connection factory
|
||||
PeerConnectionFactory.initialize(
|
||||
PeerConnectionFactory.InitializationOptions.builder(context)
|
||||
.setEnableInternalTracer(true)
|
||||
.createInitializationOptions()
|
||||
);
|
||||
|
||||
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()))
|
||||
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(rootEglBase.getEglBaseContext(), true, true))
|
||||
.setOptions(options)
|
||||
.createPeerConnectionFactory();
|
||||
|
||||
// Create local media streams
|
||||
createLocalMediaStream();
|
||||
|
||||
// Create peer connection
|
||||
createPeerConnection();
|
||||
}
|
||||
|
||||
private void createLocalMediaStream() {
|
||||
// Create video capturer
|
||||
videoCapturer = createCameraVideoCapturer();
|
||||
if (videoCapturer != null) {
|
||||
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase.getEglBaseContext());
|
||||
videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
|
||||
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
|
||||
videoCapturer.startCapture(1024, 768, 30);
|
||||
|
||||
localVideoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
|
||||
localVideoTrack.addSink(localVideoView);
|
||||
}
|
||||
|
||||
// Create audio source
|
||||
MediaConstraints audioConstraints = new MediaConstraints();
|
||||
audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
|
||||
localAudioTrack = peerConnectionFactory.createAudioTrack("101", audioSource);
|
||||
}
|
||||
|
||||
private VideoCapturer createCameraVideoCapturer() {
|
||||
CameraEnumerator enumerator;
|
||||
if (Camera2Enumerator.isSupported(context)) {
|
||||
enumerator = new Camera2Enumerator(context);
|
||||
} else {
|
||||
enumerator = new Camera1Enumerator(true);
|
||||
}
|
||||
|
||||
final String[] deviceNames = enumerator.getDeviceNames();
|
||||
|
||||
// Try front camera first
|
||||
for (String deviceName : deviceNames) {
|
||||
if (enumerator.isFrontFacing(deviceName)) {
|
||||
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
|
||||
if (videoCapturer != null) {
|
||||
return videoCapturer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try back camera
|
||||
for (String deviceName : deviceNames) {
|
||||
if (!enumerator.isFrontFacing(deviceName)) {
|
||||
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
|
||||
if (videoCapturer != null) {
|
||||
return videoCapturer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void createPeerConnection() {
|
||||
ArrayList<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
||||
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
||||
|
||||
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
|
||||
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
|
||||
rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
|
||||
rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
|
||||
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
|
||||
rtcConfig.keyType = PeerConnection.KeyType.ECDSA;
|
||||
|
||||
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnectionObserver());
|
||||
|
||||
// Create data channel for text messages
|
||||
DataChannel.Init dataChannelInit = new DataChannel.Init();
|
||||
dataChannel = peerConnection.createDataChannel("messages", dataChannelInit);
|
||||
dataChannel.registerObserver(new DataChannelObserver());
|
||||
|
||||
// Add local tracks
|
||||
if (localVideoTrack != null) {
|
||||
peerConnection.addTrack(localVideoTrack, Arrays.asList("stream"));
|
||||
}
|
||||
if (localAudioTrack != null) {
|
||||
peerConnection.addTrack(localAudioTrack, Arrays.asList("stream"));
|
||||
}
|
||||
}
|
||||
|
||||
public void connectToSignalingServer() {
|
||||
try {
|
||||
socket = IO.socket(SIGNALING_SERVER_URL);
|
||||
|
||||
socket.on(Socket.EVENT_CONNECT, args -> {
|
||||
Log.d(TAG, "Connected to signaling server");
|
||||
listener.onConnected();
|
||||
});
|
||||
|
||||
socket.on(Socket.EVENT_DISCONNECT, args -> {
|
||||
Log.d(TAG, "Disconnected from signaling server");
|
||||
listener.onDisconnected();
|
||||
});
|
||||
|
||||
socket.on("user-joined", args -> {
|
||||
String userNickname = (String) args[0];
|
||||
listener.onUserJoined(userNickname);
|
||||
});
|
||||
|
||||
socket.on("user-left", args -> {
|
||||
String userNickname = (String) args[0];
|
||||
listener.onUserLeft(userNickname);
|
||||
});
|
||||
|
||||
socket.on("offer", args -> {
|
||||
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
|
||||
handleOffer(data);
|
||||
});
|
||||
|
||||
socket.on("answer", args -> {
|
||||
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
|
||||
handleAnswer(data);
|
||||
});
|
||||
|
||||
socket.on("ice-candidate", args -> {
|
||||
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
|
||||
handleIceCandidate(data);
|
||||
});
|
||||
|
||||
socket.connect();
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error connecting to signaling server", e);
|
||||
listener.onError("Failed to connect to signaling server");
|
||||
}
|
||||
}
|
||||
|
||||
public void joinRoom(String nickname) {
|
||||
this.nickname = nickname;
|
||||
if (socket != null && socket.connected()) {
|
||||
socket.emit("join-room", nickname);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(String message) {
|
||||
if (dataChannel != null && dataChannel.state() == DataChannel.State.OPEN) {
|
||||
JsonObject messageObj = new JsonObject();
|
||||
messageObj.addProperty("type", "message");
|
||||
messageObj.addProperty("nickname", nickname);
|
||||
messageObj.addProperty("message", message);
|
||||
|
||||
String jsonMessage = gson.toJson(messageObj);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(jsonMessage.getBytes(StandardCharsets.UTF_8));
|
||||
DataChannel.Buffer dataBuffer = new DataChannel.Buffer(buffer, false);
|
||||
dataChannel.send(dataBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleVideo(boolean enabled) {
|
||||
isVideoEnabled = enabled;
|
||||
if (localVideoTrack != null) {
|
||||
localVideoTrack.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleAudio(boolean enabled) {
|
||||
isAudioEnabled = enabled;
|
||||
if (localAudioTrack != null) {
|
||||
localAudioTrack.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void switchCamera() {
|
||||
if (videoCapturer != null && videoCapturer instanceof CameraVideoCapturer) {
|
||||
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
|
||||
cameraVideoCapturer.switchCamera(null);
|
||||
isFrontCamera = !isFrontCamera;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleOffer(JsonObject data) {
|
||||
SessionDescription offer = new SessionDescription(
|
||||
SessionDescription.Type.OFFER,
|
||||
data.get("sdp").getAsString()
|
||||
);
|
||||
|
||||
peerConnection.setRemoteDescription(new SdpObserver(), offer);
|
||||
|
||||
// Create answer
|
||||
MediaConstraints constraints = new MediaConstraints();
|
||||
peerConnection.createAnswer(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
peerConnection.setLocalDescription(new SdpObserver(), sessionDescription);
|
||||
|
||||
JsonObject answerData = new JsonObject();
|
||||
answerData.addProperty("type", sessionDescription.type.canonicalForm());
|
||||
answerData.addProperty("sdp", sessionDescription.description);
|
||||
|
||||
socket.emit("answer", answerData);
|
||||
}
|
||||
}, constraints);
|
||||
}
|
||||
|
||||
private void handleAnswer(JsonObject data) {
|
||||
SessionDescription answer = new SessionDescription(
|
||||
SessionDescription.Type.ANSWER,
|
||||
data.get("sdp").getAsString()
|
||||
);
|
||||
|
||||
peerConnection.setRemoteDescription(new SdpObserver(), answer);
|
||||
}
|
||||
|
||||
private void handleIceCandidate(JsonObject data) {
|
||||
IceCandidate iceCandidate = new IceCandidate(
|
||||
data.get("sdpMid").getAsString(),
|
||||
data.get("sdpMLineIndex").getAsInt(),
|
||||
data.get("candidate").getAsString()
|
||||
);
|
||||
|
||||
peerConnection.addIceCandidate(iceCandidate);
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
if (localVideoTrack != null) {
|
||||
localVideoTrack.dispose();
|
||||
}
|
||||
if (localAudioTrack != null) {
|
||||
localAudioTrack.dispose();
|
||||
}
|
||||
if (videoCapturer != null) {
|
||||
try {
|
||||
videoCapturer.stopCapture();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
videoCapturer.dispose();
|
||||
}
|
||||
if (videoSource != null) {
|
||||
videoSource.dispose();
|
||||
}
|
||||
if (audioSource != null) {
|
||||
audioSource.dispose();
|
||||
}
|
||||
if (peerConnection != null) {
|
||||
peerConnection.close();
|
||||
}
|
||||
if (peerConnectionFactory != null) {
|
||||
peerConnectionFactory.dispose();
|
||||
}
|
||||
if (socket != null) {
|
||||
socket.disconnect();
|
||||
}
|
||||
if (rootEglBase != null) {
|
||||
rootEglBase.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Observer classes
|
||||
private class PeerConnectionObserver implements PeerConnection.Observer {
|
||||
@Override
|
||||
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
|
||||
Log.d(TAG, "onSignalingChange: " + signalingState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
||||
Log.d(TAG, "onIceConnectionChange: " + iceConnectionState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionReceivingChange(boolean b) {
|
||||
Log.d(TAG, "onIceConnectionReceivingChange: " + b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
|
||||
Log.d(TAG, "onIceGatheringChange: " + iceGatheringState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(IceCandidate iceCandidate) {
|
||||
Log.d(TAG, "onIceCandidate: " + iceCandidate);
|
||||
|
||||
JsonObject candidateData = new JsonObject();
|
||||
candidateData.addProperty("candidate", iceCandidate.sdp);
|
||||
candidateData.addProperty("sdpMid", iceCandidate.sdpMid);
|
||||
candidateData.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex);
|
||||
|
||||
socket.emit("ice-candidate", candidateData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
|
||||
Log.d(TAG, "onIceCandidatesRemoved");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddStream(MediaStream mediaStream) {
|
||||
Log.d(TAG, "onAddStream: " + mediaStream.getId());
|
||||
|
||||
if (mediaStream.videoTracks.size() > 0) {
|
||||
VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
|
||||
remoteVideoTrack.addSink(remoteVideoView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveStream(MediaStream mediaStream) {
|
||||
Log.d(TAG, "onRemoveStream: " + mediaStream.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChannel(DataChannel dataChannel) {
|
||||
Log.d(TAG, "onDataChannel: " + dataChannel.label());
|
||||
dataChannel.registerObserver(new DataChannelObserver());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenegotiationNeeded() {
|
||||
Log.d(TAG, "onRenegotiationNeeded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddTrack(org.webrtc.RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
|
||||
Log.d(TAG, "onAddTrack");
|
||||
}
|
||||
}
|
||||
|
||||
private class DataChannelObserver implements DataChannel.Observer {
|
||||
@Override
|
||||
public void onBufferedAmountChange(long l) {
|
||||
Log.d(TAG, "Data channel buffered amount changed: " + l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateChange() {
|
||||
Log.d(TAG, "Data channel state changed: " + dataChannel.state());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(DataChannel.Buffer buffer) {
|
||||
ByteBuffer data = buffer.data;
|
||||
byte[] bytes = new byte[data.remaining()];
|
||||
data.get(bytes);
|
||||
String message = new String(bytes, StandardCharsets.UTF_8);
|
||||
|
||||
try {
|
||||
JsonObject messageObj = gson.fromJson(message, JsonObject.class);
|
||||
String type = messageObj.get("type").getAsString();
|
||||
|
||||
if ("message".equals(type)) {
|
||||
String senderNickname = messageObj.get("nickname").getAsString();
|
||||
String messageText = messageObj.get("message").getAsString();
|
||||
listener.onMessageReceived(senderNickname, messageText);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error parsing received message", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class SdpObserver implements org.webrtc.SdpObserver {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
Log.d(TAG, "SDP create success");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.d(TAG, "SDP set success");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "SDP create failure: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "SDP set failure: " + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
android-app/app/src/main/res/drawable/btn_round_background.xml
Archivo normal
6
android-app/app/src/main/res/drawable/btn_round_background.xml
Archivo normal
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/button_background" />
|
||||
<stroke android:width="1dp" android:color="@color/button_border" />
|
||||
</shape>
|
||||
5
android-app/app/src/main/res/drawable/btn_send_background.xml
Archivo normal
5
android-app/app/src/main/res/drawable/btn_send_background.xml
Archivo normal
@@ -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="@color/primary_color" />
|
||||
</shape>
|
||||
6
android-app/app/src/main/res/drawable/control_background.xml
Archivo normal
6
android-app/app/src/main/res/drawable/control_background.xml
Archivo normal
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#80000000" />
|
||||
<corners android:radius="24dp" />
|
||||
</shape>
|
||||
10
android-app/app/src/main/res/drawable/ic_chat.xml
Archivo normal
10
android-app/app/src/main/res/drawable/ic_chat.xml
Archivo normal
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/icon_color">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_emoji.xml
Archivo normal
10
android-app/app/src/main/res/drawable/ic_emoji.xml
Archivo normal
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/icon_color">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM15.5,8c0.83,0 1.5,0.67 1.5,1.5S16.33,11 15.5,11S14,10.33 14,9.5S14.67,8 15.5,8zM12,17.5c-2.33,0 -4.31,-1.46 -5.11,-3.5h10.22C16.31,16.04 14.33,17.5 12,17.5z"/>
|
||||
</vector>
|
||||
10
android-app/app/src/main/res/drawable/ic_keyboard.xml
Archivo normal
10
android-app/app/src/main/res/drawable/ic_keyboard.xml
Archivo normal
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/icon_color">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,5L4,5c-1.1,0 -1.99,0.9 -1.99,2L2,17c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,7c0,-1.1 -0.9,-2 -2,-2zM20,9h-3.5v3.5h2.5c0.83,0 1.5,-0.67 1.5,-1.5L20,9zM20,17h-7v-7h7v7zM4,9h7v7L4,16L4,9zM4,7h16v1L4,8L4,7z"/>
|
||||
</vector>
|
||||
13
android-app/app/src/main/res/drawable/ic_launcher_background.xml
Archivo normal
13
android-app/app/src/main/res/drawable/ic_launcher_background.xml
Archivo normal
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108L0,108L0,0L9,0Z"
|
||||
android:strokeWidth="0"
|
||||
android:strokeMiterLimit="4"
|
||||
android:strokeLineCap="butt" />
|
||||
</vector>
|
||||
14
android-app/app/src/main/res/drawable/ic_launcher_foreground.xml
Archivo normal
14
android-app/app/src/main/res/drawable/ic_launcher_foreground.xml
Archivo normal
@@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="2.61"
|
||||
android:scaleY="2.61"
|
||||
android:translateX="22.68"
|
||||
android:translateY="22.68">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
|
||||
</group>
|
||||
</vector>
|
||||
7
android-app/app/src/main/res/drawable/message_input_background.xml
Archivo normal
7
android-app/app/src/main/res/drawable/message_input_background.xml
Archivo normal
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/input_field_background" />
|
||||
<corners android:radius="24dp" />
|
||||
<stroke android:width="1dp" android:color="@color/input_border" />
|
||||
</shape>
|
||||
7
android-app/app/src/main/res/drawable/other_message_background.xml
Archivo normal
7
android-app/app/src/main/res/drawable/other_message_background.xml
Archivo normal
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/message_background" />
|
||||
<corners android:radius="18dp" />
|
||||
<stroke android:width="1dp" android:color="@color/message_border" />
|
||||
</shape>
|
||||
6
android-app/app/src/main/res/drawable/own_message_background.xml
Archivo normal
6
android-app/app/src/main/res/drawable/own_message_background.xml
Archivo normal
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/primary_color" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
6
android-app/app/src/main/res/drawable/system_message_background.xml
Archivo normal
6
android-app/app/src/main/res/drawable/system_message_background.xml
Archivo normal
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/system_background" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
6
android-app/app/src/main/res/drawable/video_border.xml
Archivo normal
6
android-app/app/src/main/res/drawable/video_border.xml
Archivo normal
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="8dp" />
|
||||
<stroke android:width="2dp" android:color="@color/white" />
|
||||
</shape>
|
||||
130
android-app/app/src/main/res/layout/activity_chat.xml
Archivo normal
130
android-app/app/src/main/res/layout/activity_chat.xml
Archivo normal
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/background_color">
|
||||
|
||||
<!-- Video Views Container -->
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@color/video_background">
|
||||
|
||||
<!-- Remote Video View (Full Screen) -->
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/remote_video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<!-- Local Video View (Picture in Picture) -->
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/local_video_view"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="16dp"
|
||||
android:background="@drawable/video_border" />
|
||||
|
||||
<!-- Video Controls -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/control_background"
|
||||
android:padding="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_toggle_video"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="4dp"
|
||||
android:background="@drawable/btn_round_background"
|
||||
android:text="📹"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_toggle_audio"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="4dp"
|
||||
android:background="@drawable/btn_round_background"
|
||||
android:text="🎤"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_switch_camera"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="4dp"
|
||||
android:background="@drawable/btn_round_background"
|
||||
android:text="🔄"
|
||||
android:textSize="18sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Chat Section -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/chat_background">
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_chat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<!-- Message Input -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:background="@color/input_background"
|
||||
android:elevation="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_emoji"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/btn_round_background"
|
||||
android:src="@drawable/ic_emoji"
|
||||
android:contentDescription="@string/emoji_button" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_message"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/message_input_background"
|
||||
android:hint="@string/message_hint"
|
||||
android:maxLines="3"
|
||||
android:padding="12dp"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_send"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/btn_send_background"
|
||||
android:text="➤"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
61
android-app/app/src/main/res/layout/activity_main.xml
Archivo normal
61
android-app/app/src/main/res/layout/activity_main.xml
Archivo normal
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:gravity="center"
|
||||
android:background="@color/background_color">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:src="@drawable/ic_chat"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:contentDescription="@string/app_name" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/welcome_title"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/primary_text_color"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/welcome_subtitle"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/secondary_text_color"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:gravity="center" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="@string/nickname_hint"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_nickname"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:imeOptions="actionDone" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_join_chat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:text="@string/join_chat"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
style="@style/Widget.MaterialComponents.Button" />
|
||||
|
||||
</LinearLayout>
|
||||
45
android-app/app/src/main/res/layout/item_other_message.xml
Archivo normal
45
android-app/app/src/main/res/layout/item_other_message.xml
Archivo normal
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:gravity="start">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/other_message_background"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_nickname"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/primary_color"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="2dp" />
|
||||
|
||||
<com.vanniktech.emoji.EmojiTextView
|
||||
android:id="@+id/tv_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/primary_text_color"
|
||||
android:textSize="16sp"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/secondary_text_color"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
36
android-app/app/src/main/res/layout/item_own_message.xml
Archivo normal
36
android-app/app/src/main/res/layout/item_own_message.xml
Archivo normal
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:gravity="end">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="48dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/own_message_background"
|
||||
android:padding="12dp">
|
||||
|
||||
<com.vanniktech.emoji.EmojiTextView
|
||||
android:id="@+id/tv_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/message_time_color"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
19
android-app/app/src/main/res/layout/item_system_message.xml
Archivo normal
19
android-app/app/src/main/res/layout/item_system_message.xml
Archivo normal
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_system_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/system_message_background"
|
||||
android:textColor="@color/system_text_color"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
android:padding="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
5
android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Archivo normal
5
android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Archivo normal
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
5
android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Archivo normal
5
android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Archivo normal
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
33
android-app/app/src/main/res/values/colors.xml
Archivo normal
33
android-app/app/src/main/res/values/colors.xml
Archivo normal
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary_color">#2196F3</color>
|
||||
<color name="primary_color_dark">#1976D2</color>
|
||||
<color name="accent_color">#FF4081</color>
|
||||
|
||||
<color name="background_color">#FAFAFA</color>
|
||||
<color name="chat_background">#FFFFFF</color>
|
||||
<color name="video_background">#000000</color>
|
||||
<color name="input_background">#FFFFFF</color>
|
||||
|
||||
<color name="primary_text_color">#212121</color>
|
||||
<color name="secondary_text_color">#757575</color>
|
||||
<color name="system_text_color">#9E9E9E</color>
|
||||
|
||||
<color name="message_background">#F5F5F5</color>
|
||||
<color name="message_border">#E0E0E0</color>
|
||||
<color name="message_time_color">#B0BEC5</color>
|
||||
|
||||
<color name="system_background">#E8F5E8</color>
|
||||
|
||||
<color name="input_field_background">#FFFFFF</color>
|
||||
<color name="input_border">#E0E0E0</color>
|
||||
|
||||
<color name="button_background">#FFFFFF</color>
|
||||
<color name="button_border">#E0E0E0</color>
|
||||
<color name="button_enabled">#4CAF50</color>
|
||||
<color name="button_disabled">#F44336</color>
|
||||
|
||||
<color name="icon_color">#616161</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="black">#000000</color>
|
||||
</resources>
|
||||
23
android-app/app/src/main/res/values/strings.xml
Archivo normal
23
android-app/app/src/main/res/values/strings.xml
Archivo normal
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">ChatRTC</string>
|
||||
|
||||
<!-- Main Activity -->
|
||||
<string name="welcome_title">Welcome to ChatRTC</string>
|
||||
<string name="welcome_subtitle">Connect with others using audio and video chat</string>
|
||||
<string name="nickname_hint">Enter your nickname</string>
|
||||
<string name="join_chat">Join Chat</string>
|
||||
|
||||
<!-- Chat Activity -->
|
||||
<string name="message_hint">Type your message...</string>
|
||||
<string name="emoji_button">Emoji</string>
|
||||
|
||||
<!-- Permissions -->
|
||||
<string name="permission_camera">Camera permission is required for video chat</string>
|
||||
<string name="permission_microphone">Microphone permission is required for audio chat</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_nickname_empty">Please enter your nickname</string>
|
||||
<string name="error_connection">Connection failed. Please try again.</string>
|
||||
<string name="error_webrtc">WebRTC error occurred</string>
|
||||
</resources>
|
||||
18
android-app/app/src/main/res/values/themes.xml
Archivo normal
18
android-app/app/src/main/res/values/themes.xml
Archivo normal
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme -->
|
||||
<style name="Theme.ChatRTC" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color -->
|
||||
<item name="colorPrimary">@color/primary_color</item>
|
||||
<item name="colorPrimaryVariant">@color/primary_color_dark</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color -->
|
||||
<item name="colorSecondary">@color/accent_color</item>
|
||||
<item name="colorSecondaryVariant">@color/accent_color</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
<!-- Status bar color -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here -->
|
||||
<item name="android:windowBackground">@color/background_color</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
android-app/app/src/main/res/xml/backup_rules.xml
Archivo normal
4
android-app/app/src/main/res/xml/backup_rules.xml
Archivo normal
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="device_prefs.xml"/>
|
||||
</full-backup-content>
|
||||
10
android-app/app/src/main/res/xml/data_extraction_rules.xml
Archivo normal
10
android-app/app/src/main/res/xml/data_extraction_rules.xml
Archivo normal
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="root" />
|
||||
<exclude domain="device_transfer" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="root" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
8
android-app/app/src/main/res/xml/network_security_config.xml
Archivo normal
8
android-app/app/src/main/res/xml/network_security_config.xml
Archivo normal
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">192.168.1.1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
24
android-app/build.gradle
Archivo normal
24
android-app/build.gradle
Archivo normal
@@ -0,0 +1,24 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.8.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:8.0.2"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
28
android-app/gradle.properties
Archivo normal
28
android-app/gradle.properties
Archivo normal
@@ -0,0 +1,28 @@
|
||||
gradle.properties
|
||||
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
5
android-app/gradle/wrapper/gradle-wrapper.properties
vendido
Archivo normal
5
android-app/gradle/wrapper/gradle-wrapper.properties
vendido
Archivo normal
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
2
android-app/settings.gradle
Archivo normal
2
android-app/settings.gradle
Archivo normal
@@ -0,0 +1,2 @@
|
||||
include ':app'
|
||||
rootProject.name = "ChatRTC"
|
||||
22
android-app/setup-icons.sh
Archivo ejecutable
22
android-app/setup-icons.sh
Archivo ejecutable
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create basic launcher icons for the app
|
||||
# This script creates placeholder PNG files for the app icons
|
||||
|
||||
APP_DIR="/home/ale/projects/android/chatrtc/app/src/main/res"
|
||||
|
||||
# Create mipmap directories
|
||||
mkdir -p "$APP_DIR/mipmap-hdpi"
|
||||
mkdir -p "$APP_DIR/mipmap-mdpi"
|
||||
mkdir -p "$APP_DIR/mipmap-xhdpi"
|
||||
mkdir -p "$APP_DIR/mipmap-xxhdpi"
|
||||
mkdir -p "$APP_DIR/mipmap-xxxhdpi"
|
||||
|
||||
echo "Mipmap directories created. You can add your custom app icons to these directories:"
|
||||
echo "- mipmap-mdpi/ic_launcher.png (48x48px)"
|
||||
echo "- mipmap-hdpi/ic_launcher.png (72x72px)"
|
||||
echo "- mipmap-xhdpi/ic_launcher.png (96x96px)"
|
||||
echo "- mipmap-xxhdpi/ic_launcher.png (144x144px)"
|
||||
echo "- mipmap-xxxhdpi/ic_launcher.png (192x192px)"
|
||||
echo ""
|
||||
echo "For now, the app will use the vector drawable icons defined in the XML files."
|
||||
21
android-app/signaling-server/package.json
Archivo normal
21
android-app/signaling-server/package.json
Archivo normal
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "chatrtc-signaling-server",
|
||||
"version": "1.0.0",
|
||||
"description": "WebRTC signaling server for ChatRTC app",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"socket.io": "^4.7.2",
|
||||
"cors": "^2.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"keywords": ["webrtc", "signaling", "socket.io", "chat"],
|
||||
"author": "ChatRTC Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
61
android-app/signaling-server/server.js
Archivo normal
61
android-app/signaling-server/server.js
Archivo normal
@@ -0,0 +1,61 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const cors = require('cors');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = socketIo(server, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.static('public'));
|
||||
|
||||
const users = new Map();
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('User connected:', socket.id);
|
||||
|
||||
socket.on('join-room', (nickname) => {
|
||||
socket.nickname = nickname;
|
||||
users.set(socket.id, nickname);
|
||||
|
||||
// Notify others about new user
|
||||
socket.broadcast.emit('user-joined', nickname);
|
||||
|
||||
console.log(`${nickname} joined the room`);
|
||||
});
|
||||
|
||||
socket.on('offer', (data) => {
|
||||
console.log('Offer received from:', socket.nickname);
|
||||
socket.broadcast.emit('offer', data);
|
||||
});
|
||||
|
||||
socket.on('answer', (data) => {
|
||||
console.log('Answer received from:', socket.nickname);
|
||||
socket.broadcast.emit('answer', data);
|
||||
});
|
||||
|
||||
socket.on('ice-candidate', (data) => {
|
||||
console.log('ICE candidate received from:', socket.nickname);
|
||||
socket.broadcast.emit('ice-candidate', data);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
const nickname = users.get(socket.id);
|
||||
if (nickname) {
|
||||
users.delete(socket.id);
|
||||
socket.broadcast.emit('user-left', nickname);
|
||||
console.log(`${nickname} left the room`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Signaling server running on port ${PORT}`);
|
||||
});
|
||||
Referencia en una nueva incidencia
Block a user