@qusaieilouti99/call-manager 0.1.166 → 0.1.168
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.
|
@@ -34,14 +34,6 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|
|
34
34
|
import org.json.JSONArray
|
|
35
35
|
import org.json.JSONObject
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Core call‐management engine. Manages self-managed telecom calls,
|
|
39
|
-
* audio routing, UI notifications, etc.
|
|
40
|
-
*
|
|
41
|
-
* Audio routing is now fully delegated to the Android Telecom framework,
|
|
42
|
-
* which is the correct approach for self-managed calls. This ensures
|
|
43
|
-
* consistency and proper handling of device changes (BT, headset, etc.).
|
|
44
|
-
*/
|
|
45
37
|
object CallEngine {
|
|
46
38
|
private const val TAG = "CallEngine"
|
|
47
39
|
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
@@ -329,7 +321,6 @@ object CallEngine {
|
|
|
329
321
|
val extras = Bundle().apply {
|
|
330
322
|
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
331
323
|
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
332
|
-
// Let Telecom decide the initial audio route based on devices and video state
|
|
333
324
|
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCall)
|
|
334
325
|
}
|
|
335
326
|
|
|
@@ -348,11 +339,6 @@ object CallEngine {
|
|
|
348
339
|
updateLockScreenBypass()
|
|
349
340
|
}
|
|
350
341
|
|
|
351
|
-
/**
|
|
352
|
-
* Starts a call and immediately sets it to active.
|
|
353
|
-
* Use this for joining an already-established call.
|
|
354
|
-
* If a call with the same ID is already incoming, it answers it instead.
|
|
355
|
-
*/
|
|
356
342
|
fun startCall(
|
|
357
343
|
callId: String,
|
|
358
344
|
callType: String,
|
|
@@ -382,8 +368,7 @@ object CallEngine {
|
|
|
382
368
|
}
|
|
383
369
|
}
|
|
384
370
|
|
|
385
|
-
|
|
386
|
-
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
371
|
+
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
|
|
387
372
|
currentCallId = callId
|
|
388
373
|
Log.d(TAG, "Call $callId will be started as ACTIVE.")
|
|
389
374
|
|
|
@@ -399,7 +384,7 @@ object CallEngine {
|
|
|
399
384
|
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
400
385
|
putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
|
|
401
386
|
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
|
|
402
|
-
putBoolean(MyConnectionService.EXTRA_START_IMMEDIATELY_ACTIVE, true)
|
|
387
|
+
putBoolean(MyConnectionService.EXTRA_START_IMMEDIATELY_ACTIVE, true)
|
|
403
388
|
metadata?.let { putString("metadata", it) }
|
|
404
389
|
}
|
|
405
390
|
|
|
@@ -414,7 +399,6 @@ object CallEngine {
|
|
|
414
399
|
bringAppToForeground()
|
|
415
400
|
keepScreenAwake(true)
|
|
416
401
|
updateLockScreenBypass()
|
|
417
|
-
// Event is emitted by coreCallAnswered once the connection is active
|
|
418
402
|
Log.d(TAG, "Successfully placed call to be immediately active with TelecomManager.")
|
|
419
403
|
} catch (e: Exception) {
|
|
420
404
|
Log.e(TAG, "Failed to start call: ${e.message}", e)
|
|
@@ -429,19 +413,22 @@ object CallEngine {
|
|
|
429
413
|
|
|
430
414
|
fun answerCall(callId: String) {
|
|
431
415
|
Log.d(TAG, "answerCall: $callId - local party answering")
|
|
432
|
-
|
|
416
|
+
val connection = telecomConnections[callId] as? MyConnection
|
|
417
|
+
if (connection != null) {
|
|
418
|
+
connection.onAnswer()
|
|
419
|
+
} else {
|
|
420
|
+
Log.e(TAG, "Cannot answer call, no connection found for $callId")
|
|
421
|
+
}
|
|
433
422
|
}
|
|
434
423
|
|
|
435
424
|
fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
436
425
|
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
437
426
|
val callInfo = activeCalls[callId]
|
|
438
427
|
if (callInfo == null) {
|
|
439
|
-
Log.w(TAG, "Cannot
|
|
428
|
+
Log.w(TAG, "Cannot process answered call $callId - not found in active calls")
|
|
440
429
|
return
|
|
441
430
|
}
|
|
442
431
|
|
|
443
|
-
// The connection state change (e.g., onAnswer) is the source of truth.
|
|
444
|
-
// We just update our internal state and UI.
|
|
445
432
|
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
446
433
|
currentCallId = callId
|
|
447
434
|
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
@@ -463,9 +450,6 @@ object CallEngine {
|
|
|
463
450
|
keepScreenAwake(true)
|
|
464
451
|
updateLockScreenBypass()
|
|
465
452
|
|
|
466
|
-
// Audio is now managed by the Connection. We don't need to do anything here.
|
|
467
|
-
// The initial audio route is set by MyConnection when it becomes active.
|
|
468
|
-
|
|
469
453
|
if (isLocalAnswer) {
|
|
470
454
|
emitCallAnsweredWithMetadata(callId)
|
|
471
455
|
} else {
|
|
@@ -587,13 +571,11 @@ object CallEngine {
|
|
|
587
571
|
}
|
|
588
572
|
|
|
589
573
|
private fun setMutedInternal(callId: String, muted: Boolean) {
|
|
590
|
-
val
|
|
591
|
-
|
|
592
|
-
Log.w(TAG, "Cannot set mute state for call $callId - not found")
|
|
574
|
+
val connection = telecomConnections[callId] as? MyConnection ?: run {
|
|
575
|
+
Log.w(TAG, "Cannot set mute state for call $callId - no connection found")
|
|
593
576
|
return
|
|
594
577
|
}
|
|
595
|
-
|
|
596
|
-
(telecomConnections[callId] as? MyConnection)?.setMuted(muted)
|
|
578
|
+
connection.setMuted(muted)
|
|
597
579
|
}
|
|
598
580
|
|
|
599
581
|
fun endCall(callId: String) {
|
|
@@ -675,7 +657,7 @@ object CallEngine {
|
|
|
675
657
|
})
|
|
676
658
|
}
|
|
677
659
|
|
|
678
|
-
// ======
|
|
660
|
+
// ====== TELECOM-DRIVEN AUDIO ROUTING SYSTEM ======
|
|
679
661
|
|
|
680
662
|
fun getAudioDevices(): AudioRoutesInfo {
|
|
681
663
|
val context = requireContext()
|
|
@@ -726,35 +708,11 @@ object CallEngine {
|
|
|
726
708
|
connection.requestAudioRouteChange(telecomRoute)
|
|
727
709
|
}
|
|
728
710
|
|
|
729
|
-
/**
|
|
730
|
-
* Called by MyConnection when the audio route changes. This is the single
|
|
731
|
-
* source of truth for route updates.
|
|
732
|
-
*/
|
|
733
711
|
fun onTelecomAudioRouteChanged(callId: String, newRoute: String) {
|
|
734
712
|
Log.d(TAG, "onTelecomAudioRouteChanged for callId $callId, new route: $newRoute")
|
|
735
713
|
emitAudioRouteChanged()
|
|
736
714
|
}
|
|
737
715
|
|
|
738
|
-
/**
|
|
739
|
-
* Called by MyConnection when it becomes active to set the initial, logical
|
|
740
|
-
* audio route.
|
|
741
|
-
*/
|
|
742
|
-
fun setInitialAudioRouteForCall(callId: String, callType: String) {
|
|
743
|
-
val am = audioManager ?: return
|
|
744
|
-
val connection = telecomConnections[callId] as? MyConnection ?: return
|
|
745
|
-
|
|
746
|
-
// Determine default route based on Android standards
|
|
747
|
-
val defaultRoute = when {
|
|
748
|
-
connection.isBluetoothAvailable() -> "Bluetooth"
|
|
749
|
-
am.isWiredHeadsetOn -> "Headset"
|
|
750
|
-
callType.equals("Video", ignoreCase = true) -> "Speaker"
|
|
751
|
-
else -> "Earpiece"
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
Log.d(TAG, "Requesting initial audio route for call $callId: $defaultRoute")
|
|
755
|
-
setAudioRoute(defaultRoute)
|
|
756
|
-
}
|
|
757
|
-
|
|
758
716
|
private fun emitAudioRouteChanged() {
|
|
759
717
|
val info = getAudioDevices()
|
|
760
718
|
val deviceStrings = info.devices.map { it.value }
|
|
@@ -813,12 +771,11 @@ object CallEngine {
|
|
|
813
771
|
})
|
|
814
772
|
|
|
815
773
|
// Only remove metadata if there's NO existing active call with this ID
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
Log.d(TAG, "Removed metadata for rejected call $callId (no existing call)")
|
|
774
|
+
if (!activeCalls.containsKey(callId)) {
|
|
775
|
+
callMetadata.remove(callId)
|
|
776
|
+
Log.d(TAG, "Removed metadata for rejected call $callId (no existing call)")
|
|
820
777
|
} else {
|
|
821
|
-
|
|
778
|
+
Log.d(TAG, "Kept metadata for callId: $callId (an existing call is using it)")
|
|
822
779
|
}
|
|
823
780
|
}
|
|
824
781
|
|
|
@@ -836,7 +793,6 @@ object CallEngine {
|
|
|
836
793
|
enableVibration(true)
|
|
837
794
|
setBypassDnd(true)
|
|
838
795
|
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
839
|
-
// Sound is handled by the RingtoneManager directly for better control
|
|
840
796
|
setSound(null, null)
|
|
841
797
|
}
|
|
842
798
|
val manager = context.getSystemService(NotificationManager::class.java)
|
|
@@ -1141,7 +1097,6 @@ object CallEngine {
|
|
|
1141
1097
|
Log.d(TAG, "Performing cleanup")
|
|
1142
1098
|
stopForegroundService()
|
|
1143
1099
|
keepScreenAwake(false)
|
|
1144
|
-
// Reset audio mode via AudioManager when all calls are truly gone
|
|
1145
1100
|
audioManager?.mode = AudioManager.MODE_NORMAL
|
|
1146
1101
|
}
|
|
1147
1102
|
|
|
@@ -4,10 +4,12 @@ import android.os.Build
|
|
|
4
4
|
import android.os.OutcomeReceiver
|
|
5
5
|
import android.telecom.CallAudioState
|
|
6
6
|
import android.telecom.CallEndpoint
|
|
7
|
+
import android.telecom.CallEndpointException
|
|
7
8
|
import android.telecom.Connection
|
|
8
9
|
import android.telecom.DisconnectCause
|
|
9
10
|
import android.telecom.VideoProfile
|
|
10
11
|
import android.util.Log
|
|
12
|
+
import androidx.annotation.RequiresApi
|
|
11
13
|
import java.util.concurrent.Executors
|
|
12
14
|
import org.json.JSONObject
|
|
13
15
|
|
|
@@ -22,13 +24,11 @@ class MyConnection(
|
|
|
22
24
|
const val TAG = "MyConnection"
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
private var lastAudioState: CallAudioState? = null
|
|
26
27
|
private val telecomExecutor = Executors.newSingleThreadExecutor()
|
|
27
28
|
|
|
28
29
|
init {
|
|
29
30
|
connectionProperties = PROPERTY_SELF_MANAGED
|
|
30
31
|
connectionCapabilities = CAPABILITY_SUPPORT_HOLD or CAPABILITY_MUTE or CAPABILITY_HOLD
|
|
31
|
-
|
|
32
32
|
audioModeIsVoip = true
|
|
33
33
|
|
|
34
34
|
if (callType == "Video") {
|
|
@@ -43,6 +43,8 @@ class MyConnection(
|
|
|
43
43
|
Log.d(TAG, "MyConnection for callId $callId created and added to CallEngine.")
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// --- Lifecycle Callbacks ---
|
|
47
|
+
|
|
46
48
|
override fun onSilence() {
|
|
47
49
|
super.onSilence()
|
|
48
50
|
Log.d(TAG, "onSilence called by system for callId: $callId. Silencing ringtone.")
|
|
@@ -50,20 +52,22 @@ class MyConnection(
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
override fun onAnswer() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
super.onAnswer()
|
|
56
|
+
Log.d(TAG, "onAnswer called for callId: $callId. Setting connection to active.")
|
|
57
|
+
this.setActive()
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
override fun onReject() {
|
|
59
|
-
|
|
61
|
+
super.onReject()
|
|
62
|
+
Log.d(TAG, "onReject called for callId: $callId")
|
|
60
63
|
setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
|
|
61
64
|
destroy()
|
|
62
65
|
CallEngine.endCall(callId)
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
override fun onDisconnect() {
|
|
66
|
-
|
|
69
|
+
super.onDisconnect()
|
|
70
|
+
Log.d(TAG, "onDisconnect called for callId: $callId")
|
|
67
71
|
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
68
72
|
destroy()
|
|
69
73
|
CallEngine.endCall(callId)
|
|
@@ -71,35 +75,17 @@ class MyConnection(
|
|
|
71
75
|
|
|
72
76
|
override fun onHold() {
|
|
73
77
|
super.onHold()
|
|
74
|
-
Log.d(TAG, "
|
|
78
|
+
Log.d(TAG, "onHold called for callId: $callId")
|
|
75
79
|
CallEngine.holdCall(callId)
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
override fun onUnhold() {
|
|
79
83
|
super.onUnhold()
|
|
80
|
-
Log.d(TAG, "
|
|
84
|
+
Log.d(TAG, "onUnhold called for callId: $callId")
|
|
81
85
|
CallEngine.unholdCall(callId)
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
super.onCallAudioStateChanged(state)
|
|
86
|
-
Log.d(TAG, "onCallAudioStateChanged for callId: $callId. muted=${state.isMuted}, route=${state.routeToString()}")
|
|
87
|
-
|
|
88
|
-
// Mute state change
|
|
89
|
-
if (lastAudioState?.isMuted != state.isMuted) {
|
|
90
|
-
val eventType = if (state.isMuted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
|
|
91
|
-
CallEngine.emitEvent(eventType, JSONObject().put("callId", callId))
|
|
92
|
-
Log.d(TAG, "Call $callId mute state changed to: ${state.isMuted}")
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Route change
|
|
96
|
-
if (lastAudioState?.route != state.route) {
|
|
97
|
-
Log.d(TAG, "System audio route changed for callId: $callId. New route: ${state.routeToString()}")
|
|
98
|
-
CallEngine.onTelecomAudioRouteChanged(callId, state.routeToString())
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
lastAudioState = state
|
|
102
|
-
}
|
|
88
|
+
// --- State Change Callbacks ---
|
|
103
89
|
|
|
104
90
|
override fun onStateChanged(state: Int) {
|
|
105
91
|
super.onStateChanged(state)
|
|
@@ -107,12 +93,15 @@ class MyConnection(
|
|
|
107
93
|
|
|
108
94
|
when (state) {
|
|
109
95
|
STATE_ACTIVE -> {
|
|
110
|
-
|
|
111
|
-
// This is the
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
96
|
+
Log.d(TAG, "Connection is now active for callId: $callId.")
|
|
97
|
+
// This is the single source of truth for a call becoming active.
|
|
98
|
+
// We check the state in CallEngine to determine if it was an incoming
|
|
99
|
+
// or outgoing call that was just answered.
|
|
100
|
+
val callInfo = CallEngine.getActiveCalls().find { it.callId == callId }
|
|
101
|
+
if (callInfo != null && callInfo.state != CallState.ACTIVE) {
|
|
102
|
+
val isLocalAnswer = callInfo.state == CallState.INCOMING
|
|
103
|
+
CallEngine.coreCallAnswered(callId, isLocalAnswer = isLocalAnswer)
|
|
104
|
+
}
|
|
116
105
|
}
|
|
117
106
|
STATE_DISCONNECTED -> {
|
|
118
107
|
Log.d(TAG, "Connection is now disconnected for callId: $callId")
|
|
@@ -121,23 +110,24 @@ class MyConnection(
|
|
|
121
110
|
}
|
|
122
111
|
}
|
|
123
112
|
|
|
113
|
+
override fun onCallAudioStateChanged(state: CallAudioState) {
|
|
114
|
+
super.onCallAudioStateChanged(state)
|
|
115
|
+
Log.d(TAG, "onCallAudioStateChanged for callId: $callId. muted=${state.isMuted}, route=${routeToString(state.route)}")
|
|
116
|
+
|
|
117
|
+
// Forward route changes to our engine/UI.
|
|
118
|
+
CallEngine.onTelecomAudioRouteChanged(callId, routeToString(state.route))
|
|
119
|
+
|
|
120
|
+
// Emit mute/unmute events based on state changes from the system.
|
|
121
|
+
val eventType = if (state.isMuted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
|
|
122
|
+
CallEngine.emitEvent(eventType, JSONObject().put("callId", callId))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Audio Control Methods (Called from CallEngine) ---
|
|
126
|
+
|
|
124
127
|
fun requestAudioRouteChange(route: Int) {
|
|
125
128
|
Log.d(TAG, "Requesting audio route change to: ${routeToString(route)} for callId: $callId")
|
|
126
129
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
127
|
-
|
|
128
|
-
if (endpoint != null) {
|
|
129
|
-
Log.d(TAG, "Using requestCallEndpointChange for API 34+")
|
|
130
|
-
requestCallEndpointChange(endpoint, telecomExecutor, object : OutcomeReceiver<Void, Exception> {
|
|
131
|
-
override fun onResult(result: Void?) {
|
|
132
|
-
Log.d(TAG, "Successfully changed audio endpoint to ${endpoint.endpointName}")
|
|
133
|
-
}
|
|
134
|
-
override fun onError(error: Exception) {
|
|
135
|
-
Log.e(TAG, "Failed to change audio endpoint", error)
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
} else {
|
|
139
|
-
Log.w(TAG, "Could not find a matching CallEndpoint for route: ${routeToString(route)}")
|
|
140
|
-
}
|
|
130
|
+
requestAudioEndpointChangeInternal(route)
|
|
141
131
|
} else {
|
|
142
132
|
Log.d(TAG, "Using setAudioRoute (deprecated) for older API")
|
|
143
133
|
@Suppress("DEPRECATION")
|
|
@@ -145,17 +135,38 @@ class MyConnection(
|
|
|
145
135
|
}
|
|
146
136
|
}
|
|
147
137
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
139
|
+
private fun requestAudioEndpointChangeInternal(route: Int) {
|
|
140
|
+
val endpoint = this.callAudioState?.availableCallEndpoints?.find { it.type == route }
|
|
141
|
+
if (endpoint != null) {
|
|
142
|
+
Log.d(TAG, "Using requestCallEndpointChange for API 34+")
|
|
143
|
+
requestCallEndpointChange(endpoint, telecomExecutor, object : OutcomeReceiver<Void, CallEndpointException> {
|
|
144
|
+
override fun onResult(result: Void?) {
|
|
145
|
+
Log.d(TAG, "Successfully requested audio endpoint change to ${endpoint.name}")
|
|
146
|
+
}
|
|
147
|
+
override fun onError(error: CallEndpointException) {
|
|
148
|
+
Log.e(TAG, "Failed to change audio endpoint", error)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
151
|
} else {
|
|
152
|
-
|
|
153
|
-
this.callAudioState?.supportedRouteMask?.and(CallAudioState.ROUTE_BLUETOOTH) != 0
|
|
152
|
+
Log.w(TAG, "Could not find a matching CallEndpoint for route: ${routeToString(route)}")
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
|
|
156
|
+
fun setMuted(muted: Boolean) {
|
|
157
|
+
val currentState = this.callAudioState ?: return
|
|
158
|
+
if (currentState.isMuted == muted) return
|
|
159
|
+
|
|
160
|
+
// This informs the system of the change. The system will then call
|
|
161
|
+
// onCallAudioStateChanged back on this connection with the new state.
|
|
162
|
+
onCallAudioStateChanged(CallAudioState(muted, currentState.route, currentState.supportedRouteMask))
|
|
163
|
+
Log.d(TAG, "Informed Telecom of mute state change to: $muted")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Helper Functions ---
|
|
167
|
+
|
|
157
168
|
fun getCurrentRouteString(): String {
|
|
158
|
-
return
|
|
169
|
+
return this.callAudioState?.route.let { routeToString(it) }
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
private fun routeToString(route: Int?): String {
|
|
@@ -167,4 +178,18 @@ class MyConnection(
|
|
|
167
178
|
else -> "Unknown"
|
|
168
179
|
}
|
|
169
180
|
}
|
|
181
|
+
|
|
182
|
+
private fun stateToString(state: Int): String {
|
|
183
|
+
return when (state) {
|
|
184
|
+
STATE_INITIALIZING -> "INITIALIZING"
|
|
185
|
+
STATE_NEW -> "NEW"
|
|
186
|
+
STATE_RINGING -> "RINGING"
|
|
187
|
+
STATE_DIALING -> "DIALING"
|
|
188
|
+
STATE_ACTIVE -> "ACTIVE"
|
|
189
|
+
STATE_HOLDING -> "HOLDING"
|
|
190
|
+
STATE_DISCONNECTED -> "DISCONNECTED"
|
|
191
|
+
STATE_PULLING_CALL -> "PULLING_CALL"
|
|
192
|
+
else -> "UNKNOWN"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
170
195
|
}
|