expo-app-blocker 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/android/src/main/java/expo/modules/appblocker/AppBlockerPrefs.kt +25 -0
- package/android/src/main/java/expo/modules/appblocker/AppBlockerService.kt +27 -4
- package/android/src/main/java/expo/modules/appblocker/ExpoAppBlockerModule.kt +10 -0
- package/android/src/main/java/expo/modules/appblocker/OverlayManager.kt +44 -20
- package/package.json +1 -1
- package/plugin/src/index.js +69 -1
- package/src/ExpoAppBlocker.types.ts +16 -7
- package/src/index.ts +7 -0
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@ https://github.com/user-attachments/assets/37f34797-6b92-40d5-911a-90c40e9ffaaa
|
|
|
13
13
|
> **iOS requires Apple Developer Portal setup before building.** See [Prerequisites](#prerequisites) for details.
|
|
14
14
|
|
|
15
15
|
> [!IMPORTANT]
|
|
16
|
-
> **Submit your Family Controls distribution approval request now.** App Store distribution requires Apple approval per bundle ID — it can take days to weeks and you can't ship without it. [Request here](https://developer.apple.com/contact/request/family-controls-distribution) (you'll need to submit once per bundle ID — 4 total).
|
|
16
|
+
> **Submit your Family Controls distribution approval request now.** App Store distribution requires Apple approval per bundle ID — it can take days to weeks and you can't ship without it. [Request here](https://developer.apple.com/contact/request/family-controls-distribution) (you'll need to submit once per bundle ID — 4 total).
|
|
17
|
+
>
|
|
18
|
+
> **While waiting for approval**, use the **Family Controls (Development)** capability in Xcode instead of the standard "Family Controls" — it's marked "Development only" in Xcode's Signing & Capabilities tab and works without Apple's approval. Development builds with this entitlement are fully functional on device but cannot be submitted to TestFlight or the App Store.
|
|
17
19
|
|
|
18
20
|
<details>
|
|
19
21
|
<summary><strong>Table of Contents</strong></summary>
|
|
@@ -188,8 +190,9 @@ npx expo run:android # Android works on emulator
|
|
|
188
190
|
|
|
189
191
|
3. Assign the App Group to all 4 App IDs
|
|
190
192
|
|
|
191
|
-
4. Request **Family Controls** capability approval (required for App Store/TestFlight
|
|
193
|
+
4. Request **Family Controls** capability approval (required for App Store/TestFlight distribution)
|
|
192
194
|
- Submit the form **once per bundle ID** (4 total): [developer.apple.com/contact/request/family-controls-distribution](https://developer.apple.com/contact/request/family-controls-distribution)
|
|
195
|
+
- **While waiting for approval**: use **Family Controls (Development)** in Xcode's Signing & Capabilities tab — fully functional in dev builds, just not distributable
|
|
193
196
|
- Incomplete capability setup causes cryptic provisioning errors — make sure all 4 App IDs have Family Controls + App Groups enabled
|
|
194
197
|
|
|
195
198
|
### Android
|
|
@@ -6,6 +6,9 @@ import android.content.SharedPreferences
|
|
|
6
6
|
object AppBlockerPrefs {
|
|
7
7
|
const val PREFS_NAME = "expo_app_blocker_prefs"
|
|
8
8
|
const val KEY_BLOCKED_PACKAGES = "blocked_packages"
|
|
9
|
+
private const val KEY_OVERLAY_TEXT = "overlay_text"
|
|
10
|
+
private const val KEY_NOTIFICATION_TITLE = "notification_title"
|
|
11
|
+
private const val KEY_NOTIFICATION_TEXT = "notification_text"
|
|
9
12
|
|
|
10
13
|
fun get(context: Context): SharedPreferences =
|
|
11
14
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
@@ -18,4 +21,26 @@ object AppBlockerPrefs {
|
|
|
18
21
|
.putStringSet(KEY_BLOCKED_PACKAGES, packages.toSet())
|
|
19
22
|
.apply()
|
|
20
23
|
}
|
|
24
|
+
|
|
25
|
+
fun setAndroidConfig(
|
|
26
|
+
context: Context,
|
|
27
|
+
overlayText: String?,
|
|
28
|
+
notificationTitle: String?,
|
|
29
|
+
notificationText: String?,
|
|
30
|
+
) {
|
|
31
|
+
get(context).edit()
|
|
32
|
+
.putString(KEY_OVERLAY_TEXT, overlayText)
|
|
33
|
+
.putString(KEY_NOTIFICATION_TITLE, notificationTitle)
|
|
34
|
+
.putString(KEY_NOTIFICATION_TEXT, notificationText)
|
|
35
|
+
.apply()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun getOverlayText(context: Context): String =
|
|
39
|
+
get(context).getString(KEY_OVERLAY_TEXT, null) ?: "{appName} is blocked."
|
|
40
|
+
|
|
41
|
+
fun getNotificationTitle(context: Context): String =
|
|
42
|
+
get(context).getString(KEY_NOTIFICATION_TITLE, null) ?: "App Blocked"
|
|
43
|
+
|
|
44
|
+
fun getNotificationText(context: Context): String =
|
|
45
|
+
get(context).getString(KEY_NOTIFICATION_TEXT, null) ?: "{appName} is blocked. Tap to manage."
|
|
21
46
|
}
|
|
@@ -65,7 +65,10 @@ class AppBlockerService : Service() {
|
|
|
65
65
|
packageName
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
val
|
|
68
|
+
val title = AppBlockerPrefs.getNotificationTitle(this).replace("{appName}", appName)
|
|
69
|
+
val text = AppBlockerPrefs.getNotificationText(this).replace("{appName}", appName)
|
|
70
|
+
|
|
71
|
+
val scheme = getAppScheme()
|
|
69
72
|
val deepLinkIntent = Intent(
|
|
70
73
|
Intent.ACTION_VIEW,
|
|
71
74
|
Uri.parse("${scheme}://blocked?app=${Uri.encode(appName)}&package=${Uri.encode(packageName)}")
|
|
@@ -73,14 +76,23 @@ class AppBlockerService : Service() {
|
|
|
73
76
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(this.packageName)
|
|
80
|
+
?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) }
|
|
81
|
+
|
|
82
|
+
val resolvedIntent = try {
|
|
83
|
+
deepLinkIntent.resolveActivity(packageManager)?.let { deepLinkIntent } ?: launchIntent
|
|
84
|
+
} catch (e: Exception) {
|
|
85
|
+
launchIntent
|
|
86
|
+
} ?: deepLinkIntent
|
|
87
|
+
|
|
76
88
|
val pendingIntent = PendingIntent.getActivity(
|
|
77
|
-
this,
|
|
89
|
+
this, packageName.hashCode(), resolvedIntent,
|
|
78
90
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
79
91
|
)
|
|
80
92
|
|
|
81
93
|
val notification = NotificationCompat.Builder(this, BLOCKED_CHANNEL_ID)
|
|
82
|
-
.setContentTitle(
|
|
83
|
-
.setContentText(
|
|
94
|
+
.setContentTitle(title)
|
|
95
|
+
.setContentText(text)
|
|
84
96
|
.setSmallIcon(applicationInfo.icon)
|
|
85
97
|
.setAutoCancel(true)
|
|
86
98
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
@@ -91,6 +103,17 @@ class AppBlockerService : Service() {
|
|
|
91
103
|
manager.notify(BLOCKED_NOTIFICATION_ID, notification)
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
private fun getAppScheme(): String {
|
|
107
|
+
val resId = resources.getIdentifier("expo_app_blocker_scheme", "string", packageName)
|
|
108
|
+
if (resId != 0) return getString(resId)
|
|
109
|
+
return try {
|
|
110
|
+
packageManager.getLaunchIntentForPackage(packageName)?.data?.scheme
|
|
111
|
+
?: packageName.replace(".", "-")
|
|
112
|
+
} catch (e: Exception) {
|
|
113
|
+
packageName.replace(".", "-")
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
95
118
|
Log.d(TAG, "AppBlockerService onStartCommand")
|
|
96
119
|
return START_STICKY
|
|
@@ -61,6 +61,16 @@ class ExpoAppBlockerModule : Module() {
|
|
|
61
61
|
context.startActivity(intent)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
Function("setAndroidConfig") { config: Map<String, Any?> ->
|
|
65
|
+
AppBlockerPrefs.setAndroidConfig(
|
|
66
|
+
context,
|
|
67
|
+
overlayText = config["overlayText"] as? String,
|
|
68
|
+
notificationTitle = config["notificationTitle"] as? String,
|
|
69
|
+
notificationText = config["notificationText"] as? String,
|
|
70
|
+
)
|
|
71
|
+
Log.d(TAG, "setAndroidConfig: $config")
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
Function("setBlockedApps") { packageNames: List<String> ->
|
|
65
75
|
AppBlockerPrefs.setBlockedPackages(context, packageNames)
|
|
66
76
|
Log.d(TAG, "setBlockedApps: $packageNames")
|
|
@@ -4,12 +4,15 @@ import android.content.Context
|
|
|
4
4
|
import android.content.Intent
|
|
5
5
|
import android.graphics.Color
|
|
6
6
|
import android.graphics.PixelFormat
|
|
7
|
+
import android.graphics.Typeface
|
|
7
8
|
import android.net.Uri
|
|
8
9
|
import android.os.Build
|
|
9
10
|
import android.util.Log
|
|
11
|
+
import android.util.TypedValue
|
|
10
12
|
import android.view.Gravity
|
|
11
13
|
import android.view.View
|
|
12
14
|
import android.view.WindowManager
|
|
15
|
+
import android.widget.LinearLayout
|
|
13
16
|
import android.widget.TextView
|
|
14
17
|
|
|
15
18
|
class OverlayManager(private val context: Context) {
|
|
@@ -29,7 +32,8 @@ class OverlayManager(private val context: Context) {
|
|
|
29
32
|
return
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
val
|
|
35
|
+
val appName = blockedPackageName?.let { resolveAppName(it) } ?: ""
|
|
36
|
+
val view = buildOverlayView(appName)
|
|
33
37
|
try {
|
|
34
38
|
windowManager.addView(view, buildLayoutParams())
|
|
35
39
|
overlayView = view
|
|
@@ -57,14 +61,16 @@ class OverlayManager(private val context: Context) {
|
|
|
57
61
|
overlayView = null
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
private fun resolveAppName(packageName: String): String = try {
|
|
65
|
+
val pm = context.packageManager
|
|
66
|
+
val appInfo = pm.getApplicationInfo(packageName, 0)
|
|
67
|
+
pm.getApplicationLabel(appInfo).toString()
|
|
68
|
+
} catch (e: Exception) {
|
|
69
|
+
packageName
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
private fun navigateToApp(blockedPackageName: String) {
|
|
61
|
-
val appName =
|
|
62
|
-
val pm = context.packageManager
|
|
63
|
-
val appInfo = pm.getApplicationInfo(blockedPackageName, 0)
|
|
64
|
-
pm.getApplicationLabel(appInfo).toString()
|
|
65
|
-
} catch (e: Exception) {
|
|
66
|
-
blockedPackageName
|
|
67
|
-
}
|
|
73
|
+
val appName = resolveAppName(blockedPackageName)
|
|
68
74
|
|
|
69
75
|
// Use the app's own scheme for deep linking
|
|
70
76
|
val scheme = getAppScheme()
|
|
@@ -102,22 +108,40 @@ class OverlayManager(private val context: Context) {
|
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
private fun getAppScheme(): String {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
intent?.data?.scheme ?: context.packageName.replace(".", "-")
|
|
109
|
-
} catch (e: Exception) {
|
|
110
|
-
context.packageName.replace(".", "-")
|
|
111
|
-
}
|
|
111
|
+
val resId = context.resources.getIdentifier("expo_app_blocker_scheme", "string", context.packageName)
|
|
112
|
+
if (resId != 0) return context.getString(resId)
|
|
113
|
+
return context.packageName.replace(".", "-")
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
private fun buildOverlayView(): View {
|
|
115
|
-
val
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
private fun buildOverlayView(appName: String): View {
|
|
117
|
+
val density = context.resources.displayMetrics.density
|
|
118
|
+
fun dp(value: Int) = (value * density).toInt()
|
|
119
|
+
|
|
120
|
+
val overlayText = AppBlockerPrefs.getOverlayText(context)
|
|
121
|
+
.replace("{appName}", appName)
|
|
122
|
+
|
|
123
|
+
return LinearLayout(context).apply {
|
|
124
|
+
orientation = LinearLayout.VERTICAL
|
|
118
125
|
gravity = Gravity.CENTER
|
|
126
|
+
setBackgroundColor(Color.WHITE)
|
|
127
|
+
setPadding(dp(32), dp(32), dp(32), dp(32))
|
|
128
|
+
|
|
129
|
+
addView(TextView(context).apply {
|
|
130
|
+
text = "App Blocked"
|
|
131
|
+
setTextColor(Color.parseColor("#111111"))
|
|
132
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f)
|
|
133
|
+
setTypeface(typeface, Typeface.BOLD)
|
|
134
|
+
gravity = Gravity.CENTER
|
|
135
|
+
setPadding(0, 0, 0, dp(12))
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
addView(TextView(context).apply {
|
|
139
|
+
text = overlayText
|
|
140
|
+
setTextColor(Color.parseColor("#737373"))
|
|
141
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
|
142
|
+
gravity = Gravity.CENTER
|
|
143
|
+
})
|
|
119
144
|
}
|
|
120
|
-
return textView
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
private fun buildLayoutParams(): WindowManager.LayoutParams {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-blocker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Expo module for cross-platform app blocking. Android: UsageStatsManager + Overlay. iOS: Screen Time API (FamilyControls + ManagedSettings + DeviceActivity).",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
package/plugin/src/index.js
CHANGED
|
@@ -18,8 +18,20 @@ const path = require("path");
|
|
|
18
18
|
// Android
|
|
19
19
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
20
20
|
|
|
21
|
+
function getAndroidScheme(config, pluginConfig) {
|
|
22
|
+
if (pluginConfig?.android?.scheme) return pluginConfig.android.scheme;
|
|
23
|
+
const configScheme = Array.isArray(config.scheme) ? config.scheme[0] : config.scheme;
|
|
24
|
+
if (configScheme) return configScheme;
|
|
25
|
+
const pkg = config.android?.package;
|
|
26
|
+
if (pkg) return pkg.replace(/\./g, "-");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function withAppBlockerAndroid(config, pluginConfig) {
|
|
22
|
-
|
|
31
|
+
const scheme = getAndroidScheme(config, pluginConfig);
|
|
32
|
+
|
|
33
|
+
// Manifest: permissions, service, receiver, and deep-link intent filter
|
|
34
|
+
config = withAndroidManifest(config, (config) => {
|
|
23
35
|
const manifest = config.modResults;
|
|
24
36
|
const mainApplication = manifest.manifest.application?.[0];
|
|
25
37
|
if (!mainApplication) return config;
|
|
@@ -75,8 +87,64 @@ function withAppBlockerAndroid(config, pluginConfig) {
|
|
|
75
87
|
});
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
// Add deep-link intent filter to MainActivity so notification taps route back to the app
|
|
91
|
+
if (scheme) {
|
|
92
|
+
const activities = mainApplication.activity || [];
|
|
93
|
+
const mainActivity = activities.find(
|
|
94
|
+
(a) => a.$?.["android:name"] === ".MainActivity" || a.$?.["android:name"]?.endsWith(".MainActivity")
|
|
95
|
+
);
|
|
96
|
+
if (mainActivity) {
|
|
97
|
+
if (!mainActivity["intent-filter"]) mainActivity["intent-filter"] = [];
|
|
98
|
+
const alreadyHasScheme = mainActivity["intent-filter"].some((f) =>
|
|
99
|
+
(f.data || []).some((d) => d.$?.["android:scheme"] === scheme)
|
|
100
|
+
);
|
|
101
|
+
if (!alreadyHasScheme) {
|
|
102
|
+
mainActivity["intent-filter"].push({
|
|
103
|
+
action: [{ $: { "android:name": "android.intent.action.VIEW" } }],
|
|
104
|
+
category: [
|
|
105
|
+
{ $: { "android:name": "android.intent.category.DEFAULT" } },
|
|
106
|
+
{ $: { "android:name": "android.intent.category.BROWSABLE" } },
|
|
107
|
+
],
|
|
108
|
+
data: [{ $: { "android:scheme": scheme } }],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
78
114
|
return config;
|
|
79
115
|
});
|
|
116
|
+
|
|
117
|
+
// Write scheme to strings.xml so AppBlockerService can read it at runtime
|
|
118
|
+
if (scheme) {
|
|
119
|
+
config = withDangerousMod(config, [
|
|
120
|
+
"android",
|
|
121
|
+
(config) => {
|
|
122
|
+
const platformRoot = config.modRequest.platformProjectRoot;
|
|
123
|
+
const valuesDir = path.join(platformRoot, "app", "src", "main", "res", "values");
|
|
124
|
+
const stringsPath = path.join(valuesDir, "strings.xml");
|
|
125
|
+
|
|
126
|
+
if (!fs.existsSync(valuesDir)) {
|
|
127
|
+
fs.mkdirSync(valuesDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let xml = fs.existsSync(stringsPath)
|
|
131
|
+
? fs.readFileSync(stringsPath, "utf-8")
|
|
132
|
+
: '<?xml version="1.0" encoding="utf-8"?>\n<resources>\n</resources>';
|
|
133
|
+
|
|
134
|
+
const tag = `<string name="expo_app_blocker_scheme">${scheme}</string>`;
|
|
135
|
+
if (!xml.includes('name="expo_app_blocker_scheme"')) {
|
|
136
|
+
xml = xml.replace("</resources>", ` ${tag}\n</resources>`);
|
|
137
|
+
} else {
|
|
138
|
+
xml = xml.replace(/<string name="expo_app_blocker_scheme">.*?<\/string>/, tag);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fs.writeFileSync(stringsPath, xml);
|
|
142
|
+
return config;
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return config;
|
|
80
148
|
}
|
|
81
149
|
|
|
82
150
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -141,6 +141,15 @@ export interface ShieldConfig {
|
|
|
141
141
|
icon?: string;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
export interface AndroidConfig {
|
|
145
|
+
/** Text shown on the blocking overlay. Use {appName} as placeholder. Default: "{appName} is blocked." */
|
|
146
|
+
overlayText?: string;
|
|
147
|
+
/** Notification title when app is blocked. Use {appName} as placeholder. Default: "App Blocked" */
|
|
148
|
+
notificationTitle?: string;
|
|
149
|
+
/** Notification text when app is blocked. Use {appName} as placeholder. */
|
|
150
|
+
notificationText?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
144
153
|
export interface PluginConfig {
|
|
145
154
|
ios?: {
|
|
146
155
|
/** App Group identifier for shared data between app and extensions. Required. */
|
|
@@ -148,12 +157,12 @@ export interface PluginConfig {
|
|
|
148
157
|
/** Shield overlay customization */
|
|
149
158
|
shield?: ShieldConfig;
|
|
150
159
|
};
|
|
151
|
-
android?: {
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
android?: AndroidConfig & {
|
|
161
|
+
/**
|
|
162
|
+
* URL scheme used for deep-linking back into your app when a blocked app is detected.
|
|
163
|
+
* Defaults to your app's `scheme` from app.json, or the package name with dots replaced by hyphens.
|
|
164
|
+
* Must match the scheme registered in your AndroidManifest intent-filter.
|
|
165
|
+
*/
|
|
166
|
+
scheme?: string;
|
|
158
167
|
};
|
|
159
168
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
AndroidPermissions,
|
|
12
12
|
IOSPermissions,
|
|
13
13
|
AndroidBlockableApp,
|
|
14
|
+
AndroidConfig,
|
|
14
15
|
IOSBlockedItem,
|
|
15
16
|
IOSBlockConfiguration,
|
|
16
17
|
TemporaryUnlockResult,
|
|
@@ -30,6 +31,7 @@ export type {
|
|
|
30
31
|
TemporaryUnlockResult,
|
|
31
32
|
RelockResult,
|
|
32
33
|
ShieldConfig,
|
|
34
|
+
AndroidConfig,
|
|
33
35
|
PluginConfig,
|
|
34
36
|
FamilyActivityPickerSelectionEvent,
|
|
35
37
|
FamilyActivityPickerViewProps,
|
|
@@ -113,6 +115,11 @@ export function getBlockedApps(): string[] {
|
|
|
113
115
|
return NativeModule.getBlockedApps();
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
export function configureAndroid(config: AndroidConfig): void {
|
|
119
|
+
if (Platform.OS !== "android") return;
|
|
120
|
+
NativeModule.setAndroidConfig(config);
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
export function startMonitoring(): void {
|
|
117
124
|
if (Platform.OS !== "android") return;
|
|
118
125
|
NativeModule.startMonitoring();
|