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