diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000..8fc1bd66c --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Linphone \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 000000000..b589d56e9 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 000000000..a2d7c2133 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 000000000..c1c3ab734 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..773fe0fbd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..7d1138440 --- /dev/null +++ b/app/build.gradle @@ -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' \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 000000000..c26aa7702 --- /dev/null +++ b/app/google-services.json @@ -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" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f104b6252 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/assistant_default_values b/app/src/main/assets/assistant_default_values new file mode 100644 index 000000000..5800203a4 --- /dev/null +++ b/app/src/main/assets/assistant_default_values @@ -0,0 +1,42 @@ + + +
+ 0 + 0 + 0 + -1 + + 0 + 0 + 3600 + + + + 1 + + + + + 0 + 0 + 0 + +
+
+ + +
+
+ 0 +
+
+ + MD5 + -1 + 0 + -1 + 128 + 1 + ^[a-zA-Z0-9+_.\-]*$ +
+
diff --git a/app/src/main/assets/assistant_linphone_default_values b/app/src/main/assets/assistant_linphone_default_values new file mode 100644 index 000000000..7a72cd0c8 --- /dev/null +++ b/app/src/main/assets/assistant_linphone_default_values @@ -0,0 +1,42 @@ + + +
+ 1 + 0 + 1 + 120 + sip:voip-metrics@sip.linphone.org;transport=tls + 1 + 180 + 31536000 + sip:?@sip.linphone.org + <sip:sip.linphone.org;transport=tls> + <sip:sip.linphone.org;transport=tls> + 1 + nat_policy_default_values + sip.linphone.org + sip:conference-factory@sip.linphone.org + sip:videoconference-factory@sip.linphone.org + 1 + 1 + 1 + https://lime.linphone.org/lime-server/lime-server.php +
+
+ stun.linphone.org + stun,ice +
+
+ 1 +
+
+ sip.linphone.org + SHA-256 + -1 + 1 + -1 + 64 + 1 + ^[a-z0-9+_.\-]*$ +
+
diff --git a/app/src/main/assets/linphonerc_default b/app/src/main/assets/linphonerc_default new file mode 100644 index 000000000..bad1a1a76 --- /dev/null +++ b/app/src/main/assets/linphonerc_default @@ -0,0 +1,44 @@ + +## Start of default rc + +[sip] +contact="Linphone Android" +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 diff --git a/app/src/main/assets/linphonerc_factory b/app/src/main/assets/linphonerc_factory new file mode 100644 index 000000000..4115c1d0a --- /dev/null +++ b/app/src/main/assets/linphonerc_factory @@ -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= +reg_route=sip:sip.linphone.org;transport=tls +reg_identity="Sylvain Berfini" +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 diff --git a/app/src/main/java/org/linphone/LinphoneApplication.kt b/app/src/main/java/org/linphone/LinphoneApplication.kt new file mode 100644 index 000000000..11813143a --- /dev/null +++ b/app/src/main/java/org/linphone/LinphoneApplication.kt @@ -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 . + */ +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() + } +} diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt new file mode 100644 index 000000000..445f8b849 --- /dev/null +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -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 . + */ +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) + } +} diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt new file mode 100644 index 000000000..a730ebf3c --- /dev/null +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -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 . + */ +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() + } +} diff --git a/app/src/main/java/org/linphone/core/CorePushReceiver.kt b/app/src/main/java/org/linphone/core/CorePushReceiver.kt new file mode 100644 index 000000000..2030189d1 --- /dev/null +++ b/app/src/main/java/org/linphone/core/CorePushReceiver.kt @@ -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 . + */ +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") + } +} diff --git a/app/src/main/java/org/linphone/ui/MainActivity.kt b/app/src/main/java/org/linphone/ui/MainActivity.kt new file mode 100644 index 000000000..0048d8c24 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/MainActivity.kt @@ -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 . + */ +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 + } +} diff --git a/app/src/main/java/org/linphone/ui/MainViewModel.kt b/app/src/main/java/org/linphone/ui/MainViewModel.kt new file mode 100644 index 000000000..fe9b232c8 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/MainViewModel.kt @@ -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 . + */ +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() + + private val coreListener = object : CoreListenerStub() { + override fun onMessagesReceived( + core: Core, + chatRoom: ChatRoom, + messages: Array + ) { + 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) + } + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt b/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt new file mode 100644 index 000000000..981fc08fa --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt @@ -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 . + */ +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() + + val subject = MutableLiveData() + + val lastMessage = MutableLiveData() + + val unreadChatCount = MutableLiveData() + + val isComposing = MutableLiveData() + + val isSecure = MutableLiveData() + + val isSecureVerified = MutableLiveData() + + val isEphemeral = MutableLiveData() + + val isMuted = MutableLiveData() + + val lastUpdate = MutableLiveData() + + val showLastMessageImdnIcon = MutableLiveData() + + val lastMessageImdnIcon = MutableLiveData() + + 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) { + 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() +} diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationMenuDialogFragment.kt new file mode 100644 index 000000000..c8155bd58 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationMenuDialogFragment.kt @@ -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 . + */ +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 + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt new file mode 100644 index 000000000..8b3f263df --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt @@ -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 . + */ +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) + } + } + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt new file mode 100644 index 000000000..c84d5541d --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt @@ -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 . + */ +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(ConversationDiffCallback()) { + val chatRoomClickedEvent = MutableLiveData>() + + val chatRoomMenuClickedEvent = MutableLiveData>() + + 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() { + 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 + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt new file mode 100644 index 000000000..5c128b4d0 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt @@ -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 . + */ +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>() + + val notifyItemChangedEvent = MutableLiveData>() + + 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 + ) { + 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() + val chatRooms = core.chatRooms + for (chatRoom in chatRooms) { + list.add(ChatRoomData(chatRoom)) + } + chatRoomsList.postValue(list) + } + } + + private fun reorderChatRoomsList() { + coreContext.postOnCoreThread { core -> + val list = arrayListOf() + list.addAll(chatRoomsList.value.orEmpty()) + list.sortByDescending { data -> data.chatRoom.lastUpdateTime } + chatRoomsList.postValue(list) + } + } +} diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt new file mode 100644 index 000000000..7b17eefbf --- /dev/null +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -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 . + */ +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) +} diff --git a/app/src/main/java/org/linphone/utils/Event.kt b/app/src/main/java/org/linphone/utils/Event.kt new file mode 100644 index 000000000..bdff97f45 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/Event.kt @@ -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 . + */ +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(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) + } + } +} diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt new file mode 100644 index 000000000..f48796c03 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -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 . + */ +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() + } + } +} diff --git a/app/src/main/java/org/linphone/utils/TimestampUtils.kt b/app/src/main/java/org/linphone/utils/TimestampUtils.kt new file mode 100644 index 000000000..baf0e7262 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/TimestampUtils.kt @@ -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 . + */ +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] + } + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml new file mode 100644 index 000000000..60b45bdfd --- /dev/null +++ b/app/src/main/res/drawable/add.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/chat.xml b/app/src/main/res/drawable/chat.xml new file mode 100644 index 000000000..5b7a4a997 --- /dev/null +++ b/app/src/main/res/drawable/chat.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/contact_avatar.xml b/app/src/main/res/drawable/contact_avatar.xml new file mode 100644 index 000000000..da9c64873 --- /dev/null +++ b/app/src/main/res/drawable/contact_avatar.xml @@ -0,0 +1,19 @@ + + + + diff --git a/app/src/main/res/drawable/conversation_cell_background.xml b/app/src/main/res/drawable/conversation_cell_background.xml new file mode 100644 index 000000000..9212f8687 --- /dev/null +++ b/app/src/main/res/drawable/conversation_cell_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_cell_unread_count_background.xml b/app/src/main/res/drawable/conversation_cell_unread_count_background.xml new file mode 100644 index 000000000..410653ffe --- /dev/null +++ b/app/src/main/res/drawable/conversation_cell_unread_count_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/group_avatar.xml b/app/src/main/res/drawable/group_avatar.xml new file mode 100644 index 000000000..6e3f0c8d5 --- /dev/null +++ b/app/src/main/res/drawable/group_avatar.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/imdn_delivered.xml b/app/src/main/res/drawable/imdn_delivered.xml new file mode 100644 index 000000000..9deccd49e --- /dev/null +++ b/app/src/main/res/drawable/imdn_delivered.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/imdn_read.xml b/app/src/main/res/drawable/imdn_read.xml new file mode 100644 index 000000000..fb9925687 --- /dev/null +++ b/app/src/main/res/drawable/imdn_read.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/imdn_sent.xml b/app/src/main/res/drawable/imdn_sent.xml new file mode 100644 index 000000000..2fc80f60e --- /dev/null +++ b/app/src/main/res/drawable/imdn_sent.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml new file mode 100644 index 000000000..11dd472dd --- /dev/null +++ b/app/src/main/res/drawable/menu.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/search.xml b/app/src/main/res/drawable/search.xml new file mode 100644 index 000000000..a578ece79 --- /dev/null +++ b/app/src/main/res/drawable/search.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/drawable/shape_conversation_cell_background.xml b/app/src/main/res/drawable/shape_conversation_cell_background.xml new file mode 100644 index 000000000..139f19f5d --- /dev/null +++ b/app/src/main/res/drawable/shape_conversation_cell_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conversation_selected_cell_background.xml b/app/src/main/res/drawable/shape_conversation_selected_cell_background.xml new file mode 100644 index 000000000..63497fa7c --- /dev/null +++ b/app/src/main/res/drawable/shape_conversation_selected_cell_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_search_round_background.xml b/app/src/main/res/drawable/shape_search_round_background.xml new file mode 100644 index 000000000..871ac5679 --- /dev/null +++ b/app/src/main/res/drawable/shape_search_round_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_white_background.xml b/app/src/main/res/drawable/shape_white_background.xml new file mode 100644 index 000000000..500dd8cd8 --- /dev/null +++ b/app/src/main/res/drawable/shape_white_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/spinner.xml b/app/src/main/res/drawable/spinner.xml new file mode 100644 index 000000000..567562a7b --- /dev/null +++ b/app/src/main/res/drawable/spinner.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 000000000..754d7d91a --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..22d53a3d7 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_room_list_cell.xml b/app/src/main/res/layout/chat_room_list_cell.xml new file mode 100644 index 000000000..b08522bc6 --- /dev/null +++ b/app/src/main/res/layout/chat_room_list_cell.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_room_menu.xml b/app/src/main/res/layout/chat_room_menu.xml new file mode 100644 index 000000000..284350316 --- /dev/null +++ b/app/src/main/res/layout/chat_room_menu.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conversations_fragment.xml b/app/src/main/res/layout/conversations_fragment.xml new file mode 100644 index 000000000..4f7994bd4 --- /dev/null +++ b/app/src/main/res/layout/conversations_fragment.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_nav.xml b/app/src/main/res/menu/main_nav.xml new file mode 100644 index 000000000..6767dd257 --- /dev/null +++ b/app/src/main/res/menu/main_nav.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml new file mode 100644 index 000000000..810928820 --- /dev/null +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..da5983b95 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..ab795e1fb --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,13 @@ + + + #FF5E00 + + #000000 + #FFFFFF + + #6C7A87 + #F9F9F9 + #EEF6F8 + #949494 + #4E4E4E + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..0268f2388 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Linphone + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..2b24af9dc --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 000000000..fa0f996d2 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..9ee9997b0 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 000000000..a40f5cad7 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..a8b69f717 --- /dev/null +++ b/build.gradle @@ -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 +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..059a5b41d --- /dev/null +++ b/gradle.properties @@ -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= \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e708b1c02 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..a87dbe16a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..6e091c8b9 --- /dev/null +++ b/settings.gradle @@ -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'