From ab11f55f72935648182761695b6f55d2b301fed4 Mon Sep 17 00:00:00 2001 From: Aman Chaturvedi Date: Fri, 21 Feb 2025 21:55:25 +0530 Subject: [PATCH] NTP-38623 | Dialer app (#1104) Co-authored-by: Varnit Goyal Co-authored-by: Ashish Deo Co-authored-by: Mantri Ramkishor --- App.tsx | 6 +- android/app/build.gradle | 6 +- android/app/proguard-rules.pro | 3 +- android/app/src/main/AndroidManifest.xml | 25 +- .../java/com/avapp/DeviceUtilsModule.java | 72 +++++- .../main/java/com/avapp/MainApplication.java | 8 +- .../java/com/avapp/callModule/CallModule.java | 226 ++++++++++++++++++ .../avapp/callModule/CallModulePackage.java | 28 +++ .../ContentProviderModule.java | 172 +++++++++++++ .../ContentProviderPackage.java | 27 +++ .../ContentProviderThreadPool.java | 31 +++ .../avapp/contentProvider/CosmosDatabase.java | 33 +++ .../ackNumbers/AckNumberContentProvider.java | 129 ++++++++++ .../ackNumbers/AckNumberContractProvider.java | 21 ++ .../ackNumbers/AckNumberDataDao.java | 52 ++++ .../ackNumbers/AckNumbersEntity.java | 19 ++ .../PrimaryNumberContentProvider.java | 110 +++++++++ .../PrimaryNumberContractProvider.java | 18 ++ .../primaryNumber/PrimaryNumberDataDao.java | 51 ++++ .../primaryNumber/PrimaryNumberEntity.java | 11 + .../SharedPreferencesModule.java | 62 +++++ .../SharedPreferencesPackage.java | 26 ++ android/build.gradle | 2 + .../gradle/wrapper/gradle-wrapper.properties | 4 +- buildFlavor/field/buildNumber.txt | 2 +- buildFlavor/field/buildVersion.txt | 2 +- buildFlavor/tele/buildNumber.txt | 2 +- buildFlavor/tele/buildVersion.txt | 2 +- package.json | 2 +- src/action/appDownloadAction.ts | 40 +++- src/assets/icons/DialerAppIcon.tsx | 16 ++ .../AgentActivityConfigurableConstants.ts | 27 ++- src/common/BlockerScreen.tsx | 109 ++++++++- src/common/Constants.ts | 14 ++ src/common/DialerAppUpdate.tsx | 86 +++++++ src/common/utils.ts | 79 ++++++ src/components/utlis/DeviceUtils.ts | 8 + src/components/utlis/ackNumberProviders.ts | 25 ++ src/components/utlis/apiHelper.ts | 4 +- src/components/utlis/callModuleUtils.ts | 10 + src/components/utlis/sharedPreferences.ts | 9 + src/hooks/useFirestoreUpdates.ts | 10 +- src/reducer/activeCallSlice.ts | 18 ++ src/reducer/appUpdateSlice.ts | 17 +- src/reducer/metadataSlice.ts | 8 +- src/reducer/userSlice.ts | 2 + src/screens/auth/AuthRouter.tsx | 2 + src/screens/auth/ProtectedRouter.tsx | 3 +- src/screens/caseDetails/CallCustomer.tsx | 4 + .../BottomSheets/CallViaBottomSheet.tsx | 172 +++++++++++++ .../InstallDialerAppBottomSheet.tsx | 102 ++++++++ .../CallHistory/CallHistoryItem.tsx | 62 ++--- .../caseDetails/journeyStepper/RenderIcon.tsx | 106 ++++---- .../caseDetails/utils/caseDetailsUtils.tsx | 61 +++-- src/screens/login/index.tsx | 26 +- .../firebaseFetchAndUpdate.service.ts | 10 +- src/services/syncSelfCallData.ts | 44 ++++ 57 files changed, 2062 insertions(+), 164 deletions(-) create mode 100644 android/app/src/main/java/com/avapp/callModule/CallModule.java create mode 100644 android/app/src/main/java/com/avapp/callModule/CallModulePackage.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/ContentProviderModule.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/ContentProviderPackage.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/ContentProviderThreadPool.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/CosmosDatabase.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberContentProvider.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberContractProvider.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberDataDao.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumbersEntity.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberContentProvider.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberContractProvider.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberDataDao.java create mode 100644 android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberEntity.java create mode 100644 android/app/src/main/java/com/avapp/sharedPreference/SharedPreferencesModule.java create mode 100644 android/app/src/main/java/com/avapp/sharedPreference/SharedPreferencesPackage.java create mode 100644 src/assets/icons/DialerAppIcon.tsx create mode 100644 src/common/DialerAppUpdate.tsx create mode 100644 src/components/utlis/ackNumberProviders.ts create mode 100644 src/components/utlis/callModuleUtils.ts create mode 100644 src/components/utlis/sharedPreferences.ts create mode 100644 src/screens/caseDetails/CallingFlow/BottomSheets/CallViaBottomSheet.tsx create mode 100644 src/screens/caseDetails/CallingFlow/BottomSheets/InstallDialerAppBottomSheet.tsx create mode 100644 src/services/syncSelfCallData.ts diff --git a/App.tsx b/App.tsx index dfac7b08..f59b24ab 100644 --- a/App.tsx +++ b/App.tsx @@ -27,7 +27,7 @@ import { navigationRef } from '@utils/navigationUtlis'; import { sendDeviceDetailsToClickstream } from '@components/utlis/commonFunctions'; import { linkingConf } from '@components/utlis/deeplinkingUtils'; import { getBuildFlavour } from '@components/utlis/DeviceUtils'; -import { setGlobalBuildFlavour } from '@constants/Global'; +import { GLOBAL, setGlobalBuildFlavour } from '@constants/Global'; import analytics from '@react-native-firebase/analytics'; import dayJs from 'dayjs'; import { COLORS } from '@rn-ui-lib/colors'; @@ -49,6 +49,7 @@ import { StorageKeys } from './src/types/storageKeys'; import CodePushLoadingModal, { CodePushLoadingModalRef } from './CodePushModal'; import { initSentry } from '@components/utlis/sentry'; import { AppStates } from '@interfaces/appStates'; +import syncSelfCallData from '@services/syncSelfCallData'; if (!__DEV__) { initSentry(); @@ -174,6 +175,9 @@ function App() { const appStateChange = AppState.addEventListener('change', async (change) => { if (change === AppStates.ACTIVE) { askForPermissions(); + if (GLOBAL.AGENT_ID) { + syncSelfCallData(GLOBAL.AGENT_ID); + } } handleAppStateChange(change, onSyncStatusChange, onDownloadProgress); hydrateGlobalImageMap(); diff --git a/android/app/build.gradle b/android/app/build.gradle index 39bb889b..b64ac6e5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,7 +114,7 @@ def enableHermes = project.ext.react.get("enableHermes", false); def VERSION_CODE = 237 -def VERSION_NAME = "2.17.4" +def VERSION_NAME = "2.18.0" android { namespace "com.avapp" @@ -223,10 +223,12 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'com.navi.android:pulse:1.0.1-cosmos' implementation 'androidx.work:work-runtime-ktx:2.8.1' + implementation "androidx.room:room-runtime:2.5.0" + annotationProcessor "androidx.room:room-compiler:2.5.0" //noinspection GradleDynamicVersion implementation("com.facebook.react:react-android") - + debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { exclude group:'com.squareup.okhttp3', module:'okhttp' diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index b9068986..27887103 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -16,4 +16,5 @@ -keep class com.navi.alfred.db.** { *; } -keep class com.navi.alfred.AlfredConfig { *; } -keepattributes SourceFile,LineNumberTable # Keep file names and line numbers. --keep public class * extends java.lang.Exception # Optional: Keep custom exceptions. \ No newline at end of file +-keep public class * extends java.lang.Exception # Optional: Keep custom exceptions. +-keep class com.avapp.contentProvider.ackNumbers.AckNumberContentProvider { *; } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ec0860ee..38edf0da 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + package="com.avapp" + > @@ -39,6 +40,7 @@ + - - + + + + - + + + appsInstalled = isWhatsAppInstalled(); int numberOfAppsInstalled = appsInstalled.size(); @@ -378,5 +381,60 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule { } } + @ReactMethod + public void isAppInstalled(String packageName, Promise promise) { + PackageManager pm = RNContext.getPackageManager(); + try { + ApplicationInfo info = pm.getApplicationInfo(packageName, 0); + if (info != null) { + promise.resolve(true); // App is installed + } else { + promise.resolve(false); // App is not installed + } + } catch (PackageManager.NameNotFoundException e) { + promise.resolve(false); // App is not installed + } catch (Exception e) { + promise.reject("ERROR", "An unexpected error occurred: " + e.getMessage()); + } + } + @ReactMethod + public void getAppInfo(String packageName, Promise promise) { + PackageManager pm = RNContext.getPackageManager(); + try { + // Get PackageInfo for the given package name + PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); + + // Create a WritableMap to store the app information + WritableMap map = Arguments.createMap(); + map.putInt("version", packageInfo.versionCode); // App version name + map.putString("versionName", packageInfo.versionName); // App version code (deprecated in API 28+ but still useful for older devices) + + promise.resolve(map); // Resolve the promise with the app info map + } catch (PackageManager.NameNotFoundException e) { + promise.resolve(false); // App not found, resolve with false + } catch (Exception e) { + promise.reject("ERROR", "An unexpected error occurred: " + e.getMessage()); // Handle unexpected errors + } + } + + @ReactMethod + public void getAppInfoFromFilePath(String apkFilePath, Promise promise) { + try { + PackageManager pm = RNContext.getPackageManager(); + PackageInfo packageInfo = pm.getPackageArchiveInfo(apkFilePath, 0); + + if (packageInfo != null) { + WritableMap map = Arguments.createMap(); + map.putString("packageName", packageInfo.packageName); + map.putInt("version", packageInfo.versionCode); + map.putString("versionName", packageInfo.versionName); + promise.resolve(map); + } else { + promise.reject("APK_ERROR", "Failed to read APK file."); + } + } catch (Exception e) { + promise.reject(e); + } + } } diff --git a/android/app/src/main/java/com/avapp/MainApplication.java b/android/app/src/main/java/com/avapp/MainApplication.java index d71ba5b0..d79b82ca 100644 --- a/android/app/src/main/java/com/avapp/MainApplication.java +++ b/android/app/src/main/java/com/avapp/MainApplication.java @@ -11,9 +11,12 @@ import android.app.Application; import android.content.Context; import com.avapp.appInstallerModule.ApkInstallerPackage; +import com.avapp.contentProvider.ContentProviderPackage; +import com.avapp.callModule.CallModulePackage; import com.avapp.deviceDataSync.DeviceDataSyncPackage; import com.avapp.photoModule.PhotoModulePackage; import com.avapp.phoneStateBroadcastReceiver.PhoneStateModulePackage; +import com.avapp.sharedPreference.SharedPreferencesPackage; import com.avapp.utils.FirebaseRemoteConfigHelper; import com.avapp.wifiDetailsModule.WifiDetailsModulePackage; import com.avapp.restartApp.RestartPackage; @@ -69,6 +72,9 @@ public class MainApplication extends Application implements ReactApplication, Ap packages.add(new WifiDetailsModulePackage()); packages.add(new ApkInstallerPackage()); packages.add(new RestartPackage()); + packages.add(new CallModulePackage()); + packages.add(new SharedPreferencesPackage()); + packages.add(new ContentProviderPackage()); return packages; } @@ -280,4 +286,4 @@ public class MainApplication extends Application implements ReactApplication, Ap public void onActivityDestroyed(@NonNull Activity activity) { } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/avapp/callModule/CallModule.java b/android/app/src/main/java/com/avapp/callModule/CallModule.java new file mode 100644 index 00000000..3ceaf1d5 --- /dev/null +++ b/android/app/src/main/java/com/avapp/callModule/CallModule.java @@ -0,0 +1,226 @@ +package com.avapp.callModule; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Callback; +import android.os.Build; +import androidx.core.content.FileProvider; +import com.avapp.BuildConfig; +import java.io.File; +import java.io.FileInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import com.facebook.react.bridge.Callback; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.Context; +import com.facebook.react.bridge.Promise; + +public class CallModule extends ReactContextBaseJavaModule { + + private final ReactApplicationContext reactContext; + private static final String TAG = "CallModule"; + private static final String CALLING_APP_PACKAGE = "org.fossify.phone"; + + private String pendingRecipient; + private String pendingExotelNumber; + private BroadcastReceiver installReceiver; + + public CallModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + registerInstallReceiver(); + } + + @NonNull + @Override + public String getName() { + return "CallModule"; + } + + private void registerInstallReceiver() { + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addDataScheme("package"); + + installReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Uri data = intent.getData(); + Log.d(TAG, "InstallReceiver: onReceive: " + intent.getAction()); + if (data != null) { + String installedPackage = data.getSchemeSpecificPart(); + if (CALLING_APP_PACKAGE.equals(installedPackage)) { + Log.d(TAG, "Installed package detected: " + installedPackage); + retryCallAfterInstall(); + } + } + } + }; + + reactContext.registerReceiver(installReceiver, filter); + } + + private void retryCallAfterInstall() { + Activity activity = getCurrentActivity(); + if (activity != null && pendingRecipient != null) { + makeCall(activity, pendingRecipient, pendingExotelNumber); + } else if (pendingRecipient != null) { + Intent intent = new Intent(Intent.ACTION_CALL); + intent.setData(Uri.parse("tel:" + pendingRecipient)); + intent.putExtra("cosmos_call_numb", pendingRecipient); + intent.putExtra("cosmos_call_numb_exotel", pendingExotelNumber); + intent.setPackage(CALLING_APP_PACKAGE); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // REQUIRED! + activity.startActivity(intent); + } + + // Reset stored values + pendingRecipient = null; + pendingExotelNumber = null; + } + + + public static String maskFirstSixDigits(String number) { + if (number == null || number.length() != 10) { + Log.e("AVAPP", "number incorrect"); + } + if (number.length() == 10) { + return "******" + number.substring(6); + }else { + return "******" + number.substring(8); // Mask first 6 digits + + }} + + private boolean isAppInstalled(Activity activity, String packageName) { + try { + activity.getPackageManager().getPackageInfo(packageName, 0); + return true; + } catch (Exception e) { + return false; + } + } + + @ReactMethod + public void isDialerAppValidApkFile(String filePath, Promise promise) { + try { + boolean isValid = isValidApkFile(filePath); + promise.resolve(isValid); + } catch (Exception e) { + Log.e("ApkInstaller", "Error validating APK file", e); + promise.reject("APK_VALIDATION_ERROR", e); + } + } + + public boolean isValidApkFile(String filePath) { + try { + File apkFile = new File(filePath); + if (!apkFile.exists() || apkFile.length() == 0) { + return false; + } + try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(apkFile))) { + ZipEntry zipEntry = zipInputStream.getNextEntry(); + return zipEntry != null; + } catch (Exception e) { + Log.e("ApkInstaller", "Invalid APK file", e); + return false; + } + } catch (Exception e) { + Log.e("ApkInstaller", "Error installing APK", e); + return false; + } + } + + @ReactMethod + public void launchCallIntent(String recipient, String exotelNumber, String filePath, Callback callback) { + Activity activity = getCurrentActivity(); + if (activity == null) { + Log.e(TAG, "Activity is null, cannot make call"); + return; + } + + // Mask the recipient number for logging + String maskedNumber = maskFirstSixDigits(recipient); + Log.d(TAG, "Attempting to call: " + maskedNumber + " using " + CALLING_APP_PACKAGE); + pendingRecipient = recipient; + pendingExotelNumber = exotelNumber; + if (!isAppInstalled(activity, CALLING_APP_PACKAGE)) { + Log.d(TAG, "App not installed, installing it..."); + installApk(filePath, callback); + return; + } + + // Make the call if the app is installed + makeCall(activity, recipient, exotelNumber); + } + + private void makeCall(Activity activity, String recipient, String exotelNumber) { + Intent intent = new Intent(Intent.ACTION_CALL); + intent.setData(Uri.parse("tel:" + pendingRecipient)); + intent.putExtra("cosmos_call_numb", pendingRecipient); + intent.putExtra("cosmos_call_numb_exotel", pendingExotelNumber); + intent.setPackage(CALLING_APP_PACKAGE); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // Ensure the specific app is used + + if (intent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivity(intent); + Log.d(TAG, "Call initiated successfully."); + } else { + Log.e(TAG, "No app found to handle the call."); + } + } + + private void promptForInstall(Uri apkUri) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + reactContext.startActivity(intent); + } catch (Exception e) { + Log.e("ApkInstaller", "promptForInstall: Error installing APK", e); + } + } + + public void installApk(String filePath, Callback callback) { + try { + File apkFile = new File(filePath); + if (!isValidApkFile(filePath)) { + callback.invoke("Invalid or corrupted apk file", null); + return; + } + + Uri apkUri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + apkUri = FileProvider.getUriForFile(reactContext, BuildConfig.APPLICATION_ID + ".provider", apkFile); + } else { + apkUri = Uri.fromFile(apkFile); + } + promptForInstall(apkUri); + callback.invoke(null, "Success"); + } catch (Exception e) { + Log.e("ApkInstaller", "Error installing APK", e); + callback.invoke(e.toString(), null); + } + } + + @Override + public void onCatalystInstanceDestroy() { + if (installReceiver != null) { + reactContext.unregisterReceiver(installReceiver); + installReceiver = null; + } + super.onCatalystInstanceDestroy(); + } + +} diff --git a/android/app/src/main/java/com/avapp/callModule/CallModulePackage.java b/android/app/src/main/java/com/avapp/callModule/CallModulePackage.java new file mode 100644 index 00000000..ee905174 --- /dev/null +++ b/android/app/src/main/java/com/avapp/callModule/CallModulePackage.java @@ -0,0 +1,28 @@ +package com.avapp.callModule; + +import com.facebook.react.uimanager.ViewManager; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CallModulePackage implements ReactPackage { + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new CallModule(reactContext)); + + return modules; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/contentProvider/ContentProviderModule.java b/android/app/src/main/java/com/avapp/contentProvider/ContentProviderModule.java new file mode 100644 index 00000000..2269fe01 --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/ContentProviderModule.java @@ -0,0 +1,172 @@ +package com.avapp.contentProvider; + +import com.avapp.contentProvider.ackNumbers.AckNumbersEntity; +import com.avapp.contentProvider.primaryNumber.PrimaryNumberEntity; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import com.facebook.react.bridge.ReadableArray; + +public class ContentProviderModule extends ReactContextBaseJavaModule { + + private ReactApplicationContext reactApplicationContext; + private CosmosDatabase database; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + + public ContentProviderModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactApplicationContext = reactContext; + database = CosmosDatabase.getInstance(reactApplicationContext); + + } + + @Override + public String getName() { + return "ContentProviderModule"; + } + + @ReactMethod + public void getAckNumbers(Promise promise) { + ContentProviderThreadPool.getExecutorService().execute(() -> { + try { + + List ackNumbers = database.ackNumberDataDao().getAllData(); + WritableArray writableArray = Arguments.createArray(); + for (AckNumbersEntity entity : ackNumbers) { + WritableMap map = Arguments.createMap(); + map.putString("fromNumber", entity.fromNumber); + map.putString("toNumber", entity.toNumber); + map.putString("created_at", entity.createdAt+""); + map.putInt("id", entity.id); + writableArray.pushMap(map); + } + promise.resolve(writableArray); + } catch (Exception e) { + promise.reject(e); + } + }); + } + + @ReactMethod + public void deleteAckNumberById(Integer id, Promise promise) { + ContentProviderThreadPool.getExecutorService().execute(() -> { + try { + database.ackNumberDataDao().deleteDataById(id); + promise.resolve(true); + } catch (Exception e) { + promise.reject(e); + } + + }); + } + + @ReactMethod + public void addPhoneNumber(String phoneNumber, Promise promise) { + ContentProviderThreadPool.getExecutorService().execute(()->{ + try { + //check if entry already exist + PrimaryNumberEntity primaryNumberExist = database.primaryNumberDataDao().getDataByPhone(phoneNumber); + if(primaryNumberExist !=null) { + promise.resolve(phoneNumber); + return; + } + PrimaryNumberEntity primaryNumber = new PrimaryNumberEntity(); + primaryNumber.phoneNumber = phoneNumber; + database.primaryNumberDataDao().insert(primaryNumber); + promise.resolve(phoneNumber); + } + catch (Exception e) { + promise.resolve(e); + } + }); + } + + @ReactMethod + public void deletePhoneNumber(String phoneNumber, Promise promise) { + ContentProviderThreadPool.getExecutorService().execute(()->{ + try { + database.primaryNumberDataDao().deleteDataByPhone(phoneNumber); + promise.resolve(phoneNumber); + } + + catch (Exception e) { + promise.reject(e); + } + }); + } + + @ReactMethod + public void bulkAddPhoneNumbers(ReadableArray phoneNumbers, Promise promise) { + executorService.execute(() -> { + try { + for (int i = 0; i < phoneNumbers.size(); i++) { + String phoneNumber = phoneNumbers.getString(i); + PrimaryNumberEntity primaryNumberExist = database.primaryNumberDataDao().getDataByPhone(phoneNumber); + if(primaryNumberExist !=null) { + promise.resolve(phoneNumber); + return; + } + PrimaryNumberEntity primaryNumber = new PrimaryNumberEntity(); + primaryNumber.phoneNumber = phoneNumber; + database.primaryNumberDataDao().insert(primaryNumber); + } + promise.resolve(true); + } catch (Exception e) { + promise.reject(e); + } + }); + } + + @ReactMethod + public void bulkDeletePhoneNumbers(ReadableArray phoneNumbers, Promise promise) { + executorService.execute(() -> { + try { + for (int i = 0; i < phoneNumbers.size(); i++) { + String phoneNumber = phoneNumbers.getString(i); + database.primaryNumberDataDao().deleteDataByPhone(phoneNumber); + } + promise.resolve(true); + } catch (Exception e) { + promise.reject(e); + } + }); + } + + @ReactMethod + public void cleanTables(Promise promise) { + ContentProviderThreadPool.getExecutorService().execute(()->{ + try { + database.primaryNumberDataDao().deleteAllRows(); + database.ackNumberDataDao().deleteAllRows(); + promise.resolve("done"); + } + + catch (Exception e) { + promise.reject(e); + } + }); + } + @ReactMethod + public void getTableLength(Promise promise) { + ContentProviderThreadPool.getExecutorService().execute(()->{ + try { + + promise.resolve(database.primaryNumberDataDao().getTableLength()); + } + + catch (Exception e) { + promise.reject(e); + } + }); + } +} diff --git a/android/app/src/main/java/com/avapp/contentProvider/ContentProviderPackage.java b/android/app/src/main/java/com/avapp/contentProvider/ContentProviderPackage.java new file mode 100644 index 00000000..a3ef95c2 --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/ContentProviderPackage.java @@ -0,0 +1,27 @@ +package com.avapp.contentProvider; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ContentProviderPackage implements ReactPackage { + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new ContentProviderModule(reactContext)); + + return modules; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/contentProvider/ContentProviderThreadPool.java b/android/app/src/main/java/com/avapp/contentProvider/ContentProviderThreadPool.java new file mode 100644 index 00000000..4629244e --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/ContentProviderThreadPool.java @@ -0,0 +1,31 @@ +package com.avapp.contentProvider; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ContentProviderThreadPool { + + private static final int CORE_POOL_SIZE = 10; + private static final int MAX_POOL_SIZE = 20; + private static final long KEEP_ALIVE_TIME = 30L; + private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; + + private static final BlockingQueue workQueue = new LinkedBlockingQueue<>(); + + private static final ExecutorService executorService = new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + TIME_UNIT, + workQueue + ); + + public static ExecutorService getExecutorService() { + return executorService; + } +} + + diff --git a/android/app/src/main/java/com/avapp/contentProvider/CosmosDatabase.java b/android/app/src/main/java/com/avapp/contentProvider/CosmosDatabase.java new file mode 100644 index 00000000..78d04272 --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/CosmosDatabase.java @@ -0,0 +1,33 @@ +package com.avapp.contentProvider; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import android.content.Context; + +import com.avapp.contentProvider.ackNumbers.AckNumberDataDao; +import com.avapp.contentProvider.ackNumbers.AckNumbersEntity; +import com.avapp.contentProvider.primaryNumber.PrimaryNumberDataDao; +import com.avapp.contentProvider.primaryNumber.PrimaryNumberEntity; + +@Database(entities = {AckNumbersEntity.class, PrimaryNumberEntity.class}, version = 1) +public abstract class CosmosDatabase extends RoomDatabase { + public abstract AckNumberDataDao ackNumberDataDao(); + public abstract PrimaryNumberDataDao primaryNumberDataDao(); + + + private static volatile CosmosDatabase INSTANCE; + + public static CosmosDatabase getInstance(Context context) { + if (INSTANCE == null) { + synchronized (CosmosDatabase.class) { + if (INSTANCE == null) { + INSTANCE = Room.databaseBuilder(context.getApplicationContext(), + CosmosDatabase.class, "cosmos") + .build(); + } + } + } + return INSTANCE; + } +} diff --git a/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberContentProvider.java b/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberContentProvider.java new file mode 100644 index 00000000..e7c72ca3 --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberContentProvider.java @@ -0,0 +1,129 @@ +package com.avapp.contentProvider.ackNumbers; + + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; +import android.os.Looper; +import android.util.Log; + +import com.avapp.contentProvider.CosmosDatabase; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +public class AckNumberContentProvider extends ContentProvider { + + private static final int DATA = 100; + private static final int DATA_ID = 101; + + private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + static { + sUriMatcher.addURI(AckNumberContractProvider.AUTHORITY, AckNumberContractProvider.PATH_DATA, DATA); + sUriMatcher.addURI(AckNumberContractProvider.AUTHORITY, AckNumberContractProvider.PATH_DATA + "/#", DATA_ID); + } + + private CosmosDatabase database; + + @Override + public boolean onCreate() { + Log.d("ack number provider", ""); + database = CosmosDatabase.getInstance(getContext()); + + if (database == null) { + Log.e("AckNumber", "Failed to initialise database"); + return false; // Return false if the database could not be initialised + } + + Log.d("AckNumber", "Content Provider created and database initialised"); + + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + int match = sUriMatcher.match(uri); + Cursor cursor; + + switch (match) { + case DATA: + cursor = database.ackNumberDataDao().getAllDataCursor(); + break; + case DATA_ID: + int id = Integer.parseInt(uri.getLastPathSegment()); + cursor = database.ackNumberDataDao().getDataByIdCursor(id); + break; + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + Log.d("in insert",""); + int match = sUriMatcher.match(uri); + if (match == DATA) { + AckNumbersEntity data = new AckNumbersEntity(); + data.fromNumber = values.getAsString("fromPhoneNumber"); + data.toNumber = values.getAsString("toPhoneNumber"); + data.createdAt= System.currentTimeMillis(); + + long id = database.ackNumberDataDao().insert(data); + if (id == -1) { + throw new IllegalArgumentException("Failed to insert row into " + uri); + } + + getContext().getContentResolver().notifyChange(uri, null); + return Uri.withAppendedPath(uri, String.valueOf(id)); + } else { + throw new IllegalArgumentException("Insertion not supported for " + uri); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int match = sUriMatcher.match(uri); + if (match == DATA_ID) { + int id = Integer.parseInt(uri.getLastPathSegment()); + AckNumbersEntity data = database.ackNumberDataDao().getDataById(id); + if (data != null) { + return database.ackNumberDataDao().delete(data); + } + } + throw new IllegalArgumentException("Deletion not supported for " + uri); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int match = sUriMatcher.match(uri); + if (match == DATA_ID) { + int id = Integer.parseInt(uri.getLastPathSegment()); + AckNumbersEntity data = database.ackNumberDataDao().getDataById(id); + if (data != null) { + data.toNumber = values.getAsString("toPhoneNumber"); + data.fromNumber = values.getAsString("fromPhoneNumber"); + data.createdAt= System.currentTimeMillis(); + return database.ackNumberDataDao().update(data); + } + } + throw new IllegalArgumentException("Update not supported for " + uri); + } + + @Override + public String getType(Uri uri) { + int match = sUriMatcher.match(uri); + switch (match) { + case DATA: + return "vnd.android.cursor.dir/" + AckNumberContractProvider.AUTHORITY + "." + AckNumberContractProvider.PATH_DATA; + case DATA_ID: + return "vnd.android.cursor.item/" + AckNumberContractProvider.AUTHORITY + "." + AckNumberContractProvider.PATH_DATA; + default: + throw new IllegalStateException("Unknown URI: " + uri); + } + } +} diff --git a/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberContractProvider.java b/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberContractProvider.java new file mode 100644 index 00000000..2e5089c7 --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberContractProvider.java @@ -0,0 +1,21 @@ +package com.avapp.contentProvider.ackNumbers; + + +import android.net.Uri; +import android.provider.BaseColumns; + +public final class AckNumberContractProvider { + //com.avapp.ack.provider + public static final String AUTHORITY = "com.avapp.ackProvider"; + public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY); + + public static final String PATH_DATA = "data"; + + public static final class DataEntry implements BaseColumns { + public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_DATA); + + public static final String TABLE_NAME = "cosmos"; + public static final String COLUMN_NAME_FROM_NUMBER = "fromNumber"; + public static final String COLUMN_NAME_TO_NUMBER = "toNumber"; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberDataDao.java b/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberDataDao.java new file mode 100644 index 00000000..23a595f8 --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumberDataDao.java @@ -0,0 +1,52 @@ +package com.avapp.contentProvider.ackNumbers; + + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Update; +import androidx.room.Delete; + +import android.database.Cursor; + +import com.avapp.contentProvider.primaryNumber.PrimaryNumberEntity; + +import java.util.List; + +@Dao +public interface AckNumberDataDao { + @Insert + long insert(AckNumbersEntity data); + + @Query("SELECT * FROM ack") + List getAllData(); + + + @Query("SELECT * FROM ack WHERE id = :id") + AckNumbersEntity getDataById(int id); + + @Query("DELETE FROM ack WHERE id = :id") + int deleteDataById(int id); + + @Query("SELECT * FROM ack WHERE fromNumber= :fromPhone") + AckNumbersEntity getDataByFromPhone(String fromPhone); + + @Query("SELECT * FROM ack WHERE toNumber= :toPhone") + AckNumbersEntity getDataByToPhone(String toPhone); + + @Query("DELETE FROM ack") + void deleteAllRows(); + + @Update + int update(AckNumbersEntity data); + + @Delete + int delete(AckNumbersEntity data); + + // Custom methods to return Cursor for Content Provider + @Query("SELECT * FROM ack") + Cursor getAllDataCursor(); + + @Query("SELECT * FROM ack WHERE id = :id") + Cursor getDataByIdCursor(int id); +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumbersEntity.java b/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumbersEntity.java new file mode 100644 index 00000000..698b1b27 --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/ackNumbers/AckNumbersEntity.java @@ -0,0 +1,19 @@ +package com.avapp.contentProvider.ackNumbers; + +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +@Entity(tableName = "ack") +public class AckNumbersEntity { + @PrimaryKey(autoGenerate = true) + public int id; + + @ColumnInfo(name = "created_at") + public long createdAt; + + public String fromNumber; + public String toNumber; + + +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberContentProvider.java b/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberContentProvider.java new file mode 100644 index 00000000..98856cfe --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberContentProvider.java @@ -0,0 +1,110 @@ +package com.avapp.contentProvider.primaryNumber; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; + +import com.avapp.contentProvider.CosmosDatabase; + +public class PrimaryNumberContentProvider extends ContentProvider { + + private static final int DATA = 100; + private static final int DATA_ID = 101; + + private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + static { + sUriMatcher.addURI(PrimaryNumberContractProvider.AUTHORITY, PrimaryNumberContractProvider.PATH_DATA, DATA); + sUriMatcher.addURI(PrimaryNumberContractProvider.AUTHORITY, PrimaryNumberContractProvider.PATH_DATA + "/#", DATA_ID); + } + + private CosmosDatabase database; + + @Override + public boolean onCreate() { + database = CosmosDatabase.getInstance(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + int match = sUriMatcher.match(uri); + Cursor cursor; + + switch (match) { + case DATA: + cursor = database.primaryNumberDataDao().getAllDataCursor(); + break; + case DATA_ID: + String id = uri.getLastPathSegment(); + cursor = database.primaryNumberDataDao().getDataByPhoneCursor(id); + break; + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + int match = sUriMatcher.match(uri); + if (match == DATA) { + PrimaryNumberEntity data = new PrimaryNumberEntity(); + data.phoneNumber = values.getAsString("phoneNumber"); + + long id = database.primaryNumberDataDao().insert(data); + if (id == -1) { + throw new IllegalArgumentException("Failed to insert row into " + uri); + } + + getContext().getContentResolver().notifyChange(uri, null); + return Uri.withAppendedPath(uri, String.valueOf(id)); + } else { + throw new IllegalArgumentException("Insertion not supported for " + uri); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int match = sUriMatcher.match(uri); + if (match == DATA_ID) { + int id = Integer.parseInt(uri.getLastPathSegment()); + PrimaryNumberEntity data = database.primaryNumberDataDao().getDataById(id); + if (data != null) { + return database.primaryNumberDataDao().delete(data); + } + } + throw new IllegalArgumentException("Deletion not supported for " + uri); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int match = sUriMatcher.match(uri); + if (match == DATA_ID) { + int id = Integer.parseInt(uri.getLastPathSegment()); + PrimaryNumberEntity data = database.primaryNumberDataDao().getDataById(id); + if (data != null) { + data.phoneNumber = values.getAsString("phoneNumber"); + return database.primaryNumberDataDao().update(data); + } + } + throw new IllegalArgumentException("Update not supported for " + uri); + } + + @Override + public String getType(Uri uri) { + int match = sUriMatcher.match(uri); + switch (match) { + case DATA: + return "vnd.android.cursor.dir/" + PrimaryNumberContractProvider.AUTHORITY + "." + PrimaryNumberContractProvider.PATH_DATA; + case DATA_ID: + return "vnd.android.cursor.item/" + PrimaryNumberContractProvider.AUTHORITY + "." + PrimaryNumberContractProvider.PATH_DATA; + default: + throw new IllegalStateException("Unknown URI: " + uri); + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberContractProvider.java b/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberContractProvider.java new file mode 100644 index 00000000..28ca5185 --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberContractProvider.java @@ -0,0 +1,18 @@ +package com.avapp.contentProvider.primaryNumber; +import android.net.Uri; +import android.provider.BaseColumns; + + +public final class PrimaryNumberContractProvider { + public static final String AUTHORITY = "com.avapp.primaryNumberProvider";//"com.cosmos.myapp.provider"; + public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY); + + public static final String PATH_DATA = "data"; + + public static final class DataEntry implements BaseColumns { + public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_DATA); + + public static final String TABLE_NAME = "cosmos"; + public static final String COLUMN_NAME_TITLE = "phone"; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberDataDao.java b/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberDataDao.java new file mode 100644 index 00000000..261a20fc --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberDataDao.java @@ -0,0 +1,51 @@ +package com.avapp.contentProvider.primaryNumber; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Update; +import androidx.room.Delete; + +import android.database.Cursor; + +import java.util.List; + +@Dao +public interface PrimaryNumberDataDao { + @Insert + long insert(PrimaryNumberEntity data); + + @Query("SELECT * FROM `primary`") + List getAllData(); + + @Query("SELECT * FROM `primary` WHERE id = :id") + PrimaryNumberEntity getDataById(int id); + + @Query("SELECT * FROM `primary` WHERE phoneNumber = :phoneNumber LIMIT 1") + PrimaryNumberEntity getDataByPhone(String phoneNumber); + + @Query("SELECT * FROM `primary` WHERE phoneNumber = :phoneNumber LIMIT 1") + Cursor getDataByPhoneCursor(String phoneNumber); + + @Query("DELETE FROM `primary`") + void deleteAllRows(); + + @Query("SELECT DISTINCT COUNT(*) FROM `primary`") + int getTableLength(); + + @Update + int update(PrimaryNumberEntity data); + + @Delete + int delete(PrimaryNumberEntity data); + + @Query("DELETE FROM `primary` WHERE phoneNumber = :phoneNumber") + int deleteDataByPhone(String phoneNumber); + + // Custom methods to return Cursor for Content Provider + @Query("SELECT * FROM `primary`") + Cursor getAllDataCursor(); + + @Query("SELECT * FROM `primary` WHERE id = :id") + Cursor getDataByIdCursor(int id); +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberEntity.java b/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberEntity.java new file mode 100644 index 00000000..c3a2825b --- /dev/null +++ b/android/app/src/main/java/com/avapp/contentProvider/primaryNumber/PrimaryNumberEntity.java @@ -0,0 +1,11 @@ +package com.avapp.contentProvider.primaryNumber; +import androidx.room.PrimaryKey; +import androidx.room.Entity; + +@Entity(tableName = "primary") +public class PrimaryNumberEntity { + @PrimaryKey(autoGenerate = true) + public int id; + + public String phoneNumber; +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/sharedPreference/SharedPreferencesModule.java b/android/app/src/main/java/com/avapp/sharedPreference/SharedPreferencesModule.java new file mode 100644 index 00000000..41b54562 --- /dev/null +++ b/android/app/src/main/java/com/avapp/sharedPreference/SharedPreferencesModule.java @@ -0,0 +1,62 @@ +package com.avapp.sharedPreference; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +public class SharedPreferencesModule extends ReactContextBaseJavaModule { + + private static final String PREFS_NAME = "MyPrefs"; + private ReactApplicationContext reactApplicationContext; + private SharedPreferences sharedPreferences; + + public SharedPreferencesModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactApplicationContext = reactContext; + sharedPreferences = reactContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + @Override + public String getName() { + return "SharedPreferencesModule"; + } + + @ReactMethod + public void saveString(String key, String value) { + try { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(key, value); + Log.d("SharedPreferences", "SharedPreferences New Val" + value); + Log.d("SharedPreferences", "SharedPreferences editor" + editor); + editor.apply(); + } catch (Exception e) { + Log.d("SharedPreferences", "SharedPreferences Exception" + e); + } + } + + @ReactMethod + public void getString(String key, String defaultValue, Promise promise) { + try { + String value = sharedPreferences.getString(key, defaultValue); + promise.resolve(value); + } catch (Exception e) { + promise.reject(e); + } + } + @ReactMethod + public void removeKey(String key, Promise promise) { + try { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(key); + editor.apply(); + promise.resolve(null); + } catch (Exception e) { + promise.reject(e); + } + } +} diff --git a/android/app/src/main/java/com/avapp/sharedPreference/SharedPreferencesPackage.java b/android/app/src/main/java/com/avapp/sharedPreference/SharedPreferencesPackage.java new file mode 100644 index 00000000..5978222f --- /dev/null +++ b/android/app/src/main/java/com/avapp/sharedPreference/SharedPreferencesPackage.java @@ -0,0 +1,26 @@ +package com.avapp.sharedPreference; +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SharedPreferencesPackage implements ReactPackage { + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new SharedPreferencesModule(reactContext)); + + return modules; + } +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 8b1db5fa..553e95fb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -32,6 +32,8 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' classpath 'com.google.firebase:perf-plugin:1.4.2' + + } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 6ec1567a..09cc4436 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ +#Fri Feb 14 13:50:09 IST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip -networkTimeout=10000 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/buildFlavor/field/buildNumber.txt b/buildFlavor/field/buildNumber.txt index 7371afb0..f71beab7 100644 --- a/buildFlavor/field/buildNumber.txt +++ b/buildFlavor/field/buildNumber.txt @@ -1 +1 @@ -236 \ No newline at end of file +237 \ No newline at end of file diff --git a/buildFlavor/field/buildVersion.txt b/buildFlavor/field/buildVersion.txt index 5d3908e2..5b840738 100644 --- a/buildFlavor/field/buildVersion.txt +++ b/buildFlavor/field/buildVersion.txt @@ -1 +1 @@ -2.17.3 \ No newline at end of file +2.18.0 \ No newline at end of file diff --git a/buildFlavor/tele/buildNumber.txt b/buildFlavor/tele/buildNumber.txt index e26ed8de..0e92c3c0 100644 --- a/buildFlavor/tele/buildNumber.txt +++ b/buildFlavor/tele/buildNumber.txt @@ -1 +1 @@ -305 \ No newline at end of file +306 \ No newline at end of file diff --git a/buildFlavor/tele/buildVersion.txt b/buildFlavor/tele/buildVersion.txt index f193aa4d..9d3f2f5b 100644 --- a/buildFlavor/tele/buildVersion.txt +++ b/buildFlavor/tele/buildVersion.txt @@ -1 +1 @@ -100.2.1 \ No newline at end of file +100.2.2 \ No newline at end of file diff --git a/package.json b/package.json index cd775009..560dc634 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "AV_APP", - "version": "2.17.4", + "version": "2.18.0", "buildNumber": "237", "private": true, "scripts": { diff --git a/src/action/appDownloadAction.ts b/src/action/appDownloadAction.ts index 13338629..cd823d34 100644 --- a/src/action/appDownloadAction.ts +++ b/src/action/appDownloadAction.ts @@ -2,6 +2,7 @@ import { BuildFlavours, CLICKSTREAM_EVENT_NAMES } from '@common/Constants'; import { ApiKeys, getApiUrl } from '@components/utlis/apiHelper'; import { getBuildVersion } from '@components/utlis/commonFunctions'; import { logError } from '@components/utlis/errorUtils'; +import { IAppState } from '@reducers/metadataSlice'; import { addClickstreamEvent } from '@services/clickstreamEventService'; import axios from 'axios'; import { Linking, NativeModules } from 'react-native'; @@ -13,10 +14,12 @@ export const installApk = (filePath: string, callback: (error: string) => void) ApkInstaller?.installApk(filePath, callback); }; -export const deleteCachedApkFiles = async () => { +export const deleteCachedApkFiles = async (isDialerApp = false) => { try { const dirs = RNFetchBlob.fs.dirs; - const pathToSaveAPK = `${dirs.CacheDir}/latest-app/`; + const pathToSaveAPK = isDialerApp + ? `${dirs.CacheDir}/latest-dialer-app/` + : `${dirs.CacheDir}/latest-app/`; const doesExist = await RNFetchBlob.fs.exists(pathToSaveAPK); if (doesExist) { await RNFetchBlob.fs.unlink(pathToSaveAPK); @@ -26,10 +29,17 @@ export const deleteCachedApkFiles = async () => { } }; -export const downloadApkFromS3 = async (s3Url: string, fileName: string, newAppVersion: number) => { - deleteCachedApkFiles(); +export const downloadApkFromS3 = async ( + s3Url: string, + fileName: string, + newAppVersion: number, + isDialerApp = false +) => { + deleteCachedApkFiles(isDialerApp); const dirs = RNFetchBlob.fs.dirs; - const pathToSaveAPK = `${dirs.CacheDir}/latest-app/${fileName}.apk`; + const pathToSaveAPK = `${dirs.CacheDir}/${ + isDialerApp ? 'latest-dialer-app' : 'latest-app' + }/${fileName}.apk`; const oldAppVersion = getBuildVersion(); try { addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_DOWNLOAD_STARTED, { @@ -98,4 +108,22 @@ export const downloadLatestApkAndGetFilePath = async ( return ''; } return appFileUrl; -}; \ No newline at end of file +}; + +export const downloadLatestDialerApkAndGetFilePath = async (dialerAppState: IAppState) => { + const appUrl = dialerAppState?.currentProdAPK; + const appVersion = dialerAppState?.version; + if (!appUrl) { + return ''; + } + const appFileUrl = await downloadApkFromS3( + appUrl, + `Cosmos_Dialer_${Date.now()}`, + appVersion, + true + ); + if (!appFileUrl) { + return ''; + } + return appFileUrl; +}; diff --git a/src/assets/icons/DialerAppIcon.tsx b/src/assets/icons/DialerAppIcon.tsx new file mode 100644 index 00000000..928b0f55 --- /dev/null +++ b/src/assets/icons/DialerAppIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Path, Svg } from 'react-native-svg'; + +const DialerAppIcon = () => { + return ( + + + + ); +}; + +export default DialerAppIcon; diff --git a/src/common/AgentActivityConfigurableConstants.ts b/src/common/AgentActivityConfigurableConstants.ts index 582bf6cf..ef1c9751 100644 --- a/src/common/AgentActivityConfigurableConstants.ts +++ b/src/common/AgentActivityConfigurableConstants.ts @@ -1,13 +1,17 @@ -let ACTIVITY_TIME_ON_APP: number = 5; //5 seconds -let ACTIVITY_TIME_WINDOW_HIGH: number = 10; //10 minutes -let ACTIVITY_TIME_WINDOW_MEDIUM: number = 30; //30 minutes -let FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = 15; +import {GenericObject} from "@common/GenericTypes"; + +let ACTIVITY_TIME_ON_APP = 5; //5 seconds +let ACTIVITY_TIME_WINDOW_HIGH = 10; //10 minutes +let ACTIVITY_TIME_WINDOW_MEDIUM = 30; //30 minutes +let FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = 15; // 15 minutes let DATA_SYNC_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes let IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes let VIDEO_UPLOAD_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes let AUDIO_UPLOAD_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes let CALENDAR_AND_ACCOUNTS_UPLOAD_JOB_INTERVAL_IN_MINUTES = 720; // 12 hours let WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES = 30; // 30 minutes +let EXOTEL_NUMBER = ''; // fetched from firebase +let DIALER_APP_CONFIG: GenericObject = {}; // fetched from firebase export const getActivityTimeOnApp = () => ACTIVITY_TIME_ON_APP; export const getActivityTimeWindowHigh = () => ACTIVITY_TIME_WINDOW_HIGH; @@ -67,3 +71,18 @@ export const setWifiDetailsUploadJobIntervalInMinutes = ( ) => { WIFI_DETAILS_UPLOAD_JOB_INTERVAL_IN_MINUTES = wifiDetailsUploadJobIntervalInMinutes; }; + + +export const getExotelNumber = () => EXOTEL_NUMBER; + + +export const setExotelNumber = (exotelNumber: string) => { + EXOTEL_NUMBER = exotelNumber; +} + +export const setDialerAppConfig = (dialerAppConfig: string) => { + DIALER_APP_CONFIG = JSON.parse(dialerAppConfig) || {}; +} + +export const getDialerAppConfig = () => DIALER_APP_CONFIG; + diff --git a/src/common/BlockerScreen.tsx b/src/common/BlockerScreen.tsx index 64e7ee18..9aa91dcc 100644 --- a/src/common/BlockerScreen.tsx +++ b/src/common/BlockerScreen.tsx @@ -1,7 +1,7 @@ import React, { ReactNode, useCallback, useState } from 'react'; import { AppState, Linking } from 'react-native'; import { useSelector } from 'react-redux'; -import { RootState } from '../store/store'; +import store, { RootState } from '../store/store'; import { IAppState } from '../reducer/metadataSlice'; import { getBuildVersion } from '../components/utlis/commonFunctions'; import BlockerInstructions from './BlockerInstructions'; @@ -9,7 +9,7 @@ import { BLOCKER_SCREEN_DATA, BuildFlavours, CLICKSTREAM_EVENT_NAMES } from './C import { useAppDispatch, useAppSelector } from '../hooks'; import { setIsDeviceLocationEnabled } from '../reducer/foregroundServiceSlice'; import { toast } from '../../RN-UI-LIB/src/components/toast'; -import { locationEnabled } from '../components/utlis/DeviceUtils'; +import { getAppInfo, locationEnabled } from '../components/utlis/DeviceUtils'; import BlockerScreenApps from '@screens/permissions/BlockerScreenApps'; import handleBlacklistedAppsForBlockingCosmos, { Apps } from '@services/blacklistedApps.service'; import { addClickstreamEvent } from '@services/clickstreamEventService'; @@ -22,14 +22,20 @@ import { IUserRole } from '@reducers/userSlice'; import { deleteCachedApkFiles, downloadLatestApkAndGetFilePath, + downloadLatestDialerApkAndGetFilePath, installApk, openFallbackLonghornLink, } from '@actions/appDownloadAction'; import AppUpdate from './AppUpdate'; -import { setShouldUpdate } from '@reducers/appUpdateSlice'; +import { + IShouldDialerAppUpdate, + setShouldDialerAppUpdate, + setShouldUpdate, +} from '@reducers/appUpdateSlice'; import PostOperativeHours from './PostOperativeHours'; import { isFieldApp } from './utils'; import { AppStates } from '@interfaces/appStates'; +import DialerAppUpdate from './DialerAppUpdate'; interface IBlockerScreen { children?: ReactNode; @@ -42,11 +48,18 @@ const BlockerScreen = (props: IBlockerScreen) => { const { isTimeSynced, isDeviceLocationEnabled } = useAppSelector( (state) => state.foregroundService ); - const { isWifiOrCellularOn, appState } = useAppSelector((state) => state.metadata); + const appState = useAppSelector((state) => state.metadata?.appState); + const isWifiOrCellularOn = useAppSelector((state) => state.metadata?.isWifiOrCellularOn); + const dialerAppState = useAppSelector((state) => state.metadata?.dialerAppState); const approvalStatus = useAppSelector((state) => state.profile?.approvalStatus); const isLoading = useAppSelector((state) => state.profile?.isLoading); const roles = useAppSelector((state) => state.user?.agentRoles); const shouldUpdate = useAppSelector((state) => state.appUpdate.shouldUpdate) || {}; + const isCosmosDiallerEnabled = useAppSelector( + (state) => state?.user?.featureFlags?.isCosmosDiallerEnabled + ); + const shouldDialerAppUpdate = + useAppSelector((state) => state.appUpdate.shouldDialerAppUpdate) || {}; const withinOperativeHours = useAppSelector((state) => state.user?.withinOperativeHours); const isLoggedIn = useAppSelector((state: RootState) => state.user?.isLoggedIn); const isFieldAgent = @@ -81,6 +94,29 @@ const BlockerScreen = (props: IBlockerScreen) => { } }; + const downloadLatestDialerApp = async ({ + isAppInstalled = true, + dialerAppState, + }: { + isAppInstalled: boolean; + dialerAppState: IAppState; + }) => { + const apkFileUrl = await downloadLatestDialerApkAndGetFilePath(dialerAppState); + if (apkFileUrl) { + dispatch( + setShouldDialerAppUpdate({ + newApkCachedUrl: apkFileUrl, + switchToFallback: false, + isAppInstalled, + }) + ); + } else { + dispatch( + setShouldDialerAppUpdate({ newApkCachedUrl: '', switchToFallback: true, isAppInstalled }) + ); + } + }; + const handleAppUpdate = () => { let fallbackLonghornUrl; let oldAppVersion = getBuildVersion(); @@ -119,6 +155,20 @@ const BlockerScreen = (props: IBlockerScreen) => { }); }; + const handleDialerAppUpdate = async () => { + const url = dialerAppState?.currentProdAPK; + if (!shouldDialerAppUpdate.newApkCachedUrl) { + openFallbackLonghornLink(url); + return; + } + installApk(shouldDialerAppUpdate.newApkCachedUrl, (error) => { + if (!error) { + return; + } + openFallbackLonghornLink(url); + }); + }; + React.useEffect(() => { if (!appState) return; const buildToCompare = GLOBAL.BUILD_FLAVOUR; @@ -151,9 +201,56 @@ const BlockerScreen = (props: IBlockerScreen) => { } }, [appState]); + const checkAndUpdateDialerApp = React.useCallback( + async ( + dialerAppState: IAppState, + isCosmosDiallerEnabled: boolean, + shouldDialerAppUpdate: IShouldDialerAppUpdate + ) => { + if (!dialerAppState.currentProdAPK || !dialerAppState.version || !isCosmosDiallerEnabled) + return; + const updateVersion = dialerAppState?.version; + const currentDialerAppInfo = await getAppInfo('org.fossify.phone'); + const currentVersion = currentDialerAppInfo?.version; + + if (!currentVersion) { + downloadLatestDialerApp({ isAppInstalled: false, dialerAppState }); + return; + } + if ( + currentVersion && + updateVersion && + !isNaN(currentVersion) && + !isNaN(updateVersion) && + currentVersion < updateVersion + ) { + downloadLatestDialerApp({ isAppInstalled: true, dialerAppState }); + } else if (shouldDialerAppUpdate.newApkCachedUrl) { + dispatch( + setShouldDialerAppUpdate({ + newApkCachedUrl: '', + switchToFallback: false, + isAppInstalled: true, + }) + ); + deleteCachedApkFiles(true); + } + }, + [dialerAppState, shouldDialerAppUpdate, isCosmosDiallerEnabled] + ); + + React.useEffect(() => { + checkAndUpdateDialerApp(dialerAppState, isCosmosDiallerEnabled, shouldDialerAppUpdate); + }, [dialerAppState]); + React.useEffect(() => { const appStateChange = AppState.addEventListener('change', async (change) => { if (change === AppStates.ACTIVE) { + const isCosmosDiallerEnabled = + store?.getState()?.user?.featureFlags?.isCosmosDiallerEnabled; + const dialerAppState = store?.getState()?.metadata?.dialerAppState; + const shouldDialerAppUpdate = store?.getState()?.appUpdate?.shouldDialerAppUpdate; + checkAndUpdateDialerApp(dialerAppState, isCosmosDiallerEnabled, shouldDialerAppUpdate); setTimeout(async () => { handleBlacklistedAppsForBlockingCosmos().then((blacklistedAppsInstalled) => dispatch( @@ -194,6 +291,10 @@ const BlockerScreen = (props: IBlockerScreen) => { return ; } + if (shouldDialerAppUpdate.newApkCachedUrl && isCosmosDiallerEnabled) { + return ; + } + if (shouldUpdate.switchToFallback) { const { heading, instructions } = BLOCKER_SCREEN_DATA.UNINSTALL_APP; return ( diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 03a379f4..34f698a2 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -1456,6 +1456,18 @@ export const CLICKSTREAM_EVENT_NAMES = { name: 'FA_TRAINING_MATERIAL_PDF_PAGE_CHANGED', description: 'Training material PDF page changed', }, + FA_CALL_SYNC_SELF_CALL_SUCCESS: { + name: 'FA_CALL_SYNC_SELF_CALL_SUCCESS', + description: 'Call sync self call success', + }, + FA_CALL_SYNC_SELF_CALL_FAILURE: { + name: 'FA_CALL_SYNC_SELF_CALL_FAILURE', + description: 'Call sync self call failure', + }, + FA_CALL_SYNC_SELF_CALL_ERROR: { + name: 'FA_CALL_SYNC_SELF_CALL_ERROR', + description: 'Call sync self call error', + } } as const; export enum MimeType { @@ -1595,6 +1607,8 @@ export const HIT_SLOP = { right: 8, }; + +export const SELF_CALL_SHARED_PREFERENCE_KEY = 'selfCallSharedPreference'; export const LITMUS_URL = 'https://longhorn.navi.com/litmus'; export const API_ERROR_MESSAGE = 'Oops! something went wrong'; diff --git a/src/common/DialerAppUpdate.tsx b/src/common/DialerAppUpdate.tsx new file mode 100644 index 00000000..97cb7525 --- /dev/null +++ b/src/common/DialerAppUpdate.tsx @@ -0,0 +1,86 @@ +import { Linking, Pressable, StyleSheet, View } from 'react-native'; +import React from 'react'; +import Text from '@rn-ui-lib/components/Text'; +import Layout from '@screens/layout/Layout'; +import { GenericStyles } from '@rn-ui-lib/styles'; +import NavigationHeader from '@rn-ui-lib/components/NavigationHeader'; +import Button from '@rn-ui-lib/components/Button'; +import { useAppSelector } from '@hooks'; +import DialerAppIcon from '@assets/icons/DialerAppIcon'; +import { COLORS } from '@rn-ui-lib/colors'; + +interface IDialerAppUpdate { + onAppUpdate: () => void; +} + +const DialerAppUpdate: React.FC = ({ onAppUpdate }) => { + const shouldDialerAppUpdate = + useAppSelector((state) => state.appUpdate.shouldDialerAppUpdate) || {}; + const isAppInstalled = shouldDialerAppUpdate?.isAppInstalled; + + const openHelpForm = () => { + Linking.openURL( + 'https://docs.google.com/forms/d/e/1FAIpQLSdKtdzB67-yyidd2Gh_52fYsLtL9QeuTmkUb6BZt4fAPJGyOg/viewform?usp=dialog' + ); + }; + + return ( + + + + Help + + } + /> + {isAppInstalled ? ( + + + + We are better than ever! + + + We have launched a new and improved version of cosmos dialer. Please update the cosmos + dialer. + + + ) : ( + + + + Cosmos dialer + + + We have launched a new cosmos dialer. Please download the cosmos dialer to use cosmos. + + + )} + +