Initial 6.0.0 commit

This commit is contained in:
Sylvain Berfini 2023-06-22 15:38:55 +02:00
parent 418f9ba4c9
commit c0169e7bcb
83 changed files with 3079 additions and 0 deletions

15
.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View file

@ -0,0 +1 @@
Linphone

6
.idea/compiler.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

19
.idea/gradle.xml generated Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

6
.idea/kotlinc.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0-RC" />
</component>
</project>

9
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

94
app/build.gradle Normal file
View file

@ -0,0 +1,94 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'org.jlleitschuh.gradle.ktlint' version '11.3.1'
id 'org.jetbrains.kotlin.android'
}
static def getPackageName() {
return "org.linphone"
}
android {
namespace 'org.linphone'
compileSdk 34
defaultConfig {
applicationId getPackageName()
minSdk 27
targetSdk 34
versionCode 60000
versionName "6.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
resValue "string", "file_provider", getPackageName() + ".fileprovider"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "string", "file_provider", getPackageName() + ".fileprovider"
}
}
compileOptions {
sourceCompatibility = 17
targetCompatibility = 17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
dataBinding true
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.7.0-alpha02'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1-rc01'
implementation 'androidx.core:core-ktx:+'
implementation 'androidx.core:core-ktx:+'
def nav_version = "2.6.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
def emoji_version = "1.4.0-beta05"
implementation "androidx.emoji2:emoji2:$emoji_version"
implementation "androidx.emoji2:emoji2-emojipicker:$emoji_version"
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
implementation 'com.google.android.material:material:1.9.0'
// https://github.com/google/flexbox-layout/blob/main/LICENSE Apache v2.0
implementation 'com.google.android.flexbox:flexbox:3.0.0'
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
def coil_version = "2.4.0"
implementation("io.coil-kt:coil:$coil_version")
implementation("io.coil-kt:coil-gif:$coil_version")
implementation("io.coil-kt:coil-svg:$coil_version")
implementation("io.coil-kt:coil-video:$coil_version")
implementation platform('com.google.firebase:firebase-bom:30.3.2')
implementation 'com.google.firebase:firebase-messaging'
implementation 'org.linphone:linphone-sdk-android:5.3+'
}
ktlint {
android = true
ignoreFailures = true
}
project.tasks['preBuild'].dependsOn 'ktlintFormat'

57
app/google-services.json Normal file
View file

@ -0,0 +1,57 @@
{
"project_info": {
"project_number": "929724111839",
"firebase_url": "https://linphone-android-8a563.firebaseio.com",
"project_id": "linphone-android-8a563",
"storage_bucket": "linphone-android-8a563.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:929724111839:android:4662ea9a056188c4",
"android_client_info": {
"package_name": "org.linphone"
}
},
"oauth_client": [
{
"client_id": "929724111839-co5kffto4j7dets7oolvfv0056cvpfbl.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "org.linphone",
"certificate_hash": "85463a95603f7b6331899b74b85d53d043dcd500"
}
},
{
"client_id": "929724111839-v5so1tcd65iil7dd7sde8jgii44h8luf.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCKrwWhkbA7Iy3wpEI8_ZvKOMp5jf6vV6A"
}
]
},
{
"client_info": {
"mobilesdk_app_id": "1:929724111839:android:3cf90ee1d2f8fcb6",
"android_client_info": {
"package_name": "org.linphone.debug"
}
},
"oauth_client": [
{
"client_id": "929724111839-v5so1tcd65iil7dd7sde8jgii44h8luf.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCKrwWhkbA7Iy3wpEI8_ZvKOMp5jf6vV6A"
}
]
}
],
"configuration_version": "1"
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".LinphoneApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.Linphone"
tools:targetApi="34">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Receivers -->
<receiver android:name=".core.CorePushReceiver"
android:exported="false">
<intent-filter>
<action android:name="org.linphone.core.action.PUSH_RECEIVED"/>
</intent-filter>
</receiver>
<!-- Providers -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/file_provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
<section name="proxy_default_values">
<entry name="avpf" overwrite="true">0</entry>
<entry name="dial_escape_plus" overwrite="true">0</entry>
<entry name="publish" overwrite="true">0</entry>
<entry name="publish_expires" overwrite="true">-1</entry>
<entry name="quality_reporting_collector" overwrite="true"></entry>
<entry name="quality_reporting_enabled" overwrite="true">0</entry>
<entry name="quality_reporting_interval" overwrite="true">0</entry>
<entry name="reg_expires" overwrite="true">3600</entry>
<entry name="reg_identity" overwrite="true"></entry>
<entry name="reg_proxy" overwrite="true"></entry>
<entry name="reg_route" overwrite="true"></entry>
<entry name="reg_sendregister" overwrite="true">1</entry>
<entry name="nat_policy_ref" overwrite="true"></entry>
<entry name="realm" overwrite="true"></entry>
<entry name="conference_factory_uri" overwrite="true"></entry>
<entry name="audio_video_conference_factory_uri" overwrite="true"></entry>
<entry name="push_notification_allowed" overwrite="true">0</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
<entry name="rtp_bundle" overwrite="true">0</entry>
<entry name="lime_server_url" overwrite="true"></entry>
</section>
<section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true"></entry>
<entry name="protocols" overwrite="true"></entry>
</section>
<section name="net">
<entry name="friendlist_subscription_enabled" overwrite="true">0</entry>
</section>
<section name="assistant">
<entry name="domain" overwrite="true"></entry>
<entry name="algorithm" overwrite="true">MD5</entry>
<entry name="password_max_length" overwrite="true">-1</entry>
<entry name="password_min_length" overwrite="true">0</entry>
<entry name="username_length" overwrite="true">-1</entry>
<entry name="username_max_length" overwrite="true">128</entry>
<entry name="username_min_length" overwrite="true">1</entry>
<entry name="username_regex" overwrite="true">^[a-zA-Z0-9+_.\-]*$</entry>
</section>
</config>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
<section name="proxy_default_values">
<entry name="avpf" overwrite="true">1</entry>
<entry name="dial_escape_plus" overwrite="true">0</entry>
<entry name="publish" overwrite="true">1</entry>
<entry name="publish_expires" overwrite="true">120</entry>
<entry name="quality_reporting_collector" overwrite="true">sip:voip-metrics@sip.linphone.org;transport=tls</entry>
<entry name="quality_reporting_enabled" overwrite="true">1</entry>
<entry name="quality_reporting_interval" overwrite="true">180</entry>
<entry name="reg_expires" overwrite="true">31536000</entry>
<entry name="reg_identity" overwrite="true">sip:?@sip.linphone.org</entry>
<entry name="reg_proxy" overwrite="true">&lt;sip:sip.linphone.org;transport=tls&gt;</entry>
<entry name="reg_route" overwrite="true">&lt;sip:sip.linphone.org;transport=tls&gt;</entry>
<entry name="reg_sendregister" overwrite="true">1</entry>
<entry name="nat_policy_ref" overwrite="true">nat_policy_default_values</entry>
<entry name="realm" overwrite="true">sip.linphone.org</entry>
<entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
<entry name="audio_video_conference_factory_uri" overwrite="true">sip:videoconference-factory@sip.linphone.org</entry>
<entry name="push_notification_allowed" overwrite="true">1</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
<entry name="rtp_bundle" overwrite="true">1</entry>
<entry name="lime_server_url" overwrite="true">https://lime.linphone.org/lime-server/lime-server.php</entry>
</section>
<section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
<entry name="protocols" overwrite="true">stun,ice</entry>
</section>
<section name="net">
<entry name="friendlist_subscription_enabled" overwrite="true">1</entry>
</section>
<section name="assistant">
<entry name="domain" overwrite="true">sip.linphone.org</entry>
<entry name="algorithm" overwrite="true">SHA-256</entry>
<entry name="password_max_length" overwrite="true">-1</entry>
<entry name="password_min_length" overwrite="true">1</entry>
<entry name="username_length" overwrite="true">-1</entry>
<entry name="username_max_length" overwrite="true">64</entry>
<entry name="username_min_length" overwrite="true">1</entry>
<entry name="username_regex" overwrite="true">^[a-z0-9+_.\-]*$</entry>
</section>
</config>

View file

@ -0,0 +1,44 @@
## Start of default rc
[sip]
contact="Linphone Android" <sip:linphone.android@unknown-host>
use_info=0
use_ipv6=1
keepalive_period=30000
sip_port=-1
sip_tcp_port=-1
sip_tls_port=-1
media_encryption=none
update_presence_model_timestamp_before_publish_expires_refresh=1
[net]
#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"
download_bw=0
upload_bw=0
[video]
size=vga
[app]
tunnel=disabled
auto_start=1
record_aware=1
[tunnel]
host=
port=443
[misc]
log_collection_upload_server_url=https://www.linphone.org:444/lft.php
file_transfer_server_url=https://www.linphone.org:444/lft.php
version_check_url_root=https://www.linphone.org/releases
max_calls=10
history_max_size=100
conference_layout=1
[in-app-purchase]
server_url=https://subscribe.linphone.org:444/inapp.php
purchasable_items_ids=test_account_subscription
## End of default rc

View file

@ -0,0 +1,98 @@
## Start of factory rc
# This file shall not contain path referencing package name, in order to be portable when app is renamed.
# Paths to resources must be set from LinphoneManager, after creating LinphoneCore.
[net]
mtu=1300
force_ice_disablement=0
[rtp]
accept_any_encryption=1
[sip]
guess_hostname=1
register_only_when_network_is_up=1
auto_net_state_mon=1
auto_answer_replacing_calls=1
ping_with_options=0
use_cpim=1
zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_KYB512
chat_messages_aggregation_delay=1000
chat_messages_aggregation=1
[sound]
#remove this property for any application that is not Linphone public version itself
ec_calibrator_cool_tones=1
[video]
displaytype=MSAndroidTextureDisplay
auto_resize_preview_to_keep_ratio=1
max_conference_size=vga
[misc]
enable_basic_to_client_group_chat_room_migration=0
enable_simple_group_chat_message_state=0
aggregate_imdn=1
notify_each_friend_individually_when_presence_received=0
[app]
activation_code_length=4
prefer_basic_chat_room=1
record_aware=1
[account_creator]
backend=1
# 1 means FlexiAPI, 0 is XMLRPC
url=https://subscribe.linphone.org/api/
# replace above URL by https://staging-subscribe.linphone.org/api/ for testing
[lime]
lime_update_threshold=86400
[nat_policy_0]
ref=HQ0DK7mVDOPAY3i
stun_server=stun.linphone.org
protocols=stun,ice
[proxy_0]
reg_proxy=<sip:sip.linphone.org;transport=tls>
reg_route=sip:sip.linphone.org;transport=tls
reg_identity="Sylvain Berfini" <sip:sylvain@sip.linphone.org>
realm=sip.linphone.org
contact_parameters=message-expires=604800
quality_reporting_collector=sip:voip-metrics@sip.linphone.org;transport=tls
push_parameters=pn-silent=1;pn-timeout=0;
quality_reporting_enabled=1
quality_reporting_interval=180
reg_expires=600
reg_sendregister=1
publish=1
avpf=1
avpf_rr_interval=1
dial_escape_plus=0
dial_prefix=33
use_dial_prefix_for_calls_and_chats=1
privacy=32768
push_notification_allowed=1
remote_push_notification_allowed=0
cpim_in_basic_chat_rooms_enabled=1
idkey=proxy_config_WSik0NIEZbTW4fM
publish_expires=120
nat_policy_ref=-ulaFqPYu2HOZ90
conference_factory_uri=sip:conference-factory@sip.linphone.org
audio_video_conference_factory_uri=sip:videoconference-factory@sip.linphone.org
rtp_bundle=1
rtp_bundle_assumption=0
lime_server_url=https://lime.linphone.org/lime-server/lime-server.php
[auth_info_0]
username=sylvain
ha1=4028ae98f54e8ffd1ab5c90985a6e89752aa0228b3e14b7ffcc40e42b4787e56
realm=sip.linphone.org
domain=sip.linphone.org
algorithm=SHA-256
available_algorithms=SHA-256
## End of factory rc

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2010-2023 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
import android.annotation.SuppressLint
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.decode.VideoFrameDecoder
import coil.disk.DiskCache
import coil.memory.MemoryCache
import com.google.android.material.color.DynamicColors
import org.linphone.core.CoreContext
import org.linphone.core.CorePreferences
import org.linphone.core.Factory
import org.linphone.core.LogCollectionState
import org.linphone.core.LogLevel
import org.linphone.mediastream.Version
class LinphoneApplication : Application(), ImageLoaderFactory {
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var corePreferences: CorePreferences
@SuppressLint("StaticFieldLeak")
lateinit var coreContext: CoreContext
}
override fun onCreate() {
super.onCreate()
val context = applicationContext
Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
Factory.instance().enableLogCollection(LogCollectionState.Enabled)
// For VFS
Factory.instance().setCacheDir(context.cacheDir.absolutePath)
corePreferences = CorePreferences(context)
corePreferences.copyAssetsFromPackage()
val config = Factory.instance().createConfigWithFactory(
corePreferences.configPath,
corePreferences.factoryConfigPath
)
corePreferences.config = config
val appName = context.getString(R.string.app_name)
Factory.instance().setLoggerDomain(appName)
Factory.instance().enableLogcatLogs(true)
Factory.instance().loggingService.setLogLevel(LogLevel.Message)
coreContext = CoreContext(context)
coreContext.start()
DynamicColors.applyToActivitiesIfAvailable(this)
}
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.components {
add(VideoFrameDecoder.Factory())
add(SvgDecoder.Factory())
if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.memoryCache {
MemoryCache.Builder(this)
.maxSizePercent(0.25)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(cacheDir.resolve("image_cache"))
.maxSizePercent(0.02)
.build()
}
.build()
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2010-2023 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.core
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import java.util.*
import org.linphone.BuildConfig
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.tools.Log
class CoreContext(val context: Context) : HandlerThread("Core Thread") {
lateinit var core: Core
@SuppressLint("HandlerLeak")
private lateinit var coreThread: Handler
private val coreListener = object : CoreListenerStub() {
override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) {
Log.i("[Context] Global state changed: $state")
}
}
override fun run() {
Looper.prepare()
val looper = Looper.myLooper() ?: return
coreThread = Handler(looper)
core = Factory.instance().createCoreWithConfig(corePreferences.config, context)
core.isAutoIterateEnabled = false
core.addListener(coreListener)
val timer = Timer("Linphone core.iterate() scheduler")
timer.schedule(
object : TimerTask() {
override fun run() {
coreThread.post {
core.iterate()
}
}
},
0,
50
)
computeUserAgent()
core.start()
Looper.loop()
}
override fun destroy() {
core.stop()
quitSafely()
}
fun isReady(): Boolean {
return ::core.isInitialized
}
fun postOnCoreThread(lambda: (core: Core) -> Unit) {
coreThread.post {
lambda.invoke(core)
}
}
private fun computeUserAgent() {
// TODO FIXME
val deviceName: String = "Linphone6"
val appName: String = "Linphone Android"
val androidVersion = BuildConfig.VERSION_NAME
val userAgent = "$appName/$androidVersion ($deviceName) LinphoneSDK"
val sdkVersion = context.getString(org.linphone.core.R.string.linphone_sdk_version)
val sdkBranch = context.getString(org.linphone.core.R.string.linphone_sdk_branch)
val sdkUserAgent = "$sdkVersion ($sdkBranch)"
core.setUserAgent(userAgent, sdkUserAgent)
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2010-2023 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.core
import android.content.Context
import android.content.SharedPreferences
import java.io.File
import java.io.FileOutputStream
import org.linphone.LinphoneApplication.Companion.coreContext
class CorePreferences constructor(private val context: Context) {
private var _config: Config? = null
var config: Config
get() = _config ?: coreContext.core.config
set(value) {
_config = value
}
fun chatRoomMuted(id: String): Boolean {
val sharedPreferences: SharedPreferences = coreContext.context.getSharedPreferences(
"notifications",
Context.MODE_PRIVATE
)
return sharedPreferences.getBoolean(id, false)
}
fun muteChatRoom(id: String, mute: Boolean) {
val sharedPreferences: SharedPreferences = coreContext.context.getSharedPreferences(
"notifications",
Context.MODE_PRIVATE
)
val editor = sharedPreferences.edit()
editor.putBoolean(id, mute)
editor.apply()
}
val configPath: String
get() = context.filesDir.absolutePath + "/.linphonerc"
val factoryConfigPath: String
get() = context.filesDir.absolutePath + "/linphonerc"
fun copyAssetsFromPackage() {
copy("linphonerc_default", configPath)
copy("linphonerc_factory", factoryConfigPath, true)
}
private fun copy(from: String, to: String, overrideIfExists: Boolean = false) {
val outFile = File(to)
if (outFile.exists()) {
if (!overrideIfExists) {
android.util.Log.i(
context.getString(org.linphone.R.string.app_name),
"[Preferences] File $to already exists"
)
return
}
}
android.util.Log.i(
context.getString(org.linphone.R.string.app_name),
"[Preferences] Overriding $to by $from asset"
)
val outStream = FileOutputStream(outFile)
val inFile = context.assets.open(from)
val buffer = ByteArray(1024)
var length: Int = inFile.read(buffer)
while (length > 0) {
outStream.write(buffer, 0, length)
length = inFile.read(buffer)
}
inFile.close()
outStream.flush()
outStream.close()
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2023 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.core
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.linphone.core.tools.Log
class CorePushReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.i("[Push Notification] Push notification has been received in broadcast receiver")
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2010-2023 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.ui
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationBarView
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainViewModel
private val onNavDestinationChangedListener =
NavController.OnDestinationChangedListener { _, destination, _ ->
binding.mainNavView?.visibility = View.VISIBLE
}
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, true)
super.onCreate(savedInstanceState)
while (!coreContext.isReady()) {
Thread.sleep(20)
}
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
binding.viewModel = viewModel
viewModel.unreadMessagesCount.observe(this) { count ->
if (count > 0) {
getNavBar()?.getOrCreateBadge(R.id.conversationsFragment)?.apply {
isVisible = true
number = count
}
} else {
getNavBar()?.removeBadge(R.id.conversationsFragment)
}
}
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
binding.mainNavHostFragment.findNavController()
.addOnDestinationChangedListener(onNavDestinationChangedListener)
getNavBar()?.setupWithNavController(binding.mainNavHostFragment.findNavController())
}
private fun getNavBar(): NavigationBarView? {
return binding.mainNavView ?: binding.mainNavRail
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2010-2023 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.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.ChatMessage
import org.linphone.core.ChatRoom
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
class MainViewModel : ViewModel() {
val unreadMessagesCount = MutableLiveData<Int>()
private val coreListener = object : CoreListenerStub() {
override fun onMessagesReceived(
core: Core,
chatRoom: ChatRoom,
messages: Array<out ChatMessage>
) {
unreadMessagesCount.postValue(core.unreadChatMessageCount)
}
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
unreadMessagesCount.postValue(core.unreadChatMessageCount)
}
}
init {
coreContext.postOnCoreThread { core ->
unreadMessagesCount.postValue(core.unreadChatMessageCount)
core.addListener(coreListener)
}
}
override fun onCleared() {
super.onCleared()
coreContext.postOnCoreThread { core ->
core.removeListener(coreListener)
}
}
}

View file

@ -0,0 +1,212 @@
/*
* Copyright (c) 2010-2023 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.ui.conversations
import android.text.SpannableStringBuilder
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class ChatRoomData(val chatRoom: ChatRoom) {
val id = LinphoneUtils.getChatRoomId(chatRoom)
val contactName = MutableLiveData<String>()
val subject = MutableLiveData<String>()
val lastMessage = MutableLiveData<SpannableStringBuilder>()
val unreadChatCount = MutableLiveData<Int>()
val isComposing = MutableLiveData<Boolean>()
val isSecure = MutableLiveData<Boolean>()
val isSecureVerified = MutableLiveData<Boolean>()
val isEphemeral = MutableLiveData<Boolean>()
val isMuted = MutableLiveData<Boolean>()
val lastUpdate = MutableLiveData<String>()
val showLastMessageImdnIcon = MutableLiveData<Boolean>()
val lastMessageImdnIcon = MutableLiveData<Int>()
var chatRoomDataListener: ChatRoomDataListener? = null
val isOneToOne: Boolean by lazy {
chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())
}
private val chatRoomListener = object : ChatRoomListenerStub() {
override fun onIsComposingReceived(
chatRoom: ChatRoom,
remoteAddress: Address,
composing: Boolean
) {
isComposing.postValue(composing)
}
override fun onMessagesReceived(chatRoom: ChatRoom, chatMessages: Array<out ChatMessage>) {
unreadChatCount.postValue(chatRoom.unreadMessagesCount)
computeLastMessage()
}
override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) {
computeLastMessage()
}
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
computeLastMessage()
}
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
subject.postValue(
chatRoom.subject ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress)
)
}
}
init {
if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) {
val remoteAddress = chatRoom.peerAddress
val friend = chatRoom.core.findFriend(remoteAddress)
contactName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress))
} else {
if (chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
val first = chatRoom.participants.firstOrNull()
if (first != null) {
val remoteAddress = first.address
val friend = chatRoom.core.findFriend(remoteAddress)
contactName.postValue(
friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)
)
} else {
Log.e("[Chat Room Data] No participant in the chat room!")
}
}
}
subject.postValue(chatRoom.subject ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress))
lastMessageImdnIcon.postValue(R.drawable.imdn_sent)
showLastMessageImdnIcon.postValue(false)
computeLastMessage()
unreadChatCount.postValue(chatRoom.unreadMessagesCount)
isComposing.postValue(chatRoom.isRemoteComposing)
isSecure.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Encrypted)
isSecureVerified.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Safe)
isEphemeral.postValue(chatRoom.isEphemeralEnabled)
isMuted.postValue(areNotificationsMuted())
coreContext.postOnCoreThread { core ->
chatRoom.addListener(chatRoomListener)
}
}
fun onCleared() {
coreContext.postOnCoreThread { core ->
chatRoom.removeListener(chatRoomListener)
}
}
fun onClicked() {
chatRoomDataListener?.onClicked()
}
private fun computeLastMessageImdnIcon(message: ChatMessage) {
val state = message.state
showLastMessageImdnIcon.postValue(
if (message.isOutgoing) {
when (state) {
ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed,
ChatMessage.State.NotDelivered, ChatMessage.State.FileTransferError -> true
else -> false
}
} else {
false
}
)
lastMessageImdnIcon.postValue(
when (state) {
ChatMessage.State.DeliveredToUser -> R.drawable.imdn_delivered
ChatMessage.State.Displayed -> R.drawable.imdn_read
ChatMessage.State.InProgress -> R.drawable.imdn_sent
// TODO FIXME
else -> R.drawable.imdn_sent
}
)
}
private fun computeLastMessage() {
val lastUpdateTime = chatRoom.lastUpdateTime
lastUpdate.postValue(TimestampUtils.toString(lastUpdateTime, true))
val builder = SpannableStringBuilder()
val message = chatRoom.lastMessageInHistory
if (message != null) {
val senderAddress = message.fromAddress.clone()
senderAddress.clean()
if (message.isOutgoing && message.state != ChatMessage.State.Displayed) {
message.addListener(object : ChatMessageListenerStub() {
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
computeLastMessageImdnIcon(message)
}
})
}
computeLastMessageImdnIcon(message)
if (!isOneToOne) {
val sender = chatRoom.core.findFriend(senderAddress)
builder.append(sender?.name ?: LinphoneUtils.getDisplayName(senderAddress))
builder.append(": ")
}
for (content in message.contents) {
if (content.isFile || content.isFileTransfer) {
builder.append(content.name + " ")
} else if (content.isText) {
builder.append(content.utf8Text + " ")
}
}
builder.trim()
}
lastMessage.postValue(builder)
}
private fun areNotificationsMuted(): Boolean {
return corePreferences.chatRoomMuted(id)
}
}
abstract class ChatRoomDataListener {
abstract fun onClicked()
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2010-2023 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.ui.conversations
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.ChatRoom
import org.linphone.databinding.ChatRoomMenuBinding
import org.linphone.utils.LinphoneUtils
class ConversationMenuDialogFragment(
private val chatRoom: ChatRoom,
private val mutedCallback: ((Boolean) -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "ConversationMenuDialogFragment"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = ChatRoomMenuBinding.inflate(layoutInflater)
val id = LinphoneUtils.getChatRoomId(chatRoom)
view.isMuted = corePreferences.chatRoomMuted(id)
view.isRead = chatRoom.unreadMessagesCount == 0 // FIXME: danger?
view.setMarkAsReadClickListener {
coreContext.postOnCoreThread { core ->
chatRoom.markAsRead()
}
dismiss()
}
view.setCallClickListener {
// TODO
dismiss()
}
view.setMuteClickListener {
coreContext.postOnCoreThread { core ->
corePreferences.muteChatRoom(id, true)
mutedCallback?.invoke(true)
}
dismiss()
}
view.setUnMuteClickListener {
coreContext.postOnCoreThread { core ->
corePreferences.muteChatRoom(id, false)
mutedCallback?.invoke(false)
}
dismiss()
}
view.setDeleteClickListener {
coreContext.postOnCoreThread { core ->
core.deleteChatRoom(chatRoom)
}
dismiss()
}
return view.root
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2010-2023 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.ui.conversations
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.R
import org.linphone.databinding.ConversationsFragmentBinding
class ConversationsFragment : Fragment() {
private lateinit var binding: ConversationsFragmentBinding
private val listViewModel: ConversationsListViewModel by navGraphViewModels(
R.id.conversationsFragment
)
private lateinit var adapter: ConversationsListAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ConversationsFragmentBinding.inflate(layoutInflater)
val window = requireActivity().window
window.statusBarColor = ContextCompat.getColor(
requireContext(),
R.color.gray_1
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
adapter = ConversationsListAdapter(viewLifecycleOwner)
binding.conversationsList.setHasFixedSize(true)
binding.conversationsList.adapter = adapter
adapter.chatRoomClickedEvent.observe(viewLifecycleOwner) {
it.consume { data ->
}
}
adapter.chatRoomMenuClickedEvent.observe(viewLifecycleOwner) {
it.consume { data ->
val modalBottomSheet = ConversationMenuDialogFragment(data.chatRoom) { muted ->
data.isMuted.postValue(muted)
}
modalBottomSheet.show(parentFragmentManager, ConversationMenuDialogFragment.TAG)
}
}
val layoutManager = LinearLayoutManager(requireContext())
binding.conversationsList.layoutManager = layoutManager
listViewModel.chatRoomsList.observe(
viewLifecycleOwner
) {
adapter.submitList(it)
}
listViewModel.notifyItemChangedEvent.observe(viewLifecycleOwner) {
it.consume { index ->
adapter.notifyItemChanged(index)
}
}
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2010-2023 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.ui.conversations
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.ChatRoomListCellBinding
import org.linphone.utils.Event
class ConversationsListAdapter(
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<ChatRoomData, RecyclerView.ViewHolder>(ConversationDiffCallback()) {
val chatRoomClickedEvent = MutableLiveData<Event<ChatRoomData>>()
val chatRoomMenuClickedEvent = MutableLiveData<Event<ChatRoomData>>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: ChatRoomListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.chat_room_list_cell,
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(getItem(position))
}
inner class ViewHolder(
val binding: ChatRoomListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(chatRoomData: ChatRoomData) {
with(binding) {
data = chatRoomData
lifecycleOwner = viewLifecycleOwner
executePendingBindings()
chatRoomData.chatRoomDataListener = object : ChatRoomDataListener() {
override fun onClicked() {
chatRoomClickedEvent.value = Event(chatRoomData)
}
}
}
}
}
}
private class ConversationDiffCallback : DiffUtil.ItemCallback<ChatRoomData>() {
override fun areItemsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean {
return oldItem.id.compareTo(newItem.id) == 0
}
override fun areContentsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean {
return false
}
}

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2010-2023 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.ui.conversations
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.ArrayList
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.ChatMessage
import org.linphone.core.ChatRoom
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.utils.Event
class ConversationsListViewModel : ViewModel() {
val chatRoomsList = MutableLiveData<ArrayList<ChatRoomData>>()
val notifyItemChangedEvent = MutableLiveData<Event<Int>>()
private val coreListener = object : CoreListenerStub() {
override fun onChatRoomStateChanged(
core: Core,
chatRoom: ChatRoom,
state: ChatRoom.State?
) {
if (state == ChatRoom.State.Created || state == ChatRoom.State.Instantiated || state == ChatRoom.State.Deleted) {
updateChatRoomsList()
}
}
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
updateChatRoomsList()
}
override fun onMessagesReceived(
core: Core,
room: ChatRoom,
messages: Array<out ChatMessage>
) {
reorderChatRoomsList()
}
override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
reorderChatRoomsList()
}
}
init {
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
}
updateChatRoomsList()
}
override fun onCleared() {
coreContext.postOnCoreThread { core ->
core.removeListener(coreListener)
}
super.onCleared()
}
private fun updateChatRoomsList() {
coreContext.postOnCoreThread { core ->
chatRoomsList.value.orEmpty().forEach(ChatRoomData::onCleared)
val list = arrayListOf<ChatRoomData>()
val chatRooms = core.chatRooms
for (chatRoom in chatRooms) {
list.add(ChatRoomData(chatRoom))
}
chatRoomsList.postValue(list)
}
}
private fun reorderChatRoomsList() {
coreContext.postOnCoreThread { core ->
val list = arrayListOf<ChatRoomData>()
list.addAll(chatRoomsList.value.orEmpty())
list.sortByDescending { data -> data.chatRoom.lastUpdateTime }
chatRoomsList.postValue(list)
}
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2010-2023 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.widget.ImageView
import android.widget.TextView
import androidx.databinding.BindingAdapter
/**
* This file contains all the data binding necessary for the app
*/
@BindingAdapter("android:src")
fun ImageView.setSourceImageResource(resource: Int) {
this.setImageResource(resource)
}
@BindingAdapter("android:textStyle")
fun TextView.setTypeface(typeface: Int) {
this.setTypeface(null, typeface)
}

View file

@ -0,0 +1,41 @@
/*
* 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 java.util.concurrent.atomic.AtomicBoolean
/**
* This class allows to limit the number of notification for an event.
* The first one to consume the event will stop the dispatch.
*/
open class Event<out T>(private val content: T) {
private val handled = AtomicBoolean(false)
fun consumed(): Boolean {
return handled.get()
}
fun consume(handleContent: (T) -> Unit) {
if (!handled.get()) {
handled.set(true)
handleContent(content)
}
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2010-2023 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 org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Address
import org.linphone.core.ChatRoom
class LinphoneUtils {
companion object {
private fun getChatRoomId(localAddress: Address, remoteAddress: Address): String {
val localSipUri = localAddress.clone()
localSipUri.clean()
val remoteSipUri = remoteAddress.clone()
remoteSipUri.clean()
return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}"
}
fun getChatRoomId(chatRoom: ChatRoom): String {
return getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
}
fun getDisplayName(address: Address?): String {
if (address == null) return "[null]"
if (address.displayName == null) {
val account = coreContext.core.accountList.find { account ->
account.params.identityAddress?.asStringUriOnly() == address.asStringUriOnly()
}
val localDisplayName = account?.params?.identityAddress?.displayName
// Do not return an empty local display name
if (localDisplayName != null && localDisplayName.isNotEmpty()) {
return localDisplayName
}
}
// Do not return an empty display name
return address.displayName ?: address.username ?: address.asString()
}
}
}

View file

@ -0,0 +1,174 @@
/*
* Copyright (c) 2010-2020 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 java.text.DateFormat
import java.text.Format
import java.text.SimpleDateFormat
import java.util.*
import org.linphone.LinphoneApplication
class TimestampUtils {
companion object {
fun isToday(timestamp: Long, timestampInSecs: Boolean = true): Boolean {
val cal = Calendar.getInstance()
cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp
return isSameDay(cal, Calendar.getInstance())
}
fun isYesterday(timestamp: Long, timestampInSecs: Boolean = true): Boolean {
val yesterday = Calendar.getInstance()
yesterday.roll(Calendar.DAY_OF_MONTH, -1)
val cal = Calendar.getInstance()
cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp
return isSameDay(cal, yesterday)
}
fun isSameDay(timestamp1: Long, timestamp2: Long, timestampInSecs: Boolean = true): Boolean {
val cal1 = Calendar.getInstance()
cal1.timeInMillis = if (timestampInSecs) timestamp1 * 1000 else timestamp1
val cal2 = Calendar.getInstance()
cal2.timeInMillis = if (timestampInSecs) timestamp2 * 1000 else timestamp2
return isSameDay(cal1, cal2)
}
fun isSameDay(
cal1: Date,
cal2: Date
): Boolean {
return isSameDay(cal1.time, cal2.time, false)
}
fun dateToString(date: Long, timestampInSecs: Boolean = true): String {
val dateFormat: Format = android.text.format.DateFormat.getDateFormat(
LinphoneApplication.coreContext.context
)
val pattern = (dateFormat as SimpleDateFormat).toLocalizedPattern()
val calendar = Calendar.getInstance()
calendar.timeInMillis = if (timestampInSecs) date * 1000 else date
// See https://github.com/material-components/material-components-android/issues/882
val dateFormatter = SimpleDateFormat(pattern, Locale.getDefault())
dateFormatter.timeZone = TimeZone.getTimeZone("UTC")
return dateFormatter.format(calendar.time)
}
fun timeToString(hour: Int, minutes: Int): String {
val use24hFormat = android.text.format.DateFormat.is24HourFormat(
LinphoneApplication.coreContext.context
)
val calendar = Calendar.getInstance()
calendar.set(Calendar.HOUR_OF_DAY, hour)
calendar.set(Calendar.MINUTE, minutes)
return if (use24hFormat) {
SimpleDateFormat("HH'h'mm", Locale.getDefault()).format(calendar.time)
} else {
SimpleDateFormat("h:mm a", Locale.getDefault()).format(calendar.time)
}
}
fun timeToString(time: Long, timestampInSecs: Boolean = true): String {
val use24hFormat = android.text.format.DateFormat.is24HourFormat(
LinphoneApplication.coreContext.context
)
val calendar = Calendar.getInstance()
calendar.timeInMillis = if (timestampInSecs) time * 1000 else time
return if (use24hFormat) {
SimpleDateFormat("HH'h'mm", Locale.getDefault()).format(calendar.time)
} else {
SimpleDateFormat("h:mm a", Locale.getDefault()).format(calendar.time)
}
}
fun durationToString(hours: Int, minutes: Int): String {
val calendar = Calendar.getInstance()
calendar.set(Calendar.HOUR_OF_DAY, hours)
calendar.set(Calendar.MINUTE, minutes)
val pattern = when {
hours == 0 -> "mm'min'"
hours < 10 && minutes == 0 -> "H'h'"
hours < 10 && minutes > 0 -> "H'h'mm"
hours >= 10 && minutes == 0 -> "HH'h'"
else -> "HH'h'mm"
}
return SimpleDateFormat(pattern, Locale.getDefault()).format(calendar.time)
}
private fun isSameYear(timestamp: Long, timestampInSecs: Boolean = true): Boolean {
val cal = Calendar.getInstance()
cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp
return isSameYear(cal, Calendar.getInstance())
}
fun toString(
timestamp: Long,
onlyDate: Boolean = false,
timestampInSecs: Boolean = true,
shortDate: Boolean = true,
hideYear: Boolean = true
): String {
val dateFormat = if (isToday(timestamp, timestampInSecs)) {
DateFormat.getTimeInstance(DateFormat.SHORT)
} else {
if (onlyDate) {
DateFormat.getDateInstance(if (shortDate) DateFormat.SHORT else DateFormat.FULL)
} else {
DateFormat.getDateTimeInstance(
if (shortDate) DateFormat.SHORT else DateFormat.MEDIUM,
DateFormat.SHORT
)
}
} as SimpleDateFormat
if (hideYear || isSameYear(timestamp, timestampInSecs)) {
// Remove the year part of the format
dateFormat.applyPattern(
dateFormat.toPattern().replace(
"/?y+/?|,?\\s?y+\\s?".toRegex(),
if (shortDate) "" else " "
)
)
}
val millis = if (timestampInSecs) timestamp * 1000 else timestamp
return dateFormat.format(Date(millis)).capitalize(Locale.getDefault())
}
private fun isSameDay(
cal1: Calendar,
cal2: Calendar
): Boolean {
return cal1[Calendar.ERA] == cal2[Calendar.ERA] &&
cal1[Calendar.YEAR] == cal2[Calendar.YEAR] &&
cal1[Calendar.DAY_OF_YEAR] == cal2[Calendar.DAY_OF_YEAR]
}
private fun isSameYear(
cal1: Calendar,
cal2: Calendar
): Boolean {
return cal1[Calendar.ERA] == cal2[Calendar.ERA] &&
cal1[Calendar.YEAR] == cal2[Calendar.YEAR]
}
}
}

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="29dp"
android:height="28dp"
android:viewportWidth="29"
android:viewportHeight="28">
<path
android:name="path"
android:pathData="M 14.5 2.333 C 8.06 2.333 2.833 7.56 2.833 14 C 2.833 20.44 8.06 25.667 14.5 25.667 C 20.94 25.667 26.167 20.44 26.167 14 C 26.167 7.56 20.94 2.333 14.5 2.333 Z M 20.333 15.167 L 15.667 15.167 L 15.667 19.834 L 13.333 19.834 L 13.333 15.167 L 8.667 15.167 L 8.667 12.834 L 13.333 12.834 L 13.333 8.167 L 15.667 8.167 L 15.667 12.834 L 20.333 12.834 L 20.333 15.167 Z"
android:fillColor="#fe5e00"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="21dp"
android:height="21dp"
android:viewportWidth="21"
android:viewportHeight="21">
<path
android:name="path"
android:pathData="M 18.5 0.5 L 2.5 0.5 C 1.4 0.5 0.5 1.4 0.5 2.5 L 0.5 20.5 L 4.5 16.5 L 18.5 16.5 C 19.6 16.5 20.5 15.6 20.5 14.5 L 20.5 2.5 C 20.5 1.4 19.6 0.5 18.5 0.5 Z M 18.5 14.5 L 3.67 14.5 L 2.5 15.67 L 2.5 2.5 L 18.5 2.5 L 18.5 14.5 Z M 5.5 7.5 L 7.5 7.5 L 7.5 9.5 L 5.5 9.5 L 5.5 7.5 Z M 13.5 7.5 L 15.5 7.5 L 15.5 9.5 L 13.5 9.5 L 13.5 7.5 Z M 9.5 7.5 L 11.5 7.5 L 11.5 9.5 L 9.5 9.5 L 9.5 7.5 Z"
android:fillColor="#6c7a87"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,19 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:name="path"
android:pathData="M 18 0 C 13.228 0 8.646 1.898 5.272 5.272 C 1.898 8.646 0 13.228 0 18 C 0 22.772 1.898 27.354 5.272 30.728 C 8.646 34.102 13.228 36 18 36 C 22.772 36 27.354 34.102 30.728 30.728 C 34.102 27.354 36 22.772 36 18 C 36 13.228 34.102 8.646 30.728 5.272 C 27.354 1.898 22.772 0 18 0 Z"
android:fillColor="#6c7a87"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 32 25.5 C 31.259 25.498 30.537 25.73 29.937 26.165 C 29.337 26.599 28.89 27.212 28.661 27.916 C 28.432 28.62 28.432 29.38 28.661 30.084 C 28.89 30.788 29.337 31.401 29.937 31.835 C 30.537 32.27 31.259 32.502 32 32.5 C 32.741 32.502 33.463 32.27 34.063 31.835 C 34.663 31.401 35.11 30.788 35.339 30.084 C 35.568 29.38 35.568 28.62 35.339 27.916 C 35.11 27.212 34.663 26.599 34.063 26.165 C 33.463 25.73 32.741 25.498 32 25.5 Z"
android:fillColor="#4fae80"
android:strokeColor="#ffffff"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@drawable/shape_conversation_selected_cell_background" />
<item android:state_pressed="true"
android:drawable="@drawable/shape_conversation_selected_cell_background" />
<item
android:drawable="@drawable/shape_conversation_cell_background" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/primary_color"/>
<size android:width="14dp" android:height="14dp"/>
</shape>

View file

@ -0,0 +1,21 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="38dp"
android:height="36dp"
android:viewportWidth="38"
android:viewportHeight="36">
<group android:name="group">
<path
android:name="path"
android:pathData="M 25 0 C 21.554 0 18.245 1.371 15.808 3.808 C 13.371 6.245 12 9.554 12 13 C 12 16.446 13.371 19.755 15.808 22.192 C 18.245 24.629 21.554 26 25 26 C 28.446 26 31.755 24.629 34.192 22.192 C 36.629 19.755 38 16.446 38 13 C 38 9.554 36.629 6.245 34.192 3.808 C 31.755 1.371 28.446 0 25 0 Z"
android:fillColor="#6C7A87"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 13 11 C 9.819 11 6.764 12.265 4.515 14.515 C 2.265 16.764 1 19.819 1 23 C 1 26.181 2.265 29.236 4.515 31.485 C 6.764 33.735 9.819 35 13 35 C 16.181 35 19.236 33.735 21.485 31.485 C 23.735 29.236 25 26.181 25 23 C 25 19.819 23.735 16.764 21.485 14.515 C 19.236 12.265 16.181 11 13 11 Z"
android:fillColor="#6C7A87"
android:strokeColor="#fafeff"
android:strokeWidth="2"/>
</group>
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:name="path"
android:pathData="M 5.801 6.88 L 6.507 7.586 L 10.74 3.353 L 11.447 4.06 L 6.507 9 L 3.325 5.818 L 4.032 5.111 L 5.095 6.173 L 5.801 6.879 L 5.801 6.88 Z M 5.802 5.466 L 8.278 2.989 L 8.983 3.694 L 6.507 6.171 L 5.802 5.466 Z M 4.388 8.293 L 3.682 9 L 0.5 5.818 L 1.207 5.111 L 1.913 5.817 L 1.913 5.818 L 4.388 8.293 Z"
android:fillColor="#6c7a87"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:name="path"
android:pathData="M 5.801 6.88 L 6.507 7.586 L 10.74 3.353 L 11.447 4.06 L 6.507 9 L 3.325 5.818 L 4.032 5.111 L 5.095 6.173 L 5.801 6.879 L 5.801 6.88 Z M 5.802 5.466 L 8.278 2.989 L 8.983 3.694 L 6.507 6.171 L 5.802 5.466 Z M 4.388 8.293 L 3.682 9 L 0.5 5.818 L 1.207 5.111 L 1.913 5.817 L 1.913 5.818 L 4.388 8.293 Z"
android:fillColor="#FF5E00"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:name="path"
android:pathData="M 10 2 L 2 2 C 1.45 2 1 2.45 1 3 L 1 9 C 1 9.55 1.45 10 2 10 L 6.5 10 L 6.5 9 L 2 9 L 2 4 L 6 6.5 L 10 4 L 10 6.5 L 11 6.5 L 11 3 C 11 2.45 10.55 2 10 2 Z M 6 5.5 L 2 3 L 10 3 L 6 5.5 Z M 9.5 7.5 L 11.5 9.5 L 9.5 11.5 L 9.5 10 L 7.5 10 L 7.5 9 L 9.5 9 L 9.5 7.5 Z"
android:fillColor="#6c7a87"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="20dp"
android:height="14dp"
android:viewportWidth="20"
android:viewportHeight="14">
<path
android:name="path"
android:pathData="M 0 13.333 L 20 13.333 L 20 11.111 L 0 11.111 L 0 13.333 Z M 0 7.778 L 20 7.778 L 20 5.556 L 0 5.556 L 0 7.778 Z M 0 0 L 0 2.222 L 20 2.222 L 20 0 L 0 0 Z"
android:fillColor="#ffffff"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="14dp"
android:height="13dp"
android:viewportWidth="14"
android:viewportHeight="13">
<path
android:name="path"
android:pathData="M 12.858 12.358 C 12.949 12.267 13 12.143 13 12.014 C 13 11.885 12.949 11.761 12.858 11.67 L 9.353 8.167 C 10.035 7.33 10.407 6.282 10.405 5.203 C 10.404 3.956 9.908 2.761 9.026 1.879 C 8.145 0.997 6.949 0.501 5.703 0.5 C 4.456 0.501 3.261 0.997 2.379 1.879 C 1.497 2.761 1.001 3.956 1 5.203 C 1.001 6.449 1.497 7.645 2.379 8.526 C 3.261 9.408 4.456 9.904 5.703 9.905 C 6.782 9.907 7.83 9.535 8.667 8.853 L 12.171 12.358 C 12.262 12.449 12.385 12.5 12.514 12.5 C 12.578 12.5 12.641 12.488 12.7 12.463 C 12.759 12.439 12.813 12.403 12.858 12.358 Z M 9.433 5.203 C 9.432 6.191 9.038 7.139 8.339 7.839 C 7.64 8.538 6.692 8.931 5.703 8.932 C 4.714 8.931 3.766 8.538 3.067 7.839 C 2.368 7.14 1.974 6.192 1.973 5.203 C 1.974 4.214 2.368 3.266 3.067 2.567 C 3.766 1.868 4.714 1.474 5.703 1.473 C 6.691 1.474 7.64 1.867 8.339 2.567 C 9.038 3.266 9.432 4.214 9.433 5.203 Z"
android:fillColor="#676767"
android:strokeColor="#676767"
android:strokeWidth="0.5"/>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="@color/white"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="@color/gray_3"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="28dp" />
<solid android:color="@color/gray_2"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topLeftRadius="30dp" android:topRightRadius="30dp" />
<solid android:color="@color/white"/>
</shape>

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="5dp"
android:height="4dp"
android:viewportWidth="5"
android:viewportHeight="4">
<path
android:name="path"
android:pathData="M 2.475 3.65 L 0 1.175 L 0.825 0.35 L 2.475 2 L 4.125 0.35 L 4.95 1.175 L 2.475 3.65 Z"
android:fillColor="#6c7a87"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.MainViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/main_nav_rail"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_nav" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="10dp"
app:defaultNavHost="true"
app:layout_constraintStart_toEndOf="@id/main_nav_rail"
app:layout_constraintEnd_toEndOf="parent"
app:navGraph="@navigation/main_nav_graph"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.MainViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/main_nav_view"
app:navGraph="@navigation/main_nav_graph"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/main_nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:menu="@menu/main_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="android.graphics.Typeface" />
<variable
name="data"
type="org.linphone.ui.conversations.ChatRoomData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> data.onClicked()}"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="@drawable/conversation_cell_background">
<ImageView
android:id="@+id/avatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@{data.isOneToOne ? @drawable/contact_avatar : @drawable/group_avatar, default=@drawable/contact_avatar}"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{data.isOneToOne ? data.contactName : data.subject, default=`John Doe`}"
android:textColor="#000000"
android:textSize="14sp"
android:textStyle="@{data.unreadChatCount > 0 ? Typeface.BOLD : Typeface.NORMAL, default=normal}"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{data.isComposing ? `... est en train d'écrire` : data.lastMessage, default=`Lorem Ipsum`}"
android:textColor="@{data.unreadChatCount > 0 ? @color/black : @color/gray_4, default=@color/gray_4}"
android:textSize="14sp"
android:textStyle="@{data.unreadChatCount > 0 ? Typeface.BOLD : Typeface.NORMAL, default=normal}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/title" />
<TextView
android:id="@+id/date_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.lastUpdate, default=`16:45`}"
android:textColor="#9AABB5"
android:textSize="12sp"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title" />
<ImageView
android:id="@+id/imdn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@{data.lastMessageImdnIcon, default=@drawable/imdn_read}"
android:layout_marginEnd="10dp"
android:visibility="@{data.showLastMessageImdnIcon ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/subtitle"
app:layout_constraintBottom_toBottomOf="@id/subtitle"/>
<TextView
android:id="@+id/unread"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:background="@drawable/conversation_cell_unread_count_background"
android:ellipsize="end"
android:gravity="center"
android:singleLine="true"
android:text="@{String.valueOf(data.unreadChatCount), default=`1`}"
android:textSize="10sp"
android:textColor="@color/white"
android:visibility="@{data.unreadChatCount == 0 ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/subtitle"
app:layout_constraintBottom_toBottomOf="@id/subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="markAsReadClickListener"
type="View.OnClickListener" />
<variable
name="callClickListener"
type="View.OnClickListener" />
<variable
name="muteClickListener"
type="View.OnClickListener" />
<variable
name="unMuteClickListener"
type="View.OnClickListener" />
<variable
name="deleteClickListener"
type="View.OnClickListener" />
<variable
name="isMuted"
type="Boolean" />
<variable
name="isRead"
type="Boolean" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</layout>

View file

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.conversations.ConversationsListViewModel" />
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="menuClickListener"
type="View.OnClickListener" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray_1">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Conversations"
android:textSize="20sp"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:layout_margin="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/menu"
android:layout_marginStart="22dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"/>
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:src="@drawable/shape_white_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent" />
<EditText
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:padding="10dp"
android:drawableStart="@drawable/search"
android:drawablePadding="10dp"
android:background="@drawable/shape_search_round_background"
android:hint="Rechercher un contact, une conversation..."
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<TextView
android:id="@+id/sort_by_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:text="Trier par"
android:textSize="12sp"
android:textColor="@color/gray_5"
app:layout_constraintStart_toStartOf="@id/search_bar"
app:layout_constraintTop_toTopOf="@id/sort_by"
app:layout_constraintBottom_toBottomOf="@id/sort_by"/>
<TextView
android:id="@+id/sort_by"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="Récents"
android:textSize="14sp"
android:textColor="@color/gray_1"
android:drawablePadding="5dp"
app:layout_constraintStart_toEndOf="@id/sort_by_label"
app:layout_constraintTop_toBottomOf="@id/search_bar"
app:drawableEndCompat="@drawable/spinner" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/conversationsList"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/sort_by"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@drawable/add"
app:tint="@color/primary_color"
app:backgroundTint="@color/white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/conversationsFragment"
android:enabled="true"
android:icon="@drawable/chat"
android:title="Conversations"/>
<item
android:id="@+id/contactsFragment"
android:enabled="true"
android:title="Contacts"/>
<item
android:id="@+id/meetingsFragment"
android:enabled="true"
android:title="Réunions"/>
<item
android:id="@+id/historyFragment"
android:enabled="true"
android:title="Historique"/>
</menu>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_nav_graph"
app:startDestination="@id/conversationsFragment">
<fragment
android:id="@+id/conversationsFragment"
android:name="org.linphone.ui.conversations.ConversationsFragment"
android:label="ConversationsFragment"
tools:layout="@layout/conversations_fragment"/>
</navigation>

View file

@ -0,0 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Linphone" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/primary_color</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@android:color/transparent</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary_color">#FF5E00</color>
<color name="black">#000000</color>
<color name="white">#FFFFFF</color>
<color name="gray_1">#6C7A87</color>
<color name="gray_2">#F9F9F9</color>
<color name="gray_3">#EEF6F8</color>
<color name="gray_4">#949494</color>
<color name="gray_5">#4E4E4E</color>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Linphone</string>
</resources>

View file

@ -0,0 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Linphone" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/primary_color</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@android:color/transparent</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="internal_files" path="." />
<files-path name="internal_recordings" path="Download/" />
<external-files-path name="pictures" path="Pictures/" />
<external-files-path name="downloads" path="Download/" />
<external-files-path name="files" path="." />
<cache-path name="cache" path="."/>
</paths>

7
build.gradle Normal file
View file

@ -0,0 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.0.2' apply false
id 'com.android.library' version '8.0.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0-RC' apply false
id 'com.google.gms.google-services' version '4.3.15' apply false
}

26
gradle.properties Normal file
View file

@ -0,0 +1,26 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=true
LinphoneSdkBuildDir=

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Thu Jun 22 12:11:25 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

24
settings.gradle Normal file
View file

@ -0,0 +1,24 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
name "linphone.org maven repository"
url "https://linphone.org/maven_repository"
content {
includeGroup "org.linphone"
}
}
}
}
rootProject.name = "Linphone"
include ':app'