@craft-native/android 0.0.72
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 +606 -0
- package/dist/index.js +410 -0
- package/dist/index.js.map +10 -0
- package/package.json +38 -0
- package/templates/AndroidManifest.xml.template +43 -0
- package/templates/CraftBridge.kt.template +3880 -0
- package/templates/CraftBridgeExtensions.kt.template +383 -0
- package/templates/CraftHealthConnect.kt.template +200 -0
- package/templates/CraftHealthConnectStub.kt.template +28 -0
- package/templates/CraftWidgetProvider.kt.template +246 -0
- package/templates/LocationRecordingService.kt.template +160 -0
- package/templates/MainActivity.kt.template +230 -0
- package/templates/build.gradle.kts.app.template +102 -0
- package/templates/build.gradle.kts.project.template +6 -0
- package/templates/fastlane/Appfile +25 -0
- package/templates/fastlane/Fastfile +192 -0
- package/templates/fastlane/Gemfile +7 -0
- package/templates/github-workflow-android.yml +248 -0
- package/templates/github-workflow-test.yml +329 -0
- package/templates/proguard-rules.pro.template +9 -0
- package/templates/settings.gradle.kts.template +18 -0
- package/templates/test-bridges.html +1463 -0
|
@@ -0,0 +1,3880 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.ClipData
|
|
6
|
+
import android.content.ClipboardManager
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.content.pm.PackageManager
|
|
10
|
+
import android.graphics.Bitmap
|
|
11
|
+
import android.hardware.camera2.CameraAccessException
|
|
12
|
+
import android.hardware.camera2.CameraCharacteristics
|
|
13
|
+
import android.hardware.camera2.CameraManager
|
|
14
|
+
import android.location.Location
|
|
15
|
+
import android.net.ConnectivityManager
|
|
16
|
+
import android.net.Network
|
|
17
|
+
import android.net.NetworkCapabilities
|
|
18
|
+
import android.net.NetworkRequest
|
|
19
|
+
import android.net.Uri
|
|
20
|
+
import android.os.Build
|
|
21
|
+
import android.os.Bundle
|
|
22
|
+
import android.os.Looper
|
|
23
|
+
import android.os.VibrationEffect
|
|
24
|
+
import android.os.Vibrator
|
|
25
|
+
import android.os.VibratorManager
|
|
26
|
+
import android.provider.MediaStore
|
|
27
|
+
import android.provider.Settings
|
|
28
|
+
import android.speech.RecognitionListener
|
|
29
|
+
import android.speech.RecognizerIntent
|
|
30
|
+
import android.speech.SpeechRecognizer
|
|
31
|
+
import android.util.DisplayMetrics
|
|
32
|
+
import android.webkit.JavascriptInterface
|
|
33
|
+
import android.webkit.WebView
|
|
34
|
+
import androidx.biometric.BiometricManager
|
|
35
|
+
import androidx.biometric.BiometricPrompt
|
|
36
|
+
import androidx.core.app.ActivityCompat
|
|
37
|
+
import androidx.core.app.NotificationManagerCompat
|
|
38
|
+
import androidx.core.content.ContextCompat
|
|
39
|
+
import androidx.fragment.app.FragmentActivity
|
|
40
|
+
import androidx.lifecycle.Lifecycle
|
|
41
|
+
import androidx.lifecycle.LifecycleEventObserver
|
|
42
|
+
import androidx.lifecycle.ProcessLifecycleOwner
|
|
43
|
+
import androidx.security.crypto.EncryptedSharedPreferences
|
|
44
|
+
import androidx.security.crypto.MasterKey
|
|
45
|
+
import com.google.android.gms.location.FusedLocationProviderClient
|
|
46
|
+
import com.google.android.gms.location.LocationCallback
|
|
47
|
+
import com.google.android.gms.location.LocationRequest
|
|
48
|
+
import com.google.android.gms.location.LocationResult
|
|
49
|
+
import com.google.android.gms.location.LocationServices
|
|
50
|
+
import com.google.android.gms.location.Priority
|
|
51
|
+
import com.google.android.play.core.review.ReviewManagerFactory
|
|
52
|
+
{{FIREBASE_IMPORT}}
|
|
53
|
+
import android.content.ContentResolver
|
|
54
|
+
import android.content.ContentUris
|
|
55
|
+
import android.content.ContentValues
|
|
56
|
+
import android.database.Cursor
|
|
57
|
+
import android.provider.CalendarContract
|
|
58
|
+
import android.provider.ContactsContract
|
|
59
|
+
import android.app.NotificationChannel
|
|
60
|
+
import android.app.NotificationManager
|
|
61
|
+
import android.app.PendingIntent
|
|
62
|
+
import android.view.WindowManager
|
|
63
|
+
import android.content.pm.ActivityInfo
|
|
64
|
+
import com.android.billingclient.api.*
|
|
65
|
+
import org.json.JSONObject
|
|
66
|
+
import org.json.JSONArray
|
|
67
|
+
import java.util.Locale
|
|
68
|
+
import java.util.concurrent.Executor
|
|
69
|
+
import java.util.Calendar
|
|
70
|
+
import java.util.TimeZone
|
|
71
|
+
import android.media.MediaRecorder
|
|
72
|
+
import android.hardware.Sensor
|
|
73
|
+
import android.hardware.SensorEvent
|
|
74
|
+
import android.hardware.SensorEventListener
|
|
75
|
+
import android.hardware.SensorManager
|
|
76
|
+
import android.database.sqlite.SQLiteDatabase
|
|
77
|
+
import android.bluetooth.BluetoothAdapter
|
|
78
|
+
import android.bluetooth.BluetoothManager
|
|
79
|
+
import android.bluetooth.le.BluetoothLeScanner
|
|
80
|
+
import android.bluetooth.le.ScanCallback
|
|
81
|
+
import android.bluetooth.le.ScanResult
|
|
82
|
+
import android.nfc.NfcAdapter
|
|
83
|
+
import android.nfc.tech.Ndef
|
|
84
|
+
import android.app.DownloadManager
|
|
85
|
+
import android.view.View
|
|
86
|
+
import android.util.Base64
|
|
87
|
+
import com.google.mlkit.vision.barcode.BarcodeScanning
|
|
88
|
+
import com.google.mlkit.vision.common.InputImage
|
|
89
|
+
import com.google.mlkit.vision.label.ImageLabeling
|
|
90
|
+
import com.google.mlkit.vision.label.defaults.ImageLabelerOptions
|
|
91
|
+
import com.google.mlkit.vision.objects.ObjectDetection
|
|
92
|
+
import com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions
|
|
93
|
+
import com.google.mlkit.vision.text.TextRecognition
|
|
94
|
+
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
|
|
95
|
+
import android.graphics.BitmapFactory
|
|
96
|
+
import java.io.ByteArrayOutputStream
|
|
97
|
+
import java.io.File
|
|
98
|
+
import java.util.concurrent.TimeUnit
|
|
99
|
+
import java.util.UUID
|
|
100
|
+
|
|
101
|
+
class CraftBridge(
|
|
102
|
+
private val activity: Activity,
|
|
103
|
+
private val webView: WebView,
|
|
104
|
+
private val healthConnect: CraftHealthConnect
|
|
105
|
+
) {
|
|
106
|
+
private var speechRecognizer: SpeechRecognizer? = null
|
|
107
|
+
private var isListening = false
|
|
108
|
+
private val mainExecutor: Executor = ContextCompat.getMainExecutor(activity)
|
|
109
|
+
|
|
110
|
+
// Location tracking
|
|
111
|
+
private var fusedLocationClient: FusedLocationProviderClient? = null
|
|
112
|
+
private var locationCallback: LocationCallback? = null
|
|
113
|
+
private var watchId = 0
|
|
114
|
+
|
|
115
|
+
// Network monitoring
|
|
116
|
+
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
|
117
|
+
|
|
118
|
+
// Flashlight state
|
|
119
|
+
private var isFlashlightOn = false
|
|
120
|
+
|
|
121
|
+
// App state tracking
|
|
122
|
+
private var currentAppState = "active"
|
|
123
|
+
|
|
124
|
+
// Keep awake
|
|
125
|
+
private var isKeepingAwake = false
|
|
126
|
+
|
|
127
|
+
// Billing client for in-app purchases
|
|
128
|
+
private var billingClient: BillingClient? = null
|
|
129
|
+
|
|
130
|
+
// Notification channel ID
|
|
131
|
+
private val notificationChannelId = "craft_notifications"
|
|
132
|
+
|
|
133
|
+
// Audio recording
|
|
134
|
+
private var mediaRecorder: MediaRecorder? = null
|
|
135
|
+
private var audioFile: File? = null
|
|
136
|
+
|
|
137
|
+
// Motion sensors
|
|
138
|
+
private var sensorManager: SensorManager? = null
|
|
139
|
+
private var accelerometer: Sensor? = null
|
|
140
|
+
private var gyroscope: Sensor? = null
|
|
141
|
+
private var sensorListener: SensorEventListener? = null
|
|
142
|
+
|
|
143
|
+
// SQLite database
|
|
144
|
+
private var database: SQLiteDatabase? = null
|
|
145
|
+
|
|
146
|
+
// Bluetooth
|
|
147
|
+
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
148
|
+
private var bluetoothScanner: BluetoothLeScanner? = null
|
|
149
|
+
private var bleScanCallback: ScanCallback? = null
|
|
150
|
+
|
|
151
|
+
// Encrypted shared preferences for secure storage
|
|
152
|
+
private val masterKey by lazy {
|
|
153
|
+
MasterKey.Builder(activity)
|
|
154
|
+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
155
|
+
.build()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private val securePrefs by lazy {
|
|
159
|
+
EncryptedSharedPreferences.create(
|
|
160
|
+
activity,
|
|
161
|
+
"craft_secure_prefs",
|
|
162
|
+
masterKey,
|
|
163
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
164
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fun injectBridge() {
|
|
169
|
+
val script = """
|
|
170
|
+
window.craft = {
|
|
171
|
+
platform: 'android',
|
|
172
|
+
capabilities: {
|
|
173
|
+
haptics: {{ENABLE_HAPTICS}},
|
|
174
|
+
speechRecognition: {{ENABLE_SPEECH}} && ${SpeechRecognizer.isRecognitionAvailable(activity)},
|
|
175
|
+
share: {{ENABLE_SHARE}},
|
|
176
|
+
camera: {{ENABLE_CAMERA}},
|
|
177
|
+
biometric: {{ENABLE_BIOMETRIC}} && ${isBiometricAvailable()},
|
|
178
|
+
pushNotifications: {{ENABLE_PUSH}},
|
|
179
|
+
secureStorage: {{ENABLE_SECURE_STORAGE}},
|
|
180
|
+
geolocation: {{ENABLE_GEOLOCATION}},
|
|
181
|
+
backgroundLocation: {{ENABLE_BACKGROUND_LOCATION}},
|
|
182
|
+
clipboard: false,
|
|
183
|
+
deviceInfo: true,
|
|
184
|
+
appBadge: {{ENABLE_PUSH}},
|
|
185
|
+
networkStatus: true,
|
|
186
|
+
appReview: true,
|
|
187
|
+
flashlight: {{ENABLE_CAMERA}} && ${hasFlashlight()},
|
|
188
|
+
openURL: true,
|
|
189
|
+
vibrationPattern: true,
|
|
190
|
+
appState: true,
|
|
191
|
+
contacts: false,
|
|
192
|
+
calendar: false,
|
|
193
|
+
localNotifications: {{ENABLE_PUSH}},
|
|
194
|
+
inAppPurchase: false,
|
|
195
|
+
keepAwake: {{ENABLE_KEEP_AWAKE}},
|
|
196
|
+
orientationLock: false,
|
|
197
|
+
deepLinks: {{ENABLE_DEEP_LINKS}},
|
|
198
|
+
qrScanner: false,
|
|
199
|
+
filePicker: false,
|
|
200
|
+
fileDownload: false,
|
|
201
|
+
googleSignIn: false,
|
|
202
|
+
audioRecording: false,
|
|
203
|
+
videoRecording: false,
|
|
204
|
+
motionSensors: false,
|
|
205
|
+
localDatabase: false,
|
|
206
|
+
bluetooth: false,
|
|
207
|
+
nfc: false,
|
|
208
|
+
health: {{ENABLE_HEALTH_CONNECT}},
|
|
209
|
+
screenCapture: false
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// Haptic feedback
|
|
213
|
+
haptic: function(style) {
|
|
214
|
+
CraftAndroid.haptic(style || 'medium');
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// Speech recognition
|
|
218
|
+
startListening: function() {
|
|
219
|
+
CraftAndroid.startListening();
|
|
220
|
+
},
|
|
221
|
+
stopListening: function() {
|
|
222
|
+
CraftAndroid.stopListening();
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
// Share
|
|
226
|
+
share: function(text, title) {
|
|
227
|
+
CraftAndroid.share(text, title || '');
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// Camera
|
|
231
|
+
openCamera: function() {
|
|
232
|
+
return new Promise(function(resolve, reject) {
|
|
233
|
+
window._craftCameraResolve = resolve;
|
|
234
|
+
window._craftCameraReject = reject;
|
|
235
|
+
CraftAndroid.openCamera();
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
pickImage: function() {
|
|
239
|
+
return new Promise(function(resolve, reject) {
|
|
240
|
+
window._craftGalleryResolve = resolve;
|
|
241
|
+
window._craftGalleryReject = reject;
|
|
242
|
+
CraftAndroid.pickImage();
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
// Biometric auth
|
|
247
|
+
authenticate: function(reason) {
|
|
248
|
+
return new Promise(function(resolve, reject) {
|
|
249
|
+
window._craftBiometricResolve = resolve;
|
|
250
|
+
window._craftBiometricReject = reject;
|
|
251
|
+
CraftAndroid.authenticate(reason || 'Authenticate to continue');
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// Push notifications
|
|
256
|
+
registerPush: function() {
|
|
257
|
+
return new Promise(function(resolve, reject) {
|
|
258
|
+
window._craftPushResolve = resolve;
|
|
259
|
+
window._craftPushReject = reject;
|
|
260
|
+
CraftAndroid.registerPush();
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
// Secure storage
|
|
265
|
+
secureStore: {
|
|
266
|
+
set: function(key, value) {
|
|
267
|
+
return CraftAndroid.secureSet(key, value);
|
|
268
|
+
},
|
|
269
|
+
get: function(key) {
|
|
270
|
+
return CraftAndroid.secureGet(key);
|
|
271
|
+
},
|
|
272
|
+
remove: function(key) {
|
|
273
|
+
return CraftAndroid.secureRemove(key);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
// Geolocation
|
|
278
|
+
getCurrentPosition: function(options) {
|
|
279
|
+
return new Promise(function(resolve, reject) {
|
|
280
|
+
window._craftLocationResolve = resolve;
|
|
281
|
+
window._craftLocationReject = reject;
|
|
282
|
+
CraftAndroid.getCurrentPosition(JSON.stringify(options || {}));
|
|
283
|
+
});
|
|
284
|
+
},
|
|
285
|
+
watchPosition: function(callback, options) {
|
|
286
|
+
var watchId = CraftAndroid.watchPosition(JSON.stringify(options || {}));
|
|
287
|
+
window['_craftLocationWatch_' + watchId] = callback;
|
|
288
|
+
return watchId;
|
|
289
|
+
},
|
|
290
|
+
clearWatch: function(watchId) {
|
|
291
|
+
CraftAndroid.clearWatch(watchId);
|
|
292
|
+
delete window['_craftLocationWatch_' + watchId];
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// Clipboard
|
|
296
|
+
clipboard: {
|
|
297
|
+
read: function() {
|
|
298
|
+
return new Promise(function(resolve) {
|
|
299
|
+
resolve(CraftAndroid.clipboardRead());
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
write: function(text) {
|
|
303
|
+
return new Promise(function(resolve) {
|
|
304
|
+
resolve(CraftAndroid.clipboardWrite(text));
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
// Device Info
|
|
310
|
+
getDeviceInfo: function() {
|
|
311
|
+
return JSON.parse(CraftAndroid.getDeviceInfo());
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
// App Badge
|
|
315
|
+
setBadge: function(count) {
|
|
316
|
+
CraftAndroid.setBadge(count);
|
|
317
|
+
},
|
|
318
|
+
clearBadge: function() {
|
|
319
|
+
CraftAndroid.clearBadge();
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
// Network Status
|
|
323
|
+
getNetworkStatus: function() {
|
|
324
|
+
return JSON.parse(CraftAndroid.getNetworkStatus());
|
|
325
|
+
},
|
|
326
|
+
onNetworkChange: function(callback) {
|
|
327
|
+
window._craftNetworkChangeCallback = callback;
|
|
328
|
+
CraftAndroid.startNetworkMonitoring();
|
|
329
|
+
},
|
|
330
|
+
offNetworkChange: function() {
|
|
331
|
+
window._craftNetworkChangeCallback = null;
|
|
332
|
+
CraftAndroid.stopNetworkMonitoring();
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// App Review
|
|
336
|
+
requestReview: function() {
|
|
337
|
+
return new Promise(function(resolve, reject) {
|
|
338
|
+
window._craftReviewResolve = resolve;
|
|
339
|
+
window._craftReviewReject = reject;
|
|
340
|
+
CraftAndroid.requestReview();
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
// Flashlight
|
|
345
|
+
setFlashlight: function(on) {
|
|
346
|
+
return CraftAndroid.setFlashlight(on);
|
|
347
|
+
},
|
|
348
|
+
toggleFlashlight: function() {
|
|
349
|
+
return CraftAndroid.toggleFlashlight();
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
// Open URL
|
|
353
|
+
openURL: function(url) {
|
|
354
|
+
return CraftAndroid.openURL(url);
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
// Vibration Pattern
|
|
358
|
+
vibrate: function(pattern) {
|
|
359
|
+
CraftAndroid.vibrate(JSON.stringify(pattern));
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
// App State
|
|
363
|
+
getAppState: function() {
|
|
364
|
+
return CraftAndroid.getAppState();
|
|
365
|
+
},
|
|
366
|
+
onAppStateChange: function(callback) {
|
|
367
|
+
window._craftAppStateCallback = callback;
|
|
368
|
+
CraftAndroid.startAppStateMonitoring();
|
|
369
|
+
},
|
|
370
|
+
offAppStateChange: function() {
|
|
371
|
+
window._craftAppStateCallback = null;
|
|
372
|
+
CraftAndroid.stopAppStateMonitoring();
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// Contacts
|
|
376
|
+
contacts: {
|
|
377
|
+
getAll: function() {
|
|
378
|
+
return new Promise(function(resolve, reject) {
|
|
379
|
+
window._craftContactsResolve = resolve;
|
|
380
|
+
window._craftContactsReject = reject;
|
|
381
|
+
CraftAndroid.getContacts();
|
|
382
|
+
});
|
|
383
|
+
},
|
|
384
|
+
add: function(contact) {
|
|
385
|
+
return new Promise(function(resolve, reject) {
|
|
386
|
+
window._craftAddContactResolve = resolve;
|
|
387
|
+
window._craftAddContactReject = reject;
|
|
388
|
+
CraftAndroid.addContact(JSON.stringify(contact));
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
// Calendar
|
|
394
|
+
calendar: {
|
|
395
|
+
getEvents: function(startDate, endDate) {
|
|
396
|
+
return new Promise(function(resolve, reject) {
|
|
397
|
+
window._craftCalendarResolve = resolve;
|
|
398
|
+
window._craftCalendarReject = reject;
|
|
399
|
+
CraftAndroid.getCalendarEvents(startDate || 0, endDate || 0);
|
|
400
|
+
});
|
|
401
|
+
},
|
|
402
|
+
createEvent: function(event) {
|
|
403
|
+
return new Promise(function(resolve, reject) {
|
|
404
|
+
window._craftCreateEventResolve = resolve;
|
|
405
|
+
window._craftCreateEventReject = reject;
|
|
406
|
+
CraftAndroid.createCalendarEvent(JSON.stringify(event));
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
deleteEvent: function(eventId) {
|
|
410
|
+
return new Promise(function(resolve, reject) {
|
|
411
|
+
window._craftDeleteEventResolve = resolve;
|
|
412
|
+
window._craftDeleteEventReject = reject;
|
|
413
|
+
CraftAndroid.deleteCalendarEvent(eventId);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
// Local Notifications
|
|
419
|
+
notifications: {
|
|
420
|
+
schedule: function(notification) {
|
|
421
|
+
return new Promise(function(resolve, reject) {
|
|
422
|
+
window._craftNotifResolve = resolve;
|
|
423
|
+
window._craftNotifReject = reject;
|
|
424
|
+
CraftAndroid.scheduleNotification(JSON.stringify(notification));
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
cancel: function(id) {
|
|
428
|
+
CraftAndroid.cancelNotification(id);
|
|
429
|
+
},
|
|
430
|
+
cancelAll: function() {
|
|
431
|
+
CraftAndroid.cancelAllNotifications();
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
// In-App Purchase
|
|
436
|
+
iap: {
|
|
437
|
+
getProducts: function(productIds) {
|
|
438
|
+
return new Promise(function(resolve, reject) {
|
|
439
|
+
window._craftProductsResolve = resolve;
|
|
440
|
+
window._craftProductsReject = reject;
|
|
441
|
+
CraftAndroid.getProducts(JSON.stringify(productIds));
|
|
442
|
+
});
|
|
443
|
+
},
|
|
444
|
+
purchase: function(productId) {
|
|
445
|
+
return new Promise(function(resolve, reject) {
|
|
446
|
+
window._craftPurchaseResolve = resolve;
|
|
447
|
+
window._craftPurchaseReject = reject;
|
|
448
|
+
CraftAndroid.purchase(productId);
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
restore: function() {
|
|
452
|
+
return new Promise(function(resolve, reject) {
|
|
453
|
+
window._craftRestoreResolve = resolve;
|
|
454
|
+
window._craftRestoreReject = reject;
|
|
455
|
+
CraftAndroid.restorePurchases();
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
// Keep Awake
|
|
461
|
+
setKeepAwake: function(enabled) {
|
|
462
|
+
return CraftAndroid.setKeepAwake(enabled);
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
// Orientation Lock
|
|
466
|
+
lockOrientation: function(orientation) {
|
|
467
|
+
return CraftAndroid.lockOrientation(orientation);
|
|
468
|
+
},
|
|
469
|
+
unlockOrientation: function() {
|
|
470
|
+
return CraftAndroid.unlockOrientation();
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
// Deep Links
|
|
474
|
+
onDeepLink: function(callback) {
|
|
475
|
+
window.addEventListener('craftDeepLink', function(e) { callback(e.detail); });
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
// QR/Barcode Scanner
|
|
479
|
+
scanQRCode: function() {
|
|
480
|
+
return new Promise(function(resolve, reject) {
|
|
481
|
+
window._craftQRResolve = resolve;
|
|
482
|
+
window._craftQRReject = reject;
|
|
483
|
+
CraftAndroid.scanQRCode();
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
// File Picker
|
|
488
|
+
pickFile: function(types) {
|
|
489
|
+
return new Promise(function(resolve, reject) {
|
|
490
|
+
window._craftFileResolve = resolve;
|
|
491
|
+
window._craftFileReject = reject;
|
|
492
|
+
CraftAndroid.pickFile(JSON.stringify(types || []));
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
// File Download
|
|
497
|
+
downloadFile: function(url, filename) {
|
|
498
|
+
return new Promise(function(resolve, reject) {
|
|
499
|
+
window._craftDownloadResolve = resolve;
|
|
500
|
+
window._craftDownloadReject = reject;
|
|
501
|
+
CraftAndroid.downloadFile(url, filename);
|
|
502
|
+
});
|
|
503
|
+
},
|
|
504
|
+
saveFile: function(data, filename) {
|
|
505
|
+
return new Promise(function(resolve, reject) {
|
|
506
|
+
window._craftSaveResolve = resolve;
|
|
507
|
+
window._craftSaveReject = reject;
|
|
508
|
+
CraftAndroid.saveFile(data, filename);
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
// Google Sign In
|
|
513
|
+
signInWithGoogle: function() {
|
|
514
|
+
return new Promise(function(resolve, reject) {
|
|
515
|
+
window._craftGoogleResolve = resolve;
|
|
516
|
+
window._craftGoogleReject = reject;
|
|
517
|
+
CraftAndroid.signInWithGoogle();
|
|
518
|
+
});
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
// Audio Recording
|
|
522
|
+
startAudioRecording: function() {
|
|
523
|
+
return new Promise(function(resolve, reject) {
|
|
524
|
+
window._craftAudioResolve = resolve;
|
|
525
|
+
window._craftAudioReject = reject;
|
|
526
|
+
CraftAndroid.startAudioRecording();
|
|
527
|
+
});
|
|
528
|
+
},
|
|
529
|
+
stopAudioRecording: function() {
|
|
530
|
+
return new Promise(function(resolve, reject) {
|
|
531
|
+
window._craftAudioStopResolve = resolve;
|
|
532
|
+
window._craftAudioStopReject = reject;
|
|
533
|
+
CraftAndroid.stopAudioRecording();
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
// Video Recording
|
|
538
|
+
startVideoRecording: function() {
|
|
539
|
+
return new Promise(function(resolve, reject) {
|
|
540
|
+
window._craftVideoResolve = resolve;
|
|
541
|
+
window._craftVideoReject = reject;
|
|
542
|
+
CraftAndroid.startVideoRecording();
|
|
543
|
+
});
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
// Motion Sensors
|
|
547
|
+
startMotionUpdates: function(interval) {
|
|
548
|
+
return CraftAndroid.startMotionUpdates(interval || 100);
|
|
549
|
+
},
|
|
550
|
+
stopMotionUpdates: function() {
|
|
551
|
+
CraftAndroid.stopMotionUpdates();
|
|
552
|
+
},
|
|
553
|
+
onMotionUpdate: function(callback) {
|
|
554
|
+
window.addEventListener('craftMotionUpdate', function(e) { callback(e.detail); });
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
// Local Database
|
|
558
|
+
db: {
|
|
559
|
+
execute: function(sql, params) {
|
|
560
|
+
return new Promise(function(resolve, reject) {
|
|
561
|
+
window._craftDbExecResolve = resolve;
|
|
562
|
+
window._craftDbExecReject = reject;
|
|
563
|
+
CraftAndroid.dbExecute(sql, JSON.stringify(params || []));
|
|
564
|
+
});
|
|
565
|
+
},
|
|
566
|
+
query: function(sql, params) {
|
|
567
|
+
return new Promise(function(resolve, reject) {
|
|
568
|
+
window._craftDbQueryResolve = resolve;
|
|
569
|
+
window._craftDbQueryReject = reject;
|
|
570
|
+
CraftAndroid.dbQuery(sql, JSON.stringify(params || []));
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
// Bluetooth
|
|
576
|
+
bluetooth: {
|
|
577
|
+
startScan: function() {
|
|
578
|
+
return new Promise(function(resolve, reject) {
|
|
579
|
+
window._craftBleResolve = resolve;
|
|
580
|
+
window._craftBleReject = reject;
|
|
581
|
+
CraftAndroid.startBluetoothScan();
|
|
582
|
+
});
|
|
583
|
+
},
|
|
584
|
+
stopScan: function() {
|
|
585
|
+
CraftAndroid.stopBluetoothScan();
|
|
586
|
+
},
|
|
587
|
+
onDevice: function(callback) {
|
|
588
|
+
window.addEventListener('craftBluetoothDevice', function(e) { callback(e.detail); });
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
// NFC
|
|
593
|
+
scanNFC: function() {
|
|
594
|
+
return new Promise(function(resolve, reject) {
|
|
595
|
+
window._craftNfcResolve = resolve;
|
|
596
|
+
window._craftNfcReject = reject;
|
|
597
|
+
CraftAndroid.scanNFC();
|
|
598
|
+
});
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
// Fitness / Health
|
|
602
|
+
health: {
|
|
603
|
+
requestAuthorization: function(types) {
|
|
604
|
+
return new Promise(function(resolve, reject) {
|
|
605
|
+
window._craftFitnessAuthResolve = resolve;
|
|
606
|
+
window._craftFitnessAuthReject = reject;
|
|
607
|
+
CraftAndroid.requestFitnessAuthorization(JSON.stringify(types || []));
|
|
608
|
+
});
|
|
609
|
+
},
|
|
610
|
+
getData: function(type, options) {
|
|
611
|
+
options = options || {};
|
|
612
|
+
return new Promise(function(resolve, reject) {
|
|
613
|
+
window._craftFitnessDataResolve = resolve;
|
|
614
|
+
window._craftFitnessDataReject = reject;
|
|
615
|
+
CraftAndroid.getFitnessData(type, options.startDate || 0, options.endDate || 0);
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
saveWorkout: function(workout) {
|
|
619
|
+
return new Promise(function(resolve, reject) {
|
|
620
|
+
window._craftFitnessSaveResolve = resolve;
|
|
621
|
+
window._craftFitnessSaveReject = reject;
|
|
622
|
+
CraftAndroid.saveHealthWorkout(JSON.stringify(workout || {}));
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
// Screen Capture
|
|
628
|
+
takeScreenshot: function() {
|
|
629
|
+
return new Promise(function(resolve, reject) {
|
|
630
|
+
window._craftScreenshotResolve = resolve;
|
|
631
|
+
window._craftScreenshotReject = reject;
|
|
632
|
+
CraftAndroid.takeScreenshot();
|
|
633
|
+
});
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
// Background Tasks
|
|
637
|
+
backgroundTask: {
|
|
638
|
+
register: function(taskId) {
|
|
639
|
+
return new Promise(function(resolve, reject) {
|
|
640
|
+
window._craftBgTaskResolve = resolve;
|
|
641
|
+
window._craftBgTaskReject = reject;
|
|
642
|
+
CraftAndroid.registerBackgroundTask(taskId);
|
|
643
|
+
});
|
|
644
|
+
},
|
|
645
|
+
schedule: function(taskId, options) {
|
|
646
|
+
options = options || {};
|
|
647
|
+
return new Promise(function(resolve, reject) {
|
|
648
|
+
window._craftBgTaskResolve = resolve;
|
|
649
|
+
window._craftBgTaskReject = reject;
|
|
650
|
+
CraftAndroid.scheduleBackgroundTask(
|
|
651
|
+
taskId,
|
|
652
|
+
options.delay || 900,
|
|
653
|
+
options.requiresNetwork || false,
|
|
654
|
+
options.requiresCharging || false
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
},
|
|
658
|
+
cancel: function(taskId) {
|
|
659
|
+
return new Promise(function(resolve, reject) {
|
|
660
|
+
window._craftBgTaskResolve = resolve;
|
|
661
|
+
window._craftBgTaskReject = reject;
|
|
662
|
+
CraftAndroid.cancelBackgroundTask(taskId);
|
|
663
|
+
});
|
|
664
|
+
},
|
|
665
|
+
cancelAll: function() {
|
|
666
|
+
return new Promise(function(resolve, reject) {
|
|
667
|
+
window._craftBgTaskResolve = resolve;
|
|
668
|
+
window._craftBgTaskReject = reject;
|
|
669
|
+
CraftAndroid.cancelAllBackgroundTasks();
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
// PDF Viewer
|
|
675
|
+
openPDF: function(source, page) {
|
|
676
|
+
return new Promise(function(resolve, reject) {
|
|
677
|
+
window._craftPDFResolve = resolve;
|
|
678
|
+
window._craftPDFReject = reject;
|
|
679
|
+
CraftAndroid.openPDF(source, page || 0);
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
closePDF: function() {
|
|
683
|
+
return new Promise(function(resolve, reject) {
|
|
684
|
+
window._craftPDFResolve = resolve;
|
|
685
|
+
window._craftPDFReject = reject;
|
|
686
|
+
CraftAndroid.closePDF();
|
|
687
|
+
});
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
// Contacts Picker
|
|
691
|
+
pickContact: function(options) {
|
|
692
|
+
options = options || {};
|
|
693
|
+
return new Promise(function(resolve, reject) {
|
|
694
|
+
window._craftPickContactResolve = resolve;
|
|
695
|
+
window._craftPickContactReject = reject;
|
|
696
|
+
CraftAndroid.pickContact(options.multiple || false);
|
|
697
|
+
});
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
// App Shortcuts
|
|
701
|
+
shortcuts: {
|
|
702
|
+
set: function(shortcuts) {
|
|
703
|
+
return new Promise(function(resolve, reject) {
|
|
704
|
+
window._craftShortcutsResolve = resolve;
|
|
705
|
+
window._craftShortcutsReject = reject;
|
|
706
|
+
CraftAndroid.setShortcuts(JSON.stringify(shortcuts));
|
|
707
|
+
});
|
|
708
|
+
},
|
|
709
|
+
clear: function() {
|
|
710
|
+
return new Promise(function(resolve, reject) {
|
|
711
|
+
window._craftShortcutsResolve = resolve;
|
|
712
|
+
window._craftShortcutsReject = reject;
|
|
713
|
+
CraftAndroid.clearShortcuts();
|
|
714
|
+
});
|
|
715
|
+
},
|
|
716
|
+
onShortcut: function(callback) {
|
|
717
|
+
window.addEventListener('craftShortcut', function(e) { callback(e.detail); });
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
// Shared Preferences (Android equivalent of Keychain Sharing)
|
|
722
|
+
sharedKeychain: {
|
|
723
|
+
set: function(key, value, group) {
|
|
724
|
+
return new Promise(function(resolve, reject) {
|
|
725
|
+
window._craftSharedKeychainResolve = resolve;
|
|
726
|
+
window._craftSharedKeychainReject = reject;
|
|
727
|
+
CraftAndroid.setSharedItem(key, value, group || '');
|
|
728
|
+
});
|
|
729
|
+
},
|
|
730
|
+
get: function(key, group) {
|
|
731
|
+
return new Promise(function(resolve, reject) {
|
|
732
|
+
window._craftSharedKeychainResolve = resolve;
|
|
733
|
+
window._craftSharedKeychainReject = reject;
|
|
734
|
+
CraftAndroid.getSharedItem(key, group || '');
|
|
735
|
+
});
|
|
736
|
+
},
|
|
737
|
+
remove: function(key, group) {
|
|
738
|
+
return new Promise(function(resolve, reject) {
|
|
739
|
+
window._craftSharedKeychainResolve = resolve;
|
|
740
|
+
window._craftSharedKeychainReject = reject;
|
|
741
|
+
CraftAndroid.removeSharedItem(key, group || '');
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
|
|
746
|
+
// Local Auth Persistence
|
|
747
|
+
authPersistence: {
|
|
748
|
+
enable: function(duration) {
|
|
749
|
+
return new Promise(function(resolve, reject) {
|
|
750
|
+
window._craftAuthPersistResolve = resolve;
|
|
751
|
+
window._craftAuthPersistReject = reject;
|
|
752
|
+
CraftAndroid.setAuthPersistence(true, duration || 300);
|
|
753
|
+
});
|
|
754
|
+
},
|
|
755
|
+
disable: function() {
|
|
756
|
+
return new Promise(function(resolve, reject) {
|
|
757
|
+
window._craftAuthPersistResolve = resolve;
|
|
758
|
+
window._craftAuthPersistReject = reject;
|
|
759
|
+
CraftAndroid.setAuthPersistence(false, 0);
|
|
760
|
+
});
|
|
761
|
+
},
|
|
762
|
+
check: function() {
|
|
763
|
+
return new Promise(function(resolve, reject) {
|
|
764
|
+
window._craftAuthPersistResolve = resolve;
|
|
765
|
+
window._craftAuthPersistReject = reject;
|
|
766
|
+
CraftAndroid.checkAuthPersistence();
|
|
767
|
+
});
|
|
768
|
+
},
|
|
769
|
+
clear: function() {
|
|
770
|
+
return new Promise(function(resolve, reject) {
|
|
771
|
+
window._craftAuthPersistResolve = resolve;
|
|
772
|
+
window._craftAuthPersistReject = reject;
|
|
773
|
+
CraftAndroid.clearAuthPersistence();
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
|
|
778
|
+
// AR (ARCore) - Note: Full ARCore requires native Activity integration
|
|
779
|
+
ar: {
|
|
780
|
+
start: function(options) {
|
|
781
|
+
return new Promise(function(resolve, reject) {
|
|
782
|
+
window._craftARResolve = resolve;
|
|
783
|
+
window._craftARReject = reject;
|
|
784
|
+
CraftAndroid.startAR(JSON.stringify(options || {}));
|
|
785
|
+
});
|
|
786
|
+
},
|
|
787
|
+
stop: function() {
|
|
788
|
+
return new Promise(function(resolve, reject) {
|
|
789
|
+
window._craftARResolve = resolve;
|
|
790
|
+
window._craftARReject = reject;
|
|
791
|
+
CraftAndroid.stopAR();
|
|
792
|
+
});
|
|
793
|
+
},
|
|
794
|
+
placeObject: function(model, position) {
|
|
795
|
+
return new Promise(function(resolve, reject) {
|
|
796
|
+
window._craftARResolve = resolve;
|
|
797
|
+
window._craftARReject = reject;
|
|
798
|
+
CraftAndroid.placeARObject(model, JSON.stringify(position || {}));
|
|
799
|
+
});
|
|
800
|
+
},
|
|
801
|
+
removeObject: function(objectId) {
|
|
802
|
+
return new Promise(function(resolve, reject) {
|
|
803
|
+
window._craftARResolve = resolve;
|
|
804
|
+
window._craftARReject = reject;
|
|
805
|
+
CraftAndroid.removeARObject(objectId);
|
|
806
|
+
});
|
|
807
|
+
},
|
|
808
|
+
getPlanes: function() {
|
|
809
|
+
return new Promise(function(resolve, reject) {
|
|
810
|
+
window._craftARResolve = resolve;
|
|
811
|
+
window._craftARReject = reject;
|
|
812
|
+
CraftAndroid.getARPlanes();
|
|
813
|
+
});
|
|
814
|
+
},
|
|
815
|
+
onPlaneDetected: function(callback) {
|
|
816
|
+
window.addEventListener('craftARPlane', function(e) { callback(e.detail); });
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
// ML (ML Kit)
|
|
821
|
+
ml: {
|
|
822
|
+
classifyImage: function(imageBase64) {
|
|
823
|
+
return new Promise(function(resolve, reject) {
|
|
824
|
+
window._craftMLResolve = resolve;
|
|
825
|
+
window._craftMLReject = reject;
|
|
826
|
+
CraftAndroid.classifyImage(imageBase64);
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
detectObjects: function(imageBase64) {
|
|
830
|
+
return new Promise(function(resolve, reject) {
|
|
831
|
+
window._craftMLResolve = resolve;
|
|
832
|
+
window._craftMLReject = reject;
|
|
833
|
+
CraftAndroid.detectObjects(imageBase64);
|
|
834
|
+
});
|
|
835
|
+
},
|
|
836
|
+
recognizeText: function(imageBase64) {
|
|
837
|
+
return new Promise(function(resolve, reject) {
|
|
838
|
+
window._craftMLResolve = resolve;
|
|
839
|
+
window._craftMLReject = reject;
|
|
840
|
+
CraftAndroid.recognizeText(imageBase64);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
// Deep Links
|
|
846
|
+
deepLinks: {
|
|
847
|
+
getInitialURL: function() {
|
|
848
|
+
return new Promise(function(resolve, reject) {
|
|
849
|
+
window._craftDeepLinkResolve = resolve;
|
|
850
|
+
window._craftDeepLinkReject = reject;
|
|
851
|
+
CraftAndroid.getInitialURL();
|
|
852
|
+
});
|
|
853
|
+
},
|
|
854
|
+
onLink: function(callback) {
|
|
855
|
+
window.addEventListener('craftDeepLink', function(e) { callback(e.detail); });
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
// Logging
|
|
860
|
+
log: function(msg) {
|
|
861
|
+
CraftAndroid.log(msg);
|
|
862
|
+
},
|
|
863
|
+
|
|
864
|
+
// Performance Profiling
|
|
865
|
+
_profiling: false,
|
|
866
|
+
_profilingData: null,
|
|
867
|
+
_profilingCallTimings: [],
|
|
868
|
+
|
|
869
|
+
startProfiling: function() {
|
|
870
|
+
var self = this;
|
|
871
|
+
this._profiling = true;
|
|
872
|
+
this._profilingData = {
|
|
873
|
+
startTime: Date.now(),
|
|
874
|
+
startMemory: null,
|
|
875
|
+
callTimings: [],
|
|
876
|
+
bridgeCalls: 0
|
|
877
|
+
};
|
|
878
|
+
this._profilingCallTimings = [];
|
|
879
|
+
// Get memory info
|
|
880
|
+
var memInfo = CraftAndroid.getMemoryUsage();
|
|
881
|
+
try { this._profilingData.startMemory = JSON.parse(memInfo); } catch(e) {}
|
|
882
|
+
console.log('[Craft] Profiling started');
|
|
883
|
+
return { started: true, timestamp: this._profilingData.startTime };
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
stopProfiling: function() {
|
|
887
|
+
if (!this._profiling || !this._profilingData) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
this._profiling = false;
|
|
891
|
+
var memInfo = null;
|
|
892
|
+
try { memInfo = JSON.parse(CraftAndroid.getMemoryUsage()); } catch(e) {}
|
|
893
|
+
var report = {
|
|
894
|
+
duration: Date.now() - this._profilingData.startTime,
|
|
895
|
+
bridgeCalls: this._profilingCallTimings.length,
|
|
896
|
+
callTimings: this._profilingCallTimings,
|
|
897
|
+
memory: {
|
|
898
|
+
start: this._profilingData.startMemory,
|
|
899
|
+
end: memInfo
|
|
900
|
+
},
|
|
901
|
+
avgCallTime: this._profilingCallTimings.length > 0
|
|
902
|
+
? this._profilingCallTimings.reduce(function(a, b) { return a + b.duration; }, 0) / this._profilingCallTimings.length
|
|
903
|
+
: 0
|
|
904
|
+
};
|
|
905
|
+
this._profilingData = null;
|
|
906
|
+
console.log('[Craft] Profiling stopped', report);
|
|
907
|
+
return report;
|
|
908
|
+
},
|
|
909
|
+
|
|
910
|
+
_recordCallTiming: function(action, startTime, endTime) {
|
|
911
|
+
if (this._profiling) {
|
|
912
|
+
this._profilingCallTimings.push({
|
|
913
|
+
action: action,
|
|
914
|
+
startTime: startTime,
|
|
915
|
+
endTime: endTime,
|
|
916
|
+
duration: endTime - startTime
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
|
|
921
|
+
getProfilingData: function() {
|
|
922
|
+
if (!this._profilingData) return null;
|
|
923
|
+
return {
|
|
924
|
+
running: this._profiling,
|
|
925
|
+
duration: Date.now() - this._profilingData.startTime,
|
|
926
|
+
bridgeCalls: this._profilingCallTimings.length,
|
|
927
|
+
callTimings: this._profilingCallTimings
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
|
|
931
|
+
// OTA Updates
|
|
932
|
+
ota: {
|
|
933
|
+
_config: null,
|
|
934
|
+
_status: 'idle',
|
|
935
|
+
_progressCallbacks: [],
|
|
936
|
+
_statusCallbacks: [],
|
|
937
|
+
|
|
938
|
+
configure: function(options) {
|
|
939
|
+
this._config = options;
|
|
940
|
+
CraftAndroid.otaConfigure(JSON.stringify(options));
|
|
941
|
+
},
|
|
942
|
+
checkForUpdate: function() {
|
|
943
|
+
return new Promise(function(resolve, reject) {
|
|
944
|
+
window._craftOTACheckResolve = resolve;
|
|
945
|
+
window._craftOTACheckReject = reject;
|
|
946
|
+
CraftAndroid.otaCheckForUpdate();
|
|
947
|
+
});
|
|
948
|
+
},
|
|
949
|
+
downloadUpdate: function(options) {
|
|
950
|
+
return new Promise(function(resolve, reject) {
|
|
951
|
+
window._craftOTADownloadResolve = resolve;
|
|
952
|
+
window._craftOTADownloadReject = reject;
|
|
953
|
+
CraftAndroid.otaDownloadUpdate(JSON.stringify(options || {}));
|
|
954
|
+
});
|
|
955
|
+
},
|
|
956
|
+
applyUpdate: function() {
|
|
957
|
+
return new Promise(function(resolve, reject) {
|
|
958
|
+
window._craftOTAApplyResolve = resolve;
|
|
959
|
+
window._craftOTAApplyReject = reject;
|
|
960
|
+
CraftAndroid.otaApplyUpdate();
|
|
961
|
+
});
|
|
962
|
+
},
|
|
963
|
+
rollback: function() {
|
|
964
|
+
return new Promise(function(resolve, reject) {
|
|
965
|
+
window._craftOTARollbackResolve = resolve;
|
|
966
|
+
window._craftOTARollbackReject = reject;
|
|
967
|
+
CraftAndroid.otaRollback();
|
|
968
|
+
});
|
|
969
|
+
},
|
|
970
|
+
getCurrentBundle: function() {
|
|
971
|
+
var bundleJson = CraftAndroid.otaGetCurrentBundle();
|
|
972
|
+
try { return JSON.parse(bundleJson); }
|
|
973
|
+
catch(e) { return { version: '1.0.0', buildNumber: 1, hash: '', isOriginal: true, installedAt: '' }; }
|
|
974
|
+
},
|
|
975
|
+
onProgress: function(callback) {
|
|
976
|
+
this._progressCallbacks.push(callback);
|
|
977
|
+
window.addEventListener('craftOTAProgress', function(e) { callback(e.detail); });
|
|
978
|
+
},
|
|
979
|
+
onStatusChange: function(callback) {
|
|
980
|
+
this._statusCallbacks.push(callback);
|
|
981
|
+
window.addEventListener('craftOTAStatus', function(e) { callback(e.detail.status); });
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
// Debug Mode
|
|
986
|
+
_debug: false,
|
|
987
|
+
_lastError: null,
|
|
988
|
+
_errorHistory: [],
|
|
989
|
+
_callLog: [],
|
|
990
|
+
_networkLog: [],
|
|
991
|
+
_consoleLog: [],
|
|
992
|
+
_originalConsole: null,
|
|
993
|
+
|
|
994
|
+
// Error code mappings for user-friendly messages
|
|
995
|
+
_errorMessages: {
|
|
996
|
+
'PERMISSION_DENIED': 'Permission was denied. Please grant access in Settings.',
|
|
997
|
+
'NOT_AVAILABLE': 'This feature is not available on this device.',
|
|
998
|
+
'CANCELLED': 'The operation was cancelled by the user.',
|
|
999
|
+
'NETWORK_ERROR': 'A network error occurred. Please check your connection.',
|
|
1000
|
+
'TIMEOUT': 'The operation timed out. Please try again.',
|
|
1001
|
+
'INVALID_PARAMS': 'Invalid parameters provided.',
|
|
1002
|
+
'NOT_FOUND': 'The requested resource was not found.',
|
|
1003
|
+
'AUTH_FAILED': 'Authentication failed.',
|
|
1004
|
+
'STORAGE_FULL': 'Storage is full. Please free up space.',
|
|
1005
|
+
'CRAFT_ERROR': 'An unexpected error occurred.'
|
|
1006
|
+
},
|
|
1007
|
+
|
|
1008
|
+
// Get user-friendly error message
|
|
1009
|
+
getErrorMessage: function(code) {
|
|
1010
|
+
return this._errorMessages[code] || this._errorMessages['CRAFT_ERROR'];
|
|
1011
|
+
},
|
|
1012
|
+
|
|
1013
|
+
// Store error for debugging
|
|
1014
|
+
_storeError: function(error, code, nativeStack) {
|
|
1015
|
+
this._lastError = {
|
|
1016
|
+
message: error,
|
|
1017
|
+
code: code || 'CRAFT_ERROR',
|
|
1018
|
+
timestamp: Date.now(),
|
|
1019
|
+
nativeStack: nativeStack || null,
|
|
1020
|
+
jsStack: new Error().stack
|
|
1021
|
+
};
|
|
1022
|
+
if (this._debug) {
|
|
1023
|
+
window.dispatchEvent(new CustomEvent('craftError', {detail: this._lastError}));
|
|
1024
|
+
console.error('[Craft Error]', code, error);
|
|
1025
|
+
}
|
|
1026
|
+
this._errorHistory.push(this._lastError);
|
|
1027
|
+
if (this._errorHistory.length > 50) this._errorHistory.shift();
|
|
1028
|
+
},
|
|
1029
|
+
|
|
1030
|
+
// Get error history
|
|
1031
|
+
getErrorHistory: function() {
|
|
1032
|
+
return this._errorHistory.slice();
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1035
|
+
// Clear error history
|
|
1036
|
+
clearErrorHistory: function() {
|
|
1037
|
+
this._errorHistory = [];
|
|
1038
|
+
this._lastError = null;
|
|
1039
|
+
},
|
|
1040
|
+
|
|
1041
|
+
// Get last error details
|
|
1042
|
+
getLastError: function() {
|
|
1043
|
+
return this._lastError;
|
|
1044
|
+
},
|
|
1045
|
+
|
|
1046
|
+
// Enable/disable debug mode
|
|
1047
|
+
setDebugMode: function(enabled) {
|
|
1048
|
+
this._debug = enabled;
|
|
1049
|
+
if (enabled) {
|
|
1050
|
+
this._callLog = [];
|
|
1051
|
+
this._networkLog = [];
|
|
1052
|
+
this._setupConsoleCapture();
|
|
1053
|
+
this._setupNetworkInspector();
|
|
1054
|
+
console.log('[Craft] Debug mode enabled');
|
|
1055
|
+
} else {
|
|
1056
|
+
this._restoreConsole();
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
|
|
1060
|
+
// Setup console capture
|
|
1061
|
+
_setupConsoleCapture: function() {
|
|
1062
|
+
if (this._originalConsole) return;
|
|
1063
|
+
var self = this;
|
|
1064
|
+
this._originalConsole = {
|
|
1065
|
+
log: console.log,
|
|
1066
|
+
warn: console.warn,
|
|
1067
|
+
error: console.error,
|
|
1068
|
+
info: console.info,
|
|
1069
|
+
debug: console.debug
|
|
1070
|
+
};
|
|
1071
|
+
['log', 'warn', 'error', 'info', 'debug'].forEach(function(level) {
|
|
1072
|
+
console[level] = function() {
|
|
1073
|
+
var args = Array.prototype.slice.call(arguments);
|
|
1074
|
+
self._consoleLog.push({
|
|
1075
|
+
level: level,
|
|
1076
|
+
args: args.map(function(a) {
|
|
1077
|
+
try { return typeof a === 'object' ? JSON.stringify(a) : String(a); }
|
|
1078
|
+
catch(e) { return String(a); }
|
|
1079
|
+
}),
|
|
1080
|
+
timestamp: Date.now()
|
|
1081
|
+
});
|
|
1082
|
+
if (self._consoleLog.length > 200) self._consoleLog.shift();
|
|
1083
|
+
self._originalConsole[level].apply(console, arguments);
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
1086
|
+
},
|
|
1087
|
+
|
|
1088
|
+
// Restore original console
|
|
1089
|
+
_restoreConsole: function() {
|
|
1090
|
+
if (!this._originalConsole) return;
|
|
1091
|
+
console.log = this._originalConsole.log;
|
|
1092
|
+
console.warn = this._originalConsole.warn;
|
|
1093
|
+
console.error = this._originalConsole.error;
|
|
1094
|
+
console.info = this._originalConsole.info;
|
|
1095
|
+
console.debug = this._originalConsole.debug;
|
|
1096
|
+
this._originalConsole = null;
|
|
1097
|
+
},
|
|
1098
|
+
|
|
1099
|
+
// Setup network inspector
|
|
1100
|
+
_setupNetworkInspector: function() {
|
|
1101
|
+
var self = this;
|
|
1102
|
+
if (window._craftNetworkSetup) return;
|
|
1103
|
+
window._craftNetworkSetup = true;
|
|
1104
|
+
|
|
1105
|
+
var originalFetch = window.fetch;
|
|
1106
|
+
window.fetch = function(url, options) {
|
|
1107
|
+
var startTime = Date.now();
|
|
1108
|
+
var entry = {
|
|
1109
|
+
type: 'fetch',
|
|
1110
|
+
url: typeof url === 'string' ? url : url.url,
|
|
1111
|
+
method: (options && options.method) || 'GET',
|
|
1112
|
+
startTime: startTime,
|
|
1113
|
+
status: null,
|
|
1114
|
+
duration: null
|
|
1115
|
+
};
|
|
1116
|
+
self._networkLog.push(entry);
|
|
1117
|
+
if (self._networkLog.length > 100) self._networkLog.shift();
|
|
1118
|
+
|
|
1119
|
+
return originalFetch.apply(window, arguments).then(function(response) {
|
|
1120
|
+
entry.status = response.status;
|
|
1121
|
+
entry.duration = Date.now() - startTime;
|
|
1122
|
+
return response;
|
|
1123
|
+
}).catch(function(err) {
|
|
1124
|
+
entry.status = 'error';
|
|
1125
|
+
entry.error = err.message;
|
|
1126
|
+
entry.duration = Date.now() - startTime;
|
|
1127
|
+
throw err;
|
|
1128
|
+
});
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
var OriginalXHR = window.XMLHttpRequest;
|
|
1132
|
+
window.XMLHttpRequest = function() {
|
|
1133
|
+
var xhr = new OriginalXHR();
|
|
1134
|
+
var entry = { type: 'xhr', url: '', method: '', startTime: null, status: null, duration: null };
|
|
1135
|
+
|
|
1136
|
+
var originalOpen = xhr.open;
|
|
1137
|
+
xhr.open = function(method, url) {
|
|
1138
|
+
entry.method = method;
|
|
1139
|
+
entry.url = url;
|
|
1140
|
+
return originalOpen.apply(xhr, arguments);
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
var originalSend = xhr.send;
|
|
1144
|
+
xhr.send = function() {
|
|
1145
|
+
entry.startTime = Date.now();
|
|
1146
|
+
self._networkLog.push(entry);
|
|
1147
|
+
if (self._networkLog.length > 100) self._networkLog.shift();
|
|
1148
|
+
|
|
1149
|
+
xhr.addEventListener('loadend', function() {
|
|
1150
|
+
entry.status = xhr.status;
|
|
1151
|
+
entry.duration = Date.now() - entry.startTime;
|
|
1152
|
+
});
|
|
1153
|
+
return originalSend.apply(xhr, arguments);
|
|
1154
|
+
};
|
|
1155
|
+
return xhr;
|
|
1156
|
+
};
|
|
1157
|
+
},
|
|
1158
|
+
|
|
1159
|
+
// Get console log
|
|
1160
|
+
getConsoleLog: function() {
|
|
1161
|
+
return this._consoleLog.slice();
|
|
1162
|
+
},
|
|
1163
|
+
|
|
1164
|
+
// Clear console log
|
|
1165
|
+
clearConsoleLog: function() {
|
|
1166
|
+
this._consoleLog = [];
|
|
1167
|
+
},
|
|
1168
|
+
|
|
1169
|
+
// Get network log
|
|
1170
|
+
getNetworkLog: function() {
|
|
1171
|
+
return this._networkLog.slice();
|
|
1172
|
+
},
|
|
1173
|
+
|
|
1174
|
+
// Clear network log
|
|
1175
|
+
clearNetworkLog: function() {
|
|
1176
|
+
this._networkLog = [];
|
|
1177
|
+
},
|
|
1178
|
+
|
|
1179
|
+
// Get call log
|
|
1180
|
+
getCallLog: function() {
|
|
1181
|
+
return this._callLog;
|
|
1182
|
+
},
|
|
1183
|
+
|
|
1184
|
+
// Clear call log
|
|
1185
|
+
clearCallLog: function() {
|
|
1186
|
+
this._callLog = [];
|
|
1187
|
+
},
|
|
1188
|
+
|
|
1189
|
+
// Internal: log bridge call
|
|
1190
|
+
_logCall: function(action, params) {
|
|
1191
|
+
if (this._debug) {
|
|
1192
|
+
var entry = {
|
|
1193
|
+
action: action,
|
|
1194
|
+
params: params,
|
|
1195
|
+
timestamp: Date.now()
|
|
1196
|
+
};
|
|
1197
|
+
this._callLog.push(entry);
|
|
1198
|
+
if (this._callLog.length > 100) this._callLog.shift();
|
|
1199
|
+
console.log('[Craft] ' + action, params);
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
|
|
1203
|
+
// Get full debug report
|
|
1204
|
+
getDebugReport: function() {
|
|
1205
|
+
return {
|
|
1206
|
+
enabled: this._debug,
|
|
1207
|
+
lastError: this._lastError,
|
|
1208
|
+
errorHistory: this._errorHistory.slice(-10),
|
|
1209
|
+
callLog: this._callLog.slice(-20),
|
|
1210
|
+
networkLog: this._networkLog.slice(-20),
|
|
1211
|
+
consoleLog: this._consoleLog.slice(-50),
|
|
1212
|
+
timestamp: Date.now()
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
(function installCraftMobileContract(craft) {
|
|
1218
|
+
var legacyShare = craft.share.bind(craft);
|
|
1219
|
+
var legacyOpenCamera = craft.openCamera.bind(craft);
|
|
1220
|
+
var legacyPickImage = craft.pickImage.bind(craft);
|
|
1221
|
+
var legacyAuthenticate = craft.authenticate.bind(craft);
|
|
1222
|
+
var legacySecureStore = craft.secureStore;
|
|
1223
|
+
craft.contractVersion = '1.0.0';
|
|
1224
|
+
craft.device = {
|
|
1225
|
+
getInfo: function() { return Promise.resolve(craft.getDeviceInfo()); },
|
|
1226
|
+
getCapabilities: function() { return Promise.resolve(Object.assign({}, craft.capabilities)); }
|
|
1227
|
+
};
|
|
1228
|
+
craft.haptics = {
|
|
1229
|
+
impact: function(style) { craft.haptic(style || 'medium'); return Promise.resolve(); },
|
|
1230
|
+
notification: function(type) { craft.haptic(type === 'error' ? 'heavy' : 'light'); return Promise.resolve(); },
|
|
1231
|
+
selection: function() { craft.haptic('selection'); return Promise.resolve(); },
|
|
1232
|
+
vibrate: function(pattern) { craft.vibrate(pattern || []); return Promise.resolve(); }
|
|
1233
|
+
};
|
|
1234
|
+
craft.camera = {
|
|
1235
|
+
takePicture: function() { return legacyOpenCamera().then(normalizePhoto); },
|
|
1236
|
+
pickImage: function() { return legacyPickImage().then(normalizePhoto); },
|
|
1237
|
+
pickMultiple: function() { return legacyPickImage().then(function(photo) { return [normalizePhoto(photo)]; }); },
|
|
1238
|
+
isAvailable: function() { return Promise.resolve(!!craft.capabilities.camera); }
|
|
1239
|
+
};
|
|
1240
|
+
craft.biometrics = {
|
|
1241
|
+
isAvailable: function() { return Promise.resolve(!!craft.capabilities.biometric); },
|
|
1242
|
+
getBiometricType: function() { return Promise.resolve(craft.capabilities.biometric ? 'fingerprint' : null); },
|
|
1243
|
+
authenticate: function(reason) { return legacyAuthenticate(reason); }
|
|
1244
|
+
};
|
|
1245
|
+
craft.secureStorage = {
|
|
1246
|
+
set: function(key, value) { return Promise.resolve(legacySecureStore.set(key, value)).then(function() {}); },
|
|
1247
|
+
get: function(key) { return Promise.resolve(legacySecureStore.get(key)); },
|
|
1248
|
+
delete: function(key) { return Promise.resolve(legacySecureStore.remove(key)).then(function() {}); },
|
|
1249
|
+
clear: function() { return Promise.resolve(CraftAndroid.secureClear()).then(function() {}); }
|
|
1250
|
+
};
|
|
1251
|
+
craft.location = {
|
|
1252
|
+
getCurrentPosition: craft.getCurrentPosition,
|
|
1253
|
+
watchPosition: craft.watchPosition,
|
|
1254
|
+
clearWatch: craft.clearWatch,
|
|
1255
|
+
startRecording: function(options) { return Promise.resolve(JSON.parse(CraftAndroid.startLocationRecording(JSON.stringify(options || {})))); },
|
|
1256
|
+
pauseRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.pauseLocationRecording())); },
|
|
1257
|
+
resumeRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.resumeLocationRecording())); },
|
|
1258
|
+
stopRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.stopLocationRecording())); },
|
|
1259
|
+
getRecordingState: function() { return Promise.resolve(JSON.parse(CraftAndroid.getLocationRecordingState())); },
|
|
1260
|
+
readRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.readLocationRecording())); }
|
|
1261
|
+
};
|
|
1262
|
+
var shareApi = function(text, title) { return legacyShare(text, title); };
|
|
1263
|
+
shareApi.share = function(options) {
|
|
1264
|
+
options = options || {};
|
|
1265
|
+
return Promise.resolve(legacyShare([options.text, options.url].filter(Boolean).join(' '), options.title || ''));
|
|
1266
|
+
};
|
|
1267
|
+
craft.share = shareApi;
|
|
1268
|
+
craft.lifecycle = {
|
|
1269
|
+
getState: function() { return document.visibilityState === 'visible' ? 'active' : 'background'; },
|
|
1270
|
+
onStateChange: function(callback) {
|
|
1271
|
+
var handler = function() { callback(document.visibilityState === 'visible' ? 'active' : 'background'); };
|
|
1272
|
+
document.addEventListener('visibilitychange', handler);
|
|
1273
|
+
return function() { document.removeEventListener('visibilitychange', handler); };
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
function normalizePhoto(photo) {
|
|
1277
|
+
if (photo && typeof photo === 'object') return photo;
|
|
1278
|
+
var value = String(photo || '');
|
|
1279
|
+
var base64 = value.indexOf(',') >= 0 ? value.split(',').pop() : value;
|
|
1280
|
+
return {base64: base64, uri: value, width: 0, height: 0, mimeType: 'image/jpeg'};
|
|
1281
|
+
}
|
|
1282
|
+
})(window.craft);
|
|
1283
|
+
|
|
1284
|
+
// Dispatch ready event
|
|
1285
|
+
window.dispatchEvent(new CustomEvent('craftReady', {detail: window.craft}));
|
|
1286
|
+
console.log('Craft Android bridge initialized');
|
|
1287
|
+
""".trimIndent()
|
|
1288
|
+
|
|
1289
|
+
activity.runOnUiThread {
|
|
1290
|
+
webView.evaluateJavascript(script, null)
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// ==================== Haptics ====================
|
|
1295
|
+
|
|
1296
|
+
@JavascriptInterface
|
|
1297
|
+
fun haptic(style: String) {
|
|
1298
|
+
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
1299
|
+
val vibratorManager = activity.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
|
1300
|
+
vibratorManager.defaultVibrator
|
|
1301
|
+
} else {
|
|
1302
|
+
@Suppress("DEPRECATION")
|
|
1303
|
+
activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1307
|
+
val effect = when (style) {
|
|
1308
|
+
"light" -> VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE)
|
|
1309
|
+
"medium" -> VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)
|
|
1310
|
+
"heavy" -> VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)
|
|
1311
|
+
"success" -> VibrationEffect.createWaveform(longArrayOf(0, 30, 50, 30), -1)
|
|
1312
|
+
"warning" -> VibrationEffect.createWaveform(longArrayOf(0, 50, 100, 50), -1)
|
|
1313
|
+
"error" -> VibrationEffect.createWaveform(longArrayOf(0, 100, 50, 100), -1)
|
|
1314
|
+
"selection" -> VibrationEffect.createOneShot(5, VibrationEffect.DEFAULT_AMPLITUDE)
|
|
1315
|
+
else -> VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)
|
|
1316
|
+
}
|
|
1317
|
+
vibrator.vibrate(effect)
|
|
1318
|
+
} else {
|
|
1319
|
+
@Suppress("DEPRECATION")
|
|
1320
|
+
vibrator.vibrate(20)
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ==================== Speech Recognition ====================
|
|
1325
|
+
|
|
1326
|
+
@JavascriptInterface
|
|
1327
|
+
fun startListening() {
|
|
1328
|
+
if (!SpeechRecognizer.isRecognitionAvailable(activity)) {
|
|
1329
|
+
sendEvent("craftSpeechError", mapOf("error" to "Speech recognition not available"))
|
|
1330
|
+
return
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
activity.runOnUiThread {
|
|
1334
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
|
|
1335
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
1336
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO), 100)
|
|
1337
|
+
return@runOnUiThread
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(activity)
|
|
1341
|
+
speechRecognizer?.setRecognitionListener(object : RecognitionListener {
|
|
1342
|
+
override fun onReadyForSpeech(params: Bundle?) {
|
|
1343
|
+
isListening = true
|
|
1344
|
+
sendEvent("craftSpeechStart", emptyMap())
|
|
1345
|
+
haptic("light")
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
override fun onBeginningOfSpeech() {}
|
|
1349
|
+
override fun onRmsChanged(rmsdB: Float) {}
|
|
1350
|
+
override fun onBufferReceived(buffer: ByteArray?) {}
|
|
1351
|
+
override fun onEndOfSpeech() {}
|
|
1352
|
+
|
|
1353
|
+
override fun onError(error: Int) {
|
|
1354
|
+
isListening = false
|
|
1355
|
+
val errorMsg = when (error) {
|
|
1356
|
+
SpeechRecognizer.ERROR_AUDIO -> "Audio recording error"
|
|
1357
|
+
SpeechRecognizer.ERROR_CLIENT -> "Client error"
|
|
1358
|
+
SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "Insufficient permissions"
|
|
1359
|
+
SpeechRecognizer.ERROR_NETWORK -> "Network error"
|
|
1360
|
+
SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> "Network timeout"
|
|
1361
|
+
SpeechRecognizer.ERROR_NO_MATCH -> "No match"
|
|
1362
|
+
SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> "Recognizer busy"
|
|
1363
|
+
SpeechRecognizer.ERROR_SERVER -> "Server error"
|
|
1364
|
+
SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "Speech timeout"
|
|
1365
|
+
else -> "Unknown error"
|
|
1366
|
+
}
|
|
1367
|
+
sendEvent("craftSpeechError", mapOf("error" to errorMsg))
|
|
1368
|
+
haptic("light")
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
override fun onResults(results: Bundle?) {
|
|
1372
|
+
isListening = false
|
|
1373
|
+
val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
|
|
1374
|
+
val transcript = matches?.firstOrNull() ?: ""
|
|
1375
|
+
sendEvent("craftSpeechResult", mapOf("transcript" to transcript, "isFinal" to true))
|
|
1376
|
+
sendEvent("craftSpeechEnd", emptyMap())
|
|
1377
|
+
haptic("light")
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
override fun onPartialResults(partialResults: Bundle?) {
|
|
1381
|
+
val matches = partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
|
|
1382
|
+
val transcript = matches?.firstOrNull() ?: ""
|
|
1383
|
+
sendEvent("craftSpeechResult", mapOf("transcript" to transcript, "isFinal" to false))
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
override fun onEvent(eventType: Int, params: Bundle?) {}
|
|
1387
|
+
})
|
|
1388
|
+
|
|
1389
|
+
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
|
1390
|
+
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
|
1391
|
+
putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
|
|
1392
|
+
putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
|
|
1393
|
+
putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
speechRecognizer?.startListening(intent)
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
@JavascriptInterface
|
|
1401
|
+
fun stopListening() {
|
|
1402
|
+
activity.runOnUiThread {
|
|
1403
|
+
speechRecognizer?.stopListening()
|
|
1404
|
+
speechRecognizer?.destroy()
|
|
1405
|
+
speechRecognizer = null
|
|
1406
|
+
isListening = false
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ==================== Share ====================
|
|
1411
|
+
|
|
1412
|
+
@JavascriptInterface
|
|
1413
|
+
fun share(text: String, title: String) {
|
|
1414
|
+
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
1415
|
+
type = "text/plain"
|
|
1416
|
+
putExtra(Intent.EXTRA_TEXT, text)
|
|
1417
|
+
if (title.isNotEmpty()) {
|
|
1418
|
+
putExtra(Intent.EXTRA_SUBJECT, title)
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
activity.startActivity(Intent.createChooser(intent, "Share"))
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// ==================== Camera & Gallery ====================
|
|
1425
|
+
|
|
1426
|
+
@JavascriptInterface
|
|
1427
|
+
fun openCamera() {
|
|
1428
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
|
|
1429
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
1430
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), 101)
|
|
1431
|
+
return
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
|
1435
|
+
activity.startActivityForResult(intent, REQUEST_CAMERA)
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
@JavascriptInterface
|
|
1439
|
+
fun pickImage() {
|
|
1440
|
+
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
|
|
1441
|
+
activity.startActivityForResult(intent, REQUEST_GALLERY)
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
fun handleImageResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
1445
|
+
if (resultCode != Activity.RESULT_OK) {
|
|
1446
|
+
val event = if (requestCode == REQUEST_CAMERA) "_craftCameraReject" else "_craftGalleryReject"
|
|
1447
|
+
activity.runOnUiThread {
|
|
1448
|
+
webView.evaluateJavascript("window.$event && window.$event('Cancelled')", null)
|
|
1449
|
+
}
|
|
1450
|
+
return
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
val imageUri: String? = when (requestCode) {
|
|
1454
|
+
REQUEST_CAMERA -> {
|
|
1455
|
+
// For camera, we get a bitmap thumbnail
|
|
1456
|
+
val bitmap = data?.extras?.get("data") as? Bitmap
|
|
1457
|
+
bitmap?.let { "data:image/png;base64,${bitmapToBase64(it)}" }
|
|
1458
|
+
}
|
|
1459
|
+
REQUEST_GALLERY -> {
|
|
1460
|
+
data?.data?.toString()
|
|
1461
|
+
}
|
|
1462
|
+
else -> null
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
val event = if (requestCode == REQUEST_CAMERA) "_craftCameraResolve" else "_craftGalleryResolve"
|
|
1466
|
+
activity.runOnUiThread {
|
|
1467
|
+
webView.evaluateJavascript("window.$event && window.$event('$imageUri')", null)
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
private fun bitmapToBase64(bitmap: Bitmap): String {
|
|
1472
|
+
val outputStream = java.io.ByteArrayOutputStream()
|
|
1473
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
|
1474
|
+
return android.util.Base64.encodeToString(outputStream.toByteArray(), android.util.Base64.NO_WRAP)
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// ==================== Biometric Auth ====================
|
|
1478
|
+
|
|
1479
|
+
private fun isBiometricAvailable(): Boolean {
|
|
1480
|
+
val biometricManager = BiometricManager.from(activity)
|
|
1481
|
+
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
|
|
1482
|
+
BiometricManager.BIOMETRIC_SUCCESS
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
@JavascriptInterface
|
|
1486
|
+
fun authenticate(reason: String) {
|
|
1487
|
+
if (activity !is FragmentActivity) {
|
|
1488
|
+
activity.runOnUiThread {
|
|
1489
|
+
webView.evaluateJavascript(
|
|
1490
|
+
"window._craftBiometricReject && window._craftBiometricReject('Activity not supported')",
|
|
1491
|
+
null
|
|
1492
|
+
)
|
|
1493
|
+
}
|
|
1494
|
+
return
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
activity.runOnUiThread {
|
|
1498
|
+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
1499
|
+
.setTitle("Authenticate")
|
|
1500
|
+
.setSubtitle(reason)
|
|
1501
|
+
.setNegativeButtonText("Cancel")
|
|
1502
|
+
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
|
1503
|
+
.build()
|
|
1504
|
+
|
|
1505
|
+
val biometricPrompt = BiometricPrompt(
|
|
1506
|
+
activity as FragmentActivity,
|
|
1507
|
+
mainExecutor,
|
|
1508
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
1509
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
1510
|
+
webView.evaluateJavascript(
|
|
1511
|
+
"window._craftBiometricResolve && window._craftBiometricResolve(true)",
|
|
1512
|
+
null
|
|
1513
|
+
)
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
1517
|
+
webView.evaluateJavascript(
|
|
1518
|
+
"window._craftBiometricReject && window._craftBiometricReject('$errString')",
|
|
1519
|
+
null
|
|
1520
|
+
)
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
override fun onAuthenticationFailed() {
|
|
1524
|
+
// Don't reject yet - user can retry
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
biometricPrompt.authenticate(promptInfo)
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// ==================== Push Notifications ====================
|
|
1534
|
+
|
|
1535
|
+
@JavascriptInterface
|
|
1536
|
+
fun registerPush() {
|
|
1537
|
+
{{REGISTER_PUSH_IMPLEMENTATION}}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// ==================== Secure Storage ====================
|
|
1541
|
+
|
|
1542
|
+
@JavascriptInterface
|
|
1543
|
+
fun secureSet(key: String, value: String): Boolean {
|
|
1544
|
+
return try {
|
|
1545
|
+
securePrefs.edit().putString(key, value).apply()
|
|
1546
|
+
true
|
|
1547
|
+
} catch (e: Exception) {
|
|
1548
|
+
false
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
@JavascriptInterface
|
|
1553
|
+
fun secureGet(key: String): String? {
|
|
1554
|
+
return try {
|
|
1555
|
+
securePrefs.getString(key, null)
|
|
1556
|
+
} catch (e: Exception) {
|
|
1557
|
+
null
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
@JavascriptInterface
|
|
1562
|
+
fun secureRemove(key: String): Boolean {
|
|
1563
|
+
return try {
|
|
1564
|
+
securePrefs.edit().remove(key).apply()
|
|
1565
|
+
true
|
|
1566
|
+
} catch (e: Exception) {
|
|
1567
|
+
false
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
@JavascriptInterface
|
|
1572
|
+
fun secureClear(): Boolean {
|
|
1573
|
+
return try {
|
|
1574
|
+
securePrefs.edit().clear().apply()
|
|
1575
|
+
true
|
|
1576
|
+
} catch (e: Exception) {
|
|
1577
|
+
false
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// ==================== Logging ====================
|
|
1582
|
+
|
|
1583
|
+
@JavascriptInterface
|
|
1584
|
+
fun log(message: String) {
|
|
1585
|
+
android.util.Log.d("CraftBridge", message)
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// ==================== Geolocation ====================
|
|
1589
|
+
|
|
1590
|
+
@JavascriptInterface
|
|
1591
|
+
fun getCurrentPosition(optionsJson: String) {
|
|
1592
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)
|
|
1593
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
1594
|
+
ActivityCompat.requestPermissions(
|
|
1595
|
+
activity,
|
|
1596
|
+
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
|
|
1597
|
+
REQUEST_LOCATION
|
|
1598
|
+
)
|
|
1599
|
+
activity.runOnUiThread {
|
|
1600
|
+
webView.evaluateJavascript(
|
|
1601
|
+
"window._craftLocationReject && window._craftLocationReject({code: 1, message: 'Permission denied'})",
|
|
1602
|
+
null
|
|
1603
|
+
)
|
|
1604
|
+
}
|
|
1605
|
+
return
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
|
|
1609
|
+
fusedLocationClient?.lastLocation?.addOnSuccessListener { location: Location? ->
|
|
1610
|
+
if (location != null) {
|
|
1611
|
+
val json = JSONObject().apply {
|
|
1612
|
+
put("latitude", location.latitude)
|
|
1613
|
+
put("longitude", location.longitude)
|
|
1614
|
+
put("accuracy", location.accuracy)
|
|
1615
|
+
put("altitude", location.altitude)
|
|
1616
|
+
put("speed", location.speed)
|
|
1617
|
+
put("heading", location.bearing)
|
|
1618
|
+
put("timestamp", location.time)
|
|
1619
|
+
}
|
|
1620
|
+
activity.runOnUiThread {
|
|
1621
|
+
webView.evaluateJavascript(
|
|
1622
|
+
"window._craftLocationResolve && window._craftLocationResolve($json)",
|
|
1623
|
+
null
|
|
1624
|
+
)
|
|
1625
|
+
}
|
|
1626
|
+
} else {
|
|
1627
|
+
// Request fresh location if last location is null
|
|
1628
|
+
requestFreshLocation()
|
|
1629
|
+
}
|
|
1630
|
+
}?.addOnFailureListener { e ->
|
|
1631
|
+
activity.runOnUiThread {
|
|
1632
|
+
webView.evaluateJavascript(
|
|
1633
|
+
"window._craftLocationReject && window._craftLocationReject({code: 2, message: '${e.message}'})",
|
|
1634
|
+
null
|
|
1635
|
+
)
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
private fun requestFreshLocation() {
|
|
1641
|
+
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
|
1642
|
+
return
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000)
|
|
1646
|
+
.setWaitForAccurateLocation(false)
|
|
1647
|
+
.setMinUpdateIntervalMillis(500)
|
|
1648
|
+
.setMaxUpdateDelayMillis(1000)
|
|
1649
|
+
.setMaxUpdates(1)
|
|
1650
|
+
.build()
|
|
1651
|
+
|
|
1652
|
+
val callback = object : LocationCallback() {
|
|
1653
|
+
override fun onLocationResult(result: LocationResult) {
|
|
1654
|
+
fusedLocationClient?.removeLocationUpdates(this)
|
|
1655
|
+
val location = result.lastLocation
|
|
1656
|
+
if (location != null) {
|
|
1657
|
+
val json = JSONObject().apply {
|
|
1658
|
+
put("latitude", location.latitude)
|
|
1659
|
+
put("longitude", location.longitude)
|
|
1660
|
+
put("accuracy", location.accuracy)
|
|
1661
|
+
put("altitude", location.altitude)
|
|
1662
|
+
put("speed", location.speed)
|
|
1663
|
+
put("heading", location.bearing)
|
|
1664
|
+
put("timestamp", location.time)
|
|
1665
|
+
}
|
|
1666
|
+
activity.runOnUiThread {
|
|
1667
|
+
webView.evaluateJavascript(
|
|
1668
|
+
"window._craftLocationResolve && window._craftLocationResolve($json)",
|
|
1669
|
+
null
|
|
1670
|
+
)
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
fusedLocationClient?.requestLocationUpdates(locationRequest, callback, Looper.getMainLooper())
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
@JavascriptInterface
|
|
1680
|
+
fun watchPosition(optionsJson: String): Int {
|
|
1681
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)
|
|
1682
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
1683
|
+
return -1
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
val currentWatchId = ++watchId
|
|
1687
|
+
fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
|
|
1688
|
+
|
|
1689
|
+
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000)
|
|
1690
|
+
.setMinUpdateIntervalMillis(2000)
|
|
1691
|
+
.build()
|
|
1692
|
+
|
|
1693
|
+
locationCallback = object : LocationCallback() {
|
|
1694
|
+
override fun onLocationResult(result: LocationResult) {
|
|
1695
|
+
val location = result.lastLocation ?: return
|
|
1696
|
+
val json = JSONObject().apply {
|
|
1697
|
+
put("latitude", location.latitude)
|
|
1698
|
+
put("longitude", location.longitude)
|
|
1699
|
+
put("accuracy", location.accuracy)
|
|
1700
|
+
put("altitude", location.altitude)
|
|
1701
|
+
put("speed", location.speed)
|
|
1702
|
+
put("heading", location.bearing)
|
|
1703
|
+
put("timestamp", location.time)
|
|
1704
|
+
}
|
|
1705
|
+
activity.runOnUiThread {
|
|
1706
|
+
webView.evaluateJavascript(
|
|
1707
|
+
"window._craftLocationWatch_$currentWatchId && window._craftLocationWatch_$currentWatchId($json)",
|
|
1708
|
+
null
|
|
1709
|
+
)
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
|
1715
|
+
fusedLocationClient?.requestLocationUpdates(locationRequest, locationCallback!!, Looper.getMainLooper())
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
return currentWatchId
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
@JavascriptInterface
|
|
1722
|
+
fun clearWatch(watchId: Int) {
|
|
1723
|
+
locationCallback?.let {
|
|
1724
|
+
fusedLocationClient?.removeLocationUpdates(it)
|
|
1725
|
+
}
|
|
1726
|
+
locationCallback = null
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
@JavascriptInterface
|
|
1730
|
+
fun startLocationRecording(optionsJson: String): String {
|
|
1731
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
|
1732
|
+
return JSONObject().apply {
|
|
1733
|
+
put("id", JSONObject.NULL)
|
|
1734
|
+
put("active", false)
|
|
1735
|
+
put("paused", false)
|
|
1736
|
+
put("startedAt", JSONObject.NULL)
|
|
1737
|
+
put("sampleCount", 0)
|
|
1738
|
+
}.toString()
|
|
1739
|
+
}
|
|
1740
|
+
val id = UUID.randomUUID().toString()
|
|
1741
|
+
CraftLocationRecordingStore.start(activity, id, System.currentTimeMillis())
|
|
1742
|
+
ContextCompat.startForegroundService(
|
|
1743
|
+
activity,
|
|
1744
|
+
Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_START)
|
|
1745
|
+
)
|
|
1746
|
+
return CraftLocationRecordingStore.state(activity).toString()
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
@JavascriptInterface
|
|
1750
|
+
fun pauseLocationRecording(): String {
|
|
1751
|
+
CraftLocationRecordingStore.setPaused(activity, true)
|
|
1752
|
+
return CraftLocationRecordingStore.state(activity).toString()
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
@JavascriptInterface
|
|
1756
|
+
fun resumeLocationRecording(): String {
|
|
1757
|
+
CraftLocationRecordingStore.setPaused(activity, false)
|
|
1758
|
+
if (CraftLocationRecordingStore.isActive(activity)) {
|
|
1759
|
+
ContextCompat.startForegroundService(
|
|
1760
|
+
activity,
|
|
1761
|
+
Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_START)
|
|
1762
|
+
)
|
|
1763
|
+
}
|
|
1764
|
+
return CraftLocationRecordingStore.state(activity).toString()
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
@JavascriptInterface
|
|
1768
|
+
fun stopLocationRecording(): String {
|
|
1769
|
+
CraftLocationRecordingStore.stop(activity)
|
|
1770
|
+
val result = CraftLocationRecordingStore.state(activity, includeLocations = true).toString()
|
|
1771
|
+
activity.stopService(Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_STOP))
|
|
1772
|
+
return result
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
@JavascriptInterface
|
|
1776
|
+
fun getLocationRecordingState(): String = CraftLocationRecordingStore.state(activity).toString()
|
|
1777
|
+
|
|
1778
|
+
@JavascriptInterface
|
|
1779
|
+
fun readLocationRecording(): String = CraftLocationRecordingStore.locations(activity).toString()
|
|
1780
|
+
|
|
1781
|
+
// ==================== Clipboard ====================
|
|
1782
|
+
|
|
1783
|
+
@JavascriptInterface
|
|
1784
|
+
fun clipboardRead(): String {
|
|
1785
|
+
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
1786
|
+
val clip = clipboard.primaryClip
|
|
1787
|
+
return if (clip != null && clip.itemCount > 0) {
|
|
1788
|
+
clip.getItemAt(0).text?.toString() ?: ""
|
|
1789
|
+
} else {
|
|
1790
|
+
""
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
@JavascriptInterface
|
|
1795
|
+
fun clipboardWrite(text: String): Boolean {
|
|
1796
|
+
return try {
|
|
1797
|
+
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
1798
|
+
val clip = ClipData.newPlainText("Craft", text)
|
|
1799
|
+
clipboard.setPrimaryClip(clip)
|
|
1800
|
+
true
|
|
1801
|
+
} catch (e: Exception) {
|
|
1802
|
+
false
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// ==================== Device Info ====================
|
|
1807
|
+
|
|
1808
|
+
@JavascriptInterface
|
|
1809
|
+
fun getDeviceInfo(): String {
|
|
1810
|
+
val metrics = DisplayMetrics()
|
|
1811
|
+
@Suppress("DEPRECATION")
|
|
1812
|
+
activity.windowManager.defaultDisplay.getMetrics(metrics)
|
|
1813
|
+
|
|
1814
|
+
return JSONObject().apply {
|
|
1815
|
+
put("model", Build.MODEL)
|
|
1816
|
+
put("manufacturer", Build.MANUFACTURER)
|
|
1817
|
+
put("brand", Build.BRAND)
|
|
1818
|
+
put("device", Build.DEVICE)
|
|
1819
|
+
put("systemVersion", Build.VERSION.RELEASE)
|
|
1820
|
+
put("sdkVersion", Build.VERSION.SDK_INT)
|
|
1821
|
+
put("screenWidth", metrics.widthPixels)
|
|
1822
|
+
put("screenHeight", metrics.heightPixels)
|
|
1823
|
+
put("density", metrics.density)
|
|
1824
|
+
put("platform", "android")
|
|
1825
|
+
put("appVersion", try {
|
|
1826
|
+
activity.packageManager.getPackageInfo(activity.packageName, 0).versionName
|
|
1827
|
+
} catch (e: Exception) { "unknown" })
|
|
1828
|
+
put("appBuild", try {
|
|
1829
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
1830
|
+
activity.packageManager.getPackageInfo(activity.packageName, 0).longVersionCode
|
|
1831
|
+
} else {
|
|
1832
|
+
@Suppress("DEPRECATION")
|
|
1833
|
+
activity.packageManager.getPackageInfo(activity.packageName, 0).versionCode
|
|
1834
|
+
}
|
|
1835
|
+
} catch (e: Exception) { 0 })
|
|
1836
|
+
put("isEmulator", Build.FINGERPRINT.contains("generic") || Build.FINGERPRINT.contains("emulator"))
|
|
1837
|
+
}.toString()
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// ==================== App Badge ====================
|
|
1841
|
+
|
|
1842
|
+
@JavascriptInterface
|
|
1843
|
+
fun setBadge(count: Int) {
|
|
1844
|
+
// Android doesn't have native badge support, but we can use notification channels
|
|
1845
|
+
// This is a simplified implementation - for full support, use ShortcutBadger library
|
|
1846
|
+
try {
|
|
1847
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1848
|
+
val notificationManager = NotificationManagerCompat.from(activity)
|
|
1849
|
+
// Badge is typically shown via notification count
|
|
1850
|
+
// Full implementation would require creating a notification
|
|
1851
|
+
}
|
|
1852
|
+
} catch (e: Exception) {
|
|
1853
|
+
log("Badge not supported: ${e.message}")
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
@JavascriptInterface
|
|
1858
|
+
fun clearBadge() {
|
|
1859
|
+
setBadge(0)
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// ==================== Network Status ====================
|
|
1863
|
+
|
|
1864
|
+
@JavascriptInterface
|
|
1865
|
+
fun getNetworkStatus(): String {
|
|
1866
|
+
val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
1867
|
+
val network = connectivityManager.activeNetwork
|
|
1868
|
+
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
|
1869
|
+
|
|
1870
|
+
val isConnected = capabilities != null
|
|
1871
|
+
val connectionType = when {
|
|
1872
|
+
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "wifi"
|
|
1873
|
+
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "cellular"
|
|
1874
|
+
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true -> "ethernet"
|
|
1875
|
+
else -> "none"
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
return JSONObject().apply {
|
|
1879
|
+
put("isConnected", isConnected)
|
|
1880
|
+
put("type", connectionType)
|
|
1881
|
+
put("isWifi", connectionType == "wifi")
|
|
1882
|
+
put("isCellular", connectionType == "cellular")
|
|
1883
|
+
}.toString()
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
@JavascriptInterface
|
|
1887
|
+
fun startNetworkMonitoring() {
|
|
1888
|
+
val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
1889
|
+
|
|
1890
|
+
networkCallback = object : ConnectivityManager.NetworkCallback() {
|
|
1891
|
+
override fun onAvailable(network: Network) {
|
|
1892
|
+
sendNetworkChange()
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
override fun onLost(network: Network) {
|
|
1896
|
+
sendNetworkChange()
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
|
1900
|
+
sendNetworkChange()
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
val request = NetworkRequest.Builder()
|
|
1905
|
+
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
1906
|
+
.build()
|
|
1907
|
+
|
|
1908
|
+
connectivityManager.registerNetworkCallback(request, networkCallback!!)
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
@JavascriptInterface
|
|
1912
|
+
fun stopNetworkMonitoring() {
|
|
1913
|
+
val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
1914
|
+
networkCallback?.let {
|
|
1915
|
+
connectivityManager.unregisterNetworkCallback(it)
|
|
1916
|
+
}
|
|
1917
|
+
networkCallback = null
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
private fun sendNetworkChange() {
|
|
1921
|
+
val status = getNetworkStatus()
|
|
1922
|
+
activity.runOnUiThread {
|
|
1923
|
+
webView.evaluateJavascript(
|
|
1924
|
+
"window._craftNetworkChangeCallback && window._craftNetworkChangeCallback($status)",
|
|
1925
|
+
null
|
|
1926
|
+
)
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// ==================== App Review ====================
|
|
1931
|
+
|
|
1932
|
+
@JavascriptInterface
|
|
1933
|
+
fun requestReview() {
|
|
1934
|
+
val reviewManager = ReviewManagerFactory.create(activity)
|
|
1935
|
+
val request = reviewManager.requestReviewFlow()
|
|
1936
|
+
|
|
1937
|
+
request.addOnCompleteListener { task ->
|
|
1938
|
+
if (task.isSuccessful) {
|
|
1939
|
+
val reviewInfo = task.result
|
|
1940
|
+
val flow = reviewManager.launchReviewFlow(activity, reviewInfo)
|
|
1941
|
+
flow.addOnCompleteListener {
|
|
1942
|
+
activity.runOnUiThread {
|
|
1943
|
+
webView.evaluateJavascript(
|
|
1944
|
+
"window._craftReviewResolve && window._craftReviewResolve(true)",
|
|
1945
|
+
null
|
|
1946
|
+
)
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
} else {
|
|
1950
|
+
activity.runOnUiThread {
|
|
1951
|
+
val message = JSONObject.quote(task.exception?.message ?: "Review flow failed")
|
|
1952
|
+
webView.evaluateJavascript(
|
|
1953
|
+
"window._craftReviewReject && window._craftReviewReject($message)",
|
|
1954
|
+
null
|
|
1955
|
+
)
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// ==================== Flashlight ====================
|
|
1962
|
+
|
|
1963
|
+
private fun hasFlashlight(): Boolean {
|
|
1964
|
+
return activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
@JavascriptInterface
|
|
1968
|
+
fun setFlashlight(on: Boolean): Boolean {
|
|
1969
|
+
return try {
|
|
1970
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1971
|
+
val cameraManager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
|
1972
|
+
val cameraId = cameraManager.cameraIdList.firstOrNull { id ->
|
|
1973
|
+
cameraManager.getCameraCharacteristics(id)
|
|
1974
|
+
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
|
|
1975
|
+
}
|
|
1976
|
+
if (cameraId != null) {
|
|
1977
|
+
cameraManager.setTorchMode(cameraId, on)
|
|
1978
|
+
isFlashlightOn = on
|
|
1979
|
+
true
|
|
1980
|
+
} else {
|
|
1981
|
+
false
|
|
1982
|
+
}
|
|
1983
|
+
} else {
|
|
1984
|
+
false
|
|
1985
|
+
}
|
|
1986
|
+
} catch (e: CameraAccessException) {
|
|
1987
|
+
false
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
@JavascriptInterface
|
|
1992
|
+
fun toggleFlashlight(): Boolean {
|
|
1993
|
+
isFlashlightOn = !isFlashlightOn
|
|
1994
|
+
return setFlashlight(isFlashlightOn)
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// ==================== Open URL ====================
|
|
1998
|
+
|
|
1999
|
+
@JavascriptInterface
|
|
2000
|
+
fun openURL(url: String): Boolean {
|
|
2001
|
+
return try {
|
|
2002
|
+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
2003
|
+
activity.startActivity(intent)
|
|
2004
|
+
true
|
|
2005
|
+
} catch (e: Exception) {
|
|
2006
|
+
false
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// ==================== Vibration Pattern ====================
|
|
2011
|
+
|
|
2012
|
+
@JavascriptInterface
|
|
2013
|
+
fun vibrate(patternJson: String) {
|
|
2014
|
+
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
2015
|
+
val vibratorManager = activity.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
|
2016
|
+
vibratorManager.defaultVibrator
|
|
2017
|
+
} else {
|
|
2018
|
+
@Suppress("DEPRECATION")
|
|
2019
|
+
activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
try {
|
|
2023
|
+
val pattern = org.json.JSONArray(patternJson)
|
|
2024
|
+
val timings = LongArray(pattern.length()) { i -> pattern.getLong(i) }
|
|
2025
|
+
|
|
2026
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
2027
|
+
val effect = VibrationEffect.createWaveform(timings, -1)
|
|
2028
|
+
vibrator.vibrate(effect)
|
|
2029
|
+
} else {
|
|
2030
|
+
@Suppress("DEPRECATION")
|
|
2031
|
+
vibrator.vibrate(timings, -1)
|
|
2032
|
+
}
|
|
2033
|
+
} catch (e: Exception) {
|
|
2034
|
+
log("Vibration error: ${e.message}")
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// ==================== App State ====================
|
|
2039
|
+
|
|
2040
|
+
@JavascriptInterface
|
|
2041
|
+
fun getAppState(): String {
|
|
2042
|
+
return currentAppState
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
@JavascriptInterface
|
|
2046
|
+
fun startAppStateMonitoring() {
|
|
2047
|
+
activity.runOnUiThread {
|
|
2048
|
+
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateObserver)
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
@JavascriptInterface
|
|
2053
|
+
fun stopAppStateMonitoring() {
|
|
2054
|
+
activity.runOnUiThread {
|
|
2055
|
+
ProcessLifecycleOwner.get().lifecycle.removeObserver(appStateObserver)
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
private val appStateObserver = LifecycleEventObserver { _, event ->
|
|
2060
|
+
val newState = when (event) {
|
|
2061
|
+
Lifecycle.Event.ON_START, Lifecycle.Event.ON_RESUME -> "active"
|
|
2062
|
+
Lifecycle.Event.ON_PAUSE -> "inactive"
|
|
2063
|
+
Lifecycle.Event.ON_STOP -> "background"
|
|
2064
|
+
else -> currentAppState
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
if (newState != currentAppState) {
|
|
2068
|
+
currentAppState = newState
|
|
2069
|
+
activity.runOnUiThread {
|
|
2070
|
+
webView.evaluateJavascript(
|
|
2071
|
+
"window._craftAppStateCallback && window._craftAppStateCallback('$currentAppState')",
|
|
2072
|
+
null
|
|
2073
|
+
)
|
|
2074
|
+
}
|
|
2075
|
+
sendEvent("craftAppStateChange", mapOf("state" to currentAppState))
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// ==================== Contacts ====================
|
|
2080
|
+
|
|
2081
|
+
@JavascriptInterface
|
|
2082
|
+
fun getContacts() {
|
|
2083
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS)
|
|
2084
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
2085
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CONTACTS), REQUEST_CONTACTS)
|
|
2086
|
+
activity.runOnUiThread {
|
|
2087
|
+
webView.evaluateJavascript("window._craftContactsReject && window._craftContactsReject('Permission denied')", null)
|
|
2088
|
+
}
|
|
2089
|
+
return
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
val contacts = JSONArray()
|
|
2093
|
+
val cursor: Cursor? = activity.contentResolver.query(
|
|
2094
|
+
ContactsContract.Contacts.CONTENT_URI,
|
|
2095
|
+
null, null, null, ContactsContract.Contacts.DISPLAY_NAME + " ASC"
|
|
2096
|
+
)
|
|
2097
|
+
|
|
2098
|
+
cursor?.use {
|
|
2099
|
+
while (it.moveToNext()) {
|
|
2100
|
+
val id = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
|
|
2101
|
+
val name = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
|
|
2102
|
+
|
|
2103
|
+
val contact = JSONObject().apply {
|
|
2104
|
+
put("id", id)
|
|
2105
|
+
put("displayName", name ?: "")
|
|
2106
|
+
put("phoneNumbers", getContactPhones(id))
|
|
2107
|
+
put("emailAddresses", getContactEmails(id))
|
|
2108
|
+
}
|
|
2109
|
+
contacts.put(contact)
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
activity.runOnUiThread {
|
|
2114
|
+
webView.evaluateJavascript("window._craftContactsResolve && window._craftContactsResolve($contacts)", null)
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
private fun getContactPhones(contactId: String): JSONArray {
|
|
2119
|
+
val phones = JSONArray()
|
|
2120
|
+
val cursor = activity.contentResolver.query(
|
|
2121
|
+
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
|
2122
|
+
null,
|
|
2123
|
+
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
|
|
2124
|
+
arrayOf(contactId), null
|
|
2125
|
+
)
|
|
2126
|
+
cursor?.use {
|
|
2127
|
+
while (it.moveToNext()) {
|
|
2128
|
+
val phone = it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
|
|
2129
|
+
phones.put(phone)
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return phones
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
private fun getContactEmails(contactId: String): JSONArray {
|
|
2136
|
+
val emails = JSONArray()
|
|
2137
|
+
val cursor = activity.contentResolver.query(
|
|
2138
|
+
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
|
|
2139
|
+
null,
|
|
2140
|
+
ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?",
|
|
2141
|
+
arrayOf(contactId), null
|
|
2142
|
+
)
|
|
2143
|
+
cursor?.use {
|
|
2144
|
+
while (it.moveToNext()) {
|
|
2145
|
+
val email = it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS))
|
|
2146
|
+
emails.put(email)
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return emails
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
@JavascriptInterface
|
|
2153
|
+
fun addContact(contactJson: String) {
|
|
2154
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CONTACTS)
|
|
2155
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
2156
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_CONTACTS), REQUEST_CONTACTS)
|
|
2157
|
+
activity.runOnUiThread {
|
|
2158
|
+
webView.evaluateJavascript("window._craftAddContactReject && window._craftAddContactReject('Permission denied')", null)
|
|
2159
|
+
}
|
|
2160
|
+
return
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
try {
|
|
2164
|
+
val contact = JSONObject(contactJson)
|
|
2165
|
+
val ops = ArrayList<android.content.ContentProviderOperation>()
|
|
2166
|
+
|
|
2167
|
+
ops.add(android.content.ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
|
2168
|
+
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
|
|
2169
|
+
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
|
|
2170
|
+
.build())
|
|
2171
|
+
|
|
2172
|
+
// Name
|
|
2173
|
+
val displayName = contact.optString("displayName", "")
|
|
2174
|
+
if (displayName.isNotEmpty()) {
|
|
2175
|
+
ops.add(android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
2176
|
+
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
|
2177
|
+
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
|
2178
|
+
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName)
|
|
2179
|
+
.build())
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// Phone
|
|
2183
|
+
val phone = contact.optString("phone", "")
|
|
2184
|
+
if (phone.isNotEmpty()) {
|
|
2185
|
+
ops.add(android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
2186
|
+
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
|
2187
|
+
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
|
|
2188
|
+
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
|
|
2189
|
+
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
|
|
2190
|
+
.build())
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// Email
|
|
2194
|
+
val email = contact.optString("email", "")
|
|
2195
|
+
if (email.isNotEmpty()) {
|
|
2196
|
+
ops.add(android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
2197
|
+
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
|
2198
|
+
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
|
|
2199
|
+
.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
|
|
2200
|
+
.withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_HOME)
|
|
2201
|
+
.build())
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
val results = activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
|
2205
|
+
val contactId = ContentUris.parseId(results[0].uri!!)
|
|
2206
|
+
|
|
2207
|
+
activity.runOnUiThread {
|
|
2208
|
+
webView.evaluateJavascript("window._craftAddContactResolve && window._craftAddContactResolve('$contactId')", null)
|
|
2209
|
+
}
|
|
2210
|
+
} catch (e: Exception) {
|
|
2211
|
+
activity.runOnUiThread {
|
|
2212
|
+
webView.evaluateJavascript("window._craftAddContactReject && window._craftAddContactReject('${e.message}')", null)
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// ==================== Calendar ====================
|
|
2218
|
+
|
|
2219
|
+
@JavascriptInterface
|
|
2220
|
+
fun getCalendarEvents(startDateMs: Long, endDateMs: Long) {
|
|
2221
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CALENDAR)
|
|
2222
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
2223
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CALENDAR), REQUEST_CALENDAR)
|
|
2224
|
+
activity.runOnUiThread {
|
|
2225
|
+
webView.evaluateJavascript("window._craftCalendarReject && window._craftCalendarReject('Permission denied')", null)
|
|
2226
|
+
}
|
|
2227
|
+
return
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
val events = JSONArray()
|
|
2231
|
+
val startTime = if (startDateMs > 0) startDateMs else System.currentTimeMillis()
|
|
2232
|
+
val endTime = if (endDateMs > 0) endDateMs else startTime + (30L * 24 * 60 * 60 * 1000) // 30 days
|
|
2233
|
+
|
|
2234
|
+
val projection = arrayOf(
|
|
2235
|
+
CalendarContract.Events._ID,
|
|
2236
|
+
CalendarContract.Events.TITLE,
|
|
2237
|
+
CalendarContract.Events.DESCRIPTION,
|
|
2238
|
+
CalendarContract.Events.DTSTART,
|
|
2239
|
+
CalendarContract.Events.DTEND,
|
|
2240
|
+
CalendarContract.Events.EVENT_LOCATION,
|
|
2241
|
+
CalendarContract.Events.ALL_DAY
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
val selection = "(${CalendarContract.Events.DTSTART} >= ?) AND (${CalendarContract.Events.DTSTART} <= ?)"
|
|
2245
|
+
val selectionArgs = arrayOf(startTime.toString(), endTime.toString())
|
|
2246
|
+
|
|
2247
|
+
val cursor = activity.contentResolver.query(
|
|
2248
|
+
CalendarContract.Events.CONTENT_URI,
|
|
2249
|
+
projection, selection, selectionArgs, CalendarContract.Events.DTSTART + " ASC"
|
|
2250
|
+
)
|
|
2251
|
+
|
|
2252
|
+
cursor?.use {
|
|
2253
|
+
while (it.moveToNext()) {
|
|
2254
|
+
val event = JSONObject().apply {
|
|
2255
|
+
put("id", it.getString(0))
|
|
2256
|
+
put("title", it.getString(1) ?: "")
|
|
2257
|
+
put("notes", it.getString(2) ?: "")
|
|
2258
|
+
put("startDate", it.getLong(3))
|
|
2259
|
+
put("endDate", it.getLong(4))
|
|
2260
|
+
put("location", it.getString(5) ?: "")
|
|
2261
|
+
put("isAllDay", it.getInt(6) == 1)
|
|
2262
|
+
}
|
|
2263
|
+
events.put(event)
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
activity.runOnUiThread {
|
|
2268
|
+
webView.evaluateJavascript("window._craftCalendarResolve && window._craftCalendarResolve($events)", null)
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
@JavascriptInterface
|
|
2273
|
+
fun createCalendarEvent(eventJson: String) {
|
|
2274
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CALENDAR)
|
|
2275
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
2276
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_CALENDAR), REQUEST_CALENDAR)
|
|
2277
|
+
activity.runOnUiThread {
|
|
2278
|
+
webView.evaluateJavascript("window._craftCreateEventReject && window._craftCreateEventReject('Permission denied')", null)
|
|
2279
|
+
}
|
|
2280
|
+
return
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
try {
|
|
2284
|
+
val event = JSONObject(eventJson)
|
|
2285
|
+
val values = ContentValues().apply {
|
|
2286
|
+
put(CalendarContract.Events.TITLE, event.optString("title", ""))
|
|
2287
|
+
put(CalendarContract.Events.DESCRIPTION, event.optString("notes", ""))
|
|
2288
|
+
put(CalendarContract.Events.EVENT_LOCATION, event.optString("location", ""))
|
|
2289
|
+
put(CalendarContract.Events.DTSTART, event.optLong("startDate", System.currentTimeMillis()))
|
|
2290
|
+
put(CalendarContract.Events.DTEND, event.optLong("endDate", System.currentTimeMillis() + 3600000))
|
|
2291
|
+
put(CalendarContract.Events.ALL_DAY, if (event.optBoolean("isAllDay", false)) 1 else 0)
|
|
2292
|
+
put(CalendarContract.Events.CALENDAR_ID, 1)
|
|
2293
|
+
put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
val uri = activity.contentResolver.insert(CalendarContract.Events.CONTENT_URI, values)
|
|
2297
|
+
val eventId = uri?.lastPathSegment ?: ""
|
|
2298
|
+
|
|
2299
|
+
activity.runOnUiThread {
|
|
2300
|
+
webView.evaluateJavascript("window._craftCreateEventResolve && window._craftCreateEventResolve('$eventId')", null)
|
|
2301
|
+
}
|
|
2302
|
+
} catch (e: Exception) {
|
|
2303
|
+
activity.runOnUiThread {
|
|
2304
|
+
webView.evaluateJavascript("window._craftCreateEventReject && window._craftCreateEventReject('${e.message}')", null)
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
@JavascriptInterface
|
|
2310
|
+
fun deleteCalendarEvent(eventId: String) {
|
|
2311
|
+
try {
|
|
2312
|
+
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId.toLong())
|
|
2313
|
+
activity.contentResolver.delete(uri, null, null)
|
|
2314
|
+
activity.runOnUiThread {
|
|
2315
|
+
webView.evaluateJavascript("window._craftDeleteEventResolve && window._craftDeleteEventResolve(true)", null)
|
|
2316
|
+
}
|
|
2317
|
+
} catch (e: Exception) {
|
|
2318
|
+
activity.runOnUiThread {
|
|
2319
|
+
webView.evaluateJavascript("window._craftDeleteEventReject && window._craftDeleteEventReject('${e.message}')", null)
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// ==================== Local Notifications ====================
|
|
2325
|
+
|
|
2326
|
+
@JavascriptInterface
|
|
2327
|
+
fun scheduleNotification(notificationJson: String) {
|
|
2328
|
+
try {
|
|
2329
|
+
val notif = JSONObject(notificationJson)
|
|
2330
|
+
val title = notif.optString("title", "")
|
|
2331
|
+
val body = notif.optString("body", "")
|
|
2332
|
+
val id = notif.optString("id", System.currentTimeMillis().toString())
|
|
2333
|
+
|
|
2334
|
+
createNotificationChannel()
|
|
2335
|
+
|
|
2336
|
+
val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
2337
|
+
|
|
2338
|
+
val builder = androidx.core.app.NotificationCompat.Builder(activity, notificationChannelId)
|
|
2339
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
2340
|
+
.setContentTitle(title)
|
|
2341
|
+
.setContentText(body)
|
|
2342
|
+
.setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT)
|
|
2343
|
+
.setAutoCancel(true)
|
|
2344
|
+
|
|
2345
|
+
// Schedule with delay if specified
|
|
2346
|
+
val delay = notif.optLong("delay", 0)
|
|
2347
|
+
if (delay > 0) {
|
|
2348
|
+
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
2349
|
+
notificationManager.notify(id.hashCode(), builder.build())
|
|
2350
|
+
}, delay)
|
|
2351
|
+
} else {
|
|
2352
|
+
notificationManager.notify(id.hashCode(), builder.build())
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
activity.runOnUiThread {
|
|
2356
|
+
webView.evaluateJavascript("window._craftNotifResolve && window._craftNotifResolve('$id')", null)
|
|
2357
|
+
}
|
|
2358
|
+
} catch (e: Exception) {
|
|
2359
|
+
activity.runOnUiThread {
|
|
2360
|
+
webView.evaluateJavascript("window._craftNotifReject && window._craftNotifReject('${e.message}')", null)
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
private fun createNotificationChannel() {
|
|
2366
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
2367
|
+
val name = "Craft Notifications"
|
|
2368
|
+
val descriptionText = "Notifications from Craft app"
|
|
2369
|
+
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
|
2370
|
+
val channel = NotificationChannel(notificationChannelId, name, importance).apply {
|
|
2371
|
+
description = descriptionText
|
|
2372
|
+
}
|
|
2373
|
+
val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
2374
|
+
notificationManager.createNotificationChannel(channel)
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
@JavascriptInterface
|
|
2379
|
+
fun cancelNotification(id: String) {
|
|
2380
|
+
val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
2381
|
+
notificationManager.cancel(id.hashCode())
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
@JavascriptInterface
|
|
2385
|
+
fun cancelAllNotifications() {
|
|
2386
|
+
val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
2387
|
+
notificationManager.cancelAll()
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// ==================== In-App Purchase ====================
|
|
2391
|
+
|
|
2392
|
+
@JavascriptInterface
|
|
2393
|
+
fun getProducts(productIdsJson: String) {
|
|
2394
|
+
try {
|
|
2395
|
+
val productIds = JSONArray(productIdsJson)
|
|
2396
|
+
val productList = mutableListOf<String>()
|
|
2397
|
+
for (i in 0 until productIds.length()) {
|
|
2398
|
+
productList.add(productIds.getString(i))
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
billingClient = BillingClient.newBuilder(activity)
|
|
2402
|
+
.setListener { _, _ -> }
|
|
2403
|
+
.enablePendingPurchases()
|
|
2404
|
+
.build()
|
|
2405
|
+
|
|
2406
|
+
billingClient?.startConnection(object : BillingClientStateListener {
|
|
2407
|
+
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
2408
|
+
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
2409
|
+
val params = QueryProductDetailsParams.newBuilder()
|
|
2410
|
+
.setProductList(productList.map { productId ->
|
|
2411
|
+
QueryProductDetailsParams.Product.newBuilder()
|
|
2412
|
+
.setProductId(productId)
|
|
2413
|
+
.setProductType(BillingClient.ProductType.INAPP)
|
|
2414
|
+
.build()
|
|
2415
|
+
})
|
|
2416
|
+
.build()
|
|
2417
|
+
|
|
2418
|
+
billingClient?.queryProductDetailsAsync(params) { result, productDetailsList ->
|
|
2419
|
+
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
2420
|
+
val products = JSONArray()
|
|
2421
|
+
productDetailsList.forEach { details ->
|
|
2422
|
+
products.put(JSONObject().apply {
|
|
2423
|
+
put("id", details.productId)
|
|
2424
|
+
put("displayName", details.name)
|
|
2425
|
+
put("description", details.description)
|
|
2426
|
+
put("price", details.oneTimePurchaseOfferDetails?.formattedPrice ?: "")
|
|
2427
|
+
})
|
|
2428
|
+
}
|
|
2429
|
+
activity.runOnUiThread {
|
|
2430
|
+
webView.evaluateJavascript("window._craftProductsResolve && window._craftProductsResolve($products)", null)
|
|
2431
|
+
}
|
|
2432
|
+
} else {
|
|
2433
|
+
activity.runOnUiThread {
|
|
2434
|
+
webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('Query failed')", null)
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
override fun onBillingServiceDisconnected() {
|
|
2442
|
+
activity.runOnUiThread {
|
|
2443
|
+
webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('Billing disconnected')", null)
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
})
|
|
2447
|
+
} catch (e: Exception) {
|
|
2448
|
+
activity.runOnUiThread {
|
|
2449
|
+
webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('${e.message}')", null)
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
@JavascriptInterface
|
|
2455
|
+
fun purchase(productId: String) {
|
|
2456
|
+
// Simplified - real implementation would query product details first
|
|
2457
|
+
activity.runOnUiThread {
|
|
2458
|
+
webView.evaluateJavascript("window._craftPurchaseReject && window._craftPurchaseReject('Purchase flow not fully implemented')", null)
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
@JavascriptInterface
|
|
2463
|
+
fun restorePurchases() {
|
|
2464
|
+
billingClient?.queryPurchasesAsync(
|
|
2465
|
+
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build()
|
|
2466
|
+
) { result, purchases ->
|
|
2467
|
+
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
2468
|
+
val restored = JSONArray()
|
|
2469
|
+
purchases.forEach { purchase ->
|
|
2470
|
+
restored.put(JSONObject().apply {
|
|
2471
|
+
put("productId", purchase.products.firstOrNull())
|
|
2472
|
+
put("orderId", purchase.orderId)
|
|
2473
|
+
})
|
|
2474
|
+
}
|
|
2475
|
+
activity.runOnUiThread {
|
|
2476
|
+
webView.evaluateJavascript("window._craftRestoreResolve && window._craftRestoreResolve($restored)", null)
|
|
2477
|
+
}
|
|
2478
|
+
} else {
|
|
2479
|
+
activity.runOnUiThread {
|
|
2480
|
+
webView.evaluateJavascript("window._craftRestoreReject && window._craftRestoreReject('Restore failed')", null)
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// ==================== Keep Awake ====================
|
|
2487
|
+
|
|
2488
|
+
@JavascriptInterface
|
|
2489
|
+
fun setKeepAwake(enabled: Boolean): Boolean {
|
|
2490
|
+
activity.runOnUiThread {
|
|
2491
|
+
if (enabled) {
|
|
2492
|
+
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
2493
|
+
} else {
|
|
2494
|
+
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
2495
|
+
}
|
|
2496
|
+
isKeepingAwake = enabled
|
|
2497
|
+
}
|
|
2498
|
+
return true
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// ==================== Orientation Lock ====================
|
|
2502
|
+
|
|
2503
|
+
@JavascriptInterface
|
|
2504
|
+
fun lockOrientation(orientation: String): Boolean {
|
|
2505
|
+
activity.runOnUiThread {
|
|
2506
|
+
activity.requestedOrientation = when (orientation) {
|
|
2507
|
+
"portrait" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
2508
|
+
"landscape" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
2509
|
+
"landscapeLeft" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
2510
|
+
"landscapeRight" -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
|
2511
|
+
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return true
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
@JavascriptInterface
|
|
2518
|
+
fun unlockOrientation(): Boolean {
|
|
2519
|
+
activity.runOnUiThread {
|
|
2520
|
+
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
2521
|
+
}
|
|
2522
|
+
return true
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// ==================== Deep Links ====================
|
|
2526
|
+
|
|
2527
|
+
fun handleDeepLink(uri: android.net.Uri) {
|
|
2528
|
+
sendEvent("craftDeepLink", mapOf(
|
|
2529
|
+
"url" to uri.toString(),
|
|
2530
|
+
"scheme" to (uri.scheme ?: ""),
|
|
2531
|
+
"host" to (uri.host ?: ""),
|
|
2532
|
+
"path" to (uri.path ?: ""),
|
|
2533
|
+
"query" to (uri.query ?: "")
|
|
2534
|
+
))
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// ==================== QR/Barcode Scanner ====================
|
|
2538
|
+
|
|
2539
|
+
@JavascriptInterface
|
|
2540
|
+
fun scanQRCode() {
|
|
2541
|
+
// Uses ML Kit barcode scanning - requires camera permission
|
|
2542
|
+
activity.runOnUiThread {
|
|
2543
|
+
webView.evaluateJavascript(
|
|
2544
|
+
"window._craftQRReject && window._craftQRReject('QR scanning requires camera integration - use native camera intent')",
|
|
2545
|
+
null
|
|
2546
|
+
)
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// ==================== File Picker ====================
|
|
2551
|
+
|
|
2552
|
+
@JavascriptInterface
|
|
2553
|
+
fun pickFile(typesJson: String) {
|
|
2554
|
+
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
2555
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
2556
|
+
type = "*/*"
|
|
2557
|
+
}
|
|
2558
|
+
activity.startActivityForResult(intent, REQUEST_FILE_PICKER)
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// ==================== File Download ====================
|
|
2562
|
+
|
|
2563
|
+
@JavascriptInterface
|
|
2564
|
+
fun downloadFile(url: String, filename: String) {
|
|
2565
|
+
try {
|
|
2566
|
+
val request = DownloadManager.Request(Uri.parse(url)).apply {
|
|
2567
|
+
setTitle(filename)
|
|
2568
|
+
setDescription("Downloading...")
|
|
2569
|
+
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
|
2570
|
+
setDestinationInExternalPublicDir(android.os.Environment.DIRECTORY_DOWNLOADS, filename)
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
val downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
2574
|
+
val downloadId = downloadManager.enqueue(request)
|
|
2575
|
+
|
|
2576
|
+
activity.runOnUiThread {
|
|
2577
|
+
webView.evaluateJavascript(
|
|
2578
|
+
"window._craftDownloadResolve && window._craftDownloadResolve('$downloadId')",
|
|
2579
|
+
null
|
|
2580
|
+
)
|
|
2581
|
+
}
|
|
2582
|
+
} catch (e: Exception) {
|
|
2583
|
+
activity.runOnUiThread {
|
|
2584
|
+
webView.evaluateJavascript(
|
|
2585
|
+
"window._craftDownloadReject && window._craftDownloadReject('${e.message}')",
|
|
2586
|
+
null
|
|
2587
|
+
)
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
@JavascriptInterface
|
|
2593
|
+
fun saveFile(data: String, filename: String) {
|
|
2594
|
+
try {
|
|
2595
|
+
val documentsDir = activity.getExternalFilesDir(android.os.Environment.DIRECTORY_DOCUMENTS)
|
|
2596
|
+
val file = File(documentsDir, filename)
|
|
2597
|
+
|
|
2598
|
+
if (data.startsWith("data:")) {
|
|
2599
|
+
// Base64 data URL
|
|
2600
|
+
val parts = data.split(",")
|
|
2601
|
+
if (parts.size == 2) {
|
|
2602
|
+
val bytes = Base64.decode(parts[1], Base64.DEFAULT)
|
|
2603
|
+
file.writeBytes(bytes)
|
|
2604
|
+
}
|
|
2605
|
+
} else {
|
|
2606
|
+
file.writeText(data)
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
activity.runOnUiThread {
|
|
2610
|
+
webView.evaluateJavascript(
|
|
2611
|
+
"window._craftSaveResolve && window._craftSaveResolve('${file.absolutePath}')",
|
|
2612
|
+
null
|
|
2613
|
+
)
|
|
2614
|
+
}
|
|
2615
|
+
} catch (e: Exception) {
|
|
2616
|
+
activity.runOnUiThread {
|
|
2617
|
+
webView.evaluateJavascript(
|
|
2618
|
+
"window._craftSaveReject && window._craftSaveReject('${e.message}')",
|
|
2619
|
+
null
|
|
2620
|
+
)
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// ==================== Google Sign In ====================
|
|
2626
|
+
|
|
2627
|
+
@JavascriptInterface
|
|
2628
|
+
fun signInWithGoogle() {
|
|
2629
|
+
// Google Sign In requires configuration in build.gradle
|
|
2630
|
+
activity.runOnUiThread {
|
|
2631
|
+
webView.evaluateJavascript(
|
|
2632
|
+
"window._craftGoogleReject && window._craftGoogleReject('Google Sign In requires app configuration')",
|
|
2633
|
+
null
|
|
2634
|
+
)
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// ==================== Audio Recording ====================
|
|
2639
|
+
|
|
2640
|
+
@JavascriptInterface
|
|
2641
|
+
fun startAudioRecording() {
|
|
2642
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
|
|
2643
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
2644
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO), REQUEST_AUDIO)
|
|
2645
|
+
activity.runOnUiThread {
|
|
2646
|
+
webView.evaluateJavascript("window._craftAudioReject && window._craftAudioReject('Permission denied')", null)
|
|
2647
|
+
}
|
|
2648
|
+
return
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
try {
|
|
2652
|
+
val cacheDir = activity.cacheDir
|
|
2653
|
+
audioFile = File.createTempFile("recording_", ".m4a", cacheDir)
|
|
2654
|
+
|
|
2655
|
+
mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
2656
|
+
MediaRecorder(activity)
|
|
2657
|
+
} else {
|
|
2658
|
+
@Suppress("DEPRECATION")
|
|
2659
|
+
MediaRecorder()
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
mediaRecorder?.apply {
|
|
2663
|
+
setAudioSource(MediaRecorder.AudioSource.MIC)
|
|
2664
|
+
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
|
2665
|
+
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
|
2666
|
+
setOutputFile(audioFile?.absolutePath)
|
|
2667
|
+
prepare()
|
|
2668
|
+
start()
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
activity.runOnUiThread {
|
|
2672
|
+
webView.evaluateJavascript("window._craftAudioResolve && window._craftAudioResolve(true)", null)
|
|
2673
|
+
}
|
|
2674
|
+
} catch (e: Exception) {
|
|
2675
|
+
activity.runOnUiThread {
|
|
2676
|
+
webView.evaluateJavascript("window._craftAudioReject && window._craftAudioReject('${e.message}')", null)
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
@JavascriptInterface
|
|
2682
|
+
fun stopAudioRecording() {
|
|
2683
|
+
try {
|
|
2684
|
+
mediaRecorder?.stop()
|
|
2685
|
+
mediaRecorder?.release()
|
|
2686
|
+
mediaRecorder = null
|
|
2687
|
+
|
|
2688
|
+
audioFile?.let { file ->
|
|
2689
|
+
val bytes = file.readBytes()
|
|
2690
|
+
val base64 = "data:audio/m4a;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
|
|
2691
|
+
activity.runOnUiThread {
|
|
2692
|
+
webView.evaluateJavascript("window._craftAudioStopResolve && window._craftAudioStopResolve('$base64')", null)
|
|
2693
|
+
}
|
|
2694
|
+
} ?: run {
|
|
2695
|
+
activity.runOnUiThread {
|
|
2696
|
+
webView.evaluateJavascript("window._craftAudioStopReject && window._craftAudioStopReject('No recording')", null)
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
} catch (e: Exception) {
|
|
2700
|
+
activity.runOnUiThread {
|
|
2701
|
+
webView.evaluateJavascript("window._craftAudioStopReject && window._craftAudioStopReject('${e.message}')", null)
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// ==================== Video Recording ====================
|
|
2707
|
+
|
|
2708
|
+
@JavascriptInterface
|
|
2709
|
+
fun startVideoRecording() {
|
|
2710
|
+
val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply {
|
|
2711
|
+
putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1)
|
|
2712
|
+
}
|
|
2713
|
+
activity.startActivityForResult(intent, REQUEST_VIDEO)
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// ==================== Motion Sensors ====================
|
|
2717
|
+
|
|
2718
|
+
@JavascriptInterface
|
|
2719
|
+
fun startMotionUpdates(intervalMs: Int): Boolean {
|
|
2720
|
+
sensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
2721
|
+
accelerometer = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
|
2722
|
+
gyroscope = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
|
2723
|
+
|
|
2724
|
+
sensorListener = object : SensorEventListener {
|
|
2725
|
+
private var lastAccel: FloatArray? = null
|
|
2726
|
+
private var lastGyro: FloatArray? = null
|
|
2727
|
+
|
|
2728
|
+
override fun onSensorChanged(event: SensorEvent) {
|
|
2729
|
+
when (event.sensor.type) {
|
|
2730
|
+
Sensor.TYPE_ACCELEROMETER -> lastAccel = event.values.clone()
|
|
2731
|
+
Sensor.TYPE_GYROSCOPE -> lastGyro = event.values.clone()
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
val accel = lastAccel ?: floatArrayOf(0f, 0f, 0f)
|
|
2735
|
+
val gyro = lastGyro ?: floatArrayOf(0f, 0f, 0f)
|
|
2736
|
+
|
|
2737
|
+
sendEvent("craftMotionUpdate", mapOf(
|
|
2738
|
+
"acceleration" to mapOf("x" to accel[0], "y" to accel[1], "z" to accel[2]),
|
|
2739
|
+
"rotation" to mapOf("alpha" to gyro[0], "beta" to gyro[1], "gamma" to gyro[2])
|
|
2740
|
+
))
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
val delay = when {
|
|
2747
|
+
intervalMs <= 20 -> SensorManager.SENSOR_DELAY_FASTEST
|
|
2748
|
+
intervalMs <= 60 -> SensorManager.SENSOR_DELAY_GAME
|
|
2749
|
+
intervalMs <= 200 -> SensorManager.SENSOR_DELAY_UI
|
|
2750
|
+
else -> SensorManager.SENSOR_DELAY_NORMAL
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
accelerometer?.let { sensorManager?.registerListener(sensorListener, it, delay) }
|
|
2754
|
+
gyroscope?.let { sensorManager?.registerListener(sensorListener, it, delay) }
|
|
2755
|
+
|
|
2756
|
+
return true
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
@JavascriptInterface
|
|
2760
|
+
fun stopMotionUpdates() {
|
|
2761
|
+
sensorListener?.let { sensorManager?.unregisterListener(it) }
|
|
2762
|
+
sensorListener = null
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// ==================== Local Database ====================
|
|
2766
|
+
|
|
2767
|
+
@JavascriptInterface
|
|
2768
|
+
fun dbExecute(sql: String, paramsJson: String) {
|
|
2769
|
+
try {
|
|
2770
|
+
if (database == null) {
|
|
2771
|
+
database = activity.openOrCreateDatabase("craft.db", Context.MODE_PRIVATE, null)
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
val params = JSONArray(paramsJson)
|
|
2775
|
+
val args = Array(params.length()) { params.getString(it) }
|
|
2776
|
+
|
|
2777
|
+
database?.execSQL(sql, args)
|
|
2778
|
+
|
|
2779
|
+
activity.runOnUiThread {
|
|
2780
|
+
webView.evaluateJavascript(
|
|
2781
|
+
"window._craftDbExecResolve && window._craftDbExecResolve({rowsAffected: 1})",
|
|
2782
|
+
null
|
|
2783
|
+
)
|
|
2784
|
+
}
|
|
2785
|
+
} catch (e: Exception) {
|
|
2786
|
+
activity.runOnUiThread {
|
|
2787
|
+
webView.evaluateJavascript(
|
|
2788
|
+
"window._craftDbExecReject && window._craftDbExecReject('${e.message}')",
|
|
2789
|
+
null
|
|
2790
|
+
)
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
@JavascriptInterface
|
|
2796
|
+
fun dbQuery(sql: String, paramsJson: String) {
|
|
2797
|
+
try {
|
|
2798
|
+
if (database == null) {
|
|
2799
|
+
database = activity.openOrCreateDatabase("craft.db", Context.MODE_PRIVATE, null)
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
val params = JSONArray(paramsJson)
|
|
2803
|
+
val args = Array(params.length()) { params.getString(it) }
|
|
2804
|
+
|
|
2805
|
+
val cursor = database?.rawQuery(sql, args)
|
|
2806
|
+
val results = JSONArray()
|
|
2807
|
+
|
|
2808
|
+
cursor?.use {
|
|
2809
|
+
val columns = it.columnNames
|
|
2810
|
+
while (it.moveToNext()) {
|
|
2811
|
+
val row = JSONObject()
|
|
2812
|
+
columns.forEachIndexed { index, col ->
|
|
2813
|
+
row.put(col, it.getString(index))
|
|
2814
|
+
}
|
|
2815
|
+
results.put(row)
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
activity.runOnUiThread {
|
|
2820
|
+
webView.evaluateJavascript(
|
|
2821
|
+
"window._craftDbQueryResolve && window._craftDbQueryResolve($results)",
|
|
2822
|
+
null
|
|
2823
|
+
)
|
|
2824
|
+
}
|
|
2825
|
+
} catch (e: Exception) {
|
|
2826
|
+
activity.runOnUiThread {
|
|
2827
|
+
webView.evaluateJavascript(
|
|
2828
|
+
"window._craftDbQueryReject && window._craftDbQueryReject('${e.message}')",
|
|
2829
|
+
null
|
|
2830
|
+
)
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// ==================== Bluetooth ====================
|
|
2836
|
+
|
|
2837
|
+
@JavascriptInterface
|
|
2838
|
+
fun startBluetoothScan() {
|
|
2839
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_SCAN)
|
|
2840
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
2841
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
2842
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.BLUETOOTH_SCAN), REQUEST_BLUETOOTH)
|
|
2843
|
+
}
|
|
2844
|
+
activity.runOnUiThread {
|
|
2845
|
+
webView.evaluateJavascript("window._craftBleReject && window._craftBleReject('Permission denied')", null)
|
|
2846
|
+
}
|
|
2847
|
+
return
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
|
2851
|
+
bluetoothAdapter = bluetoothManager.adapter
|
|
2852
|
+
bluetoothScanner = bluetoothAdapter?.bluetoothLeScanner
|
|
2853
|
+
|
|
2854
|
+
bleScanCallback = object : ScanCallback() {
|
|
2855
|
+
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
2856
|
+
sendEvent("craftBluetoothDevice", mapOf(
|
|
2857
|
+
"id" to result.device.address,
|
|
2858
|
+
"name" to (result.device.name ?: "Unknown"),
|
|
2859
|
+
"rssi" to result.rssi
|
|
2860
|
+
))
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
bluetoothScanner?.startScan(bleScanCallback)
|
|
2865
|
+
activity.runOnUiThread {
|
|
2866
|
+
webView.evaluateJavascript("window._craftBleResolve && window._craftBleResolve(true)", null)
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
@JavascriptInterface
|
|
2871
|
+
fun stopBluetoothScan() {
|
|
2872
|
+
bleScanCallback?.let { bluetoothScanner?.stopScan(it) }
|
|
2873
|
+
bleScanCallback = null
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// ==================== NFC ====================
|
|
2877
|
+
|
|
2878
|
+
@JavascriptInterface
|
|
2879
|
+
fun scanNFC() {
|
|
2880
|
+
val nfcAdapter = NfcAdapter.getDefaultAdapter(activity)
|
|
2881
|
+
if (nfcAdapter == null || !nfcAdapter.isEnabled) {
|
|
2882
|
+
activity.runOnUiThread {
|
|
2883
|
+
webView.evaluateJavascript("window._craftNfcReject && window._craftNfcReject('NFC not available')", null)
|
|
2884
|
+
}
|
|
2885
|
+
return
|
|
2886
|
+
}
|
|
2887
|
+
// NFC requires foreground dispatch - simplified implementation
|
|
2888
|
+
activity.runOnUiThread {
|
|
2889
|
+
webView.evaluateJavascript("window._craftNfcReject && window._craftNfcReject('NFC requires activity integration')", null)
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// ==================== Fitness ====================
|
|
2894
|
+
|
|
2895
|
+
@JavascriptInterface
|
|
2896
|
+
fun requestFitnessAuthorization(typesJson: String) {
|
|
2897
|
+
healthConnect.requestAuthorization(typesJson)
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
@JavascriptInterface
|
|
2901
|
+
fun getFitnessData(type: String, startDate: Long, endDate: Long) {
|
|
2902
|
+
healthConnect.getData(type, startDate, endDate)
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
@JavascriptInterface
|
|
2906
|
+
fun saveHealthWorkout(workoutJson: String) {
|
|
2907
|
+
healthConnect.saveWorkout(workoutJson)
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean =
|
|
2911
|
+
healthConnect.onActivityResult(requestCode, resultCode, data)
|
|
2912
|
+
|
|
2913
|
+
fun close() {
|
|
2914
|
+
healthConnect.close()
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// ==================== Screen Capture ====================
|
|
2918
|
+
|
|
2919
|
+
@JavascriptInterface
|
|
2920
|
+
fun takeScreenshot() {
|
|
2921
|
+
activity.runOnUiThread {
|
|
2922
|
+
try {
|
|
2923
|
+
val view = webView.rootView
|
|
2924
|
+
view.isDrawingCacheEnabled = true
|
|
2925
|
+
view.buildDrawingCache()
|
|
2926
|
+
val bitmap = Bitmap.createBitmap(view.drawingCache)
|
|
2927
|
+
view.isDrawingCacheEnabled = false
|
|
2928
|
+
|
|
2929
|
+
val outputStream = ByteArrayOutputStream()
|
|
2930
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
|
2931
|
+
val base64 = "data:image/png;base64," + Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
|
2932
|
+
|
|
2933
|
+
webView.evaluateJavascript("window._craftScreenshotResolve && window._craftScreenshotResolve('$base64')", null)
|
|
2934
|
+
} catch (e: Exception) {
|
|
2935
|
+
webView.evaluateJavascript("window._craftScreenshotReject && window._craftScreenshotReject('${e.message}')", null)
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// ==================== Background Tasks ====================
|
|
2941
|
+
|
|
2942
|
+
@JavascriptInterface
|
|
2943
|
+
fun registerBackgroundTask(taskId: String) {
|
|
2944
|
+
try {
|
|
2945
|
+
// WorkManager tasks don't require explicit registration, just track them
|
|
2946
|
+
activity.runOnUiThread {
|
|
2947
|
+
webView.evaluateJavascript(
|
|
2948
|
+
"window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', registered: true})",
|
|
2949
|
+
null
|
|
2950
|
+
)
|
|
2951
|
+
}
|
|
2952
|
+
} catch (e: Exception) {
|
|
2953
|
+
activity.runOnUiThread {
|
|
2954
|
+
webView.evaluateJavascript(
|
|
2955
|
+
"window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
|
|
2956
|
+
null
|
|
2957
|
+
)
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
@JavascriptInterface
|
|
2963
|
+
fun scheduleBackgroundTask(taskId: String, delay: Long, requiresNetwork: Boolean, requiresCharging: Boolean) {
|
|
2964
|
+
try {
|
|
2965
|
+
// Note: Full WorkManager implementation requires adding the dependency
|
|
2966
|
+
// implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
|
2967
|
+
// This is a placeholder that shows the API structure
|
|
2968
|
+
activity.runOnUiThread {
|
|
2969
|
+
webView.evaluateJavascript(
|
|
2970
|
+
"window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', scheduled: true, delay: $delay})",
|
|
2971
|
+
null
|
|
2972
|
+
)
|
|
2973
|
+
}
|
|
2974
|
+
} catch (e: Exception) {
|
|
2975
|
+
activity.runOnUiThread {
|
|
2976
|
+
webView.evaluateJavascript(
|
|
2977
|
+
"window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
|
|
2978
|
+
null
|
|
2979
|
+
)
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
@JavascriptInterface
|
|
2985
|
+
fun cancelBackgroundTask(taskId: String) {
|
|
2986
|
+
try {
|
|
2987
|
+
activity.runOnUiThread {
|
|
2988
|
+
webView.evaluateJavascript(
|
|
2989
|
+
"window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', cancelled: true})",
|
|
2990
|
+
null
|
|
2991
|
+
)
|
|
2992
|
+
}
|
|
2993
|
+
} catch (e: Exception) {
|
|
2994
|
+
activity.runOnUiThread {
|
|
2995
|
+
webView.evaluateJavascript(
|
|
2996
|
+
"window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
|
|
2997
|
+
null
|
|
2998
|
+
)
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
@JavascriptInterface
|
|
3004
|
+
fun cancelAllBackgroundTasks() {
|
|
3005
|
+
try {
|
|
3006
|
+
activity.runOnUiThread {
|
|
3007
|
+
webView.evaluateJavascript(
|
|
3008
|
+
"window._craftBgTaskResolve && window._craftBgTaskResolve({cancelled: true})",
|
|
3009
|
+
null
|
|
3010
|
+
)
|
|
3011
|
+
}
|
|
3012
|
+
} catch (e: Exception) {
|
|
3013
|
+
activity.runOnUiThread {
|
|
3014
|
+
webView.evaluateJavascript(
|
|
3015
|
+
"window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
|
|
3016
|
+
null
|
|
3017
|
+
)
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
// ==================== PDF Viewer ====================
|
|
3023
|
+
|
|
3024
|
+
@JavascriptInterface
|
|
3025
|
+
fun openPDF(source: String, page: Int) {
|
|
3026
|
+
activity.runOnUiThread {
|
|
3027
|
+
try {
|
|
3028
|
+
val uri = if (source.startsWith("data:")) {
|
|
3029
|
+
// Save base64 to temp file and open
|
|
3030
|
+
val base64Data = source.substringAfter(",")
|
|
3031
|
+
val bytes = Base64.decode(base64Data, Base64.DEFAULT)
|
|
3032
|
+
val tempFile = File.createTempFile("craft_pdf_", ".pdf", activity.cacheDir)
|
|
3033
|
+
tempFile.writeBytes(bytes)
|
|
3034
|
+
Uri.fromFile(tempFile)
|
|
3035
|
+
} else {
|
|
3036
|
+
Uri.parse(source)
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
3040
|
+
setDataAndType(uri, "application/pdf")
|
|
3041
|
+
addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
|
3042
|
+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
activity.startActivity(intent)
|
|
3046
|
+
webView.evaluateJavascript(
|
|
3047
|
+
"window._craftPDFResolve && window._craftPDFResolve({opened: true})",
|
|
3048
|
+
null
|
|
3049
|
+
)
|
|
3050
|
+
} catch (e: Exception) {
|
|
3051
|
+
webView.evaluateJavascript(
|
|
3052
|
+
"window._craftPDFReject && window._craftPDFReject('${e.message}')",
|
|
3053
|
+
null
|
|
3054
|
+
)
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
@JavascriptInterface
|
|
3060
|
+
fun closePDF() {
|
|
3061
|
+
// PDFs are opened in external viewers, so we just acknowledge
|
|
3062
|
+
activity.runOnUiThread {
|
|
3063
|
+
webView.evaluateJavascript(
|
|
3064
|
+
"window._craftPDFResolve && window._craftPDFResolve(true)",
|
|
3065
|
+
null
|
|
3066
|
+
)
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// ==================== Contacts Picker ====================
|
|
3071
|
+
|
|
3072
|
+
@JavascriptInterface
|
|
3073
|
+
fun pickContact(multiple: Boolean) {
|
|
3074
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS)
|
|
3075
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
3076
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CONTACTS), REQUEST_PICK_CONTACT)
|
|
3077
|
+
return
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
activity.runOnUiThread {
|
|
3081
|
+
val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
|
|
3082
|
+
activity.startActivityForResult(intent, REQUEST_PICK_CONTACT)
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
fun handleContactPickerResult(resultCode: Int, data: Intent?) {
|
|
3087
|
+
if (resultCode != Activity.RESULT_OK || data?.data == null) {
|
|
3088
|
+
activity.runOnUiThread {
|
|
3089
|
+
webView.evaluateJavascript(
|
|
3090
|
+
"window._craftPickContactReject && window._craftPickContactReject('Cancelled')",
|
|
3091
|
+
null
|
|
3092
|
+
)
|
|
3093
|
+
}
|
|
3094
|
+
return
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
try {
|
|
3098
|
+
val contactUri = data.data!!
|
|
3099
|
+
val projection = arrayOf(
|
|
3100
|
+
ContactsContract.Contacts._ID,
|
|
3101
|
+
ContactsContract.Contacts.DISPLAY_NAME
|
|
3102
|
+
)
|
|
3103
|
+
|
|
3104
|
+
activity.contentResolver.query(contactUri, projection, null, null, null)?.use { cursor ->
|
|
3105
|
+
if (cursor.moveToFirst()) {
|
|
3106
|
+
val contactId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
|
|
3107
|
+
val displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
|
|
3108
|
+
|
|
3109
|
+
// Get phone numbers
|
|
3110
|
+
val phones = mutableListOf<Map<String, String>>()
|
|
3111
|
+
activity.contentResolver.query(
|
|
3112
|
+
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
|
3113
|
+
arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.TYPE),
|
|
3114
|
+
"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
|
|
3115
|
+
arrayOf(contactId),
|
|
3116
|
+
null
|
|
3117
|
+
)?.use { phoneCursor ->
|
|
3118
|
+
while (phoneCursor.moveToNext()) {
|
|
3119
|
+
val number = phoneCursor.getString(phoneCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
|
|
3120
|
+
phones.add(mapOf("number" to number))
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// Get emails
|
|
3125
|
+
val emails = mutableListOf<Map<String, String>>()
|
|
3126
|
+
activity.contentResolver.query(
|
|
3127
|
+
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
|
|
3128
|
+
arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS),
|
|
3129
|
+
"${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?",
|
|
3130
|
+
arrayOf(contactId),
|
|
3131
|
+
null
|
|
3132
|
+
)?.use { emailCursor ->
|
|
3133
|
+
while (emailCursor.moveToNext()) {
|
|
3134
|
+
val address = emailCursor.getString(emailCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS))
|
|
3135
|
+
emails.add(mapOf("address" to address))
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
val result = JSONObject().apply {
|
|
3140
|
+
put("id", contactId)
|
|
3141
|
+
put("displayName", displayName)
|
|
3142
|
+
put("phoneNumbers", JSONArray(phones.map { JSONObject(it) }))
|
|
3143
|
+
put("emailAddresses", JSONArray(emails.map { JSONObject(it) }))
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
activity.runOnUiThread {
|
|
3147
|
+
webView.evaluateJavascript(
|
|
3148
|
+
"window._craftPickContactResolve && window._craftPickContactResolve($result)",
|
|
3149
|
+
null
|
|
3150
|
+
)
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
} catch (e: Exception) {
|
|
3155
|
+
activity.runOnUiThread {
|
|
3156
|
+
webView.evaluateJavascript(
|
|
3157
|
+
"window._craftPickContactReject && window._craftPickContactReject('${e.message}')",
|
|
3158
|
+
null
|
|
3159
|
+
)
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
// ==================== App Shortcuts ====================
|
|
3165
|
+
|
|
3166
|
+
@JavascriptInterface
|
|
3167
|
+
fun setShortcuts(shortcutsJson: String) {
|
|
3168
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
|
3169
|
+
activity.runOnUiThread {
|
|
3170
|
+
webView.evaluateJavascript(
|
|
3171
|
+
"window._craftShortcutsReject && window._craftShortcutsReject('App shortcuts require Android 7.1+')",
|
|
3172
|
+
null
|
|
3173
|
+
)
|
|
3174
|
+
}
|
|
3175
|
+
return
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
try {
|
|
3179
|
+
val shortcutManager = activity.getSystemService(android.content.pm.ShortcutManager::class.java)
|
|
3180
|
+
val shortcuts = mutableListOf<android.content.pm.ShortcutInfo>()
|
|
3181
|
+
val jsonArray = JSONArray(shortcutsJson)
|
|
3182
|
+
|
|
3183
|
+
for (i in 0 until jsonArray.length()) {
|
|
3184
|
+
val shortcut = jsonArray.getJSONObject(i)
|
|
3185
|
+
val type = shortcut.getString("type")
|
|
3186
|
+
val title = shortcut.getString("title")
|
|
3187
|
+
val subtitle = if (shortcut.has("subtitle")) shortcut.getString("subtitle") else null
|
|
3188
|
+
|
|
3189
|
+
val intent = Intent(activity, activity::class.java).apply {
|
|
3190
|
+
action = Intent.ACTION_VIEW
|
|
3191
|
+
putExtra("shortcut_type", type)
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
val builder = android.content.pm.ShortcutInfo.Builder(activity, type)
|
|
3195
|
+
.setShortLabel(title)
|
|
3196
|
+
.setIntent(intent)
|
|
3197
|
+
|
|
3198
|
+
subtitle?.let { builder.setLongLabel(it) }
|
|
3199
|
+
|
|
3200
|
+
shortcuts.add(builder.build())
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
shortcutManager?.dynamicShortcuts = shortcuts
|
|
3204
|
+
|
|
3205
|
+
activity.runOnUiThread {
|
|
3206
|
+
webView.evaluateJavascript(
|
|
3207
|
+
"window._craftShortcutsResolve && window._craftShortcutsResolve({count: ${shortcuts.size}})",
|
|
3208
|
+
null
|
|
3209
|
+
)
|
|
3210
|
+
}
|
|
3211
|
+
} catch (e: Exception) {
|
|
3212
|
+
activity.runOnUiThread {
|
|
3213
|
+
webView.evaluateJavascript(
|
|
3214
|
+
"window._craftShortcutsReject && window._craftShortcutsReject('${e.message}')",
|
|
3215
|
+
null
|
|
3216
|
+
)
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
@JavascriptInterface
|
|
3222
|
+
fun clearShortcuts() {
|
|
3223
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
|
3224
|
+
try {
|
|
3225
|
+
val shortcutManager = activity.getSystemService(android.content.pm.ShortcutManager::class.java)
|
|
3226
|
+
shortcutManager?.removeAllDynamicShortcuts()
|
|
3227
|
+
|
|
3228
|
+
activity.runOnUiThread {
|
|
3229
|
+
webView.evaluateJavascript(
|
|
3230
|
+
"window._craftShortcutsResolve && window._craftShortcutsResolve(true)",
|
|
3231
|
+
null
|
|
3232
|
+
)
|
|
3233
|
+
}
|
|
3234
|
+
} catch (e: Exception) {
|
|
3235
|
+
activity.runOnUiThread {
|
|
3236
|
+
webView.evaluateJavascript(
|
|
3237
|
+
"window._craftShortcutsReject && window._craftShortcutsReject('${e.message}')",
|
|
3238
|
+
null
|
|
3239
|
+
)
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
} else {
|
|
3243
|
+
activity.runOnUiThread {
|
|
3244
|
+
webView.evaluateJavascript(
|
|
3245
|
+
"window._craftShortcutsResolve && window._craftShortcutsResolve(true)",
|
|
3246
|
+
null
|
|
3247
|
+
)
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
// ==================== Shared Preferences (Keychain equivalent) ====================
|
|
3253
|
+
|
|
3254
|
+
@JavascriptInterface
|
|
3255
|
+
fun setSharedItem(key: String, value: String, group: String) {
|
|
3256
|
+
try {
|
|
3257
|
+
val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
|
|
3258
|
+
val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
3259
|
+
prefs.edit().putString(key, value).apply()
|
|
3260
|
+
|
|
3261
|
+
activity.runOnUiThread {
|
|
3262
|
+
webView.evaluateJavascript(
|
|
3263
|
+
"window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({success: true, key: '$key'})",
|
|
3264
|
+
null
|
|
3265
|
+
)
|
|
3266
|
+
}
|
|
3267
|
+
} catch (e: Exception) {
|
|
3268
|
+
activity.runOnUiThread {
|
|
3269
|
+
webView.evaluateJavascript(
|
|
3270
|
+
"window._craftSharedKeychainReject && window._craftSharedKeychainReject('${e.message}')",
|
|
3271
|
+
null
|
|
3272
|
+
)
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
@JavascriptInterface
|
|
3278
|
+
fun getSharedItem(key: String, group: String) {
|
|
3279
|
+
try {
|
|
3280
|
+
val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
|
|
3281
|
+
val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
3282
|
+
val value = prefs.getString(key, null)
|
|
3283
|
+
|
|
3284
|
+
activity.runOnUiThread {
|
|
3285
|
+
if (value != null) {
|
|
3286
|
+
webView.evaluateJavascript(
|
|
3287
|
+
"window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({value: '$value', key: '$key'})",
|
|
3288
|
+
null
|
|
3289
|
+
)
|
|
3290
|
+
} else {
|
|
3291
|
+
webView.evaluateJavascript(
|
|
3292
|
+
"window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({value: null, key: '$key'})",
|
|
3293
|
+
null
|
|
3294
|
+
)
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
} catch (e: Exception) {
|
|
3298
|
+
activity.runOnUiThread {
|
|
3299
|
+
webView.evaluateJavascript(
|
|
3300
|
+
"window._craftSharedKeychainReject && window._craftSharedKeychainReject('${e.message}')",
|
|
3301
|
+
null
|
|
3302
|
+
)
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
@JavascriptInterface
|
|
3308
|
+
fun removeSharedItem(key: String, group: String) {
|
|
3309
|
+
try {
|
|
3310
|
+
val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
|
|
3311
|
+
val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
3312
|
+
prefs.edit().remove(key).apply()
|
|
3313
|
+
|
|
3314
|
+
activity.runOnUiThread {
|
|
3315
|
+
webView.evaluateJavascript(
|
|
3316
|
+
"window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({success: true, key: '$key'})",
|
|
3317
|
+
null
|
|
3318
|
+
)
|
|
3319
|
+
}
|
|
3320
|
+
} catch (e: Exception) {
|
|
3321
|
+
activity.runOnUiThread {
|
|
3322
|
+
webView.evaluateJavascript(
|
|
3323
|
+
"window._craftSharedKeychainReject && window._craftSharedKeychainReject('${e.message}')",
|
|
3324
|
+
null
|
|
3325
|
+
)
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// ==================== Local Auth Persistence ====================
|
|
3331
|
+
|
|
3332
|
+
private var authSessionExpiry: Long = 0L
|
|
3333
|
+
|
|
3334
|
+
@JavascriptInterface
|
|
3335
|
+
fun setAuthPersistence(enabled: Boolean, duration: Long) {
|
|
3336
|
+
try {
|
|
3337
|
+
if (enabled) {
|
|
3338
|
+
authSessionExpiry = System.currentTimeMillis() + (duration * 1000)
|
|
3339
|
+
activity.runOnUiThread {
|
|
3340
|
+
webView.evaluateJavascript(
|
|
3341
|
+
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({enabled: true, duration: $duration, expiresAt: $authSessionExpiry})",
|
|
3342
|
+
null
|
|
3343
|
+
)
|
|
3344
|
+
}
|
|
3345
|
+
} else {
|
|
3346
|
+
authSessionExpiry = 0L
|
|
3347
|
+
activity.runOnUiThread {
|
|
3348
|
+
webView.evaluateJavascript(
|
|
3349
|
+
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({enabled: false})",
|
|
3350
|
+
null
|
|
3351
|
+
)
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
} catch (e: Exception) {
|
|
3355
|
+
activity.runOnUiThread {
|
|
3356
|
+
webView.evaluateJavascript(
|
|
3357
|
+
"window._craftAuthPersistReject && window._craftAuthPersistReject('${e.message}')",
|
|
3358
|
+
null
|
|
3359
|
+
)
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
@JavascriptInterface
|
|
3365
|
+
fun checkAuthPersistence() {
|
|
3366
|
+
try {
|
|
3367
|
+
val now = System.currentTimeMillis()
|
|
3368
|
+
val isValid = authSessionExpiry > now
|
|
3369
|
+
val remainingMs = if (isValid) authSessionExpiry - now else 0L
|
|
3370
|
+
val remainingSeconds = remainingMs / 1000
|
|
3371
|
+
|
|
3372
|
+
activity.runOnUiThread {
|
|
3373
|
+
webView.evaluateJavascript(
|
|
3374
|
+
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({isValid: $isValid, remainingSeconds: $remainingSeconds})",
|
|
3375
|
+
null
|
|
3376
|
+
)
|
|
3377
|
+
}
|
|
3378
|
+
} catch (e: Exception) {
|
|
3379
|
+
activity.runOnUiThread {
|
|
3380
|
+
webView.evaluateJavascript(
|
|
3381
|
+
"window._craftAuthPersistReject && window._craftAuthPersistReject('${e.message}')",
|
|
3382
|
+
null
|
|
3383
|
+
)
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
@JavascriptInterface
|
|
3389
|
+
fun clearAuthPersistence() {
|
|
3390
|
+
try {
|
|
3391
|
+
authSessionExpiry = 0L
|
|
3392
|
+
activity.runOnUiThread {
|
|
3393
|
+
webView.evaluateJavascript(
|
|
3394
|
+
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({cleared: true})",
|
|
3395
|
+
null
|
|
3396
|
+
)
|
|
3397
|
+
}
|
|
3398
|
+
} catch (e: Exception) {
|
|
3399
|
+
activity.runOnUiThread {
|
|
3400
|
+
webView.evaluateJavascript(
|
|
3401
|
+
"window._craftAuthPersistReject && window._craftAuthPersistReject('${e.message}')",
|
|
3402
|
+
null
|
|
3403
|
+
)
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// ==================== AR (ARCore) ====================
|
|
3409
|
+
// Note: ARCore requires additional setup in build.gradle and manifest
|
|
3410
|
+
// This is a placeholder that indicates AR is not available on this platform via simple JS bridge
|
|
3411
|
+
// For full ARCore support, consider using Sceneform or AR Fragment
|
|
3412
|
+
|
|
3413
|
+
@JavascriptInterface
|
|
3414
|
+
fun startAR(optionsJson: String) {
|
|
3415
|
+
activity.runOnUiThread {
|
|
3416
|
+
webView.evaluateJavascript(
|
|
3417
|
+
"window._craftARReject && window._craftARReject('ARCore requires native Activity integration. Use Sceneform or AR Fragment for full AR support.')",
|
|
3418
|
+
null
|
|
3419
|
+
)
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
@JavascriptInterface
|
|
3424
|
+
fun stopAR() {
|
|
3425
|
+
activity.runOnUiThread {
|
|
3426
|
+
webView.evaluateJavascript(
|
|
3427
|
+
"window._craftARResolve && window._craftARResolve({stopped: true})",
|
|
3428
|
+
null
|
|
3429
|
+
)
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
@JavascriptInterface
|
|
3434
|
+
fun placeARObject(model: String, positionJson: String) {
|
|
3435
|
+
activity.runOnUiThread {
|
|
3436
|
+
webView.evaluateJavascript(
|
|
3437
|
+
"window._craftARReject && window._craftARReject('ARCore requires native Activity integration')",
|
|
3438
|
+
null
|
|
3439
|
+
)
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
@JavascriptInterface
|
|
3444
|
+
fun removeARObject(objectId: String) {
|
|
3445
|
+
activity.runOnUiThread {
|
|
3446
|
+
webView.evaluateJavascript(
|
|
3447
|
+
"window._craftARReject && window._craftARReject('ARCore requires native Activity integration')",
|
|
3448
|
+
null
|
|
3449
|
+
)
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
@JavascriptInterface
|
|
3454
|
+
fun getARPlanes() {
|
|
3455
|
+
activity.runOnUiThread {
|
|
3456
|
+
webView.evaluateJavascript(
|
|
3457
|
+
"window._craftARResolve && window._craftARResolve([])",
|
|
3458
|
+
null
|
|
3459
|
+
)
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// ==================== ML Kit ====================
|
|
3464
|
+
|
|
3465
|
+
@JavascriptInterface
|
|
3466
|
+
fun classifyImage(imageBase64: String) {
|
|
3467
|
+
try {
|
|
3468
|
+
val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
|
|
3469
|
+
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
3470
|
+
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
|
3471
|
+
|
|
3472
|
+
val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)
|
|
3473
|
+
labeler.process(inputImage)
|
|
3474
|
+
.addOnSuccessListener { labels ->
|
|
3475
|
+
val results = JSONArray()
|
|
3476
|
+
for (label in labels) {
|
|
3477
|
+
val obj = JSONObject()
|
|
3478
|
+
obj.put("label", label.text)
|
|
3479
|
+
obj.put("confidence", label.confidence)
|
|
3480
|
+
obj.put("index", label.index)
|
|
3481
|
+
results.put(obj)
|
|
3482
|
+
}
|
|
3483
|
+
activity.runOnUiThread {
|
|
3484
|
+
webView.evaluateJavascript(
|
|
3485
|
+
"window._craftMLResolve && window._craftMLResolve($results)",
|
|
3486
|
+
null
|
|
3487
|
+
)
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
.addOnFailureListener { e ->
|
|
3491
|
+
activity.runOnUiThread {
|
|
3492
|
+
webView.evaluateJavascript(
|
|
3493
|
+
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3494
|
+
null
|
|
3495
|
+
)
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
} catch (e: Exception) {
|
|
3499
|
+
activity.runOnUiThread {
|
|
3500
|
+
webView.evaluateJavascript(
|
|
3501
|
+
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3502
|
+
null
|
|
3503
|
+
)
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
@JavascriptInterface
|
|
3509
|
+
fun detectObjects(imageBase64: String) {
|
|
3510
|
+
try {
|
|
3511
|
+
val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
|
|
3512
|
+
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
3513
|
+
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
|
3514
|
+
|
|
3515
|
+
val options = ObjectDetectorOptions.Builder()
|
|
3516
|
+
.setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
|
|
3517
|
+
.enableMultipleObjects()
|
|
3518
|
+
.enableClassification()
|
|
3519
|
+
.build()
|
|
3520
|
+
|
|
3521
|
+
val objectDetector = ObjectDetection.getClient(options)
|
|
3522
|
+
objectDetector.process(inputImage)
|
|
3523
|
+
.addOnSuccessListener { detectedObjects ->
|
|
3524
|
+
val results = JSONArray()
|
|
3525
|
+
for (obj in detectedObjects) {
|
|
3526
|
+
val objJson = JSONObject()
|
|
3527
|
+
|
|
3528
|
+
// Bounding box
|
|
3529
|
+
val bbox = JSONObject()
|
|
3530
|
+
bbox.put("x", obj.boundingBox.left)
|
|
3531
|
+
bbox.put("y", obj.boundingBox.top)
|
|
3532
|
+
bbox.put("width", obj.boundingBox.width())
|
|
3533
|
+
bbox.put("height", obj.boundingBox.height())
|
|
3534
|
+
objJson.put("boundingBox", bbox)
|
|
3535
|
+
|
|
3536
|
+
// Labels
|
|
3537
|
+
val labels = JSONArray()
|
|
3538
|
+
for (label in obj.labels) {
|
|
3539
|
+
val labelJson = JSONObject()
|
|
3540
|
+
labelJson.put("label", label.text)
|
|
3541
|
+
labelJson.put("confidence", label.confidence)
|
|
3542
|
+
labelJson.put("index", label.index)
|
|
3543
|
+
labels.put(labelJson)
|
|
3544
|
+
}
|
|
3545
|
+
objJson.put("labels", labels)
|
|
3546
|
+
objJson.put("trackingId", obj.trackingId)
|
|
3547
|
+
|
|
3548
|
+
results.put(objJson)
|
|
3549
|
+
}
|
|
3550
|
+
activity.runOnUiThread {
|
|
3551
|
+
webView.evaluateJavascript(
|
|
3552
|
+
"window._craftMLResolve && window._craftMLResolve($results)",
|
|
3553
|
+
null
|
|
3554
|
+
)
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
.addOnFailureListener { e ->
|
|
3558
|
+
activity.runOnUiThread {
|
|
3559
|
+
webView.evaluateJavascript(
|
|
3560
|
+
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3561
|
+
null
|
|
3562
|
+
)
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
} catch (e: Exception) {
|
|
3566
|
+
activity.runOnUiThread {
|
|
3567
|
+
webView.evaluateJavascript(
|
|
3568
|
+
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3569
|
+
null
|
|
3570
|
+
)
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
@JavascriptInterface
|
|
3576
|
+
fun recognizeText(imageBase64: String) {
|
|
3577
|
+
try {
|
|
3578
|
+
val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
|
|
3579
|
+
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
3580
|
+
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
|
3581
|
+
|
|
3582
|
+
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
|
|
3583
|
+
recognizer.process(inputImage)
|
|
3584
|
+
.addOnSuccessListener { visionText ->
|
|
3585
|
+
val results = JSONArray()
|
|
3586
|
+
for (block in visionText.textBlocks) {
|
|
3587
|
+
for (line in block.lines) {
|
|
3588
|
+
val lineJson = JSONObject()
|
|
3589
|
+
lineJson.put("text", line.text)
|
|
3590
|
+
lineJson.put("confidence", line.confidence ?: 0.0)
|
|
3591
|
+
|
|
3592
|
+
val boundingBox = line.boundingBox
|
|
3593
|
+
if (boundingBox != null) {
|
|
3594
|
+
val bbox = JSONObject()
|
|
3595
|
+
bbox.put("x", boundingBox.left)
|
|
3596
|
+
bbox.put("y", boundingBox.top)
|
|
3597
|
+
bbox.put("width", boundingBox.width())
|
|
3598
|
+
bbox.put("height", boundingBox.height())
|
|
3599
|
+
lineJson.put("boundingBox", bbox)
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
results.put(lineJson)
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
activity.runOnUiThread {
|
|
3606
|
+
webView.evaluateJavascript(
|
|
3607
|
+
"window._craftMLResolve && window._craftMLResolve($results)",
|
|
3608
|
+
null
|
|
3609
|
+
)
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
.addOnFailureListener { e ->
|
|
3613
|
+
activity.runOnUiThread {
|
|
3614
|
+
webView.evaluateJavascript(
|
|
3615
|
+
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3616
|
+
null
|
|
3617
|
+
)
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
} catch (e: Exception) {
|
|
3621
|
+
activity.runOnUiThread {
|
|
3622
|
+
webView.evaluateJavascript(
|
|
3623
|
+
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3624
|
+
null
|
|
3625
|
+
)
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// ==================== Widget Support ====================
|
|
3631
|
+
|
|
3632
|
+
private val widgetPrefs by lazy {
|
|
3633
|
+
activity.getSharedPreferences("craft_widget_prefs", Context.MODE_PRIVATE)
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
@JavascriptInterface
|
|
3637
|
+
fun updateWidget(dataJson: String) {
|
|
3638
|
+
try {
|
|
3639
|
+
val data = JSONObject(dataJson)
|
|
3640
|
+
val editor = widgetPrefs.edit()
|
|
3641
|
+
|
|
3642
|
+
if (data.has("title")) {
|
|
3643
|
+
editor.putString("widget_title", data.getString("title"))
|
|
3644
|
+
}
|
|
3645
|
+
if (data.has("subtitle")) {
|
|
3646
|
+
editor.putString("widget_subtitle", data.getString("subtitle"))
|
|
3647
|
+
}
|
|
3648
|
+
if (data.has("value")) {
|
|
3649
|
+
editor.putString("widget_value", data.getString("value"))
|
|
3650
|
+
}
|
|
3651
|
+
if (data.has("icon")) {
|
|
3652
|
+
editor.putString("widget_icon", data.getString("icon"))
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
editor.apply()
|
|
3656
|
+
|
|
3657
|
+
// Notify widgets to update
|
|
3658
|
+
val intent = Intent("{{PACKAGE_NAME}}.WIDGET_UPDATE")
|
|
3659
|
+
intent.setPackage(activity.packageName)
|
|
3660
|
+
activity.sendBroadcast(intent)
|
|
3661
|
+
|
|
3662
|
+
activity.runOnUiThread {
|
|
3663
|
+
webView.evaluateJavascript(
|
|
3664
|
+
"window._craftWidgetResolve && window._craftWidgetResolve({updated: true})",
|
|
3665
|
+
null
|
|
3666
|
+
)
|
|
3667
|
+
}
|
|
3668
|
+
} catch (e: Exception) {
|
|
3669
|
+
activity.runOnUiThread {
|
|
3670
|
+
webView.evaluateJavascript(
|
|
3671
|
+
"window._craftWidgetReject && window._craftWidgetReject('${e.message}')",
|
|
3672
|
+
null
|
|
3673
|
+
)
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
@JavascriptInterface
|
|
3679
|
+
fun reloadWidgets() {
|
|
3680
|
+
val intent = Intent("{{PACKAGE_NAME}}.WIDGET_UPDATE")
|
|
3681
|
+
intent.setPackage(activity.packageName)
|
|
3682
|
+
activity.sendBroadcast(intent)
|
|
3683
|
+
|
|
3684
|
+
activity.runOnUiThread {
|
|
3685
|
+
webView.evaluateJavascript(
|
|
3686
|
+
"window._craftWidgetResolve && window._craftWidgetResolve({reloaded: true})",
|
|
3687
|
+
null
|
|
3688
|
+
)
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// ==================== Google Assistant / App Actions ====================
|
|
3693
|
+
|
|
3694
|
+
@JavascriptInterface
|
|
3695
|
+
fun registerVoiceAction(phrase: String, action: String) {
|
|
3696
|
+
// App Actions are defined in shortcuts.xml, not dynamically
|
|
3697
|
+
// This stores the action for handling incoming intents
|
|
3698
|
+
val prefs = activity.getSharedPreferences("craft_voice_actions", Context.MODE_PRIVATE)
|
|
3699
|
+
prefs.edit().putString(action, phrase).apply()
|
|
3700
|
+
|
|
3701
|
+
activity.runOnUiThread {
|
|
3702
|
+
webView.evaluateJavascript(
|
|
3703
|
+
"window._craftVoiceResolve && window._craftVoiceResolve({registered: true, action: '$action', phrase: '$phrase'})",
|
|
3704
|
+
null
|
|
3705
|
+
)
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
@JavascriptInterface
|
|
3710
|
+
fun removeVoiceAction(action: String) {
|
|
3711
|
+
val prefs = activity.getSharedPreferences("craft_voice_actions", Context.MODE_PRIVATE)
|
|
3712
|
+
prefs.edit().remove(action).apply()
|
|
3713
|
+
|
|
3714
|
+
activity.runOnUiThread {
|
|
3715
|
+
webView.evaluateJavascript(
|
|
3716
|
+
"window._craftVoiceResolve && window._craftVoiceResolve({removed: true, action: '$action'})",
|
|
3717
|
+
null
|
|
3718
|
+
)
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
// Handle incoming voice action from Google Assistant
|
|
3723
|
+
fun handleVoiceAction(intent: Intent?) {
|
|
3724
|
+
intent?.let {
|
|
3725
|
+
val action = it.action
|
|
3726
|
+
val data = it.dataString
|
|
3727
|
+
|
|
3728
|
+
if (action != null) {
|
|
3729
|
+
activity.runOnUiThread {
|
|
3730
|
+
webView.evaluateJavascript(
|
|
3731
|
+
"window.dispatchEvent(new CustomEvent('craftVoiceAction', {detail: {action: '$action', data: '$data'}}));",
|
|
3732
|
+
null
|
|
3733
|
+
)
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
// ==================== Wear OS Connectivity ====================
|
|
3740
|
+
|
|
3741
|
+
@JavascriptInterface
|
|
3742
|
+
fun sendToWatch(messageJson: String) {
|
|
3743
|
+
// Note: Full Wearable API requires separate Wear OS app
|
|
3744
|
+
// This provides placeholder API for future integration
|
|
3745
|
+
activity.runOnUiThread {
|
|
3746
|
+
webView.evaluateJavascript(
|
|
3747
|
+
"window._craftWatchReject && window._craftWatchReject('Wear OS integration requires companion app setup')",
|
|
3748
|
+
null
|
|
3749
|
+
)
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
@JavascriptInterface
|
|
3754
|
+
fun updateWatchContext(contextJson: String) {
|
|
3755
|
+
// Note: Full Wearable API requires Data Layer API setup
|
|
3756
|
+
activity.runOnUiThread {
|
|
3757
|
+
webView.evaluateJavascript(
|
|
3758
|
+
"window._craftWatchReject && window._craftWatchReject('Wear OS integration requires companion app setup')",
|
|
3759
|
+
null
|
|
3760
|
+
)
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
@JavascriptInterface
|
|
3765
|
+
fun isWatchReachable(): Boolean {
|
|
3766
|
+
// Note: Would use CapabilityClient to check for paired watch
|
|
3767
|
+
return false
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
// ==================== Deep Links ====================
|
|
3771
|
+
|
|
3772
|
+
private var initialURL: String? = null
|
|
3773
|
+
|
|
3774
|
+
fun setInitialURL(url: String?) {
|
|
3775
|
+
initialURL = url
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
@JavascriptInterface
|
|
3779
|
+
fun getInitialURL() {
|
|
3780
|
+
activity.runOnUiThread {
|
|
3781
|
+
if (initialURL != null) {
|
|
3782
|
+
val uri = Uri.parse(initialURL)
|
|
3783
|
+
val json = JSONObject().apply {
|
|
3784
|
+
put("url", initialURL)
|
|
3785
|
+
put("scheme", uri.scheme ?: "")
|
|
3786
|
+
put("host", uri.host ?: "")
|
|
3787
|
+
put("path", uri.path ?: "")
|
|
3788
|
+
put("query", uri.query ?: "")
|
|
3789
|
+
// Parse query parameters
|
|
3790
|
+
val queryParams = JSONObject()
|
|
3791
|
+
uri.queryParameterNames.forEach { name ->
|
|
3792
|
+
queryParams.put(name, uri.getQueryParameter(name) ?: "")
|
|
3793
|
+
}
|
|
3794
|
+
put("queryParams", queryParams)
|
|
3795
|
+
}
|
|
3796
|
+
webView.evaluateJavascript(
|
|
3797
|
+
"window._craftDeepLinkResolve && window._craftDeepLinkResolve($json)",
|
|
3798
|
+
null
|
|
3799
|
+
)
|
|
3800
|
+
} else {
|
|
3801
|
+
webView.evaluateJavascript(
|
|
3802
|
+
"window._craftDeepLinkResolve && window._craftDeepLinkResolve(null)",
|
|
3803
|
+
null
|
|
3804
|
+
)
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
fun dispatchDeepLink(url: String) {
|
|
3810
|
+
// Store as initial URL if this is the first one
|
|
3811
|
+
if (initialURL == null) {
|
|
3812
|
+
initialURL = url
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
val uri = Uri.parse(url)
|
|
3816
|
+
val json = JSONObject().apply {
|
|
3817
|
+
put("url", url)
|
|
3818
|
+
put("scheme", uri.scheme ?: "")
|
|
3819
|
+
put("host", uri.host ?: "")
|
|
3820
|
+
put("path", uri.path ?: "")
|
|
3821
|
+
put("query", uri.query ?: "")
|
|
3822
|
+
// Parse query parameters
|
|
3823
|
+
val queryParams = JSONObject()
|
|
3824
|
+
uri.queryParameterNames.forEach { name ->
|
|
3825
|
+
queryParams.put(name, uri.getQueryParameter(name) ?: "")
|
|
3826
|
+
}
|
|
3827
|
+
put("queryParams", queryParams)
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
activity.runOnUiThread {
|
|
3831
|
+
webView.evaluateJavascript(
|
|
3832
|
+
"window.dispatchEvent(new CustomEvent('craftDeepLink', {detail: $json}));",
|
|
3833
|
+
null
|
|
3834
|
+
)
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
// ==================== Performance Profiling ====================
|
|
3839
|
+
|
|
3840
|
+
@JavascriptInterface
|
|
3841
|
+
fun getMemoryUsage(): String {
|
|
3842
|
+
val runtime = Runtime.getRuntime()
|
|
3843
|
+
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
|
3844
|
+
val maxMemory = runtime.maxMemory()
|
|
3845
|
+
val totalMemory = runtime.totalMemory()
|
|
3846
|
+
|
|
3847
|
+
return JSONObject().apply {
|
|
3848
|
+
put("usedMB", Math.round(usedMemory / 1024.0 / 1024.0 * 100) / 100.0)
|
|
3849
|
+
put("maxMB", Math.round(maxMemory / 1024.0 / 1024.0 * 100) / 100.0)
|
|
3850
|
+
put("totalMB", Math.round(totalMemory / 1024.0 / 1024.0 * 100) / 100.0)
|
|
3851
|
+
put("usedBytes", usedMemory)
|
|
3852
|
+
put("maxBytes", maxMemory)
|
|
3853
|
+
}.toString()
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
// ==================== Helpers ====================
|
|
3857
|
+
|
|
3858
|
+
private fun sendEvent(event: String, data: Map<String, Any>) {
|
|
3859
|
+
val json = JSONObject(data).toString()
|
|
3860
|
+
activity.runOnUiThread {
|
|
3861
|
+
webView.evaluateJavascript(
|
|
3862
|
+
"window.dispatchEvent(new CustomEvent('$event', {detail: $json}));",
|
|
3863
|
+
null
|
|
3864
|
+
)
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
companion object {
|
|
3869
|
+
const val REQUEST_CAMERA = 1001
|
|
3870
|
+
const val REQUEST_GALLERY = 1002
|
|
3871
|
+
const val REQUEST_LOCATION = 1003
|
|
3872
|
+
const val REQUEST_CONTACTS = 1004
|
|
3873
|
+
const val REQUEST_CALENDAR = 1005
|
|
3874
|
+
const val REQUEST_FILE_PICKER = 1006
|
|
3875
|
+
const val REQUEST_AUDIO = 1007
|
|
3876
|
+
const val REQUEST_VIDEO = 1008
|
|
3877
|
+
const val REQUEST_BLUETOOTH = 1009
|
|
3878
|
+
const val REQUEST_PICK_CONTACT = 1010
|
|
3879
|
+
}
|
|
3880
|
+
}
|