react-native-app-installation-listener
v1.4.0
Published
A React Native library that listens for app installation events on Android and emits events to JavaScript.
Downloads
12
Maintainers
Readme
react-native-app-installation-listener
A React Native library that listens for app installation events on Android and emits events to JavaScript. Now includes native API calling and notification handling for background operation.
Features
- Detects when new apps are installed, updated, or uninstalled on Android devices
- Emits events with package name and other details
- Native API calling and notification handling (works when app is killed)
- API base URL and token are hardcoded for security
- Lightweight and easy to integrate
- Supports React Native 0.60+ with autolinking
Installation
npm install react-native-app-installation-listener
# or
yarn add react-native-app-installation-listenerAndroid Setup
- Add the following permission to your
android/app/src/main/AndroidManifest.xml:
<manifest ...>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>- For React Native 0.60+, autolinking should handle the rest. If not, run:
cd android && ./gradlew cleanUsage
Basic Usage (with JS event handling)
import AppInstallationListener from 'react-native-app-installation-listener';
// Configure notification channel (API details are hardcoded)
AppInstallationListener.configure({
notificationChannelId: 'new-app-alerts'
});
// Start listening for app installations
AppInstallationListener.startListening();
// Listen for installation events (optional - notifications are handled natively)
const subscription = AppInstallationListener.addListener((event) => {
console.log('New app installed:', event.packageName);
// event contains: { packageName: string, appName?: string, versionCode?: number, versionName?: string }
});
// Stop listening when done
AppInstallationListener.stopListening();
subscription.remove();Expo/React Native App Integration
// In your app's monitoring hook
import AppInstallationListener from 'react-native-app-installation-listener';
import * as Notifications from 'expo-notifications';
export function useAppMonitoring() {
useEffect(() => {
async function setupMonitoring() {
if (Platform.OS !== 'android') return;
// Request notification permissions
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return;
// Create notification channel
await Notifications.setNotificationChannelAsync('new-app-alerts', {
name: 'New App Alerts',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
// Configure notification channel (API details are hardcoded)
AppInstallationListener.configure({
notificationChannelId: 'new-app-alerts'
});
AppInstallationListener.startListening();
console.log('AppInstallationListener started with native API handling');
// Optional: Listen for events
const subscription = AppInstallationListener.addListener((event) => {
console.log('New app installed:', event);
});
return () => {
subscription?.remove();
AppInstallationListener.stopListening();
};
}
setupMonitoring();
}, []);
}API
Methods
configure(config)
Configures the listener with notification settings. API base URL and token are hardcoded in the native code.
Parameters:
config(object):notificationChannelId(string, optional): Android notification channel ID (default: 'new-app-alerts')
startListening()
Starts listening for app installation events. Must be called after configuration.
stopListening()
Stops listening for app installation events.
addListener(callback)
Adds an event listener for app installation events.
Parameters:
callback(function): Function called when an app is installed
Returns: Subscription object with remove() method
Events
onAppInstalled
Emitted when a new app is installed, updated, or uninstalled.
Event payload:
{
type: 'install' | 'update' | 'uninstall',
packageName: string, // e.g., "com.example.app"
appName?: string, // Display name (if available)
versionCode?: number, // Version code
versionName?: string // Version name
}Native Implementation Details
Key Features
- BroadcastReceiver: Listens for
ACTION_PACKAGE_ADDEDsystem broadcasts - Native API Calls: HTTP requests handled in native code using
AsyncTask - Native Notifications: Push notifications shown directly from Android system
- Background Operation: Works even when the React Native app is completely killed
File Structure
android/src/main/java/com/reactnativeappinstallationlistener/
├── AppInstallationModule.java # React Native bridge module
├── AppInstallationReceiver.java # BroadcastReceiver for app installations
└── AppInstallationListenerPackage.java # React PackageAppInstallationModule.java
package com.reactnativeappinstallationlistener;
import android.content.IntentFilter;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
public class AppInstallationModule extends ReactContextBaseJavaModule {
private AppInstallationReceiver receiver;
private String apiBase;
private String apiToken;
private String notificationChannelId = "new-app-alerts";
@Override
public String getName() {
return "AppInstallationListener";
}
@ReactMethod
public void configure(ReadableMap config) {
// API base and token are hardcoded in ApiCheckTask
if (config.hasKey("notificationChannelId")) {
this.notificationChannelId = config.getString("notificationChannelId");
}
}
@ReactMethod
public void startListening() {
if (receiver == null) {
receiver = new AppInstallationReceiver(apiBase, apiToken, notificationChannelId);
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package");
getReactApplicationContext().registerReceiver(receiver, filter);
}
}
@ReactMethod
public void stopListening() {
if (receiver != null) {
getReactApplicationContext().unregisterReceiver(receiver);
receiver = null;
}
}
}AppInstallationReceiver.java
package com.reactnativeappinstallationlistener;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class AppInstallationReceiver extends BroadcastReceiver {
private static final String TAG = "AppInstallationReceiver";
private final ReactApplicationContext reactContext;
private String apiBase;
private String apiToken;
private String notificationChannelId;
public AppInstallationReceiver(ReactApplicationContext reactContext, String apiBase, String apiToken, String notificationChannelId) {
this.reactContext = reactContext;
this.apiBase = apiBase;
this.apiToken = apiToken;
this.notificationChannelId = notificationChannelId;
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) return;
String packageName = intent.getData().getSchemeSpecificPart();
if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
Log.d(TAG, "New app installed: " + packageName);
sendEvent("install", packageName);
handleNewOrUpdatedApp(context, packageName, "Scanning the new app");
} else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
Log.d(TAG, "App updated: " + packageName);
sendEvent("update", packageName);
handleNewOrUpdatedApp(context, packageName, "Scanning updated app");
} else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
Log.d(TAG, "App uninstalled: " + packageName);
sendEvent("uninstall", packageName);
}
}
}
private void handleNewOrUpdatedApp(Context context, String packageName, String notificationTitle) {
// Show scanning notification
showNotification(context, notificationTitle, "Scanning " + packageName + "...", packageName);
// Start API check in background
new ApiCheckTask(context, packageName, apiBase, apiToken, notificationChannelId).execute();
}
private void sendEvent(String type, String packageName) {
if (reactContext.hasActiveCatalystInstance()) {
WritableMap params = Arguments.createMap();
params.putString("type", type);
params.putString("packageName", packageName);
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("onAppInstalled", params);
}
}
private void showNotification(Context context, String title, String body, String packageName) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
notificationChannelId,
"New App Alerts",
NotificationManager.IMPORTANCE_HIGH
);
channel.setVibrationPattern(new long[]{0, 250, 250, 250});
channel.enableLights(true);
channel.setLightColor(0xFFFF317C);
notificationManager.createNotificationChannel(channel);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannelId)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText(body)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
notificationManager.notify(packageName.hashCode(), builder.build());
}
private static class ApiCheckTask extends AsyncTask<Void, Void, String> {
private Context context;
private String packageName;
private String apiBase;
private String apiToken;
private String notificationChannelId;
public ApiCheckTask(Context context, String packageName, String apiBase, String apiToken, String notificationChannelId) {
this.context = context;
this.packageName = packageName;
// API details are hardcoded
this.apiBase = "https://aft.frautect.com";
this.apiToken = "e77e34622f71202c7f744b28464e648b855af3e3c7c1c23fb69ff25d3c574996";
this.notificationChannelId = notificationChannelId;
}
@Override
protected String doInBackground(Void... voids) {
try {
URL url = new URL(apiBase + "/api/v1/apps/check");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "Bearer " + apiToken);
conn.setDoOutput(true);
JSONObject payload = new JSONObject();
JSONArray apps = new JSONArray();
JSONObject app = new JSONObject();
app.put("appId", packageName);
app.put("countryCode", "US");
apps.put(app);
payload.put("apps", apps);
OutputStream os = conn.getOutputStream();
os.write(payload.toString().getBytes());
os.flush();
os.close();
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
return response.toString();
} else {
Log.e(TAG, "API call failed with code: " + responseCode);
return null;
}
} catch (Exception e) {
Log.e(TAG, "Error calling API", e);
return null;
}
}
@Override
protected void onPostExecute(String result) {
if (result != null) {
try {
JSONObject jsonResponse = new JSONObject(result);
if (jsonResponse.optBoolean("success", false)) {
JSONArray data = jsonResponse.optJSONArray("data");
if (data != null && data.length() > 0) {
JSONObject appResult = data.getJSONObject(0);
String appType = appResult.optString("appType", "UNKNOWN");
String status = appType.replace("-", " ").toUpperCase();
JSONArray tagsArray = appResult.optJSONArray("appTags");
String tags = "";
if (tagsArray != null && tagsArray.length() > 0) {
StringBuilder tagsBuilder = new StringBuilder(" | Tags: ");
for (int i = 0; i < tagsArray.length(); i++) {
if (i > 0) tagsBuilder.append(", ");
tagsBuilder.append(tagsArray.getString(i));
}
tags = tagsBuilder.toString();
}
showResultNotification("Scan Complete",
"App: " + packageName + "\nStatus: " + status + tags);
}
} else {
showResultNotification("Scan Failed",
"Could not scan " + packageName + ". Please check manually.");
}
} catch (Exception e) {
Log.e(TAG, "Error parsing API response", e);
showResultNotification("Scan Failed",
"Could not scan " + packageName + ". Please check manually.");
}
} else {
showResultNotification("Scan Failed",
"Could not scan " + packageName + ". Please check manually.");
}
}
private void showResultNotification(String title, String body) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannelId)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText(body)
.setStyle(new NotificationCompat.BigTextStyle().bigText(body))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
notificationManager.notify(packageName.hashCode() + 1, builder.build());
}
}
}JavaScript Interface (index.js)
import { NativeModules, DeviceEventEmitter } from 'react-native';
const { AppInstallationListener } = NativeModules;
export default {
configure: (config) => {
AppInstallationListener.configure(config);
},
startListening: () => {
AppInstallationListener.startListening();
},
stopListening: () => {
AppInstallationListener.stopListening();
},
addListener: (callback) => {
return DeviceEventEmitter.addListener('onAppInstalled', callback);
}
};Requirements
- React Native 0.60+
- Android API level 21+ (Android 5.0+)
- The app must have
QUERY_ALL_PACKAGESandINTERNETpermissions
Permissions
This library requires the following Android permissions:
android.permission.QUERY_ALL_PACKAGES: To access package informationandroid.permission.INTERNET: To make API calls
Example
See the /example directory for a complete React Native app demonstrating the usage.
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Building from Source
# Clone the repository
git clone https://github.com/yourusername/react-native-app-installation-listener.git
cd react-native-app-installation-listener
# Install dependencies
npm install
# Build the Android library
cd android && ./gradlew assembleReleaseTesting
# Run tests
npm test
# Run linting
npm run lintLicense
This project is licensed under the MIT License - see the LICENSE file for details.
Support
If you have any questions or issues, please open an issue on GitHub or contact the maintainers.
Changelog
[1.2.0] - 2025-12-10
- Added detection for app updates and uninstalls
- Added
typefield to event payload ('install', 'update', 'uninstall') - Restored event emission to JavaScript
[1.1.0] - 2025-12-06
- Added native API calling and notification handling
- Notifications now work when app is completely killed
- Added configuration method for API details
- Improved background processing with AsyncTask
[1.0.0] - 2025-12-06
- Initial release
- Android app installation detection
- Event emission to React Native
