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