Added activity monitor & presence status

This commit is contained in:
Sylvain Berfini 2023-08-18 14:44:33 +02:00
parent a10f416f15
commit 4018155899
7 changed files with 184 additions and 16 deletions

View file

@ -20,6 +20,7 @@
package org.linphone.core
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.Intent
import android.os.Handler
@ -32,15 +33,22 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.contacts.ContactsManager
import org.linphone.core.tools.Log
import org.linphone.ui.voip.VoipActivity
import org.linphone.utils.ActivityMonitor
import org.linphone.utils.LinphoneUtils
class CoreContext(val context: Context) : HandlerThread("Core Thread") {
companion object {
const val TAG = "[Core Context]"
}
lateinit var core: Core
val emojiCompat: EmojiCompat
val contactsManager = ContactsManager()
private val activityMonitor = ActivityMonitor()
private val mainThread = Handler(Looper.getMainLooper())
@SuppressLint("HandlerLeak")
@ -48,7 +56,7 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
private val coreListener = object : CoreListenerStub() {
override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) {
Log.i("[Context] Global state changed: $state")
Log.i("$TAG Global state changed: $state")
}
override fun onCallStateChanged(
@ -57,7 +65,7 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
state: Call.State?,
message: String
) {
Log.i("[Context] Call state changed [$state]")
Log.i("$TAG Call state changed [$state]")
if (state == Call.State.OutgoingProgress) {
showCallActivity()
} else if (state == Call.State.IncomingReceived) {
@ -70,6 +78,8 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
init {
EmojiCompat.init(context)
emojiCompat = EmojiCompat.get()
(context as Application).registerActivityLifecycleCallbacks(activityMonitor)
}
override fun run() {
@ -109,6 +119,10 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
core.stop()
contactsManager.onCoreStopped()
postOnMainThread {
(context as Application).unregisterActivityLifecycleCallbacks(activityMonitor)
}
quitSafely()
}
@ -128,6 +142,30 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
}
}
fun onForeground() {
postOnCoreThread {
// We can't rely on defaultAccount?.params?.isPublishEnabled
// as it will be modified by the SDK when changing the presence status
if (corePreferences.publishPresence) {
Log.i("$TAG App is in foreground, PUBLISHING presence as Online")
core.consolidatedPresence = ConsolidatedPresence.Online
}
}
}
fun onBackground() {
postOnCoreThread {
// We can't rely on defaultAccount?.params?.isPublishEnabled
// as it will be modified by the SDK when changing the presence status
if (corePreferences.publishPresence) {
Log.i("$TAG App is in background, un-PUBLISHING presence info")
// We don't use ConsolidatedPresence.Busy but Offline to do an unsubscribe,
// Flexisip will handle the Busy status depending on other devices
core.consolidatedPresence = ConsolidatedPresence.Offline
}
}
}
fun startCall(
address: Address,
callParams: CallParams? = null,
@ -136,14 +174,14 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
) {
// Core thread
if (!core.isNetworkReachable) {
Log.e("[Context] Network unreachable, abort outgoing call")
Log.e("$TAG Network unreachable, abort outgoing call")
return
}
val params = callParams ?: core.createCallParams(null)
if (params == null) {
val call = core.inviteAddress(address)
Log.w("[Context] Starting call $call without params")
Log.w("$TAG Starting call $call without params")
return
}
@ -163,11 +201,11 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
if (account != null) {
params.account = account
Log.i(
"[Context] Using account matching address ${localAddress.asStringUriOnly()} as From"
"$TAG Using account matching address ${localAddress.asStringUriOnly()} as From"
)
} else {
Log.e(
"[Context] Failed to find account matching address ${localAddress.asStringUriOnly()}"
"$TAG Failed to find account matching address ${localAddress.asStringUriOnly()}"
)
}
}
@ -177,16 +215,16 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
}*/
val call = core.inviteAddressWithParams(address, params)
Log.i("[Context] Starting call $call")
Log.i("$TAG Starting call $call")
}
fun switchCamera() {
val currentDevice = core.videoDevice
Log.i("[Context] Current camera device is $currentDevice")
Log.i("$TAG Current camera device is $currentDevice")
for (camera in core.videoDevicesList) {
if (camera != currentDevice && camera != "StaticImage: Static picture") {
Log.i("[Context] New camera device will be $camera")
Log.i("$TAG New camera device will be $camera")
core.videoDevice = camera
break
}
@ -194,7 +232,7 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
val call = core.currentCall
if (call == null) {
Log.w("[Context] Switching camera while not in call")
Log.w("$TAG Switching camera while not in call")
return
}
call.update(null)
@ -205,7 +243,7 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
}
private fun showCallActivity() {
Log.i("[Context] Starting VoIP activity")
Log.i("$TAG Starting VoIP activity")
val intent = Intent(context, VoipActivity::class.java)
// This flag is required to start an Activity from a Service context
intent.addFlags(

View file

@ -51,6 +51,12 @@ class CorePreferences constructor(private val context: Context) {
editor.apply()
}
var publishPresence: Boolean
get() = config.getBool("app", "publish_presence", true)
set(value) {
config.setBool("app", "publish_presence", value)
}
val defaultDomain: String
get() = config.getString("app", "default_domain", "sip.linphone.org")!!

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2010-2021 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.utils
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.service.AndroidDispatcher
class ActivityMonitor : ActivityLifecycleCallbacks {
private val activities = ArrayList<Activity>()
private var mActive = false
private var mRunningActivities = 0
private var mLastChecker: InactivityChecker? = null
@Synchronized
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (!activities.contains(activity)) activities.add(activity)
}
override fun onActivityStarted(activity: Activity) {
}
@Synchronized
override fun onActivityResumed(activity: Activity) {
if (!activities.contains(activity)) {
activities.add(activity)
}
mRunningActivities++
checkActivity()
}
@Synchronized
override fun onActivityPaused(activity: Activity) {
if (!activities.contains(activity)) {
activities.add(activity)
} else {
mRunningActivities--
checkActivity()
}
}
override fun onActivityStopped(activity: Activity) {
}
@Synchronized
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity)
}
private fun startInactivityChecker() {
if (mLastChecker != null) mLastChecker!!.cancel()
AndroidDispatcher.dispatchOnUIThreadAfter(
InactivityChecker().also { mLastChecker = it },
2000
)
}
private fun checkActivity() {
if (mRunningActivities == 0) {
if (mActive) startInactivityChecker()
} else if (mRunningActivities > 0) {
if (!mActive) {
mActive = true
onForegroundMode()
}
if (mLastChecker != null) {
mLastChecker!!.cancel()
mLastChecker = null
}
}
}
private fun onBackgroundMode() {
coreContext.onBackground()
}
private fun onForegroundMode() {
coreContext.onForeground()
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
internal inner class InactivityChecker : Runnable {
private var isCanceled = false
fun cancel() {
isCanceled = true
}
override fun run() {
if (!isCanceled) {
if (mRunningActivities == 0 && mActive) {
mActive = false
onBackgroundMode()
}
}
}
}
}

View file

@ -73,7 +73,6 @@
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="10dp"
android:background="@color/gray_7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -241,6 +240,7 @@
android:layout_marginTop="45dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
android:paddingBottom="16dp"
android:background="@drawable/shape_round_white_background"
android:orientation="vertical"

View file

@ -48,7 +48,6 @@
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="10dp"
android:background="@color/gray_7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -73,7 +73,6 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp"
android:background="@color/gray_7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -463,7 +462,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/actions"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toBottomOf="@id/action_delete" />
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.editContact()}"
@ -552,6 +551,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<View
android:id="@+id/anchor"
android:layout_width="wrap_content"
android:layout_height="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/action_delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -67,7 +67,6 @@
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="10dp"
android:background="@color/gray_7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -257,6 +256,7 @@
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="24dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@={viewModel.jobTitle, default=`Android dev`}"
@ -265,6 +265,7 @@
android:maxLines="1"
android:background="@drawable/shape_edit_text_background"
android:inputType="text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/job_title_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>