NTP-57687 | Yash | Call Logs native module + Sim Details (#1143)

This commit is contained in:
Mantri Ramkishor
2025-05-21 17:01:16 +05:30
committed by GitHub
parent 83671db125
commit 6975b5d0ca
13 changed files with 414 additions and 24 deletions

View File

@@ -21,7 +21,7 @@ import { hydrateGlobalImageMap } from '@common/CachedImage';
import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from '@common/Constants';
import { getAppVersion, sendDeviceDetailsToClickstream } from '@components/utlis/commonFunctions';
import { linkingConf } from '@components/utlis/deeplinkingUtils';
import { getBuildFlavour, restartApp } from '@components/utlis/DeviceUtils';
import { fetchSimDetails, getBuildFlavour, restartApp } from '@components/utlis/DeviceUtils';
import { initSentry } from '@components/utlis/sentry';
import { GLOBAL, setGlobalBuildFlavour } from '@constants/Global';
import { AppStates } from '@interfaces/appStates';
@@ -141,6 +141,7 @@ function App() {
});
// Device Details
sendDeviceDetailsToClickstream();
fetchSimDetails();
}, []);
React.useEffect(() => {

View File

@@ -113,8 +113,8 @@ def jscFlavor = 'org.webkit:android-jsc:+'
def enableHermes = project.ext.react.get("enableHermes", false);
def VERSION_CODE = 263
def VERSION_NAME = "2.19.4"
def VERSION_CODE = 264
def VERSION_NAME = "2.19.5"
android {
namespace "com.avapp"

View File

@@ -11,12 +11,14 @@ import android.app.Application;
import android.content.Context;
import com.avapp.appInstallerModule.ApkInstallerPackage;
import com.avapp.callLogs.CallLogsModulePackage;
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.simDetails.SimDetailsModulePackage;
import com.avapp.utils.FirebaseRemoteConfigHelper;
import com.avapp.wifiDetailsModule.WifiDetailsModulePackage;
import com.avapp.restartApp.RestartPackage;
@@ -76,6 +78,8 @@ public class MainApplication extends Application implements ReactApplication, Ap
packages.add(new CallModulePackage());
packages.add(new SharedPreferencesPackage());
packages.add(new ContentProviderPackage());
packages.add(new SimDetailsModulePackage());
packages.add(new CallLogsModulePackage());
return packages;
}

View File

@@ -0,0 +1,210 @@
package com.avapp.callLogs;
import android.Manifest;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.CallLog;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
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.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import org.json.JSONArray;
import org.json.JSONException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CallLogsModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
public CallLogsModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "CallLogsDetails";
}
@ReactMethod
public void loadAll(Promise promise) {
load(-1, promise);
}
@ReactMethod
public void load(int limit, Promise promise) {
loadWithFilter(limit, null, promise);
}
@ReactMethod
public void loadWithFilter(int limit, @Nullable ReadableMap filter, Promise promise) {
if (ContextCompat.checkSelfPermission(reactContext, Manifest.permission.READ_CALL_LOG)
!= PackageManager.PERMISSION_GRANTED) {
promise.reject("PERMISSION_DENIED", "READ_CALL_LOG permission not granted");
return;
}
WritableArray result = Arguments.createArray();
Cursor cursor = null;
String selection = "";
List<String> selectionArgsList = new ArrayList<>();
boolean nullFilter = filter == null;
if (!nullFilter && filter.hasKey("minTimestamp")) {
selection += CallLog.Calls.DATE + " >= ?";
selectionArgsList.add(filter.getString("minTimestamp"));
}
if (!nullFilter && filter.hasKey("maxTimestamp")) {
if (!selection.isEmpty()) selection += " AND ";
selection += CallLog.Calls.DATE + " <= ?";
selectionArgsList.add(filter.getString("maxTimestamp"));
}
try {
String[] selectionArgs = selectionArgsList.toArray(new String[0]);
cursor = reactContext.getContentResolver().query(
CallLog.Calls.CONTENT_URI,
null,
selection.isEmpty() ? null : selection,
selectionArgs,
CallLog.Calls.DATE + " DESC"
);
if (cursor == null) {
promise.resolve(result);
return;
}
int callLogCount = 0;
final int NUMBER_COLUMN_INDEX = cursor.getColumnIndex(CallLog.Calls.NUMBER);
final int TYPE_COLUMN_INDEX = cursor.getColumnIndex(CallLog.Calls.TYPE);
final int DATE_COLUMN_INDEX = cursor.getColumnIndex(CallLog.Calls.DATE);
final int DURATION_COLUMN_INDEX = cursor.getColumnIndex(CallLog.Calls.DURATION);
final int NAME_COLUMN_INDEX = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME);
final int FEATURES_COLUMN_INDEX = cursor.getColumnIndex(CallLog.Calls.FEATURES);
while (cursor.moveToNext() && shouldContinue(limit, callLogCount)) {
String phoneNumber = cursor.getString(NUMBER_COLUMN_INDEX);
int duration = cursor.getInt(DURATION_COLUMN_INDEX);
String name = cursor.getString(NAME_COLUMN_INDEX);
String timestampStr = cursor.getString(DATE_COLUMN_INDEX);
long timestamp = safeParseLong(timestampStr, 0);
boolean featuresAvailable = FEATURES_COLUMN_INDEX != -1;
int features = featuresAvailable ? cursor.getInt(FEATURES_COLUMN_INDEX) : 0;
String isVideoCall = "undetected";
String isWiFiCall = "undetected";
String isHDCall = "undetected";
String isPulledExternally = "undetected";
String isVolte = "undetected";
if(featuresAvailable) {
isVideoCall = String.valueOf((features & CallLog.Calls.FEATURES_VIDEO) != 0);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
isWiFiCall = String.valueOf((features & CallLog.Calls.FEATURES_WIFI) != 0);
isHDCall = String.valueOf((features & CallLog.Calls.FEATURES_HD_CALL) != 0);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
isPulledExternally = String.valueOf((features & CallLog.Calls.FEATURES_PULLED_EXTERNALLY) != 0);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
isVolte = String.valueOf((features & CallLog.Calls.FEATURES_VOLTE) != 0);
}
}
DateFormat df = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.MEDIUM, SimpleDateFormat.MEDIUM);
String dateTime = df.format(new Date(timestamp));
String type = resolveCallType(cursor.getInt(TYPE_COLUMN_INDEX));
WritableMap callLog = Arguments.createMap();
callLog.putString("phoneNumber", phoneNumber);
callLog.putInt("duration", duration);
callLog.putString("name", name);
callLog.putString("timestamp", timestampStr);
callLog.putString("dateTime", dateTime);
callLog.putString("type", type);
callLog.putString("isVideoCall", isVideoCall);
callLog.putString("isWiFiCall", isWiFiCall);
callLog.putString("isHDCall", isHDCall);
callLog.putString("isPulledExternally", isPulledExternally);
callLog.putString("isVolte", isVolte);
callLog.putInt("rawType", cursor.getInt(TYPE_COLUMN_INDEX));
result.pushMap(callLog);
callLogCount++;
}
promise.resolve(result);
} catch (Exception e) {
promise.reject("CALL_LOG_ERROR", "Unexpected error while loading call logs", e);
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
}
private long safeParseLong(String value, long defaultValue) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
public static String[] toStringArray(JSONArray array) {
if (array == null)
return null;
String[] arr = new String[array.length()];
for (int i = 0; i < arr.length; i++) {
arr[i] = array.optString(i);
}
return arr;
}
private String resolveCallType(int callTypeCode) {
switch (callTypeCode) {
case CallLog.Calls.OUTGOING_TYPE:
return "OUTGOING";
case CallLog.Calls.INCOMING_TYPE:
return "INCOMING";
case CallLog.Calls.MISSED_TYPE:
return "MISSED";
case CallLog.Calls.VOICEMAIL_TYPE:
return "VOICEMAIL";
case CallLog.Calls.REJECTED_TYPE:
return "REJECTED";
case CallLog.Calls.BLOCKED_TYPE:
return "BLOCKED";
case CallLog.Calls.ANSWERED_EXTERNALLY_TYPE:
return "ANSWERED_EXTERNALLY";
default:
return "UNKNOWN";
}
}
private boolean shouldContinue(int limit, int count) {
return limit < 0 || count < limit;
}
}

View File

@@ -0,0 +1,29 @@
package com.avapp.callLogs;
import androidx.annotation.NonNull;
import com.avapp.simDetails.SimDetailsModule;
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 CallLogsModulePackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CallLogsModule(reactContext));
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,78 @@
package com.avapp.simDetails;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import androidx.core.app.ActivityCompat;
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 com.facebook.react.bridge.Arguments;
import java.util.List;
public class SimDetailsModule extends ReactContextBaseJavaModule {
ReactApplicationContext reactContext;
public SimDetailsModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "SimDetails";
}
@ReactMethod
public void getSimDetails(Promise promise) {
try {
WritableMap result = Arguments.createMap();
boolean hasPermission = ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED;
result.putBoolean("hasPermission", hasPermission);
WritableArray simsArray = Arguments.createArray();
if (hasPermission && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
SubscriptionManager manager = (SubscriptionManager) reactContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
TelephonyManager telManager = (TelephonyManager) reactContext.getSystemService(Context.TELEPHONY_SERVICE);
List<SubscriptionInfo> subscriptionInfos = manager.getActiveSubscriptionInfoList();
if (subscriptionInfos != null) {
for (SubscriptionInfo subInfo : subscriptionInfos) {
WritableMap simInfo = Arguments.createMap();
simInfo.putString("carrierName", subInfo.getCarrierName().toString());
simInfo.putString("displayName", subInfo.getDisplayName().toString());
simInfo.putString("countryCode", subInfo.getCountryIso());
simInfo.putInt("mcc", subInfo.getMcc());
simInfo.putInt("mnc", subInfo.getMnc());
simInfo.putBoolean("isNetworkRoaming", telManager.isNetworkRoaming());
simInfo.putBoolean("isDataRoaming", subInfo.getDataRoaming() == 1);
simInfo.putInt("simSlotIndex", subInfo.getSimSlotIndex());
simInfo.putString("phoneNumber", subInfo.getNumber());
simInfo.putString("simSerialNumber", subInfo.getIccId());
simInfo.putInt("subscriptionId", subInfo.getSubscriptionId());
simsArray.pushMap(simInfo);
}
}
}
result.putArray("sims", simsArray);
promise.resolve(result);
} catch (Exception e) {
promise.reject("SIM_ERROR", "Failed to get SIM details", e);
}
}
}

View File

@@ -0,0 +1,29 @@
package com.avapp.simDetails;
import androidx.annotation.NonNull;
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 SimDetailsModulePackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new SimDetailsModule(reactContext));
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -1 +1 @@
263
264

View File

@@ -1 +1 @@
2.19.4
2.19.5

View File

@@ -1,7 +1,7 @@
{
"name": "AV_APP",
"version": "2.19.4",
"buildNumber": "263",
"version": "2.19.5",
"buildNumber": "264",
"private": true,
"scripts": {
"android:dev": "yarn move:dev && react-native run-android",

View File

@@ -1644,6 +1644,11 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_MAP_VIEW_EXPERIMENT_ERROR',
description: 'Map view fetch experiment error',
},
FA_FETCH_SIM_DETAILS: {
name: 'FA_FETCH_SIM_DETAILS',
description: 'Fetch SIM details',
}
} as const;
export enum MimeType {

View File

@@ -1,12 +1,14 @@
import { NativeModules } from 'react-native';
import {GenericObject} from "@common/GenericTypes";
import { GenericObject } from '@common/GenericTypes';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { addClickstreamEvent } from '@services/clickstreamEventService';
interface CrashError {
message: string;
screenName: string;
}
const { DeviceUtilsModule, RNRestart } = NativeModules; // this is the same name we returned in getName function.
const { DeviceUtilsModule, RNRestart, SimDetails } = NativeModules; // this is the same name we returned in getName function.
export const restartJSBundle = () => RNRestart.restart();
@@ -27,7 +29,9 @@ export type buildConfig = {
export const getBuildInfo = (): Promise<string> => DeviceUtilsModule.getBuildInfo();
export const getBlacklistedApps = (blacklistedAppsObject: Record<string, boolean>): Promise<string> => DeviceUtilsModule.getBlacklistedApps(blacklistedAppsObject);
export const getBlacklistedApps = (
blacklistedAppsObject: Record<string, boolean>
): Promise<string> => DeviceUtilsModule.getBlacklistedApps(blacklistedAppsObject);
export const alfredHandleSWWEvent = (error: Error) => {
const { message = '', stack = '', name = '' } = error;
@@ -72,8 +76,20 @@ export const sendContentToWhatsapp = (
export const isAppInstalled = (packageName: string): Promise<boolean> =>
DeviceUtilsModule.isAppInstalled(packageName);
export const getAppInfo = (packageName: string): Promise<GenericObject> => DeviceUtilsModule.getAppInfo(packageName);
export const getAppInfo = (packageName: string): Promise<GenericObject> =>
DeviceUtilsModule.getAppInfo(packageName);
export const getAppInfoFromFilePath = (filePath: string): Promise<GenericObject> => DeviceUtilsModule.getAppInfoFromFilePath(filePath);
export const getAppInfoFromFilePath = (filePath: string): Promise<GenericObject> =>
DeviceUtilsModule.getAppInfoFromFilePath(filePath);
export const restartApp = ():Promise<boolean> => DeviceUtilsModule.restartApp();
export const restartApp = (): Promise<boolean> => DeviceUtilsModule.restartApp();
export const fetchSimDetails = async () => {
try {
const details = await SimDetails.getSimDetails();
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FETCH_SIM_DETAILS, { details });
return details;
} catch (err) {
return null;
}
};

View File

@@ -1,20 +1,14 @@
// @ts-expect-error
import CallLogs from 'react-native-call-log'; // package does not have typescript implementation, used any type here
import { DATA_SYNC_ENUM } from './dataSync.service';
import { getGzipData, getMaxByPropFromList } from '../components/utlis/commonFunctions';
import { API_STATUS_CODE } from '../components/utlis/apiHelper';
import { logError } from '../components/utlis/errorUtils';
import { Permission, PermissionsAndroid } from 'react-native';
import { NativeModules, Permission, PermissionsAndroid } from 'react-native';
import { addClickstreamEvent } from './clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import axios from 'axios';
const MAXIMUM_NUMBER_CALL_LOGS = 1000;
const callLogFilter = {
minTimestamp: 0,
};
enum CallTypeEnum {
OUTGOING = 'OUTGOING',
INCOMING = 'INCOMING',
@@ -34,6 +28,11 @@ interface ICallLogs {
rawType: number;
timestamp: string;
type: CallTypeEnum;
isHDCall: boolean;
isPulledExternally: boolean;
isVideoCall: boolean;
isVolte: boolean;
isWiFiCall: boolean;
}
interface ICallLogsDetails {
@@ -42,6 +41,11 @@ interface ICallLogsDetails {
duration?: number;
name?: string;
type?: CallTypeEnum;
isHDCall?: boolean;
isPulledExternally?: boolean;
isVideoCall?: boolean;
isVolte?: boolean;
isWiFiCall?: boolean;
}
interface ICallLogsDataPayload {
@@ -71,15 +75,22 @@ export const callLogSyncServiceTele = async (url: string, syncFrom: string) => {
const isCallLogsReadPermissionGranted = await checkReadPermissions(
PermissionsAndroid.PERMISSIONS.READ_CALL_LOG
);
const { CallLogsDetails } = NativeModules;
if (isCallLogsReadPermissionGranted) {
return new Promise((resolve, reject) => {
CallLogs.loadAll().then(async (callLogJson: ICallLogs[]) => {
CallLogsDetails.loadAll().then(async (callLogJson: ICallLogs[]) => {
const callLogsDetailsList: ICallLogsDetails[] = callLogJson.map((callLogItem) => ({
phoneNumber: callLogItem.phoneNumber,
timestamp: Number.parseFloat(callLogItem.timestamp),
duration: callLogItem.duration,
name: callLogItem.name,
type: callLogItem.type,
isHDCall: callLogItem.isHDCall,
isPulledExternally: callLogItem.isPulledExternally,
isVideoCall: callLogItem.isVideoCall,
isVolte: callLogItem.isVolte,
isWiFiCall: callLogItem.isWiFiCall,
}));
const maxCallLogsTimeStamp = getMaxByPropFromList(
@@ -118,11 +129,13 @@ export const callLogSyncService = async (url: string, syncFrom: string) => {
const isCallLogsReadPermissionGranted = await checkReadPermissions(
PermissionsAndroid.PERMISSIONS.READ_CALL_LOG
);
const { CallLogsDetails } = NativeModules;
if (isCallLogsReadPermissionGranted) {
return new Promise((resolve, reject) => {
if (syncFrom) callLogFilter.minTimestamp = parseFloat(syncFrom);
CallLogs.load(MAXIMUM_NUMBER_CALL_LOGS, callLogFilter).then(
CallLogsDetails.loadWithFilter(MAXIMUM_NUMBER_CALL_LOGS, {
minTimestamp: syncFrom?.toString() || '0',
}).then(
async (callLogJson: Array<ICallLogs>) => {
const callLogsDetailsList: ICallLogsDetails[] = callLogJson.map((callLogItem) => ({
phoneNumber: callLogItem.phoneNumber,
@@ -130,6 +143,11 @@ export const callLogSyncService = async (url: string, syncFrom: string) => {
duration: callLogItem.duration,
name: callLogItem.name,
type: callLogItem.type,
isHDCall: callLogItem.isHDCall,
isPulledExternally: callLogItem.isPulledExternally,
isVideoCall: callLogItem.isVideoCall,
isVolte: callLogItem.isVolte,
isWiFiCall: callLogItem.isWiFiCall,
}));
const maxCallLogsTimeStamp = getMaxByPropFromList(