Commit 4e5308fd authored by Wei Han's avatar Wei Han

prototype done

parent 9892c2bf
{
"java.configuration.updateBuildConfiguration": "interactive"
}
\ No newline at end of file
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import { NewAppScreen } from '@react-native/new-app-screen';
import { StatusBar, StyleSheet, useColorScheme, View } from 'react-native';
import {
SafeAreaProvider,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
function App() {
const isDarkMode = useColorScheme() === 'dark';
import React, { useEffect, useState, useCallback } from 'react';
import { View, Text, Button, StyleSheet, Alert, Linking, Platform, NativeModules } from 'react-native';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import AsyncStorage from '@react-native-async-storage/async-storage';
import DeviceInfo from 'react-native-device-info';
return (
<SafeAreaProvider>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<AppContent />
</SafeAreaProvider>
);
const permissionsList = [
{ key: 'READ_PHONE_STATE', android: PERMISSIONS.ANDROID.READ_PHONE_STATE },
{ key: 'READ_CALL_LOG', android: PERMISSIONS.ANDROID.READ_CALL_LOG },
{ key: 'Display Over Other Apps', android: null }, // overlay special case
];
async function saveVIPNumber(name: string, number: string) {
try {
await AsyncStorage.setItem('vipName', name);
await AsyncStorage.setItem('vipNumber', number);
const {VIPStorage} = NativeModules;
if (!VIPStorage) {
console.error("❌ VIPStorage is undefined. Did you register StoragePackage in MainApplication?");
return;
}
await VIPStorage.saveVIP(name, number);
console.log('VIPStorage from NativeModules:', NativeModules.VIPStorage);
} catch (error) {
console.error('Failed to save VIP info:', error);
}
}
function AppContent() {
const safeAreaInsets = useSafeAreaInsets();
export default function App() {
const [statuses, setStatuses] = useState<Record<string, string>>({});
const checkAllPermissions = useCallback(async () => {
const newStatuses: Record<string, string> = {};
for (const perm of permissionsList) {
if (perm.key === 'Display Over Other Apps') {
// Overlay not checkable in RN
newStatuses[perm.key] = RESULTS.DENIED;
continue;
}
const result = await check(perm.android!);
newStatuses[perm.key] = result;
}
setStatuses(newStatuses);
for (const perm of permissionsList) {
if (newStatuses[perm.key] !== RESULTS.GRANTED) {
if (perm.key === 'Display Over Other Apps') {
Alert.alert(
'Overlay Permission Required',
'This app needs permission to display over other apps.',
[{ text: 'Open Settings', onPress: openOverlaySettings }]
);
} else {
const result = await request(perm.android!);
setStatuses(prev => ({ ...prev, [perm.key]: result }));
}
}
}
},[]);
useEffect(() => {
console.log("PermissionsScreen mounted");
console.log("All NativeModules keys:", Object.keys(NativeModules));
console.log("VIPStorage:", NativeModules.VIPStorage);
saveVIPNumber('VIP User', '+60123456789');
checkAllPermissions();
},[checkAllPermissions]);
// Open Overlay Settings
const openOverlaySettings = async () => {
if (Platform.OS === 'android') {
const pkg = DeviceInfo.getBundleId(); // gets your app package name
const url = `package:${pkg}`;
try {
await Linking.openSettings(); // fallback
await Linking.openURL(`android.settings.action.MANAGE_OVERLAY_PERMISSION/${url}`);
} catch (err) {
console.warn('Failed to open overlay settings:', err);
}
}
};
const allGranted = Object.values(statuses).every(s => s === RESULTS.GRANTED);
return (
<View style={styles.container}>
<NewAppScreen
templateFileName="App.tsx"
safeAreaInsets={safeAreaInsets}
<Text style={styles.title}>Permissions Status</Text>
{permissionsList.map(perm => (
<View key={perm.key} style={styles.row}>
<Text style={styles.text}>{perm.key}</Text>
<Text
style={[
styles.text,
{ color: statuses[perm.key] === RESULTS.GRANTED ? 'green' : 'red' },
]}>
{statuses[perm.key] || 'Checking...'}
</Text>
</View>
))}
{allGranted && <Text style={styles.granted}>All permissions granted ✅</Text>}
<Button title="Check Again" onPress={checkAllPermissions} />
<Button
title="Simulate VIP Call"
onPress={async () => {
const number = '+60123456789';
const name = 'VIP User';
// Save VIP info in AsyncStorage + native storage
await saveVIPNumber(name, number);
// Call native module to start OverlayService (simulate incoming call)
if (NativeModules.VIPStorage?.simulateVipCall) {
NativeModules.VIPStorage.simulateVipCall(number, name);
} else {
console.warn('simulateVipCall not implemented in NativeModules.');
}
Alert.alert('Simulated VIP call', `${name} (${number})`);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
container: { flex: 1, justifyContent: 'center', padding: 20, backgroundColor: 'white' },
title: { fontSize: 24, marginBottom: 20, textAlign: 'center' },
row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 },
text: { fontSize: 16 },
granted: { fontSize: 18, color: 'green', textAlign: 'center', marginTop: 20 },
});
export default App;
......@@ -105,6 +105,9 @@ android {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
buildFeatures {
viewBinding = true
}
}
dependencies {
......@@ -116,4 +119,9 @@ dependencies {
} else {
implementation jscFlavor
}
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.activity:activity-ktx:1.9.2")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.whoscall">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:name=".MainApplication"
......@@ -10,6 +16,21 @@
android:allowBackup="false"
android:theme="@style/AppTheme"
android:supportsRtl="true">
<service
android:name=".IncomingCallService"
android:permission="android.permission.BIND_SCREENING_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.CallScreeningService" />
</intent-filter>
</service>
<service
android:name=".OverlayService"
android:exported="false"
android:foregroundServiceType="phoneCall|mediaProjection"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
......@@ -22,5 +43,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
package com.whoscall
import android.content.Intent
import android.os.Build
import android.telecom.CallScreeningService
import android.telecom.Call.Details
import androidx.annotation.RequiresApi
import android.util.Log
import android.content.Context
import android.content.SharedPreferences
@RequiresApi(Build.VERSION_CODES.N)
class IncomingCallService : CallScreeningService() {
override fun onScreenCall(callDetails: Details) {
val number = callDetails.handle?.schemeSpecificPart ?: "Unknown"
// Lookup VIPs
val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE)
val vipNumber = "+60108222562"
val vipName = "Arvin"
Log.d("IncomingCallReceiver", "Looking up number: $number, Found VIPNumber: $vipNumber, Found VIPName: $vipName")
// Only show overlay if VIP
if (!vipNumber.isNullOrEmpty() && number.contains(vipNumber)) {
val overlayIntent = Intent(this, OverlayService::class.java).apply {
putExtra("number", number)
putExtra("displayName", vipName)
}
startService(overlayIntent)
} else {
val overlayIntent = Intent(this, OverlayService::class.java).apply {
putExtra("number", number)
putExtra("displayName", "Unknown Caller")
}
startService(overlayIntent)
}
// Let the call through without blocking
respondToCall(callDetails, CallResponse.Builder().build())
}
}
package com.whoscall
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "whoscall"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.app.role.RoleManager
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import com.whoscall.databinding.ActivityMainBinding
import android.content.SharedPreferences
import android.util.Log
class MainActivity : ComponentActivity() {
private lateinit var binding: ActivityMainBinding
private val roleLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { /* no-op */ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnOverlay.setOnClickListener { requestOverlay() }
binding.btnRole.setOnClickListener { requestCallScreeningRoleIfNeeded() }
}
private fun requestOverlay() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
!Settings.canDrawOverlays(this)) {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.fromParts("package", packageName, null)
)
roleLauncher.launch(intent)
}
}
private fun requestCallScreeningRoleIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_CALL_SCREENING)
roleLauncher.launch(intent)
}
}
private fun simulateVipCall() {
// Example VIP number and name
val vipNumber = "+60189884118"
val vipName = "Wei Yi"
// Save to SharedPreferences
val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE)
prefs.edit().putString(vipNumber, vipName).apply()
// Start overlay service directly
val overlayIntent = Intent(this, OverlayService::class.java).apply {
putExtra("number", vipNumber)
putExtra("displayName", vipName)
}
startService(overlayIntent)
}
}
......@@ -18,6 +18,7 @@ class MainApplication : Application(), ReactApplication {
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(StoragePackage())
}
override fun getJSMainModuleName(): String = "index"
......
package com.whoscall
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import android.content.Context
import android.content.SharedPreferences
class OverlayService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val number = intent?.getStringExtra("number") ?: ""
val displayName = intent?.getStringExtra("displayName") ?: number
Log.d("OverlayService", "Showing overlay for $number")
// Show overlay using your existing singleton
OverlayWindow.show(this, displayName, number)
return START_NOT_STICKY
}
override fun onDestroy() {
OverlayWindow.close()
super.onDestroy()
}
}
package com.whoscall
import android.content.Context
import android.graphics.PixelFormat
import android.os.Build
import android.util.DisplayMetrics
import android.view.*
import android.widget.TextView
import com.whoscall.R
import android.widget.ImageView
import android.content.SharedPreferences
import android.util.Log
object OverlayWindow {
private var windowManager: WindowManager? = null
private var layout: ViewGroup? = null
fun show(context: Context, displayName: String, number: String) {
if (layout != null) return
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
layout = LayoutInflater.from(context).inflate(R.layout.window_call_info, null) as ViewGroup
val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
WindowManager.LayoutParams.TYPE_PHONE
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
type,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.CENTER
format = 1
}
// Width calculation: Android 11+ vs legacy
params.width = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val wm = windowManager?.currentWindowMetrics
val insets = wm?.windowInsets?.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
if (wm != null && insets != null) {
(0.8 * (wm.bounds.width() - insets.left - insets.right)).toInt()
} else {
params.width
}
} else {
val metrics = DisplayMetrics()
@Suppress("DEPRECATION")
windowManager?.defaultDisplay?.getMetrics(metrics)
(0.8 * metrics.widthPixels.toDouble()).toInt()
}
// 🔹 Lookup stored name for number
// val prefs = context.getSharedPreferences("WhoscallPrefs", Context.MODE_PRIVATE)
// val savedNumber = prefs.getString("vipNumber", null)
// val savedName = prefs.getString(savedNumber, null)
Log.d("OverlayWindow", "savedName: $displayName, number: $number")
val nameView = layout?.findViewById<TextView>(R.id.name)
val numberView = layout?.findViewById<TextView>(R.id.number)
nameView?.text = displayName
numberView?.text = number
// } else {
// nameView?.text = "Unknown Caller"
// numberView?.text = number
// }
val closeBtn = layout?.findViewById<ImageView>(R.id.btnClose)
closeBtn?.setOnClickListener {
close()
}
windowManager?.addView(layout, params)
}
fun close() {
layout?.let { windowManager?.removeView(it) }
layout = null
windowManager = null
}
}
package com.whoscall
import android.content.Context
import android.content.Intent
import android.util.Log
import com.facebook.react.bridge.*
import android.content.SharedPreferences
class StorageModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
private val prefs = reactContext.getSharedPreferences("WhoscallPrefs", Context.MODE_PRIVATE)
override fun getName(): String {
return "VIPStorage"
}
@ReactMethod
fun saveVIP(name: String, number: String, promise: Promise) {
try {
prefs.edit().putString(number, name).apply()
Log.d("VIPStorage", "Saved VIP $name -> $number")
promise.resolve(true)
} catch (e: Exception) {
promise.reject("SAVE_ERROR", e)
}
}
@ReactMethod
fun simulateVipCall(number: String, name: String) {
Log.d("VIPStorage", "Simulating VIP call: $name ($number)")
val context = reactApplicationContext
// Save VIP first
prefs.edit().putString(number, name).apply()
// Start OverlayService
val intent = Intent(context, OverlayService::class.java).apply {
putExtra("number", number)
putExtra("displayName", name)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startService(intent)
}
}
package com.whoscall
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class StoragePackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(StorageModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btnRole"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Grant Call Screening Role (Android 10+)" />
<Space android:layout_width="0dp" android:layout_height="16dp"/>
<Button
android:id="@+id/btnOverlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Grant Overlay Permission" />
<Space android:layout_width="0dp" android:layout_height="16dp"/>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#FFFFFF"
android:layout_height="wrap_content">
<!-- Caller Name -->
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="32dp"
android:textColor="@android:color/black"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Caller Number -->
<TextView
android:id="@+id/number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="24dp"
android:textColor="@android:color/black"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="@id/name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- ❌ Close button -->
<ImageView
android:id="@+id/btnClose"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Close"
android:padding="4dp"
android:clickable="true"
android:focusable="true"/>
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -3,7 +3,7 @@
*/
import { AppRegistry } from 'react-native';
import App from './App';
import App from './App.tsx';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);
......@@ -10,9 +10,12 @@
"test": "jest"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native/new-app-screen": "0.81.1",
"react": "19.1.0",
"react-native": "0.81.1",
"@react-native/new-app-screen": "0.81.1",
"react-native-device-info": "^14.0.4",
"react-native-permissions": "^5.4.2",
"react-native-safe-area-context": "^5.5.2"
},
"devDependencies": {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment