Commit dfded887 authored by alep's avatar alep

handle permission android well

parent 74d782e7
import React, { useEffect, useState, useCallback } from 'react';
import { View, Text, Button, StyleSheet, Alert, Linking, Platform, NativeModules } from 'react-native';
import {
View,
Text,
Button,
StyleSheet,
Alert,
Linking,
Platform,
NativeModules,
NativeEventEmitter,
TextInput,
FlatList,
TouchableOpacity,
AppState,
} from 'react-native';
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';
const permissionsList = Platform.select({
android: [
const { ContactModule, VIPStorage ,OverlayPermission } = NativeModules as any;
const eventEmitter = new NativeEventEmitter(ContactModule);
type Contact = { name: string; number: string };
const CONTACTS_KEY = 'whoscallContacts';
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
],
ios: [{ key: 'Call Directory Extension', ios: true }],
})!;
const { OverlayPermission } = NativeModules;
type CallDirectoryManagerType = {
getEnabledStatusForExtension(identifier: string): Promise<number>;
reloadExtension(identifier: string): Promise<boolean>;
};
const CallDirectoryManager: CallDirectoryManagerType | undefined = NativeModules.CallDirectoryManager;
// 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);
// }
// }
async function checkOverlayPermission() {
if (Platform.OS === 'android') {
try {
const granted: boolean = await OverlayPermission.hasPermission();
if (!granted) {
Alert.alert(
'Overlay Permission Required',
'This app needs permission to display over other apps.',
[{ text: 'Open Settings', onPress: openOverlaySettings }]
);
}
return granted;
} catch (e) {
console.error('Error checking overlay permission:', e);
return false;
}
} else {
return true; // iOS doesn't require this
}
];
function normalize(n: string) {
return (n || '').replace(/[^+0-9]/g, '');
}
const openOverlaySettings = async () => {
if (Platform.OS === 'android') {
const pkg = DeviceInfo.getBundleId(); // gets your app package name
async function loadContacts(): Promise<Contact[]> {
const json = await AsyncStorage.getItem(CONTACTS_KEY);
return json ? JSON.parse(json) : [];
}
async function persistContacts(contacts: Contact[]) {
const json = JSON.stringify(contacts);
await AsyncStorage.setItem(CONTACTS_KEY, json);
// Sync to native so IncomingCallService can resolve when JS isn’t active
if (VIPStorage?.setContactsJson) {
try {
await Linking.openSettings(); // fallback
await Linking.openURL(`package:${pkg}`);
} catch (err) {
console.warn('Failed to open overlay settings:', err);
await VIPStorage.setContactsJson(json);
} catch (e) {
console.warn('VIPStorage.setContactsJson failed:', e);
}
}
};
}
export default function App() {
const [statuses, setStatuses] = useState<Record<string, string>>({});
const [contacts, setContacts] = useState<Contact[]>([]);
const [name, setName] = useState('');
const [number, setNumber] = useState('');
const checkAllPermissions = useCallback(async () => {
const newStatuses: Record<string, string> = {};
const refreshContacts = useCallback(async () => {
const list = await loadContacts();
setContacts(list);
}, []);
if (Platform.OS === 'ios') {
try {
if (CallDirectoryManager) {
const status = await CallDirectoryManager.getEnabledStatusForExtension(
"com.whoscall.app.CallDirectoryExtension"
);
newStatuses['Call Directory Extension'] = status === 2 ? RESULTS.GRANTED : RESULTS.DENIED;
} else {
console.warn("⚠️ CallDirectoryManager native module not available");
newStatuses['Call Directory Extension'] = RESULTS.DENIED;
}
} catch (e) {
console.error("Error checking Call Directory extension:", e);
newStatuses['Call Directory Extension'] = RESULTS.DENIED;
// ---- Permissions flow (kept from your code) ----
// 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) {
// console.log(newStatuses)
// 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 }));
// }
// }
// }
// }, []);
const RESULTS_BOOL = { true: RESULTS.GRANTED, false: RESULTS.DENIED } as const;
const openOverlaySettings = async () => {
if (Platform.OS === 'android' && OverlayPermission?.openOverlaySettings) {
OverlayPermission.openOverlaySettings();
}
} else {
};
const checkAllPermissions = useCallback(async () => {
const newStatuses: Record<string, string> = {};
for (const perm of permissionsList) {
if (perm.key === 'Display Over Other Apps') {
const granted = await checkOverlayPermission();
newStatuses[perm.key] = granted ? RESULTS.GRANTED : RESULTS.DENIED;
// ✅ Ask native for the real status
try {
const allowed = await OverlayPermission.hasOverlayPermission();
newStatuses[perm.key] = RESULTS_BOOL[String(allowed) as 'true' | 'false'];
} catch {
newStatuses[perm.key] = RESULTS.DENIED;
}
continue;
}
if ('android' in perm && perm.android) {
const result = await check(perm.android);
const result = await check(perm.android!);
newStatuses[perm.key] = result;
}
}
setStatuses(newStatuses);
// Request any missing ones (overlay requires manual settings)
for (const perm of permissionsList) {
if (newStatuses[perm.key] !== RESULTS.GRANTED) {
if (perm.key === 'Display Over Other Apps') {
......@@ -114,89 +134,174 @@ export default function App() {
'This app needs permission to display over other apps.',
[{ text: 'Open Settings', onPress: openOverlaySettings }]
);
} else if ('android' in perm && perm.android) {
const result = await request(perm.android);
} else {
const result = await request(perm.android!);
setStatuses(prev => ({ ...prev, [perm.key]: result }));
}
}
}
}, []);
useEffect(() => {
const sub = AppState.addEventListener('change', (state) => {
if (state === 'active') checkAllPermissions();
});
return () => sub.remove();
}, [checkAllPermissions]);
// (Optional) Guard the old event flow so it won't crash if method not present
useEffect(() => {
const subscription = eventEmitter.addListener('resolveContact', async (incomingNumber: string) => {
try {
const json = await AsyncStorage.getItem(CONTACTS_KEY);
if (!json) return;
const displayName = await ContactModule.getDisplayName(json, incomingNumber);
if (ContactModule?.sendResolvedContact) {
ContactModule.sendResolvedContact(incomingNumber, displayName || incomingNumber);
}
if (Platform.OS === 'ios') {
Alert.alert(
"Enable Caller ID",
"Go to Settings → Phone → Call Blocking & Identification and enable [Your App] to see caller ID labels.",
[
{ text: "Open Settings", onPress: () => Linking.openURL("app-settings:") },
{ text: "Cancel", style: "cancel" }
]
);
} catch (err) {
console.error('Failed to resolve contact:', err);
}
});
return () => subscription.remove();
}, []);
useEffect(() => {
console.log("PermissionsScreen mounted");
console.log('PermissionsScreen mounted');
console.log('All NativeModules keys:', Object.keys(NativeModules));
console.log('VIPStorage:', VIPStorage);
// Initial load
refreshContacts();
checkAllPermissions();
}, [checkAllPermissions, refreshContacts]);
console.log("All NativeModules keys:", Object.keys(NativeModules));
// console.log("VIPStorage:", NativeModules.VIPStorage);
// saveVIPNumber('VIP User', '+60123456789');
checkAllPermissions();
},[checkAllPermissions]);
// ---- Overlay settings opener (kept) ----
// 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);
// --- iOS: Call Directory management ---
const checkCallDirectoryStatus = async () => {
if (Platform.OS !== "ios" || !CallDirectoryManager) {
console.warn("CallDirectoryManager not available on this platform");
// ---- Contacts CRUD ----
const onAddOrUpdate = async () => {
const trimmedName = name.trim();
const trimmedNum = number.trim();
if (!trimmedName) {
Alert.alert('Validation', 'Please enter a name.');
return;
}
try {
const status = await CallDirectoryManager.getEnabledStatusForExtension(
"com.whoscall.app.CallDirectoryExtension" // <-- must match your extension bundle ID
);
console.log("Call Directory status:", status);
if (status === 2) {
Alert.alert("✅ Enabled", "Call Directory Extension is enabled");
} else {
Alert.alert(
"⚠️ Not Enabled",
"Go to Settings → Phone → Call Blocking & Identification and enable Whoscall."
);
}
} catch (e) {
console.error("Error checking Call Directory status:", e);
if (!trimmedNum) {
Alert.alert('Validation', 'Please enter a number.');
return;
}
const normalized = normalize(trimmedNum);
const next = [...contacts];
const idx = next.findIndex(c => normalize(c.number) === normalized);
const newContact = { name: trimmedName, number: trimmedNum };
if (idx >= 0) next[idx] = newContact; else next.push(newContact);
await persistContacts(next);
setContacts(next);
setName('');
setNumber('');
};
const reloadCallDirectory = async () => {
if (Platform.OS !== "ios" || !CallDirectoryManager) {
console.warn("CallDirectoryManager not available on this platform");
return;
}
try {
const success = await CallDirectoryManager.reloadExtension(
"com.whoscall.app.CallDirectoryExtension"
);
console.log("Reload result:", success);
if (success) {
Alert.alert("🔄 Reloaded", "Extension reloaded successfully");
}
} catch (e) {
console.error("Error reloading Call Directory:", e);
Alert.alert("❌ Reload Failed", String(e));
const onDelete = async (target: Contact) => {
const targetNorm = normalize(target.number);
const next = contacts.filter(c => normalize(c.number) !== targetNorm);
await persistContacts(next);
setContacts(next);
};
const onClearAll = async () => {
Alert.alert('Clear All', 'Remove all contacts?', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Clear',
style: 'destructive',
onPress: async () => {
await AsyncStorage.removeItem(CONTACTS_KEY);
if (VIPStorage?.setContactsJson) {
try { await VIPStorage.setContactsJson('[]'); } catch {}
}
setContacts([]);
},
},
]);
};
const renderItem = ({ item }: { item: Contact }) => (
<View style={styles.itemRow}>
<View style={{ flex: 1 }}>
<Text style={styles.itemName}>{item.name}</Text>
<Text style={styles.itemNumber}>{item.number}</Text>
</View>
<TouchableOpacity style={styles.deleteBtn} onPress={() => onDelete(item)}>
<Text style={styles.deleteTxt}>Delete</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
<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.header}>Whoscall Contacts</Text>
{/* Add / Update form */}
<View style={styles.form}>
<TextInput
value={name}
onChangeText={setName}
placeholder="Contact name"
style={styles.input}
autoCapitalize="words"
/>
<TextInput
value={number}
onChangeText={setNumber}
placeholder="Phone number (e.g., +60123456789)"
style={styles.input}
keyboardType="phone-pad"
/>
<View style={{ flexDirection: 'row', gap: 12 }}>
<Button title="Save Contact" onPress={onAddOrUpdate} />
<Button title="Clear All" color="#c62828" onPress={onClearAll} />
</View>
</View>
{/* Contact list */}
<Text style={styles.subheader}>Saved Contacts ({contacts.length})</Text>
<FlatList
data={contacts}
keyExtractor={(item, idx) => `${normalize(item.number)}_${idx}`}
renderItem={renderItem}
ItemSeparatorComponent={() => <View style={styles.separator} />}
style={{ flexGrow: 0, maxHeight: 260 }}
ListEmptyComponent={<Text style={styles.empty}>No contacts yet. Add one above.</Text>}
/>
{/* Permissions status */}
<Text style={styles.subheader}>Permissions Status</Text>
{permissionsList.map(perm => (
<View key={perm.key} style={styles.permRow}>
<Text style={styles.permKey}>{perm.key}</Text>
<Text
style={[
styles.text,
styles.permVal,
{ color: statuses[perm.key] === RESULTS.GRANTED ? 'green' : 'red' },
]}>
{statuses[perm.key] || 'Checking...'}
......@@ -204,22 +309,37 @@ export default function App() {
</View>
))}
{allGranted && <Text style={styles.granted}>All permissions granted ✅</Text>}
<View style={{ height: 12 }} />
<Button title="Check Again" onPress={checkAllPermissions} />
{Platform.OS === "ios" && (
<>
<Button title="Check Call Directory Status" onPress={checkCallDirectoryStatus} />
<View style={{ height: 10 }} />
<Button title="Reload Call Directory" onPress={reloadCallDirectory} />
</>
)}
</View>
);
}
const styles = StyleSheet.create({
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 },
container: { flex: 1, padding: 16, backgroundColor: '#FFFFFF',paddingTop:50 },
header: { fontSize: 22, fontWeight: '600', textAlign: 'center', marginBottom: 12 },
subheader: { fontSize: 16, fontWeight: '600', marginTop: 18, marginBottom: 8 },
form: { marginBottom: 8, gap: 8 },
input: {
borderWidth: 1, borderColor: '#DDD', borderRadius: 8,
paddingHorizontal: 12, paddingVertical: 10, fontSize: 16,
},
itemRow: {
flexDirection: 'row', alignItems: 'center',
paddingVertical: 10, paddingHorizontal: 8,
},
itemName: { fontSize: 16, fontWeight: '600', color: '#222' },
itemNumber: { fontSize: 14, color: '#555', marginTop: 2 },
deleteBtn: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: '#eee', borderRadius: 8 },
deleteTxt: { color: '#c62828', fontWeight: '600' },
separator: { height: 1, backgroundColor: '#F0F0F0', marginLeft: 8 },
permRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 },
permKey: { fontSize: 14, color: '#333' },
permVal: { fontSize: 14, fontWeight: '600' },
granted: { fontSize: 16, color: 'green', textAlign: 'center', marginTop: 8 },
empty: { color: '#777', fontStyle: 'italic', paddingHorizontal: 8, paddingVertical: 6 },
});
// OverlayPermissionModule.kt
package com.whoscall
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import com.facebook.react.bridge.*
class OverlayPermissionModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
class OverlayPermissionModule(private val reactContext: ReactApplicationContext)
: ReactContextBaseJavaModule(reactContext) {
override fun getName(): String {
return "OverlayPermission"
}
override fun getName(): String = "OverlayPermission"
@ReactMethod
fun hasPermission(promise: Promise) {
val canDraw = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(reactApplicationContext)
} else {
true
fun hasOverlayPermission(promise: Promise) {
try {
val allowed = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(reactContext)
} else true
promise.resolve(allowed)
} catch (e: Exception) {
promise.reject("OVERLAY_CHECK_ERROR", e)
}
}
promise.resolve(canDraw)
@ReactMethod
fun openOverlaySettings() {
try {
val ctx = reactContext.currentActivity ?: reactContext
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${reactContext.packageName}")
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.startActivity(intent)
} catch (_: Exception) {
// best-effort
}
}
}
......@@ -7,7 +7,10 @@ import com.facebook.react.uimanager.ViewManager
class StoragePackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(StorageModule(reactContext))
return listOf(
StorageModule(reactContext),
OverlayPermissionModule(reactContext) // 👈 add this
)
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
......
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