From dcecd2c470f913c726de7692ee9ef6046b8fe34f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrien=20B=C3=A9raud?= <adrien.beraud@savoirfairelinux.com>
Date: Thu, 26 Aug 2021 06:52:05 -0400
Subject: [PATCH] more Kotlin migration

Change-Id: Ief52a65dbc99ce149f083e362658f19352e6c93f
---
 .../cx/ring/account/AccountWizardActivity.kt  |   4 +-
 .../cx/ring/application/JamiApplication.kt    |  14 +-
 .../cx/ring/fragments/ShareWithFragment.kt    | 130 ++++-----
 .../java/cx/ring/service/JamiJobService.java  |  87 ------
 .../java/cx/ring/service/JamiJobService.kt    |  80 ++++++
 .../java/cx/ring/service/SyncService.java     | 121 --------
 .../main/java/cx/ring/service/SyncService.kt  | 109 +++++++
 .../ring/services/DeviceRuntimeServiceImpl.kt |  80 ++----
 .../cx/ring/settings/SettingsFragment.java    | 236 ----------------
 .../java/cx/ring/settings/SettingsFragment.kt | 212 ++++++++++++++
 .../cx/ring/tv/account/TVAccountWizard.kt     |   7 +-
 .../java/cx/ring/tv/main/HomeActivity.java    | 267 ------------------
 .../main/java/cx/ring/tv/main/HomeActivity.kt | 243 ++++++++++++++++
 .../java/cx/ring/utils/AndroidFileUtils.kt    |   2 +-
 .../main/java/net/jami/call/CallPresenter.kt  |  17 +-
 .../conversation/ConversationPresenter.kt     |   2 +-
 .../java/net/jami/services/AccountService.kt  |  28 +-
 .../net/jami/services/ConversationFacade.kt   |  92 +++---
 .../jami/services/DeviceRuntimeService.java   |  82 ------
 .../net/jami/services/DeviceRuntimeService.kt |  66 +++++
 .../net/jami/services/HistoryService.java     | 243 ----------------
 .../java/net/jami/services/HistoryService.kt  | 222 +++++++++++++++
 22 files changed, 1088 insertions(+), 1256 deletions(-)
 delete mode 100644 ring-android/app/src/main/java/cx/ring/service/JamiJobService.java
 create mode 100644 ring-android/app/src/main/java/cx/ring/service/JamiJobService.kt
 delete mode 100644 ring-android/app/src/main/java/cx/ring/service/SyncService.java
 create mode 100644 ring-android/app/src/main/java/cx/ring/service/SyncService.kt
 delete mode 100644 ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.java
 create mode 100644 ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.kt
 delete mode 100644 ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java
 create mode 100644 ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.kt
 delete mode 100644 ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
 create mode 100644 ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.kt
 delete mode 100644 ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java
 create mode 100644 ring-android/libringclient/src/main/java/net/jami/services/HistoryService.kt

diff --git a/ring-android/app/src/main/java/cx/ring/account/AccountWizardActivity.kt b/ring-android/app/src/main/java/cx/ring/account/AccountWizardActivity.kt
index 058a4d0c6..4beaf67e2 100644
--- a/ring-android/app/src/main/java/cx/ring/account/AccountWizardActivity.kt
+++ b/ring-android/app/src/main/java/cx/ring/account/AccountWizardActivity.kt
@@ -31,7 +31,7 @@ import androidx.appcompat.app.AlertDialog
 import androidx.fragment.app.Fragment
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import cx.ring.R
-import cx.ring.application.JamiApplication.Companion.instance
+import cx.ring.application.JamiApplication
 import cx.ring.client.HomeActivity
 import cx.ring.fragments.AccountMigrationFragment
 import cx.ring.fragments.SIPAccountCreationFragment
@@ -55,7 +55,7 @@ class AccountWizardActivity : BaseActivity<AccountWizardPresenter>(), AccountWiz
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        instance?.startDaemon()
+        JamiApplication.instance?.startDaemon()
         setContentView(R.layout.activity_wizard)
         var accountToMigrate: String? = null
         val intent = intent
diff --git a/ring-android/app/src/main/java/cx/ring/application/JamiApplication.kt b/ring-android/app/src/main/java/cx/ring/application/JamiApplication.kt
index d01769276..d9317b019 100644
--- a/ring-android/app/src/main/java/cx/ring/application/JamiApplication.kt
+++ b/ring-android/app/src/main/java/cx/ring/application/JamiApplication.kt
@@ -39,7 +39,6 @@ import cx.ring.service.DRingService
 import cx.ring.service.JamiJobService
 import cx.ring.utils.AndroidFileUtils
 import cx.ring.views.AvatarFactory
-import dagger.hilt.android.AndroidEntryPoint
 import io.reactivex.rxjava3.core.Completable
 import io.reactivex.rxjava3.schedulers.Schedulers
 import net.jami.daemon.JamiService
@@ -55,7 +54,7 @@ abstract class JamiApplication : Application() {
         const val DRING_CONNECTION_CHANGED = BuildConfig.APPLICATION_ID + ".event.DRING_CONNECTION_CHANGE"
         const val PERMISSIONS_REQUEST = 57
         private val RINGER_FILTER = IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION)
-        @JvmStatic var instance: JamiApplication? = null
+        var instance: JamiApplication? = null
     }
 
     @Inject
@@ -85,6 +84,9 @@ abstract class JamiApplication : Application() {
     @Inject lateinit
     var mContactService: ContactService
 
+    @Inject lateinit
+    var mConversationFacade: ConversationFacade
+
     private val ringerModeListener: BroadcastReceiver = object : BroadcastReceiver() {
         override fun onReceive(context: Context, intent: Intent) {
             ringerModeChanged(intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, AudioManager.RINGER_MODE_NORMAL))
@@ -194,14 +196,6 @@ abstract class JamiApplication : Application() {
 
         //RxJavaPlugins.setErrorHandler(e -> Log.e(TAG, "Unhandled RxJava error", e));
 
-        // building injection dependency tree
-        /*injectionComponent = DaggerJamiInjectionComponent.builder()
-                .jamiInjectionModule(JamiInjectionModule(this))
-                .serviceInjectionModule(ServiceInjectionModule(this))
-                .build()
-
-        // we can now inject in our self whatever modules define
-        injectionComponent!!.inject(this)*/
         bootstrapDaemon()
         mPreferencesService.loadDarkMode()
         Completable.fromAction {
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.kt b/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.kt
index c17089713..e2180d584 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.kt
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ShareWithFragment.kt
@@ -37,9 +37,9 @@ import cx.ring.viewholders.SmartListViewHolder.SmartListListeners
 import dagger.hilt.android.AndroidEntryPoint
 import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
 import io.reactivex.rxjava3.disposables.CompositeDisposable
-import net.jami.services.ConversationFacade
 import net.jami.model.Account
 import net.jami.services.ContactService
+import net.jami.services.ConversationFacade
 import net.jami.smartlist.SmartListViewModel
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -48,50 +48,66 @@ import javax.inject.Singleton
 class ShareWithFragment : Fragment() {
     private val mDisposable = CompositeDisposable()
 
-    @JvmField
     @Inject
     @Singleton
-    var mConversationFacade: ConversationFacade? = null
+    lateinit var mConversationFacade: ConversationFacade
 
-    @JvmField
     @Inject
     @Singleton
-    var mContactService: ContactService? = null
+    lateinit var mContactService: ContactService
+
     private var mPendingIntent: Intent? = null
     private var adapter: SmartListAdapter? = null
     private var binding: FragSharewithBinding? = null
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View {
-        binding = FragSharewithBinding.inflate(inflater)
-        val context = binding!!.root.context
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        adapter = SmartListAdapter(null, object : SmartListListeners {
+            override fun onItemClick(smartListViewModel: SmartListViewModel) {
+                mPendingIntent?.let { intent ->
+                    mPendingIntent = null
+                    val type = intent.type
+                    if (type != null && type.startsWith("text/")) {
+                        intent.putExtra(Intent.EXTRA_TEXT, binding!!.previewText.text.toString())
+                    }
+                    intent.putExtras(ConversationPath.toBundle(smartListViewModel.accountId, smartListViewModel.uri))
+                    intent.setClass(requireActivity(), ConversationActivity::class.java)
+                    startActivity(intent)
+                }
+            }
+
+            override fun onItemLongClick(smartListViewModel: SmartListViewModel) {}
+        }, mDisposable)
+
+        val binding = FragSharewithBinding.inflate(inflater).apply {
+            shareList.layoutManager = LinearLayoutManager(inflater.context)
+            shareList.adapter = adapter
+            this@ShareWithFragment.binding = this
+        }
         val activity: Activity? = activity
         if (activity is AppCompatActivity) {
-            activity.setSupportActionBar(binding!!.toolbar)
+            activity.setSupportActionBar(binding.toolbar)
             val ab = activity.supportActionBar
             ab?.setDisplayHomeAsUpEnabled(true)
         }
-        if (mPendingIntent != null) {
-            val type = mPendingIntent!!.type!!
-            val clip = mPendingIntent!!.clipData
+        mPendingIntent?.let { pendingIntent -> pendingIntent.type?.let { type ->
+            val clip = pendingIntent.clipData
             when {
                 type.startsWith("text/") -> {
-                    binding!!.previewText.setText(mPendingIntent!!.getStringExtra(Intent.EXTRA_TEXT))
-                    binding!!.previewText.visibility = View.VISIBLE
+                    binding.previewText.setText(pendingIntent.getStringExtra(Intent.EXTRA_TEXT))
+                    binding.previewText.visibility = View.VISIBLE
                 }
                 type.startsWith("image/") -> {
-                    var data = mPendingIntent!!.data
+                    var data = pendingIntent.data
                     if (data == null && clip != null && clip.itemCount > 0) data = clip.getItemAt(0).uri
-                    binding!!.previewImage.setImageURI(data)
-                    binding!!.previewImage.visibility = View.VISIBLE
+                    binding.previewImage.setImageURI(data)
+                    binding.previewImage.visibility = View.VISIBLE
                 }
                 type.startsWith("video/") -> {
-                    var data = mPendingIntent!!.data
+                    var data = pendingIntent.data
                     if (data == null && clip != null && clip.itemCount > 0) data = clip.getItemAt(0).uri
                     try {
-                        binding!!.previewVideo.setVideoURI(data)
-                        binding!!.previewVideo.visibility = View.VISIBLE
+                        binding.previewVideo.setVideoURI(data)
+                        binding.previewVideo.visibility = View.VISIBLE
                     } catch (e: NullPointerException) {
                         Log.e(TAG, e.message!!)
                     } catch (e: InflateException) {
@@ -99,48 +115,28 @@ class ShareWithFragment : Fragment() {
                     } catch (e: NumberFormatException) {
                         Log.e(TAG, e.message!!)
                     }
-                    binding!!.previewVideo.setOnCompletionListener { binding!!.previewVideo.start() }
-                }
-            }
-        }
-        adapter = SmartListAdapter(null, object : SmartListListeners {
-            override fun onItemClick(smartListViewModel: SmartListViewModel) {
-                mPendingIntent?.let { intent ->
-                    mPendingIntent = null
-                    val type = intent.type
-                    if (type != null && type.startsWith("text/")) {
-                        intent.putExtra(Intent.EXTRA_TEXT, binding!!.previewText.text.toString())
-                    }
-                    intent.putExtras(
-                        ConversationPath.toBundle(
-                            smartListViewModel.accountId,
-                            smartListViewModel.uri
-                        )
-                    )
-                    intent.setClass(requireActivity(), ConversationActivity::class.java)
-                    startActivity(intent)
+                    binding.previewVideo.setOnCompletionListener { binding.previewVideo.start() }
                 }
             }
-
-            override fun onItemLongClick(smartListViewModel: SmartListViewModel) {}
-        }, mDisposable)
-        binding!!.shareList.layoutManager = LinearLayoutManager(context)
-        binding!!.shareList.adapter = adapter
-        return binding!!.root
+        }}
+        return binding.root
     }
 
     override fun onStart() {
         super.onStart()
-        if (mPendingIntent == null) requireActivity().finish()
-        mDisposable.add(mConversationFacade!!
+        if (mPendingIntent == null) {
+            requireActivity().finish()
+            return
+        }
+        mDisposable.add(mConversationFacade
             .currentAccountSubject
             .switchMap { a: Account -> a.getConversationsViewModels(false) }
             .observeOn(AndroidSchedulers.mainThread())
-            .subscribe { list: MutableList<SmartListViewModel> ->
-                adapter?.update(list)
-            })
-        if (binding != null && binding!!.previewVideo.visibility != View.GONE) {
-            binding!!.previewVideo.start()
+            .subscribe { list -> adapter?.update(list) })
+        binding?.let { binding ->
+            if (binding.previewVideo.visibility != View.GONE) {
+                binding.previewVideo.start()
+            }
         }
     }
 
@@ -151,13 +147,14 @@ class ShareWithFragment : Fragment() {
 
     override fun onCreate(bundle: Bundle?) {
         super.onCreate(bundle)
-        /*Intent intent = getActivity().getIntent();
-        Bundle extra = intent.getExtras();
+        val intent = requireActivity().intent
+        val extra = intent.extras
         if (ConversationPath.fromBundle(extra) != null) {
-            intent.setClass(getActivity(), ConversationActivity.class);
-            startActivity(intent);
-            return;
-        }*/mPendingIntent = requireActivity().intent
+            intent.setClass(requireActivity(), ConversationActivity::class.java)
+            startActivity(intent)
+            return
+        }
+        mPendingIntent = intent
     }
 
     override fun onDestroy() {
@@ -167,15 +164,8 @@ class ShareWithFragment : Fragment() {
     }
 
     companion object {
-        private val TAG = ShareWithFragment::class.java.simpleName
+        private val TAG = ShareWithFragment::class.simpleName!!
 
-        /**
-         * Mandatory empty constructor for the fragment manager to instantiate the
-         * fragment (e.g. upon screen orientation changes).
-         */
-        /*public ShareWithFragment() {
-        JamiApplication.getInstance().getInjectionComponent().inject(this);
-    }*/
         fun newInstance(): ShareWithFragment {
             return ShareWithFragment()
         }
diff --git a/ring-android/app/src/main/java/cx/ring/service/JamiJobService.java b/ring-android/app/src/main/java/cx/ring/service/JamiJobService.java
deleted file mode 100644
index a3ad53cef..000000000
--- a/ring-android/app/src/main/java/cx/ring/service/JamiJobService.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
- *
- *  Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
- *
- *  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 <https://www.gnu.org/licenses/>.
- */
-package cx.ring.service;
-
-import android.app.job.JobParameters;
-import android.app.job.JobService;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Handler;
-import android.text.format.DateUtils;
-import android.util.Log;
-
-import androidx.annotation.RequiresApi;
-import androidx.core.content.ContextCompat;
-
-import cx.ring.application.JamiApplication;
-
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class JamiJobService extends JobService
-{
-    private static final String TAG = JamiJobService.class.getName();
-
-    public static final long JOB_INTERVAL = 6 * DateUtils.HOUR_IN_MILLIS;
-    public static final long JOB_FLEX = 30 * DateUtils.MINUTE_IN_MILLIS;
-    public static final long JOB_DURATION = 10 * DateUtils.SECOND_IN_MILLIS;
-    public static final int JOB_ID = 3905;
-
-    @Override
-    public boolean onStartJob(final JobParameters params) {
-        if (params.getJobId() != JOB_ID)
-            return false;
-        Log.w(TAG, "onStartJob() " + params);
-        try {
-            try {
-                ContextCompat.startForegroundService(this, new Intent(SyncService.ACTION_START)
-                        .setClass(this, SyncService.class));
-            } catch (IllegalStateException e) {
-                Log.e(TAG, "Error starting service", e);
-            }
-            new Handler().postDelayed(() -> {
-                Log.w(TAG, "jobFinished() " + params);
-                try {
-                    startService(new Intent(SyncService.ACTION_STOP).setClass(this, SyncService.class));
-                } catch (IllegalStateException ignored) {
-                }
-                jobFinished(params, false);
-            }, JOB_DURATION);
-            JamiApplication.getInstance().startDaemon();
-        } catch (Exception e) {
-            Log.e(TAG, "onStartJob failed", e);
-        }
-        return true;
-    }
-
-    @Override
-    public boolean onStopJob(JobParameters params) {
-        Log.w(TAG, "onStopJob() " + params);
-        try {
-            synchronized (this) {
-                notify();
-            }
-            try {
-                startService(new Intent(SyncService.ACTION_STOP).setClass(this, SyncService.class));
-            } catch (IllegalStateException ignored) {
-            }
-        } catch (Exception e) {
-            Log.e(TAG, "onStopJob failed", e);
-        }
-        return false;
-    }
-}
diff --git a/ring-android/app/src/main/java/cx/ring/service/JamiJobService.kt b/ring-android/app/src/main/java/cx/ring/service/JamiJobService.kt
new file mode 100644
index 000000000..7fe01430a
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/service/JamiJobService.kt
@@ -0,0 +1,80 @@
+/*
+ *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
+ *
+ *  Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ *  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 <https://www.gnu.org/licenses/>.
+ */
+package cx.ring.service
+
+import android.app.job.JobParameters
+import android.app.job.JobService
+import android.content.Intent
+import android.os.Build
+import android.os.Handler
+import android.text.format.DateUtils
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat
+import cx.ring.application.JamiApplication
+
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class JamiJobService : JobService() {
+    override fun onStartJob(params: JobParameters): Boolean {
+        if (params.jobId != JOB_ID) return false
+        Log.w(TAG, "onStartJob() $params")
+        try {
+            try {
+                ContextCompat.startForegroundService(this, Intent(SyncService.ACTION_START)
+                        .setClass(this, SyncService::class.java))
+            } catch (e: IllegalStateException) {
+                Log.e(TAG, "Error starting service", e)
+            }
+            Handler().postDelayed({
+                Log.w(TAG, "jobFinished() $params")
+                try {
+                    startService(Intent(SyncService.ACTION_STOP).setClass(this, SyncService::class.java))
+                } catch (ignored: IllegalStateException) {
+                }
+                jobFinished(params, false)
+            }, JOB_DURATION)
+            JamiApplication.instance?.startDaemon()
+        } catch (e: Exception) {
+            Log.e(TAG, "onStartJob failed", e)
+        }
+        return true
+    }
+
+    override fun onStopJob(params: JobParameters): Boolean {
+        Log.w(TAG, "onStopJob() $params")
+        try {
+            //synchronized(this) { notify() }
+            try {
+                startService(Intent(SyncService.ACTION_STOP).setClass(this, SyncService::class.java))
+            } catch (ignored: IllegalStateException) {
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "onStopJob failed", e)
+        }
+        return false
+    }
+
+    companion object {
+        private val TAG = JamiJobService::class.java.name
+        const val JOB_INTERVAL = 6 * DateUtils.HOUR_IN_MILLIS
+        const val JOB_FLEX = 30 * DateUtils.MINUTE_IN_MILLIS
+        const val JOB_DURATION = 10 * DateUtils.SECOND_IN_MILLIS
+        const val JOB_ID = 3905
+    }
+}
\ No newline at end of file
diff --git a/ring-android/app/src/main/java/cx/ring/service/SyncService.java b/ring-android/app/src/main/java/cx/ring/service/SyncService.java
deleted file mode 100644
index 48d2dab7c..000000000
--- a/ring-android/app/src/main/java/cx/ring/service/SyncService.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
- *
- *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
- *
- *  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, write to the Free Software
- *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
- */
-package cx.ring.service;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Intent;
-import android.content.pm.ServiceInfo;
-import android.os.Build;
-import android.os.Handler;
-import android.os.IBinder;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
-
-import net.jami.services.NotificationService;
-
-import java.util.Random;
-
-import javax.inject.Inject;
-
-import cx.ring.R;
-import cx.ring.application.JamiApplication;
-import cx.ring.client.HomeActivity;
-import cx.ring.services.NotificationServiceImpl;
-import dagger.hilt.android.AndroidEntryPoint;
-
-@AndroidEntryPoint
-public class SyncService extends Service {
-    public static final int NOTIF_SYNC_SERVICE_ID = 1004;
-
-    public static final String ACTION_START = "startService";
-    public static final String ACTION_STOP = "stopService";
-    public static final String EXTRA_TIMEOUT = "timeout";
-
-    private int serviceUsers = 0;
-    private final Random mRandom = new Random();
-
-    private Notification notification = null;
-
-    @Inject
-    NotificationService mNotificationService;
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        String action = intent.getAction();
-        if (ACTION_START.equals(action)) {
-            if (notification == null) {
-                final Intent deleteIntent = new Intent(ACTION_STOP)
-                        .setClass(getApplicationContext(), SyncService.class);
-                final Intent contentIntent = new Intent(Intent.ACTION_VIEW)
-                        .setClass(getApplicationContext(), HomeActivity.class)
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                notification = new NotificationCompat.Builder(this, NotificationServiceImpl.NOTIF_CHANNEL_SYNC)
-                        .setContentTitle(getString(R.string.notif_sync_title))
-                        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
-                        .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
-                        .setAutoCancel(false)
-                        .setVibrate(null)
-                        .setSmallIcon(R.drawable.ic_ring_logo_white)
-                        .setCategory(NotificationCompat.CATEGORY_PROGRESS)
-                        .setOnlyAlertOnce(true)
-                        .setDeleteIntent(PendingIntent.getService(getApplicationContext(), mRandom.nextInt(), deleteIntent, 0))
-                        .setContentIntent(PendingIntent.getActivity(getApplicationContext(), mRandom.nextInt(), contentIntent, 0))
-                        .build();
-            }
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
-                startForeground(NOTIF_SYNC_SERVICE_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
-            else
-                startForeground(NOTIF_SYNC_SERVICE_ID, notification);
-            if (serviceUsers == 0) {
-                JamiApplication.getInstance().startDaemon();
-            }
-            serviceUsers++;
-
-            long timeout = intent.getLongExtra(EXTRA_TIMEOUT, -1);
-            if (timeout > 0) {
-                new Handler().postDelayed(() -> {
-                    try {
-                        startService(new Intent(SyncService.ACTION_STOP).setClass(getApplicationContext(), SyncService.class));
-                    } catch (IllegalStateException ignored) {
-                    }
-                }, timeout);
-            }
-        }
-        else if (ACTION_STOP.equals(action)) {
-            serviceUsers--;
-            if (serviceUsers == 0) {
-                stopForeground(true);
-                stopSelf();
-                notification = null;
-            }
-        }
-        return START_NOT_STICKY;
-    }
-
-    @Nullable
-    @Override
-    public IBinder onBind(@NonNull Intent intent) {
-        return null;
-    }
-}
\ No newline at end of file
diff --git a/ring-android/app/src/main/java/cx/ring/service/SyncService.kt b/ring-android/app/src/main/java/cx/ring/service/SyncService.kt
new file mode 100644
index 000000000..3eb213887
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/service/SyncService.kt
@@ -0,0 +1,109 @@
+/*
+ *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ *  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, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+package cx.ring.service
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.Handler
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import cx.ring.R
+import cx.ring.application.JamiApplication
+import cx.ring.client.HomeActivity
+import cx.ring.services.NotificationServiceImpl
+import dagger.hilt.android.AndroidEntryPoint
+import net.jami.services.NotificationService
+import java.util.*
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class SyncService : Service() {
+    private var serviceUsers = 0
+    private val mRandom = Random()
+    private var notification: Notification? = null
+
+    @Inject
+    lateinit var mNotificationService: NotificationService
+
+    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+        val action = intent.action
+        if (ACTION_START == action) {
+            if (notification == null) {
+                val deleteIntent = Intent(ACTION_STOP)
+                    .setClass(applicationContext, SyncService::class.java)
+                val contentIntent = Intent(Intent.ACTION_VIEW)
+                    .setClass(applicationContext, HomeActivity::class.java)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                notification = NotificationCompat.Builder(this, NotificationServiceImpl.NOTIF_CHANNEL_SYNC)
+                    .setContentTitle(getString(R.string.notif_sync_title))
+                    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+                    .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
+                    .setAutoCancel(false)
+                    .setVibrate(null)
+                    .setSmallIcon(R.drawable.ic_ring_logo_white)
+                    .setCategory(NotificationCompat.CATEGORY_PROGRESS)
+                    .setOnlyAlertOnce(true)
+                    .setDeleteIntent(PendingIntent.getService(applicationContext, mRandom.nextInt(), deleteIntent, 0))
+                    .setContentIntent(PendingIntent.getActivity(applicationContext, mRandom.nextInt(), contentIntent, 0))
+                    .build()
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+                startForeground(NOTIF_SYNC_SERVICE_ID, notification!!, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
+            else
+                startForeground(NOTIF_SYNC_SERVICE_ID, notification)
+            if (serviceUsers == 0) {
+                JamiApplication.instance?.startDaemon()
+            }
+            serviceUsers++
+            val timeout = intent.getLongExtra(EXTRA_TIMEOUT, -1)
+            if (timeout > 0) {
+                Handler().postDelayed({
+                    try {
+                        startService(Intent(ACTION_STOP).setClass(applicationContext, SyncService::class.java))
+                    } catch (ignored: IllegalStateException) {
+                    }
+                }, timeout)
+            }
+        } else if (ACTION_STOP == action) {
+            serviceUsers--
+            if (serviceUsers == 0) {
+                stopForeground(true)
+                stopSelf()
+                notification = null
+            }
+        }
+        return START_NOT_STICKY
+    }
+
+    override fun onBind(intent: Intent): IBinder? {
+        return null
+    }
+
+    companion object {
+        const val NOTIF_SYNC_SERVICE_ID = 1004
+        const val ACTION_START = "startService"
+        const val ACTION_STOP = "stopService"
+        const val EXTRA_TIMEOUT = "timeout"
+    }
+}
\ No newline at end of file
diff --git a/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.kt b/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.kt
index 06bfafac0..ba56e362a 100644
--- a/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.kt
+++ b/ring-android/app/src/main/java/cx/ring/services/DeviceRuntimeServiceImpl.kt
@@ -31,12 +31,8 @@ import android.system.ErrnoException
 import android.system.Os
 import android.util.Log
 import androidx.core.content.ContextCompat
-import cx.ring.application.JamiApplication.Companion.instance
-import cx.ring.utils.AndroidFileUtils.copyAssetFolder
-import cx.ring.utils.AndroidFileUtils.getConversationDir
-import cx.ring.utils.AndroidFileUtils.getConversationPath
-import cx.ring.utils.AndroidFileUtils.getFilePath
-import cx.ring.utils.AndroidFileUtils.getTempPath
+import cx.ring.application.JamiApplication
+import cx.ring.utils.AndroidFileUtils
 import cx.ring.utils.NetworkUtils
 import net.jami.daemon.IntVect
 import net.jami.daemon.StringVect
@@ -57,7 +53,7 @@ class DeviceRuntimeServiceImpl(
         val pluginsPath = File(mContext.filesDir, "plugins")
         Log.w(TAG, "Plugins: " + pluginsPath.absolutePath)
         // Overwrite existing plugins folder in order to use newer plugins
-        copyAssetFolder(mContext.assets, "plugins", pluginsPath)
+        AndroidFileUtils.copyAssetFolder(mContext.assets, "plugins", pluginsPath)
     }
 
     override fun loadNativeLibrary() {
@@ -79,57 +75,47 @@ class DeviceRuntimeServiceImpl(
     }
 
     override fun getFilePath(filename: String): File {
-        return getFilePath(mContext, filename)
+        return AndroidFileUtils.getFilePath(mContext, filename)
     }
 
     override fun getConversationPath(conversationId: String, name: String): File {
-        return getConversationPath(mContext, conversationId, name)
+        return AndroidFileUtils.getConversationPath(mContext, conversationId, name)
     }
 
-    override fun getConversationPath(
-        accountId: String,
-        conversationId: String,
-        name: String
-    ): File {
-        return getConversationPath(mContext, accountId, conversationId, name)
+    override fun getConversationPath(accountId: String, conversationId: String,name: String): File {
+        return AndroidFileUtils.getConversationPath(mContext, accountId, conversationId, name)
     }
 
     override fun getTemporaryPath(conversationId: String, name: String): File {
-        return getTempPath(mContext, conversationId, name)
+        return AndroidFileUtils.getTempPath(mContext, conversationId, name)
     }
 
     override fun getConversationDir(conversationId: String): File {
-        return getConversationDir(mContext, conversationId)
+        return AndroidFileUtils.getConversationDir(mContext, conversationId)
     }
 
-    override fun getCacheDir(): File {
-        return mContext.cacheDir
-    }
+    override val cacheDir: File
+        get() = mContext.cacheDir
 
-    override fun getPushToken(): String? {
-        return instance?.pushToken
-    }
+    override val pushToken: String?
+        get() = JamiApplication.instance?.pushToken
 
     private fun isNetworkConnectedForType(connectivityManagerType: Int): Boolean {
         val info = NetworkUtils.getNetworkInfo(mContext)
         return info != null && info.isConnected && info.type == connectivityManagerType
     }
 
-    override fun isConnectedBluetooth(): Boolean {
-        return isNetworkConnectedForType(ConnectivityManager.TYPE_BLUETOOTH)
-    }
+    override val isConnectedWifi: Boolean
+        get() = isNetworkConnectedForType(ConnectivityManager.TYPE_WIFI)
 
-    override fun isConnectedWifi(): Boolean {
-        return isNetworkConnectedForType(ConnectivityManager.TYPE_WIFI)
-    }
+    override val isConnectedBluetooth: Boolean
+        get() = isNetworkConnectedForType(ConnectivityManager.TYPE_BLUETOOTH)
 
-    override fun isConnectedMobile(): Boolean {
-        return isNetworkConnectedForType(ConnectivityManager.TYPE_MOBILE)
-    }
+    override val isConnectedMobile: Boolean
+        get() = isNetworkConnectedForType(ConnectivityManager.TYPE_MOBILE)
 
-    override fun isConnectedEthernet(): Boolean {
-        return isNetworkConnectedForType(ConnectivityManager.TYPE_ETHERNET)
-    }
+    override val isConnectedEthernet: Boolean
+        get() = isNetworkConnectedForType(ConnectivityManager.TYPE_ETHERNET)
 
     override fun hasVideoPermission(): Boolean {
         return checkPermission(Manifest.permission.CAMERA)
@@ -155,20 +141,15 @@ class DeviceRuntimeServiceImpl(
         return checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
     }
 
-    override fun getProfileName(): String? {
-        mContext.contentResolver.query(
-            ContactsContract.Profile.CONTENT_URI,
-            PROFILE_PROJECTION,
-            null,
-            null,
-            null
-        )?.use { cursor ->
-            if (cursor.moveToFirst()) {
-                return cursor.getString(cursor.getColumnIndex(ContactsContract.Profile.DISPLAY_NAME_PRIMARY))
+    override val profileName: String?
+        get() {
+            mContext.contentResolver.query(ContactsContract.Profile.CONTENT_URI, PROFILE_PROJECTION, null, null, null)?.use { cursor ->
+                if (cursor.moveToFirst()) {
+                    return cursor.getString(cursor.getColumnIndex(ContactsContract.Profile.DISPLAY_NAME_PRIMARY))
+                }
             }
+            return null
         }
-        return null
-    }
 
     override fun hardLinkOrCopy(source: File, dest: File): Boolean {
         try {
@@ -181,10 +162,7 @@ class DeviceRuntimeServiceImpl(
     }
 
     private fun checkPermission(permission: String): Boolean {
-        return ContextCompat.checkSelfPermission(
-            mContext,
-            permission
-        ) == PackageManager.PERMISSION_GRANTED
+        return ContextCompat.checkSelfPermission(mContext, permission) == PackageManager.PERMISSION_GRANTED
     }
 
     override fun getHardwareAudioFormat(ret: IntVect) {
diff --git a/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.java b/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.java
deleted file mode 100644
index eeacca004..000000000
--- a/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
- *
- *  Author:     Adrien Béraud <adrien.beraud@savoirfairelinux.com>
- *              Romain Bertozzi <romain.bertozzi@savoirfairelinux.com>
- *
- *  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 cx.ring.settings;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.snackbar.Snackbar;
-
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.widget.CompoundButton;
-import android.widget.Toast;
-
-import cx.ring.R;
-import cx.ring.application.JamiApplication;
-import cx.ring.client.HomeActivity;
-import cx.ring.client.LogsActivity;
-import cx.ring.databinding.FragSettingsBinding;
-
-import net.jami.daemon.JamiService;
-import net.jami.model.Settings;
-import cx.ring.mvp.BaseSupportFragment;
-import dagger.hilt.android.AndroidEntryPoint;
-
-import net.jami.mvp.GenericView;
-import net.jami.settings.SettingsPresenter;
-
-/**
- * TODO: improvements : handle multiples permissions for feature.
- */
-@AndroidEntryPoint
-public class SettingsFragment extends BaseSupportFragment<SettingsPresenter, GenericView<Settings>> implements GenericView<Settings>, ViewTreeObserver.OnScrollChangedListener {
-
-    private static final int SCROLL_DIRECTION_UP = -1;
-
-    public static final int NOTIFICATION_PRIVATE = 0;
-    public static final int NOTIFICATION_PUBLIC = 1;
-    public static final int NOTIFICATION_SECRET = 2;
-
-    private FragSettingsBinding binding;
-    private Settings currentSettings = null;
-
-    private boolean mIsRefreshingViewFromPresenter = true;
-    private int mNotificationVisibility = NOTIFICATION_PRIVATE;
-
-    @Nullable
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
-        binding = FragSettingsBinding.inflate(inflater, container, false);
-        return binding.getRoot();
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        binding = null;
-    }
-
-    @Override
-    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
-        setHasOptionsMenu(true);
-        super.onViewCreated(view, savedInstanceState);
-        binding.settingsDarkTheme.setChecked(presenter.getDarkMode());
-        binding.settingsPluginsSwitch.setChecked(JamiService.getPluginsEnabled());
-        if (TextUtils.isEmpty(JamiApplication.getInstance().getPushToken())) {
-            binding.settingsPushNotificationsLayout.setVisibility(View.GONE);
-        }
-        // loading preferences
-        presenter.loadSettings();
-        ((HomeActivity) getActivity()).setToolbarTitle(R.string.menu_item_settings);
-
-        binding.scrollview.getViewTreeObserver().addOnScrollChangedListener(this);
-        binding.settingsDarkTheme.setOnCheckedChangeListener((buttonView, isChecked) -> presenter.setDarkMode(isChecked));
-        binding.settingsPluginsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> JamiService.setPluginsEnabled(isChecked));
-
-        CompoundButton.OnCheckedChangeListener save = (buttonView, isChecked) -> {
-            if (!mIsRefreshingViewFromPresenter)
-                saveSettings();
-        };
-        binding.settingsPushNotifications.setOnCheckedChangeListener(save);
-        binding.settingsStartup.setOnCheckedChangeListener(save);
-        binding.settingsPersistNotification.setOnCheckedChangeListener(save);
-        binding.settingsTyping.setOnCheckedChangeListener(save);
-        binding.settingsRead.setOnCheckedChangeListener(save);
-        binding.settingsBlockRecord.setOnCheckedChangeListener(save);
-
-        binding.settingsVideoLayout.setOnClickListener(v -> {
-            HomeActivity activity = (HomeActivity) getActivity();
-            if (activity != null)
-                activity.goToVideoSettings();
-        });
-
-        binding.settingsClearHistory.setOnClickListener(v -> new MaterialAlertDialogBuilder(view.getContext())
-                .setTitle(getString(R.string.clear_history_dialog_title))
-                .setMessage(getString(R.string.clear_history_dialog_message))
-                .setPositiveButton(android.R.string.ok, (dialog, id) -> {
-                    // ask the presenter to clear history
-                    presenter.clearHistory();
-                    Snackbar.make(view,
-                            getString(R.string.clear_history_completed),
-                            Snackbar.LENGTH_SHORT).show();
-                })
-                .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
-                    //~ Empty
-                })
-                .show());
-        binding.settingsPluginsLayout.setOnClickListener(v -> {
-            HomeActivity activity = (HomeActivity) getActivity();
-            if (activity != null && JamiService.getPluginsEnabled()){
-                activity.goToPluginsListSettings();
-            }
-        });
-
-        String[] singleItems = {getString(R.string.notification_private), getString(R.string.notification_public), getString(R.string.notification_secret)};
-        final int[] checkedItem = {mNotificationVisibility};
-
-        binding.settingsNotification.setOnClickListener(v -> new MaterialAlertDialogBuilder(view.getContext())
-                .setTitle(getString(R.string.pref_notification_title))
-                .setSingleChoiceItems(singleItems, mNotificationVisibility, (dialogInterface, i) -> checkedItem[0] = i)
-                .setPositiveButton(android.R.string.ok, (dialog, id) -> {
-                    mNotificationVisibility = checkedItem[0];
-                    saveSettings();
-                })
-                .setNegativeButton(android.R.string.cancel, (dialog, id) -> {})
-                .show());
-
-        binding.settingsLogs.setOnClickListener(v -> startActivity(new Intent(v.getContext(), LogsActivity.class)));
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        ((HomeActivity) getActivity()).setToolbarTitle(R.string.menu_item_settings);
-    }
-
-    @Override
-    public void onCreateOptionsMenu(Menu menu, @NonNull MenuInflater inflater) {
-        menu.clear();
-    }
-
-    @Override
-    public void onPrepareOptionsMenu(Menu menu) {
-        menu.clear();
-    }
-
-    private void saveSettings() {
-        Settings newSettings = new Settings(currentSettings);
-        newSettings.setAllowRingOnStartup(binding.settingsStartup.isChecked());
-        newSettings.setAllowPushNotifications(binding.settingsPushNotifications.isChecked());
-        newSettings.setAllowPersistentNotification(binding.settingsPersistNotification.isChecked());
-        newSettings.setAllowPersistentNotification(binding.settingsPersistNotification.isChecked());
-        newSettings.setAllowTypingIndicator(binding.settingsTyping.isChecked());
-        newSettings.setAllowReadIndicator(binding.settingsRead.isChecked());
-        newSettings.setBlockRecordIndicator(binding.settingsBlockRecord.isChecked());
-		newSettings.setNotificationVisibility(mNotificationVisibility);
-
-        // save settings according to UI inputs
-        presenter.saveSettings(newSettings);
-    }
-
-    /**
-     * Presents a Toast explaining why the Read Contacts permission is required to display the devi-
-     * ces contacts in Ring.
-     */
-    private void presentReadContactPermissionExplanationToast() {
-        Activity activity = getActivity();
-        if (null != activity) {
-            String toastMessage = getString(R.string.permission_dialog_read_contacts_message);
-            Toast.makeText(activity, toastMessage, Toast.LENGTH_LONG).show();
-        }
-    }
-
-    /**
-     * Presents a Toast explaining why the Write Call Log permission is required to enable the cor-
-     * responding feature.
-     */
-    private void presentWriteCallLogPermissionExplanationToast() {
-        Activity activity = getActivity();
-        if (null != activity) {
-            String toastMessage = getString(R.string.permission_dialog_write_call_log_message);
-            Toast.makeText(activity, toastMessage, Toast.LENGTH_LONG).show();
-        }
-    }
-
-    @Override
-    public void showViewModel(Settings viewModel) {
-        currentSettings = viewModel;
-        mIsRefreshingViewFromPresenter = true;
-        binding.settingsPushNotifications.setChecked(viewModel.isAllowPushNotifications());
-        binding.settingsPersistNotification.setChecked(viewModel.isAllowPersistentNotification());
-        binding.settingsStartup.setChecked(viewModel.isAllowOnStartup());
-        binding.settingsTyping.setChecked(viewModel.isAllowTypingIndicator());
-        binding.settingsRead.setChecked(viewModel.isAllowReadIndicator());
-        binding.settingsBlockRecord.setChecked(viewModel.isRecordingBlocked());
-        mIsRefreshingViewFromPresenter = false;
-        mNotificationVisibility = viewModel.getNotificationVisibility();
-    }
-
-    @Override
-    public void onScrollChanged() {
-        if (binding != null) {
-            Activity activity = getActivity();
-            if (activity instanceof HomeActivity)
-                ((HomeActivity) activity).setToolbarElevation(binding.scrollview.canScrollVertically(SCROLL_DIRECTION_UP));
-        }
-    }
-
-}
diff --git a/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.kt b/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.kt
new file mode 100644
index 000000000..5e8bfaf17
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.kt
@@ -0,0 +1,212 @@
+/*
+ *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
+ *
+ *  Author:     Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *              Romain Bertozzi <romain.bertozzi@savoirfairelinux.com>
+ *
+ *  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 cx.ring.settings
+
+import android.app.Activity
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.*
+import android.view.ViewTreeObserver.OnScrollChangedListener
+import android.widget.CompoundButton
+import android.widget.Toast
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
+import cx.ring.R
+import cx.ring.application.JamiApplication
+import cx.ring.client.HomeActivity
+import cx.ring.client.LogsActivity
+import cx.ring.databinding.FragSettingsBinding
+import cx.ring.mvp.BaseSupportFragment
+import dagger.hilt.android.AndroidEntryPoint
+import net.jami.daemon.JamiService
+import net.jami.model.Settings
+import net.jami.mvp.GenericView
+import net.jami.settings.SettingsPresenter
+
+@AndroidEntryPoint
+class SettingsFragment : BaseSupportFragment<SettingsPresenter, GenericView<Settings>>(), GenericView<Settings>,
+    OnScrollChangedListener {
+    private var binding: FragSettingsBinding? = null
+    private var currentSettings: Settings? = null
+    private var mIsRefreshingViewFromPresenter = true
+    private var mNotificationVisibility = NOTIFICATION_PRIVATE
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        return FragSettingsBinding.inflate(inflater, container, false).apply {
+            settingsPluginsLayout.setOnClickListener {
+                val activity = activity as HomeActivity?
+                if (activity != null && JamiService.getPluginsEnabled()) {
+                    activity.goToPluginsListSettings()
+                }
+            }
+            scrollview.viewTreeObserver.addOnScrollChangedListener(this@SettingsFragment)
+            settingsDarkTheme.setOnCheckedChangeListener { _, isChecked: Boolean ->
+                presenter.darkMode = isChecked
+            }
+            settingsPluginsSwitch.setOnCheckedChangeListener { _, isChecked: Boolean ->
+                JamiService.setPluginsEnabled(isChecked)
+            }
+            val save = CompoundButton.OnCheckedChangeListener { _, isChecked: Boolean ->
+                if (!mIsRefreshingViewFromPresenter) saveSettings(this)
+            }
+            settingsPushNotifications.setOnCheckedChangeListener(save)
+            settingsStartup.setOnCheckedChangeListener(save)
+            settingsPersistNotification.setOnCheckedChangeListener(save)
+            settingsTyping.setOnCheckedChangeListener(save)
+            settingsRead.setOnCheckedChangeListener(save)
+            settingsBlockRecord.setOnCheckedChangeListener(save)
+            settingsVideoLayout.setOnClickListener {
+                (activity as HomeActivity?)?.goToVideoSettings()
+            }
+            settingsClearHistory.setOnClickListener {
+                MaterialAlertDialogBuilder(inflater.context)
+                    .setTitle(getString(R.string.clear_history_dialog_title))
+                    .setMessage(getString(R.string.clear_history_dialog_message))
+                    .setPositiveButton(android.R.string.ok) { _, _ ->
+                        // ask the presenter to clear history
+                        presenter.clearHistory()
+                        Snackbar.make(root, getString(R.string.clear_history_completed), Snackbar.LENGTH_SHORT).show()
+                    }
+                    .setNegativeButton(android.R.string.cancel) { dialog: DialogInterface?, id: Int -> }
+                    .show()
+            }
+            val singleItems = arrayOf(
+                getString(R.string.notification_private),
+                getString(R.string.notification_public),
+                getString(R.string.notification_secret)
+            )
+            val checkedItem = intArrayOf(mNotificationVisibility)
+            settingsNotification.setOnClickListener { v ->
+                MaterialAlertDialogBuilder(v.context)
+                    .setTitle(getString(R.string.pref_notification_title))
+                    .setSingleChoiceItems(singleItems, mNotificationVisibility) { _, i: Int -> checkedItem[0] = i }
+                    .setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, id: Int ->
+                        mNotificationVisibility = checkedItem[0]
+                        saveSettings(this)
+                    }
+                    .setNegativeButton(android.R.string.cancel) { dialog: DialogInterface?, id: Int -> }
+                    .show()
+            }
+            settingsLogs.setOnClickListener { v: View ->
+                startActivity(Intent(v.context, LogsActivity::class.java))
+            }
+            binding = this
+        }.root
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        binding = null
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        setHasOptionsMenu(true)
+        super.onViewCreated(view, savedInstanceState)
+        binding!!.settingsDarkTheme.isChecked = presenter.darkMode
+        binding!!.settingsPluginsSwitch.isChecked = JamiService.getPluginsEnabled()
+        if (TextUtils.isEmpty(JamiApplication.instance?.pushToken)) {
+            binding!!.settingsPushNotificationsLayout.visibility = View.GONE
+        }
+        // loading preferences
+        presenter.loadSettings()
+        (activity as HomeActivity?)?.setToolbarTitle(R.string.menu_item_settings)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        (activity as HomeActivity?)?.setToolbarTitle(R.string.menu_item_settings)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        menu.clear()
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        menu.clear()
+    }
+
+    private fun saveSettings(binding: FragSettingsBinding) {
+        // save settings according to UI inputs
+        presenter.saveSettings(Settings(currentSettings).apply {
+            setAllowRingOnStartup(binding.settingsStartup.isChecked)
+            isAllowPushNotifications = binding.settingsPushNotifications.isChecked
+            isAllowPersistentNotification = binding.settingsPersistNotification.isChecked
+            isAllowPersistentNotification = binding.settingsPersistNotification.isChecked
+            isAllowTypingIndicator = binding.settingsTyping.isChecked
+            isAllowReadIndicator = binding.settingsRead.isChecked
+            setBlockRecordIndicator(binding.settingsBlockRecord.isChecked)
+            notificationVisibility = mNotificationVisibility
+        })
+    }
+
+    /**
+     * Presents a Toast explaining why the Read Contacts permission is required to display the devi-
+     * ces contacts in Ring.
+     */
+    private fun presentReadContactPermissionExplanationToast() {
+        val activity: Activity? = activity
+        if (null != activity) {
+            val toastMessage = getString(R.string.permission_dialog_read_contacts_message)
+            Toast.makeText(activity, toastMessage, Toast.LENGTH_LONG).show()
+        }
+    }
+
+    /**
+     * Presents a Toast explaining why the Write Call Log permission is required to enable the cor-
+     * responding feature.
+     */
+    private fun presentWriteCallLogPermissionExplanationToast() {
+        val activity: Activity? = activity
+        if (null != activity) {
+            val toastMessage = getString(R.string.permission_dialog_write_call_log_message)
+            Toast.makeText(activity, toastMessage, Toast.LENGTH_LONG).show()
+        }
+    }
+
+    override fun showViewModel(viewModel: Settings) {
+        currentSettings = viewModel
+        mIsRefreshingViewFromPresenter = true
+        binding!!.settingsPushNotifications.isChecked = viewModel.isAllowPushNotifications
+        binding!!.settingsPersistNotification.isChecked = viewModel.isAllowPersistentNotification
+        binding!!.settingsStartup.isChecked = viewModel.isAllowOnStartup
+        binding!!.settingsTyping.isChecked = viewModel.isAllowTypingIndicator
+        binding!!.settingsRead.isChecked = viewModel.isAllowReadIndicator
+        binding!!.settingsBlockRecord.isChecked = viewModel.isRecordingBlocked
+        mIsRefreshingViewFromPresenter = false
+        mNotificationVisibility = viewModel.notificationVisibility
+    }
+
+    override fun onScrollChanged() {
+        binding?.let { binding ->
+            val activity: Activity? = activity
+            if (activity is HomeActivity)
+                activity.setToolbarElevation(binding.scrollview.canScrollVertically(SCROLL_DIRECTION_UP))
+        }
+    }
+
+    companion object {
+        private const val SCROLL_DIRECTION_UP = -1
+        const val NOTIFICATION_PRIVATE = 0
+        const val NOTIFICATION_PUBLIC = 1
+        const val NOTIFICATION_SECRET = 2
+    }
+}
\ No newline at end of file
diff --git a/ring-android/app/src/main/java/cx/ring/tv/account/TVAccountWizard.kt b/ring-android/app/src/main/java/cx/ring/tv/account/TVAccountWizard.kt
index 3dc1262c2..00a9cb5c3 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/account/TVAccountWizard.kt
+++ b/ring-android/app/src/main/java/cx/ring/tv/account/TVAccountWizard.kt
@@ -26,9 +26,8 @@ import androidx.appcompat.app.AlertDialog
 import androidx.leanback.app.GuidedStepSupportFragment
 import cx.ring.R
 import cx.ring.account.AccountCreationModelImpl
-import cx.ring.application.JamiApplication.Companion.instance
+import cx.ring.application.JamiApplication
 import cx.ring.mvp.BaseActivity
-import cx.ring.tv.account.TVProfileCreationFragment.Companion.newInstance
 import dagger.hilt.android.AndroidEntryPoint
 import ezvcard.VCard
 import io.reactivex.rxjava3.core.Single
@@ -49,7 +48,7 @@ class TVAccountWizard : BaseActivity<AccountWizardPresenter?>(), AccountWizardVi
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        instance?.startDaemon()
+        JamiApplication.instance?.startDaemon()
         val intent = intent
         if (intent != null) {
             mAccountType = intent.action
@@ -100,7 +99,7 @@ class TVAccountWizard : BaseActivity<AccountWizardPresenter?>(), AccountWizardVi
     override fun goToProfileCreation(accountCreationModel: AccountCreationModel) {
         GuidedStepSupportFragment.add(
             supportFragmentManager,
-            newInstance(accountCreationModel as AccountCreationModelImpl)
+            TVProfileCreationFragment.newInstance(accountCreationModel as AccountCreationModelImpl)
         )
     }
 
diff --git a/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java b/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java
deleted file mode 100644
index 61368718e..000000000
--- a/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
- *
- *  Author: Michel Schmit <michel.schmit@savoirfairelinux.com>
- *  Author: Aline Bonnet <aline.bonnet@savoirfairelinux.com>
- *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
- *  Author: AmirHossein Naghshzan <amirhossein.naghshzan@savoirfairelinux.com>
- *
- *  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 cx.ring.tv.main;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.hardware.Camera;
-import android.hardware.camera2.CameraManager;
-import android.os.Bundle;
-import android.renderscript.Allocation;
-import android.renderscript.Element;
-import android.renderscript.RenderScript;
-import android.renderscript.ScriptIntrinsicBlur;
-import android.renderscript.ScriptIntrinsicYuvToRGB;
-import android.renderscript.Type;
-import android.util.Log;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.FragmentActivity;
-import androidx.leanback.app.BackgroundManager;
-import androidx.leanback.app.GuidedStepSupportFragment;
-
-import net.jami.services.AccountService;
-
-import javax.inject.Inject;
-
-import cx.ring.R;
-import cx.ring.application.JamiApplication;
-import cx.ring.tv.account.TVAccountWizard;
-import dagger.hilt.android.AndroidEntryPoint;
-import cx.ring.tv.camera.CameraPreview;
-import cx.ring.tv.contact.TVContactFragment;
-import io.reactivex.rxjava3.core.Single;
-import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
-import io.reactivex.rxjava3.disposables.CompositeDisposable;
-import io.reactivex.rxjava3.schedulers.Schedulers;
-
-@AndroidEntryPoint
-public class HomeActivity extends FragmentActivity {
-
-    private static final String TAG = HomeActivity.class.getSimpleName();
-
-    private static final float BITMAP_SCALE = 0.4f;
-    private static final float BLUR_RADIUS = 7.5f;
-
-    private final CompositeDisposable mDisposableBag = new CompositeDisposable();
-
-    @Inject
-    AccountService mAccountService;
-
-    private BackgroundManager mBackgroundManager;
-    private ImageView mBlurImage;
-    private FrameLayout mPreviewView;
-    private View mFadeView;
-    private Camera mCamera;
-    private CameraPreview mCameraPreview;
-    private CameraManager mCameraManager;
-
-    private Bitmap mBlurOutputBitmap;
-    private RenderScript rs;
-    private ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic;
-    private ScriptIntrinsicBlur blurIntrinsic;
-    private Allocation in, out, mBlurOut;
-
-    private final Camera.ErrorCallback mErrorCallback = new Camera.ErrorCallback() {
-        @Override
-        public void onError(int error, Camera camera) {
-            mBlurImage.setVisibility(View.INVISIBLE);
-            mBackgroundManager.setDrawable(ContextCompat.getDrawable(HomeActivity.this, R.drawable.tv_background));
-        }
-    };
-
-    private final Object mCameraAvailabilityCallback = new CameraManager.AvailabilityCallback() {
-        @Override
-        public void onCameraAvailable(String cameraId) {
-            if (mBlurImage.getVisibility() == View.INVISIBLE) {
-                setUpCamera();
-            }
-        }
-    };
-
-    private final Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
-        @Override
-        public void onPreviewFrame(byte[] data, Camera camera) {
-            if (getSupportFragmentManager().findFragmentByTag(TVContactFragment.TAG) != null) {
-                mBlurImage.setVisibility(View.GONE);
-                mFadeView.setVisibility(View.GONE);
-                mPreviewView.setVisibility(View.VISIBLE);
-                return;
-            }
-            if (mBlurOutputBitmap == null) {
-                Camera.Size size = camera.getParameters().getPreviewSize();
-                rs = RenderScript.create(HomeActivity.this);
-                Type yuvType = new Type.Builder(rs, Element.U8(rs)).setX(data.length).create();
-                in = Allocation.createTyped(rs, yuvType, Allocation.USAGE_SCRIPT);
-                yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));
-                yuvToRgbIntrinsic.setInput(in);
-                Type rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(size.width).setY(size.height).create();
-                out = Allocation.createTyped(rs, rgbaType, Allocation.USAGE_SCRIPT);
-                blurIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
-                blurIntrinsic.setRadius(BLUR_RADIUS * size.width / 1080);
-                blurIntrinsic.setInput(out);
-                mBlurOutputBitmap = Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888);
-                mBlurOut = Allocation.createFromBitmap(rs, mBlurOutputBitmap);
-            }
-            in.copyFrom(data);
-            yuvToRgbIntrinsic.forEach(out);
-            blurIntrinsic.forEach(mBlurOut);
-            mBlurOut.copyTo(mBlurOutputBitmap);
-            mBlurImage.setImageBitmap(mBlurOutputBitmap);
-            if (mBlurImage.getVisibility() == View.GONE) {
-                mPreviewView.setVisibility(View.INVISIBLE);
-                mBlurImage.setVisibility(View.VISIBLE);
-                mFadeView.setVisibility(View.VISIBLE);
-            }
-        }
-    };
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        JamiApplication.getInstance().startDaemon();
-        setContentView(R.layout.tv_activity_home);
-        mBackgroundManager = BackgroundManager.getInstance(this);
-        mBackgroundManager.attach(getWindow());
-        mPreviewView = findViewById(R.id.previewView);
-        mBlurImage = findViewById(R.id.blur);
-        mFadeView = findViewById(R.id.fade);
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (GuidedStepSupportFragment.getCurrentGuidedStepSupportFragment(getSupportFragmentManager()) != null) {
-//            mIsContactFragmentVisible = false;
-            getSupportFragmentManager().popBackStack();
-        } else {
-            super.onBackPressed();
-        }
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        //mDisposable.clear();
-        mDisposableBag.add(mAccountService.getObservableAccountList()
-                .observeOn(AndroidSchedulers.mainThread())
-                .firstElement()
-                .subscribe(accounts -> {
-                    if (accounts.isEmpty()) {
-                        startActivity(new Intent(this, TVAccountWizard.class));
-                    }
-                }));
-    }
-
-    @Override
-    protected void onPostResume() {
-        super.onPostResume();
-        setUpCamera();
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-        if (mCameraPreview != null) {
-            mCamera.setPreviewCallback(null);
-            mCameraPreview.stop();
-            mCameraPreview = null;
-        }
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        mDisposableBag.dispose();
-        if (mCameraManager != null) {
-            mCameraManager.unregisterAvailabilityCallback((CameraManager.AvailabilityCallback) mCameraAvailabilityCallback);
-        }
-        if (mCamera != null) {
-            mCamera.release();
-            mCamera = null;
-        }
-        if (mBlurOutputBitmap != null){
-            in.destroy();
-            in = null;
-            out.destroy();
-            out = null;
-            blurIntrinsic.destroy();
-            blurIntrinsic = null;
-            mBlurOut.destroy();
-            mBlurOut = null;
-            yuvToRgbIntrinsic.destroy();
-            yuvToRgbIntrinsic = null;
-            mBlurOutputBitmap.recycle();
-            mBlurOutputBitmap = null;
-            rs.destroy();
-            rs = null;
-        }
-    }
-
-    private Camera getCameraInstance() {
-        try {
-            int currentCamera = 0;
-            mCamera = Camera.open(currentCamera);
-        }
-        catch (RuntimeException e) {
-            Log.e(TAG, "failed to open camera");
-        }
-        return mCamera;
-    }
-
-    private void setUpCamera() {
-        mDisposableBag.add(Single.fromCallable(this::getCameraInstance)
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(camera -> {
-                    Log.w(TAG, "setUpCamera()");
-                    Camera.Parameters params = camera.getParameters();
-                    Camera.Size selectSize = null;
-                    for (Camera.Size size :  params.getSupportedPictureSizes()) {
-                        if (size.width == 1280 && size.height == 720) {
-                            selectSize = size;
-                            break;
-                        }
-                    }
-                    if (selectSize == null)
-                        throw new IllegalStateException("No supported size");
-                    Log.w(TAG, "setUpCamera() selectSize " + selectSize.width + "x" + selectSize.height);
-                    params.setPictureSize(selectSize.width, selectSize.height);
-                    params.setPreviewSize(selectSize.width, selectSize.height);
-                    camera.setParameters(params);
-                    mBlurImage.setVisibility(View.VISIBLE);
-                    if (mCameraManager == null) {
-                        mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
-                        mCameraManager.registerAvailabilityCallback((CameraManager.AvailabilityCallback) mCameraAvailabilityCallback, null);
-                    }
-                    mCameraPreview = new CameraPreview(this, camera);
-                    mPreviewView.removeAllViews();
-                    mPreviewView.addView(mCameraPreview, 0);
-                    camera.setErrorCallback(mErrorCallback);
-                    camera.setPreviewCallback(mPreviewCallback);
-                }, e -> mBackgroundManager.setDrawable(ContextCompat.getDrawable(HomeActivity.this, R.drawable.tv_background))));
-    }
-
-}
\ No newline at end of file
diff --git a/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.kt b/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.kt
new file mode 100644
index 000000000..31543355a
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/tv/main/HomeActivity.kt
@@ -0,0 +1,243 @@
+/*
+ *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Michel Schmit <michel.schmit@savoirfairelinux.com>
+ *  Author: Aline Bonnet <aline.bonnet@savoirfairelinux.com>
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *  Author: AmirHossein Naghshzan <amirhossein.naghshzan@savoirfairelinux.com>
+ *
+ *  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 cx.ring.tv.main
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.hardware.Camera
+import android.hardware.Camera.ErrorCallback
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CameraManager.AvailabilityCallback
+import android.os.Bundle
+import android.renderscript.*
+import android.util.Log
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+import androidx.leanback.app.BackgroundManager
+import androidx.leanback.app.GuidedStepSupportFragment
+import cx.ring.R
+import cx.ring.application.JamiApplication
+import cx.ring.tv.account.TVAccountWizard
+import cx.ring.tv.camera.CameraPreview
+import cx.ring.tv.contact.TVContactFragment
+import dagger.hilt.android.AndroidEntryPoint
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.core.Single
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import io.reactivex.rxjava3.schedulers.Schedulers
+import net.jami.model.Account
+import net.jami.services.AccountService
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class HomeActivity : FragmentActivity() {
+    private val mDisposableBag = CompositeDisposable()
+
+    @Inject
+    lateinit var mAccountService: AccountService
+    private lateinit var mBackgroundManager: BackgroundManager
+    private lateinit var mBlurImage: ImageView
+    private lateinit var mPreviewView: FrameLayout
+    private lateinit var mFadeView: View
+    private var mCamera: Camera? = null
+    private var mCameraPreview: CameraPreview? = null
+    private var mCameraManager: CameraManager? = null
+    private var mBlurOutputBitmap: Bitmap? = null
+    private var rs: RenderScript? = null
+    private var yuvToRgbIntrinsic: ScriptIntrinsicYuvToRGB? = null
+    private var blurIntrinsic: ScriptIntrinsicBlur? = null
+    private var input: Allocation? = null
+    private var out: Allocation? = null
+    private var mBlurOut: Allocation? = null
+
+    private val mErrorCallback = ErrorCallback { error, camera ->
+        mBlurImage.visibility = View.INVISIBLE
+        mBackgroundManager.drawable = ContextCompat.getDrawable(this@HomeActivity, R.drawable.tv_background)
+    }
+    private val mCameraAvailabilityCallback: AvailabilityCallback = object : AvailabilityCallback() {
+        override fun onCameraAvailable(cameraId: String) {
+            if (mBlurImage.visibility == View.INVISIBLE) {
+                setUpCamera()
+            }
+        }
+    }
+    private val mPreviewCallback = Camera.PreviewCallback { data, camera ->
+        if (supportFragmentManager.findFragmentByTag(TVContactFragment.TAG) != null) {
+            mBlurImage.visibility = View.GONE
+            mFadeView.visibility = View.GONE
+            mPreviewView.visibility = View.VISIBLE
+            return@PreviewCallback
+        }
+        if (mBlurOutputBitmap == null) {
+            val size = camera.parameters.previewSize
+            rs = RenderScript.create(this@HomeActivity)
+            val yuvType = Type.Builder(rs, Element.U8(rs)).setX(data.size).create()
+            input = Allocation.createTyped(rs, yuvType, Allocation.USAGE_SCRIPT)
+            yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)).apply { setInput(input) }
+            val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs)).setX(size.width).setY(size.height).create()
+            out = Allocation.createTyped(rs, rgbaType, Allocation.USAGE_SCRIPT)
+            blurIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)).apply {
+                setRadius(BLUR_RADIUS * size.width / 1080)
+                setInput(input)
+            }
+            mBlurOutputBitmap = Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888)
+            mBlurOut = Allocation.createFromBitmap(rs, mBlurOutputBitmap)
+        }
+        input!!.copyFrom(data)
+        yuvToRgbIntrinsic!!.forEach(out)
+        blurIntrinsic!!.forEach(mBlurOut)
+        mBlurOut!!.copyTo(mBlurOutputBitmap)
+        mBlurImage.setImageBitmap(mBlurOutputBitmap)
+        if (mBlurImage.visibility == View.GONE) {
+            mPreviewView.visibility = View.INVISIBLE
+            mBlurImage.visibility = View.VISIBLE
+            mFadeView.visibility = View.VISIBLE
+        }
+    }
+
+    public override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        JamiApplication.instance!!.startDaemon()
+        setContentView(R.layout.tv_activity_home)
+        mBackgroundManager = BackgroundManager.getInstance(this).apply { attach(window) }
+        mPreviewView = findViewById(R.id.previewView)
+        mBlurImage = findViewById(R.id.blur)
+        mFadeView = findViewById(R.id.fade)
+    }
+
+    override fun onBackPressed() {
+        if (GuidedStepSupportFragment.getCurrentGuidedStepSupportFragment(supportFragmentManager) != null) {
+//            mIsContactFragmentVisible = false;
+            supportFragmentManager.popBackStack()
+        } else {
+            super.onBackPressed()
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        //mDisposable.clear();
+        mDisposableBag.add(
+            mAccountService!!.observableAccountList
+                .observeOn(AndroidSchedulers.mainThread())
+                .firstElement()
+                .subscribe { accounts: List<Account?> ->
+                    if (accounts.isEmpty()) {
+                        startActivity(Intent(this, TVAccountWizard::class.java))
+                    }
+                })
+    }
+
+    override fun onPostResume() {
+        super.onPostResume()
+        setUpCamera()
+    }
+
+    override fun onPause() {
+        super.onPause()
+        if (mCameraPreview != null) {
+            mCamera!!.setPreviewCallback(null)
+            mCameraPreview!!.stop()
+            mCameraPreview = null
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        mDisposableBag.dispose()
+        mCameraManager?.unregisterAvailabilityCallback(mCameraAvailabilityCallback)
+        mCamera?.let { camera ->
+            camera.release();
+            mCamera = null
+        }
+        if (mBlurOutputBitmap != null) {
+            input!!.destroy()
+            input = null
+            out!!.destroy()
+            out = null
+            blurIntrinsic!!.destroy()
+            blurIntrinsic = null
+            mBlurOut!!.destroy()
+            mBlurOut = null
+            yuvToRgbIntrinsic!!.destroy()
+            yuvToRgbIntrinsic = null
+            mBlurOutputBitmap!!.recycle()
+            mBlurOutputBitmap = null
+            rs!!.destroy()
+            rs = null
+        }
+    }
+
+    private val cameraInstance: Camera
+        private get() {
+            try {
+                val currentCamera = 0
+                mCamera = Camera.open(currentCamera)
+            } catch (e: RuntimeException) {
+                Log.e(TAG, "failed to open camera")
+            }
+            return mCamera!!
+        }
+
+    private fun setUpCamera() {
+        mDisposableBag.add(Single.fromCallable { cameraInstance }
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe({ camera: Camera ->
+                Log.w(TAG, "setUpCamera()")
+                val params = camera.parameters
+                var selectSize: Camera.Size? = null
+                for (size in params.supportedPictureSizes) {
+                    if (size.width == 1280 && size.height == 720) {
+                        selectSize = size
+                        break
+                    }
+                }
+                checkNotNull(selectSize) { "No supported size" }
+                Log.w(TAG, "setUpCamera() selectSize " + selectSize.width + "x" + selectSize.height)
+                params.setPictureSize(selectSize.width, selectSize.height)
+                params.setPreviewSize(selectSize.width, selectSize.height)
+                camera.parameters = params
+                mBlurImage.visibility = View.VISIBLE
+                if (mCameraManager == null) {
+                    mCameraManager = (getSystemService(CAMERA_SERVICE) as CameraManager).apply {
+                        registerAvailabilityCallback((mCameraAvailabilityCallback), null)
+                    }
+                }
+                mCameraPreview = CameraPreview(this, camera)
+                mPreviewView.removeAllViews()
+                mPreviewView.addView(mCameraPreview, 0)
+                camera.setErrorCallback(mErrorCallback)
+                camera.setPreviewCallback(mPreviewCallback)
+            }) {
+                mBackgroundManager.drawable = ContextCompat.getDrawable(this@HomeActivity, R.drawable.tv_background)
+            })
+    }
+
+    companion object {
+        private val TAG = HomeActivity::class.simpleName!!
+        private const val BLUR_RADIUS = 7.5f
+    }
+}
\ No newline at end of file
diff --git a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.kt b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.kt
index 94adcdb0e..ff6f2339b 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.kt
+++ b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.kt
@@ -368,7 +368,7 @@ object AndroidFileUtils {
         return File(context.cacheDir, filename)
     }
 
-    fun getFilePath(context: Context, filename: String?): File {
+    fun getFilePath(context: Context, filename: String): File {
         return context.getFileStreamPath(filename)
     }
 
diff --git a/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.kt b/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.kt
index da0acdd51..afe93d983 100644
--- a/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.kt
+++ b/ring-android/libringclient/src/main/java/net/jami/call/CallPresenter.kt
@@ -49,8 +49,9 @@ class CallPresenter @Inject constructor(
     private val mHardwareService: HardwareService,
     private val mCallService: CallService,
     private val mDeviceRuntimeService: DeviceRuntimeService,
-    private val mConversationFacade: ConversationFacade
-) : RootPresenter<CallView>() {
+    private val mConversationFacade: ConversationFacade,
+    @param:Named("UiScheduler") private val mUiScheduler: Scheduler
+    ) : RootPresenter<CallView>() {
     private var mConference: Conference? = null
     private val mPendingCalls: MutableList<Call> = ArrayList()
     private val mPendingSubject: Subject<List<Call>> = BehaviorSubject.createDefault(mPendingCalls)
@@ -70,10 +71,6 @@ class CallPresenter @Inject constructor(
     private var currentPluginSurfaceId: String? = null
     private var timeUpdateTask: Disposable? = null
 
-    @Inject
-    @Named("UiScheduler")
-    lateinit var mUiScheduler: Scheduler
-
     fun cameraPermissionChanged(isGranted: Boolean) {
         if (isGranted && mHardwareService.isVideoAvailable) {
             mHardwareService.initVideo()
@@ -201,14 +198,14 @@ class CallPresenter @Inject constructor(
             .switchMap { obj: Conference -> obj.participantInfo }
             .observeOn(mUiScheduler)
             .subscribe(
-                { info: List<ParticipantInfo>? -> view!!.updateConfInfo(info) }
-            ) { e: Throwable? -> Log.e(TAG, "Error with initIncoming, action view flow: ", e) })
+                { info: List<ParticipantInfo> -> view?.updateConfInfo(info) }
+            ) { e: Throwable -> Log.e(TAG, "Error with initIncoming, action view flow: ", e) })
         mCompositeDisposable.add(conference
             .switchMap { obj: Conference -> obj.participantRecording }
             .observeOn(mUiScheduler)
             .subscribe(
-                { contacts: Set<Contact>? -> view!!.updateParticipantRecording(contacts) }
-            ) { e: Throwable? -> Log.e(TAG, "Error with initIncoming, action view flow: ", e) })
+                { contacts: Set<Contact> -> view?.updateParticipantRecording(contacts) }
+            ) { e: Throwable -> Log.e(TAG, "Error with initIncoming, action view flow: ", e) })
     }
 
     fun prepareOptionMenu() {
diff --git a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.kt b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.kt
index 42e68480d..f36d6ba98 100644
--- a/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.kt
+++ b/ring-android/libringclient/src/main/java/net/jami/conversation/ConversationPresenter.kt
@@ -293,7 +293,7 @@ class ConversationPresenter @Inject constructor(
     }
 
     fun deleteConversationItem(element: Interaction) {
-        mConversationFacade.deleteConversationItem(mConversation, element)
+        mConversationFacade.deleteConversationItem(mConversation!!, element)
     }
 
     fun cancelMessage(message: Interaction) {
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/AccountService.kt b/ring-android/libringclient/src/main/java/net/jami/services/AccountService.kt
index 297df6be4..31c70d754 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/AccountService.kt
+++ b/ring-android/libringclient/src/main/java/net/jami/services/AccountService.kt
@@ -782,21 +782,15 @@ class AccountService(
     }
 
     fun validateCertificatePath(
-        accountID: String?,
-        certificatePath: String?,
-        privateKeyPath: String?,
-        privateKeyPass: String?
+        accountID: String,
+        certificatePath: String,
+        privateKeyPath: String,
+        privateKeyPass: String
     ): Map<String, String>? {
         try {
             return mExecutor.submit<HashMap<String, String>> {
                 Log.i(TAG, "validateCertificatePath() running...")
-                JamiService.validateCertificatePath(
-                    accountID,
-                    certificatePath,
-                    privateKeyPath,
-                    privateKeyPass,
-                    ""
-                ).toNative()
+                JamiService.validateCertificatePath(accountID, certificatePath, privateKeyPath, privateKeyPass, "").toNative()
             }.get()
         } catch (e: Exception) {
             Log.e(TAG, "Error running validateCertificatePath()", e)
@@ -804,7 +798,7 @@ class AccountService(
         return null
     }
 
-    fun validateCertificate(accountId: String?, certificate: String?): Map<String, String>? {
+    fun validateCertificate(accountId: String, certificate: String): Map<String, String>? {
         try {
             return mExecutor.submit<HashMap<String, String>> {
                 Log.i(TAG, "validateCertificate() running...")
@@ -816,7 +810,7 @@ class AccountService(
         return null
     }
 
-    fun getCertificateDetailsPath(certificatePath: String?): Map<String, String>? {
+    fun getCertificateDetailsPath(certificatePath: String): Map<String, String>? {
         try {
             return mExecutor.submit<HashMap<String, String>> {
                 Log.i(TAG, "getCertificateDetailsPath() running...")
@@ -828,7 +822,7 @@ class AccountService(
         return null
     }
 
-    fun getCertificateDetails(certificateRaw: String?): Map<String, String>? {
+    fun getCertificateDetails(certificateRaw: String): Map<String, String>? {
         try {
             return mExecutor.submit<HashMap<String, String>> {
                 Log.i(TAG, "getCertificateDetails() running...")
@@ -852,7 +846,7 @@ class AccountService(
     /**
      * @return the account's credentials from the Daemon
      */
-    fun getCredentials(accountId: String?): List<Map<String, String>>? {
+    fun getCredentials(accountId: String): List<Map<String, String>>? {
         try {
             return mExecutor.submit<ArrayList<Map<String, String>>> {
                 Log.i(TAG, "getCredentials() running...")
@@ -1397,8 +1391,8 @@ class AccountService(
         interaction.setSwarmInfo(conversation.uri.rawRingId, id, if (StringUtils.isEmpty(parent)) null else parent)
         interaction.conversation = conversation
         if (conversation.addSwarmElement(interaction)) {
-            if (conversation.isVisible)
-                mHistoryService.setMessageRead(account.accountID, conversation.uri, interaction.messageId)
+            /*if (conversation.isVisible)
+                mHistoryService.setMessageRead(account.accountID, conversation.uri, interaction.messageId!!)*/
         }
         return interaction
     }
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/ConversationFacade.kt b/ring-android/libringclient/src/main/java/net/jami/services/ConversationFacade.kt
index 6c369270a..10c4948f5 100644
--- a/ring-android/libringclient/src/main/java/net/jami/services/ConversationFacade.kt
+++ b/ring-android/libringclient/src/main/java/net/jami/services/ConversationFacade.kt
@@ -75,25 +75,21 @@ class ConversationFacade(
             .switchMap { obj: Account -> obj.getConversationsSubject() }
 
     fun readMessages(accountId: String, contact: Uri): String? {
-        val account = mAccountService.getAccount(accountId)
-        return if (account != null) readMessages(account, account.getByUri(contact), true) else null
+        val account = mAccountService.getAccount(accountId) ?: return null
+        val conversation  = account.getByUri(contact) ?: return null
+        return readMessages(account, conversation, true)
     }
 
-    fun readMessages(account: Account, conversation: Conversation?, cancelNotification: Boolean): String? {
-        if (conversation != null) {
-            val lastMessage = readMessages(conversation)
-            if (lastMessage != null) {
-                account.refreshed(conversation)
-                if (mPreferencesService.settings.isAllowReadIndicator) {
-                    mAccountService.setMessageDisplayed(account.accountID, conversation.uri, lastMessage)
-                }
-                if (cancelNotification) {
-                    mNotificationService.cancelTextNotification(account.accountID, conversation.uri)
-                }
-            }
-            return lastMessage
+    fun readMessages(account: Account, conversation: Conversation, cancelNotification: Boolean): String? {
+        val lastMessage = readMessages(conversation) ?: return null
+        account.refreshed(conversation)
+        if (mPreferencesService.settings.isAllowReadIndicator) {
+            mAccountService.setMessageDisplayed(account.accountID, conversation.uri, lastMessage)
         }
-        return null
+        if (cancelNotification) {
+            mNotificationService.cancelTextNotification(account.accountID, conversation.uri)
+        }
+        return lastMessage
     }
 
     private fun readMessages(conversation: Conversation): String? {
@@ -182,33 +178,31 @@ class ConversationFacade(
             .subscribeOn(Schedulers.io())
     }
 
-    fun deleteConversationItem(conversation: Conversation?, element: Interaction) {
+    fun deleteConversationItem(conversation: Conversation, element: Interaction) {
         if (element.type === Interaction.InteractionType.DATA_TRANSFER) {
             val transfer = element as DataTransfer
             if (transfer.status === InteractionStatus.TRANSFER_ONGOING) {
                 mAccountService.cancelDataTransfer(
-                    conversation!!.accountId,
+                    conversation.accountId,
                     conversation.uri.rawRingId,
                     transfer.messageId,
                     transfer.fileId!!
                 )
             } else {
-                val file = mDeviceRuntimeService.getConversationPath(conversation!!.uri.rawRingId, transfer.storagePath)
+                val file = mDeviceRuntimeService.getConversationPath(conversation.uri.rawRingId, transfer.storagePath)
                 mDisposableBag.add(Completable.mergeArrayDelayError(
-                    mHistoryService.deleteInteraction(element.id, element.account),
+                    mHistoryService.deleteInteraction(element.id, element.account!!),
                     Completable.fromAction { file.delete() }
                         .subscribeOn(Schedulers.io()))
-                    .subscribe(
-                        { conversation.removeInteraction(transfer) }
-                    ) { e: Throwable? -> Log.e(TAG, "Can't delete file transfer", e) })
+                    .subscribe({ conversation.removeInteraction(transfer) })
+                        { e: Throwable -> Log.e(TAG, "Can't delete file transfer", e) })
             }
         } else {
             // handling is the same for calls and texts
-            mDisposableBag.add(mHistoryService.deleteInteraction(element.id, element.account)
+            mDisposableBag.add(mHistoryService.deleteInteraction(element.id, element.account!!)
                 .subscribeOn(Schedulers.io())
-                .subscribe(
-                    { conversation!!.removeInteraction(element) }
-                ) { e: Throwable? -> Log.e(TAG, "Can't delete message", e) })
+                .subscribe({ conversation.removeInteraction(element) })
+                    { e: Throwable -> Log.e(TAG, "Can't delete message", e) })
         }
     }
 
@@ -546,7 +540,7 @@ class ConversationFacade(
             if (newState === CallStatus.HUNGUP || call.timestampEnd == 0L) {
                 call.timestampEnd = now
             }
-            if (conference != null && conference.removeParticipant(call) && !conversation!!.isSwarm) {
+            if (conference != null && conference.removeParticipant(call) && conversation != null && !conversation.isSwarm) {
                 Log.w(TAG, "Adding call history for conversation " + conversation.uri)
                 mHistoryService.insertInteraction(account.accountID, conversation, call).subscribe()
                 conversation.addCall(call)
@@ -556,34 +550,26 @@ class ConversationFacade(
                 account.updated(conversation)
             }
             mCallService.removeCallForId(call.daemonIdString!!)
-            if (conversation != null && conference!!.participants.isEmpty()) {
+            if (conversation != null && conference != null && conference.participants.isEmpty()) {
                 conversation.removeConference(conference)
             }
         }
     }
 
-    fun placeCall(accountId: String, contactUri: Uri, video: Boolean): Single<Call> {
-        //String rawId = contactUri.getRawRingId();
-        return getAccountSubject(accountId).flatMap { account: Account ->
-            mCallService.placeCall(accountId, null, contactUri, video) }
-    }
-
     fun cancelFileTransfer(accountId: String, conversationId: Uri, messageId: String?, fileId: String?) {
-        mAccountService.cancelDataTransfer(accountId,
-            if (conversationId.isSwarm) conversationId.rawRingId else "",
-            messageId, fileId!!)
+        mAccountService.cancelDataTransfer(accountId, if (conversationId.isSwarm) conversationId.rawRingId else "", messageId, fileId!!)
         mNotificationService.removeTransferNotification(accountId, conversationId, fileId)
-        val transfer = mAccountService.getAccount(accountId)?.getDataTransfer(fileId)
-        if (transfer != null)
-            deleteConversationItem(transfer.conversation as Conversation?, transfer)
+        mAccountService.getAccount(accountId)?.getDataTransfer(fileId)?.let { transfer ->
+            deleteConversationItem(transfer.conversation as Conversation, transfer)
+        }
     }
 
     fun removeConversation(accountId: String, conversationUri: Uri): Completable {
+        val account = mAccountService.getAccount(accountId) ?: return Completable.error(IllegalArgumentException("Unknown account"))
         return if (conversationUri.isSwarm) {
             // For a one to one conversation, contact is strongly related, so remove the contact.
             // This will remove related conversations
-            val account = mAccountService.getAccount(accountId)
-            val conversation = account!!.getSwarm(conversationUri.rawRingId)
+            val conversation = account.getSwarm(conversationUri.rawRingId)
             if (conversation != null && conversation.mode.blockingFirst() === Conversation.Mode.OneToOne) {
                 val contact = conversation.contact
                 mAccountService.removeContact(accountId, contact!!.uri.rawRingId, false)
@@ -595,7 +581,7 @@ class ConversationFacade(
             mHistoryService
                 .clearHistory(conversationUri.uri, accountId, true)
                 .doOnSubscribe {
-                    mAccountService.getAccount(accountId)?.clearHistory(conversationUri, true)
+                    account.clearHistory(conversationUri, true)
                     mAccountService.removeContact(accountId, conversationUri.rawRingId, false)
                 }
         }
@@ -642,9 +628,8 @@ class ConversationFacade(
             .subscribe())
         mDisposableBag.add(mAccountService.incomingRequests
             .concatMapSingle { r: TrustRequest -> getAccountSubject(r.accountId) }
-            .subscribe(
-                { account: Account? -> mNotificationService.showIncomingTrustRequestNotification(account) }
-            ) { Log.e(TAG, "Error showing contact request") })
+            .subscribe({ account: Account -> mNotificationService.showIncomingTrustRequestNotification(account) })
+                { Log.e(TAG, "Error showing contact request") })
         mDisposableBag.add(mAccountService
             .incomingMessages
             .concatMapSingle { msg: TextMessage ->
@@ -653,8 +638,8 @@ class ConversationFacade(
                         a.addTextMessage(msg)
                         msg }
             }
-            .subscribe({ txt: TextMessage -> parseNewMessage(txt) }
-            ) { e: Throwable -> Log.e(TAG, "Error adding text message", e) })
+            .subscribe({ txt: TextMessage -> parseNewMessage(txt) })
+                { e: Throwable -> Log.e(TAG, "Error adding text message", e) })
         mDisposableBag.add(mAccountService.incomingSwarmMessages
                 .subscribe({ txt: TextMessage -> parseNewMessage(txt) },
                     { e: Throwable -> Log.e(TAG, "Error adding text message", e) }))
@@ -663,8 +648,7 @@ class ConversationFacade(
                 getAccountSubject(location.account)
                     .map { a: Account ->
                         val expiration = a.onLocationUpdate(location)
-                        mDisposableBag.add(
-                            Completable.timer(expiration, TimeUnit.MILLISECONDS)
+                        mDisposableBag.add(Completable.timer(expiration, TimeUnit.MILLISECONDS)
                                 .subscribe { a.maintainLocation() })
                         location
                     } }
@@ -688,11 +672,11 @@ class ConversationFacade(
                 getAccountSubject(e.account!!)
                     .map { a: Account ->
                         if (e.conversation == null)
-                            a.getSwarm(e.conversationId!!)
+                            a.getSwarm(e.conversationId!!)!!
                         else
-                            a.getByUri(e.conversation!!.participant)
+                            a.getByUri(e.conversation!!.participant)!!
                     }
-                    .doOnSuccess { conversation: Conversation? -> conversation!!.updateInteraction(e) }
+                    .doOnSuccess { conversation -> conversation.updateInteraction(e) }
             }
             .subscribe({}) { e: Throwable -> Log.e(TAG, "Error updating text message", e) })
         mDisposableBag.add(mAccountService.dataTransfers
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
deleted file mode 100644
index c6d5aafc5..000000000
--- a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
- *
- *  Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
- *
- *  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, write to the Free Software
- *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
- */
-package net.jami.services;
-
-import net.jami.model.Conversation;
-import net.jami.model.DataTransfer;
-
-import java.io.File;
-
-public abstract class DeviceRuntimeService implements DaemonService.SystemInfoCallbacks {
-
-    public abstract void loadNativeLibrary();
-
-    public abstract File provideFilesDir();
-    public abstract File getCacheDir();
-
-    public abstract File getFilePath(String name);
-    public abstract File getConversationPath(String conversationId, String name);
-    public abstract File getConversationPath(String accountId, String conversationId, String name);
-
-    public File getConversationPath(DataTransfer interaction) {
-        return interaction.getConversationId() == null
-                ? getConversationPath(interaction.getConversation().getParticipant(), interaction.getStoragePath())
-                : interaction.getPublicPath();
-    }
-    public File getNewConversationPath(String accountId, String conversationId, String name) {
-        int prefix = 0;
-        File destPath;
-        do {
-            String fileName = prefix == 0 ? name : prefix + '_' + name;
-            destPath = getConversationPath(accountId, conversationId, fileName);
-            prefix++;
-        } while (destPath.exists());
-        return destPath;
-    }
-
-    public abstract File getTemporaryPath(String conversationId, String name);
-    public abstract File getConversationDir(String conversationId);
-
-    public abstract String getPushToken();
-
-    public abstract boolean isConnectedMobile();
-
-    public abstract boolean isConnectedEthernet();
-
-    public abstract boolean isConnectedWifi();
-
-    public abstract boolean isConnectedBluetooth();
-
-    public abstract boolean hasVideoPermission();
-
-    public abstract boolean hasAudioPermission();
-
-    public abstract boolean hasContactPermission();
-
-    public abstract boolean hasCallLogPermission();
-
-    public abstract boolean hasGalleryPermission();
-
-    public abstract boolean hasWriteExternalStoragePermission();
-
-    public abstract String getProfileName();
-
-    public abstract boolean hardLinkOrCopy(File source, File dest);
-}
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.kt b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.kt
new file mode 100644
index 000000000..38ab0eb9c
--- /dev/null
+++ b/ring-android/libringclient/src/main/java/net/jami/services/DeviceRuntimeService.kt
@@ -0,0 +1,66 @@
+/*
+ *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
+ *
+ *  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, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+package net.jami.services
+
+import net.jami.services.DaemonService.SystemInfoCallbacks
+import net.jami.model.DataTransfer
+import java.io.File
+
+abstract class DeviceRuntimeService : SystemInfoCallbacks {
+    abstract fun loadNativeLibrary()
+    abstract fun provideFilesDir(): File
+    abstract val cacheDir: File
+    abstract fun getFilePath(name: String): File
+    abstract fun getConversationPath(conversationId: String, name: String): File
+    abstract fun getConversationPath(accountId: String, conversationId: String, name: String): File
+    fun getConversationPath(interaction: DataTransfer): File {
+        return if (interaction.conversationId == null) getConversationPath(
+            interaction.conversation!!.participant,
+            interaction.storagePath
+        ) else interaction.publicPath!!
+    }
+
+    fun getNewConversationPath(accountId: String, conversationId: String, name: String): File {
+        var prefix = 0
+        var destPath: File
+        do {
+            val fileName = if (prefix == 0) name else "${prefix}_$name"
+            destPath = getConversationPath(accountId, conversationId, fileName)
+            prefix++
+        } while (destPath.exists())
+        return destPath
+    }
+
+    abstract fun getTemporaryPath(conversationId: String, name: String): File
+    abstract fun getConversationDir(conversationId: String): File?
+    abstract val pushToken: String?
+    abstract val isConnectedMobile: Boolean
+    abstract val isConnectedEthernet: Boolean
+    abstract val isConnectedWifi: Boolean
+    abstract val isConnectedBluetooth: Boolean
+    abstract fun hasVideoPermission(): Boolean
+    abstract fun hasAudioPermission(): Boolean
+    abstract fun hasContactPermission(): Boolean
+    abstract fun hasCallLogPermission(): Boolean
+    abstract fun hasGalleryPermission(): Boolean
+    abstract fun hasWriteExternalStoragePermission(): Boolean
+    abstract val profileName: String?
+    abstract fun hardLinkOrCopy(source: File, dest: File): Boolean
+}
\ No newline at end of file
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java b/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java
deleted file mode 100644
index 5e460b2df..000000000
--- a/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
- *
- *  Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
- *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
- *
- *  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, write to the Free Software
- *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
- */
-package net.jami.services;
-
-import com.j256.ormlite.dao.Dao;
-import com.j256.ormlite.stmt.DeleteBuilder;
-import com.j256.ormlite.support.ConnectionSource;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import net.jami.model.Account;
-import net.jami.model.Conversation;
-import net.jami.model.ConversationHistory;
-import net.jami.model.Interaction;
-import net.jami.model.TextMessage;
-import net.jami.model.Uri;
-import net.jami.utils.Log;
-import net.jami.utils.StringUtils;
-
-import io.reactivex.rxjava3.core.Completable;
-import io.reactivex.rxjava3.core.Scheduler;
-import io.reactivex.rxjava3.core.Single;
-import io.reactivex.rxjava3.schedulers.Schedulers;
-
-public abstract class HistoryService {
-    private static final String TAG = HistoryService.class.getSimpleName();
-
-    private final Scheduler scheduler = Schedulers.single();
-
-    protected abstract ConnectionSource getConnectionSource(String dbName);
-
-    protected abstract Dao<Interaction, Integer> getInteractionDataDao(String dbName);
-
-    protected abstract Dao<ConversationHistory, Integer> getConversationDataDao(String dbName);
-
-    protected abstract Object getHelper(String dbName);
-
-    public abstract void setMessageRead(String accountId, Uri conversationUri, String lastId);
-    public abstract String getLastMessageRead(String accountId, Uri conversationUri);
-
-    protected abstract void deleteAccountHistory(String accountId);
-
-    public Scheduler getScheduler() {
-        return scheduler;
-    }
-
-    public Completable clearHistory(final String accountId) {
-        return Completable.fromAction(() -> deleteAccountHistory(accountId))
-                .subscribeOn(scheduler);
-    }
-
-    /**
-     * Clears a conversation's history
-     *
-     * @param contactId          the participant's contact ID
-     * @param accountId          the user's contact ID
-     * @param deleteConversation true to completely delete the conversation including contact events
-     * @return
-     */
-    public Completable clearHistory(final String contactId, final String accountId, boolean deleteConversation) {
-        if (StringUtils.isEmpty(accountId))
-            return Completable.complete();
-        return Completable.fromAction(() -> {
-            int deleted = 0;
-
-            ConversationHistory conversation = getConversationDataDao(accountId).queryBuilder()
-                    .where().eq(ConversationHistory.COLUMN_PARTICIPANT, contactId).queryForFirst();
-
-            if (conversation == null)
-                return;
-
-            DeleteBuilder<Interaction, Integer> deleteBuilder = getInteractionDataDao(accountId).deleteBuilder();
-            if (deleteConversation) {
-                // complete delete, remove conversation and all interactions
-                deleteBuilder.where().eq(Interaction.COLUMN_CONVERSATION, conversation.getId());
-                getConversationDataDao(accountId).deleteById(conversation.getId());
-            } else {
-                // keep conversation and contact event interactions
-                deleteBuilder.where()
-                        .eq(Interaction.COLUMN_CONVERSATION, conversation.getId()).and()
-                        .ne(Interaction.COLUMN_TYPE, Interaction.InteractionType.CONTACT.toString());
-            }
-
-            deleted += deleteBuilder.delete();
-
-            Log.w(TAG, "clearHistory: removed " + deleted + " elements");
-        }).subscribeOn(scheduler);
-    }
-
-    /**
-     * Clears all interactions in the app. Maintains contact events and actual conversations.
-     *
-     * @param accounts the list of accounts in the app
-     * @return a completable
-     */
-    public Completable clearHistory(List<Account> accounts) {
-        return Completable.fromAction(() -> {
-            for (Account account : accounts) {
-                String accountId = account.getAccountID();
-                DeleteBuilder<Interaction, Integer> deleteBuilder = getInteractionDataDao(accountId).deleteBuilder();
-                deleteBuilder.where()
-                        .ne(Interaction.COLUMN_TYPE, Interaction.InteractionType.CONTACT.toString());
-                deleteBuilder.delete();
-            }
-        }).subscribeOn(scheduler);
-    }
-
-    public Completable updateInteraction(Interaction interaction, String accountId) {
-        return Completable.fromAction(() -> getInteractionDataDao(accountId).update(interaction))
-                .subscribeOn(scheduler);
-    }
-
-    public Completable deleteInteraction(int id, String accountId) {
-        return Completable
-                .fromAction(() -> getInteractionDataDao(accountId).deleteById(id))
-                .subscribeOn(scheduler);
-    }
-
-    /**
-     * Inserts an interaction into the database, and if necessary, a conversation
-     *
-     * @param accountId    the user's account ID
-     * @param conversation the conversation
-     * @param interaction  the interaction to insert
-     * @return a conversation single
-     */
-    public Completable insertInteraction(String accountId, Conversation conversation, Interaction interaction) {
-        return Completable.fromAction(() -> {
-            Log.d(TAG, "Inserting interaction for account -> " + accountId);
-            Dao<ConversationHistory, Integer> conversationDataDao = getConversationDataDao(accountId);
-            ConversationHistory history = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, conversation.getParticipant()).queryForFirst();
-            if (history == null) {
-                history = conversationDataDao.createIfNotExists(new ConversationHistory(conversation.getParticipant()));
-            }
-            //interaction.setConversation(conversation);
-            conversation.setId(history.getId());
-            getInteractionDataDao(accountId).create(interaction);
-        })
-                .doOnError(e -> Log.e(TAG, "Can't insert interaction", e))
-                .subscribeOn(scheduler);
-    }
-
-    /**
-     * Loads data required to load the smartlist. Only requires the most recent message or contact action.
-     *
-     * @param accountId required to query the appropriate account database
-     * @return a list of the most recent interactions with each contact
-     */
-    public Single<List<Interaction>> getSmartlist(final String accountId) {
-        Log.d(TAG, "Loading smartlist");
-        return Single.fromCallable(() ->
-                // a raw query is done as MAX is not supported by ormlite without a raw query and a raw query cannot be combined with an orm query so a complete raw query is done
-                // raw row mapper maps the sqlite result which is a list of strings, into the interactions object
-                getInteractionDataDao(accountId).queryRaw("SELECT * FROM (SELECT DISTINCT id, author, conversation, MAX(timestamp), body, type, status, daemon_id, is_read, extra_data from interactions GROUP BY interactions.conversation) as final\n" +
-                        "JOIN conversations\n" +
-                        "WHERE conversations.id = final.conversation\n" +
-                        "GROUP BY final.conversation\n", (columnNames, resultColumns) -> new Interaction(resultColumns[0],
-                        resultColumns[1], new ConversationHistory(Integer.parseInt(resultColumns[2]), resultColumns[12]), resultColumns[3], resultColumns[4], resultColumns[5], resultColumns[6], resultColumns[7], resultColumns[8], resultColumns[9])).getResults())
-                .subscribeOn(scheduler)
-                .doOnError(e -> Log.e(TAG, "Can't load smartlist from database", e))
-                .onErrorReturn(e -> new ArrayList<>());
-    }
-
-    /**
-     * Retrieves an entire conversations history
-     *
-     * @param accountId      the user's account id
-     * @param conversationId the conversation id
-     * @return a conversation and all of its interactions
-     */
-    public Single<List<Interaction>> getConversationHistory(final String accountId, final int conversationId) {
-        Log.d(TAG, "Loading conversation history:  Account ID -> " + accountId + ", ConversationID -> " + conversationId);
-        return Single.fromCallable(() -> {
-            Dao<Interaction, Integer> interactionDataDao = getInteractionDataDao(accountId);
-
-            return interactionDataDao.query(interactionDataDao.queryBuilder()
-                    .orderBy(Interaction.COLUMN_TIMESTAMP, true)
-                    .where().eq(Interaction.COLUMN_CONVERSATION, conversationId)
-                    .prepare());
-
-        }).subscribeOn(scheduler).doOnError(e -> Log.e(TAG, "Can't load conversation from database", e))
-                .onErrorReturn(e -> new ArrayList<>());
-    }
-
-    Single<TextMessage> incomingMessage(final String accountId, final String daemonId, final String from, final String message) {
-        return Single.fromCallable(() -> {
-            String fromUri = Uri.fromString(from).getUri();
-            Dao<ConversationHistory, Integer> conversationDataDao = getConversationDataDao(accountId);
-            ConversationHistory conversation = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, fromUri).queryForFirst();
-            if (conversation == null) {
-                conversation = new ConversationHistory(fromUri);
-                conversation.setId(conversationDataDao.extractId(conversationDataDao.createIfNotExists(conversation)));
-            }
-
-            TextMessage txt = new TextMessage(fromUri, accountId, daemonId, conversation, message);
-            txt.setStatus(Interaction.InteractionStatus.SUCCESS);
-
-            Log.w(TAG, "New text messsage " + txt.getAuthor() + " " + txt.getDaemonId() + " " + txt.getBody());
-            getInteractionDataDao(accountId).create(txt);
-            return txt;
-        }).subscribeOn(scheduler);
-    }
-
-
-    Single<TextMessage> accountMessageStatusChanged(String accountId, String daemonId, String peer, Interaction.InteractionStatus status) {
-        return Single.fromCallable(() -> {
-            List<Interaction> textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, daemonId);
-            if (textList == null || textList.isEmpty()) {
-                throw new RuntimeException("accountMessageStatusChanged: not able to find message with id " + daemonId + " in database");
-            }
-            Interaction text = textList.get(0);
-            String participant = Uri.fromString(peer).getUri();
-            if (!text.getConversation().getParticipant().equals(participant)) {
-                throw new RuntimeException("accountMessageStatusChanged: received an invalid text message");
-            }
-            TextMessage msg = new TextMessage(text);
-            msg.setStatus(status);
-            getInteractionDataDao(accountId).update(msg);
-            msg.setAccount(accountId);
-            return msg;
-        }).subscribeOn(scheduler);
-    }
-
-}
\ No newline at end of file
diff --git a/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.kt b/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.kt
new file mode 100644
index 000000000..99a33285d
--- /dev/null
+++ b/ring-android/libringclient/src/main/java/net/jami/services/HistoryService.kt
@@ -0,0 +1,222 @@
+/*
+ *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ *  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, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+package net.jami.services
+
+import com.j256.ormlite.dao.Dao
+import com.j256.ormlite.support.ConnectionSource
+import io.reactivex.rxjava3.core.Completable
+import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.core.Single
+import io.reactivex.rxjava3.schedulers.Schedulers
+import net.jami.model.*
+import net.jami.model.Interaction.InteractionStatus
+import net.jami.model.Uri.Companion.fromString
+import net.jami.utils.Log
+import net.jami.utils.StringUtils.isEmpty
+import java.util.*
+
+abstract class HistoryService {
+    val scheduler: Scheduler = Schedulers.single()
+    protected abstract fun getConnectionSource(dbName: String): ConnectionSource?
+    protected abstract fun getInteractionDataDao(dbName: String): Dao<Interaction, Int>
+    protected abstract fun getConversationDataDao(dbName: String): Dao<ConversationHistory, Int>
+    protected abstract fun getHelper(dbName: String): Any?
+    abstract fun setMessageRead(accountId: String, conversationUri: Uri, lastId: String)
+    abstract fun getLastMessageRead(accountId: String, conversationUri: Uri): String?
+    protected abstract fun deleteAccountHistory(accountId: String)
+    fun clearHistory(accountId: String): Completable {
+        return Completable.fromAction { deleteAccountHistory(accountId) }
+            .subscribeOn(scheduler)
+    }
+
+    /**
+     * Clears a conversation's history
+     *
+     * @param contactId          the participant's contact ID
+     * @param accountId          the user's contact ID
+     * @param deleteConversation true to completely delete the conversation including contact events
+     * @return
+     */
+    fun clearHistory(contactId: String, accountId: String, deleteConversation: Boolean): Completable {
+        return if (isEmpty(accountId)) Completable.complete() else Completable.fromAction {
+            var deleted = 0
+            val conversation = getConversationDataDao(accountId).queryBuilder()
+                .where().eq(ConversationHistory.COLUMN_PARTICIPANT, contactId).queryForFirst() ?: return@fromAction
+            val deleteBuilder = getInteractionDataDao(accountId).deleteBuilder()
+            if (deleteConversation) {
+                // complete delete, remove conversation and all interactions
+                deleteBuilder.where().eq(Interaction.COLUMN_CONVERSATION, conversation.id)
+                getConversationDataDao(accountId).deleteById(conversation.id)
+            } else {
+                // keep conversation and contact event interactions
+                deleteBuilder.where()
+                    .eq(Interaction.COLUMN_CONVERSATION, conversation.id).and()
+                    .ne(Interaction.COLUMN_TYPE, Interaction.InteractionType.CONTACT.toString())
+            }
+            deleted += deleteBuilder.delete()
+            Log.w(TAG, "clearHistory: removed $deleted elements")
+        }.subscribeOn(scheduler)
+    }
+
+    /**
+     * Clears all interactions in the app. Maintains contact events and actual conversations.
+     *
+     * @param accounts the list of accounts in the app
+     * @return a completable
+     */
+    fun clearHistory(accounts: List<Account>): Completable {
+        return Completable.fromAction {
+            for (account in accounts) {
+                getInteractionDataDao(account.accountID).deleteBuilder().let { deleteBuilder ->
+                    deleteBuilder.where().ne(Interaction.COLUMN_TYPE, Interaction.InteractionType.CONTACT.toString())
+                    deleteBuilder.delete()
+                }
+            }
+        }.subscribeOn(scheduler)
+    }
+
+    fun updateInteraction(interaction: Interaction, accountId: String): Completable {
+        return Completable.fromAction { getInteractionDataDao(accountId).update(interaction) }
+            .subscribeOn(scheduler)
+    }
+
+    fun deleteInteraction(id: Int, accountId: String): Completable {
+        return Completable
+            .fromAction { getInteractionDataDao(accountId).deleteById(id) }
+            .subscribeOn(scheduler)
+    }
+
+    /**
+     * Inserts an interaction into the database, and if necessary, a conversation
+     *
+     * @param accountId    the user's account ID
+     * @param conversation the conversation
+     * @param interaction  the interaction to insert
+     * @return a conversation single
+     */
+    fun insertInteraction(accountId: String, conversation: Conversation, interaction: Interaction): Completable {
+        return Completable.fromAction {
+            Log.d(TAG, "Inserting interaction for account -> $accountId")
+            val conversationDataDao = getConversationDataDao(accountId)
+            val history = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, conversation.participant).queryForFirst() ?:
+            conversationDataDao.createIfNotExists(ConversationHistory(conversation.participant))!!
+            //interaction.setConversation(conversation);
+            conversation.id = history.id
+            getInteractionDataDao(accountId).create(interaction)
+        }
+            .doOnError { e: Throwable? -> Log.e(TAG, "Can't insert interaction", e) }
+            .subscribeOn(scheduler)
+    }
+
+    /**
+     * Loads data required to load the smartlist. Only requires the most recent message or contact action.
+     *
+     * @param accountId required to query the appropriate account database
+     * @return a list of the most recent interactions with each contact
+     */
+    fun getSmartlist(accountId: String): Single<List<Interaction>> {
+        Log.d(TAG, "Loading smartlist")
+        return Single.fromCallable { // a raw query is done as MAX is not supported by ormlite without a raw query and a raw query cannot be combined with an orm query so a complete raw query is done
+            // raw row mapper maps the sqlite result which is a list of strings, into the interactions object
+            getInteractionDataDao(accountId).queryRaw("""
+    SELECT * FROM (SELECT DISTINCT id, author, conversation, MAX(timestamp), body, type, status, daemon_id, is_read, extra_data from interactions GROUP BY interactions.conversation) as final
+    JOIN conversations
+    WHERE conversations.id = final.conversation
+    GROUP BY final.conversation
+    
+    """.trimIndent(), { columnNames: Array<String>, resultColumns: Array<String> ->
+                Interaction(
+                    resultColumns[0],
+                    resultColumns[1],
+                    ConversationHistory(resultColumns[2].toInt(), resultColumns[12]),
+                    resultColumns[3],
+                    resultColumns[4],
+                    resultColumns[5],
+                    resultColumns[6],
+                    resultColumns[7],
+                    resultColumns[8],
+                    resultColumns[9]
+                )
+            }).results
+        }
+            .subscribeOn(scheduler)
+            .doOnError { e: Throwable -> Log.e(TAG, "Can't load smartlist from database", e) }
+            .onErrorReturn { ArrayList() }
+    }
+
+    /**
+     * Retrieves an entire conversations history
+     *
+     * @param accountId      the user's account id
+     * @param conversationId the conversation id
+     * @return a conversation and all of its interactions
+     */
+    fun getConversationHistory(accountId: String, conversationId: Int): Single<List<Interaction>> {
+        Log.d(TAG, "Loading conversation history:  Account ID -> $accountId, ConversationID -> $conversationId")
+        return Single.fromCallable {
+            val interactionDataDao = getInteractionDataDao(accountId)
+            interactionDataDao.query(interactionDataDao.queryBuilder()
+                    .orderBy(Interaction.COLUMN_TIMESTAMP, true)
+                    .where().eq(Interaction.COLUMN_CONVERSATION, conversationId)
+                    .prepare())
+        }.subscribeOn(scheduler).doOnError { e: Throwable? -> Log.e(TAG, "Can't load conversation from database", e) }
+            .onErrorReturn { e: Throwable? -> ArrayList() }
+    }
+
+    fun incomingMessage(accountId: String, daemonId: String?, from: String, message: String): Single<TextMessage> {
+        return Single.fromCallable {
+            val fromUri = fromString(from).uri
+            val conversationDataDao = getConversationDataDao(accountId)
+            val conversation = conversationDataDao.queryBuilder().where().eq(ConversationHistory.COLUMN_PARTICIPANT, fromUri)
+                    .queryForFirst() ?: ConversationHistory(fromUri).apply {
+                id = conversationDataDao.extractId(conversationDataDao.createIfNotExists(this))
+            }
+            val txt = TextMessage(fromUri, accountId, daemonId, conversation, message)
+            txt.status = InteractionStatus.SUCCESS
+            Log.w(TAG, "New text messsage " + txt.author + " " + txt.daemonId + " " + txt.body)
+            getInteractionDataDao(accountId).create(txt)
+            txt
+        }.subscribeOn(scheduler)
+    }
+
+    fun accountMessageStatusChanged(accountId: String, daemonId: String, peer: String, status: InteractionStatus): Single<TextMessage> {
+        return Single.fromCallable {
+            val textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, daemonId)
+            if (textList == null || textList.isEmpty()) {
+                throw RuntimeException("accountMessageStatusChanged: not able to find message with id $daemonId in database")
+            }
+            val text = textList[0]
+            val participant = fromString(peer).uri
+            if (text.conversation!!.participant != participant) {
+                throw RuntimeException("accountMessageStatusChanged: received an invalid text message")
+            }
+            val msg = TextMessage(text)
+            msg.status = status
+            getInteractionDataDao(accountId).update(msg)
+            msg.account = accountId
+            msg
+        }.subscribeOn(scheduler)
+    }
+
+    companion object {
+        private val TAG = HistoryService::class.java.simpleName
+    }
+}
\ No newline at end of file
-- 
GitLab