Commit 94dd73a3 authored by alep's avatar alep

Update on android

parent b72847bb
......@@ -17,8 +17,9 @@ import {
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';
import VIPListScreen from './src/screens/VIPListScreen';
const { ContactModule, VIPStorage ,OverlayPermission } = NativeModules as any;
const { ContactModule, VIPStorage } = NativeModules as any;
const eventEmitter = new NativeEventEmitter(ContactModule);
type Contact = { name: string; number: string };
......@@ -290,7 +291,9 @@ const checkAllPermissions = useCallback(async () => {
return (
<View style={styles.container}>
<Text style={styles.title}>Permissions Status</Text>
<Text style={{
color:"#000000"
}}>Permissions Status</Text>
{(permissionsList ?? []).map(perm => (
<View key={perm.key} style={styles.row}>
<Text style={styles.text}>{perm.key}</Text>
......@@ -310,6 +313,7 @@ const checkAllPermissions = useCallback(async () => {
<Button title="Reload Call Directory" onPress={reloadCallDirectory} />
</>
)}
<VIPListScreen />
</View>
);
}
......
......@@ -6,43 +6,56 @@ 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
import org.json.JSONObject
@RequiresApi(Build.VERSION_CODES.N)
class IncomingCallService : CallScreeningService() {
override fun onScreenCall(callDetails: Details) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (callDetails.callDirection != Details.DIRECTION_INCOMING) {
// Not an incoming call, ignore
return
}
}
val number = callDetails.handle?.schemeSpecificPart ?: "Unknown"
// Lookup VIPs
// val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE)
val vipNumber = "+60164034061"
val vipName = "Alyp"
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())
private fun normalize(num: String?): String {
if (num.isNullOrBlank()) return ""
val digits = num.filter { it.isDigit() }
if (digits.isEmpty()) return ""
return if (digits.startsWith("0")) "60" + digits.drop(1) else digits
}
private fun loadVIPMap(): org.json.JSONObject {
val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE)
val json = prefs.getString("vip_map", "{}") ?: "{}"
return try { org.json.JSONObject(json) } catch (_: Exception) { org.json.JSONObject() }
}
override fun onScreenCall(callDetails: Details) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (callDetails.callDirection != Details.DIRECTION_INCOMING) return
}
val raw = callDetails.handle?.schemeSpecificPart ?: ""
val incoming = normalize(raw)
val vipMap = loadVIPMap()
var matchedName: String? = null
vipMap.keys().forEach { saved ->
if (incoming.endsWith(saved) || saved.endsWith(incoming)) {
matchedName = vipMap.optString(saved, null)
return@forEach
}
}
if (matchedName != null) {
// ✅ VIP matched → show overlay
val overlayIntent = Intent(this, OverlayService::class.java).apply {
putExtra("number", raw.ifBlank { "Unknown" })
putExtra("displayName", matchedName)
}
startService(overlayIntent)
} else {
// 🚫 Not VIP → do nothing (no overlay)
android.util.Log.d("IncomingCallService", "No VIP match for '$raw' (norm='$incoming'). Skipping overlay.")
}
// Always let the call through
respondToCall(callDetails, CallResponse.Builder().build())
}
}
......@@ -55,7 +55,7 @@ class MainActivity : ReactActivity() {
}
private fun simulateVipCall() {
val vipNumber = "+60189884118"
val vipNumber = "+60164034061"
val vipName = "Wei Yi"
val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE)
......
package com.whoscall
import android.content.Context
import android.content.Intent
import android.util.Log
import com.facebook.react.bridge.*
import android.content.SharedPreferences
import org.json.JSONObject
class StorageModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
ReactContextBaseJavaModule(reactContext) {
private val prefs = reactContext.getSharedPreferences("WhoscallPrefs", Context.MODE_PRIVATE)
private val prefs = reactContext.getSharedPreferences("WhoscallPrefs", Context.MODE_PRIVATE)
private val VIP_KEY = "vip_map"
override fun getName(): String {
return "VIPStorage"
override fun getName() = "VIPStorage"
// --- utils ---
private fun normalize(num: String?): String {
if (num.isNullOrBlank()) return ""
val digits = num.filter { it.isDigit() }
if (digits.isEmpty()) return ""
return if (digits.startsWith("0")) "60" + digits.drop(1) else digits
}
private fun readMap(): JSONObject {
val json = prefs.getString(VIP_KEY, "{}") ?: "{}"
return try { JSONObject(json) } catch (_: Exception) { JSONObject() }
}
private fun writeMap(obj: JSONObject) {
prefs.edit().putString(VIP_KEY, obj.toString()).apply()
}
// --- API: add/update a VIP ---
@ReactMethod
fun saveVIP(name: String, number: String, promise: Promise) {
try {
val key = normalize(number)
val map = readMap()
map.put(key, name)
writeMap(map)
Log.d("VIPStorage", "Saved VIP $name -> $key")
promise.resolve(true)
} catch (e: Exception) {
promise.reject("SAVE_ERROR", e)
}
}
@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)
}
// --- API: remove a VIP ---
@ReactMethod
fun removeVIP(number: String, promise: Promise) {
try {
val key = normalize(number)
val map = readMap()
map.remove(key)
writeMap(map)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("REMOVE_ERROR", e)
}
}
// --- API: list all VIPs (as array of {name, number}) ---
@ReactMethod
fun listVIPs(promise: Promise) {
try {
val map = readMap()
val arr = Arguments.createArray()
map.keys().forEach { k ->
val item = Arguments.createMap()
item.putString("number", k)
item.putString("name", map.getString(k))
arr.pushMap(item)
}
promise.resolve(arr)
} catch (e: Exception) {
promise.reject("LIST_ERROR", e)
}
}
// --- Optional: clear all ---
@ReactMethod
fun clearVIPs(promise: Promise) {
writeMap(JSONObject())
promise.resolve(true)
}
}
import React from 'react';
import {
View, Text, TextInput, TouchableOpacity, FlatList, StyleSheet, Alert, NativeModules, Platform
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
type VIP = { name: string; number: string }; // number should be normalized before save
const { VIPStorage } = NativeModules as any;
const ASYNC_KEY = 'vip_list_array_v1';
function normalize(num: string) {
const digits = (num || '').replace(/\D+/g, '');
if (!digits) return '';
return digits.startsWith('0') ? '60' + digits.slice(1) : digits;
}
type State = {
list: VIP[];
name: string;
number: string;
loading: boolean;
};
export default class VIPListScreen extends React.PureComponent<{}, State> {
state: State = { list: [], name: '', number: '', loading: true };
async componentDidMount() {
await this.load();
}
// --- load from AsyncStorage AND reconcile with native
load = async () => {
try {
const raw = await AsyncStorage.getItem(ASYNC_KEY);
const list: VIP[] = raw ? JSON.parse(raw) : [];
this.setState({ list, loading: false });
// optional one-way sync to native on mount
for (const item of list) {
await VIPStorage?.saveVIP(item.name, item.number);
}
} catch (e) {
console.warn('Failed to load VIP list', e);
this.setState({ loading: false });
}
};
saveAll = async (list: VIP[]) => {
await AsyncStorage.setItem(ASYNC_KEY, JSON.stringify(list));
};
add = async () => {
const { name, number, list } = this.state;
const n = normalize(number);
if (!name.trim() || !n) {
Alert.alert('Invalid', 'Please enter a name and a valid phone number.');
return;
}
// prevent duplicates
if (list.some(v => v.number === n)) {
Alert.alert('Exists', 'This number is already in your VIP list.');
return;
}
const next = [...list, { name: name.trim(), number: n }];
this.setState({ list: next, name: '', number: '' });
await this.saveAll(next);
// mirror to native
await VIPStorage?.saveVIP(name.trim(), n);
};
remove = async (number: string) => {
const next = this.state.list.filter(v => v.number !== number);
this.setState({ list: next });
await this.saveAll(next);
// mirror to native
await VIPStorage?.removeVIP(number);
};
renderItem = ({ item }: { item: VIP }) => (
<View style={styles.item}>
<View style={{ flex: 1 }}>
<Text style={styles.name}>{item.name}</Text>
<Text style={styles.number}>+{item.number.startsWith('60') ? item.number : item.number}</Text>
</View>
<TouchableOpacity style={styles.removeBtn} onPress={() => this.remove(item.number)}>
<Text style={styles.removeText}>Remove</Text>
</TouchableOpacity>
</View>
);
render() {
const { list, name, number, loading } = this.state;
return (
<View style={styles.container}>
<Text style={styles.title}>VIP List</Text>
<View style={styles.row}>
<TextInput
placeholderTextColor={"#000000"}
value={name}
onChangeText={t => this.setState({ name: t })}
style={styles.input}
placeholder="Name (e.g., Alyp)"
/>
<TextInput
placeholderTextColor={"#000000"}
value={number}
onChangeText={t => this.setState({ number: t })}
style={styles.input}
keyboardType="phone-pad"
placeholder="Phone (e.g., +6012-3456789)"
/>
</View>
<TouchableOpacity style={styles.addBtn} onPress={this.add}>
<Text style={styles.addText}>Add VIP</Text>
</TouchableOpacity>
{loading ? (
<Text style={{ textAlign: 'center', marginTop: 20 }}>Loading…</Text>
) : (
<FlatList
data={list}
keyExtractor={(it) => it.number}
renderItem={this.renderItem}
ListEmptyComponent={<Text style={{ textAlign: 'center', marginTop: 20 }}>No VIPs yet.</Text>}
/>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: 'white' },
title: { fontSize: 22, fontWeight: '600', marginBottom: 12, textAlign: 'center' },
row: { flexDirection: 'row', gap: 8 },
input: { flex: 1, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12,color:"#000000" },
addBtn: { backgroundColor: '#0a7', padding: 12, borderRadius: 8, marginVertical: 12, alignItems: 'center' },
addText: { color: 'white', fontWeight: '600' },
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderColor: '#eee' },
name: { fontSize: 16, fontWeight: '600' },
number: { color: '#666', marginTop: 4 },
removeBtn: { paddingVertical: 6, paddingHorizontal: 12, backgroundColor: '#d33', borderRadius: 6 },
removeText: { color: 'white', fontWeight: '600' },
});
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