Browse Source

Merge pull request #79 from vernu/android-ui-ux

improve android app ui/ux
pull/81/head
Israel Abebe 9 months ago
committed by GitHub
parent
commit
601f33d95e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      android/app/build.gradle
  2. 3
      android/app/src/main/AndroidManifest.xml
  3. BIN
      android/app/src/main/ic_launcher-playstore.png
  4. 3
      android/app/src/main/java/com/vernu/sms/AppConstants.java
  5. 69
      android/app/src/main/java/com/vernu/sms/TextBeeUtils.java
  6. 218
      android/app/src/main/java/com/vernu/sms/activities/MainActivity.java
  7. 111
      android/app/src/main/java/com/vernu/sms/helpers/VersionTracker.java
  8. 57
      android/app/src/main/java/com/vernu/sms/services/StickyNotificationService.java
  9. 6
      android/app/src/main/res/color/radio_button_text_color.xml
  10. 6
      android/app/src/main/res/color/radio_button_tint.xml
  11. 10
      android/app/src/main/res/drawable/ic_baseline_edit_24.xml
  12. 10
      android/app/src/main/res/drawable/ic_baseline_info_24.xml
  13. 622
      android/app/src/main/res/layout/activity_main.xml
  14. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  15. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
  16. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  17. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
  18. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  19. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  20. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  21. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
  22. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  23. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
  24. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  25. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  26. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  27. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  28. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  29. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
  30. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  31. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  32. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  33. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  34. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  35. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
  36. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  37. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  38. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  39. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  40. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  41. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
  42. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  43. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  44. 17
      android/app/src/main/res/values-night/colors.xml
  45. 33
      android/app/src/main/res/values-night/themes.xml
  46. 7
      android/app/src/main/res/values/colors.xml
  47. 2
      android/app/src/main/res/values/strings.xml
  48. 10
      android/app/src/main/res/values/styles.xml
  49. 18
      android/app/src/main/res/values/themes.xml
  50. 1
      android/build.gradle

4
android/app/build.gradle

@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
}
android {
@ -37,7 +38,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,6 +48,7 @@ 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'

3
android/app/src/main/AndroidManifest.xml

@ -55,7 +55,8 @@
<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" />

BIN
android/app/src/main/ic_launcher-playstore.png

Before

Width: 512  |  Height: 512  |  Size: 237 KiB

After

Width: 512  |  Height: 512  |  Size: 131 KiB

3
android/app/src/main/java/com/vernu/sms/AppConstants.java

@ -16,4 +16,7 @@ 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";
}

69
android/app/src/main/java/com/vernu/sms/TextBeeUtils.java

@ -7,16 +7,22 @@ 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 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 +39,82 @@ public class TextBeeUtils {
}
public static void startStickyNotificationService(Context context) {
if(!isPermissionGranted(context, Manifest.permission.RECEIVE_SMS)){
return;
}
// 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 {
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);
}
}

218
android/app/src/main/java/com/vernu/sms/activities/MainActivity.java

@ -31,6 +31,8 @@ import com.vernu.sms.R;
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
import com.vernu.sms.helpers.SharedPreferenceHelper;
import com.vernu.sms.helpers.VersionTracker;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import java.util.Arrays;
import java.util.Objects;
import retrofit2.Call;
@ -40,11 +42,11 @@ 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;
private Button registerDeviceBtn, grantSMSPermissionBtn, scanQRBtn, checkUpdatesBtn;
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;
@ -60,8 +62,10 @@ 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);
registerDeviceBtn = findViewById(R.id.registerDeviceBtn);
grantSMSPermissionBtn = findViewById(R.id.grantSMSPermissionBtn);
scanQRBtn = findViewById(R.id.scanQRButton);
@ -69,10 +73,40 @@ 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);
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");
}
if (deviceId == null || deviceId.isEmpty()) {
registerDeviceBtn.setText("Register");
} else {
@ -117,7 +151,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, response.message().isEmpty() ? "An error occurred :( "+ response.code() : response.message(), Snackbar.LENGTH_LONG).show();
compoundButton.setEnabled(true);
return;
}
@ -125,11 +159,14 @@ 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);
}
} else {
TextBeeUtils.stopStickyNotificationService(mContext);
}
compoundButton.setEnabled(true);
}
@Override
@ -137,6 +174,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 +188,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 +218,50 @@ 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);
});
}
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() + ")";
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) {
@ -206,6 +280,42 @@ public class MainActivity extends AppCompatActivity {
}
}
/**
* 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) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
@ -225,8 +335,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);
@ -253,31 +364,88 @@ public class MainActivity extends AppCompatActivity {
registerDeviceInput.setAppVersionCode(BuildConfig.VERSION_CODE);
registerDeviceInput.setAppVersionName(BuildConfig.VERSION_NAME);
// 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, response.message().isEmpty() ? "An error occurred :( "+ response.code() : response.message(), 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());
}
// 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");
}
});
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, response.message(), Snackbar.LENGTH_LONG).show();
Snackbar.make(view, response.message().isEmpty() ? "An error occurred :( "+ response.code() : response.message(), 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());
}
// 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 +455,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 +484,30 @@ public class MainActivity extends AppCompatActivity {
updateDeviceInput.setAppVersionCode(BuildConfig.VERSION_CODE);
updateDeviceInput.setAppVersionName(BuildConfig.VERSION_NAME);
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService().updateDevice(deviceId, apiKey, updateDeviceInput);
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, response.message().isEmpty() ? "An error occurred :( "+ response.code() : response.message(), 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);
}
// 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 +518,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 +548,11 @@ public class MainActivity extends AppCompatActivity {
}
String scannedQR = intentResult.getContents();
apiKeyEditText.setText(scannedQR);
if(deviceIdEditText.getText().toString().isEmpty()) {
handleRegisterDevice();
} else {
handleUpdateDevice();
}
}
}

111
android/app/src/main/java/com/vernu/sms/helpers/VersionTracker.java

@ -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());
}
});
}
}

57
android/app/src/main/java/com/vernu/sms/services/StickyNotificationService.java

@ -15,6 +15,8 @@ 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 {
@ -32,15 +34,25 @@ public class StickyNotificationService extends Service {
super.onCreate();
Log.i(TAG, "Service Started");
// 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);
// Only register receiver and show notification if enabled in preferences
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
getApplicationContext(),
AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY,
false
);
if (stickyNotificationEnabled) {
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);
Log.i(TAG, "Started foreground service with sticky notification");
} else {
Log.i(TAG, "Sticky notification disabled by user preference");
}
}
@Override
@ -52,9 +64,19 @@ public class StickyNotificationService extends Service {
@Override
public void onDestroy() {
super.onDestroy();
// unregisterReceiver(receiver);
// Only unregister if we had registered in the first place
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
getApplicationContext(),
AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY,
false
);
if (stickyNotificationEnabled) {
unregisterReceiver(receiver);
}
Log.i(TAG, "StickyNotificationService destroyed");
// Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show();
}
private Notification createNotification() {
@ -72,10 +94,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();
}
}

6
android/app/src/main/res/color/radio_button_text_color.xml

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

6
android/app/src/main/res/color/radio_button_tint.xml

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

10
android/app/src/main/res/drawable/ic_baseline_edit_24.xml

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

10
android/app/src/main/res/drawable/ic_baseline_info_24.xml

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

622
android/app/src/main/res/layout/activity_main.xml

@ -1,55 +1,227 @@
<?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">
android:background="?attr/colorPrimary"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_width="match_parent"
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:text="textbee.dev - sms gateway"
android:textAlignment="center"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="Your ultimate solution for seamless SMS communication"
android:textAlignment="center"
android:layout_gravity="center" />
android:textColor="@color/white"
android:textSize="12sp" />
</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="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ccccccee"
android:layout_margin="5dp"
android:gravity="center"
android:orientation="horizontal"
android:padding="6dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:padding="3dp"
android:src="@drawable/ic_baseline_phone_android_24"
android:tint="?attr/colorPrimary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="3dp"
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_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Device ID:"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
<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="24dp"
android:layout_height="24dp"
android:layout_marginStart="2dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="2dp"
android:src="@drawable/ic_baseline_content_copy_24"
android:tint="?attr/colorPrimary" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<Switch
android:id="@+id/gatewaySwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="32dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</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_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/background_secondary"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
android:padding="16dp">
<EditText
android:id="@+id/apiKeyEditText"
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:layout_marginBottom="16dp"
android:text="Account Information"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<!-- 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">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/apiKeyEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:minHeight="48dp"
android:textColor="@color/text_primary"
android:textIsSelectable="true" />
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/fcmTokenEditText"
@ -60,191 +232,357 @@
android:gravity="start|top"
android:hint="FCM Token"
android:inputType="textMultiLine"
android:textColor="@color/text_primary"
android:textColorHint="@color/text_secondary"
android:visibility="gone" />
<Button
android:id="@+id/registerDeviceBtn"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Register" />
</LinearLayout>
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
<Button
android:id="@+id/registerDeviceBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
android:backgroundTint="?attr/colorPrimary"
android:paddingHorizontal="24dp"
android:text="Connect"
android:textColor="@color/white" />
<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_margin="5dp"
android:drawableTop="@drawable/ic_baseline_qr_code_24"
android:text="Scan"
android:drawableLeft="@drawable/ic_baseline_qr_code_24"
android:text="Scan QR"
android:textColor="@color/black"
android:theme="@style/Theme.Design.Light" />
</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="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_margin="5dp"
android:orientation="horizontal">
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Configuration"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<!-- 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:orientation="vertical">
android:text="SMS Permissions"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<ImageView
<TextView
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" />
android:text="REQUIRED for textbee to function!"
android:textColor="@android:color/holo_red_dark"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/grantSMSPermissionBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="?attr/colorPrimary"
android:text="Grant SMS Permissions"
android:textColor="@color/white"
android:visibility="visible" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider"
android:layout_marginBottom="16dp" />
<!-- Receive SMS Toggle -->
<LinearLayout
android:layout_width="wrap_content"
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_gravity="center"
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:text="Receive SMS"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
android:text="Toggle to enable SMS receiving"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
<Switch
android:id="@+id/receiveSMSSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minHeight="32dp" />
</LinearLayout>
<!-- 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="Device ID"
android:textStyle="italic" />
android:text="Sticky Notification"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/deviceIdTxt"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ae5ce05c05cde3" />
</LinearLayout>
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageButton
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@android:drawable/ic_dialog_info"
android:tint="?attr/colorPrimary"
android:layout_marginEnd="4dp" />
android:id="@+id/copyDeviceIdImgBtn"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_content_copy_24" />
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>
<LinearLayout
<Switch
android:id="@+id/stickyNotificationSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
android:minHeight="32dp" />
</LinearLayout>
<Switch
android:id="@+id/gatewaySwitch"
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider"
android:layout_marginBottom="16dp" />
<!-- Default SIM Selection -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:minHeight="32dp"
android:text="" />
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:text="Status"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
android:layout_marginBottom="8dp"
android:text="Select your preferred SIM for sending SMS"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<RadioGroup
android:id="@+id/defaultSimSlotRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="8dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- How To Use Card -->
<androidx.cardview.widget.CardView
android:id="@+id/bottom"
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="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:orientation="vertical"
android:padding="10px">
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Configuration"
android:layout_marginBottom="8dp"
android:text="How To Use"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<View
<LinearLayout
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" />
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:background="?attr/colorPrimary"
android:gravity="center"
android:text="1"
android:textColor="@color/white"
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:text="TextBee will only work if you grant SMS Permissions"
android:textSize="14dp"
android:textStyle="italic" />
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:background="?attr/colorPrimary"
android:gravity="center"
android:text="2"
android:textColor="@color/white"
android:textStyle="bold" />
<View
<TextView
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000" />
android:layout_height="wrap_content"
android:text="Click 'Generate API Key / Get Started'"
android:textColor="@color/text_primary" />
</LinearLayout>
<Switch
android:id="@+id/receiveSMSSwitch"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:minHeight="32dp"
android:text="Receive SMS" />
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:background="?attr/colorPrimary"
android:gravity="center"
android:text="3"
android:textColor="@color/white"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Toggle this if you want to receive SMS"
android:textSize="14dp"
android:textStyle="italic" />
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="8dp"
app:cardElevation="2dp">
<View
<LinearLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000" />
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_baseline_info_24"
android:tint="?attr/colorPrimary"
android:layout_marginEnd="12dp" />
<LinearLayout
android:layout_width="match_parent"
@ -254,50 +592,72 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Default SIM"
android:text="App Information"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/defaultSimSlotRadioGroup"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"></RadioGroup>
</LinearLayout>
android:orientation="horizontal"
android:layout_marginTop="4dp">
<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>
android:text="Version: "
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<LinearLayout
android:id="@+id/bottom"
android:layout_width="match_parent"
<TextView
android:id="@+id/appVersionNameTxt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ccccccee"
android:orientation="vertical"
android:layout_marginTop="30dp"
android:padding="12dp">
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="How To Use"
android:textStyle="bold" />
android:text="100"
android:textColor="@color/text_primary"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
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: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>

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png

Before

Width: 72  |  Height: 72  |  Size: 4.0 KiB

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png

Before

Width: 162  |  Height: 162  |  Size: 10 KiB

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png

Before

Width: 72  |  Height: 72  |  Size: 6.2 KiB

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png

Before

Width: 48  |  Height: 48  |  Size: 2.2 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png

Before

Width: 108  |  Height: 108  |  Size: 5.0 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png

Before

Width: 48  |  Height: 48  |  Size: 3.5 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png

Before

Width: 96  |  Height: 96  |  Size: 6.3 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png

Before

Width: 216  |  Height: 216  |  Size: 18 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png

Before

Width: 96  |  Height: 96  |  Size: 9.7 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png

Before

Width: 144  |  Height: 144  |  Size: 12 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png

Before

Width: 324  |  Height: 324  |  Size: 40 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png

Before

Width: 144  |  Height: 144  |  Size: 18 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

Before

Width: 192  |  Height: 192  |  Size: 21 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png

Before

Width: 432  |  Height: 432  |  Size: 74 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

Before

Width: 192  |  Height: 192  |  Size: 30 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp

17
android/app/src/main/res/values-night/colors.xml

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

33
android/app/src/main/res/values-night/themes.xml

@ -2,8 +2,8 @@
<!-- 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">#D97706</item>
<item name="colorPrimaryVariant">#B86504</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">#f35b04</item>
@ -11,6 +11,33 @@
<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">#D97706</item>
<item name="colorPrimaryVariant">#B86504</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">#f35b04</item>
<item name="colorSecondaryVariant">#f18701</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
android/app/src/main/res/values/colors.xml

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

2
android/app/src/main/res/values/strings.xml

@ -1,3 +1,3 @@
<resources>
<string name="app_name">TextBee</string>
<string name="app_name">textbee.dev</string>
</resources>

10
android/app/src/main/res/values/styles.xml

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

18
android/app/src/main/res/values/themes.xml

@ -2,8 +2,8 @@
<!-- 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">#D97706</item>
<item name="colorPrimaryVariant">#B86504</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">#f35b04</item>
@ -13,4 +13,18 @@
<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">#D97706</item>
<item name="colorPrimaryVariant">#B86504</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">#f35b04</item>
<item name="colorSecondaryVariant">#f18701</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
</style>
</resources>

1
android/build.gradle

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

Loading…
Cancel
Save