Commit 94dd73a3 authored by alep's avatar alep

Update on android

parent b72847bb
...@@ -17,8 +17,9 @@ import { ...@@ -17,8 +17,9 @@ import {
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions'; import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
// import AsyncStorage from '@react-native-async-storage/async-storage'; // import AsyncStorage from '@react-native-async-storage/async-storage';
import DeviceInfo from 'react-native-device-info'; 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); const eventEmitter = new NativeEventEmitter(ContactModule);
type Contact = { name: string; number: string }; type Contact = { name: string; number: string };
...@@ -290,7 +291,9 @@ const checkAllPermissions = useCallback(async () => { ...@@ -290,7 +291,9 @@ const checkAllPermissions = useCallback(async () => {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>Permissions Status</Text> <Text style={{
color:"#000000"
}}>Permissions Status</Text>
{(permissionsList ?? []).map(perm => ( {(permissionsList ?? []).map(perm => (
<View key={perm.key} style={styles.row}> <View key={perm.key} style={styles.row}>
<Text style={styles.text}>{perm.key}</Text> <Text style={styles.text}>{perm.key}</Text>
...@@ -310,6 +313,7 @@ const checkAllPermissions = useCallback(async () => { ...@@ -310,6 +313,7 @@ const checkAllPermissions = useCallback(async () => {
<Button title="Reload Call Directory" onPress={reloadCallDirectory} /> <Button title="Reload Call Directory" onPress={reloadCallDirectory} />
</> </>
)} )}
<VIPListScreen />
</View> </View>
); );
} }
......
...@@ -6,43 +6,56 @@ import android.telecom.CallScreeningService ...@@ -6,43 +6,56 @@ import android.telecom.CallScreeningService
import android.telecom.Call.Details import android.telecom.Call.Details
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import android.util.Log import android.util.Log
import android.content.Context import org.json.JSONObject
import android.content.SharedPreferences
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
class IncomingCallService : CallScreeningService() { class IncomingCallService : CallScreeningService() {
override fun onScreenCall(callDetails: Details) { private fun normalize(num: String?): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (num.isNullOrBlank()) return ""
if (callDetails.callDirection != Details.DIRECTION_INCOMING) { val digits = num.filter { it.isDigit() }
// Not an incoming call, ignore if (digits.isEmpty()) return ""
return return if (digits.startsWith("0")) "60" + digits.drop(1) else digits
} }
}
val number = callDetails.handle?.schemeSpecificPart ?: "Unknown" private fun loadVIPMap(): org.json.JSONObject {
val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE)
// Lookup VIPs val json = prefs.getString("vip_map", "{}") ?: "{}"
// val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE) return try { org.json.JSONObject(json) } catch (_: Exception) { org.json.JSONObject() }
val vipNumber = "+60164034061" }
val vipName = "Alyp"
override fun onScreenCall(callDetails: Details) {
Log.d("IncomingCallReceiver", "Looking up number: $number, Found VIPNumber: $vipNumber, Found VIPName: $vipName") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Only show overlay if VIP if (callDetails.callDirection != Details.DIRECTION_INCOMING) return
if (!vipNumber.isNullOrEmpty() && number.contains(vipNumber)) { }
val overlayIntent = Intent(this, OverlayService::class.java).apply {
putExtra("number", number) val raw = callDetails.handle?.schemeSpecificPart ?: ""
putExtra("displayName", vipName) val incoming = normalize(raw)
}
startService(overlayIntent) val vipMap = loadVIPMap()
} else { var matchedName: String? = null
val overlayIntent = Intent(this, OverlayService::class.java).apply {
putExtra("number", number) vipMap.keys().forEach { saved ->
putExtra("displayName", "Unknown Caller") if (incoming.endsWith(saved) || saved.endsWith(incoming)) {
} matchedName = vipMap.optString(saved, null)
startService(overlayIntent) return@forEach
} }
}
// Let the call through without blocking
respondToCall(callDetails, CallResponse.Builder().build()) 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() { ...@@ -55,7 +55,7 @@ class MainActivity : ReactActivity() {
} }
private fun simulateVipCall() { private fun simulateVipCall() {
val vipNumber = "+60189884118" val vipNumber = "+60164034061"
val vipName = "Wei Yi" val vipName = "Wei Yi"
val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE) val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE)
......
package com.whoscall package com.whoscall
import android.content.Context import android.content.Context
import android.content.Intent
import android.util.Log import android.util.Log
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import android.content.SharedPreferences import org.json.JSONObject
class StorageModule(reactContext: ReactApplicationContext) : 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 { override fun getName() = "VIPStorage"
return "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 // --- API: remove a VIP ---
fun saveVIP(name: String, number: String, promise: Promise) { @ReactMethod
try { fun removeVIP(number: String, promise: Promise) {
prefs.edit().putString(number, name).apply() try {
Log.d("VIPStorage", "Saved VIP $name -> $number") val key = normalize(number)
promise.resolve(true) val map = readMap()
} catch (e: Exception) { map.remove(key)
promise.reject("SAVE_ERROR", e) 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