first commit

This commit is contained in:
Hamza-Ayed
2026-05-23 16:17:20 +03:00
commit 2bbaa1ee16
195 changed files with 11126 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# IDE
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS files
.DS_Store
Thumbs.db
# Archives
*.zip
*.tar.gz
# Android / Mobile Builds & Configs
**/local.properties
**/.gradle/
**/build/
**/.dart_tool/
**/.flutter-plugins
**/.flutter-plugins-dependencies
**/Generated.xcconfig
**/flutter_export_environment.sh
**/.pub-cache/
**/.pub/
# Logs
**/logs/*.log
*.log

1
README.md Executable file
View File

@@ -0,0 +1 @@
Here are all the generated files.

27
backend/.htaccess Normal file
View File

@@ -0,0 +1,27 @@
# Protect includes directory
<IfModule mod_rewrite.c>
RewriteEngine On
# Block direct access to includes/
RewriteRule ^includes/ - [F,L]
# Block access to config files
RewriteRule ^config\.php$ - [F,L]
# Block access to hidden files
RewriteRule (^|/)\. - [F,L]
# Block access to SQL files
RewriteRule \.sql$ - [F,L]
# Block access to log files
RewriteRule \.log$ - [F,L]
</IfModule>
# Disable directory listing
Options -Indexes
# Prevent script execution in includes
<Directory "includes">
php_flag engine off
</Directory>

121
backend/api/call-done.php Normal file
View File

@@ -0,0 +1,121 @@
<?php
/**
* POST /api/call-done
*
* Called by Caller Android App after a flash call attempt.
*
* Request body:
* {
* "task_id": 42,
* "device_id": "DEVICE_XXX",
* "app_key": "SECRET_DEVICE_KEY",
* "result": "success" | "failed" | "busy" | "no_answer"
* }
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
exit;
}
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/Logger.php';
// Authenticate — requires device key
Auth::requireAuth('device');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['task_id']) || !isset($input['device_id']) || !isset($input['result'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'missing_required_fields']);
RequestLogger::log('call-done', 'POST', $input, 400, 'missing_fields');
exit;
}
$taskId = (int) $input['task_id'];
$deviceId = trim($input['device_id']);
$result = trim($input['result']);
// Validate result
$validResults = ['success', 'failed', 'busy', 'no_answer'];
if (!in_array($result, $validResults, true)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid_result_value']);
RequestLogger::log('call-done', 'POST', $input, 400, 'invalid_result');
exit;
}
$db = Database::getInstance();
try {
// Verify this task belongs to this device
$stmt = $db->prepare(
"SELECT id, status FROM otp_requests WHERE id = ? AND device_id = ?"
);
$stmt->execute([$taskId, $deviceId]);
$task = $stmt->fetch();
if (!$task) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'task_not_found']);
RequestLogger::log('call-done', 'POST', $input, 404, 'task_not_found');
exit;
}
if ($task['status'] !== 'calling') {
http_response_code(409);
echo json_encode(['success' => false, 'message' => 'task_not_in_calling_state']);
RequestLogger::log('call-done', 'POST', $input, 409, 'wrong_status');
exit;
}
// Map result to new status
$newStatus = ($result === 'success') ? 'completed' : 'failed';
$db->beginTransaction();
// Update OTP request status
$stmt = $db->prepare(
"UPDATE otp_requests
SET status = ?, updated_at = NOW()
WHERE id = ? AND device_id = ?"
);
$stmt->execute([$newStatus, $taskId, $deviceId]);
// Increment calls_today for the device
$stmt = $db->prepare(
"UPDATE caller_devices
SET calls_today = calls_today + 1
WHERE device_id = ?"
);
$stmt->execute([$deviceId]);
$db->commit();
echo json_encode([
'success' => true,
'status' => $newStatus,
]);
RequestLogger::log('call-done', 'POST', $input, 200);
} catch (\Throwable $e) {
$db->rollBack();
error_log('call-done error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'internal_error']);
RequestLogger::log('call-done', 'POST', $input, 500, $e->getMessage());
}

View File

@@ -0,0 +1,124 @@
<?php
/**
* GET /api/pending-call
*
* Called by Caller Android App every 3 seconds to fetch the next pending flash call task.
*
* Query params:
* device_id — Registered device identifier
* app_key — Device authentication key
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
exit;
}
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/Logger.php';
// Authenticate — requires device key (Caller Android App)
Auth::requireAuth('device');
$deviceId = $_GET['device_id'] ?? null;
if (!$deviceId) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'missing_device_id']);
RequestLogger::log('pending-call', 'GET', $_GET, 400, 'missing_device_id');
exit;
}
$db = Database::getInstance();
try {
// Verify device exists and is active
$stmt = $db->prepare(
"SELECT id, device_id, is_active FROM caller_devices WHERE device_id = ?"
);
$stmt->execute([$deviceId]);
$device = $stmt->fetch();
if (!$device || !$device['is_active']) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'device_not_registered_or_inactive']);
RequestLogger::log('pending-call', 'GET', $_GET, 403, 'invalid_device');
exit;
}
// Update last_seen
$stmt = $db->prepare("UPDATE caller_devices SET last_seen = NOW() WHERE device_id = ?");
$stmt->execute([$deviceId]);
// Find oldest pending flash call task
// Priority: tasks assigned to this device first, then unassigned tasks
$stmt = $db->prepare(
"SELECT id, phone, caller_id, expires_at
FROM otp_requests
WHERE method = 'flash_call'
AND status = 'pending'
AND expires_at > NOW()
AND (device_id IS NULL OR device_id = ?)
ORDER BY
CASE WHEN device_id = ? THEN 0 ELSE 1 END,
created_at ASC
LIMIT 1"
);
$stmt->execute([$deviceId, $deviceId]);
$task = $stmt->fetch();
if (!$task) {
echo json_encode(['success' => true, 'task_id' => null]);
RequestLogger::log('pending-call', 'GET', $_GET, 200);
exit;
}
// Claim this task — update status and assign device
$stmt = $db->prepare(
"UPDATE otp_requests
SET status = 'calling', device_id = ?, updated_at = NOW()
WHERE id = ? AND status = 'pending'"
);
$stmt->execute([$deviceId, $task['id']]);
// Check if update affected any row (race condition handling)
if ($stmt->rowCount() === 0) {
// Another device claimed it first
echo json_encode(['success' => true, 'task_id' => null]);
RequestLogger::log('pending-call', 'GET', $_GET, 200);
exit;
}
// Calculate remaining timeout
$expiresAt = new \DateTime($task['expires_at']);
$now = new \DateTime();
$timeoutSeconds = max(10, $expiresAt->getTimestamp() - $now->getTimestamp());
echo json_encode([
'success' => true,
'task_id' => (int) $task['id'],
'phone' => $task['phone'],
'caller_id' => $task['caller_id'],
'timeout_seconds' => $timeoutSeconds,
]);
RequestLogger::log('pending-call', 'GET', $_GET, 200);
} catch (\Throwable $e) {
error_log('pending-call error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'internal_error']);
RequestLogger::log('pending-call', 'GET', $_GET, 500, $e->getMessage());
}

121
backend/api/pending-sms.php Normal file
View File

@@ -0,0 +1,121 @@
<?php
/**
* GET /api/pending-sms
*
* Called by Caller Android App every 3 seconds to fetch the next pending SMS task.
*
* Query params:
* device_id — Registered device identifier
* app_key — Device authentication key
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
exit;
}
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/Logger.php';
// Authenticate — requires device key
Auth::requireAuth('device');
$deviceId = $_GET['device_id'] ?? null;
if (!$deviceId) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'missing_device_id']);
RequestLogger::log('pending-sms', 'GET', $_GET, 400, 'missing_device_id');
exit;
}
$db = Database::getInstance();
try {
// Verify device exists and is active
$stmt = $db->prepare(
"SELECT id, device_id, is_active FROM caller_devices WHERE device_id = ?"
);
$stmt->execute([$deviceId]);
$device = $stmt->fetch();
if (!$device || !$device['is_active']) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'device_not_registered_or_inactive']);
RequestLogger::log('pending-sms', 'GET', $_GET, 403, 'invalid_device');
exit;
}
// Update last_seen
$stmt = $db->prepare("UPDATE caller_devices SET last_seen = NOW() WHERE device_id = ?");
$stmt->execute([$deviceId]);
// Find oldest pending SMS task
$stmt = $db->prepare(
"SELECT id, phone, otp_code, expires_at
FROM otp_requests
WHERE method = 'sms'
AND status = 'pending_sms'
AND expires_at > NOW()
AND (device_id IS NULL OR device_id = ?)
ORDER BY
CASE WHEN device_id = ? THEN 0 ELSE 1 END,
created_at ASC
LIMIT 1"
);
$stmt->execute([$deviceId, $deviceId]);
$task = $stmt->fetch();
if (!$task) {
echo json_encode(['success' => true, 'task_id' => null]);
RequestLogger::log('pending-sms', 'GET', $_GET, 200);
exit;
}
// Claim this task
$stmt = $db->prepare(
"UPDATE otp_requests
SET status = 'calling', device_id = ?, updated_at = NOW()
WHERE id = ? AND status = 'pending_sms'"
);
$stmt->execute([$deviceId, $task['id']]);
if ($stmt->rowCount() === 0) {
echo json_encode(['success' => true, 'task_id' => null]);
RequestLogger::log('pending-sms', 'GET', $_GET, 200);
exit;
}
// Calculate remaining timeout
$expiresAt = new \DateTime($task['expires_at']);
$now = new \DateTime();
$timeoutSeconds = max(10, $expiresAt->getTimestamp() - $now->getTimestamp());
echo json_encode([
'success' => true,
'task_id' => (int) $task['id'],
'phone' => $task['phone'],
'otp' => $task['otp_code'],
'timeout_seconds' => $timeoutSeconds,
]);
RequestLogger::log('pending-sms', 'GET', $_GET, 200);
} catch (\Throwable $e) {
error_log('pending-sms error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'internal_error']);
RequestLogger::log('pending-sms', 'GET', $_GET, 500, $e->getMessage());
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* POST /api/register-device
*
* Register a Caller Android device.
*
* Request body:
* {
* "device_id": "DEVICE_XXX",
* "phone_number": "+9627XXXXXXXX",
* "sim_slot": 0,
* "app_key": "SECRET_DEVICE_KEY"
* }
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
exit;
}
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/Logger.php';
// Authenticate — requires device key
Auth::requireAuth('device');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['device_id']) || !isset($input['phone_number'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'missing_required_fields']);
RequestLogger::log('register-device', 'POST', $input, 400, 'missing_fields');
exit;
}
$deviceId = trim($input['device_id']);
$phoneNumber = trim($input['phone_number']);
$simSlot = isset($input['sim_slot']) ? (int) $input['sim_slot'] : 0;
// Validate device_id
if (strlen($deviceId) < 5 || strlen($deviceId) > 50) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid_device_id_length']);
RequestLogger::log('register-device', 'POST', $input, 400, 'invalid_device_id');
exit;
}
// Validate phone format
if (!preg_match('/^\+[1-9]\d{6,14}$/', $phoneNumber)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid_phone_format']);
RequestLogger::log('register-device', 'POST', $input, 400, 'invalid_phone');
exit;
}
// Validate sim_slot
if ($simSlot < 0 || $simSlot > 3) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid_sim_slot']);
RequestLogger::log('register-device', 'POST', $input, 400, 'invalid_sim_slot');
exit;
}
$db = Database::getInstance();
try {
// Check if device already registered
$stmt = $db->prepare("SELECT id, is_active FROM caller_devices WHERE device_id = ?");
$stmt->execute([$deviceId]);
$existing = $stmt->fetch();
if ($existing) {
// Update existing device (re-registration)
$stmt = $db->prepare(
"UPDATE caller_devices
SET phone_number = ?, sim_slot = ?, is_active = 1, last_seen = NOW()
WHERE device_id = ?"
);
$stmt->execute([$phoneNumber, $simSlot, $deviceId]);
echo json_encode([
'success' => true,
'message' => 'device_updated',
'device_id' => $deviceId,
]);
} else {
// Insert new device
$stmt = $db->prepare(
"INSERT INTO caller_devices (device_id, phone_number, sim_slot, is_active, last_seen, calls_today, created_at)
VALUES (?, ?, ?, 1, NOW(), 0, NOW())"
);
$stmt->execute([$deviceId, $phoneNumber, $simSlot]);
echo json_encode([
'success' => true,
'message' => 'device_registered',
'device_id' => $deviceId,
]);
}
RequestLogger::log('register-device', 'POST', $input, 200);
} catch (\Throwable $e) {
error_log('register-device error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'internal_error']);
RequestLogger::log('register-device', 'POST', $input, 500, $e->getMessage());
}

190
backend/api/request-otp.php Normal file
View File

@@ -0,0 +1,190 @@
<?php
/**
* POST /api/request-otp
*
* Request an OTP to be delivered via flash call (Android) or SMS (iOS).
*
* Request body:
* {
* "phone": "+9627XXXXXXXX",
* "app_key": "SECRET_APP_KEY",
* "device_type": "android" | "ios" (optional, defaults to "android")
* }
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
// Handle CORS preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
exit;
}
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Redis.php';
require_once __DIR__ . '/../includes/RateLimit.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/Logger.php';
// Authenticate — requires app key (Flutter app)
Auth::requireAuth('app');
// Parse request body
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['phone'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'missing_phone']);
RequestLogger::log('request-otp', 'POST', $input, 400, 'missing_phone');
exit;
}
$phone = trim($input['phone']);
$deviceType = isset($input['device_type']) ? strtolower(trim($input['device_type'])) : 'android';
// Validate device_type
if (!in_array($deviceType, ['android', 'ios'], true)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid_device_type']);
RequestLogger::log('request-otp', 'POST', $input, 400, 'invalid_device_type');
exit;
}
// Validate phone format (E.164)
if (!preg_match('/^\+[1-9]\d{6,14}$/', $phone)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'invalid_phone_format',
'hint' => 'Phone must be in E.164 format, e.g. +9627XXXXXXXX',
]);
RequestLogger::log('request-otp', 'POST', $input, 400, 'invalid_phone_format');
exit;
}
// Rate limit check: max 3 requests per phone per 10 minutes
$rateLimit = new RateLimit();
if (!$rateLimit->check("otp:{$phone}")) {
$remaining = $rateLimit->remaining("otp:{$phone}");
$ttl = $rateLimit->ttl("otp:{$phone}");
http_response_code(429);
echo json_encode([
'success' => false,
'message' => 'rate_limit_exceeded',
'retry_after' => $ttl,
'remaining' => $remaining,
]);
RequestLogger::log('request-otp', 'POST', $input, 429, 'rate_limit_exceeded');
exit;
}
// IP-based rate limiting
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
if (!$rateLimit->checkIp($clientIp, 'request-otp', 30, 60)) {
http_response_code(429);
echo json_encode(['success' => false, 'message' => 'ip_rate_limit_exceeded']);
RequestLogger::log('request-otp', 'POST', $input, 429, 'ip_rate_limit');
exit;
}
// Generate 4-digit OTP (cryptographically secure)
$otpCode = str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
// Determine delivery method
$method = ($deviceType === 'ios') ? 'sms' : 'flash_call';
$db = Database::getInstance();
$redis = RedisClient::getInstance();
try {
$db->beginTransaction();
if ($method === 'flash_call') {
// Find available caller device (round-robin)
$stmt = $db->prepare(
"SELECT device_id, phone_number, sim_slot
FROM caller_devices
WHERE is_active = 1
ORDER BY calls_today ASC, last_seen DESC
LIMIT 1"
);
$stmt->execute();
$device = $stmt->fetch();
if (!$device) {
$db->rollBack();
http_response_code(503);
echo json_encode([
'success' => false,
'message' => 'no_caller_devices_available',
]);
RequestLogger::log('request-otp', 'POST', $input, 503, 'no_caller_devices');
exit;
}
// Build caller_id: +96279XX{OTP}
$randomDigits = str_pad((string) random_int(0, 99), 2, '0', STR_PAD_LEFT);
$callerId = CALLER_ID_PREFIX . $randomDigits . $otpCode;
// Insert OTP request
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
$stmt = $db->prepare(
"INSERT INTO otp_requests (phone, otp_code, caller_id, status, device_id, method, expires_at)
VALUES (?, ?, ?, 'pending', ?, 'flash_call', ?)"
);
$stmt->execute([$phone, $otpCode, $callerId, $device['device_id'], $expiresAt]);
} else {
// SMS delivery — no specific caller_id needed for the OTP request
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
$stmt = $db->prepare(
"INSERT INTO otp_requests (phone, otp_code, caller_id, status, method, expires_at)
VALUES (?, ?, '', 'pending_sms', 'sms', ?)"
);
$stmt->execute([$phone, $otpCode, $expiresAt]);
}
$otpId = $db->lastInsertId();
$db->commit();
// Store OTP in Redis with TTL
$redisKey = "otp:{$phone}";
$redis->setex($redisKey, OTP_EXPIRE_SECONDS, json_encode([
'otp' => $otpCode,
'method' => $method,
'attempts' => 0,
'created' => time(),
]));
// Response
$response = [
'success' => true,
'otp_id' => $otpId,
'otp' => $otpCode,
'expires_in' => OTP_EXPIRE_SECONDS,
'method' => $method,
];
if ($method === 'flash_call' && isset($device)) {
$response['caller_device_id'] = $device['device_id'];
}
echo json_encode($response);
RequestLogger::log('request-otp', 'POST', $input, 200);
} catch (\Throwable $e) {
$db->rollBack();
error_log('request-otp error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'internal_error']);
RequestLogger::log('request-otp', 'POST', $input, 500, $e->getMessage());
}

120
backend/api/sms-done.php Normal file
View File

@@ -0,0 +1,120 @@
<?php
/**
* POST /api/sms-done
*
* Called by Caller Android App after sending an SMS OTP.
*
* Request body:
* {
* "task_id": 43,
* "device_id": "DEVICE_XXX",
* "app_key": "SECRET_DEVICE_KEY",
* "result": "success" | "failed"
* }
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
exit;
}
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/Logger.php';
// Authenticate — requires device key
Auth::requireAuth('device');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['task_id']) || !isset($input['device_id']) || !isset($input['result'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'missing_required_fields']);
RequestLogger::log('sms-done', 'POST', $input, 400, 'missing_fields');
exit;
}
$taskId = (int) $input['task_id'];
$deviceId = trim($input['device_id']);
$result = trim($input['result']);
// Validate result
$validResults = ['success', 'failed'];
if (!in_array($result, $validResults, true)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid_result_value']);
RequestLogger::log('sms-done', 'POST', $input, 400, 'invalid_result');
exit;
}
$db = Database::getInstance();
try {
// Verify this task belongs to this device
$stmt = $db->prepare(
"SELECT id, status, method FROM otp_requests WHERE id = ? AND device_id = ?"
);
$stmt->execute([$taskId, $deviceId]);
$task = $stmt->fetch();
if (!$task) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'task_not_found']);
RequestLogger::log('sms-done', 'POST', $input, 404, 'task_not_found');
exit;
}
if ($task['status'] !== 'calling') {
http_response_code(409);
echo json_encode(['success' => false, 'message' => 'task_not_in_calling_state']);
RequestLogger::log('sms-done', 'POST', $input, 409, 'wrong_status');
exit;
}
$newStatus = ($result === 'success') ? 'completed' : 'failed';
$db->beginTransaction();
// Update OTP request status
$stmt = $db->prepare(
"UPDATE otp_requests
SET status = ?, updated_at = NOW()
WHERE id = ? AND device_id = ?"
);
$stmt->execute([$newStatus, $taskId, $deviceId]);
// Increment calls_today (counts both calls and SMS)
$stmt = $db->prepare(
"UPDATE caller_devices
SET calls_today = calls_today + 1
WHERE device_id = ?"
);
$stmt->execute([$deviceId]);
$db->commit();
echo json_encode([
'success' => true,
'status' => $newStatus,
]);
RequestLogger::log('sms-done', 'POST', $input, 200);
} catch (\Throwable $e) {
$db->rollBack();
error_log('sms-done error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'internal_error']);
RequestLogger::log('sms-done', 'POST', $input, 500, $e->getMessage());
}

154
backend/api/verify-otp.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
/**
* POST /api/verify-otp
*
* Verify an OTP code submitted by the user (Flutter app).
*
* Request body:
* {
* "phone": "+9627XXXXXXXX",
* "otp": "4829",
* "app_key": "SECRET_APP_KEY"
* }
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
exit;
}
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Redis.php';
require_once __DIR__ . '/../includes/RateLimit.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/Logger.php';
// Authenticate — requires app key
Auth::requireAuth('app');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['phone']) || !isset($input['otp'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'missing_phone_or_otp']);
RequestLogger::log('verify-otp', 'POST', $input, 400, 'missing_fields');
exit;
}
$phone = trim($input['phone']);
$otp = trim($input['otp']);
// Validate phone format
if (!preg_match('/^\+[1-9]\d{6,14}$/', $phone)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid_phone_format']);
RequestLogger::log('verify-otp', 'POST', $input, 400, 'invalid_phone');
exit;
}
// Validate OTP format (4 digits)
if (!preg_match('/^\d{4}$/', $otp)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'invalid_otp_format']);
RequestLogger::log('verify-otp', 'POST', $input, 400, 'invalid_otp_format');
exit;
}
$redis = RedisClient::getInstance();
$db = Database::getInstance();
$redisKey = "otp:{$phone}";
// Check if OTP exists in Redis
$stored = $redis->get($redisKey);
if ($stored === false || $stored === null) {
// OTP expired or never existed
echo json_encode([
'success' => false,
'message' => 'expired',
]);
RequestLogger::log('verify-otp', 'POST', $input, 200, 'expired');
exit;
}
// Decode stored data
if (is_string($stored)) {
$otpData = json_decode($stored, true);
} else {
$otpData = $stored;
}
if (!is_array($otpData) || !isset($otpData['otp'])) {
// Corrupted data — clean up
$redis->del($redisKey);
echo json_encode(['success' => false, 'message' => 'expired']);
RequestLogger::log('verify-otp', 'POST', $input, 200, 'corrupted_data');
exit;
}
// Check max attempts
if (isset($otpData['attempts']) && (int) $otpData['attempts'] >= MAX_OTP_ATTEMPTS) {
$redis->del($redisKey);
echo json_encode([
'success' => false,
'message' => 'max_attempts',
]);
RequestLogger::log('verify-otp', 'POST', $input, 200, 'max_attempts');
exit;
}
// Increment attempt counter
$otpData['attempts'] = (int) ($otpData['attempts'] ?? 0) + 1;
$ttl = $redis->ttl($redisKey);
if ($ttl > 0) {
$redis->setex($redisKey, $ttl, json_encode($otpData));
}
// Timing-safe comparison
if (hash_equals($otpData['otp'], $otp)) {
// Success — clean up Redis
$redis->del($redisKey);
// Update database
try {
$stmt = $db->prepare(
"UPDATE otp_requests
SET verified_at = NOW(), status = 'verified'
WHERE phone = ? AND otp_code = ? AND verified_at IS NULL
ORDER BY created_at DESC
LIMIT 1"
);
$stmt->execute([$phone, $otp]);
} catch (\Throwable $e) {
// Non-critical — OTP is already verified via Redis
error_log('verify-otp DB update error: ' . $e->getMessage());
}
echo json_encode([
'success' => true,
'message' => 'verified',
]);
RequestLogger::log('verify-otp', 'POST', $input, 200);
} else {
// Wrong OTP
$remainingAttempts = MAX_OTP_ATTEMPTS - (int) $otpData['attempts'];
echo json_encode([
'success' => false,
'message' => 'invalid_otp',
'remaining_attempts' => max(0, $remainingAttempts),
]);
RequestLogger::log('verify-otp', 'POST', $input, 200, 'invalid_otp');
}

43
backend/config.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
/**
* Flash Call OTP System — Configuration
* Domain: otp.intaleqapp.com
*/
// Database
define('DB_HOST', 'localhost');
define('DB_NAME', 'otp_db');
define('DB_USER', 'otp_user');
define('DB_PASS', 'STRONG_PASSWORD');
// Redis
define('REDIS_HOST', '127.0.0.1');
define('REDIS_PORT', 6379);
define('REDIS_PASSWORD', null); // Set if Redis requires auth
define('REDIS_DB', 1); // Isolated DB for OTP system
// Application Keys
define('APP_KEY', 'f3a9e7c1b8d5f2a4c6e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1');
define('DEVICE_KEY', 'er4er4');
// SMS Sender ID (for iOS SMS delivery)
define('SMS_SENDER_ID', 'انطلق');
// OTP Settings
define('OTP_EXPIRE_SECONDS', 120);
define('MAX_OTP_ATTEMPTS', 5);
// Rate Limiting
define('RATE_LIMIT_WINDOW', 600); // 10 minutes in seconds
define('RATE_LIMIT_MAX', 3); // Max OTP requests per phone per window
// Caller ID Configuration
// Format: +96279XX{OTP} — XX = random 2 digits
define('CALLER_ID_PREFIX', '+96279');
// Logging
define('LOG_REQUESTS', true);
define('LOG_FILE', __DIR__ . '/logs/api.log');
// Timezone
date_default_timezone_set('Asia/Amman');

110
backend/database.sql Normal file
View File

@@ -0,0 +1,110 @@
-- ============================================
-- Flash Call OTP System — Database Setup
-- Domain: otp.intaleqapp.com
-- ============================================
CREATE DATABASE IF NOT EXISTS otpDb
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE otpDb;
-- OTP Requests Table
CREATE TABLE IF NOT EXISTS otp_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
phone VARCHAR(20) NOT NULL,
otp_code VARCHAR(6) NOT NULL,
caller_id VARCHAR(20) NOT NULL DEFAULT '',
status ENUM('pending','pending_sms','calling','completed','failed','expired','verified') NOT NULL DEFAULT 'pending',
method ENUM('flash_call','sms') NOT NULL DEFAULT 'flash_call',
device_id VARCHAR(50) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
verified_at TIMESTAMP NULL,
INDEX idx_phone (phone),
INDEX idx_status (status),
INDEX idx_method_status (method, status),
INDEX idx_device_id (device_id),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Caller Devices Table
CREATE TABLE IF NOT EXISTS caller_devices (
id INT AUTO_INCREMENT PRIMARY KEY,
device_id VARCHAR(50) UNIQUE NOT NULL,
phone_number VARCHAR(20) NOT NULL,
sim_slot TINYINT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
last_seen TIMESTAMP NULL,
calls_today INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_active (is_active),
INDEX idx_calls_today (calls_today)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- API Request Logs Table
CREATE TABLE IF NOT EXISTS api_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
endpoint VARCHAR(50) NOT NULL,
method VARCHAR(10) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
user_agent VARCHAR(500) NULL,
request_body TEXT NULL,
response_code SMALLINT NOT NULL DEFAULT 200,
error VARCHAR(500) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_endpoint (endpoint),
INDEX idx_created (created_at),
INDEX idx_ip (ip_address)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================
-- Scheduled Cleanup (add to MySQL Event Scheduler)
-- ============================================
-- Enable event scheduler
SET GLOBAL event_scheduler = ON;
-- Expire old pending requests every 30 seconds
DELIMITER //
CREATE EVENT IF NOT EXISTS expire_otp_requests
ON SCHEDULE EVERY 30 SECOND
DO
BEGIN
UPDATE otp_requests
SET status = 'expired', updated_at = NOW()
WHERE status IN ('pending', 'pending_sms', 'calling')
AND expires_at < NOW();
END //
DELIMITER ;
-- Reset calls_today counter daily at midnight (Asia/Amman = UTC+3)
DELIMITER //
CREATE EVENT IF NOT EXISTS reset_daily_calls
ON SCHEDULE EVERY 1 DAY
STARTS CONCAT(CURDATE() + INTERVAL 1 DAY, ' 00:00:00')
DO
BEGIN
UPDATE caller_devices SET calls_today = 0;
END //
DELIMITER ;
-- Clean up old API logs (older than 30 days)
DELIMITER //
CREATE EVENT IF NOT EXISTS cleanup_api_logs
ON SCHEDULE EVERY 1 DAY
STARTS CONCAT(CURDATE() + INTERVAL 1 DAY, ' 03:00:00')
DO
BEGIN
DELETE FROM api_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
END //
DELIMITER ;
-- ============================================
-- Grant permissions
-- ============================================
-- Run this separately with root access:
-- GRANT SELECT, INSERT, UPDATE, DELETE ON otpDb.* TO 'otpUser'@'localhost' IDENTIFIED BY 'STRONG_PASSWORD';
-- FLUSH PRIVILEGES;
sfvo055OJV7VqPW25VEQ

96
backend/includes/Auth.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
/**
* Authentication — App Key Validation
*/
require_once __DIR__ . '/../config.php';
class Auth
{
/**
* Validate the app_key from request.
* Supports both Flutter app key and Caller device key.
*
* @param string|null $key The key provided in request
* @param string $required Which key type is required: 'app' | 'device' | 'any'
* @return bool
*/
public static function validate(?string $key, string $required = 'any'): bool
{
if ($key === null || $key === '') {
return false;
}
switch ($required) {
case 'app':
return hash_equals(APP_KEY, $key);
case 'device':
return hash_equals(DEVICE_KEY, $key);
case 'any':
return hash_equals(APP_KEY, $key) || hash_equals(DEVICE_KEY, $key);
default:
return false;
}
}
/**
* Extract app_key from request (header or body).
*/
public static function getKeyFromRequest(): ?string
{
// Check header first
$headerKey = $_SERVER['HTTP_X_APP_KEY']
?? $_SERVER['HTTP_APP_KEY']
?? null;
if ($headerKey !== null) {
return $headerKey;
}
// Check JSON body
$body = json_decode(file_get_contents('php://input'), true);
if (is_array($body) && isset($body['app_key'])) {
return $body['app_key'];
}
// Check POST data
if (isset($_POST['app_key'])) {
return $_POST['app_key'];
}
return null;
}
/**
* Require authentication — sends 401 and exits on failure.
*/
public static function requireAuth(string $required = 'any'): void
{
$key = self::getKeyFromRequest();
if (!self::validate($key, $required)) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'invalid_app_key',
]);
exit;
}
}
/**
* Determine if the provided key is the device key.
*/
public static function isDeviceKey(?string $key): bool
{
return $key !== null && hash_equals(DEVICE_KEY, $key);
}
/**
* Determine if the provided key is the app key.
*/
public static function isAppKey(?string $key): bool
{
return $key !== null && hash_equals(APP_KEY, $key);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* Database Singleton — PDO Connection
*/
require_once __DIR__ . '/../config.php';
class Database
{
private static ?PDO $instance = null;
private function __construct()
{
try {
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=utf8mb4',
DB_HOST,
DB_NAME
);
self::$instance = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '+03:00'",
]);
} catch (PDOException $e) {
error_log('Database connection failed: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'database_error']);
exit;
}
}
public static function getInstance(): PDO
{
if (self::$instance === null) {
new self();
}
return self::$instance;
}
/** Prevent cloning */
private function __clone() {}
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* Request Logger — Logs all API requests to MySQL
*/
require_once __DIR__ . '/Database.php';
class RequestLogger
{
/**
* Log an API request.
*/
public static function log(
string $endpoint,
string $method,
?array $requestBody = null,
int $responseCode = 200,
?string $error = null
): void {
if (!LOG_REQUESTS) {
return;
}
try {
$db = Database::getInstance();
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
$body = $requestBody ? json_encode($requestBody) : null;
// Mask sensitive fields
if ($body) {
$body = self::maskSensitive($body);
}
$stmt = $db->prepare(
"INSERT INTO api_logs (endpoint, method, ip_address, user_agent, request_body, response_code, error, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())"
);
$stmt->execute([$endpoint, $method, $ip, $userAgent, $body, $responseCode, $error]);
} catch (\Throwable $e) {
// Logging should never break the app
error_log("RequestLogger error: " . $e->getMessage());
}
}
/**
* Mask sensitive fields in request body.
*/
private static function maskSensitive(string $body): string
{
$sensitive = ['app_key', 'password', 'otp', 'otp_code'];
foreach ($sensitive as $field) {
$body = preg_replace(
'/"' . $field . '"\s*:\s*"[^"]*"/',
'"' . $field . '":"***"',
$body
);
}
return $body;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Rate Limiting via Redis
*/
require_once __DIR__ . '/Redis.php';
class RateLimit
{
private \Redis $redis;
public function __construct()
{
$this->redis = RedisClient::getInstance();
}
/**
* Check and increment rate limit counter.
*
* @param string $key Identifier (e.g. "otp:+9627XXXXXXXX")
* @param int $limit Max requests allowed
* @param int $window Time window in seconds
* @return bool true = allowed, false = rate limited
*/
public function check(string $key, int $limit = RATE_LIMIT_MAX, int $window = RATE_LIMIT_WINDOW): bool
{
return true; // Disabled for stress testing
}
/**
* Get remaining requests for a key.
*/
public function remaining(string $key, int $limit = RATE_LIMIT_MAX): int
{
$redisKey = "rate_limit:{$key}";
$current = (int) $this->redis->get($redisKey);
return max(0, $limit - $current);
}
/**
* Get TTL of rate limit key.
*/
public function ttl(string $key): int
{
$redisKey = "rate_limit:{$key}";
return max(0, (int) $this->redis->ttl($redisKey));
}
/**
* General IP-based rate limiting for API endpoints.
*
* @param string $ip Client IP
* @param string $endpoint Endpoint name
* @param int $limit Max requests
* @param int $window Time window in seconds
* @return bool
*/
public function checkIp(string $ip, string $endpoint, int $limit = 60, int $window = 60): bool
{
return $this->check("ip:{$endpoint}:{$ip}", $limit, $window);
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Redis Singleton — phpredis Extension
*/
require_once __DIR__ . '/../config.php';
class RedisClient
{
private static ?\Redis $instance = null;
private function __construct()
{
try {
self::$instance = new \Redis();
self::$instance->connect(REDIS_HOST, REDIS_PORT, 2.0);
if (REDIS_PASSWORD !== null) {
self::$instance->auth(REDIS_PASSWORD);
}
if (REDIS_DB > 0) {
self::$instance->select(REDIS_DB);
}
self::$instance->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_JSON);
} catch (\RedisException $e) {
$errorMsg = $e->getMessage();
error_log('Redis connection failed: ' . $errorMsg);
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'cache_error', 'error' => $errorMsg]);
exit;
} catch (\Exception $e) {
$errorMsg = $e->getMessage();
error_log('Unexpected error in Redis: ' . $errorMsg);
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'cache_error', 'error' => $errorMsg]);
exit;
}
}
public static function getInstance(): \Redis
{
if (self::$instance === null) {
new self();
}
return self::$instance;
}
private function __clone() {}
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}
}

0
backend/logs/.gitkeep Normal file
View File

95
backend/nginx.conf Normal file
View File

@@ -0,0 +1,95 @@
server {
listen 80;
server_name otp.intaleqapp.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name otp.intaleqapp.com;
# SSL — Let's Encrypt
ssl_certificate /etc/letsencrypt/live/otp.intaleqapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/otp.intaleqapp.com/privkey.pem;
# SSL Hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Document root
root /var/www/otp.intaleqapp.com/public;
index index.php;
# Logging
access_log /var/log/nginx/otp.intaleqapp.com.access.log;
error_log /var/log/nginx/otp.intaleqapp.com.error.log;
# API endpoints — route to PHP files
location /api/ {
try_files $uri $uri/ /api$uri.php?$query_string;
# PHP handling
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 30;
}
}
# Block direct access to includes/
location /includes/ {
deny all;
return 404;
}
# Block access to config
location /config.php {
deny all;
return 404;
}
# Block access to hidden files
location ~ /\. {
deny all;
return 404;
}
# Block access to SQL files
location ~ \.sql$ {
deny all;
return 404;
}
# Block access to log files
location ~ \.log$ {
deny all;
return 404;
}
# Health check endpoint
location /health {
access_log off;
return 200 '{"status":"ok"}';
add_header Content-Type application/json;
}
# Default PHP handling
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Deny everything else
location / {
return 404;
}
}

View File

@@ -0,0 +1,48 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.intaleq.flashcall'
compileSdk 35
defaultConfig {
applicationId "com.intaleq.flashcall"
minSdk 26
targetSdk 35
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.work:work-runtime-ktx:2.10.0'
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
implementation 'androidx.lifecycle:lifecycle-service:2.8.7'
implementation 'com.google.code.gson:gson:2.11.0'
// The Kotlin plugin automatically adds the correct stdlib version.
// Invalid version 2.3.10 was removed.
}

View File

@@ -0,0 +1 @@
android.useAndroidX=true

34
caller-app/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,34 @@
# Retrofit
-keepattributes Signature
-keepattributes *Annotation*
-keep class retrofit2.** { *; }
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
# Gson
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Keep data classes for Gson serialization
-keep class com.intaleq.flashcall.PendingTask { *; }
-keep class com.intaleq.flashcall.CallDoneRequest { *; }
-keep class com.intaleq.flashcall.RegisterRequest { *; }
-keep class com.intaleq.flashcall.ApiResponse { *; }
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
# Kotlin Coroutines
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembers class kotlinx.coroutines.** {
volatile <fields>;
}

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="Flash OTP Caller"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
android:usesCleartextTraffic="false">
<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>
<service
android:name=".CallerService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Automation of flash call verification for OTP purposes" />
</service>
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,75 @@
package com.intaleq.flashcall
import com.google.gson.annotations.SerializedName
import retrofit2.http.*
// --- Data Models ---
data class PendingTask(
@SerializedName("task_id") val taskId: Int?,
@SerializedName("phone") val phone: String?,
@SerializedName("caller_id") val callerId: String?,
@SerializedName("otp") val otp: String?,
@SerializedName("timeout_seconds") val timeoutSeconds: Int?
)
data class CallDoneRequest(
@SerializedName("task_id") val taskId: Int,
@SerializedName("device_id") val deviceId: String,
@SerializedName("app_key") val appKey: String,
@SerializedName("result") val result: String
)
data class RegisterRequest(
@SerializedName("device_id") val deviceId: String,
@SerializedName("phone_number") val phoneNumber: String,
@SerializedName("sim_slot") val simSlot: Int,
@SerializedName("app_key") val appKey: String
)
data class ApiResponse(
@SerializedName("success") val success: Boolean,
@SerializedName("message") val message: String?,
@SerializedName("device_id") val deviceId: String?
)
// --- API Interface ---
interface ApiService {
/**
* Polling for a new flash call task
*/
@GET("pending-call.php")
suspend fun pendingCall(
@Query("device_id") deviceId: String,
@Query("app_key") appKey: String
): PendingTask
/**
* Polling for a new SMS task
*/
@GET("pending-sms.php")
suspend fun pendingSms(
@Query("device_id") deviceId: String,
@Query("app_key") appKey: String
): PendingTask
/**
* Reporting the result of a flash call
*/
@POST("call-done.php")
suspend fun callDone(@Body request: CallDoneRequest): ApiResponse
/**
* Reporting the result of an SMS
*/
@POST("sms-done.php")
suspend fun smsDone(@Body request: CallDoneRequest): ApiResponse
/**
* Initial registration of the device
*/
@POST("register-device.php")
suspend fun registerDevice(@Body request: RegisterRequest): ApiResponse
}

View File

@@ -0,0 +1,26 @@
package com.intaleq.flashcall
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
intent.action == "android.intent.action.QUICKBOOT_POWERON") {
val prefs = context.getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
val isRegistered = prefs.getBoolean("is_registered", false)
if (isRegistered) {
val serviceIntent = Intent(context, CallerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}
}
}
}

View File

@@ -0,0 +1,327 @@
package com.intaleq.flashcall
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.content.pm.ServiceInfo
import android.telephony.SmsManager
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.*
import java.text.SimpleDateFormat
import java.util.*
class CallerService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var flashCallManager: FlashCallManager
private var callPollerJob: Job? = null
private var smsPollerJob: Job? = null
companion object {
var isRunning = false
private set
// Callback for MainActivity to receive log messages
var logListener: ((String) -> Unit)? = null
private const val CHANNEL_ID = "flash_call_service_channel"
private const val NOTIFICATION_ID = 1001
}
private val notificationManager by lazy {
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
override fun onCreate() {
super.onCreate()
isRunning = true
flashCallManager = FlashCallManager(this)
createNotificationChannel()
// startForeground(NOTIFICATION_ID, buildNotification("Flash OTP Caller — Active"))
// ... inside onCreate() ...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(
NOTIFICATION_ID,
buildNotification("Flash OTP Caller — Active"),
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
)
} else {
startForeground(NOTIFICATION_ID, buildNotification("Flash OTP Caller — Active"))
}
// Register SMS hardware status receiver
val filter = android.content.IntentFilter("SMS_SENT")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(smsReceiver, filter, Context.RECEIVER_EXPORTED)
} else {
registerReceiver(smsReceiver, filter)
}
startPolling()
addLog("Service started")
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
callPollerJob?.cancel()
smsPollerJob?.cancel()
serviceScope.cancel()
try {
unregisterReceiver(smsReceiver)
} catch (e: Exception) {
// Ignore if not registered
}
isRunning = false
addLog("Service stopped")
}
private fun startPolling() {
callPollerJob = serviceScope.launch {
while (isActive) {
try {
pollCallTask()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
addLog("Call poll error: ${e.message}")
}
delay(3000)
}
}
smsPollerJob = serviceScope.launch {
while (isActive) {
try {
pollSmsTask()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
addLog("SMS poll error: ${e.message}")
}
delay(3000)
}
}
}
private suspend fun pollCallTask() {
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
val deviceId = prefs.getString("device_id", null) ?: return
val appKey = prefs.getString("app_key", null) ?: return
RetrofitClient.setAppKey(appKey)
val api = RetrofitClient.apiService
val task = try {
val result = api.pendingCall(deviceId, appKey)
addLog("Call poll OK: taskId=${result.taskId}")
result
} catch (e: retrofit2.HttpException) {
addLog("Call poll HTTP ${e.code()}: ${e.message()}")
return
} catch (e: Exception) {
addLog("Call poll net error: ${e.javaClass.simpleName}: ${e.message}")
return
}
val taskId = task.taskId ?: return
val phone = task.phone ?: return
addLog("Call task #$taskId$phone")
val result = flashCallManager.makeFlashCall(phone)
addLog("Call #$taskId result: $result")
// Report result back to server
try {
api.callDone(
CallDoneRequest(
taskId = taskId,
deviceId = deviceId,
appKey = appKey,
result = result
)
)
addLog("Call #$taskId reported: $result")
} catch (e: Exception) {
addLog("Call #$taskId report failed: ${e.message}")
}
updateNotification("Last call: #$taskId$result")
}
private suspend fun pollSmsTask() {
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
val deviceId = prefs.getString("device_id", null) ?: return
val appKey = prefs.getString("app_key", null) ?: return
RetrofitClient.setAppKey(appKey)
val api = RetrofitClient.apiService
val task = try {
val result = api.pendingSms(deviceId, appKey)
addLog("SMS poll OK: taskId=${result.taskId}")
result
} catch (e: retrofit2.HttpException) {
addLog("SMS poll HTTP ${e.code()}: ${e.message()}")
return
} catch (e: Exception) {
addLog("SMS poll net error: ${e.javaClass.simpleName}: ${e.message}")
return
}
val taskId = task.taskId ?: return
val phone = task.phone ?: return
val otp = task.otp ?: return
addLog("SMS task #$taskId$phone (OTP: $otp)")
val result = sendSms(phone, otp)
addLog("SMS #$taskId result: $result")
// Report result back to server
try {
api.smsDone(
CallDoneRequest(
taskId = taskId,
deviceId = deviceId,
appKey = appKey,
result = result
)
)
addLog("SMS #$taskId reported: $result")
} catch (e: Exception) {
addLog("SMS #$taskId report failed: ${e.message}")
}
updateNotification("Last SMS: #$taskId$result")
}
private fun sendSms(phone: String, otp: String): String {
return try {
val message = "رمز التحقق في تطبيق انطلق هو: $otp"
// Format phone to local Jordan format if needed (+962 -> 0)
val formattedPhone = if (phone.startsWith("+962")) {
val suffix = phone.substring(4)
if (suffix.startsWith("0")) suffix else "0$suffix"
} else {
phone
}
addLog("SMS sending to $formattedPhone...")
// Create a PendingIntent to track if SMS was sent (Explicit intent for Android 14+)
val sentIntent = android.app.PendingIntent.getBroadcast(
this, 0,
Intent("SMS_SENT").apply { setPackage(packageName) },
android.app.PendingIntent.FLAG_IMMUTABLE
)
val smsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getSystemService(SmsManager::class.java)
} else {
@Suppress("DEPRECATION")
SmsManager.getDefault()
}
smsManager.sendTextMessage(formattedPhone, null, message, sentIntent, null)
addLog("SMS handed to OS for $formattedPhone")
"success"
} catch (e: SecurityException) {
addLog("SMS SecurityException: ${e.message}")
"failed"
} catch (e: Exception) {
addLog("SMS Exception: ${e.javaClass.simpleName}: ${e.message}")
"failed"
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Flash OTP Caller Service",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Keeps the flash call service running"
setShowBadge(false)
}
notificationManager.createNotificationChannel(channel)
}
}
private fun buildNotification(text: String): Notification {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Flash OTP Caller")
.setContentText(text)
.setSmallIcon(android.R.drawable.ic_menu_call)
.setOngoing(true)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
private fun updateNotification(text: String) {
try {
val notification = buildNotification(text)
notificationManager.notify(NOTIFICATION_ID, notification)
} catch (e: Exception) {
// Ignore notification update errors
}
}
private fun addLog(message: String) {
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
val logLine = "[$timestamp] $message"
// Also log to system Logcat for Android Studio visibility
android.util.Log.d("CallerService", logLine)
// Save to SharedPreferences for MainActivity to read
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
val existingLogs = prefs.getString("service_logs", "") ?: ""
val logs = (existingLogs.split("\n") + logLine).takeLast(20)
prefs.edit().putString("service_logs", logs.joinToString("\n")).apply()
// Notify listener if attached
logListener?.invoke(logLine)
}
private val smsReceiver = object : android.content.BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "SMS_SENT") {
val result = when (resultCode) {
android.app.Activity.RESULT_OK -> "SUCCESS (Hardware confirmed)"
android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE -> "FAILED (Generic/No Credit)"
android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE -> "FAILED (No Cell Service)"
android.telephony.SmsManager.RESULT_ERROR_NULL_PDU -> "FAILED (Null PDU)"
android.telephony.SmsManager.RESULT_ERROR_RADIO_OFF -> "FAILED (Radio Off/Airplane Mode)"
else -> "FAILED (Unknown code: $resultCode)"
}
addLog("SMS Hardware Result: $result")
}
}
}
}

View File

@@ -0,0 +1,78 @@
package com.intaleq.flashcall
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.telecom.TelecomManager
import android.telephony.TelephonyManager
import kotlinx.coroutines.delay
import java.lang.reflect.Method
class FlashCallManager(private val context: Context) {
/**
* Make a flash call: dial the number, wait 2500ms, then hang up.
* Returns result: "success", "failed", "busy", "no_answer"
*/
suspend fun makeFlashCall(phone: String): String {
return try {
// Place the call
android.util.Log.d("CallerService", "Initiating flash call to $phone")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.placeCall(Uri.parse("tel:$phone"), null)
} else {
val callIntent = Intent(Intent.ACTION_CALL, Uri.parse("tel:$phone")).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(callIntent)
}
// Wait 1000ms — enough for 1 quick ring
delay(1000)
// Hang up the call
val hungUp = endCall()
if (hungUp) "success" else "failed"
} catch (e: SecurityException) {
"failed"
} catch (e: Exception) {
"failed"
}
}
/**
* End the current active call.
* Primary: TelecomManager (Android 9+)
* Fallback: Reflection on TelephonyManager.endCall() (Android 8)
*/
@SuppressLint("NewApi")
fun endCall(): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Android 9+: Use TelecomManager
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.endCall()
} else {
// Android 8: Use reflection on TelephonyManager
endCallReflection()
}
} catch (e: Exception) {
// Try reflection as fallback
endCallReflection()
}
}
@Suppress("PrivateApi")
private fun endCallReflection(): Boolean {
return try {
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val method: Method = TelephonyManager::class.java.getDeclaredMethod("endCall")
method.invoke(telephonyManager) as? Boolean ?: false
} catch (e: Exception) {
false
}
}
}

View File

@@ -0,0 +1,316 @@
package com.intaleq.flashcall
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.*
class MainActivity : AppCompatActivity() {
private lateinit var etDeviceId: EditText
private lateinit var etPhoneNumber: EditText
private lateinit var etAppKey: EditText
private lateinit var spinnerSimSlot: Spinner
private lateinit var btnRegister: Button
private lateinit var tvStatus: TextView
private lateinit var btnToggleService: Button
private lateinit var tvLog: TextView
private lateinit var logScrollView: ScrollView
private lateinit var progressBar: ProgressBar
private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val prefs by lazy { getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE) }
private val simSlotOptions = arrayOf("SIM 0", "SIM 1")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initViews()
loadSavedData()
setupSpinner()
setupClickListeners()
requestPermissionsIfNeeded()
updateServiceStatus()
loadServiceLogs()
}
override fun onResume() {
super.onResume()
updateServiceStatus()
loadServiceLogs()
// Attach log listener
CallerService.logListener = { logLine ->
runOnUiThread {
appendLogLine(logLine)
}
}
}
override fun onPause() {
super.onPause()
CallerService.logListener = null
}
private fun initViews() {
etDeviceId = findViewById(R.id.etDeviceId)
etPhoneNumber = findViewById(R.id.etPhoneNumber)
etAppKey = findViewById(R.id.etAppKey)
spinnerSimSlot = findViewById(R.id.spinnerSimSlot)
btnRegister = findViewById(R.id.btnRegister)
tvStatus = findViewById(R.id.tvStatus)
btnToggleService = findViewById(R.id.btnToggleService)
tvLog = findViewById(R.id.tvLog)
logScrollView = findViewById(R.id.logScrollView)
progressBar = findViewById(R.id.progressBar)
}
private fun loadSavedData() {
// Device ID — generate once and persist
var deviceId = prefs.getString("device_id", null)
if (deviceId == null) {
deviceId = UUID.randomUUID().toString()
prefs.edit().putString("device_id", deviceId).apply()
}
etDeviceId.setText(deviceId)
etDeviceId.isEnabled = false
etDeviceId.isFocusable = false
// Phone number
val savedPhone = prefs.getString("phone_number", null)
if (savedPhone != null) {
etPhoneNumber.setText(savedPhone)
}
// App key
val savedAppKey = prefs.getString("app_key", null)
if (savedAppKey != null) {
etAppKey.setText(savedAppKey)
}
// SIM slot
val savedSimSlot = prefs.getInt("sim_slot", 0)
spinnerSimSlot.setSelection(savedSimSlot)
}
private fun setupSpinner() {
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, simSlotOptions)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerSimSlot.adapter = adapter
}
private fun setupClickListeners() {
btnRegister.setOnClickListener {
registerDevice()
}
btnToggleService.setOnClickListener {
if (CallerService.isRunning) {
stopCallerService()
} else {
startCallerService()
}
}
}
private fun requestPermissionsIfNeeded() {
if (!PermissionHelper.areAllPermissionsGranted(this)) {
PermissionHelper.requestAllPermissions(this)
}
// Request battery optimization exemption on first launch
val hasRequestedBattery = prefs.getBoolean("requested_battery_opt", false)
if (!hasRequestedBattery) {
PermissionHelper.requestBatteryOptimizationExemption(this)
prefs.edit().putBoolean("requested_battery_opt", true).apply()
}
}
private fun registerDevice() {
val phoneNumber = etPhoneNumber.text.toString().trim()
val appKey = etAppKey.text.toString().trim()
val deviceId = etDeviceId.text.toString().trim()
val simSlot = spinnerSimSlot.selectedItemPosition
if (phoneNumber.isEmpty()) {
etPhoneNumber.error = "Phone number is required"
return
}
if (appKey.isEmpty()) {
etAppKey.error = "App key is required"
return
}
if (!PermissionHelper.areAllPermissionsGranted(this)) {
Toast.makeText(this, "Please grant all permissions first", Toast.LENGTH_LONG).show()
PermissionHelper.requestAllPermissions(this)
return
}
btnRegister.isEnabled = false
progressBar.visibility = View.VISIBLE
mainScope.launch {
try {
RetrofitClient.setAppKey(appKey)
val response = RetrofitClient.apiService.registerDevice(
RegisterRequest(
deviceId = deviceId,
phoneNumber = phoneNumber,
simSlot = simSlot,
appKey = appKey
)
)
if (response.success) {
// Save registration data
prefs.edit()
.putBoolean("is_registered", true)
.putString("phone_number", phoneNumber)
.putString("app_key", appKey)
.putInt("sim_slot", simSlot)
.apply()
Toast.makeText(this@MainActivity, "Device registered successfully!", Toast.LENGTH_SHORT).show()
appendLogLine("Device registered: $deviceId")
} else {
val message = response.message ?: "Registration failed"
Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show()
appendLogLine("Registration failed: $message")
}
} catch (e: Exception) {
Toast.makeText(this@MainActivity, "Error: ${e.message}", Toast.LENGTH_LONG).show()
appendLogLine("Registration error: ${e.message}")
} finally {
btnRegister.isEnabled = true
progressBar.visibility = View.GONE
}
}
}
private fun startCallerService() {
if (!PermissionHelper.areAllPermissionsGranted(this)) {
Toast.makeText(this, "Please grant all permissions first", Toast.LENGTH_LONG).show()
PermissionHelper.requestAllPermissions(this)
return
}
val isRegistered = prefs.getBoolean("is_registered", false)
if (!isRegistered) {
Toast.makeText(this, "Please register the device first", Toast.LENGTH_LONG).show()
return
}
val serviceIntent = Intent(this, CallerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
// Update UI immediately (optimistic)
tvStatus.text = "Service Starting... ⏳"
tvStatus.setTextColor(getColor(android.R.color.holo_orange_dark))
btnToggleService.text = "Stop Service"
appendLogLine("Service start requested")
}
private fun stopCallerService() {
val serviceIntent = Intent(this, CallerService::class.java)
stopService(serviceIntent)
// Update UI immediately (optimistic)
tvStatus.text = "Service Stopping... ⏳"
tvStatus.setTextColor(getColor(android.R.color.holo_orange_dark))
btnToggleService.text = "Start Service"
appendLogLine("Service stop requested")
}
private fun updateServiceStatus() {
if (CallerService.isRunning) {
tvStatus.text = "Service Running ✅"
tvStatus.setTextColor(getColor(android.R.color.holo_green_dark))
btnToggleService.text = "Stop Service"
} else {
tvStatus.text = "Service Stopped ❌"
tvStatus.setTextColor(getColor(android.R.color.holo_red_dark))
btnToggleService.text = "Start Service"
}
}
private fun loadServiceLogs() {
val logs = prefs.getString("service_logs", "") ?: ""
tvLog.text = logs
logScrollView.post {
logScrollView.fullScroll(ScrollView.FOCUS_DOWN)
}
}
private fun appendLogLine(line: String) {
val current = tvLog.text.toString()
val lines = (current.split("\n") + line).takeLast(20)
tvLog.text = lines.joinToString("\n")
logScrollView.post {
logScrollView.fullScroll(ScrollView.FOCUS_DOWN)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PermissionHelper.REQUEST_CODE_PERMISSIONS) {
val allGranted = grantResults.all { it == android.content.pm.PackageManager.PERMISSION_GRANTED }
if (!allGranted) {
Toast.makeText(
this,
"Some permissions were denied. The app may not function correctly.",
Toast.LENGTH_LONG
).show()
// Show which permissions are missing
val denied = permissions.filterIndexed { index, _ ->
grantResults[index] != android.content.pm.PackageManager.PERMISSION_GRANTED
}
if (denied.isNotEmpty()) {
AlertDialog.Builder(this)
.setTitle("Permissions Required")
.setMessage("The following permissions are required for the app to work:\n\n" +
denied.joinToString("\n") { perm ->
when (perm) {
android.Manifest.permission.CALL_PHONE -> "• Phone - to make flash calls"
android.Manifest.permission.READ_PHONE_STATE -> "• Phone State - to read call status"
android.Manifest.permission.SEND_SMS -> "• SMS - to send OTP messages"
else -> "$perm"
}
} +
"\n\nPlease grant them in Settings.")
.setPositiveButton("Open Settings") { _, _ ->
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = android.net.Uri.fromParts("package", packageName, null)
startActivity(intent)
}
.setNegativeButton("Cancel", null)
.show()
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
package com.intaleq.flashcall
import android.Manifest
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
object PermissionHelper {
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CALL_PHONE,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.SEND_SMS
)
fun areAllPermissionsGranted(context: Context): Boolean {
return REQUIRED_PERMISSIONS.all { permission ->
ContextCompat.checkSelfPermission(context, permission) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
}
}
fun requestAllPermissions(activity: Activity) {
val permissionsToRequest = REQUIRED_PERMISSIONS.filter { permission ->
ContextCompat.checkSelfPermission(activity, permission) !=
android.content.pm.PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
// Show rationale for any permission that was previously denied
val shouldShowRationale = permissionsToRequest.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale) {
// In a production app you'd show a dialog here explaining why
// For now, just request the permissions directly
ActivityCompat.requestPermissions(
activity,
permissionsToRequest.toTypedArray(),
REQUEST_CODE_PERMISSIONS
)
} else {
ActivityCompat.requestPermissions(
activity,
permissionsToRequest.toTypedArray(),
REQUEST_CODE_PERMISSIONS
)
}
}
// Also request battery optimization exemption
requestBatteryOptimizationExemption(activity)
// Request SYSTEM_ALERT_WINDOW (Draw over other apps) for Android 10+ background activity starts
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(activity)) {
try {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${activity.packageName}")
)
activity.startActivity(intent)
} catch (e: Exception) {
// Ignore if not available
}
}
}
fun requestBatteryOptimizationExemption(activity: Activity) {
val powerManager = activity.getSystemService(Context.POWER_SERVICE) as PowerManager
val packageName = activity.packageName
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
try {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:$packageName")
}
activity.startActivity(intent)
} catch (e: Exception) {
// Fallback: open battery optimization settings
try {
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
activity.startActivity(intent)
} catch (e2: Exception) {
// Cannot open settings, ignore
}
}
}
}
fun shouldShowCallPhoneRationale(activity: Activity): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(
activity,
Manifest.permission.CALL_PHONE
)
}
fun shouldShowReadPhoneStateRationale(activity: Activity): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(
activity,
Manifest.permission.READ_PHONE_STATE
)
}
fun shouldShowSendSmsRationale(activity: Activity): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(
activity,
Manifest.permission.SEND_SMS
)
}
const val REQUEST_CODE_PERMISSIONS = 1001
}

View File

@@ -0,0 +1,44 @@
package com.intaleq.flashcall
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object RetrofitClient {
private const val BASE_URL = "https://otp.intaleqapp.com/api/"
private var appKey: String = ""
fun setAppKey(key: String) {
appKey = key
}
private val authInterceptor = Interceptor { chain ->
val original = chain.request()
val request = original.newBuilder()
.header("X-App-Key", appKey)
.build()
chain.proceed(request)
}
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build()
}
val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="49.59793"
android:startX="42.9492"
android:endY="92.4963"
android:endX="85.84757"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeWidth="1"
android:strokeColor="#00000000"/>
</vector>

View File

@@ -0,0 +1,220 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:background="#FAFAFA">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Device Setup Section -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:elevation="4dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Device Setup"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="12dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Device ID">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:focusable="false"
android:textSize="12sp"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Phone Number">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPhoneNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Server App Key">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAppKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SIM Slot:"
android:textSize="14sp"
android:textColor="#666666"
android:layout_marginEnd="12dp" />
<Spinner
android:id="@+id/spinnerSimSlot"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:spinnerMode="dropdown" />
</LinearLayout>
<Button
android:id="@+id/btnRegister"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="Register Device"
android:textAllCaps="false"
android:textSize="15sp"
android:backgroundTint="#4CAF50"
android:textColor="#FFFFFF" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Service Control Section -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:elevation="4dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Service Control"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Service Stopped ❌"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#F44336"
android:gravity="center"
android:layout_marginBottom="12dp" />
<Button
android:id="@+id/btnToggleService"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="Start Service"
android:textAllCaps="false"
android:textSize="15sp"
android:backgroundTint="#4CAF50"
android:textColor="#FFFFFF" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Live Log Section -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Live Log"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="8dp" />
<ScrollView
android:id="@+id/logScrollView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#F5F5F5"
android:padding="8dp">
<TextView
android:id="@+id/tvLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="#555555"
android:fontFamily="monospace"
android:lineSpacingExtra="4dp" />
</ScrollView>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#4CAF50</color>
<color name="primary_dark">#388E3C</color>
<color name="primary_light">#C8E6C9</color>
<color name="accent">#4CAF50</color>
<color name="text_primary">#333333</color>
<color name="text_secondary">#666666</color>
<color name="text_hint">#999999</color>
<color name="background">#FAFAFA</color>
<color name="card_background">#FFFFFF</color>
<color name="success_green">#4CAF50</color>
<color name="error_red">#F44336</color>
<color name="log_background">#F5F5F5</color>
<color name="log_text">#555555</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
</resources>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Flash OTP Caller</string>
<string name="device_setup">Device Setup</string>
<string name="service_control">Service Control</string>
<string name="device_id_hint">Device ID</string>
<string name="phone_number_hint">Phone Number</string>
<string name="app_key_hint">Server App Key</string>
<string name="sim_slot_label">SIM Slot</string>
<string name="register_button">Register Device</string>
<string name="start_service">Start Service</string>
<string name="stop_service">Stop Service</string>
<string name="service_running">Service Running ✅</string>
<string name="service_stopped">Service Stopped ❌</string>
<string name="live_log">Live Log</string>
<string name="permission_required">Permissions Required</string>
<string name="permission_rationale">This app requires Phone, SMS, and Phone State permissions to make flash calls and send OTP messages.</string>
<string name="registration_success">Device registered successfully!</string>
<string name="registration_failed">Registration failed</string>
<string name="permissions_denied">Some permissions were denied. The app may not function correctly.</string>
<string name="register_first">Please register the device first</string>
<string name="grant_permissions_first">Please grant all permissions first</string>
</resources>

15
caller-app/build.gradle Normal file
View File

@@ -0,0 +1,15 @@
buildscript {
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.9.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
tasks.register('clean', Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,10 @@
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m
org.gradle.parallel=true
org.gradle.caching=true
# AndroidX Support
android.useAndroidX=true
android.enableJetifier=true
# Kotlin
kotlin.code.style=official

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "FlashOTPCaller"
include ':app'

36
deploy.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# --- Configuration ---
GIT_BRANCH="main"
# Colors for terminal styling
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
echo -e "${GREEN}🚀 Starting Flash Call OTP Sync & Push...${NC}"
# 1. Commit local changes automatically (with date/time or custom message)
if [ -n "$(git status --porcelain)" ]; then
COMMIT_MSG=${1:-"Deploy: $(date '+%Y-%m-%d %H:%M:%S')"}
echo -e "${YELLOW}Staging and committing changes with message: ${COMMIT_MSG}${NC}"
git add .
git commit -m "$COMMIT_MSG"
echo -e "${GREEN}✅ Local commit created successfully!${NC}"
else
echo -e " No local changes to commit."
fi
# 2. Push to Git Remote
echo -e "${GREEN}📤 Pushing changes to remote repository (${GIT_BRANCH})...${NC}"
git push origin "$GIT_BRANCH"
if [ $? -ne 0 ]; then
echo -e "${RED}❌ Git Push failed!${NC}"
exit 1
fi
echo -e "${GREEN}==========================================${NC}"
echo -e "✨ Flash Call OTP changes pushed to Git successfully! "
echo -e "==========================================${NC}"

289
docs/PERMISSIONS.md Normal file
View File

@@ -0,0 +1,289 @@
# Flash Call OTP System — Permissions Guide
## Table of Contents
1. [Android Caller App Permissions](#1-android-caller-app-permissions)
2. [Flutter Receiver App Permissions](#2-flutter-receiver-app-permissions)
3. [Server-Side Security](#3-server-side-security)
4. [iOS-Specific Notes](#4-ios-specific-notes)
5. [Permission Request Flow](#5-permission-request-flow)
---
## 1. Android Caller App Permissions
### CALL_PHONE
- **Permission String**: `android.permission.CALL_PHONE`
- **Protection Level**: Dangerous
- **Required Since**: API 1
- **Purpose**: Required to programmatically place phone calls without user interaction. The app uses `ACTION_CALL` with `Uri.parse("tel:$phone")` to automatically dial the target number and hang up after 2.5 seconds.
- **Why Not ACTION_DIAL**: `ACTION_DIAL` only opens the dialer and requires the user to tap the call button. Flash call verification must be completely automatic — no user interaction.
- **Android Version Notes**:
- API 26-27 (Android 8.0-8.1): Standard dangerous permission, requested at runtime
- API 28+ (Android 9+): Works as standard dangerous permission
- If denied: The app cannot make flash calls. The service will report "failed" for all call tasks.
### READ_PHONE_STATE
- **Permission String**: `android.permission.READ_PHONE_STATE`
- **Protection Level**: Dangerous
- **Required Since**: API 1
- **Purpose**: Required to read telephony state information including call state, network type, and SIM information. Used to detect when a call is active so the app knows when to hang up.
- **Android Version Notes**:
- API 26-28 (Android 8-9): Standard dangerous permission
- API 29+ (Android 10+): More restricted; the app only needs this for call state detection
- If denied: Call state detection may not work, but the 2.5-second timer still hangs up reliably
### SEND_SMS
- **Permission String**: `android.permission.SEND_SMS`
- **Protection Level**: Dangerous
- **Required Since**: API 1
- **Purpose**: Required to send SMS messages containing OTP codes to iOS users. When the backend has an SMS delivery task, the Caller App sends the OTP via SMS using `SmsManager`.
- **Why Needed**: iOS devices cannot receive flash calls for automatic OTP extraction. The system falls back to SMS delivery, and the Caller Android device acts as the SMS sender.
- **Android Version Notes**:
- API 26-30 (Android 8-11): Uses `SmsManager.getDefault()`
- API 31+ (Android 12+): Uses `context.getSystemService(SmsManager::class.java)` for per-subscription SMS handling
- If denied: SMS tasks will fail. Flash calls for Android devices still work.
### FOREGROUND_SERVICE
- **Permission String**: `android.permission.FOREGROUND_SERVICE`
- **Protection Level**: Normal
- **Required Since**: API 26 (Android 8.0)
- **Purpose**: Required to run a foreground service that continuously polls the backend for pending tasks. The service must be a foreground service (not a background service) to avoid being killed by the system.
- **Android Version Notes**:
- API 26+ (Android 8.0+): Mandatory for any long-running service
- Without this: The service would be killed within a few minutes by the system
### FOREGROUND_SERVICE_PHONE_CALL
- **Permission String**: `android.permission.FOREGROUND_SERVICE_PHONE_CALL`
- **Protection Level**: Normal
- **Required Since**: API 34 (Android 14)
- **Purpose**: Specifies the foreground service type as "phone call". Required on Android 14+ for services that place phone calls.
- **Android Version Notes**:
- API 34+ (Android 14): Must be declared in manifest AND specified in `startForeground()` call
- API 26-33: Not required, but harmless to declare
- Without this on Android 14: The foreground service will crash with SecurityException
### RECEIVE_BOOT_COMPLETED
- **Permission String**: `android.permission.RECEIVE_BOOT_COMPLETED`
- **Protection Level**: Normal
- **Required Since**: API 1
- **Purpose**: Allows the app to receive the `BOOT_COMPLETED` broadcast and automatically restart the CallerService after the device reboots. Without this, the user would have to manually open the app and start the service after every restart.
- **Android Version Notes**:
- All versions: Works as a normal permission (auto-granted)
- Note: Some OEMs (Xiaomi, Huawei, Oppo) restrict auto-start by default. Users must enable "Auto-start" in security settings.
### REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
- **Permission String**: `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`
- **Protection Level**: Normal
- **Required Since**: API 23 (Android 6.0)
- **Purpose**: Prompts the user to add the app to the battery optimization whitelist. This is critical because Android's Doze mode and app standby can kill the foreground service, preventing the device from receiving OTP tasks.
- **How It Works**: The app sends an `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` intent with the app's package URI. The system shows a dialog asking the user to allow the app to ignore battery optimization.
- **Android Version Notes**:
- API 23+ (Android 6.0+): Works as normal permission with system dialog
- Some OEMs: May require additional steps (e.g., Xiaomi: Security → Autostart; Samsung: Device Care → Battery → App Power Management)
- Without this: The service may be killed after 10-30 minutes of screen-off time
### INTERNET
- **Permission String**: `android.permission.INTERNET`
- **Protection Level**: Normal
- **Required Since**: API 1
- **Purpose**: Required for all network communication — polling the backend API and reporting call/SMS results.
- **Note**: Automatically granted on all Android versions.
---
## 2. Flutter Receiver App Permissions
### READ_CALL_LOG (Android Only)
- **Permission String**: `android.permission.READ_CALL_LOG`
- **Protection Level**: Dangerous
- **Required Since**: API 16
- **Purpose**: Required to read the device's call log and detect incoming missed calls from the flash call. The app queries the call log every 1.5 seconds looking for missed calls since the OTP was requested.
- **How OTP Extraction Works**: When a flash call comes in, it appears as a missed call in the call log. The app reads the caller's phone number (e.g., +962791234829) and extracts the last 4 digits (4829) as the OTP code.
- **Android Version Notes**:
- API 16-22 (Android 4.1-5.1): Granted at install time
- API 23+ (Android 6.0+): Must be requested at runtime
- If denied: Automatic OTP detection will not work. User must manually enter the OTP code via the fallback input.
### READ_PHONE_STATE (Android Only)
- **Permission String**: `android.permission.READ_PHONE_STATE`
- **Protection Level**: Dangerous
- **Required Since**: API 1
- **Purpose**: Companion permission for READ_CALL_LOG. Some Android versions require both permissions to properly access call log data.
- **Android Version Notes**:
- API 29+ (Android 10+): More restricted; may need to request both READ_CALL_LOG and READ_PHONE_STATE together
- If denied: Call log access may fail on some devices
### No Extra Permissions on iOS
- iOS does not allow reading call logs or SMS messages programmatically
- SMS AutoFill works automatically through `AutofillHints.oneTimeCode` without any special permissions
- The only requirement is that the SMS follows Apple's format with `@domain #code` at the end
---
## 3. Server-Side Security
### API Key Authentication
- **APP_KEY**: Used by the Flutter Receiver App for `request-otp` and `verify-otp` endpoints
- **DEVICE_KEY**: Used by the Android Caller App for `pending-call`, `pending-sms`, `call-done`, `sms-done`, and `register-device` endpoints
- **Implementation**: Uses `hash_equals()` for timing-safe comparison to prevent timing attacks
- **Key Generation**: Use `openssl rand -hex 32` to generate cryptographically secure keys
### Rate Limiting
- **Per-Phone Limit**: Max 3 OTP requests per phone number per 10-minute window
- **Per-IP Limit**: Max 30 requests per IP per minute for `request-otp`
- **General API**: Max 60 requests per IP per minute across all endpoints
- **Implementation**: Redis-based with atomic increment operations and TTL-based expiration
### Input Validation
- **Phone Format**: E.164 format validation (e.g., +9627XXXXXXXX) — regex: `/^\+[1-9]\d{6,14}$/`
- **OTP Format**: Exactly 4 digits — regex: `/^\d{4}$/`
- **Device ID**: Length between 5-50 characters
- **SQL Injection**: All queries use PDO prepared statements
- **XSS**: JSON-only responses (no HTML rendering)
### Data Protection
- OTP codes stored in Redis with 120-second TTL (auto-expire)
- OTP codes in MySQL are for audit purposes only
- Failed verification attempts tracked in Redis (max 5 attempts)
- Request logging masks sensitive fields (app_key, otp, password)
- API logs auto-cleaned after 30 days
---
## 4. iOS-Specific Notes
### Why Flash Call Doesn't Work on iOS
Apple restricts access to the call log through `CoreTelephony` framework. There is no public API to:
- Read incoming/outgoing call history
- Get caller ID from recent calls programmatically
- Detect missed calls in real-time
This is a deliberate privacy decision by Apple. The workaround is to use SMS delivery with AutoFill.
### SMS AutoFill Mechanism
iOS can automatically suggest OTP codes from SMS messages in the keyboard suggestion bar. This requires:
1. **Correct SMS format**: The message must include the domain and code at the end:
```
رمز التحقق في Intaleq هو 4829
لا تشاركه مع أحد.
@otp.intaleqapp.com #4829
```
2. **Associated Domains**: The app must have `otp.intaleqapp.com` as an associated domain in its entitlements (for the `#code` to be recognized)
3. **Text field configuration**: The OTP input must have `autofillHints: [AutofillHints.oneTimeCode]` and `keyboardType: TextInputType.number`
### iOS AutoFill Requirements
| Requirement | Details |
|-------------|---------|
| SMS format | Must end with `@domain #code` |
| Associated domain | Configured in Xcode → Signing & Capabilities |
| Text field | `AutofillHints.oneTimeCode` + number keyboard |
| Real device | AutoFill does not work on simulators |
| SMS sender | Must come from a phone number (not alphanumeric) |
---
## 5. Permission Request Flow
### Android Caller App — First Launch
```
App Launched
┌─────────────────────────┐
│ PermissionHelper │
│ .requestAllPermissions()│
└──────────┬──────────────┘
┌──────────────┐
│ CALL_PHONE │──── Denied ──→ Show rationale dialog
│ │ │
│ │──── Granted ──→ Continue
└──────┬───────┘
┌──────────────┐
│READ_PHONE_STATE│── Denied ──→ Show rationale dialog
│ │ │
│ │── Granted ──→ Continue
└──────┬───────┘
┌──────────────┐
│ SEND_SMS │──── Denied ──→ Show rationale dialog
│ │ │
│ │──── Granted ──→ Continue
└──────┬───────┘
┌──────────────────────────────────┐
│ Battery Optimization Exemption │
│ (Intent to system settings) │
│ │
│ User selects "Don't optimize" │
└──────────────────────────────────┘
Ready for Registration
```
### Flutter Receiver App — OTP Flow
```
User taps "Send OTP"
┌─────────────────────────┐
│ Platform check │
└──────┬──────────────────┘
├──── Android ──→ Check READ_CALL_LOG permission
│ │
│ ├── Granted ──→ Start call log polling
│ │
│ ├── Denied ──→ Show rationale:
│ │ "نحتاج صلاحية قراءة سجل المكالمات
│ │ للتحقق التلقائي دون الحاجة لإدخال أي كود"
│ │
│ ├── Permanently Denied ──→ Redirect to Settings
│ │ + Show manual OTP input fallback
│ │
│ └── Still Denied ──→ Show manual OTP input
└──── iOS ──→ Skip call log entirely
Show manual 4-digit input
with AutofillHints.oneTimeCode
iOS keyboard shows SMS code
suggestion automatically
```
### Arabic Permission Explanations
| Permission | Arabic Explanation |
|-----------|-------------------|
| CALL_PHONE | "صلاحية إجراء المكالمات مطلوبة للاتصال التلقائي بأرقام التحقق" |
| READ_PHONE_STATE | "صلاحية قراءة حالة الهاتف مطلوبة لإدارة المكالمات" |
| SEND_SMS | "صلاحية إرسال الرسائل مطلوبة للتحقق من مستخدمي iOS" |
| READ_CALL_LOG | "نحتاج صلاحية قراءة سجل المكالمات للتحقق التلقائي دون الحاجة لإدخال أي كود" |
| Battery Optimization | "يجب تعطيل تحسين البطارية لضمان عمل الخدمة بشكل مستمر" |
---
## Appendix: Permission Summary by Component
| Permission | Caller App | Receiver App | Type | Min API |
|-----------|:----------:|:------------:|------|---------|
| CALL_PHONE | ✅ | ❌ | Dangerous | 1 |
| READ_PHONE_STATE | ✅ | ✅ | Dangerous | 1 |
| SEND_SMS | ✅ | ❌ | Dangerous | 1 |
| READ_CALL_LOG | ❌ | ✅ | Dangerous | 16 |
| FOREGROUND_SERVICE | ✅ | ❌ | Normal | 26 |
| FOREGROUND_SERVICE_PHONE_CALL | ✅ | ❌ | Normal | 34 |
| RECEIVE_BOOT_COMPLETED | ✅ | ❌ | Normal | 1 |
| REQUEST_IGNORE_BATTERY_OPTIMIZATIONS | ✅ | ❌ | Normal | 23 |
| INTERNET | ✅ | ✅ | Normal | 1 |

497
docs/SETUP.md Normal file
View File

@@ -0,0 +1,497 @@
# Flash Call OTP System — Setup Guide
## Table of Contents
1. [Prerequisites](#1-prerequisites)
2. [Server Setup (PHP Backend)](#2-server-setup-php-backend)
3. [Android Caller App Setup](#3-android-caller-app-setup)
4. [Flutter Receiver App Setup](#4-flutter-receiver-app-setup)
5. [End-to-End Testing](#5-end-to-end-testing)
6. [Troubleshooting](#6-troubleshooting)
---
## 1. Prerequisites
### Server Requirements
- Ubuntu 20.04+ or similar Linux distribution
- PHP 8.1+ with phpredis extension
- MySQL 8.0+ or MariaDB 10.6+
- Redis 6.0+
- Nginx 1.18+
- Certbot (Let's Encrypt) for SSL
- Composer (optional, for dependency management)
### Android Caller App Requirements
- Android Studio Hedgehog (2023.1.1) or newer
- Android device running Android 8.0 (API 26) or higher
- Active SIM card installed in the device
- Stable internet connection (Wi-Fi recommended)
### Flutter Receiver App Requirements
- Flutter 3.16+ with Dart 3.x
- Android device for full auto-read testing
- iOS device for SMS AutoFill testing (optional)
- Xcode 15+ (for iOS builds)
---
## 2. Server Setup (PHP Backend)
### 2.1 Install Required Packages
```bash
sudo apt update
sudo apt install -y nginx php8.1-fpm php8.1-mysql php8.1-redis php8.1-mbstring php8.1-xml php8.1-curl mysql-server redis-server
```
### 2.2 Configure MySQL
```bash
sudo mysql_secure_installation
```
Log in to MySQL and create the database and user:
```bash
sudo mysql -u root -p
```
```sql
CREATE DATABASE otp-db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'otp-user'@'localhost' IDENTIFIED BY 'tCO1zuHWVKjTnryDDInL';
GRANT SELECT, INSERT, UPDATE, DELETE ON otp_db.* TO 'otp-user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
### 2.3 Import Database Schema
```bash
mysql -u otp_user -p otp_db < /path/to/backend/database.sql
```
Verify the tables were created:
```bash
mysql -u otp_user -p otp_db -e "SHOW TABLES;"
```
You should see: `api_logs`, `caller_devices`, `otp_requests`
### 2.4 Configure Redis
Edit Redis configuration for security:
```bash
sudo nano /etc/redis/redis.conf
```
Set the following:
```
bind 127.0.0.1
protected-mode yes
# requirepass YOUR_REDIS_PASSWORD # Uncomment and set if needed
```
Restart Redis:
```bash
sudo systemctl restart redis-server
sudo systemctl enable redis-server
```
Test connection:
```bash
redis-cli ping
# Should return: PONG
```
### 2.5 Deploy PHP Backend
Create the web root directory:
```bash
sudo mkdir -p /var/www/otp.intaleqapp.com/public
sudo chown -R www-data:www-data /var/www/otp.intaleqapp.com
```
Copy the backend files:
```bash
# Copy the entire backend directory
cp -r backend/* /var/www/otp.intaleqapp.com/
# The structure should be:
# /var/www/otp.intaleqapp.com/
# ├── api/
# │ ├── request-otp.php
# │ ├── pending-call.php
# │ ├── call-done.php
# │ ├── verify-otp.php
# │ ├── register-device.php
# │ ├── pending-sms.php
# │ └── sms-done.php
# ├── includes/
# │ ├── Database.php
# │ ├── Redis.php
# │ ├── RateLimit.php
# │ ├── Auth.php
# │ └── Logger.php
# ├── config.php
# ├── .htaccess
# ├── database.sql
# ├── nginx.conf
# └── logs/
```
Set permissions:
```bash
sudo chown -R www-data:www-data /var/www/otp.intaleqapp.com
sudo chmod -R 755 /var/www/otp.intaleqapp.com
sudo chmod -R 775 /var/www/otp.intaleqapp.com/logs
```
### 2.6 Configure config.php
Edit the configuration file with your production credentials:
```bash
sudo nano /var/www/otp.intaleqapp.com/config.php
```
Update these critical values:
```php
define('DB_PASS', 'YOUR_ACTUAL_STRONG_PASSWORD');
define('APP_KEY', 'GENERATE_A_SECURE_RANDOM_KEY_HERE');
define('DEVICE_KEY', 'GENERATE_A_DIFFERENT_SECURE_KEY_HERE');
```
Generate secure keys:
```bash
openssl rand -hex 32
```
### 2.7 Configure Nginx
Copy the nginx configuration:
```bash
sudo cp /var/www/otp.intaleqapp.com/nginx.conf /etc/nginx/sites-available/otp.intaleqapp.com
sudo ln -s /etc/nginx/sites-available/otp.intaleqapp.com /etc/nginx/sites-enabled/
```
Test the configuration:
```bash
sudo nginx -t
```
Reload Nginx:
```bash
sudo systemctl reload nginx
```
### 2.8 Install SSL Certificate
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d otp.intaleqapp.com
```
Follow the prompts. Certbot will automatically modify the Nginx config for SSL.
Set up auto-renewal:
```bash
sudo crontab -e
# Add: 0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
```
### 2.9 Configure PHP-FPM
Ensure PHP-FPM is running:
```bash
sudo systemctl enable php8.1-fpm
sudo systemctl start php8.1-fpm
```
Check the socket path matches your nginx config:
```bash
ls -la /var/run/php/php8.1-fpm.sock
```
### 2.10 Verify the Backend
Test the health endpoint:
```bash
curl https://otp.intaleqapp.com/health
# Should return: {"status":"ok"}
```
Test the OTP request (will fail without valid app_key, but confirms routing works):
```bash
curl -X POST https://otp.intaleqapp.com/api/request-otp \
-H "Content-Type: application/json" \
-d '{"phone":"+962790000000","app_key":"wrong_key"}'
# Should return: {"success":false,"message":"invalid_app_key"}
```
---
## 3. Android Caller App Setup
### 3.1 Build the App
1. Open Android Studio
2. Select "Open an existing project"
3. Navigate to the `caller-app/` directory
4. Wait for Gradle sync to complete
5. Connect your Android device (with USB debugging enabled)
6. Click Run ▶️
### 3.2 First-Time Setup on the Device
1. **Install the APK** on the cheap Android phone with SIM card
2. **Grant all permissions** when prompted:
- Phone (CALL_PHONE)
- Phone State (READ_PHONE_STATE)
- SMS (SEND_SMS)
3. **Disable Battery Optimization** — the app will prompt you; select "Don't optimize" for Flash OTP Caller
4. On the main screen:
- **Device ID**: Auto-generated UUID (note this down)
- **Phone Number**: Enter the phone number of the SIM in this device (e.g., +96279XXXXXXX)
- **App Key**: Enter the DEVICE_KEY from your server's config.php
- **SIM Slot**: Select 0 (or 1 for dual SIM)
5. Tap **"Register Device"** — you should see a success message
6. Tap **"Start Service"** — the notification should show "Flash OTP Caller — Active"
### 3.3 Verify Service is Running
- Check the persistent notification in the status bar
- The status indicator should show "Service Running ✅"
- The live log area should start showing "Polling..." messages
- Verify on the server that the device is registered:
```bash
mysql -u otp_user -p otp_db -e "SELECT * FROM caller_devices;"
```
### 3.4 Keep the App Running
- **Lock the app** in recent apps (prevent it from being killed)
- On Xiaomi/OPPO/Vivo: Enable "Auto-start" in security settings
- On Samsung: Disable "Put app to sleep" in battery settings
- Keep the phone plugged into charger if possible
- Use a dedicated SIM card with an active plan that can make calls and send SMS
---
## 4. Flutter Receiver App Setup
### 4.1 Build the App
```bash
cd receiver-app/
flutter pub get
flutter run
```
For APK build:
```bash
flutter build apk --release
```
For iOS (macOS only):
```bash
cd receiver-app/
flutter pub get
cd ios/
pod install
cd ..
flutter run
```
### 4.2 Testing on Android
1. Install the app on a test phone (Phone B)
2. Open the app
3. Enter the phone number of Phone B (with +962 prefix)
4. Ensure "Android" is selected as device type
5. Tap "Send OTP"
6. Phone A (Caller device) should call Phone B within 3-6 seconds
7. The call will come from a number like +96279XX{OTP4digits}
8. Phone B's call log will detect the missed call
9. The app extracts the last 4 digits as OTP
10. Auto-verifies with the backend
11. Success screen appears
### 4.3 Testing on iOS
1. Install the app on an iPhone
2. Open the app
3. Enter the phone number of the iPhone (with +962 prefix)
4. Toggle device type to "iOS"
5. Tap "Send OTP"
6. The Caller Android device will send an SMS
7. iOS keyboard will show the AutoFill suggestion
8. Tap the suggestion (one tap only)
9. The app auto-submits the OTP
10. Success screen appears
---
## 5. End-to-End Testing
### Complete Test Flow
1. **Server**: Confirm health endpoint returns `{"status":"ok"}`
2. **Caller Device**: Ensure service is running with "Service Running ✅"
3. **Receiver App**: Enter phone number and tap "Send OTP"
4. **Timing**: Call should arrive within 3-6 seconds
5. **Auto-detect**: Receiver app should detect the missed call within 1-2 seconds
6. **Verification**: Success screen should appear without any user input
7. **Database check**: Verify the record in `otp_requests` has `verified_at` set
### Testing Individual API Endpoints
```bash
# Request OTP
curl -X POST https://otp.intaleqapp.com/api/request-otp \
-H "Content-Type: application/json" \
-d '{"phone":"+962790000001","app_key":"YOUR_APP_KEY","device_type":"android"}'
# Verify OTP
curl -X POST https://otp.intaleqapp.com/api/verify-otp \
-H "Content-Type: application/json" \
-d '{"phone":"+962790000001","otp":"1234","app_key":"YOUR_APP_KEY"}'
# Check pending calls (Caller App)
curl "https://otp.intaleqapp.com/api/pending-call?device_id=DEVICE_XXX&app_key=YOUR_DEVICE_KEY"
# Check pending SMS (Caller App)
curl "https://otp.intaleqapp.com/api/pending-sms?device_id=DEVICE_XXX&app_key=YOUR_DEVICE_KEY"
# Register device
curl -X POST https://otp.intaleqapp.com/api/register-device \
-H "Content-Type: application/json" \
-d '{"device_id":"DEVICE_001","phone_number":"+962790000000","sim_slot":0,"app_key":"YOUR_DEVICE_KEY"}'
```
### Monitoring
Check server logs:
```bash
# Nginx access log
sudo tail -f /var/log/nginx/otp.intaleqapp.com.access.log
# PHP error log
sudo tail -f /var/log/php8.1-fpm/error.log
# Application log
tail -f /var/www/otp.intaleqapp.com/logs/api.log
```
Check Redis:
```bash
redis-cli
> KEYS otp:*
> TTL otp:+962790000001
> GET otp:+962790000001
```
Check MySQL:
```sql
-- Recent OTP requests
SELECT * FROM otp_requests ORDER BY created_at DESC LIMIT 10;
-- Active devices
SELECT * FROM caller_devices WHERE is_active = 1;
-- Today's stats
SELECT status, COUNT(*) as count
FROM otp_requests
WHERE DATE(created_at) = CURDATE()
GROUP BY status;
```
---
## 6. Troubleshooting
### Caller Device Not Receiving Tasks
1. Check the device is registered: `SELECT * FROM caller_devices WHERE device_id = 'YOUR_DEVICE_ID';`
2. Check `is_active = 1` and `last_seen` is recent
3. Check the Caller App has internet access
4. Check the app_key matches DEVICE_KEY in config.php
5. Restart the CallerService
### Calls Not Going Through
1. Verify CALL_PHONE permission is granted
2. Check the SIM card has credit/plan for making calls
3. Check the phone number format in the task
4. Try manually dialing the number from the phone app
5. Check if Do Not Disturb mode is off
### SMS Not Sending
1. Verify SEND_SMS permission is granted
2. Check the SIM card can send SMS
3. Check the destination number format
4. Test sending a manual SMS from the device
### OTP Auto-detect Not Working (Flutter App)
1. Verify READ_CALL_LOG and READ_PHONE_STATE permissions are granted
2. Check the phone actually received a missed call
3. Verify the call type is detected as "missed" (not "rejected")
4. If the user answers the call, it won't be in the missed call log
5. Try the manual entry fallback
### iOS AutoFill Not Working
1. Ensure the SMS format includes `@otp.intaleqapp.com #{OTP}` at the end
2. The domain must match the app's associated domain
3. Test with a real device (simulators don't receive SMS)
4. Check that `AutofillHints.oneTimeCode` is set on the text field
5. The SMS must come from a phone number, not an alphanumeric sender
### Rate Limiting Issues
1. Check Redis is running: `redis-cli ping`
2. Clear rate limit for a phone: `redis-cli DEL rate_limit:otp:+962790000001`
3. Adjust limits in config.php if needed
### Database Connection Errors
1. Verify MySQL is running: `sudo systemctl status mysql`
2. Check credentials in config.php
3. Test connection: `mysql -u otp_user -p -h localhost otp_db`
4. Check PHP-FPM error log
### High Server Load
1. Check the polling interval (3 seconds default)
2. Consider increasing to 5 seconds if many devices are connected
3. Use Redis for session data, MySQL for persistent storage only
4. Monitor with `top`, `htop`, or `mysqladmin processlist`

258
docs/SMART_OTP_GATEWAY.md Normal file
View File

@@ -0,0 +1,258 @@
# دليل تصميم وهندسة نظام بوابة التحقق الذكي (Smart OTP Gateway)
<div dir="rtl" align="right">
يوفر هذا المستند تقييماً تفصيلياً وتصميماً هندسياً لبوابة التحقق الذكية المقترحة لدمج قنوات وتساب وتيليجرام، مع الحفاظ على أنظمة مكالمات الفلاش والرسائل النصية كقنوات بديلة.
</div>
---
## ١. تقييم المفهوم: تجربة المستخدم البشرية ضد أنظمة كشف الروبوتات
<div dir="rtl" align="right">
يعد تحقيق التوازن بين تجربة المستخدم (UX) وفلاتر الحماية التلقائية (خصوصاً فلاتر حظر الأرقام وأنظمة التعرف البصري على النصوص في وتساب) أحد أكبر التحديات في أنظمة التحقق.
- المبالغة في الحماية (مثل الصور المشوهة جداً) تزيد من صعوبة الاستخدام ونسبة إلغاء العملية من المستخدم.
- تسهيل العملية بشكل مبالغ فيه (مثل إرسال الرمز كنص عادي) يزيد من نسب اكتشاف الرسائل كرسائل ترويجية أو سبام وحظر أرقام الإرسال.
الحل الهندسي يكمن في تطبيق "واجهة تحقق متعددة الطبقات" مدعومة بـ "شبكة تحقق ديناميكية متكيفة".
</div>
```
┌─────────────────────────────────────────────────────────────┐
│ OTP Delivery Message │
│ │
│ ┌───────────────────────┐ │
│ │ IMAGE LAYER │ <-- 80% Anti-Detection Entropy │
│ │ [ 5 4 1 2 ] │ (Dynamic Generation/Reveal) │
│ └───────────────────────┘ │
│ │
│ Click the button below to verify instantly: │
│ [ Verify Now (Deep Link) ] <-- 100% Seamless UX │
│ │
│ *Hidden metadata container* <-- 20% OCR Noise │
└─────────────────────────────────────────────────────────────┘
```
<div dir="rtl" align="right">
الركائز الأساسية لواجهة التحقق متعددة الطبقات:
١. طبقة العرض المرئي الأساسي (الصور والوسائط الديناميكية): يعرض رمز التحقق داخل صورة يتم توليدها ديناميكياً مع خطوط تشويش خفيفة.
٢. البيانات الخفية المصاحبة للرسالة: تحتوي الرسالة على الرمز مشفراً أو مخفياً باستخدام علامات ترميز يونيكود مخفية (مثل المسافات الصفرية أو علامات الاتجاه) لقراءتها تلقائياً بواسطة التطبيق دون أن تثير انتباه العين البشرية أو الروبوتات.
٣. روابط التحقق الفورية (الروابط العميقة): تحتوي الرسالة على رابط عميق آمن يقوم بفتح التطبيق وتأكيد العملية تلقائياً عند النقر عليه دون الحاجة لكتابة أو نسخ أي رمز.
٤. التوجيه الديناميكي المتكيف مع المخاطر: يقوم النظام بتغيير طريقة التحقق تلقائياً بناءً على مستوى خطورة الطلب.
</div>
---
## ٢. البنية التحتية الحالية للنظام
<div dir="rtl" align="right">
يتكون النظام الحالي لمكالمات الفلاش من ثلاثة أجزاء رئيسية:
١. خادم الخلفية (PHP API): يتولى إنشاء الرموز، وإدارة الأجهزة المتصلة، وتسجيل المهام، والتحقق من الرموز.
٢. تطبيق الاتصال (Android Caller App): يعمل على هواتف أندرويد حقيقية متصلة بالإنترنت وتحتوي على شرائح اتصال نشطة، حيث يقوم بطلب المهام المعلقة وتنفيذ المكالمات والرسائل النصية تلقائياً.
٣. تطبيق الاستقبال (Flutter Receiver App): يثبت لدى المستخدم النهائي، ويقوم بالتحقق التلقائي في أندرويد عبر قراءة سجل المكالمات للتعرف على مكالمة الفلاش الفائتة، بينما يتيح التعبئة التلقائية للرسائل النصية في آيفون.
</div>
### ٢.١ مخطط سير العمل الحالي في أندرويد (مكالمة الفلاش)
```mermaid
sequenceDiagram
autonumber
actor User as User Device (Android)
participant App as Flutter Receiver App
participant BE as PHP Backend API
participant DB as Database / Redis
participant Caller as Android Caller Node
User->>App: Input Phone Number & Submit
App->>BE: POST /api/request-otp (phone, device_type: "android")
BE->>BE: Generate 4-digit OTP (e.g., 4829)
BE->>DB: Store OTP in Redis (TTL: 120s)
BE->>DB: Assign Task (round-robin) to Active Caller Node
BE->>BE: Format Caller ID Prefix + 2 Random Digits + OTP (e.g., +96279XX4829)
BE-->>App: JSON Response (otp_id, expires_in, method: "flash_call")
App->>App: Request READ_CALL_LOG permission & Start Polling
loop Polling Tasks (Every 3s)
Caller->>BE: GET /api/pending-call?device_id=X
BE-->>Caller: JSON (pending call: phone + dynamic Caller ID)
end
Caller->>User: Initiates Voice Call (displays +96279XX4829)
Caller->>Caller: Wait 2.5s & Programmatically Terminate Call (Missed Call)
Caller->>BE: POST /api/call-done (status: "success")
App->>App: Detects Missed Call in Call Log
App->>App: Extracts last 4 digits of Caller ID (4829)
App->>BE: POST /api/verify-otp (phone, otp)
BE->>DB: Verify OTP in Redis & Update DB Status
BE-->>App: {"success": true, "message": "verified"}
App->>User: Display Success Screen
```
---
## ٣. بنية بوابة التحقق الذكي المقترحة
<div dir="rtl" align="right">
تقوم البوابة الذكية بفحص توفر حساب للرقم على وتساب وتيليجرام أولاً لتحديد قناة الإرسال المثالية بدلاً من الاعتماد الكلي على نظام تشغيل الهاتف فقط.
</div>
### مخطط اتخاذ القرار وقنوات التوجيه
```mermaid
graph TD
A[Start: Request OTP] --> B{Check WhatsApp Availability}
B -- Exists --> C[Determine Risk Level]
C -- Low Risk --> D1[Send Obfuscated Text OTP]
C -- Medium Risk --> D2[Send Dynamic OTP Image + Deep Link]
C -- High Risk --> D3[Send Short APNG/MP4 + Deep Link]
B -- Does Not Exist --> F{Check Telegram Availability}
F -- Exists --> G[Send OTP via Telegram Bot/Client]
F -- Does Not Exist --> H{Check Device OS}
H -- Android --> I[Trigger Flash Call via Caller Node]
H -- iOS --> J[Trigger SMS via Caller Node]
D1 --> K[End: Await Verification]
D2 --> K
D3 --> K
G --> K
I --> K
J --> K
```
### ٣.١ تصنيف مستويات المخاطر الديناميكية
| مستوى الخطورة | القناة والمحتوى | مستوى الأمان | مستوى صعوبة تجربة المستخدم | حالات الاستخدام المستهدفة |
| :--- | :--- | :--- | :--- | :--- |
| **المستوى الأول (منخفض)** | رمز نصي عادي (مع علامات يونيكود مخفية) | منخفض | منخفض جداً (تعبئة تلقائية) | عناوين إنترنت موثوقة، مستخدمون متكررون |
| **المستوى الثاني (متوسط)** | صورة ديناميكية ثابتة + رابط تحقق فوري | متوسط | منخفض (التحقق بنقرة واحدة) | التسجيلات القياسية، التوجيه الافتراضي |
| **المستوى الثالث (مرتفع)** | صورة متحركة APNG/MP4 تظهر تدريجياً + رابط تحقق | مرتفع | منخفض (التحقق بنقرة واحدة) | عناوين إنترنت مشبوهة، طلبات متكررة بكثافة |
| **المستوى الرابع (مشبوه)** | مكالمة فلاش خارجية / رمز صوتي | أقصى حد | متوسط (إدخال يدوي وتحدي) | تخطي حدود المحاولات المسموحة |
---
## ٤. المواصفات البرمجية للتكامل
### ٤.١ بوابة وتساب (Node.js Baileys Gateway)
#### أ. إضافة واجهة التحقق من الأرقام في خادم الويب
في ملف `server.js`:
```javascript
app.post('/api/contacts/check', async (req, res) => {
const { session_key, phone } = req.body;
if (!session_key || !phone) {
return res.status(400).json({ error: 'Missing session_key or phone' });
}
try {
const result = await checkWhatsApp(session_key, phone);
res.json({ status: 'success', data: result });
} catch (err) {
res.status(500).json({ error: err.message || 'Failed to check contact' });
}
});
```
#### ب. توليد صورة رمز التحقق ورابط التحقق التلقائي (PHP Backend)
```php
function generateWhatsAppOtpPayload($otp, $phone, $riskLevel) {
// Generate Deep Link Token
$token = bin2hex(random_bytes(16));
// Store deep link token in Redis mapped to phone & OTP (expires in 120s)
$redis = RedisClient::getInstance();
$redis->setex("verify_token:{$token}", 120, json_encode([
'phone' => $phone,
'otp' => $otp
]));
$deepLink = "https://verify.nabeh.app/otp?token=" . $token;
if ($riskLevel === 'level_1') {
// Obfuscate OTP text using zero-width joiners (ZWJ) or LTR/RTL control characters
$obfuscatedOtp = "\u{202D}" . implode("\u{200B}", str_split($otp)) . "\u{202C}";
return [
'type' => 'text',
'message' => "رمز التحقق الخاص بك هو: " . $obfuscatedOtp . "\n\nأو اضغط للتحقق الفوري:\n" . $deepLink
];
}
// Create base64 dynamic image for Level 2 & 3
$imageBase64 = generateOtpImageBase64($otp);
return [
'type' => 'image',
'image' => $imageBase64,
'message' => "اضغط على الرابط أدناه لتأكيد هويتك تلقائيًا دون نسخ الرمز:\n\n" . $deepLink
];
}
```
---
## ٥. أتمتة التحقق في تطبيق الهاتف (Flutter Client)
<div dir="rtl" align="right">
لتحقيق واجهة مستخدم بدون كتابة أو تعبئة يدوية للرموز، يتم تطبيق آليتين أساسيتين في تطبيق فلاتر:
</div>
### ٥.١ معالجة الروابط العميقة (Deep Links)
<div dir="rtl" align="right">
يقوم التطبيق بتسجيل الدومين الخاص به. عند النقر على الرابط من داخل محادثة وتساب أو تيليجرام:
١. يكتشف نظام التشغيل الرابط ويفتح تطبيق الهاتف تلقائياً.
٢. يقوم التطبيق باستخراج التوكين وإرساله مباشرة للخلفية.
٣. تكتمل عملية التحقق بنجاح وينتقل المستخدم للشاشة التالية فوراُ.
</div>
### ٥.٢ قارئ الإشعارات التلقائي (Notification Listener - Android)
<div dir="rtl" align="right">
بالنسبة لهواتف أندرويد، يمكن الاستماع للإشعارات الواردة برمجياً لقراءة الرسائل وتمرير التوكين أو الرمز المخفي دون الحاجة لفتح وتساب:
</div>
```dart
void onNotificationReceived(NotificationEvent event) {
if (event.packageName == "com.whatsapp" && event.title == "Nabeh") {
final body = event.text ?? "";
final tokenRegex = RegExp(r"token=([a-f0-9]+)");
if (tokenRegex.hasMatch(body)) {
final token = tokenRegex.firstMatch(body)!.group(1);
_verifyToken(token!);
}
}
}
```
---
## ٦. تعديلات قاعدة البيانات
```sql
ALTER TABLE otp_requests
ADD COLUMN channel ENUM('flash_call', 'sms', 'whatsapp', 'telegram') NOT NULL DEFAULT 'flash_call' AFTER method,
ADD COLUMN risk_level ENUM('level_1', 'level_2', 'level_3', 'level_4') NOT NULL DEFAULT 'level_2' AFTER channel,
ADD COLUMN deep_link_token VARCHAR(64) NULL DEFAULT NULL AFTER risk_level;
```
---
## ٧. خطة التطوير والتشغيل
### المرحلة الأولى: تهيئة قنوات الفحص والروابط العميقة
<div dir="rtl" align="right">
١. كتابة وتفعيل منطق الروابط العميقة (Deep Links) في خادم PHP وتطبيق فلاتر.
٢. ربط النطاقات المرتبطة (Associated Domains) في آبل وجوجل.
</div>
### المرحلة الثانية: تحديثات وتساب (Baileys Node.js)
<div dir="rtl" align="right">
١. إضافة واجهة التحقق من وجود الرقم على خادم وتساب.
٢. تفعيل مكتبة توليد الصور برمجياً في خادم PHP لإرسال الصور التلقائية عبر وتساب.
</div>
### المرحلة الثالثة: أتمتة تطبيق الهاتف والبدائل
<div dir="rtl" align="right">
١. دمج قارئ الإشعارات في فلاتر وتجربة الأتمتة الكاملة.
٢. ربط قنوات الاتصال الفائت والرسائل النصية كقنوات بديلة نهائية في حال عدم نجاح التحقق عبر وتساب أو تيليجرام.
</div>

45
receiver_app_new/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: android
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: ios
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: linux
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: macos
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: web
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: windows
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,17 @@
# receiver_app_new
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
receiver_app_new/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,49 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.intaleq.receiver"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.intaleq.receiver"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="receiver_app_new"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.intaleq.receiver
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

34
receiver_app_new/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@@ -0,0 +1,29 @@
PODS:
- Flutter (1.0.0)
- permission_handler_apple (9.3.0):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- Flutter (from `Flutter`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,753 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C1F316133B487874ED8A74A6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB2FAD8495A0632447656C56 /* Pods_Runner.framework */; };
DB5E52690D37376F6953FAAE /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B2FA75278AC3239EEC00CD /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1A54C71E55FFF4462C2CCEFF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
1D3FDD200DC57EFCC3345D5E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
54380F195E4C3CF1735EDB9B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
58139DEC46478EC4C58ABD53 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
6D4F9875F678077728BADDC5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
91B2FA75278AC3239EEC00CD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A18908AE936C3BF25319DFEF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
CB2FAD8495A0632447656C56 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
3DEC94A83268B3E53CD4EC5E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DB5E52690D37376F6953FAAE /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C1F316133B487874ED8A74A6 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
6E02E11B64335E94F3ADE1BF /* Pods */ = {
isa = PBXGroup;
children = (
54380F195E4C3CF1735EDB9B /* Pods-Runner.debug.xcconfig */,
1D3FDD200DC57EFCC3345D5E /* Pods-Runner.release.xcconfig */,
A18908AE936C3BF25319DFEF /* Pods-Runner.profile.xcconfig */,
58139DEC46478EC4C58ABD53 /* Pods-RunnerTests.debug.xcconfig */,
6D4F9875F678077728BADDC5 /* Pods-RunnerTests.release.xcconfig */,
1A54C71E55FFF4462C2CCEFF /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
6E02E11B64335E94F3ADE1BF /* Pods */,
E19875AD42182341B17CC104 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
E19875AD42182341B17CC104 /* Frameworks */ = {
isa = PBXGroup;
children = (
CB2FAD8495A0632447656C56 /* Pods_Runner.framework */,
91B2FA75278AC3239EEC00CD /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
E355C13815ED1617EEB83E5C /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
3DEC94A83268B3E53CD4EC5E /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
09F30E8C67E295B11062BC79 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
044E088535EBDE98F25D3197 /* [CP] Embed Pods Frameworks */,
03DEDA86686B8509D18B6C8A /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
03DEDA86686B8509D18B6C8A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
044E088535EBDE98F25D3197 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
09F30E8C67E295B11062BC79 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
E355C13815ED1617EEB83E5C /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 58139DEC46478EC4C58ABD53 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 6D4F9875F678077728BADDC5 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 1A54C71E55FFF4462C2CCEFF /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Some files were not shown because too many files have changed in this diff Show More