diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index e604428f7..bab8564ec 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -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( diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 999c07bef..6ef49b00a 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -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")!! diff --git a/app/src/main/java/org/linphone/utils/ActivityMonitor.kt b/app/src/main/java/org/linphone/utils/ActivityMonitor.kt new file mode 100644 index 000000000..fca1e7329 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/ActivityMonitor.kt @@ -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 . + */ +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() + 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() + } + } + } + } +} diff --git a/app/src/main/res/layout/call_fragment.xml b/app/src/main/res/layout/call_fragment.xml index 937a5db9e..dc68843eb 100644 --- a/app/src/main/res/layout/call_fragment.xml +++ b/app/src/main/res/layout/call_fragment.xml @@ -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" diff --git a/app/src/main/res/layout/call_start_fragment.xml b/app/src/main/res/layout/call_start_fragment.xml index 0af41ff30..25537fb3a 100644 --- a/app/src/main/res/layout/call_start_fragment.xml +++ b/app/src/main/res/layout/call_start_fragment.xml @@ -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" diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index 551ae1d50..93003216e 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -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" /> + + diff --git a/app/src/main/res/layout/contact_new_or_edit_fragment.xml b/app/src/main/res/layout/contact_new_or_edit_fragment.xml index 3ba1c8d8e..5113ec1e4 100644 --- a/app/src/main/res/layout/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout/contact_new_or_edit_fragment.xml @@ -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"/>