Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d613217efb | ||
|
|
598d697306 | ||
|
|
5ae010f1c3 | ||
|
|
94a0cd8c0a | ||
|
|
2ec9207779 | ||
|
|
51cb1ca198 | ||
|
|
e49f3954c3 | ||
|
|
17351e2148 | ||
|
|
8eec17ba1b | ||
|
|
25c70eb09d | ||
|
|
acde97f080 | ||
|
|
e14b71a790 | ||
|
|
5023f80f0f | ||
|
|
2bdcdeb9a9 | ||
|
|
b42a681816 | ||
|
|
14fd2f9b03 | ||
|
|
540bb9068b | ||
|
|
3061bd8d66 | ||
|
|
338fb665bc | ||
|
|
71475f0db8 | ||
|
|
40b0df297c | ||
|
|
863aac04e8 | ||
|
|
6e7ed42fe8 | ||
|
|
8fd06f06d1 | ||
|
|
0144cff16e | ||
|
|
7ff6ea23fc | ||
|
|
fd4969c92c | ||
|
|
4d1b53e247 | ||
|
|
c668530188 | ||
|
|
3cd1d94b20 | ||
|
|
30591c4847 | ||
|
|
f5bef39cec | ||
|
|
482049a764 | ||
|
|
ac65907453 | ||
|
|
584e2c5bec | ||
|
|
9685ecb7b3 | ||
|
|
d8460bcb8b | ||
|
|
eb94085f6e | ||
|
|
b1281d4ec1 | ||
|
|
46fa5cda2e | ||
|
|
709f05f33d | ||
|
|
844a815813 | ||
|
|
b8d277a563 | ||
|
|
d335f76088 | ||
|
|
42abcec94b | ||
|
|
014ae354c6 | ||
|
|
2692d4a154 | ||
|
|
1b24cd9754 | ||
|
|
e6ad6a18dc | ||
|
|
d51693d26d | ||
|
|
07f80fdf84 | ||
|
|
b762c6e2ef | ||
|
|
c1952d2030 | ||
|
|
ebc90d8105 | ||
|
|
fd0d86efab | ||
|
|
32b463a10c | ||
|
|
ed55406687 | ||
|
|
b6dde1a096 | ||
|
|
312e3a4fd4 | ||
|
|
3939502db7 | ||
|
|
9ae9f5978b | ||
|
|
58e6c73a72 | ||
|
|
f1c8530257 | ||
|
|
ede8e4c210 | ||
|
|
a6df612bfa | ||
|
|
4b4f3bace9 | ||
|
|
8a42b95e8f | ||
|
|
e0e6be9379 | ||
|
|
0665aa5432 | ||
|
|
dc8ca9d9a7 | ||
|
|
541f32406e | ||
|
|
141b8b334a | ||
|
|
f9fed4d3d8 | ||
|
|
4ef65b08ff | ||
|
|
86060456fa | ||
|
|
ca0ac8cf6e | ||
|
|
ba0437ff01 | ||
|
|
99a9ee3cc1 | ||
|
|
f2ca0cecf6 | ||
|
|
0ff96f4857 | ||
|
|
e2cf5d2e37 | ||
|
|
d6d52e80cf | ||
|
|
30691e2e0d | ||
|
|
7308ab6721 | ||
|
|
fd184a4700 | ||
|
|
af19f6c75a | ||
|
|
10ead48014 | ||
|
|
463747b82f | ||
|
|
d99b53f84c | ||
|
|
bbbe85e0fd | ||
|
|
df0ef06495 | ||
|
|
130a6934d6 | ||
|
|
3ea8c0d5a0 | ||
|
|
fd5ba1c538 | ||
|
|
91ac0b3eb0 | ||
|
|
c26ac378f6 | ||
|
|
bf66e94e6d | ||
|
|
552a90685a | ||
|
|
e2246cf8e8 | ||
|
|
455696275b | ||
|
|
0ffa945668 | ||
|
|
6c40e2311e | ||
|
|
fbac1fae0b | ||
|
|
127d1f4127 | ||
|
|
7ce4efff70 | ||
|
|
dd9ec0aacc | ||
|
|
6d081c7489 | ||
|
|
500d880aea | ||
|
|
4995b6023b | ||
|
|
ea8c21ff21 | ||
|
|
a9b1cd6549 | ||
|
|
e3745d5071 | ||
|
|
82aa3cb425 | ||
|
|
b57ebc7500 | ||
|
|
740b691f67 | ||
|
|
5e509b1b57 | ||
|
|
111dbc43f2 | ||
|
|
d704b04e3d | ||
|
|
0d5f9a7cec | ||
|
|
f66d2855a7 | ||
|
|
fe588ea05a | ||
|
|
704a8dbf95 | ||
|
|
4f64880cf8 | ||
|
|
63733651d5 | ||
|
|
88e0b3df63 | ||
|
|
9b98a9e509 | ||
|
|
52565cc55c | ||
|
|
023df71d3c | ||
|
|
bce6b3be94 | ||
|
|
3227fb6696 | ||
|
|
e5b63dbb6a | ||
|
|
183dbeb29a | ||
|
|
da79575db6 | ||
|
|
b08b732b61 | ||
|
|
3f838ce253 | ||
|
|
938d3d6f17 | ||
|
|
321842a021 | ||
|
|
db640de638 | ||
|
|
3c511a0f53 | ||
|
|
cf80be9611 | ||
|
|
38dd56df26 | ||
|
|
97dd062477 | ||
|
|
771282187d | ||
|
|
445de6b8fc | ||
|
|
d1b311a7c5 | ||
|
|
9f5b858ffb | ||
|
|
1e5c4de4ed | ||
|
|
1f0db4c4d2 | ||
|
|
7a24c0cd2b | ||
|
|
4c164e6bad | ||
|
|
7fc521023f | ||
|
|
99efd20e7a | ||
|
|
2e8403a056 | ||
|
|
8914cc2b0b | ||
|
|
364b876c8c | ||
|
|
453087b3ce | ||
|
|
d023fc385f | ||
|
|
8e81c6d7b0 | ||
|
|
b1b87e1acc | ||
|
|
68593dff9e | ||
|
|
49d65b87cd | ||
|
|
0f6c9eff32 | ||
|
|
e29d145397 | ||
|
|
0fa930a0ff | ||
|
|
400bda9e72 | ||
|
|
dd14c6ba4e | ||
|
|
f9005e6063 | ||
|
|
58b8eef92f | ||
|
|
08afce15b0 | ||
|
|
6ac8361aa6 | ||
|
|
014590fb9e | ||
|
|
217c068bfb | ||
|
|
fb06cfd33f | ||
|
|
aac621bd16 | ||
|
|
6252e4f04e | ||
|
|
0fe0f48ef9 | ||
|
|
f520f378d0 | ||
|
|
b0ce11e392 | ||
|
|
7012b04439 | ||
|
|
375cf63adb | ||
|
|
6886dd2f1a | ||
|
|
5acec3f04a | ||
|
|
6861a4d36b | ||
|
|
54fb586d9b | ||
|
|
60bcd691c8 | ||
|
|
dd2c01678f | ||
|
|
2633fc80cc | ||
|
|
da9efc036b | ||
|
|
7d2b8093ae | ||
|
|
15a67446ea | ||
|
|
50e7b7536d | ||
|
|
d868b43d52 | ||
|
|
c4dc4bcb88 | ||
|
|
c7b206be91 | ||
|
|
f88c94fbbd | ||
|
|
59525cb126 | ||
|
|
383cf213d8 | ||
|
|
9e15483752 | ||
|
|
ea49398e4d | ||
|
|
cde19e01e0 | ||
|
|
ef59f86071 | ||
|
|
1e602b85e1 | ||
|
|
2b110b6fc5 | ||
|
|
89ed36849b | ||
|
|
0ac3ccb144 | ||
|
|
9b9d92592c | ||
|
|
7fc7fbf86f | ||
|
|
cbffeca689 | ||
|
|
dfd6c5c3d2 | ||
|
|
c1af584093 | ||
|
|
db3ac97af8 | ||
|
|
25ca1f7be0 | ||
|
|
2be8b3e8c7 | ||
|
|
7694f3f60a | ||
|
|
52e88e7e36 | ||
|
|
64859b38ad | ||
|
|
601f33d95e | ||
|
|
f355065bfb | ||
|
|
0bd1818678 | ||
|
|
cdf3b7bb51 | ||
|
|
61a666da35 | ||
|
|
fa81292bd8 |
@@ -15,6 +15,14 @@ on:
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
android_variant:
|
||||
description: 'Android build variant'
|
||||
required: true
|
||||
default: 'dev'
|
||||
type: choice
|
||||
options:
|
||||
- dev
|
||||
- prod
|
||||
|
||||
jobs:
|
||||
build-and-test-web-and-api:
|
||||
@@ -62,12 +70,14 @@ jobs:
|
||||
run: |
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm run prisma:generate
|
||||
pnpm run build
|
||||
|
||||
build-and-test-android:
|
||||
name: Build and Test Android
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [dev, prod]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -90,19 +100,40 @@ jobs:
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x android/gradlew
|
||||
|
||||
- name: Create debug keystore
|
||||
run: |
|
||||
mkdir -p ~/.android
|
||||
keytool -genkey -v -keystore ~/.android/debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"
|
||||
|
||||
- name: Create google-services.json for dev
|
||||
if: matrix.variant == 'dev'
|
||||
run: |
|
||||
mkdir -p android/app/src/dev
|
||||
echo '${{ secrets.GOOGLE_SERVICES_JSON_DEV }}' > android/app/src/dev/google-services.json
|
||||
|
||||
- name: Create google-services.json for prod
|
||||
if: matrix.variant == 'prod'
|
||||
run: |
|
||||
mkdir -p android/app/src/prod
|
||||
echo '${{ secrets.GOOGLE_SERVICES_JSON_PROD }}' > android/app/src/prod/google-services.json
|
||||
|
||||
- name: Build Android app
|
||||
run: |
|
||||
cd android
|
||||
./gradlew assembleDebug
|
||||
./gradlew assemble${{ matrix.variant }}Debug
|
||||
|
||||
# - name: Run Android tests
|
||||
# run: |
|
||||
# cd android
|
||||
# ./gradlew testDebugUnitTest
|
||||
# ./gradlew test${{ matrix.variant }}DebugUnitTest
|
||||
|
||||
- name: Sanitize ref name for artifact
|
||||
shell: bash
|
||||
run: echo "SAFE_REF_NAME=${GITHUB_REF_NAME//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: textbee.dev-build-${{ github.ref_name }}-${{ github.sha }}.apk
|
||||
path: android/app/build/outputs/apk/debug/app-debug.apk
|
||||
name: textbee-${{ matrix.variant }}-build-${{ env.SAFE_REF_NAME }}-${{ github.sha }}.apk
|
||||
path: android/app/build/outputs/apk/${{ matrix.variant }}/debug/app-${{ matrix.variant }}-debug.apk
|
||||
retention-days: 7
|
||||
@@ -1,3 +1,5 @@
|
||||
.vercel
|
||||
.DS_Store
|
||||
*-monitor
|
||||
|
||||
.cursor
|
||||
@@ -1,11 +1,11 @@
|
||||
# TextBee - Android SMS Gateway
|
||||
# textbee.dev - android sms gateway
|
||||
|
||||
TextBee is an open-source SMS gateway that enables users to send and receive SMS messages via a web dashboard or a REST API. Perfect for businesses, developers, and hobbyists who need a reliable and cost-effective way to automate SMS messaging.
|
||||
textbee.dev is an open-source SMS gateway that enables users to send and receive SMS messages via a web dashboard or a REST API. Perfect for businesses, developers, and hobbyists who need a reliable and cost-effective way to automate SMS messaging.
|
||||
|
||||
- **Technology stack**: React, Next.js, Node.js, NestJs, MongoDB, Android, Java
|
||||
- **Link**: [https://textbee.dev](https://textbee.dev/)
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Features
|
||||
@@ -25,7 +25,7 @@ TextBee is an open-source SMS gateway that enables users to send and receive SMS
|
||||
## Getting Started
|
||||
|
||||
1. Go to [textbee.dev](https://textbee.dev) and register or login with your account
|
||||
2. Install the app on your android phone from [dl.textbee.dev](https://dl.textbee.dev)
|
||||
2. Install the app on your android phone from [textbee.dev/download](https://textbee.dev/download)
|
||||
3. Open the app and grant the permissions for SMS
|
||||
4. Go to [textbee.dev/dashboard](https://textbee.dev/dashboard) and click register device/ generate API Key
|
||||
5. Scan the QR code with the app or enter the API key manually
|
||||
|
||||
@@ -13,4 +13,11 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/app/release/*
|
||||
/app/release/*
|
||||
|
||||
*.apk
|
||||
|
||||
# Firebase configuration files
|
||||
/app/src/dev/google-services.json
|
||||
/app/src/prod/google-services.json
|
||||
/app/google-services.json
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State>
|
||||
<runningDeviceTargetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="RUNNING_DEVICE_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="SERIAL_NUMBER" />
|
||||
<value value="adb-RFCX417HC5V-ZWjtU2._adb-tls-connect._tcp." />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</runningDeviceTargetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2025-08-09T15:50:54.600783Z" />
|
||||
</State>
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
||||
@@ -4,15 +4,15 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="jbr-17" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
@@ -7,7 +6,7 @@
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.vernu.sms"
|
||||
minSdk 24
|
||||
targetSdk 32
|
||||
versionCode 12
|
||||
versionName "2.5.0"
|
||||
versionCode 17
|
||||
versionName "2.7.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -21,13 +20,50 @@ android {
|
||||
// }
|
||||
// }
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file("${System.getenv("HOME")}/.android/debug.keystore")
|
||||
storePassword "android"
|
||||
keyAlias "androiddebugkey"
|
||||
keyPassword "android"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
// TODO: Change to release signing config
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
debuggable true
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "environment"
|
||||
|
||||
productFlavors {
|
||||
dev {
|
||||
dimension "environment"
|
||||
applicationId "com.vernu.sms.dev"
|
||||
versionNameSuffix "-dev"
|
||||
buildConfigField "String", "API_BASE_URL", '"https://api.dev.textbee.dev/api/v1/"'
|
||||
buildConfigField "String", "ENVIRONMENT", '"development"'
|
||||
resValue "string", "app_name", "textbee.dev (Dev)"
|
||||
}
|
||||
|
||||
prod {
|
||||
dimension "environment"
|
||||
applicationId "com.vernu.sms"
|
||||
buildConfigField "String", "API_BASE_URL", '"https://api.textbee.dev/api/v1/"'
|
||||
buildConfigField "String", "ENVIRONMENT", '"production"'
|
||||
resValue "string", "app_name", "textbee.dev"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
@@ -37,7 +73,7 @@ android {
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
@@ -47,14 +83,18 @@ dependencies {
|
||||
implementation 'com.google.firebase:firebase-analytics'
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
implementation 'com.google.firebase:firebase-messaging-directboot'
|
||||
implementation 'com.google.firebase:firebase-crashlytics'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.1.0'
|
||||
|
||||
implementation 'androidx.work:work-runtime:2.7.1'
|
||||
|
||||
// def room_version = "2.4.2"
|
||||
// implementation "androidx.room:room-runtime:$room_version"
|
||||
// annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "14326313838",
|
||||
"project_id": "vernu-sms",
|
||||
"storage_bucket": "vernu-sms.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:14326313838:android:4ea4380d88b057e9eb877f",
|
||||
"android_client_info": {
|
||||
"package_name": "com.vernu.sms"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "14326313838-0kmpefogqk8h5ntf2mth4e9cht9pbfai.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDNZP2HeVPZ6_V9oq2UTYWmJ4FfenrRBIQ"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "14326313838-0kmpefogqk8h5ntf2mth4e9cht9pbfai.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "YOUR_DEV_PROJECT_NUMBER",
|
||||
"project_id": "your-dev-project-id",
|
||||
"storage_bucket": "your-dev-project-id.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:YOUR_DEV_PROJECT_NUMBER:android:YOUR_DEV_APP_ID",
|
||||
"android_client_info": {
|
||||
"package_name": "com.vernu.sms.dev"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "YOUR_DEV_CLIENT_ID",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "YOUR_DEV_API_KEY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "YOUR_DEV_CLIENT_ID",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -14,7 +14,10 @@
|
||||
<uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<application
|
||||
android:name=".SMSGatewayApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -22,6 +25,12 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SMSGateway"
|
||||
android:usesCleartextTraffic="true" >
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove" />
|
||||
|
||||
<service
|
||||
android:name=".services.FCMService"
|
||||
android:exported="false"
|
||||
@@ -44,6 +53,15 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.SMSStatusReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="SMS_SENT" />
|
||||
<action android:name="SMS_DELIVERED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:enabled="true"
|
||||
android:name=".receivers.BootCompletedReceiver"
|
||||
android:exported="true"
|
||||
@@ -55,13 +73,19 @@
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SMSGateway.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.SMSFilterActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.SMSGateway.NoActionBar"
|
||||
android:parentActivityName=".activities.MainActivity" />
|
||||
<activity
|
||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||
android:screenOrientation="portrait"
|
||||
|
||||
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 131 KiB |
@@ -3,7 +3,7 @@ package com.vernu.sms;
|
||||
import android.Manifest;
|
||||
|
||||
public class AppConstants {
|
||||
public static final String API_BASE_URL = "https://api.textbee.dev/api/v1/";
|
||||
public static final String API_BASE_URL = BuildConfig.API_BASE_URL;
|
||||
public static final String[] requiredPermissions = new String[]{
|
||||
Manifest.permission.SEND_SMS,
|
||||
Manifest.permission.READ_SMS,
|
||||
@@ -16,4 +16,15 @@ public class AppConstants {
|
||||
public static final String SHARED_PREFS_PREFERRED_SIM_KEY = "PREFERRED_SIM";
|
||||
public static final String SHARED_PREFS_RECEIVE_SMS_ENABLED_KEY = "RECEIVE_SMS_ENABLED";
|
||||
public static final String SHARED_PREFS_TRACK_SENT_SMS_STATUS_KEY = "TRACK_SENT_SMS_STATUS";
|
||||
public static final String SHARED_PREFS_LAST_VERSION_CODE_KEY = "LAST_VERSION_CODE";
|
||||
public static final String SHARED_PREFS_LAST_VERSION_NAME_KEY = "LAST_VERSION_NAME";
|
||||
public static final String SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY = "STICKY_NOTIFICATION_ENABLED";
|
||||
public static final String HEARTBEAT_WORK_TAG = "heartbeat";
|
||||
public static final String SHARED_PREFS_HEARTBEAT_ENABLED_KEY = "HEARTBEAT_ENABLED";
|
||||
public static final String SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY = "HEARTBEAT_INTERVAL_MINUTES";
|
||||
public static final String SHARED_PREFS_SMS_FILTER_CONFIG_KEY = "SMS_FILTER_CONFIG";
|
||||
public static final String SHARED_PREFS_DEVICE_NAME_KEY = "DEVICE_NAME";
|
||||
public static final String SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY = "SMS_SEND_DELAY_SECONDS";
|
||||
/** Default delay between SMS sends (seconds). 5s helps avoid carrier/device throttling. */
|
||||
public static final int DEFAULT_SMS_SEND_DELAY_SECONDS = 5;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.vernu.sms;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.work.Configuration;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
public class SMSGatewayApplication extends Application implements Configuration.Provider {
|
||||
private static final String TAG = "SMSGatewayApplication";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Initialize WorkManager early to ensure it's ready for background work
|
||||
// This is important for background tasks like heartbeat
|
||||
try {
|
||||
WorkManager.initialize(this, getWorkManagerConfiguration());
|
||||
Log.d(TAG, "WorkManager initialized successfully");
|
||||
} catch (IllegalStateException e) {
|
||||
// WorkManager might already be initialized (e.g., by androidx.startup)
|
||||
// This is fine, we can continue
|
||||
Log.d(TAG, "WorkManager already initialized or will be initialized automatically");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration getWorkManagerConfiguration() {
|
||||
return new Configuration.Builder()
|
||||
.setMinimumLoggingLevel(android.util.Log.INFO)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,23 @@ import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.telephony.SubscriptionInfo;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics;
|
||||
import com.vernu.sms.services.StickyNotificationService;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.dtos.SimInfoDTO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TextBeeUtils {
|
||||
private static final String TAG = "TextBeeUtils";
|
||||
|
||||
public static boolean isPermissionGranted(Context context, String permission) {
|
||||
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
@@ -33,21 +40,266 @@ public class TextBeeUtils {
|
||||
}
|
||||
|
||||
public static void startStickyNotificationService(Context context) {
|
||||
|
||||
if(!isPermissionGranted(context, Manifest.permission.RECEIVE_SMS)){
|
||||
return;
|
||||
}
|
||||
|
||||
Intent notificationIntent = new Intent(context, StickyNotificationService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(notificationIntent);
|
||||
|
||||
// Only start service if user has enabled sticky notification
|
||||
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
|
||||
if (stickyNotificationEnabled) {
|
||||
Intent notificationIntent = new Intent(context, StickyNotificationService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(notificationIntent);
|
||||
} else {
|
||||
context.startService(notificationIntent);
|
||||
}
|
||||
Log.i(TAG, "Starting sticky notification service");
|
||||
} else {
|
||||
context.startService(notificationIntent);
|
||||
Log.i(TAG, "Sticky notification disabled by user, not starting service");
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopStickyNotificationService(Context context) {
|
||||
Intent notificationIntent = new Intent(context, StickyNotificationService.class);
|
||||
context.stopService(notificationIntent);
|
||||
Log.i(TAG, "Stopping sticky notification service");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a non-fatal exception to Crashlytics with additional context information
|
||||
*
|
||||
* @param throwable The exception to log
|
||||
* @param message A message describing what happened
|
||||
* @param customData Optional map of custom key-value pairs to add as context
|
||||
*/
|
||||
public static void logException(Throwable throwable, String message, Map<String, Object> customData) {
|
||||
try {
|
||||
Log.e(TAG, message, throwable);
|
||||
|
||||
FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
|
||||
crashlytics.log(message);
|
||||
|
||||
// Add any custom data as key-value pairs
|
||||
if (customData != null) {
|
||||
for (Map.Entry<String, Object> entry : customData.entrySet()) {
|
||||
if (entry.getValue() instanceof String) {
|
||||
crashlytics.setCustomKey(entry.getKey(), (String) entry.getValue());
|
||||
} else if (entry.getValue() instanceof Boolean) {
|
||||
crashlytics.setCustomKey(entry.getKey(), (Boolean) entry.getValue());
|
||||
} else if (entry.getValue() instanceof Integer) {
|
||||
crashlytics.setCustomKey(entry.getKey(), (Integer) entry.getValue());
|
||||
} else if (entry.getValue() instanceof Long) {
|
||||
crashlytics.setCustomKey(entry.getKey(), (Long) entry.getValue());
|
||||
} else if (entry.getValue() instanceof Float) {
|
||||
crashlytics.setCustomKey(entry.getKey(), (Float) entry.getValue());
|
||||
} else if (entry.getValue() instanceof Double) {
|
||||
crashlytics.setCustomKey(entry.getKey(), (Double) entry.getValue());
|
||||
} else if (entry.getValue() != null) {
|
||||
crashlytics.setCustomKey(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record the exception
|
||||
crashlytics.recordException(throwable);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error logging exception to Crashlytics", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified method to log a non-fatal exception with just a message
|
||||
*/
|
||||
public static void logException(Throwable throwable, String message) {
|
||||
logException(throwable, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all available SIM information (physical SIMs and eSIMs) from the device
|
||||
*
|
||||
* @param context The application context
|
||||
* @return List of SimInfoDTO objects containing SIM information, or empty list if permission not granted
|
||||
*/
|
||||
public static List<SimInfoDTO> collectSimInfo(Context context) {
|
||||
List<SimInfoDTO> simInfoList = new ArrayList<>();
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.w(TAG, "READ_PHONE_STATE permission not granted, cannot collect SIM info");
|
||||
return simInfoList;
|
||||
}
|
||||
|
||||
try {
|
||||
SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
|
||||
List<SubscriptionInfo> subscriptionInfoList = subscriptionManager.getActiveSubscriptionInfoList();
|
||||
|
||||
if (subscriptionInfoList == null) {
|
||||
Log.d(TAG, "No active subscriptions found");
|
||||
return simInfoList;
|
||||
}
|
||||
|
||||
for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
|
||||
SimInfoDTO simInfo = new SimInfoDTO();
|
||||
simInfo.setSubscriptionId(subscriptionInfo.getSubscriptionId());
|
||||
|
||||
// Get ICCID (may be null for eSIM)
|
||||
try {
|
||||
String iccId = subscriptionInfo.getIccId();
|
||||
if (iccId != null && !iccId.isEmpty()) {
|
||||
simInfo.setIccId(iccId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get ICCID for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
// Get Card ID
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
int cardId = subscriptionInfo.getCardId();
|
||||
// INVALID_CARD_ID is -1, check for valid card ID (>= 0)
|
||||
if (cardId >= 0) {
|
||||
simInfo.setCardId(cardId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get Card ID for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
// Get carrier name
|
||||
try {
|
||||
CharSequence carrierName = subscriptionInfo.getCarrierName();
|
||||
if (carrierName != null) {
|
||||
simInfo.setCarrierName(carrierName.toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get carrier name for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
// Get display name
|
||||
try {
|
||||
CharSequence displayName = subscriptionInfo.getDisplayName();
|
||||
if (displayName != null) {
|
||||
simInfo.setDisplayName(displayName.toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get display name for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
// Get SIM slot index
|
||||
try {
|
||||
int simSlotIndex = subscriptionInfo.getSimSlotIndex();
|
||||
if (simSlotIndex >= 0) {
|
||||
simInfo.setSimSlotIndex(simSlotIndex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get SIM slot index for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
// Get MCC (getMccString() is API 29+; use getMcc() on older devices)
|
||||
try {
|
||||
String mcc = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
mcc = subscriptionInfo.getMccString();
|
||||
} else {
|
||||
int mccInt = subscriptionInfo.getMcc();
|
||||
if (mccInt != Integer.MAX_VALUE) {
|
||||
mcc = String.format("%03d", mccInt);
|
||||
}
|
||||
}
|
||||
if (mcc != null && !mcc.isEmpty()) {
|
||||
simInfo.setMcc(mcc);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get MCC for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
// Get MNC (getMncString() is API 29+; use getMnc() on older devices)
|
||||
try {
|
||||
String mnc = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
mnc = subscriptionInfo.getMncString();
|
||||
} else {
|
||||
int mncInt = subscriptionInfo.getMnc();
|
||||
if (mncInt != Integer.MAX_VALUE) {
|
||||
mnc = String.valueOf(mncInt);
|
||||
}
|
||||
}
|
||||
if (mnc != null && !mnc.isEmpty()) {
|
||||
simInfo.setMnc(mnc);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get MNC for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
// Get country ISO
|
||||
try {
|
||||
String countryIso = subscriptionInfo.getCountryIso();
|
||||
if (countryIso != null && !countryIso.isEmpty()) {
|
||||
simInfo.setCountryIso(countryIso);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get country ISO for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
// Get subscription type (0 = physical SIM, 1 = eSIM)
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
int subscriptionType = subscriptionInfo.getSubscriptionType();
|
||||
if (subscriptionType == SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM) {
|
||||
simInfo.setSubscriptionType("PHYSICAL_SIM");
|
||||
} else if (subscriptionType == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM) {
|
||||
simInfo.setSubscriptionType("ESIM");
|
||||
}
|
||||
} else {
|
||||
// For older Android versions, default to PHYSICAL_SIM
|
||||
simInfo.setSubscriptionType("PHYSICAL_SIM");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not get subscription type for subscription " + subscriptionInfo.getSubscriptionId());
|
||||
}
|
||||
|
||||
simInfoList.add(simInfo);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error collecting SIM info: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return simInfoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a subscription ID exists in the active subscriptions
|
||||
*
|
||||
* @param context The application context
|
||||
* @param subscriptionId The subscription ID to validate
|
||||
* @return true if the subscription ID exists, false otherwise
|
||||
*/
|
||||
public static boolean isValidSubscriptionId(Context context, int subscriptionId) {
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
|
||||
List<SubscriptionInfo> subscriptionInfoList = subscriptionManager.getActiveSubscriptionInfoList();
|
||||
|
||||
if (subscriptionInfoList == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
|
||||
if (subscriptionInfo.getSubscriptionId() == subscriptionId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error validating subscription ID: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,13 @@ import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import com.vernu.sms.activities.SMSFilterActivity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -30,7 +35,14 @@ import com.vernu.sms.TextBeeUtils;
|
||||
import com.vernu.sms.R;
|
||||
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
|
||||
import com.vernu.sms.dtos.SimInfoCollectionDTO;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.helpers.VersionTracker;
|
||||
import com.vernu.sms.helpers.HeartbeatManager;
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics;
|
||||
import com.google.gson.Gson;
|
||||
import okhttp3.ResponseBody;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import retrofit2.Call;
|
||||
@@ -40,14 +52,17 @@ import retrofit2.Response;
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private Context mContext;
|
||||
private Switch gatewaySwitch, receiveSMSSwitch;
|
||||
private EditText apiKeyEditText, fcmTokenEditText;
|
||||
private Button registerDeviceBtn, grantSMSPermissionBtn, scanQRBtn;
|
||||
private Switch gatewaySwitch, receiveSMSSwitch, stickyNotificationSwitch;
|
||||
private EditText apiKeyEditText, fcmTokenEditText, deviceIdEditText, deviceNameEditText, smsSendDelayEditText;
|
||||
private Button registerDeviceBtn, grantSMSPermissionBtn, scanQRBtn, checkUpdatesBtn, configureFilterBtn;
|
||||
private ImageButton copyDeviceIdImgBtn;
|
||||
private TextView deviceBrandAndModelTxt, deviceIdTxt;
|
||||
private TextView deviceBrandAndModelTxt, deviceIdTxt, appVersionNameTxt, appVersionCodeTxt;
|
||||
private RadioGroup defaultSimSlotRadioGroup;
|
||||
private static final int SCAN_QR_REQUEST_CODE = 49374;
|
||||
private static final int PERMISSION_REQUEST_CODE = 0;
|
||||
private static final long SMS_DELAY_SAVE_DEBOUNCE_MS = 3000L;
|
||||
private final Handler smsDelaySaveHandler = new Handler(Looper.getMainLooper());
|
||||
private Runnable smsDelaySaveRunnable;
|
||||
private String deviceId = null;
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
@@ -60,8 +75,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
setContentView(R.layout.activity_main);
|
||||
gatewaySwitch = findViewById(R.id.gatewaySwitch);
|
||||
receiveSMSSwitch = findViewById(R.id.receiveSMSSwitch);
|
||||
stickyNotificationSwitch = findViewById(R.id.stickyNotificationSwitch);
|
||||
apiKeyEditText = findViewById(R.id.apiKeyEditText);
|
||||
fcmTokenEditText = findViewById(R.id.fcmTokenEditText);
|
||||
deviceIdEditText = findViewById(R.id.deviceIdEditText);
|
||||
deviceNameEditText = findViewById(R.id.deviceNameEditText);
|
||||
registerDeviceBtn = findViewById(R.id.registerDeviceBtn);
|
||||
grantSMSPermissionBtn = findViewById(R.id.grantSMSPermissionBtn);
|
||||
scanQRBtn = findViewById(R.id.scanQRButton);
|
||||
@@ -69,9 +87,47 @@ public class MainActivity extends AppCompatActivity {
|
||||
deviceIdTxt = findViewById(R.id.deviceIdTxt);
|
||||
copyDeviceIdImgBtn = findViewById(R.id.copyDeviceIdImgBtn);
|
||||
defaultSimSlotRadioGroup = findViewById(R.id.defaultSimSlotRadioGroup);
|
||||
appVersionNameTxt = findViewById(R.id.appVersionNameTxt);
|
||||
appVersionCodeTxt = findViewById(R.id.appVersionCodeTxt);
|
||||
checkUpdatesBtn = findViewById(R.id.checkUpdatesBtn);
|
||||
configureFilterBtn = findViewById(R.id.configureFilterBtn);
|
||||
smsSendDelayEditText = findViewById(R.id.smsSendDelayEditText);
|
||||
|
||||
deviceIdTxt.setText(deviceId);
|
||||
deviceIdEditText.setText(deviceId);
|
||||
deviceBrandAndModelTxt.setText(Build.BRAND + " " + Build.MODEL);
|
||||
|
||||
// Set app version information
|
||||
String versionName = BuildConfig.VERSION_NAME;
|
||||
appVersionNameTxt.setText(versionName);
|
||||
appVersionCodeTxt.setText(String.valueOf(BuildConfig.VERSION_CODE));
|
||||
|
||||
// Check for app version changes and report if needed
|
||||
if (VersionTracker.hasVersionChanged(mContext)) {
|
||||
Log.d(TAG, "App version changed or first launch, reporting to server");
|
||||
VersionTracker.reportVersionToServer(mContext);
|
||||
}
|
||||
|
||||
// Initialize Crashlytics with user information
|
||||
FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
|
||||
crashlytics.setCustomKey("device_id", deviceId != null ? deviceId : "not_registered");
|
||||
crashlytics.setCustomKey("device_model", Build.MODEL);
|
||||
crashlytics.setCustomKey("app_version", versionName);
|
||||
crashlytics.setCustomKey("app_version_code", BuildConfig.VERSION_CODE);
|
||||
|
||||
// Start sticky notification service if enabled
|
||||
boolean gatewayEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, false);
|
||||
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY, false);
|
||||
if (gatewayEnabled && stickyNotificationEnabled) {
|
||||
TextBeeUtils.startStickyNotificationService(mContext);
|
||||
Log.d(TAG, "Starting sticky notification service on app start");
|
||||
}
|
||||
|
||||
// Schedule heartbeat if device is enabled and registered
|
||||
if (gatewayEnabled && !deviceId.isEmpty()) {
|
||||
HeartbeatManager.scheduleHeartbeat(mContext);
|
||||
Log.d(TAG, "Scheduling heartbeat on app start");
|
||||
}
|
||||
|
||||
if (deviceId == null || deviceId.isEmpty()) {
|
||||
registerDeviceBtn.setText("Register");
|
||||
@@ -100,6 +156,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
apiKeyEditText.setText(SharedPreferenceHelper.getSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_API_KEY_KEY, ""));
|
||||
String storedDeviceName = SharedPreferenceHelper.getSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_NAME_KEY, "");
|
||||
if (storedDeviceName.isEmpty()) {
|
||||
deviceNameEditText.setText(Build.BRAND + " " + Build.MODEL);
|
||||
} else {
|
||||
deviceNameEditText.setText(storedDeviceName);
|
||||
}
|
||||
gatewaySwitch.setChecked(SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, false));
|
||||
gatewaySwitch.setOnCheckedChangeListener((compoundButton, isCheked) -> {
|
||||
View view = compoundButton.getRootView();
|
||||
@@ -117,7 +179,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
Log.d(TAG, response.toString());
|
||||
if (!response.isSuccessful()) {
|
||||
Snackbar.make(view, response.message(), Snackbar.LENGTH_LONG).show();
|
||||
Snackbar.make(view, extractErrorMessage(response), Snackbar.LENGTH_LONG).show();
|
||||
compoundButton.setEnabled(true);
|
||||
return;
|
||||
}
|
||||
@@ -125,11 +187,18 @@ public class MainActivity extends AppCompatActivity {
|
||||
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, isCheked);
|
||||
boolean enabled = Boolean.TRUE.equals(Objects.requireNonNull(response.body()).data.get("enabled"));
|
||||
compoundButton.setChecked(enabled);
|
||||
// if (enabled) {
|
||||
// TextBeeUtils.startStickyNotificationService(mContext);
|
||||
// } else {
|
||||
// TextBeeUtils.stopStickyNotificationService(mContext);
|
||||
// }
|
||||
if (enabled) {
|
||||
// Check if sticky notification is enabled
|
||||
if (SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY, false)) {
|
||||
TextBeeUtils.startStickyNotificationService(mContext);
|
||||
}
|
||||
// Schedule heartbeat
|
||||
HeartbeatManager.scheduleHeartbeat(mContext);
|
||||
} else {
|
||||
TextBeeUtils.stopStickyNotificationService(mContext);
|
||||
// Cancel heartbeat
|
||||
HeartbeatManager.cancelHeartbeat(mContext);
|
||||
}
|
||||
compoundButton.setEnabled(true);
|
||||
}
|
||||
@Override
|
||||
@@ -137,6 +206,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
Snackbar.make(view, "An error occurred :(", Snackbar.LENGTH_LONG).show();
|
||||
Log.e(TAG, "API_ERROR "+ t.getMessage());
|
||||
Log.e(TAG, "API_ERROR "+ t.getLocalizedMessage());
|
||||
TextBeeUtils.logException(t, "Error updating device");
|
||||
compoundButton.setEnabled(true);
|
||||
}
|
||||
});
|
||||
@@ -150,6 +220,21 @@ public class MainActivity extends AppCompatActivity {
|
||||
Snackbar.make(view, "Receive SMS " + (isCheked ? "enabled" : "disabled"), Snackbar.LENGTH_LONG).show();
|
||||
});
|
||||
|
||||
// Setup sticky notification switch
|
||||
stickyNotificationSwitch.setChecked(SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY, false));
|
||||
stickyNotificationSwitch.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
||||
View view = compoundButton.getRootView();
|
||||
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY, isChecked);
|
||||
|
||||
if (isChecked) {
|
||||
TextBeeUtils.startStickyNotificationService(mContext);
|
||||
Snackbar.make(view, "Background service enabled - app will be more reliable", Snackbar.LENGTH_LONG).show();
|
||||
} else {
|
||||
TextBeeUtils.stopStickyNotificationService(mContext);
|
||||
Snackbar.make(view, "Background service disabled - app may be killed when in background", Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: check gateway status/api key/device validity and update UI accordingly
|
||||
registerDeviceBtn.setOnClickListener(view -> {
|
||||
String _deviceId = SharedPreferenceHelper.getSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, "");
|
||||
@@ -165,29 +250,113 @@ public class MainActivity extends AppCompatActivity {
|
||||
intentIntegrator.setRequestCode(SCAN_QR_REQUEST_CODE);
|
||||
intentIntegrator.initiateScan();
|
||||
});
|
||||
|
||||
checkUpdatesBtn.setOnClickListener(view -> {
|
||||
String versionInfo = BuildConfig.VERSION_NAME + "(" + BuildConfig.VERSION_CODE + ")";
|
||||
String encodedVersionInfo = android.net.Uri.encode(versionInfo);
|
||||
String downloadUrl = "https://textbee.dev/download?currentVersion=" + encodedVersionInfo;
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(downloadUrl));
|
||||
startActivity(browserIntent);
|
||||
});
|
||||
|
||||
configureFilterBtn.setOnClickListener(view -> {
|
||||
Intent filterIntent = new Intent(MainActivity.this, SMSFilterActivity.class);
|
||||
startActivity(filterIntent);
|
||||
});
|
||||
|
||||
// SMS Send Delay setting: save 3 seconds after user stops typing
|
||||
int currentDelay = SharedPreferenceHelper.getSharedPreferenceInt(
|
||||
mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS);
|
||||
smsSendDelayEditText.setText(String.valueOf(currentDelay));
|
||||
smsDelaySaveRunnable = this::saveSendDelay;
|
||||
smsSendDelayEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
smsDelaySaveHandler.removeCallbacks(smsDelaySaveRunnable);
|
||||
smsDelaySaveHandler.postDelayed(smsDelaySaveRunnable, SMS_DELAY_SAVE_DEBOUNCE_MS);
|
||||
}
|
||||
});
|
||||
smsSendDelayEditText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
smsDelaySaveHandler.removeCallbacks(smsDelaySaveRunnable);
|
||||
saveSendDelay();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void saveSendDelay() {
|
||||
String text = smsSendDelayEditText.getText().toString().trim();
|
||||
if (text.isEmpty()) {
|
||||
int defaultDelay = AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS;
|
||||
smsSendDelayEditText.setText(String.valueOf(defaultDelay));
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, defaultDelay);
|
||||
Snackbar.make(smsSendDelayEditText, "SMS send delay saved (" + defaultDelay + " sec)", Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int value = Integer.parseInt(text);
|
||||
if (value < 0) {
|
||||
value = 0;
|
||||
smsSendDelayEditText.setText("0");
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, 0);
|
||||
Snackbar.make(smsSendDelayEditText, "Minimum delay is 0 seconds. Saved.", Snackbar.LENGTH_SHORT).show();
|
||||
} else if (value > 3600) {
|
||||
value = 3600;
|
||||
smsSendDelayEditText.setText("3600");
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, 3600);
|
||||
Snackbar.make(smsSendDelayEditText, "Maximum delay is 3600 seconds. Saved.", Snackbar.LENGTH_SHORT).show();
|
||||
} else {
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, value);
|
||||
Snackbar.make(smsSendDelayEditText, "SMS send delay saved (" + value + " sec)", Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
int defaultDelay = AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS;
|
||||
smsSendDelayEditText.setText(String.valueOf(defaultDelay));
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, defaultDelay);
|
||||
Snackbar.make(smsSendDelayEditText, "Invalid value. Reset to " + defaultDelay + " sec.", Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderAvailableSimOptions() {
|
||||
try {
|
||||
defaultSimSlotRadioGroup.removeAllViews();
|
||||
|
||||
// Set radio group styling for dark mode compatibility
|
||||
defaultSimSlotRadioGroup.setBackgroundColor(getResources().getColor(R.color.background_secondary));
|
||||
defaultSimSlotRadioGroup.setPadding(16, 8, 16, 8);
|
||||
|
||||
// Create the default radio button with proper styling
|
||||
RadioButton defaultSimSlotRadioBtn = new RadioButton(mContext);
|
||||
defaultSimSlotRadioBtn.setText("Device Default");
|
||||
defaultSimSlotRadioBtn.setId((int)123456);
|
||||
applyRadioButtonStyle(defaultSimSlotRadioBtn);
|
||||
defaultSimSlotRadioGroup.addView(defaultSimSlotRadioBtn);
|
||||
|
||||
// Create radio buttons for each SIM with proper styling
|
||||
TextBeeUtils.getAvailableSimSlots(mContext).forEach(subscriptionInfo -> {
|
||||
String simInfo = "SIM " + (subscriptionInfo.getSimSlotIndex() + 1) + " (" + subscriptionInfo.getDisplayName() + ")";
|
||||
String displayName = subscriptionInfo.getDisplayName() != null ? subscriptionInfo.getDisplayName().toString() : "Unknown";
|
||||
String simInfo = displayName + " (Subscription ID: " + subscriptionInfo.getSubscriptionId() + ")";
|
||||
RadioButton radioButton = new RadioButton(mContext);
|
||||
radioButton.setText(simInfo);
|
||||
radioButton.setId(subscriptionInfo.getSubscriptionId());
|
||||
applyRadioButtonStyle(radioButton);
|
||||
defaultSimSlotRadioGroup.addView(radioButton);
|
||||
});
|
||||
|
||||
// Check the preferred SIM based on saved preferences
|
||||
int preferredSim = SharedPreferenceHelper.getSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY, -1);
|
||||
if (preferredSim == -1) {
|
||||
defaultSimSlotRadioGroup.check(defaultSimSlotRadioBtn.getId());
|
||||
} else {
|
||||
defaultSimSlotRadioGroup.check(preferredSim);
|
||||
}
|
||||
|
||||
// Set the listener for SIM selection changes
|
||||
defaultSimSlotRadioGroup.setOnCheckedChangeListener((radioGroup, i) -> {
|
||||
RadioButton radioButton = findViewById(i);
|
||||
if (radioButton == null) {
|
||||
@@ -205,6 +374,80 @@ public class MainActivity extends AppCompatActivity {
|
||||
Log.e(TAG, "SIM_SLOT_ERROR "+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts error message from API response, trying multiple sources:
|
||||
* 1. Error message from response body (error field)
|
||||
* 2. Response message from HTTP headers
|
||||
* 3. Generic error with status code as fallback
|
||||
*/
|
||||
private String extractErrorMessage(Response<?> response) {
|
||||
// Try to parse error from response body
|
||||
try {
|
||||
ResponseBody errorBody = response.errorBody();
|
||||
if (errorBody != null) {
|
||||
String errorBodyString = errorBody.string();
|
||||
if (errorBodyString != null && !errorBodyString.isEmpty()) {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
RegisterDeviceResponseDTO errorResponse = gson.fromJson(errorBodyString, RegisterDeviceResponseDTO.class);
|
||||
if (errorResponse != null && errorResponse.error != null && !errorResponse.error.isEmpty()) {
|
||||
return errorResponse.error;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// If JSON parsing fails, try to extract message from raw string
|
||||
Log.d(TAG, "Could not parse error response as JSON: " + errorBodyString);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Could not read error body: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Fall back to response message
|
||||
if (response.message() != null && !response.message().isEmpty()) {
|
||||
return response.message();
|
||||
}
|
||||
|
||||
// Final fallback to generic error with status code
|
||||
return "An error occurred :( " + response.code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the custom radio button style to a programmatically created radio button
|
||||
*/
|
||||
private void applyRadioButtonStyle(RadioButton radioButton) {
|
||||
// Set text color using the color state list for proper dark/light mode handling
|
||||
setRadioButtonTextColor(radioButton);
|
||||
|
||||
// Set button tint for the radio circle
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
radioButton.setButtonTintList(getResources().getColorStateList(R.color.radio_button_tint, getTheme()));
|
||||
} else {
|
||||
radioButton.setButtonTintList(getResources().getColorStateList(R.color.radio_button_tint));
|
||||
}
|
||||
}
|
||||
|
||||
// Add proper padding for better touch experience
|
||||
radioButton.setPadding(
|
||||
radioButton.getPaddingLeft() + 8,
|
||||
radioButton.getPaddingTop() + 12,
|
||||
radioButton.getPaddingRight(),
|
||||
radioButton.getPaddingBottom() + 12
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set radio button text color in a backward-compatible way
|
||||
*/
|
||||
private void setRadioButtonTextColor(RadioButton radioButton) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
radioButton.setTextColor(getResources().getColorStateList(R.color.radio_button_text_color, getTheme()));
|
||||
} else {
|
||||
radioButton.setTextColor(getResources().getColorStateList(R.color.radio_button_text_color));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
|
||||
@@ -225,8 +468,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void handleRegisterDevice() {
|
||||
|
||||
String newKey = apiKeyEditText.getText().toString();
|
||||
String deviceIdInput = deviceIdEditText.getText().toString();
|
||||
|
||||
registerDeviceBtn.setEnabled(false);
|
||||
registerDeviceBtn.setText("Loading...");
|
||||
View view = findViewById(R.id.registerDeviceBtn);
|
||||
@@ -252,32 +496,148 @@ public class MainActivity extends AppCompatActivity {
|
||||
registerDeviceInput.setOs(Build.VERSION.BASE_OS);
|
||||
registerDeviceInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
registerDeviceInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
|
||||
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService().registerDevice(newKey, registerDeviceInput);
|
||||
apiCall.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
Log.d(TAG, response.toString());
|
||||
if (!response.isSuccessful()) {
|
||||
Snackbar.make(view, response.message(), Snackbar.LENGTH_LONG).show();
|
||||
|
||||
// Get device name from input field or default to "brand model"
|
||||
String deviceName = deviceNameEditText.getText().toString().trim();
|
||||
if (deviceName.isEmpty()) {
|
||||
deviceName = Build.BRAND + " " + Build.MODEL;
|
||||
}
|
||||
registerDeviceInput.setName(deviceName);
|
||||
|
||||
// Collect SIM information
|
||||
SimInfoCollectionDTO simInfoCollection = new SimInfoCollectionDTO();
|
||||
simInfoCollection.setLastUpdated(System.currentTimeMillis());
|
||||
simInfoCollection.setSims(TextBeeUtils.collectSimInfo(mContext));
|
||||
registerDeviceInput.setSimInfo(simInfoCollection);
|
||||
|
||||
// If the user provided a device ID, use it for updating instead of creating new
|
||||
if (!deviceIdInput.isEmpty()) {
|
||||
Log.d(TAG, "Updating device with deviceId: "+ deviceIdInput);
|
||||
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService().updateDevice(deviceIdInput, newKey, registerDeviceInput);
|
||||
apiCall.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
Log.d(TAG, response.toString());
|
||||
if (!response.isSuccessful()) {
|
||||
Snackbar.make(view, extractErrorMessage(response), Snackbar.LENGTH_LONG).show();
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
return;
|
||||
}
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_API_KEY_KEY, newKey);
|
||||
Snackbar.make(view, "Device Updated Successfully :)", Snackbar.LENGTH_LONG).show();
|
||||
|
||||
// Update deviceId from response if available
|
||||
if (response.body() != null && response.body().data != null && response.body().data.get("_id") != null) {
|
||||
deviceId = response.body().data.get("_id").toString();
|
||||
deviceIdTxt.setText(deviceId);
|
||||
deviceIdEditText.setText(deviceId);
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, deviceId);
|
||||
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, registerDeviceInput.isEnabled());
|
||||
gatewaySwitch.setChecked(registerDeviceInput.isEnabled());
|
||||
|
||||
// Sync heartbeatIntervalMinutes from server response
|
||||
if (response.body().data.get("heartbeatIntervalMinutes") != null) {
|
||||
Object intervalObj = response.body().data.get("heartbeatIntervalMinutes");
|
||||
if (intervalObj instanceof Number) {
|
||||
int intervalMinutes = ((Number) intervalObj).intValue();
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY, intervalMinutes);
|
||||
Log.d(TAG, "Synced heartbeat interval from server: " + intervalMinutes + " minutes");
|
||||
}
|
||||
}
|
||||
|
||||
// Sync device name from server response
|
||||
if (response.body().data.get("name") != null) {
|
||||
String deviceName = response.body().data.get("name").toString();
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_NAME_KEY, deviceName);
|
||||
deviceNameEditText.setText(deviceName);
|
||||
Log.d(TAG, "Synced device name from server: " + deviceName);
|
||||
}
|
||||
|
||||
// Schedule heartbeat if device is enabled
|
||||
if (registerDeviceInput.isEnabled()) {
|
||||
HeartbeatManager.scheduleHeartbeat(mContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Update stored version information
|
||||
VersionTracker.updateStoredVersion(mContext);
|
||||
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
return;
|
||||
}
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_API_KEY_KEY, newKey);
|
||||
Snackbar.make(view, "Device Registration Successful :)", Snackbar.LENGTH_LONG).show();
|
||||
deviceId = response.body().data.get("_id").toString();
|
||||
deviceIdTxt.setText(deviceId);
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, deviceId);
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<RegisterDeviceResponseDTO> call, Throwable t) {
|
||||
Snackbar.make(view, "An error occurred :(", Snackbar.LENGTH_LONG).show();
|
||||
Log.e(TAG, "API_ERROR "+ t.getMessage());
|
||||
Log.e(TAG, "API_ERROR "+ t.getLocalizedMessage());
|
||||
TextBeeUtils.logException(t, "Error registering device");
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService().registerDevice(newKey, registerDeviceInput);
|
||||
apiCall.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
Log.d(TAG, response.toString());
|
||||
if (!response.isSuccessful()) {
|
||||
Snackbar.make(view, extractErrorMessage(response), Snackbar.LENGTH_LONG).show();
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
return;
|
||||
}
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_API_KEY_KEY, newKey);
|
||||
Snackbar.make(view, "Device Registration Successful :)", Snackbar.LENGTH_LONG).show();
|
||||
|
||||
if (response.body() != null && response.body().data != null && response.body().data.get("_id") != null) {
|
||||
deviceId = response.body().data.get("_id").toString();
|
||||
deviceIdTxt.setText(deviceId);
|
||||
deviceIdEditText.setText(deviceId);
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, deviceId);
|
||||
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, registerDeviceInput.isEnabled());
|
||||
gatewaySwitch.setChecked(registerDeviceInput.isEnabled());
|
||||
|
||||
// Sync heartbeatIntervalMinutes from server response
|
||||
if (response.body().data.get("heartbeatIntervalMinutes") != null) {
|
||||
Object intervalObj = response.body().data.get("heartbeatIntervalMinutes");
|
||||
if (intervalObj instanceof Number) {
|
||||
int intervalMinutes = ((Number) intervalObj).intValue();
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY, intervalMinutes);
|
||||
Log.d(TAG, "Synced heartbeat interval from server: " + intervalMinutes + " minutes");
|
||||
}
|
||||
}
|
||||
|
||||
// Sync device name from server response
|
||||
if (response.body().data.get("name") != null) {
|
||||
String deviceName = response.body().data.get("name").toString();
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_NAME_KEY, deviceName);
|
||||
deviceNameEditText.setText(deviceName);
|
||||
Log.d(TAG, "Synced device name from server: " + deviceName);
|
||||
}
|
||||
|
||||
// Schedule heartbeat if device is enabled
|
||||
if (registerDeviceInput.isEnabled()) {
|
||||
HeartbeatManager.scheduleHeartbeat(mContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Update stored version information
|
||||
VersionTracker.updateStoredVersion(mContext);
|
||||
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
|
||||
}
|
||||
@Override
|
||||
public void onFailure(Call<RegisterDeviceResponseDTO> call, Throwable t) {
|
||||
Snackbar.make(view, "An error occurred :(", Snackbar.LENGTH_LONG).show();
|
||||
Log.e(TAG, "API_ERROR "+ t.getMessage());
|
||||
Log.e(TAG, "API_ERROR "+ t.getLocalizedMessage());
|
||||
TextBeeUtils.logException(t, "Error registering device");
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
}
|
||||
@@ -287,6 +647,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private void handleUpdateDevice() {
|
||||
String apiKey = apiKeyEditText.getText().toString();
|
||||
String deviceIdInput = deviceIdEditText.getText().toString();
|
||||
String deviceIdToUse = !deviceIdInput.isEmpty() ? deviceIdInput : deviceId;
|
||||
|
||||
registerDeviceBtn.setEnabled(false);
|
||||
registerDeviceBtn.setText("Loading...");
|
||||
View view = findViewById(R.id.registerDeviceBtn);
|
||||
@@ -313,18 +676,66 @@ public class MainActivity extends AppCompatActivity {
|
||||
updateDeviceInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
updateDeviceInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
|
||||
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService().updateDevice(deviceId, apiKey, updateDeviceInput);
|
||||
// Get device name from input field or default to "brand model"
|
||||
String deviceName = deviceNameEditText.getText().toString().trim();
|
||||
if (deviceName.isEmpty()) {
|
||||
deviceName = Build.BRAND + " " + Build.MODEL;
|
||||
}
|
||||
updateDeviceInput.setName(deviceName);
|
||||
|
||||
// Collect SIM information
|
||||
SimInfoCollectionDTO simInfoCollection = new SimInfoCollectionDTO();
|
||||
simInfoCollection.setLastUpdated(System.currentTimeMillis());
|
||||
simInfoCollection.setSims(TextBeeUtils.collectSimInfo(mContext));
|
||||
updateDeviceInput.setSimInfo(simInfoCollection);
|
||||
|
||||
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService().updateDevice(deviceIdToUse, apiKey, updateDeviceInput);
|
||||
apiCall.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
Log.d(TAG, response.toString());
|
||||
if (!response.isSuccessful()) {
|
||||
Snackbar.make(view, response.message(), Snackbar.LENGTH_LONG).show();
|
||||
Snackbar.make(view, extractErrorMessage(response), Snackbar.LENGTH_LONG).show();
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
return;
|
||||
}
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_API_KEY_KEY, apiKey);
|
||||
|
||||
// Update deviceId from response if available
|
||||
if (response.body() != null && response.body().data != null && response.body().data.get("_id") != null) {
|
||||
deviceId = response.body().data.get("_id").toString();
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, deviceId);
|
||||
deviceIdTxt.setText(deviceId);
|
||||
deviceIdEditText.setText(deviceId);
|
||||
|
||||
// Sync heartbeatIntervalMinutes from server response
|
||||
if (response.body().data.get("heartbeatIntervalMinutes") != null) {
|
||||
Object intervalObj = response.body().data.get("heartbeatIntervalMinutes");
|
||||
if (intervalObj instanceof Number) {
|
||||
int intervalMinutes = ((Number) intervalObj).intValue();
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY, intervalMinutes);
|
||||
Log.d(TAG, "Synced heartbeat interval from server: " + intervalMinutes + " minutes");
|
||||
}
|
||||
}
|
||||
|
||||
// Sync device name from server response
|
||||
if (response.body().data.get("name") != null) {
|
||||
String deviceName = response.body().data.get("name").toString();
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_NAME_KEY, deviceName);
|
||||
deviceNameEditText.setText(deviceName);
|
||||
Log.d(TAG, "Synced device name from server: " + deviceName);
|
||||
}
|
||||
|
||||
// Schedule heartbeat if device is enabled
|
||||
if (updateDeviceInput.isEnabled()) {
|
||||
HeartbeatManager.scheduleHeartbeat(mContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Update stored version information
|
||||
VersionTracker.updateStoredVersion(mContext);
|
||||
|
||||
Snackbar.make(view, "Device Updated Successfully :)", Snackbar.LENGTH_LONG).show();
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
@@ -335,6 +746,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
Snackbar.make(view, "An error occurred :(", Snackbar.LENGTH_LONG).show();
|
||||
Log.e(TAG, "API_ERROR "+ t.getMessage());
|
||||
Log.e(TAG, "API_ERROR "+ t.getLocalizedMessage());
|
||||
TextBeeUtils.logException(t, "Error updating device");
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
}
|
||||
@@ -364,7 +776,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
String scannedQR = intentResult.getContents();
|
||||
apiKeyEditText.setText(scannedQR);
|
||||
handleRegisterDevice();
|
||||
if(deviceIdEditText.getText().toString().isEmpty()) {
|
||||
handleRegisterDevice();
|
||||
} else {
|
||||
handleUpdateDevice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.vernu.sms.activities;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.vernu.sms.R;
|
||||
import com.vernu.sms.helpers.SMSFilterHelper;
|
||||
import com.vernu.sms.models.SMSFilterRule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SMSFilterActivity extends AppCompatActivity {
|
||||
private Context mContext;
|
||||
private Switch filterEnabledSwitch;
|
||||
private RadioGroup filterModeRadioGroup;
|
||||
private RadioButton allowListRadio;
|
||||
private RadioButton blockListRadio;
|
||||
private RecyclerView filterRulesRecyclerView;
|
||||
private FloatingActionButton addRuleFab;
|
||||
private FilterRulesAdapter adapter;
|
||||
private SMSFilterHelper.FilterConfig filterConfig;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_sms_filter);
|
||||
|
||||
mContext = getApplicationContext();
|
||||
|
||||
// Initialize views
|
||||
ImageButton backButton = findViewById(R.id.backButton);
|
||||
filterEnabledSwitch = findViewById(R.id.filterEnabledSwitch);
|
||||
filterModeRadioGroup = findViewById(R.id.filterModeRadioGroup);
|
||||
allowListRadio = findViewById(R.id.allowListRadio);
|
||||
blockListRadio = findViewById(R.id.blockListRadio);
|
||||
filterRulesRecyclerView = findViewById(R.id.filterRulesRecyclerView);
|
||||
addRuleFab = findViewById(R.id.addRuleFab);
|
||||
|
||||
// Setup back button
|
||||
backButton.setOnClickListener(v -> finish());
|
||||
|
||||
// Load filter config
|
||||
filterConfig = SMSFilterHelper.loadFilterConfig(mContext);
|
||||
|
||||
// Setup RecyclerView
|
||||
adapter = new FilterRulesAdapter(filterConfig.getRules());
|
||||
filterRulesRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
filterRulesRecyclerView.setAdapter(adapter);
|
||||
|
||||
// Load current settings
|
||||
filterEnabledSwitch.setChecked(filterConfig.isEnabled());
|
||||
// Default to block list if mode is not set
|
||||
if (filterConfig.getMode() == SMSFilterHelper.FilterMode.ALLOW_LIST) {
|
||||
allowListRadio.setChecked(true);
|
||||
} else {
|
||||
blockListRadio.setChecked(true);
|
||||
}
|
||||
|
||||
// Setup listeners
|
||||
filterEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
filterConfig.setEnabled(isChecked);
|
||||
saveFilterConfig();
|
||||
});
|
||||
|
||||
filterModeRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
if (checkedId == R.id.allowListRadio) {
|
||||
filterConfig.setMode(SMSFilterHelper.FilterMode.ALLOW_LIST);
|
||||
} else {
|
||||
filterConfig.setMode(SMSFilterHelper.FilterMode.BLOCK_LIST);
|
||||
}
|
||||
saveFilterConfig();
|
||||
});
|
||||
|
||||
addRuleFab.setOnClickListener(v -> showAddEditRuleDialog(-1));
|
||||
}
|
||||
|
||||
private void saveFilterConfig() {
|
||||
SMSFilterHelper.saveFilterConfig(mContext, filterConfig);
|
||||
}
|
||||
|
||||
private void showAddEditRuleDialog(int position) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_filter_rule, null);
|
||||
builder.setView(dialogView);
|
||||
|
||||
TextInputEditText patternEditText = dialogView.findViewById(R.id.patternEditText);
|
||||
Spinner filterTargetSpinner = dialogView.findViewById(R.id.filterTargetSpinner);
|
||||
Spinner matchTypeSpinner = dialogView.findViewById(R.id.matchTypeSpinner);
|
||||
Switch caseSensitiveSwitch = dialogView.findViewById(R.id.caseSensitiveSwitch);
|
||||
Button cancelButton = dialogView.findViewById(R.id.cancelButton);
|
||||
Button saveButton = dialogView.findViewById(R.id.saveButton);
|
||||
|
||||
// Setup filter target spinner
|
||||
String[] filterTargets = {"Sender", "Message", "Both"};
|
||||
ArrayAdapter<String> targetAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, filterTargets);
|
||||
targetAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
filterTargetSpinner.setAdapter(targetAdapter);
|
||||
|
||||
// Setup match type spinner
|
||||
String[] matchTypes = {"Exact Match", "Starts With", "Ends With", "Contains"};
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, matchTypes);
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
matchTypeSpinner.setAdapter(spinnerAdapter);
|
||||
|
||||
// If editing, populate fields
|
||||
boolean isEdit = position >= 0;
|
||||
TextView dialogTitle = dialogView.findViewById(R.id.dialogTitle);
|
||||
if (isEdit) {
|
||||
SMSFilterRule rule = filterConfig.getRules().get(position);
|
||||
patternEditText.setText(rule.getPattern());
|
||||
filterTargetSpinner.setSelection(rule.getFilterTarget().ordinal());
|
||||
matchTypeSpinner.setSelection(rule.getMatchType().ordinal());
|
||||
caseSensitiveSwitch.setChecked(rule.isCaseSensitive());
|
||||
if (dialogTitle != null) {
|
||||
dialogTitle.setText("Edit Filter Rule");
|
||||
}
|
||||
} else {
|
||||
// Default to case insensitive
|
||||
caseSensitiveSwitch.setChecked(false);
|
||||
}
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
cancelButton.setOnClickListener(v -> dialog.dismiss());
|
||||
|
||||
saveButton.setOnClickListener(v -> {
|
||||
String pattern = patternEditText.getText() != null ? patternEditText.getText().toString().trim() : "";
|
||||
if (pattern.isEmpty()) {
|
||||
Toast.makeText(this, "Please enter a pattern", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
SMSFilterRule.FilterTarget filterTarget = SMSFilterRule.FilterTarget.values()[filterTargetSpinner.getSelectedItemPosition()];
|
||||
SMSFilterRule.MatchType matchType = SMSFilterRule.MatchType.values()[matchTypeSpinner.getSelectedItemPosition()];
|
||||
boolean caseSensitive = caseSensitiveSwitch.isChecked();
|
||||
|
||||
if (isEdit) {
|
||||
SMSFilterRule rule = filterConfig.getRules().get(position);
|
||||
rule.setPattern(pattern);
|
||||
rule.setFilterTarget(filterTarget);
|
||||
rule.setMatchType(matchType);
|
||||
rule.setCaseSensitive(caseSensitive);
|
||||
adapter.notifyItemChanged(position);
|
||||
} else {
|
||||
SMSFilterRule newRule = new SMSFilterRule(pattern, matchType, filterTarget, caseSensitive);
|
||||
filterConfig.getRules().add(newRule);
|
||||
adapter.notifyItemInserted(filterConfig.getRules().size() - 1);
|
||||
}
|
||||
|
||||
saveFilterConfig();
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void deleteRule(int position) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Delete Rule")
|
||||
.setMessage("Are you sure you want to delete this filter rule?")
|
||||
.setPositiveButton("Delete", (dialog, which) -> {
|
||||
filterConfig.getRules().remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
saveFilterConfig();
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private class FilterRulesAdapter extends RecyclerView.Adapter<FilterRulesAdapter.ViewHolder> {
|
||||
private List<SMSFilterRule> rules;
|
||||
|
||||
public FilterRulesAdapter(List<SMSFilterRule> rules) {
|
||||
this.rules = rules != null ? rules : new ArrayList<>();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_filter_rule, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
SMSFilterRule rule = rules.get(position);
|
||||
holder.patternText.setText(rule.getPattern());
|
||||
|
||||
String matchTypeText = "";
|
||||
switch (rule.getMatchType()) {
|
||||
case EXACT:
|
||||
matchTypeText = "Exact Match";
|
||||
break;
|
||||
case STARTS_WITH:
|
||||
matchTypeText = "Starts With";
|
||||
break;
|
||||
case ENDS_WITH:
|
||||
matchTypeText = "Ends With";
|
||||
break;
|
||||
case CONTAINS:
|
||||
matchTypeText = "Contains";
|
||||
break;
|
||||
}
|
||||
holder.matchTypeText.setText(matchTypeText);
|
||||
|
||||
String filterTargetText = "";
|
||||
switch (rule.getFilterTarget()) {
|
||||
case SENDER:
|
||||
filterTargetText = "Filter: Sender";
|
||||
break;
|
||||
case MESSAGE:
|
||||
filterTargetText = "Filter: Message";
|
||||
break;
|
||||
case BOTH:
|
||||
filterTargetText = "Filter: Sender or Message";
|
||||
break;
|
||||
}
|
||||
String caseText = rule.isCaseSensitive() ? " (Case Sensitive)" : " (Case Insensitive)";
|
||||
holder.filterTargetText.setText(filterTargetText + caseText);
|
||||
|
||||
holder.editButton.setOnClickListener(v -> showAddEditRuleDialog(position));
|
||||
holder.deleteButton.setOnClickListener(v -> deleteRule(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return rules.size();
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView patternText;
|
||||
TextView matchTypeText;
|
||||
TextView filterTargetText;
|
||||
ImageButton editButton;
|
||||
ImageButton deleteButton;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
patternText = itemView.findViewById(R.id.patternText);
|
||||
matchTypeText = itemView.findViewById(R.id.matchTypeText);
|
||||
filterTargetText = itemView.findViewById(R.id.filterTargetText);
|
||||
editButton = itemView.findViewById(R.id.editButton);
|
||||
deleteButton = itemView.findViewById(R.id.deleteButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.vernu.sms.dtos;
|
||||
|
||||
public class HeartbeatInputDTO {
|
||||
private String fcmToken;
|
||||
private Integer batteryPercentage;
|
||||
private Boolean isCharging;
|
||||
private String networkType;
|
||||
private String appVersionName;
|
||||
private Integer appVersionCode;
|
||||
private Long deviceUptimeMillis;
|
||||
private Long memoryFreeBytes;
|
||||
private Long memoryTotalBytes;
|
||||
private Long memoryMaxBytes;
|
||||
private Long storageAvailableBytes;
|
||||
private Long storageTotalBytes;
|
||||
private String timezone;
|
||||
private String locale;
|
||||
private Boolean receiveSMSEnabled;
|
||||
private Integer smsSendDelaySeconds;
|
||||
private SimInfoCollectionDTO simInfo;
|
||||
|
||||
public HeartbeatInputDTO() {
|
||||
}
|
||||
|
||||
public String getFcmToken() {
|
||||
return fcmToken;
|
||||
}
|
||||
|
||||
public void setFcmToken(String fcmToken) {
|
||||
this.fcmToken = fcmToken;
|
||||
}
|
||||
|
||||
public Integer getBatteryPercentage() {
|
||||
return batteryPercentage;
|
||||
}
|
||||
|
||||
public void setBatteryPercentage(Integer batteryPercentage) {
|
||||
this.batteryPercentage = batteryPercentage;
|
||||
}
|
||||
|
||||
public Boolean getIsCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
public void setIsCharging(Boolean isCharging) {
|
||||
this.isCharging = isCharging;
|
||||
}
|
||||
|
||||
public String getNetworkType() {
|
||||
return networkType;
|
||||
}
|
||||
|
||||
public void setNetworkType(String networkType) {
|
||||
this.networkType = networkType;
|
||||
}
|
||||
|
||||
public String getAppVersionName() {
|
||||
return appVersionName;
|
||||
}
|
||||
|
||||
public void setAppVersionName(String appVersionName) {
|
||||
this.appVersionName = appVersionName;
|
||||
}
|
||||
|
||||
public Integer getAppVersionCode() {
|
||||
return appVersionCode;
|
||||
}
|
||||
|
||||
public void setAppVersionCode(Integer appVersionCode) {
|
||||
this.appVersionCode = appVersionCode;
|
||||
}
|
||||
|
||||
public Long getDeviceUptimeMillis() {
|
||||
return deviceUptimeMillis;
|
||||
}
|
||||
|
||||
public void setDeviceUptimeMillis(Long deviceUptimeMillis) {
|
||||
this.deviceUptimeMillis = deviceUptimeMillis;
|
||||
}
|
||||
|
||||
public Long getMemoryFreeBytes() {
|
||||
return memoryFreeBytes;
|
||||
}
|
||||
|
||||
public void setMemoryFreeBytes(Long memoryFreeBytes) {
|
||||
this.memoryFreeBytes = memoryFreeBytes;
|
||||
}
|
||||
|
||||
public Long getMemoryTotalBytes() {
|
||||
return memoryTotalBytes;
|
||||
}
|
||||
|
||||
public void setMemoryTotalBytes(Long memoryTotalBytes) {
|
||||
this.memoryTotalBytes = memoryTotalBytes;
|
||||
}
|
||||
|
||||
public Long getMemoryMaxBytes() {
|
||||
return memoryMaxBytes;
|
||||
}
|
||||
|
||||
public void setMemoryMaxBytes(Long memoryMaxBytes) {
|
||||
this.memoryMaxBytes = memoryMaxBytes;
|
||||
}
|
||||
|
||||
public Long getStorageAvailableBytes() {
|
||||
return storageAvailableBytes;
|
||||
}
|
||||
|
||||
public void setStorageAvailableBytes(Long storageAvailableBytes) {
|
||||
this.storageAvailableBytes = storageAvailableBytes;
|
||||
}
|
||||
|
||||
public Long getStorageTotalBytes() {
|
||||
return storageTotalBytes;
|
||||
}
|
||||
|
||||
public void setStorageTotalBytes(Long storageTotalBytes) {
|
||||
this.storageTotalBytes = storageTotalBytes;
|
||||
}
|
||||
|
||||
public String getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
public void setTimezone(String timezone) {
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
public String getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public void setLocale(String locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public Boolean getReceiveSMSEnabled() {
|
||||
return receiveSMSEnabled;
|
||||
}
|
||||
|
||||
public void setReceiveSMSEnabled(Boolean receiveSMSEnabled) {
|
||||
this.receiveSMSEnabled = receiveSMSEnabled;
|
||||
}
|
||||
|
||||
public Integer getSmsSendDelaySeconds() {
|
||||
return smsSendDelaySeconds;
|
||||
}
|
||||
|
||||
public void setSmsSendDelaySeconds(Integer smsSendDelaySeconds) {
|
||||
this.smsSendDelaySeconds = smsSendDelaySeconds;
|
||||
}
|
||||
|
||||
public SimInfoCollectionDTO getSimInfo() {
|
||||
return simInfo;
|
||||
}
|
||||
|
||||
public void setSimInfo(SimInfoCollectionDTO simInfo) {
|
||||
this.simInfo = simInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.vernu.sms.dtos;
|
||||
|
||||
public class HeartbeatResponseDTO {
|
||||
public boolean success;
|
||||
public boolean fcmTokenUpdated;
|
||||
public long lastHeartbeat;
|
||||
public String name;
|
||||
}
|
||||
@@ -2,16 +2,18 @@ package com.vernu.sms.dtos;
|
||||
|
||||
public class RegisterDeviceInputDTO {
|
||||
private String fcmToken;
|
||||
private boolean enabled;
|
||||
private Boolean enabled;
|
||||
private String brand;
|
||||
private String manufacturer;
|
||||
private String model;
|
||||
private String name;
|
||||
private String serial;
|
||||
private String buildId;
|
||||
private String os;
|
||||
private String osVersion;
|
||||
private String appVersionName;
|
||||
private int appVersionCode;
|
||||
private SimInfoCollectionDTO simInfo;
|
||||
|
||||
public RegisterDeviceInputDTO() {
|
||||
}
|
||||
@@ -28,11 +30,11 @@ public class RegisterDeviceInputDTO {
|
||||
this.fcmToken = fcmToken;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
public Boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@@ -60,6 +62,14 @@ public class RegisterDeviceInputDTO {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getSerial() {
|
||||
return serial;
|
||||
}
|
||||
@@ -107,4 +117,12 @@ public class RegisterDeviceInputDTO {
|
||||
public void setAppVersionCode(int appVersionCode) {
|
||||
this.appVersionCode = appVersionCode;
|
||||
}
|
||||
|
||||
public SimInfoCollectionDTO getSimInfo() {
|
||||
return simInfo;
|
||||
}
|
||||
|
||||
public void setSimInfo(SimInfoCollectionDTO simInfo) {
|
||||
this.simInfo = simInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,16 @@ public class SMSDTO {
|
||||
private String sender;
|
||||
private String message = "";
|
||||
private long receivedAtInMillis;
|
||||
private String fingerprint;
|
||||
|
||||
private String smsId;
|
||||
private String smsBatchId;
|
||||
private String status;
|
||||
private long sentAtInMillis;
|
||||
private long deliveredAtInMillis;
|
||||
private long failedAtInMillis;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
public SMSDTO() {
|
||||
}
|
||||
@@ -34,4 +44,76 @@ public class SMSDTO {
|
||||
public void setReceivedAtInMillis(long receivedAtInMillis) {
|
||||
this.receivedAtInMillis = receivedAtInMillis;
|
||||
}
|
||||
|
||||
public String getSmsId() {
|
||||
return smsId;
|
||||
}
|
||||
|
||||
public void setSmsId(String smsId) {
|
||||
this.smsId = smsId;
|
||||
}
|
||||
|
||||
public String getSmsBatchId() {
|
||||
return smsBatchId;
|
||||
}
|
||||
|
||||
public void setSmsBatchId(String smsBatchId) {
|
||||
this.smsBatchId = smsBatchId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public long getSentAtInMillis() {
|
||||
return sentAtInMillis;
|
||||
}
|
||||
|
||||
public void setSentAtInMillis(long sentAtInMillis) {
|
||||
this.sentAtInMillis = sentAtInMillis;
|
||||
}
|
||||
|
||||
public long getDeliveredAtInMillis() {
|
||||
return deliveredAtInMillis;
|
||||
}
|
||||
|
||||
public void setDeliveredAtInMillis(long deliveredAtInMillis) {
|
||||
this.deliveredAtInMillis = deliveredAtInMillis;
|
||||
}
|
||||
|
||||
public long getFailedAtInMillis() {
|
||||
return failedAtInMillis;
|
||||
}
|
||||
|
||||
public void setFailedAtInMillis(long failedAtInMillis) {
|
||||
this.failedAtInMillis = failedAtInMillis;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
public void setFingerprint(String fingerprint) {
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.vernu.sms.dtos;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SimInfoCollectionDTO {
|
||||
private long lastUpdated;
|
||||
private List<SimInfoDTO> sims;
|
||||
|
||||
public SimInfoCollectionDTO() {
|
||||
}
|
||||
|
||||
public long getLastUpdated() {
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
public void setLastUpdated(long lastUpdated) {
|
||||
this.lastUpdated = lastUpdated;
|
||||
}
|
||||
|
||||
public List<SimInfoDTO> getSims() {
|
||||
return sims;
|
||||
}
|
||||
|
||||
public void setSims(List<SimInfoDTO> sims) {
|
||||
this.sims = sims;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.vernu.sms.dtos;
|
||||
|
||||
public class SimInfoDTO {
|
||||
private int subscriptionId;
|
||||
private String iccId;
|
||||
private Integer cardId;
|
||||
private String carrierName;
|
||||
private String displayName;
|
||||
private Integer simSlotIndex;
|
||||
private String mcc;
|
||||
private String mnc;
|
||||
private String countryIso;
|
||||
private String subscriptionType;
|
||||
|
||||
public SimInfoDTO() {
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public void setSubscriptionId(int subscriptionId) {
|
||||
this.subscriptionId = subscriptionId;
|
||||
}
|
||||
|
||||
public String getIccId() {
|
||||
return iccId;
|
||||
}
|
||||
|
||||
public void setIccId(String iccId) {
|
||||
this.iccId = iccId;
|
||||
}
|
||||
|
||||
public Integer getCardId() {
|
||||
return cardId;
|
||||
}
|
||||
|
||||
public void setCardId(Integer cardId) {
|
||||
this.cardId = cardId;
|
||||
}
|
||||
|
||||
public String getCarrierName() {
|
||||
return carrierName;
|
||||
}
|
||||
|
||||
public void setCarrierName(String carrierName) {
|
||||
this.carrierName = carrierName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public Integer getSimSlotIndex() {
|
||||
return simSlotIndex;
|
||||
}
|
||||
|
||||
public void setSimSlotIndex(Integer simSlotIndex) {
|
||||
this.simSlotIndex = simSlotIndex;
|
||||
}
|
||||
|
||||
public String getMcc() {
|
||||
return mcc;
|
||||
}
|
||||
|
||||
public void setMcc(String mcc) {
|
||||
this.mcc = mcc;
|
||||
}
|
||||
|
||||
public String getMnc() {
|
||||
return mnc;
|
||||
}
|
||||
|
||||
public void setMnc(String mnc) {
|
||||
this.mnc = mnc;
|
||||
}
|
||||
|
||||
public String getCountryIso() {
|
||||
return countryIso;
|
||||
}
|
||||
|
||||
public void setCountryIso(String countryIso) {
|
||||
this.countryIso = countryIso;
|
||||
}
|
||||
|
||||
public String getSubscriptionType() {
|
||||
return subscriptionType;
|
||||
}
|
||||
|
||||
public void setSubscriptionType(String subscriptionType) {
|
||||
this.subscriptionType = subscriptionType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package com.vernu.sms.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.StatFs;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.BuildConfig;
|
||||
import com.vernu.sms.dtos.HeartbeatInputDTO;
|
||||
import com.vernu.sms.dtos.HeartbeatResponseDTO;
|
||||
import com.vernu.sms.dtos.SimInfoCollectionDTO;
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class HeartbeatHelper {
|
||||
private static final String TAG = "HeartbeatHelper";
|
||||
|
||||
/**
|
||||
* Collects device information and sends a heartbeat request to the API.
|
||||
*
|
||||
* @param context Application context
|
||||
* @param deviceId Device ID
|
||||
* @param apiKey API key for authentication
|
||||
* @return true if heartbeat was sent successfully, false otherwise
|
||||
*/
|
||||
public static boolean sendHeartbeat(Context context, String deviceId, String apiKey) {
|
||||
if (deviceId == null || deviceId.isEmpty()) {
|
||||
Log.d(TAG, "Device not registered, skipping heartbeat");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
Log.e(TAG, "API key not available, skipping heartbeat");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect device information
|
||||
HeartbeatInputDTO heartbeatInput = new HeartbeatInputDTO();
|
||||
|
||||
try {
|
||||
// Get FCM token (blocking wait)
|
||||
try {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
final String[] fcmToken = new String[1];
|
||||
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
fcmToken[0] = task.getResult();
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
if (latch.await(5, TimeUnit.SECONDS) && fcmToken[0] != null) {
|
||||
heartbeatInput.setFcmToken(fcmToken[0]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to get FCM token: " + e.getMessage());
|
||||
// Continue without FCM token
|
||||
}
|
||||
|
||||
// Get battery information
|
||||
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
|
||||
Intent batteryStatus = context.registerReceiver(null, ifilter);
|
||||
if (batteryStatus != null) {
|
||||
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
int batteryPct = (int) ((level / (float) scale) * 100);
|
||||
heartbeatInput.setBatteryPercentage(batteryPct);
|
||||
|
||||
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
heartbeatInput.setIsCharging(isCharging);
|
||||
}
|
||||
|
||||
// Get network type
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (cm != null) {
|
||||
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||
if (activeNetwork != null && activeNetwork.isConnected()) {
|
||||
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
heartbeatInput.setNetworkType("wifi");
|
||||
} else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
|
||||
heartbeatInput.setNetworkType("cellular");
|
||||
} else {
|
||||
heartbeatInput.setNetworkType("none");
|
||||
}
|
||||
} else {
|
||||
heartbeatInput.setNetworkType("none");
|
||||
}
|
||||
}
|
||||
|
||||
// Get app version
|
||||
heartbeatInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
heartbeatInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
|
||||
// Get device uptime
|
||||
heartbeatInput.setDeviceUptimeMillis(SystemClock.uptimeMillis());
|
||||
|
||||
// Get memory information
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
heartbeatInput.setMemoryFreeBytes(runtime.freeMemory());
|
||||
heartbeatInput.setMemoryTotalBytes(runtime.totalMemory());
|
||||
heartbeatInput.setMemoryMaxBytes(runtime.maxMemory());
|
||||
|
||||
// Get storage information
|
||||
File internalStorage = context.getFilesDir();
|
||||
StatFs statFs = new StatFs(internalStorage.getPath());
|
||||
long availableBytes = statFs.getAvailableBytes();
|
||||
long totalBytes = statFs.getTotalBytes();
|
||||
heartbeatInput.setStorageAvailableBytes(availableBytes);
|
||||
heartbeatInput.setStorageTotalBytes(totalBytes);
|
||||
|
||||
// Get system information
|
||||
heartbeatInput.setTimezone(TimeZone.getDefault().getID());
|
||||
heartbeatInput.setLocale(Locale.getDefault().toString());
|
||||
|
||||
// Get receive SMS enabled status
|
||||
boolean receiveSMSEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_RECEIVE_SMS_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
heartbeatInput.setReceiveSMSEnabled(receiveSMSEnabled);
|
||||
|
||||
// SMS send delay (device queue)
|
||||
int smsSendDelaySeconds = SharedPreferenceHelper.getSharedPreferenceInt(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY,
|
||||
AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS
|
||||
);
|
||||
heartbeatInput.setSmsSendDelaySeconds(smsSendDelaySeconds);
|
||||
|
||||
// Collect SIM information
|
||||
SimInfoCollectionDTO simInfoCollection = new SimInfoCollectionDTO();
|
||||
simInfoCollection.setLastUpdated(System.currentTimeMillis());
|
||||
simInfoCollection.setSims(TextBeeUtils.collectSimInfo(context));
|
||||
heartbeatInput.setSimInfo(simInfoCollection);
|
||||
|
||||
// Send heartbeat request
|
||||
Call<HeartbeatResponseDTO> call = ApiManager.getApiService().heartbeat(deviceId, apiKey, heartbeatInput);
|
||||
Response<HeartbeatResponseDTO> response = call.execute();
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
HeartbeatResponseDTO responseBody = response.body();
|
||||
if (responseBody.fcmTokenUpdated) {
|
||||
Log.d(TAG, "FCM token was updated during heartbeat");
|
||||
}
|
||||
|
||||
// Sync device name from heartbeat response (ignore if blank)
|
||||
if (responseBody.name != null && !responseBody.name.trim().isEmpty()) {
|
||||
SharedPreferenceHelper.setSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_DEVICE_NAME_KEY,
|
||||
responseBody.name
|
||||
);
|
||||
Log.d(TAG, "Synced device name from heartbeat: " + responseBody.name);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Heartbeat sent successfully");
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send heartbeat. Response code: " + (response.code()));
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Heartbeat API call failed: " + e.getMessage());
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error collecting device information: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if device is eligible to send heartbeat (registered, enabled, heartbeat enabled).
|
||||
*
|
||||
* @param context Application context
|
||||
* @return true if device is eligible, false otherwise
|
||||
*/
|
||||
public static boolean isDeviceEligibleForHeartbeat(Context context) {
|
||||
// Check if device is registered
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_DEVICE_ID_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
if (deviceId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if device is enabled
|
||||
boolean deviceEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
|
||||
if (!deviceEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if heartbeat feature is enabled
|
||||
boolean heartbeatEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_HEARTBEAT_ENABLED_KEY,
|
||||
true // Default to true
|
||||
);
|
||||
|
||||
return heartbeatEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.vernu.sms.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.workers.HeartbeatWorker;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class HeartbeatManager {
|
||||
private static final String TAG = "HeartbeatManager";
|
||||
private static final int MIN_INTERVAL_MINUTES = 15; // Android WorkManager minimum
|
||||
private static final String UNIQUE_WORK_NAME = "heartbeat_unique_work";
|
||||
|
||||
public static void scheduleHeartbeat(Context context) {
|
||||
// Use application context to ensure WorkManager works even when app is closed
|
||||
Context appContext = context.getApplicationContext();
|
||||
|
||||
// Get interval from shared preferences (default 30 minutes)
|
||||
int intervalMinutes = SharedPreferenceHelper.getSharedPreferenceInt(
|
||||
appContext,
|
||||
AppConstants.SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY,
|
||||
30
|
||||
);
|
||||
|
||||
// Enforce minimum interval
|
||||
if (intervalMinutes < MIN_INTERVAL_MINUTES) {
|
||||
Log.w(TAG, "Interval " + intervalMinutes + " minutes is less than minimum " + MIN_INTERVAL_MINUTES + " minutes, using minimum");
|
||||
intervalMinutes = MIN_INTERVAL_MINUTES;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Scheduling heartbeat with interval: " + intervalMinutes + " minutes");
|
||||
|
||||
// Create constraints
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
// Create periodic work request
|
||||
PeriodicWorkRequest heartbeatWork = new PeriodicWorkRequest.Builder(
|
||||
HeartbeatWorker.class,
|
||||
intervalMinutes,
|
||||
TimeUnit.MINUTES
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.addTag(AppConstants.HEARTBEAT_WORK_TAG)
|
||||
.build();
|
||||
|
||||
// Use enqueueUniquePeriodicWork to ensure only one periodic work exists
|
||||
// This ensures the work persists across app restarts and device reboots
|
||||
WorkManager.getInstance(appContext)
|
||||
.enqueueUniquePeriodicWork(
|
||||
UNIQUE_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
heartbeatWork
|
||||
);
|
||||
|
||||
Log.d(TAG, "Heartbeat scheduled successfully with unique work name: " + UNIQUE_WORK_NAME);
|
||||
}
|
||||
|
||||
public static void cancelHeartbeat(Context context) {
|
||||
Log.d(TAG, "Cancelling heartbeat work");
|
||||
Context appContext = context.getApplicationContext();
|
||||
|
||||
// Cancel by unique work name (more reliable)
|
||||
WorkManager.getInstance(appContext)
|
||||
.cancelUniqueWork(UNIQUE_WORK_NAME);
|
||||
|
||||
// Also cancel by tag as fallback
|
||||
WorkManager.getInstance(appContext)
|
||||
.cancelAllWorkByTag(AppConstants.HEARTBEAT_WORK_TAG);
|
||||
}
|
||||
|
||||
public static void triggerHeartbeat(Context context) {
|
||||
// This can be used for testing - trigger immediate heartbeat
|
||||
Log.d(TAG, "Triggering immediate heartbeat");
|
||||
// For immediate execution, we could create a OneTimeWorkRequest
|
||||
// but for now, just reschedule which will run soon
|
||||
scheduleHeartbeat(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.vernu.sms.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.models.SMSFilterRule;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SMSFilterHelper {
|
||||
private static final String TAG = "SMSFilterHelper";
|
||||
|
||||
public enum FilterMode {
|
||||
ALLOW_LIST,
|
||||
BLOCK_LIST
|
||||
}
|
||||
|
||||
public static class FilterConfig {
|
||||
private boolean enabled = false;
|
||||
private FilterMode mode = FilterMode.BLOCK_LIST; // Default to block list
|
||||
private List<SMSFilterRule> rules = new ArrayList<>();
|
||||
|
||||
public FilterConfig() {
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public FilterMode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(FilterMode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public List<SMSFilterRule> getRules() {
|
||||
return rules;
|
||||
}
|
||||
|
||||
public void setRules(List<SMSFilterRule> rules) {
|
||||
this.rules = rules != null ? rules : new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filter configuration from SharedPreferences
|
||||
*/
|
||||
public static FilterConfig loadFilterConfig(Context context) {
|
||||
String json = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_SMS_FILTER_CONFIG_KEY,
|
||||
null
|
||||
);
|
||||
|
||||
if (json == null || json.isEmpty()) {
|
||||
return new FilterConfig();
|
||||
}
|
||||
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
Type type = new TypeToken<FilterConfig>() {}.getType();
|
||||
FilterConfig config = gson.fromJson(json, type);
|
||||
return config != null ? config : new FilterConfig();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error loading filter config: " + e.getMessage());
|
||||
return new FilterConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save filter configuration to SharedPreferences
|
||||
*/
|
||||
public static void saveFilterConfig(Context context, FilterConfig config) {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(config);
|
||||
SharedPreferenceHelper.setSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_SMS_FILTER_CONFIG_KEY,
|
||||
json
|
||||
);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error saving filter config: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an SMS should be processed based on filter configuration
|
||||
* @param sender The sender phone number
|
||||
* @param message The message content
|
||||
* @param context Application context
|
||||
* @return true if SMS should be processed, false if it should be filtered out
|
||||
*/
|
||||
public static boolean shouldProcessSMS(String sender, String message, Context context) {
|
||||
FilterConfig config = loadFilterConfig(context);
|
||||
|
||||
// If filter is disabled, process all SMS
|
||||
if (!config.isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no rules, process all SMS (empty filter doesn't block anything)
|
||||
if (config.getRules() == null || config.getRules().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if sender and/or message matches any rule
|
||||
boolean matchesAnyRule = false;
|
||||
for (SMSFilterRule rule : config.getRules()) {
|
||||
if (rule.matches(sender, message)) {
|
||||
matchesAnyRule = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filter mode
|
||||
if (config.getMode() == FilterMode.ALLOW_LIST) {
|
||||
// Only process if matches a rule
|
||||
return matchesAnyRule;
|
||||
} else {
|
||||
// Block list: process if it does NOT match any rule
|
||||
return !matchesAnyRule;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy method for backward compatibility - checks sender only
|
||||
*/
|
||||
public static boolean shouldProcessSMS(String sender, Context context) {
|
||||
return shouldProcessSMS(sender, null, context);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,213 @@
|
||||
package com.vernu.sms.helpers;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.telephony.SmsManager;
|
||||
import android.os.Build;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
import com.vernu.sms.dtos.SMSDTO;
|
||||
import com.vernu.sms.receivers.SMSStatusReceiver;
|
||||
import com.vernu.sms.workers.SMSStatusUpdateWorker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class SMSHelper {
|
||||
public static void sendSMS(String phoneNo, String message) {
|
||||
SmsManager smsManager = SmsManager.getDefault();
|
||||
private static final String TAG = "SMSHelper";
|
||||
|
||||
/**
|
||||
* Sends an SMS message and returns whether the operation was successful
|
||||
*
|
||||
* @param phoneNo The recipient's phone number
|
||||
* @param message The SMS message to send
|
||||
* @param smsId The unique ID for this SMS
|
||||
* @param smsBatchId The batch ID for this SMS
|
||||
* @param context The application context
|
||||
* @return boolean True if sending was initiated, false if permissions aren't granted
|
||||
*/
|
||||
public static boolean sendSMS(String phoneNo, String message, String smsId, String smsBatchId, Context context) {
|
||||
// Check if we have permission to send SMS
|
||||
if (!TextBeeUtils.isPermissionGranted(context, Manifest.permission.SEND_SMS)) {
|
||||
Log.e(TAG, "SMS permission not granted. Unable to send SMS.");
|
||||
|
||||
// Report failure to API
|
||||
reportPermissionError(context, smsId, smsBatchId);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
SmsManager smsManager = SmsManager.getDefault();
|
||||
|
||||
//for sms with more than 160 chars
|
||||
ArrayList<String> parts = smsManager.divideMessage(message);
|
||||
if (parts.size() > 1) {
|
||||
smsManager.sendMultipartTextMessage(phoneNo, null, parts, null, null);
|
||||
} else {
|
||||
smsManager.sendTextMessage(phoneNo, null, message, null, null);
|
||||
// Create pending intents for status tracking
|
||||
PendingIntent sentIntent = createSentPendingIntent(context, smsId, smsBatchId);
|
||||
PendingIntent deliveredIntent = createDeliveredPendingIntent(context, smsId, smsBatchId);
|
||||
|
||||
// For SMS with more than 160 chars
|
||||
ArrayList<String> parts = smsManager.divideMessage(message);
|
||||
if (parts.size() > 1) {
|
||||
ArrayList<PendingIntent> sentIntents = new ArrayList<>();
|
||||
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
sentIntents.add(sentIntent);
|
||||
deliveredIntents.add(deliveredIntent);
|
||||
}
|
||||
|
||||
smsManager.sendMultipartTextMessage(phoneNo, null, parts, sentIntents, deliveredIntents);
|
||||
} else {
|
||||
smsManager.sendTextMessage(phoneNo, null, message, sentIntent, deliveredIntent);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Exception when sending SMS: " + e.getMessage());
|
||||
|
||||
// Report exception to API
|
||||
reportSendingError(context, smsId, smsBatchId, e.getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an SMS message from a specific SIM slot and returns whether the operation was successful
|
||||
*
|
||||
* @param phoneNo The recipient's phone number
|
||||
* @param message The SMS message to send
|
||||
* @param simSubscriptionId The specific SIM subscription ID to use
|
||||
* @param smsId The unique ID for this SMS
|
||||
* @param smsBatchId The batch ID for this SMS
|
||||
* @param context The application context
|
||||
* @return boolean True if sending was initiated, false if permissions aren't granted
|
||||
*/
|
||||
public static boolean sendSMSFromSpecificSim(String phoneNo, String message, int simSubscriptionId,
|
||||
String smsId, String smsBatchId, Context context) {
|
||||
// Check for required permissions
|
||||
if (!TextBeeUtils.isPermissionGranted(context, Manifest.permission.SEND_SMS) ||
|
||||
!TextBeeUtils.isPermissionGranted(context, Manifest.permission.READ_PHONE_STATE)) {
|
||||
Log.e(TAG, "SMS or Phone State permission not granted. Unable to send SMS from specific SIM.");
|
||||
|
||||
// Report failure to API
|
||||
reportPermissionError(context, smsId, smsBatchId);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the SmsManager for the specific SIM
|
||||
SmsManager smsManager;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
smsManager = SmsManager.getSmsManagerForSubscriptionId(simSubscriptionId);
|
||||
} else {
|
||||
// Fallback to default SmsManager for older Android versions
|
||||
smsManager = SmsManager.getDefault();
|
||||
Log.w(TAG, "Using default SIM as specific SIM selection not supported on this Android version");
|
||||
}
|
||||
|
||||
// Create pending intents for status tracking
|
||||
PendingIntent sentIntent = createSentPendingIntent(context, smsId, smsBatchId);
|
||||
PendingIntent deliveredIntent = createDeliveredPendingIntent(context, smsId, smsBatchId);
|
||||
|
||||
// For SMS with more than 160 chars
|
||||
ArrayList<String> parts = smsManager.divideMessage(message);
|
||||
if (parts.size() > 1) {
|
||||
ArrayList<PendingIntent> sentIntents = new ArrayList<>();
|
||||
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
sentIntents.add(sentIntent);
|
||||
deliveredIntents.add(deliveredIntent);
|
||||
}
|
||||
|
||||
smsManager.sendMultipartTextMessage(phoneNo, null, parts, sentIntents, deliveredIntents);
|
||||
} else {
|
||||
smsManager.sendTextMessage(phoneNo, null, message, sentIntent, deliveredIntent);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Exception when sending SMS from specific SIM: " + e.getMessage());
|
||||
|
||||
// Report exception to API
|
||||
reportSendingError(context, smsId, smsBatchId, e.getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void reportPermissionError(Context context, String smsId, String smsBatchId) {
|
||||
SMSDTO smsDTO = new SMSDTO();
|
||||
smsDTO.setSmsId(smsId);
|
||||
smsDTO.setSmsBatchId(smsBatchId);
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(System.currentTimeMillis());
|
||||
smsDTO.setErrorCode("PERMISSION_DENIED");
|
||||
smsDTO.setErrorMessage("SMS permission not granted");
|
||||
|
||||
updateSMSStatus(context, smsDTO);
|
||||
}
|
||||
|
||||
private static void reportSendingError(Context context, String smsId, String smsBatchId, String errorMessage) {
|
||||
SMSDTO smsDTO = new SMSDTO();
|
||||
smsDTO.setSmsId(smsId);
|
||||
smsDTO.setSmsBatchId(smsBatchId);
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(System.currentTimeMillis());
|
||||
smsDTO.setErrorCode("SENDING_EXCEPTION");
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
|
||||
updateSMSStatus(context, smsDTO);
|
||||
}
|
||||
|
||||
private static void updateSMSStatus(Context context, SMSDTO smsDTO) {
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(context, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, "");
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(context, AppConstants.SHARED_PREFS_API_KEY_KEY, "");
|
||||
|
||||
if (deviceId.isEmpty() || apiKey.isEmpty()) {
|
||||
Log.e(TAG, "Device ID or API key not found");
|
||||
return;
|
||||
}
|
||||
|
||||
SMSStatusUpdateWorker.enqueueWork(context, deviceId, apiKey, smsDTO);
|
||||
}
|
||||
|
||||
public static void sendSMSFromSpecificSim(String phoneNo, String message, int simSlot) {
|
||||
SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(simSlot);
|
||||
smsManager.sendMultipartTextMessage(phoneNo, null, smsManager.divideMessage(message), null, null);
|
||||
|
||||
private static PendingIntent createSentPendingIntent(Context context, String smsId, String smsBatchId) {
|
||||
// Create explicit intent (specify the component)
|
||||
Intent intent = new Intent(context, SMSStatusReceiver.class);
|
||||
intent.setAction(SMSStatusReceiver.SMS_SENT);
|
||||
intent.putExtra("sms_id", smsId);
|
||||
intent.putExtra("sms_batch_id", smsBatchId);
|
||||
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags |= PendingIntent.FLAG_MUTABLE;
|
||||
}
|
||||
|
||||
// Use a unique request code to avoid PendingIntent collisions
|
||||
int requestCode = (smsId + "_sent").hashCode();
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags);
|
||||
}
|
||||
|
||||
private static PendingIntent createDeliveredPendingIntent(Context context, String smsId, String smsBatchId) {
|
||||
// Create explicit intent (specify the component)
|
||||
Intent intent = new Intent(context, SMSStatusReceiver.class);
|
||||
intent.setAction(SMSStatusReceiver.SMS_DELIVERED);
|
||||
intent.putExtra("sms_id", smsId);
|
||||
intent.putExtra("sms_batch_id", smsBatchId);
|
||||
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags |= PendingIntent.FLAG_MUTABLE;
|
||||
}
|
||||
|
||||
// Use a unique request code to avoid PendingIntent collisions
|
||||
int requestCode = (smsId + "_delivered").hashCode();
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.vernu.sms.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.BuildConfig;
|
||||
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class VersionTracker {
|
||||
private static final String TAG = "VersionTracker";
|
||||
|
||||
/**
|
||||
* Checks if the app version has changed since the last time it was run
|
||||
* @param context Application context
|
||||
* @return true if version has changed, false otherwise
|
||||
*/
|
||||
public static boolean hasVersionChanged(Context context) {
|
||||
int lastVersionCode = SharedPreferenceHelper.getSharedPreferenceInt(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_LAST_VERSION_CODE_KEY,
|
||||
-1
|
||||
);
|
||||
|
||||
String lastVersionName = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_LAST_VERSION_NAME_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
int currentVersionCode = BuildConfig.VERSION_CODE;
|
||||
String currentVersionName = BuildConfig.VERSION_NAME;
|
||||
|
||||
// First app launch or version changed
|
||||
return lastVersionCode == -1 ||
|
||||
lastVersionCode != currentVersionCode ||
|
||||
!lastVersionName.equals(currentVersionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the stored version information with current version
|
||||
* @param context Application context
|
||||
*/
|
||||
public static void updateStoredVersion(Context context) {
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_LAST_VERSION_CODE_KEY,
|
||||
BuildConfig.VERSION_CODE
|
||||
);
|
||||
|
||||
SharedPreferenceHelper.setSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_LAST_VERSION_NAME_KEY,
|
||||
BuildConfig.VERSION_NAME
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports current app version to the server
|
||||
* @param context Application context
|
||||
*/
|
||||
public static void reportVersionToServer(Context context) {
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_DEVICE_ID_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_API_KEY_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
// If device is not registered or no API key, can't report version
|
||||
if (deviceId.isEmpty() || apiKey.isEmpty()) {
|
||||
Log.d(TAG, "Can't report version: device not registered or no API key");
|
||||
return;
|
||||
}
|
||||
|
||||
RegisterDeviceInputDTO updateInput = new RegisterDeviceInputDTO();
|
||||
updateInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
updateInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
|
||||
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService()
|
||||
.updateDevice(deviceId, apiKey, updateInput);
|
||||
|
||||
apiCall.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
Log.d(TAG, "Version update reported successfully");
|
||||
updateStoredVersion(context);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to report version update: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<RegisterDeviceResponseDTO> call, Throwable t) {
|
||||
Log.e(TAG, "Error reporting version update: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.vernu.sms.models;
|
||||
|
||||
public class SMSFilterRule {
|
||||
public enum MatchType {
|
||||
EXACT,
|
||||
STARTS_WITH,
|
||||
ENDS_WITH,
|
||||
CONTAINS
|
||||
}
|
||||
|
||||
public enum FilterTarget {
|
||||
SENDER,
|
||||
MESSAGE,
|
||||
BOTH
|
||||
}
|
||||
|
||||
private String pattern;
|
||||
private MatchType matchType;
|
||||
private FilterTarget filterTarget = FilterTarget.SENDER; // Default to sender for backward compatibility
|
||||
private boolean caseSensitive = false; // Default to case insensitive
|
||||
|
||||
public SMSFilterRule() {
|
||||
}
|
||||
|
||||
public SMSFilterRule(String pattern, MatchType matchType) {
|
||||
this.pattern = pattern;
|
||||
this.matchType = matchType;
|
||||
this.filterTarget = FilterTarget.SENDER;
|
||||
this.caseSensitive = false;
|
||||
}
|
||||
|
||||
public SMSFilterRule(String pattern, MatchType matchType, FilterTarget filterTarget) {
|
||||
this.pattern = pattern;
|
||||
this.matchType = matchType;
|
||||
this.filterTarget = filterTarget;
|
||||
this.caseSensitive = false;
|
||||
}
|
||||
|
||||
public SMSFilterRule(String pattern, MatchType matchType, FilterTarget filterTarget, boolean caseSensitive) {
|
||||
this.pattern = pattern;
|
||||
this.matchType = matchType;
|
||||
this.filterTarget = filterTarget;
|
||||
this.caseSensitive = caseSensitive;
|
||||
}
|
||||
|
||||
public String getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
public void setPattern(String pattern) {
|
||||
this.pattern = pattern;
|
||||
}
|
||||
|
||||
public MatchType getMatchType() {
|
||||
return matchType;
|
||||
}
|
||||
|
||||
public void setMatchType(MatchType matchType) {
|
||||
this.matchType = matchType;
|
||||
}
|
||||
|
||||
public FilterTarget getFilterTarget() {
|
||||
return filterTarget;
|
||||
}
|
||||
|
||||
public void setFilterTarget(FilterTarget filterTarget) {
|
||||
this.filterTarget = filterTarget != null ? filterTarget : FilterTarget.SENDER;
|
||||
}
|
||||
|
||||
public boolean isCaseSensitive() {
|
||||
return caseSensitive;
|
||||
}
|
||||
|
||||
public void setCaseSensitive(boolean caseSensitive) {
|
||||
this.caseSensitive = caseSensitive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string matches this filter rule based on match type
|
||||
*/
|
||||
private boolean matchesString(String text) {
|
||||
if (pattern == null || text == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String patternToMatch = pattern;
|
||||
String textToMatch = text;
|
||||
|
||||
// Apply case sensitivity
|
||||
if (!caseSensitive) {
|
||||
patternToMatch = patternToMatch.toLowerCase();
|
||||
textToMatch = textToMatch.toLowerCase();
|
||||
}
|
||||
|
||||
switch (matchType) {
|
||||
case EXACT:
|
||||
return textToMatch.equals(patternToMatch);
|
||||
case STARTS_WITH:
|
||||
return textToMatch.startsWith(patternToMatch);
|
||||
case ENDS_WITH:
|
||||
return textToMatch.endsWith(patternToMatch);
|
||||
case CONTAINS:
|
||||
return textToMatch.contains(patternToMatch);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given sender and/or message matches this filter rule
|
||||
*/
|
||||
public boolean matches(String sender, String message) {
|
||||
if (pattern == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (filterTarget) {
|
||||
case SENDER:
|
||||
return matchesString(sender);
|
||||
case MESSAGE:
|
||||
return matchesString(message);
|
||||
case BOTH:
|
||||
return matchesString(sender) || matchesString(message);
|
||||
default:
|
||||
return matchesString(sender);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy method for backward compatibility - checks sender only
|
||||
*/
|
||||
public boolean matches(String sender) {
|
||||
return matches(sender, null);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ public class SMSPayload {
|
||||
|
||||
private String[] recipients;
|
||||
private String message;
|
||||
private String smsId;
|
||||
private String smsBatchId;
|
||||
private Integer simSubscriptionId;
|
||||
|
||||
// Legacy fields that are no longer used
|
||||
private String[] receivers;
|
||||
@@ -27,4 +30,28 @@ public class SMSPayload {
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getSmsId() {
|
||||
return smsId;
|
||||
}
|
||||
|
||||
public void setSmsId(String smsId) {
|
||||
this.smsId = smsId;
|
||||
}
|
||||
|
||||
public String getSmsBatchId() {
|
||||
return smsBatchId;
|
||||
}
|
||||
|
||||
public void setSmsBatchId(String smsBatchId) {
|
||||
this.smsBatchId = smsBatchId;
|
||||
}
|
||||
|
||||
public Integer getSimSubscriptionId() {
|
||||
return simSubscriptionId;
|
||||
}
|
||||
|
||||
public void setSimSubscriptionId(Integer simSubscriptionId) {
|
||||
this.simSubscriptionId = simSubscriptionId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,120 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.BuildConfig;
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.helpers.HeartbeatManager;
|
||||
import com.vernu.sms.services.StickyNotificationService;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class BootCompletedReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "BootCompletedReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||
// if(TextBeeUtils.isPermissionGranted(context, Manifest.permission.RECEIVE_SMS)){
|
||||
// TextBeeUtils.startStickyNotificationService(context);
|
||||
// }
|
||||
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
|
||||
if(stickyNotificationEnabled && TextBeeUtils.isPermissionGranted(context, Manifest.permission.RECEIVE_SMS)){
|
||||
Log.i(TAG, "Device booted, starting sticky notification service");
|
||||
TextBeeUtils.startStickyNotificationService(context);
|
||||
}
|
||||
|
||||
// Report device info to server if device is registered
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_DEVICE_ID_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_API_KEY_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
// Only proceed if both device ID and API key are available
|
||||
if (!deviceId.isEmpty() && !apiKey.isEmpty()) {
|
||||
updateDeviceInfo(context, deviceId, apiKey);
|
||||
|
||||
// Schedule heartbeat if device is enabled
|
||||
boolean deviceEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
if (deviceEnabled) {
|
||||
Log.i(TAG, "Device booted, scheduling heartbeat");
|
||||
HeartbeatManager.scheduleHeartbeat(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates device information on the server after boot
|
||||
*/
|
||||
private void updateDeviceInfo(Context context, String deviceId, String apiKey) {
|
||||
FirebaseMessaging.getInstance().getToken()
|
||||
.addOnCompleteListener(task -> {
|
||||
if (!task.isSuccessful()) {
|
||||
Log.e(TAG, "Failed to obtain FCM token after boot");
|
||||
return;
|
||||
}
|
||||
|
||||
String token = task.getResult();
|
||||
|
||||
RegisterDeviceInputDTO updateInput = new RegisterDeviceInputDTO();
|
||||
updateInput.setFcmToken(token);
|
||||
updateInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
updateInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
|
||||
Log.d(TAG, "Updating device info after boot - deviceId: " + deviceId);
|
||||
|
||||
ApiManager.getApiService()
|
||||
.updateDevice(deviceId, apiKey, updateInput)
|
||||
.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
Log.d(TAG, "Device info updated successfully after boot");
|
||||
|
||||
// Sync heartbeatIntervalMinutes from server response
|
||||
if (response.body() != null && response.body().data != null) {
|
||||
if (response.body().data.get("heartbeatIntervalMinutes") != null) {
|
||||
Object intervalObj = response.body().data.get("heartbeatIntervalMinutes");
|
||||
if (intervalObj instanceof Number) {
|
||||
int intervalMinutes = ((Number) intervalObj).intValue();
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(context, AppConstants.SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY, intervalMinutes);
|
||||
Log.d(TAG, "Synced heartbeat interval from server: " + intervalMinutes + " minutes");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Failed to update device info after boot. Response code: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<RegisterDeviceResponseDTO> call, Throwable t) {
|
||||
Log.e(TAG, "Error updating device info after boot: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,24 @@ import android.content.Intent;
|
||||
import android.provider.Telephony;
|
||||
import android.telephony.SmsMessage;
|
||||
import android.util.Log;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.dtos.SMSDTO;
|
||||
import com.vernu.sms.dtos.SMSForwardResponseDTO;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.helpers.SMSFilterHelper;
|
||||
import com.vernu.sms.workers.SMSReceivedWorker;
|
||||
|
||||
import java.util.Date;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class SMSBroadcastReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "SMSBroadcastReceiver";
|
||||
// In-memory cache to prevent rapid duplicate processing (5 seconds TTL)
|
||||
private static final ConcurrentHashMap<String, Long> processedFingerprints = new ConcurrentHashMap<>();
|
||||
private static final long CACHE_TTL_MS = 5000; // 5 seconds
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@@ -64,32 +68,38 @@ public class SMSBroadcastReceiver extends BroadcastReceiver {
|
||||
// receivedSMSDTO.setMessage(receivedSMS.getMessage());
|
||||
// receivedSMSDTO.setReceivedAt(receivedSMS.getReceivedAt());
|
||||
|
||||
Call<SMSForwardResponseDTO> apiCall = ApiManager.getApiService().sendReceivedSMS(deviceId, apiKey, receivedSMSDTO);
|
||||
apiCall.enqueue(new retrofit2.Callback<SMSForwardResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<SMSForwardResponseDTO> call, Response<SMSForwardResponseDTO> response) {
|
||||
// Date now = new Date();
|
||||
if (response.isSuccessful()) {
|
||||
Log.d(TAG, "SMS sent to server successfully");
|
||||
// receivedSMS.setLastAcknowledgedRequestAt(now);
|
||||
// receivedSMS.setServerAcknowledgedAt(now);
|
||||
// updateLocalReceivedSMS(receivedSMS, context);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send SMS to server");
|
||||
// receivedSMS.setServerAcknowledgedAt(null);
|
||||
// receivedSMS.setLastAcknowledgedRequestAt(now);
|
||||
// receivedSMS.setRetryCount(localReceivedSMS.getRetryCount() + 1);
|
||||
// updateLocalReceivedSMS(receivedSMS, context);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(Call<SMSForwardResponseDTO> call, Throwable t) {
|
||||
Log.e(TAG, "Failed to send SMS to server", t);
|
||||
// receivedSMS.setServerAcknowledgedAt(null);
|
||||
// receivedSMS.setLastAcknowledgedRequestAt(new Date());
|
||||
// updateLocalReceivedSMS(receivedSMS, context);
|
||||
}
|
||||
});
|
||||
// Apply SMS filter
|
||||
String sender = receivedSMSDTO.getSender();
|
||||
String message = receivedSMSDTO.getMessage();
|
||||
if (sender != null && !SMSFilterHelper.shouldProcessSMS(sender, message, context)) {
|
||||
Log.d(TAG, "SMS from " + sender + " filtered out by filter rules");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate fingerprint for deduplication
|
||||
String fingerprint = generateFingerprint(
|
||||
receivedSMSDTO.getSender(),
|
||||
receivedSMSDTO.getMessage(),
|
||||
receivedSMSDTO.getReceivedAtInMillis()
|
||||
);
|
||||
receivedSMSDTO.setFingerprint(fingerprint);
|
||||
|
||||
// Check in-memory cache to prevent rapid duplicate processing
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long lastProcessedTime = processedFingerprints.get(fingerprint);
|
||||
|
||||
if (lastProcessedTime != null && (currentTime - lastProcessedTime) < CACHE_TTL_MS) {
|
||||
Log.d(TAG, "Duplicate SMS detected in cache, skipping: " + fingerprint);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update cache
|
||||
processedFingerprints.put(fingerprint, currentTime);
|
||||
|
||||
// Clean up old cache entries periodically
|
||||
cleanupCache(currentTime);
|
||||
|
||||
SMSReceivedWorker.enqueueWork(context, deviceId, apiKey, receivedSMSDTO);
|
||||
}
|
||||
|
||||
// private void updateLocalReceivedSMS(SMS localReceivedSMS, Context context) {
|
||||
@@ -98,4 +108,50 @@ public class SMSBroadcastReceiver extends BroadcastReceiver {
|
||||
// appDatabase.localReceivedSMSDao().insertAll(localReceivedSMS);
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Generate a unique fingerprint for an SMS message based on sender, message content, and timestamp
|
||||
*/
|
||||
private String generateFingerprint(String sender, String message, long timestamp) {
|
||||
try {
|
||||
String data = (sender != null ? sender : "") + "|" +
|
||||
(message != null ? message : "") + "|" +
|
||||
timestamp;
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] hashBytes = md.digest(data.getBytes("UTF-8"));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error generating fingerprint: " + e.getMessage());
|
||||
// Fallback to simple string concatenation if MD5 fails
|
||||
return (sender != null ? sender : "") + "_" +
|
||||
(message != null ? message : "") + "_" +
|
||||
timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old cache entries to prevent memory leaks
|
||||
*/
|
||||
private void cleanupCache(long currentTime) {
|
||||
// Only cleanup occasionally (every 100 entries processed)
|
||||
if (processedFingerprints.size() > 100) {
|
||||
Set<String> keysToRemove = new HashSet<>();
|
||||
for (String key : processedFingerprints.keySet()) {
|
||||
Long timestamp = processedFingerprints.get(key);
|
||||
if (timestamp != null && (currentTime - timestamp) > CACHE_TTL_MS) {
|
||||
keysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
for (String key : keysToRemove) {
|
||||
processedFingerprints.remove(key);
|
||||
}
|
||||
Log.d(TAG, "Cleaned up " + keysToRemove.size() + " expired cache entries");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.vernu.sms.receivers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.telephony.SmsManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.dtos.SMSDTO;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.workers.SMSStatusUpdateWorker;
|
||||
|
||||
|
||||
public class SMSStatusReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "SMSStatusReceiver";
|
||||
|
||||
public static final String SMS_SENT = "SMS_SENT";
|
||||
public static final String SMS_DELIVERED = "SMS_DELIVERED";
|
||||
|
||||
/**
|
||||
* Resolves a result code to the constant name (e.g. SmsManager.RESULT_ERROR_GENERIC_FAILURE)
|
||||
* via reflection. Returns null if no matching constant is found.
|
||||
*/
|
||||
private static String getResultCodeName(int resultCode) {
|
||||
for (Class<?> clazz : new Class<?>[]{ SmsManager.class, Activity.class }) {
|
||||
try {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.getType() != int.class) continue;
|
||||
if (!Modifier.isStatic(field.getModifiers()) || !Modifier.isFinal(field.getModifiers())) continue;
|
||||
if (!field.getName().startsWith("RESULT_")) continue;
|
||||
field.setAccessible(true);
|
||||
if (field.getInt(null) == resultCode) {
|
||||
return clazz.getSimpleName() + "." + field.getName();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Reflection failed for " + clazz.getSimpleName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String smsId = intent.getStringExtra("sms_id");
|
||||
String smsBatchId = intent.getStringExtra("sms_batch_id");
|
||||
String action = intent.getAction();
|
||||
|
||||
SMSDTO smsDTO = new SMSDTO();
|
||||
smsDTO.setSmsId(smsId);
|
||||
smsDTO.setSmsBatchId(smsBatchId);
|
||||
|
||||
if (SMS_SENT.equals(action)) {
|
||||
handleSentStatus(context, intent, getResultCode(), smsDTO);
|
||||
} else if (SMS_DELIVERED.equals(action)) {
|
||||
handleDeliveredStatus(context, getResultCode(), smsDTO);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSentStatus(Context context, Intent intent, int resultCode, SMSDTO smsDTO) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String errorMessage = "";
|
||||
|
||||
switch (resultCode) {
|
||||
case Activity.RESULT_OK:
|
||||
smsDTO.setStatus("SENT");
|
||||
smsDTO.setSentAtInMillis(timestamp);
|
||||
Log.d(TAG, "SMS sent successfully - ID: " + smsDTO.getSmsId());
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
|
||||
errorMessage = "SMS failed on device. Common causes: no SMS credit on SIM, weak signal, or carrier blocked. Check SIM balance and signal, then try again.";
|
||||
int radioCode = intent.getIntExtra("errorCode", -1);
|
||||
if (radioCode != -1) {
|
||||
errorMessage += " (code " + radioCode + ")";
|
||||
}
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_RADIO_OFF:
|
||||
errorMessage = "Mobile radio is off (e.g. airplane mode). Turn off airplane mode and ensure cellular is on.";
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_NULL_PDU:
|
||||
errorMessage = "Message could not be sent; invalid format or carrier issue. Try a shorter message or different recipient.";
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_NO_SERVICE:
|
||||
errorMessage = "No cellular service. Check signal and try again when you have coverage.";
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_LIMIT_EXCEEDED:
|
||||
errorMessage = "Device/carrier send limit reached (too many SMS in a short time). Wait a few minutes or lower the send rate.";
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_SHORT_CODE_NOT_ALLOWED:
|
||||
errorMessage = "Short code not allowed on this carrier. Use a full phone number.";
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED:
|
||||
errorMessage = "Short codes are not supported on this carrier. Use a full phone number.";
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
case SmsManager.RESULT_NETWORK_ERROR:
|
||||
errorMessage = "Network error while sending. Check signal and try again.";
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
default:
|
||||
String codeName = getResultCodeName(resultCode);
|
||||
errorMessage = codeName != null ? codeName : ("Unknown error (code " + resultCode + ")");
|
||||
smsDTO.setStatus("FAILED");
|
||||
smsDTO.setFailedAtInMillis(timestamp);
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error: " + errorMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
updateSMSStatus(context, smsDTO);
|
||||
}
|
||||
|
||||
private void handleDeliveredStatus(Context context, int resultCode, SMSDTO smsDTO) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String errorMessage = "";
|
||||
|
||||
switch (resultCode) {
|
||||
case Activity.RESULT_OK:
|
||||
smsDTO.setStatus("DELIVERED");
|
||||
smsDTO.setDeliveredAtInMillis(timestamp);
|
||||
Log.d(TAG, "SMS delivered successfully - ID: " + smsDTO.getSmsId());
|
||||
break;
|
||||
case Activity.RESULT_CANCELED:
|
||||
errorMessage = "Delivery report was canceled (e.g. carrier does not support delivery receipts). Message may still have been delivered.";
|
||||
smsDTO.setStatus("DELIVERY_FAILED");
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS delivery failed - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
|
||||
break;
|
||||
default:
|
||||
String deliveryCodeName = getResultCodeName(resultCode);
|
||||
errorMessage = deliveryCodeName != null ? deliveryCodeName : ("Unknown delivery error (code " + resultCode + ")");
|
||||
smsDTO.setStatus("DELIVERY_FAILED");
|
||||
smsDTO.setErrorCode(String.valueOf(resultCode));
|
||||
smsDTO.setErrorMessage(errorMessage);
|
||||
Log.e(TAG, "SMS delivery failed - ID: " + smsDTO.getSmsId() + ", Error: " + errorMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
updateSMSStatus(context, smsDTO);
|
||||
}
|
||||
|
||||
private void updateSMSStatus(Context context, SMSDTO smsDTO) {
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(context, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, "");
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(context, AppConstants.SHARED_PREFS_API_KEY_KEY, "");
|
||||
|
||||
if (deviceId.isEmpty() || apiKey.isEmpty()) {
|
||||
Log.e(TAG, "Device ID or API key not found");
|
||||
return;
|
||||
}
|
||||
|
||||
SMSStatusUpdateWorker.enqueueWork(context, deviceId, apiKey, smsDTO);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,17 @@ import com.google.gson.Gson;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.R;
|
||||
import com.vernu.sms.activities.MainActivity;
|
||||
import com.vernu.sms.helpers.SMSHelper;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.helpers.HeartbeatHelper;
|
||||
import com.vernu.sms.helpers.HeartbeatManager;
|
||||
import com.vernu.sms.models.SMSPayload;
|
||||
import com.vernu.sms.workers.SmsSendWorker;
|
||||
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class FCMService extends FirebaseMessagingService {
|
||||
|
||||
@@ -27,33 +35,98 @@ public class FCMService extends FirebaseMessagingService {
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||
|
||||
Log.d(TAG, remoteMessage.getData().toString());
|
||||
|
||||
Gson gson = new Gson();
|
||||
SMSPayload smsPayload = gson.fromJson(remoteMessage.getData().get("smsData"), SMSPayload.class);
|
||||
|
||||
// Check if message contains a data payload.
|
||||
if (remoteMessage.getData().size() > 0) {
|
||||
int preferedSim = SharedPreferenceHelper.getSharedPreferenceInt(this, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY, -1);
|
||||
for (String receiver : smsPayload.getRecipients()) {
|
||||
if(preferedSim == -1) {
|
||||
SMSHelper.sendSMS(receiver, smsPayload.getMessage());
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
SMSHelper.sendSMSFromSpecificSim(receiver, smsPayload.getMessage(), preferedSim);
|
||||
} catch(Exception e) {
|
||||
Log.d("SMS_SEND_ERROR", e.getMessage());
|
||||
}
|
||||
try {
|
||||
// Check message type first
|
||||
String messageType = remoteMessage.getData().get("type");
|
||||
|
||||
if ("heartbeat_check".equals(messageType)) {
|
||||
// Handle heartbeat check request from backend
|
||||
handleHeartbeatCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse SMS payload data (legacy handling)
|
||||
Gson gson = new Gson();
|
||||
SMSPayload smsPayload = gson.fromJson(remoteMessage.getData().get("smsData"), SMSPayload.class);
|
||||
|
||||
// Check if message contains a data payload
|
||||
if (remoteMessage.getData().size() > 0) {
|
||||
sendSMS(smsPayload);
|
||||
}
|
||||
|
||||
// Handle any notification message
|
||||
if (remoteMessage.getNotification() != null) {
|
||||
// sendNotification("notif msg", "msg body");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error processing FCM message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle heartbeat check request from backend
|
||||
*/
|
||||
private void handleHeartbeatCheck() {
|
||||
Log.d(TAG, "Received heartbeat check request from backend");
|
||||
|
||||
// Check if device is eligible for heartbeat
|
||||
if (!HeartbeatHelper.isDeviceEligibleForHeartbeat(this)) {
|
||||
Log.d(TAG, "Device not eligible for heartbeat, skipping heartbeat check");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Handle FCM Notification Messages
|
||||
if (remoteMessage.getNotification() != null) {
|
||||
// sendNotification("notif msg", "msg body");
|
||||
// Get device ID and API key
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
this,
|
||||
AppConstants.SHARED_PREFS_DEVICE_ID_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
this,
|
||||
AppConstants.SHARED_PREFS_API_KEY_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
// Send heartbeat using shared helper
|
||||
boolean success = HeartbeatHelper.sendHeartbeat(this, deviceId, apiKey);
|
||||
|
||||
if (success) {
|
||||
Log.d(TAG, "Heartbeat sent successfully in response to backend check");
|
||||
// Ensure scheduled work is added if missing
|
||||
HeartbeatManager.scheduleHeartbeat(this);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send heartbeat in response to backend check");
|
||||
// Still try to ensure scheduled work is added
|
||||
HeartbeatManager.scheduleHeartbeat(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue SMS to recipients via the device-side send queue.
|
||||
* SIM resolution and rate limiting are handled by SmsSendWorker.
|
||||
*/
|
||||
private void sendSMS(SMSPayload smsPayload) {
|
||||
if (smsPayload == null) {
|
||||
Log.e(TAG, "SMS payload is null");
|
||||
return;
|
||||
}
|
||||
|
||||
String[] recipients = smsPayload.getRecipients();
|
||||
if (recipients == null || recipients.length == 0) {
|
||||
Log.e(TAG, "No recipients found in SMS payload");
|
||||
return;
|
||||
}
|
||||
|
||||
for (String recipient : recipients) {
|
||||
SmsSendWorker.enqueue(this, recipient, smsPayload.getMessage(),
|
||||
smsPayload.getSmsId(), smsPayload.getSmsBatchId(),
|
||||
smsPayload.getSimSubscriptionId());
|
||||
}
|
||||
|
||||
Log.d(TAG, "Enqueued " + recipients.length + " SMS for sending - Batch: " + smsPayload.getSmsBatchId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,7 +135,39 @@ public class FCMService extends FirebaseMessagingService {
|
||||
}
|
||||
|
||||
private void sendRegistrationToServer(String token) {
|
||||
|
||||
// Check if device ID and API key are saved in shared preferences
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(this, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, "");
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(this, AppConstants.SHARED_PREFS_API_KEY_KEY, "");
|
||||
|
||||
// Only proceed if both device ID and API key are available
|
||||
if (deviceId.isEmpty() || apiKey.isEmpty()) {
|
||||
Log.d(TAG, "Device ID or API key not available, skipping FCM token update");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create update payload with new FCM token
|
||||
RegisterDeviceInputDTO updateInput = new RegisterDeviceInputDTO();
|
||||
updateInput.setFcmToken(token);
|
||||
|
||||
// Call API to update the device with new token
|
||||
Log.d(TAG, "Updating FCM token for device: " + deviceId);
|
||||
ApiManager.getApiService()
|
||||
.updateDevice(deviceId, apiKey, updateInput)
|
||||
.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
Log.d(TAG, "FCM token updated successfully");
|
||||
} else {
|
||||
Log.e(TAG, "Failed to update FCM token. Response code: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<RegisterDeviceResponseDTO> call, Throwable t) {
|
||||
Log.e(TAG, "Error updating FCM token: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* build and show notification */
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.vernu.sms.dtos.SMSDTO;
|
||||
import com.vernu.sms.dtos.SMSForwardResponseDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
|
||||
import com.vernu.sms.dtos.HeartbeatInputDTO;
|
||||
import com.vernu.sms.dtos.HeartbeatResponseDTO;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
@@ -21,4 +23,10 @@ public interface GatewayApiService {
|
||||
|
||||
@POST("gateway/devices/{deviceId}/receive-sms")
|
||||
Call<SMSForwardResponseDTO> sendReceivedSMS(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() SMSDTO body);
|
||||
|
||||
@PATCH("gateway/devices/{deviceId}/sms-status")
|
||||
Call<SMSForwardResponseDTO> updateSMSStatus(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() SMSDTO body);
|
||||
|
||||
@POST("gateway/devices/{deviceId}/heartbeat")
|
||||
Call<HeartbeatResponseDTO> heartbeat(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() HeartbeatInputDTO body);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vernu.sms.services;
|
||||
|
||||
import android.app.ForegroundServiceStartNotAllowedException;
|
||||
import android.app.*;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@@ -15,11 +16,12 @@ import androidx.core.app.NotificationCompat;
|
||||
import com.vernu.sms.R;
|
||||
import com.vernu.sms.activities.MainActivity;
|
||||
import com.vernu.sms.receivers.SMSBroadcastReceiver;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
|
||||
public class StickyNotificationService extends Service {
|
||||
|
||||
private static final String TAG = "StickyNotificationService";
|
||||
private final BroadcastReceiver receiver = new SMSBroadcastReceiver();
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
@@ -32,15 +34,26 @@ public class StickyNotificationService extends Service {
|
||||
super.onCreate();
|
||||
Log.i(TAG, "Service Started");
|
||||
|
||||
// Only show notification if enabled in preferences
|
||||
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
getApplicationContext(),
|
||||
AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
|
||||
// IntentFilter filter = new IntentFilter();
|
||||
// filter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
|
||||
// filter.addAction(android.telephony.TelephonyManager.ACTION_PHONE_STATE_CHANGED);
|
||||
// registerReceiver(receiver, filter);
|
||||
//
|
||||
// Notification notification = createNotification();
|
||||
// startForeground(1, notification);
|
||||
|
||||
if (stickyNotificationEnabled) {
|
||||
Notification notification = createNotification();
|
||||
try {
|
||||
startForeground(1, notification);
|
||||
Log.i(TAG, "Started foreground service with sticky notification");
|
||||
} catch (ForegroundServiceStartNotAllowedException e) {
|
||||
Log.w(TAG, "Cannot start foreground from background, stopping service: " + e.getMessage());
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Sticky notification disabled by user preference");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,9 +65,8 @@ public class StickyNotificationService extends Service {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
// unregisterReceiver(receiver);
|
||||
|
||||
Log.i(TAG, "StickyNotificationService destroyed");
|
||||
// Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private Notification createNotification() {
|
||||
@@ -72,10 +84,19 @@ public class StickyNotificationService extends Service {
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
Notification.Builder builder = new Notification.Builder(this, notificationChannelId);
|
||||
return builder.setContentTitle("TextBee is running").setContentText("TextBee is running in the background.").setContentIntent(pendingIntent).setOngoing(true).setSmallIcon(R.drawable.ic_launcher_foreground).build();
|
||||
return builder.setContentTitle("TextBee Active")
|
||||
.setContentText("SMS gateway service is active")
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.build();
|
||||
} else {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationChannelId);
|
||||
return builder.setContentTitle("TextBee is running").setContentText("TextBee is running in the background.").setOngoing(true).setSmallIcon(R.drawable.ic_launcher_foreground).build();
|
||||
return builder.setContentTitle("TextBee Active")
|
||||
.setContentText("SMS gateway service is active")
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.vernu.sms.workers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.helpers.HeartbeatHelper;
|
||||
|
||||
public class HeartbeatWorker extends Worker {
|
||||
private static final String TAG = "HeartbeatWorker";
|
||||
|
||||
public HeartbeatWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Check if device is eligible for heartbeat
|
||||
if (!HeartbeatHelper.isDeviceEligibleForHeartbeat(context)) {
|
||||
Log.d(TAG, "Device not eligible for heartbeat, skipping");
|
||||
return Result.success(); // Not a failure, just skip
|
||||
}
|
||||
|
||||
// Get device ID and API key
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_DEVICE_ID_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_API_KEY_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
// Send heartbeat using shared helper
|
||||
boolean success = HeartbeatHelper.sendHeartbeat(context, deviceId, apiKey);
|
||||
|
||||
if (success) {
|
||||
return Result.success();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send heartbeat, will retry");
|
||||
return Result.retry();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.vernu.sms.workers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.dtos.SMSDTO;
|
||||
import com.vernu.sms.dtos.SMSForwardResponseDTO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class SMSReceivedWorker extends Worker {
|
||||
private static final String TAG = "SMSReceivedWorker";
|
||||
private static final int MAX_RETRIES = 5;
|
||||
|
||||
public static final String KEY_DEVICE_ID = "device_id";
|
||||
public static final String KEY_API_KEY = "api_key";
|
||||
public static final String KEY_SMS_DTO = "sms_dto";
|
||||
public static final String KEY_RETRY_COUNT = "retry_count";
|
||||
|
||||
public SMSReceivedWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
String deviceId = getInputData().getString(KEY_DEVICE_ID);
|
||||
String apiKey = getInputData().getString(KEY_API_KEY);
|
||||
String smsDtoJson = getInputData().getString(KEY_SMS_DTO);
|
||||
int retryCount = getInputData().getInt(KEY_RETRY_COUNT, 0);
|
||||
|
||||
if (deviceId == null || apiKey == null || smsDtoJson == null) {
|
||||
Log.e(TAG, "Missing required parameters");
|
||||
return Result.failure();
|
||||
}
|
||||
|
||||
// Check if we've exceeded the maximum retry count
|
||||
if (retryCount >= MAX_RETRIES) {
|
||||
Log.e(TAG, "Maximum retry count reached for received SMS");
|
||||
return Result.failure();
|
||||
}
|
||||
|
||||
SMSDTO smsDTO = new Gson().fromJson(smsDtoJson, SMSDTO.class);
|
||||
|
||||
try {
|
||||
Call<SMSForwardResponseDTO> call = ApiManager.getApiService().sendReceivedSMS(deviceId, apiKey, smsDTO);
|
||||
Response<SMSForwardResponseDTO> response = call.execute();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
Log.d(TAG, "Received SMS sent to server successfully");
|
||||
return Result.success();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send received SMS to server. Response code: " + response.code());
|
||||
return Result.retry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "API call failed: " + e.getMessage());
|
||||
return Result.retry();
|
||||
}
|
||||
}
|
||||
|
||||
public static void enqueueWork(Context context, String deviceId, String apiKey, SMSDTO smsDTO) {
|
||||
Data inputData = new Data.Builder()
|
||||
.putString(KEY_DEVICE_ID, deviceId)
|
||||
.putString(KEY_API_KEY, apiKey)
|
||||
.putString(KEY_SMS_DTO, new Gson().toJson(smsDTO))
|
||||
.putInt(KEY_RETRY_COUNT, 0)
|
||||
.build();
|
||||
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(SMSReceivedWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
|
||||
.setInputData(inputData)
|
||||
.addTag("sms_received")
|
||||
.build();
|
||||
|
||||
// Use fingerprint for unique work name if available, otherwise fallback to timestamp
|
||||
String uniqueWorkName;
|
||||
if (smsDTO.getFingerprint() != null && !smsDTO.getFingerprint().isEmpty()) {
|
||||
uniqueWorkName = "sms_received_" + smsDTO.getFingerprint();
|
||||
} else {
|
||||
// Fallback to timestamp if fingerprint is not available
|
||||
uniqueWorkName = "sms_received_" + System.currentTimeMillis();
|
||||
Log.w(TAG, "Fingerprint not available, using timestamp for work name");
|
||||
}
|
||||
|
||||
WorkManager.getInstance(context)
|
||||
.beginUniqueWork(uniqueWorkName,
|
||||
androidx.work.ExistingWorkPolicy.KEEP,
|
||||
workRequest)
|
||||
.enqueue();
|
||||
|
||||
Log.d(TAG, "Work enqueued for received SMS from: " + smsDTO.getSender() + " with fingerprint: " + uniqueWorkName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.vernu.sms.workers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.dtos.SMSDTO;
|
||||
import com.vernu.sms.dtos.SMSForwardResponseDTO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class SMSStatusUpdateWorker extends Worker {
|
||||
private static final String TAG = "SMSStatusUpdateWorker";
|
||||
private static final int MAX_RETRIES = 5;
|
||||
|
||||
public static final String KEY_DEVICE_ID = "device_id";
|
||||
public static final String KEY_API_KEY = "api_key";
|
||||
public static final String KEY_SMS_DTO = "sms_dto";
|
||||
public static final String KEY_RETRY_COUNT = "retry_count";
|
||||
|
||||
public SMSStatusUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
String deviceId = getInputData().getString(KEY_DEVICE_ID);
|
||||
String apiKey = getInputData().getString(KEY_API_KEY);
|
||||
String smsDtoJson = getInputData().getString(KEY_SMS_DTO);
|
||||
int retryCount = getInputData().getInt(KEY_RETRY_COUNT, 0);
|
||||
|
||||
if (deviceId == null || apiKey == null || smsDtoJson == null) {
|
||||
Log.e(TAG, "Missing required parameters");
|
||||
return Result.failure();
|
||||
}
|
||||
|
||||
// Check if we've exceeded the maximum retry count
|
||||
if (retryCount >= MAX_RETRIES) {
|
||||
Log.e(TAG, "Maximum retry count reached for SMS status update");
|
||||
return Result.failure();
|
||||
}
|
||||
|
||||
SMSDTO smsDTO = new Gson().fromJson(smsDtoJson, SMSDTO.class);
|
||||
|
||||
try {
|
||||
Call<SMSForwardResponseDTO> call = ApiManager.getApiService().updateSMSStatus(deviceId, apiKey, smsDTO);
|
||||
Response<SMSForwardResponseDTO> response = call.execute();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
Log.d(TAG, "SMS status updated successfully - ID: " + smsDTO.getSmsId() + ", Status: " + smsDTO.getStatus());
|
||||
return Result.success();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to update SMS status. Response code: " + response.code());
|
||||
return Result.retry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "API call failed: " + e.getMessage());
|
||||
return Result.retry();
|
||||
}
|
||||
}
|
||||
|
||||
public static void enqueueWork(Context context, String deviceId, String apiKey, SMSDTO smsDTO) {
|
||||
Data inputData = new Data.Builder()
|
||||
.putString(KEY_DEVICE_ID, deviceId)
|
||||
.putString(KEY_API_KEY, apiKey)
|
||||
.putString(KEY_SMS_DTO, new Gson().toJson(smsDTO))
|
||||
.putInt(KEY_RETRY_COUNT, 0)
|
||||
.build();
|
||||
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(SMSStatusUpdateWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
|
||||
.setInputData(inputData)
|
||||
.build();
|
||||
|
||||
String uniqueWorkName = "sms_status_" + smsDTO.getStatus() + "_" + System.currentTimeMillis();
|
||||
WorkManager.getInstance(context)
|
||||
.beginUniqueWork(uniqueWorkName,
|
||||
androidx.work.ExistingWorkPolicy.REPLACE,
|
||||
workRequest)
|
||||
.enqueue();
|
||||
|
||||
Log.d(TAG, "Work enqueued for SMS status update - ID: " + smsDTO.getSmsId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.vernu.sms.workers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
import com.vernu.sms.helpers.SMSHelper;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
|
||||
public class SmsSendWorker extends Worker {
|
||||
private static final String TAG = "SmsSendWorker";
|
||||
private static final String QUEUE_NAME = "sms_send_queue";
|
||||
|
||||
public static final String KEY_PHONE = "phone";
|
||||
public static final String KEY_MESSAGE = "message";
|
||||
public static final String KEY_SMS_ID = "sms_id";
|
||||
public static final String KEY_SMS_BATCH_ID = "sms_batch_id";
|
||||
public static final String KEY_SIM_SUBSCRIPTION_ID = "sim_subscription_id";
|
||||
|
||||
public SmsSendWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
String phone = getInputData().getString(KEY_PHONE);
|
||||
String message = getInputData().getString(KEY_MESSAGE);
|
||||
String smsId = getInputData().getString(KEY_SMS_ID);
|
||||
String smsBatchId = getInputData().getString(KEY_SMS_BATCH_ID);
|
||||
int simSubscriptionId = getInputData().getInt(KEY_SIM_SUBSCRIPTION_ID, -1);
|
||||
|
||||
if (phone == null || message == null || smsId == null) {
|
||||
Log.e(TAG, "Missing required parameters");
|
||||
return Result.failure();
|
||||
}
|
||||
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Resolve SIM: backend-provided > app preference > device default
|
||||
Integer resolvedSim = resolveSim(context, simSubscriptionId);
|
||||
|
||||
if (resolvedSim != null) {
|
||||
SMSHelper.sendSMSFromSpecificSim(phone, message, resolvedSim, smsId, smsBatchId, context);
|
||||
} else {
|
||||
SMSHelper.sendSMS(phone, message, smsId, smsBatchId, context);
|
||||
}
|
||||
|
||||
// Enforce rate limit delay
|
||||
int delaySeconds = SharedPreferenceHelper.getSharedPreferenceInt(
|
||||
context, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS);
|
||||
delaySeconds = Math.max(0, Math.min(delaySeconds, 3600));
|
||||
|
||||
if (delaySeconds > 0) {
|
||||
try {
|
||||
Thread.sleep(delaySeconds * 1000L);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private Integer resolveSim(Context context, int backendSimId) {
|
||||
// Priority 1: backend-provided SIM
|
||||
if (backendSimId != -1 && TextBeeUtils.isValidSubscriptionId(context, backendSimId)) {
|
||||
Log.d(TAG, "Using backend-provided SIM subscription ID: " + backendSimId);
|
||||
return backendSimId;
|
||||
}
|
||||
|
||||
// Priority 2: app preference
|
||||
int preferredSim = SharedPreferenceHelper.getSharedPreferenceInt(
|
||||
context, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY, -1);
|
||||
if (preferredSim != -1 && TextBeeUtils.isValidSubscriptionId(context, preferredSim)) {
|
||||
Log.d(TAG, "Using app-preferred SIM subscription ID: " + preferredSim);
|
||||
return preferredSim;
|
||||
}
|
||||
|
||||
// Priority 3: device default
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void enqueue(Context context, String phone, String message,
|
||||
String smsId, String smsBatchId, Integer simSubscriptionId) {
|
||||
Data inputData = new Data.Builder()
|
||||
.putString(KEY_PHONE, phone)
|
||||
.putString(KEY_MESSAGE, message)
|
||||
.putString(KEY_SMS_ID, smsId)
|
||||
.putString(KEY_SMS_BATCH_ID, smsBatchId)
|
||||
.putInt(KEY_SIM_SUBSCRIPTION_ID, simSubscriptionId != null ? simSubscriptionId : -1)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(SmsSendWorker.class)
|
||||
.setInputData(inputData)
|
||||
.build();
|
||||
|
||||
WorkManager.getInstance(context)
|
||||
.beginUniqueWork(QUEUE_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
||||
.enqueue();
|
||||
|
||||
Log.d(TAG, "SMS enqueued for sending - ID: " + smsId + ", Phone: " + phone);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorPrimary" android:state_enabled="true"/>
|
||||
<item android:color="?attr/colorPrimary" android:alpha="0.12"/>
|
||||
</selector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/white" android:state_enabled="true"/>
|
||||
<item android:color="@color/white" android:alpha="0.38"/>
|
||||
</selector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="?attr/colorPrimary" />
|
||||
<item android:state_enabled="false" android:color="@color/text_secondary" />
|
||||
<item android:color="@color/text_primary" />
|
||||
</selector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="?attr/colorPrimary" />
|
||||
<item android:state_enabled="false" android:color="@color/text_secondary" />
|
||||
<item android:color="@color/text_secondary" />
|
||||
</selector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
||||
</vector>
|
||||
@@ -1,164 +1,148 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background_primary"
|
||||
tools:context=".activities.MainActivity">
|
||||
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView2"
|
||||
<!-- Sticky Header Section -->
|
||||
<LinearLayout
|
||||
android:id="@+id/stickyHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Header Section -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Enter your API key or scan the QR code below to get started"
|
||||
android:textSize="20dp"
|
||||
android:layout_margin="5dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
android:background="?attr/colorPrimary"
|
||||
android:orientation="vertical"
|
||||
android:padding="28dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#ccccccee"
|
||||
android:layout_margin="5dp"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:contentDescription="App Icon" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:padding="5dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/apiKeyEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:hint="API Key"
|
||||
android:inputType="text"
|
||||
android:minHeight="48dp"
|
||||
android:textIsSelectable="true" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/fcmTokenEditText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:enabled="false"
|
||||
android:gravity="start|top"
|
||||
android:hint="FCM Token"
|
||||
android:inputType="textMultiLine"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/registerDeviceBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Register" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/scanQRButton"
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:drawableTop="@drawable/ic_baseline_qr_code_24"
|
||||
android:text="Scan"
|
||||
android:textColor="@color/black"
|
||||
android:theme="@style/Theme.Design.Light" />
|
||||
android:text="textbee.dev - sms gateway"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.02"
|
||||
android:shadowColor="#80000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="2"
|
||||
android:shadowRadius="4" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Streamline your SMS workflows"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13sp"
|
||||
android:letterSpacing="0.01"
|
||||
android:alpha="0.95" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Device Info Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="-24dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/background_secondary"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="6dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_margin="5dp"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_gravity="center"
|
||||
android:padding="6dp"
|
||||
android:src="@drawable/ic_baseline_phone_android_24"
|
||||
android:tint="?attr/colorPrimary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="5dp"
|
||||
android:src="@drawable/ic_baseline_phone_android_24" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
||||
android:id="@+id/deviceBrandAndModelTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Device Brand, Model"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical">
|
||||
android:text="Device ID:"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Device ID"
|
||||
android:textStyle="italic" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deviceIdTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="ae5ce05c05cde3" />
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/deviceIdTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:text="ae5ce05c05cde3"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<ImageButton
|
||||
|
||||
android:id="@+id/copyDeviceIdImgBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:src="@drawable/ic_baseline_content_copy_24" />
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/ic_baseline_content_copy_24"
|
||||
android:tint="?attr/colorPrimary" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@@ -172,132 +156,646 @@
|
||||
android:id="@+id/gatewaySwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:minHeight="32dp"
|
||||
android:text="" />
|
||||
android:minHeight="32dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Status"
|
||||
android:textSize="16dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
<!-- Scrollable Content -->
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/stickyHeader">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- API Key Registration Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="10px">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Configuration"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="#000000" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/grantSMSPermissionBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Grant Permissions"
|
||||
android:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TextBee will only work if you grant SMS Permissions"
|
||||
android:textSize="14dp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#000000" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/receiveSMSSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:minHeight="32dp"
|
||||
android:text="Receive SMS" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Toggle this if you want to receive SMS"
|
||||
android:textSize="14dp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#000000" />
|
||||
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/background_secondary"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="18dp"
|
||||
android:text="Account Information"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="19sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.01" />
|
||||
|
||||
<!-- Device ID Input Field -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="Device ID (optional)"
|
||||
app:boxBackgroundColor="@android:color/transparent"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
app:hintTextColor="?attr/colorPrimary"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@drawable/ic_baseline_edit_24"
|
||||
app:endIconTint="?attr/colorPrimary"
|
||||
app:helperText="Leaving this field blank will register your device as new. If you have already registered this device previously, please enter the id here."
|
||||
app:helperTextTextColor="@color/text_secondary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/deviceIdEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textIsSelectable="true" />
|
||||
</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="16dp"
|
||||
android:hint="API Key"
|
||||
app:boxBackgroundColor="@android:color/transparent"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
app:hintTextColor="?attr/colorPrimary"
|
||||
app:endIconMode="password_toggle"
|
||||
app:endIconTint="?attr/colorPrimary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/apiKeyEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textIsSelectable="true" />
|
||||
</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="16dp"
|
||||
android:hint="Device name (optional)"
|
||||
app:boxBackgroundColor="@android:color/transparent"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
app:hintTextColor="?attr/colorPrimary"
|
||||
app:endIconTint="?attr/colorPrimary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/deviceNameEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textIsSelectable="true" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/fcmTokenEditText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:enabled="false"
|
||||
android:gravity="start|top"
|
||||
android:hint="FCM Token"
|
||||
android:inputType="textMultiLine"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_secondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/registerDeviceBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/button_background_tint"
|
||||
android:paddingHorizontal="28dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:text="Connect"
|
||||
android:textColor="@color/button_text_color"
|
||||
android:textSize="15sp"
|
||||
android:letterSpacing="0.01"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
app:cornerRadius="8dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
<Button
|
||||
android:id="@+id/scanQRButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:drawableLeft="@drawable/ic_baseline_qr_code_24"
|
||||
android:drawablePadding="6dp"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:text="Scan QR"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textSize="15sp"
|
||||
android:letterSpacing="0.01"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
app:cornerRadius="8dp"
|
||||
app:strokeColor="?attr/colorPrimary"
|
||||
app:strokeWidth="1.5dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Configuration Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/background_secondary"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="18dp"
|
||||
android:text="Configuration"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="19sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.01" />
|
||||
|
||||
<!-- Permissions Section -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="SMS Permissions"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/grantSMSPermissionBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/button_background_tint"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:text="Grant SMS Permissions"
|
||||
android:textColor="@color/button_text_color"
|
||||
android:textSize="15sp"
|
||||
android:letterSpacing="0.01"
|
||||
android:visibility="visible"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
app:cornerRadius="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1.5dp"
|
||||
android:background="@color/divider"
|
||||
android:alpha="0.6"
|
||||
android:layout_marginBottom="18dp" />
|
||||
|
||||
<!-- Receive SMS Toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Receive SMS"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Toggle to enable SMS receiving"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/receiveSMSSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="32dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Filter Configuration Link -->
|
||||
<Button
|
||||
android:id="@+id/configureFilterBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="Configure Filter"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textSize="15sp"
|
||||
android:letterSpacing="0.01"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
android:drawableStart="@android:drawable/ic_menu_manage"
|
||||
android:drawablePadding="6dp"
|
||||
android:paddingHorizontal="0dp"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton" />
|
||||
|
||||
<!-- Sticky Notification Setting -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Sticky Notification"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:src="@android:drawable/ic_dialog_info"
|
||||
android:tint="?attr/colorPrimary"
|
||||
android:layout_marginEnd="6dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Prevents app from being killed by the system (optional, but recommended for reliability)"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/stickyNotificationSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="32dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1.5dp"
|
||||
android:background="@color/divider"
|
||||
android:alpha="0.6"
|
||||
android:layout_marginBottom="18dp" />
|
||||
|
||||
<!-- SMS Send Delay Setting -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="SMS Send Delay"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="Delay between each SMS in seconds (0 = no delay, max 3600)"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"
|
||||
android:lineSpacingMultiplier="1.2" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Delay (seconds)"
|
||||
app:boxBackgroundColor="@android:color/transparent"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
app:hintTextColor="?attr/colorPrimary"
|
||||
app:suffixText="sec"
|
||||
app:suffixTextColor="@color/text_secondary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/smsSendDelayEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1.5dp"
|
||||
android:background="@color/divider"
|
||||
android:alpha="0.6"
|
||||
android:layout_marginBottom="18dp" />
|
||||
|
||||
<!-- Default SIM Selection -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Default SIM"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="This is the default SIM that will be used for sending SMS. You can override for individual SMS by providing the specific simSubscriptionId in your API request."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"
|
||||
android:lineSpacingMultiplier="1.2" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/defaultSimSlotRadioGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"></RadioGroup>
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Select your preferred SIM for sending SMS"
|
||||
android:textSize="14dp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
<!-- How To Use Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#ccccccee"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="12dp">
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/background_secondary"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<TextView
|
||||
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="How To Use"
|
||||
android:textStyle="bold" />
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="How To Use"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="19sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.01" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:gravity="center"
|
||||
android:text="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dashboardLinkText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Go to https://textbee.dev/dashboard"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:gravity="center"
|
||||
android:text="2"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Click 'Generate API Key / Get Started'"
|
||||
android:textColor="@color/text_primary" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:gravity="center"
|
||||
android:text="3"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Copy the API key or scan the QR code"
|
||||
android:textColor="@color/text_primary" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- App Version Info Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
app:cardBackgroundColor="@color/background_secondary"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Go to textbee.dev/dashboard and click `generate API Key / Get started`, then copy and paste the API key generated or scan the QR code" />
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_baseline_info_24"
|
||||
android:tint="?attr/colorPrimary"
|
||||
android:padding="4dp"
|
||||
android:layout_marginEnd="14dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="App Information"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Version: "
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appVersionNameTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1.0.0"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=" (Build: "
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginStart="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appVersionCodeTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=")"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/checkUpdatesBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="Check for Updates"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:background="@android:color/transparent"
|
||||
android:textAllCaps="false"
|
||||
android:paddingHorizontal="0dp"
|
||||
style="@style/Widget.AppCompat.Button.Borderless" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,230 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background_primary"
|
||||
tools:context=".activities.SMSFilterActivity">
|
||||
|
||||
<!-- Header Section -->
|
||||
<LinearLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:orientation="vertical"
|
||||
android:padding="28dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="10dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@android:drawable/ic_menu_revert"
|
||||
android:tint="@color/white"
|
||||
android:contentDescription="Back"
|
||||
android:padding="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="SMS Filter"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.02"
|
||||
android:shadowColor="#80000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="2"
|
||||
android:shadowRadius="4" />
|
||||
|
||||
<View
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Configure SMS filtering rules"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13sp"
|
||||
android:letterSpacing="0.01"
|
||||
android:alpha="0.95" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/header">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Filter Settings Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/background_secondary"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="18dp"
|
||||
android:text="Filter Settings"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="19sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.01" />
|
||||
|
||||
<!-- Filter Enabled Toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Enable Filter"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Enable SMS filtering"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/filterEnabledSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="32dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1.5dp"
|
||||
android:background="@color/divider"
|
||||
android:alpha="0.6"
|
||||
android:layout_marginBottom="18dp" />
|
||||
|
||||
<!-- Filter Mode Selection -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="Filter Mode"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/filterModeRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/allowListRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Allow List (only process SMS matching these filters)"
|
||||
android:textColor="@color/text_primary"
|
||||
android:paddingVertical="8dp" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/blockListRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Block List (exclude these numbers/messages)"
|
||||
android:textColor="@color/text_primary"
|
||||
android:paddingVertical="8dp" />
|
||||
</RadioGroup>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Filter Rules Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/background_secondary"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="18dp"
|
||||
android:text="Filter Rules"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="19sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.01" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/filterRulesRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/addRuleFab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@android:drawable/ic_input_add"
|
||||
app:tint="@color/white"
|
||||
app:backgroundTint="?attr/colorPrimary" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialogTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:text="Add Filter Rule"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="Pattern (phone number or text)"
|
||||
app:boxBackgroundColor="@android:color/transparent"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
app:hintTextColor="?attr/colorPrimary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/patternEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:textColor="@color/text_primary"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="Filter Target"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/filterTargetSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="Match Type"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/matchTypeSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<!-- Case Sensitivity Toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Case Sensitive"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Match pattern with exact case"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/caseSensitiveSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="32dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end">
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="Cancel"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/saveButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Save"
|
||||
android:textColor="@color/button_text_color"
|
||||
android:backgroundTint="@color/button_background_tint"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
app:cornerRadius="8dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardBackgroundColor="@color/background_primary"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/patternText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Pattern"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/matchTypeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Match Type"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filterTargetText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="Filter Target"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/editButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@android:drawable/ic_menu_edit"
|
||||
android:tint="?attr/colorPrimary"
|
||||
android:contentDescription="Edit rule" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/deleteButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@android:drawable/ic_menu_delete"
|
||||
android:tint="#F44336"
|
||||
android:contentDescription="Delete rule" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<!-- Dark mode specific colors -->
|
||||
<color name="background_primary">#121212</color>
|
||||
<color name="background_secondary">#1E1E1E</color>
|
||||
<color name="text_primary">#FFFFFF</color>
|
||||
<color name="text_secondary">#B3FFFFFF</color> <!-- 70% white -->
|
||||
<color name="divider">#454545</color>
|
||||
</resources>
|
||||
@@ -2,15 +2,42 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.SMSGateway" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">#4299E1</item>
|
||||
<item name="colorPrimaryVariant">#4299cc</item>
|
||||
<item name="colorPrimary">#C4620A</item>
|
||||
<item name="colorPrimaryVariant">#A04405</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">#f35b04</item>
|
||||
<item name="colorSecondaryVariant">#f18701</item>
|
||||
<item name="colorSecondary">#B45309</item>
|
||||
<item name="colorSecondaryVariant">#92400E</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<!-- Dark mode specific overrides -->
|
||||
<item name="android:textColorPrimary">@color/text_primary</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary</item>
|
||||
<item name="android:windowBackground">@color/background_primary</item>
|
||||
<item name="android:colorBackground">@color/background_primary</item>
|
||||
<item name="android:editTextColor">@color/text_primary</item>
|
||||
<item name="android:textColorHint">@color/text_secondary</item>
|
||||
</style>
|
||||
|
||||
<!-- Theme without action bar for MainActivity -->
|
||||
<style name="Theme.SMSGateway.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">#C4620A</item>
|
||||
<item name="colorPrimaryVariant">#A04405</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">#B45309</item>
|
||||
<item name="colorSecondaryVariant">#92400E</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Dark mode specific overrides -->
|
||||
<item name="android:textColorPrimary">@color/text_primary</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary</item>
|
||||
<item name="android:windowBackground">@color/background_primary</item>
|
||||
<item name="android:colorBackground">@color/background_primary</item>
|
||||
<item name="android:editTextColor">@color/text_primary</item>
|
||||
<item name="android:textColorHint">@color/text_secondary</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -7,4 +7,11 @@
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<!-- Light mode specific colors -->
|
||||
<color name="background_primary">#FFFFFF</color>
|
||||
<color name="background_secondary">#F5F5F5</color>
|
||||
<color name="text_primary">#000000</color>
|
||||
<color name="text_secondary">#757575</color>
|
||||
<color name="divider">#E0E0E0</color>
|
||||
</resources>
|
||||
@@ -1,3 +1,2 @@
|
||||
<resources>
|
||||
<string name="app_name">TextBee</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="RadioButtonStyle" parent="Widget.AppCompat.CompoundButton.RadioButton">
|
||||
<item name="android:textColor">@color/radio_button_text_color</item>
|
||||
<item name="android:paddingStart">8dp</item>
|
||||
<item name="android:paddingTop">12dp</item>
|
||||
<item name="android:paddingBottom">12dp</item>
|
||||
<item name="android:buttonTint">?attr/colorPrimary</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -2,15 +2,29 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.SMSGateway" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">#4299E1</item>
|
||||
<item name="colorPrimaryVariant">#4299cc</item>
|
||||
<item name="colorPrimary">#C4620A</item>
|
||||
<item name="colorPrimaryVariant">#A04405</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">#f35b04</item>
|
||||
<item name="colorSecondaryVariant">#f18701</item>
|
||||
<item name="colorSecondary">#B45309</item>
|
||||
<item name="colorSecondaryVariant">#92400E</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
|
||||
<!-- Theme without action bar for MainActivity -->
|
||||
<style name="Theme.SMSGateway.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">#C4620A</item>
|
||||
<item name="colorPrimaryVariant">#A04405</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">#B45309</item>
|
||||
<item name="colorSecondaryVariant">#92400E</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "YOUR_DEV_PROJECT_NUMBER",
|
||||
"project_id": "your-dev-project-id",
|
||||
"storage_bucket": "your-dev-project-id.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:YOUR_DEV_PROJECT_NUMBER:android:YOUR_DEV_APP_ID",
|
||||
"android_client_info": {
|
||||
"package_name": "com.vernu.sms.dev"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "YOUR_DEV_CLIENT_ID",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "YOUR_DEV_API_KEY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "YOUR_DEV_CLIENT_ID",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id 'com.android.application' version '7.1.2' apply false
|
||||
id 'com.android.library' version '7.1.2' apply false
|
||||
id 'com.google.gms.google-services' version '4.3.10' apply true
|
||||
id 'com.google.firebase.crashlytics' version '2.9.9' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
||||
@@ -24,7 +24,19 @@ MAIL_USER=
|
||||
MAIL_PASS=
|
||||
MAIL_FROM=
|
||||
MAIL_REPLY_TO=
|
||||
ADMIN_EMAIL=
|
||||
|
||||
# Webhook delivery HTTP timeout in milliseconds (default 30000, min 10000, max 60000)
|
||||
WEBHOOK_DELIVERY_TIMEOUT_MS=30000
|
||||
|
||||
# Auto-disable webhook subscriptions with high failure rate (cron runs daily)
|
||||
WEBHOOK_AUTO_DISABLE_FAILURE_THRESHOLD=50
|
||||
WEBHOOK_AUTO_DISABLE_LOOKBACK_DAYS=30
|
||||
# Min failure rate 0–1 to disable (e.g. 0.50 = 50%; only disable when failures/total >= this)
|
||||
WEBHOOK_AUTO_DISABLE_MIN_FAILURE_RATE=0.50
|
||||
|
||||
# SMS Queue Configuration
|
||||
USE_SMS_QUEUE=false
|
||||
REDIS_URL=redis://localhost:6379 # if queue is enabled, redis url is required
|
||||
|
||||
CLOUDFLARE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
|
||||
@@ -11,7 +11,7 @@
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start": "node dist/main",
|
||||
"start": "node dist/main.js",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
@@ -21,59 +21,60 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nest-modules/mailer": "^1.3.22",
|
||||
"@nestjs/bull": "^11.0.2",
|
||||
"@nestjs/common": "^10.4.5",
|
||||
"@nestjs/config": "^4.0.1",
|
||||
"@nestjs/core": "^10.4.5",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/bull": "^11.0.4",
|
||||
"@nestjs/common": "^11.1.9",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.9",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/mongoose": "^10.0.10",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.4.5",
|
||||
"@nestjs/schedule": "^4.1.1",
|
||||
"@nestjs/swagger": "^7.4.2",
|
||||
"@nestjs/throttler": "^6.2.1",
|
||||
"@polar-sh/sdk": "^0.32.3",
|
||||
"axios": "^1.8.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"@nestjs/mongoose": "^11.0.4",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.1.9",
|
||||
"@nestjs/schedule": "^6.1.0",
|
||||
"@nestjs/swagger": "^11.2.3",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@polar-sh/sdk": "^0.41.5",
|
||||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"bull": "^4.16.5",
|
||||
"class-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.2",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"class-validator": "^0.14.3",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"ioredis": "^5.6.0",
|
||||
"mongoose": "^8.12.1",
|
||||
"nodemailer": "^6.10.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"mongoose": "^9.0.1",
|
||||
"nodemailer": "^7.0.11",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"rimraf": "^6.1.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"uuid": "^10.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.5",
|
||||
"@nestjs/schematics": "^10.2.2",
|
||||
"@nestjs/testing": "^10.4.5",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "29.5.13",
|
||||
"@types/node": "^22.7.7",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@nestjs/cli": "^11.0.14",
|
||||
"@nestjs/schematics": "^11.0.9",
|
||||
"@nestjs/testing": "^11.1.9",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/node": "^25.0.1",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.10.0",
|
||||
"@typescript-eslint/parser": "^8.10.0",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
||||
"@typescript-eslint/parser": "^8.49.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"jest": "^30.2.0",
|
||||
"prettier": "^3.7.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.1",
|
||||
"supertest": "^7.1.4",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-loader": "^9.5.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
@@ -19,6 +19,7 @@ import { BillingModule } from './billing/billing.module'
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config'
|
||||
import { BullModule } from '@nestjs/bull'
|
||||
import { SupportModule } from './support/support.module'
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter'
|
||||
|
||||
@Injectable()
|
||||
export class LoggerMiddleware implements NestMiddleware {
|
||||
@@ -36,6 +37,7 @@ export class LoggerMiddleware implements NestMiddleware {
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
EventEmitterModule.forRoot(),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60000,
|
||||
|
||||
@@ -12,6 +12,9 @@ export class RegisterInputDTO {
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
password: string
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
turnstileToken: string
|
||||
}
|
||||
|
||||
export class LoginInputDTO {
|
||||
@@ -20,11 +23,17 @@ export class LoginInputDTO {
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
password: string
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
turnstileToken: string
|
||||
}
|
||||
|
||||
export class RequestResetPasswordInputDTO {
|
||||
@ApiProperty({ type: String, required: true })
|
||||
email: string
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
turnstileToken: string
|
||||
}
|
||||
|
||||
export class ResetPasswordInputDTO {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AuthService } from './auth.service'
|
||||
import { JwtStrategy } from './jwt.strategy'
|
||||
import { ApiKey, ApiKeySchema } from './schemas/api-key.schema'
|
||||
import { MailModule } from 'src/mail/mail.module'
|
||||
import { CommonModule } from '../common/common.module'
|
||||
import {
|
||||
PasswordReset,
|
||||
PasswordResetSchema,
|
||||
@@ -41,12 +42,21 @@ import { OptionalAuthGuard } from './guards/optional-auth.guard'
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
signOptions: { expiresIn: process.env.JWT_EXPIRATION || '60d' },
|
||||
signOptions: {
|
||||
expiresIn: process.env.JWT_EXPIRATION || '60d' as any
|
||||
},
|
||||
}),
|
||||
MailModule,
|
||||
CommonModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy, AuthGuard, OptionalAuthGuard, MongooseModule],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
AuthGuard,
|
||||
OptionalAuthGuard,
|
||||
MongooseModule,
|
||||
],
|
||||
exports: [AuthService, JwtModule, AuthGuard, OptionalAuthGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
PasswordResetDocument,
|
||||
} from './schemas/password-reset.schema'
|
||||
import { MailService } from '../mail/mail.service'
|
||||
import { TurnstileService } from '../common/turnstile.service'
|
||||
import { RequestResetPasswordInputDTO, ResetPasswordInputDTO } from './auth.dto'
|
||||
import { AccessLog } from './schemas/access-log.schema'
|
||||
import {
|
||||
@@ -32,9 +33,12 @@ export class AuthService {
|
||||
@InjectModel(EmailVerification.name)
|
||||
private emailVerificationModel: Model<EmailVerificationDocument>,
|
||||
private readonly mailService: MailService,
|
||||
private readonly turnstileService: TurnstileService,
|
||||
) {}
|
||||
|
||||
async login(userData: any) {
|
||||
await this.turnstileService.verify(userData.turnstileToken)
|
||||
|
||||
const user = await this.usersService.findOne({ email: userData.email })
|
||||
if (!user) {
|
||||
throw new HttpException(
|
||||
@@ -109,6 +113,8 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async register(userData: any) {
|
||||
await this.turnstileService.verify(userData.turnstileToken)
|
||||
|
||||
const existingUser = await this.usersService.findOne({
|
||||
email: userData.email,
|
||||
})
|
||||
@@ -123,8 +129,9 @@ export class AuthService {
|
||||
this.validatePassword(userData.password)
|
||||
|
||||
const hashedPassword = await bcrypt.hash(userData.password, 10)
|
||||
const { turnstileToken, ...sanitizedUserData } = userData
|
||||
const user = await this.usersService.create({
|
||||
...userData,
|
||||
...sanitizedUserData,
|
||||
password: hashedPassword,
|
||||
})
|
||||
|
||||
@@ -152,7 +159,12 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async requestResetPassword({ email }: RequestResetPasswordInputDTO) {
|
||||
async requestResetPassword({
|
||||
email,
|
||||
turnstileToken,
|
||||
}: RequestResetPasswordInputDTO) {
|
||||
await this.turnstileService.verify(turnstileToken)
|
||||
|
||||
const user = await this.usersService.findOne({ email })
|
||||
if (!user) {
|
||||
return {
|
||||
@@ -160,6 +172,20 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user has requested password reset more than 5 times in the last 24 hours
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
const resetCount = await this.passwordResetModel.countDocuments({
|
||||
user: user._id,
|
||||
createdAt: { $gte: twentyFourHoursAgo }
|
||||
})
|
||||
|
||||
if (resetCount >= 5) {
|
||||
throw new HttpException(
|
||||
{ error: 'Too many password reset requests. Please try again later.' },
|
||||
HttpStatus.TOO_MANY_REQUESTS
|
||||
)
|
||||
}
|
||||
|
||||
const otp = Math.floor(100000 + Math.random() * 900000).toString()
|
||||
const expiresAt = new Date(Date.now() + 20 * 60 * 1000)
|
||||
|
||||
@@ -169,7 +195,7 @@ export class AuthService {
|
||||
otp: hashedOtp,
|
||||
expiresAt,
|
||||
})
|
||||
passwordReset.save()
|
||||
await passwordReset.save()
|
||||
|
||||
const resetLink = `${process.env.FRONTEND_URL || 'https://textbee.dev'}/reset-password?email=${encodeURIComponent(user.email)}&otp=${otp}`
|
||||
|
||||
@@ -205,6 +231,9 @@ export class AuthService {
|
||||
user.password = hashedPassword
|
||||
await user.save()
|
||||
|
||||
passwordReset.expiresAt = new Date(Date.now())
|
||||
await passwordReset.save()
|
||||
|
||||
this.mailService.sendEmailFromTemplate({
|
||||
to: user.email,
|
||||
subject: 'textbee.dev - Password Reset',
|
||||
@@ -237,12 +266,28 @@ export class AuthService {
|
||||
)
|
||||
}
|
||||
|
||||
this.validatePassword(input.newPassword)
|
||||
|
||||
const hashedPassword = await bcrypt.hash(input.newPassword, 10)
|
||||
userToUpdate.password = hashedPassword
|
||||
await userToUpdate.save()
|
||||
}
|
||||
|
||||
async sendEmailVerificationEmail(user: UserDocument) {
|
||||
// Check if user has requested email verification more than 5 times in the last 24 hours
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
const verificationCount = await this.emailVerificationModel.countDocuments({
|
||||
user: user._id,
|
||||
createdAt: { $gte: twentyFourHoursAgo }
|
||||
})
|
||||
|
||||
if (verificationCount >= 5) {
|
||||
throw new HttpException(
|
||||
{ error: 'Too many email verification requests. Please try again later.' },
|
||||
HttpStatus.TOO_MANY_REQUESTS
|
||||
)
|
||||
}
|
||||
|
||||
const verificationCode = uuidv4()
|
||||
const expiresAt = new Date(Date.now() + 20 * 60 * 1000) // 20 minutes
|
||||
|
||||
@@ -422,9 +467,9 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
async validatePassword(password: string) {
|
||||
if (password.length < 6) {
|
||||
if (password.length < 6 || password.length > 128) {
|
||||
throw new HttpException(
|
||||
{ error: 'Password must be at least 6 characters' },
|
||||
{ error: 'Password must be between 6 and 128 characters' },
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ export class AccessLog {
|
||||
_id?: Types.ObjectId
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: ApiKey.name })
|
||||
apiKey: ApiKey
|
||||
apiKey: ApiKey | Types.ObjectId
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: User.name })
|
||||
user: User
|
||||
user: User | Types.ObjectId
|
||||
|
||||
@Prop({ type: String })
|
||||
url: string
|
||||
|
||||
@@ -18,7 +18,7 @@ export class ApiKey {
|
||||
hashedApiKey: string
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: User.name })
|
||||
user: User
|
||||
user: User | Types.ObjectId
|
||||
|
||||
@Prop({ type: Number, default: 0 })
|
||||
usageCount: number
|
||||
|
||||
@@ -9,7 +9,7 @@ export class EmailVerification {
|
||||
_id?: Types.ObjectId
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: User.name })
|
||||
user: User
|
||||
user: User | Types.ObjectId
|
||||
|
||||
@Prop({ type: String })
|
||||
verificationCode: string // hashed
|
||||
|
||||