From 7cc25f6101640c87bf685f17d3f98fde12e88c2e Mon Sep 17 00:00:00 2001 From: mosazaid Date: Wed, 30 Jun 2021 14:41:51 +0300 Subject: [PATCH 01/13] make floating video screen --- android/app/src/main/AndroidManifest.xml | 11 +- .../main/kotlin/com/hmg/hmgDr/MainActivity.kt | 261 ++-- .../VideoStreamFloatingWidgetService.kt | 1057 +++++++++++++++++ .../hmgDr/ui/fragment/VideoCallFragment.kt | 5 +- .../kotlin/com/hmg/hmgDr/util/viewsUtil.kt | 17 + lib/config/config.dart | 4 +- lib/core/service/VideoCallService.dart | 13 +- lib/screens/home/home_screen.dart | 43 +- .../patient_profile_screen.dart | 27 +- 9 files changed, 1262 insertions(+), 176 deletions(-) create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0ac76ea8..7c18e957 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,9 @@ + + + @@ -41,7 +44,13 @@ - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7c18e957..2bd477be 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ FlutterApplication and put your custom class here. --> - + diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt index 670facf1..e20eef1e 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt @@ -14,9 +14,8 @@ import android.util.Log import android.widget.Toast import androidx.annotation.NonNull import com.google.gson.GsonBuilder -import com.hmg.hmgDr.Model.GetSessionStatusModel -import com.hmg.hmgDr.Model.SessionStatusModel -import com.hmg.hmgDr.Service.VideoStreamContainerService +import com.hmg.hmgDr.model.GetSessionStatusModel +import com.hmg.hmgDr.model.SessionStatusModel import com.hmg.hmgDr.Service.VideoStreamFloatingWidgetService import com.hmg.hmgDr.ui.VideoCallResponseListener import io.flutter.embedding.android.FlutterFragmentActivity @@ -115,6 +114,7 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, serviceIntent = Intent(this@MainActivity, VideoStreamFloatingWidgetService::class.java) serviceIntent?.run { putExtras(arguments) + action = VideoStreamFloatingWidgetService.ACTION_START_CALL } checkFloatingWidgetPermission() } @@ -145,6 +145,13 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, bindService() } + override fun onDestroy() { + super.onDestroy() + if (bound) { + unbindService() + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { /*if (requestCode == LAUNCH_VIDEO) { if (resultCode == Activity.RESULT_OK) { @@ -230,7 +237,13 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, } else { super.onBackPressed() } + } + override fun onPause() { + if (videoStreamService != null && videoStreamService?.serviceRunning == true && videoStreamService?.isFullScreen!!) { + videoStreamService!!.onMinimizedClicked() + } + super.onPause() } // override fun onStart() { @@ -279,5 +292,4 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, } } - } diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/SessionStatusAPI.java b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/SessionStatusAPI.java index e507650e..7c458b25 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/SessionStatusAPI.java +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/SessionStatusAPI.java @@ -1,8 +1,8 @@ package com.hmg.hmgDr.Service; -import com.hmg.hmgDr.Model.ChangeCallStatusRequestModel; -import com.hmg.hmgDr.Model.GetSessionStatusModel; -import com.hmg.hmgDr.Model.SessionStatusModel; +import com.hmg.hmgDr.model.ChangeCallStatusRequestModel; +import com.hmg.hmgDr.model.GetSessionStatusModel; +import com.hmg.hmgDr.model.SessionStatusModel; import retrofit2.Call; diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt index 1ff2a2c2..f79f23a4 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt @@ -1,10 +1,10 @@ package com.hmg.hmgDr.Service import android.annotation.SuppressLint -import android.app.Activity -import android.app.Service +import android.app.* import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory import android.graphics.PixelFormat import android.graphics.Point import android.opengl.GLSurfaceView @@ -13,20 +13,25 @@ import android.util.Log import android.view.* import android.widget.* import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.view.GestureDetectorCompat -import com.hmg.hmgDr.Model.ChangeCallStatusRequestModel -import com.hmg.hmgDr.Model.GetSessionStatusModel -import com.hmg.hmgDr.Model.SessionStatusModel import com.hmg.hmgDr.R +import com.hmg.hmgDr.model.ChangeCallStatusRequestModel +import com.hmg.hmgDr.model.GetSessionStatusModel +import com.hmg.hmgDr.model.NotificationVideoModel +import com.hmg.hmgDr.model.SessionStatusModel import com.hmg.hmgDr.ui.VideoCallContract import com.hmg.hmgDr.ui.VideoCallPresenterImpl import com.hmg.hmgDr.ui.VideoCallResponseListener import com.hmg.hmgDr.util.DynamicVideoRenderer +import com.hmg.hmgDr.util.NotificationUtil import com.hmg.hmgDr.util.ViewsUtil import com.opentok.android.* import kotlin.math.ceil + class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, PublisherKit.PublisherListener, SubscriberKit.VideoListener, VideoCallContract.VideoCallView { @@ -39,6 +44,14 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, private const val RC_SETTINGS_SCREEN_PERM = 123 private const val RC_VIDEO_APP_PERM = 124 + const val CHANNEL_DEFAULT_IMPORTANCE = "Video_stream_channel" + const val CHANNEL_DEFAULT_NAME = "Video cAll" + const val ONGOING_NOTIFICATION_ID = 1 + + + const val ACTION_START_CALL = "com.hmg.hmgDr.Service.action.startCall" + const val ACTION_MINIMIZE_CALL = "com.hmg.hmgDr.Service.action.minimizeCall" + const val ACTION_END_CALL = "com.hmg.hmgDr.Service.action.endCall" } private lateinit var windowManagerParams: WindowManager.LayoutParams @@ -111,35 +124,52 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, private val serviceBinder: IBinder = VideoStreamBinder() + // Notification variables + private lateinit var mNotificationManagerCompat: NotificationManagerCompat + override fun onBind(intent: Intent?): IBinder { return serviceBinder } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent != null && intent.extras != null) { - arguments = intent.extras - - arguments?.run { - apiKey = getString("apiKey") - sessionId = getString("sessionId") - token = getString("token") - appLang = getString("appLang") - baseUrl = getString("baseUrl") - sessionStatusModel = getParcelable("sessionStatusModel") - if (sessionStatusModel != null) - isRecording = sessionStatusModel!!.isRecording - } + if (intent != null) { + + val action = intent.action - //init WindowManager - mWindowManager = getSystemService(WINDOW_SERVICE) as WindowManager + if (action == ACTION_START_CALL) { + if (intent.extras != null) { + arguments = intent.extras + arguments?.run { + apiKey = getString("apiKey") + sessionId = getString("sessionId") + token = getString("token") + appLang = getString("appLang") + baseUrl = getString("baseUrl") + sessionStatusModel = getParcelable("sessionStatusModel") + if (sessionStatusModel != null) + isRecording = sessionStatusModel!!.isRecording + } - getWindowManagerDefaultDisplay() + //init WindowManager + mWindowManager = getSystemService(WINDOW_SERVICE) as WindowManager - //Init LayoutInflater - val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater + getWindowManagerDefaultDisplay() + + //Init LayoutInflater + val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater + + addFloatingWidgetView(inflater) + handleDragDialog() + + addForegroundService() + } + } else if (action == ACTION_END_CALL) { + closeVideoCall() + } else if (action == ACTION_MINIMIZE_CALL) { + if (!isFullScreen) + onMinimizedClicked() + } - addFloatingWidgetView(inflater) - handleDragDialog() } // Toast.makeText(this, "Service started by user.", Toast.LENGTH_LONG).show() return START_STICKY @@ -308,7 +338,10 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, videoCallPresenter.callClintConnected(sessionStatusModel) } } - mConnectedHandler!!.postDelayed(mConnectedRunnable!!, (10 * 1000).toLong()) // TODO MOSA return from 10 to 55 + mConnectedHandler!!.postDelayed( + mConnectedRunnable!!, + (55 * 1000).toLong() + ) } private fun handleVideoViewHeight() { @@ -726,6 +759,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, private fun disconnectSession() { if (mSession == null) { videoCallResponseListener?.onCallFinished(Activity.RESULT_CANCELED) + stopForeground(true) stopSelf() return } @@ -753,6 +787,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, sessionStatusModel!!.vcid ) ) + stopForeground(true) stopSelf() } @@ -865,6 +900,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, disconnectSession() videoCallResponseListener?.onCallFinished(Activity.RESULT_OK, returnIntent) stopSelf() + stopForeground(true) } } @@ -889,6 +925,158 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, ).toInt() } + private fun addForegroundService() { + mNotificationManagerCompat = NotificationManagerCompat.from(applicationContext) + val areNotificationsEnabled = mNotificationManagerCompat.areNotificationsEnabled() + if (!areNotificationsEnabled) { + Toast.makeText( + this, + "You need to enable notifications for this app", + Toast.LENGTH_SHORT + ).show() + // Links to this app's notification settings + openNotificationSettingsForApp() + return + } + generateBigTextStyleNotification() + } + + private fun generateBigTextStyleNotification() { + val notificationData: NotificationVideoModel = + NotificationVideoModel( + sessionStatusModel!!.patientName, + "Tap to return to call", + CHANNEL_DEFAULT_IMPORTANCE, + CHANNEL_DEFAULT_NAME, + "Video call stream background", + mSummaryText = "timer" + ) + // 1. Create/Retrieve Notification Channel for O and beyond devices (26+). + val notificationChannelId: String = + NotificationUtil.createNotificationChannel(this, notificationData) + + // 2. Build the BIG_TEXT_STYLE. + val bigTextStyle = + NotificationCompat.BigTextStyle() // Overrides ContentText in the big form of the template. + .bigText(notificationData.mBigText) // Overrides ContentTitle in the big form of the template. + .setBigContentTitle(notificationData.mBigContentTitle) // Summary line after the detail section in the big form of the template. + // Note: To improve readability, don't overload the user with info. If Summary Text + // doesn't add critical information, you should skip it. + .setSummaryText(notificationData.mSummaryText) + + // 3. Set up main Intent for notification. + val pendingIntent: PendingIntent = + Intent(this, VideoStreamFloatingWidgetService::class.java) + .let { notificationIntent -> + notificationIntent.action = ACTION_MINIMIZE_CALL + PendingIntent.getService(this, 0, notificationIntent, 0) + } + + // 4. Create additional Actions (Intents) for the Notification. + // Dismiss Action. + val endCallPendingIntent: PendingIntent = + Intent(this, VideoStreamFloatingWidgetService::class.java).apply { + action = ACTION_END_CALL + } + .let { notificationIntent -> + PendingIntent.getService(this, 0, notificationIntent, 0) + } +// val endCallAction = NotificationCompat.Action.Builder( +// R.drawable.ic_end_call, +// "End Call", +// endCallPendingIntent +// ) +// .build() + + // 5. Build and issue the notification. + // Notification Channel Id is ignored for Android pre O (26). + + + // 5. Build and issue the notification. + // Notification Channel Id is ignored for Android pre O (26). + val notificationCompatBuilder = notificationChannelId?.let { + NotificationCompat.Builder( + applicationContext, it + ) + } + + // notification's layout + val mRemoteViews = RemoteViews(packageName, R.layout.notifi_video_view) + mRemoteViews.setImageViewResource(R.id.iv_icon, R.mipmap.ic_launcher) + // notification's title + mRemoteViews.setTextViewText(R.id.notify_title, notificationData.mContentTitle) + // notification's content + mRemoteViews.setTextViewText(R.id.notify_content, notificationData.mContentText) + mRemoteViews.setOnClickPendingIntent(R.id.btn_end, endCallPendingIntent) + + val notification: Notification = notificationCompatBuilder + // BIG_TEXT_STYLE sets title and content for API 16 (4.1 and after). + .setStyle(bigTextStyle) + // Title for API <16 (4.0 and below) devices. + .setContentTitle(notificationData.mBigContentTitle) + // Content for API <24 (7.0 and below) devices. + .setContentText(notificationData.mContentText) + .setSmallIcon(R.mipmap.ic_launcher) + .setLargeIcon( + BitmapFactory.decodeResource( + resources, + R.mipmap.ic_launcher + ) + ) + .setCustomContentView(mRemoteViews) + .setCustomBigContentView(mRemoteViews) + .setContentIntent(pendingIntent) + .setDefaults(NotificationCompat.DEFAULT_ALL) + // Set primary color (important for Wear 2.0 Notifications). + .setColor(ContextCompat.getColor(applicationContext, R.color.colorPrimary)) + .setCategory(Notification.CATEGORY_SERVICE) + .setPriority(notificationData.mPriority) + .setVisibility(notificationData.mChannelLockscreenVisibility) +// .addAction(endCallAction) + .build() + + mNotificationManagerCompat.notify(ONGOING_NOTIFICATION_ID, notification) + + startForeground(ONGOING_NOTIFICATION_ID, notification) + /*val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE) + .setContentTitle("") + .setContentText("") + .setSmallIcon(R.mipmap.ic_launcher) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build() + } else { + Notification.Builder(this) + .setContentTitle("") + .setContentText("") + .setSmallIcon(R.mipmap.ic_launcher) + .setOngoing(true) + .setContentIntent(pendingIntent) + .build() + } + // Notification ID cannot be 0. + */ + } + + /** + * IMPORTANT NOTE: You should not do this action unless the user takes an action to see your + * Notifications like this sample demonstrates. Spamming users to re-enable your notifications + * is a bad idea. + */ + private fun openNotificationSettingsForApp() { + // Links to this app's notification settings. + val intent = Intent() + intent.action = "android.settings.APP_NOTIFICATION_SETTINGS" + intent.putExtra("app_package", packageName) + intent.putExtra("app_uid", applicationInfo.uid) + + // for Android 8 and above + intent.putExtra("android.provider.extra.APP_PACKAGE", packageName) + startActivity(intent) + } + + /** * OnTouch actions */ @@ -1001,7 +1189,11 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, mParams.x = (szWindow.x - current_x_cord * current_x_cord - videoCallContainer.width).toInt() - mWindowManager?.updateViewLayout(floatingWidgetView, mParams) + try { + mWindowManager?.updateViewLayout(floatingWidgetView, mParams) + } catch (e: Exception) { + Log.e("windowManagerUpdate", "${e.localizedMessage}.") + } val x = szWindow.x - current_x_cord object : CountDownTimer(500, 5) { //get params of Floating Widget view @@ -1014,13 +1206,21 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, mParams.x = (szWindow.x - current_x_cord * current_x_cord * step - videoCallContainer.width).toInt() - mWindowManager?.updateViewLayout(floatingWidgetView, mParams) + try { + mWindowManager?.updateViewLayout(floatingWidgetView, mParams) + } catch (e: Exception) { + Log.e("windowManagerUpdate", "${e.localizedMessage}.") + } } override fun onFinish() { mParams.x = -(szWindow.x - videoCallContainer.width) - mWindowManager?.updateViewLayout(floatingWidgetView, mParams) + try { + mWindowManager?.updateViewLayout(floatingWidgetView, mParams) + } catch (e: Exception) { + Log.e("windowManagerUpdate", "${e.localizedMessage}.") + } } }.start() } @@ -1042,7 +1242,12 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, mParams.x = (szWindow.x + current_x_cord * current_x_cord * step - videoCallContainer.width).toInt() - mWindowManager?.updateViewLayout(floatingWidgetView, mParams) + try { + mWindowManager?.updateViewLayout(floatingWidgetView, mParams) + } catch (e: Exception) { + Log.e("windowManagerUpdate", "${e.localizedMessage}.") + } + } override fun onFinish() { diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Model/ChangeCallStatusRequestModel.java b/android/app/src/main/kotlin/com/hmg/hmgDr/model/ChangeCallStatusRequestModel.java similarity index 99% rename from android/app/src/main/kotlin/com/hmg/hmgDr/Model/ChangeCallStatusRequestModel.java rename to android/app/src/main/kotlin/com/hmg/hmgDr/model/ChangeCallStatusRequestModel.java index 5fcdb611..c7993d5f 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Model/ChangeCallStatusRequestModel.java +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/model/ChangeCallStatusRequestModel.java @@ -1,4 +1,4 @@ -package com.hmg.hmgDr.Model; +package com.hmg.hmgDr.model; import android.os.Parcel; diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Model/GetSessionStatusModel.java b/android/app/src/main/kotlin/com/hmg/hmgDr/model/GetSessionStatusModel.java similarity index 99% rename from android/app/src/main/kotlin/com/hmg/hmgDr/Model/GetSessionStatusModel.java rename to android/app/src/main/kotlin/com/hmg/hmgDr/model/GetSessionStatusModel.java index 9e2e1cf4..32b3a96c 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Model/GetSessionStatusModel.java +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/model/GetSessionStatusModel.java @@ -1,4 +1,4 @@ -package com.hmg.hmgDr.Model; +package com.hmg.hmgDr.model; import android.os.Parcel; import android.os.Parcelable; diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationDataModel.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationDataModel.kt new file mode 100644 index 00000000..bc025132 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationDataModel.kt @@ -0,0 +1,16 @@ +package com.hmg.hmgDr.model + +/** Represents standard data needed for a Notification. */ +open class NotificationDataModel( + // Standard notification values: + var mContentTitle: String, + var mContentText: String, + var mPriority: Int , + // Notification channel values (O and above): + var mChannelId: String, + var mChannelName: CharSequence, + var mChannelDescription: String, + var mChannelImportance: Int , + var mChannelEnableVibrate: Boolean , + var mChannelLockscreenVisibility: Int +) \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt new file mode 100644 index 00000000..78c15a91 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt @@ -0,0 +1,35 @@ +package com.hmg.hmgDr.model + +import android.app.NotificationManager +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat + +class NotificationVideoModel constructor( + mContentTitle: String, + mContentText: String, + mChannelId: String, + mChannelName: CharSequence, + mChannelDescription: String, + mPriority: Int = NotificationCompat.PRIORITY_DEFAULT, + mChannelImportance: Int = NotificationManager.IMPORTANCE_DEFAULT, + mChannelEnableVibrate: Boolean = false, + mChannelLockscreenVisibility: Int = NotificationCompat.VISIBILITY_PUBLIC, + // Unique data for this Notification.Style: + var mBigContentTitle: String = mContentTitle, + val mBigText: String = mContentText, + val mSummaryText: String + +) : NotificationDataModel( + mContentTitle, + mContentText, + mPriority, + mChannelId, + mChannelName, + mChannelDescription, + mChannelImportance, + mChannelEnableVibrate, + mChannelLockscreenVisibility +) { + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Model/SessionStatusModel.java b/android/app/src/main/kotlin/com/hmg/hmgDr/model/SessionStatusModel.java similarity index 98% rename from android/app/src/main/kotlin/com/hmg/hmgDr/Model/SessionStatusModel.java rename to android/app/src/main/kotlin/com/hmg/hmgDr/model/SessionStatusModel.java index 51b0b1ee..fe309922 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Model/SessionStatusModel.java +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/model/SessionStatusModel.java @@ -1,4 +1,4 @@ -package com.hmg.hmgDr.Model; +package com.hmg.hmgDr.model; import android.os.Parcel; import android.os.Parcelable; diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/ui/VideoCallContract.java b/android/app/src/main/kotlin/com/hmg/hmgDr/ui/VideoCallContract.java index 2b099551..8b615e69 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/ui/VideoCallContract.java +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/ui/VideoCallContract.java @@ -1,8 +1,8 @@ package com.hmg.hmgDr.ui; -import com.hmg.hmgDr.Model.ChangeCallStatusRequestModel; -import com.hmg.hmgDr.Model.GetSessionStatusModel; -import com.hmg.hmgDr.Model.SessionStatusModel; +import com.hmg.hmgDr.model.ChangeCallStatusRequestModel; +import com.hmg.hmgDr.model.GetSessionStatusModel; +import com.hmg.hmgDr.model.SessionStatusModel; public interface VideoCallContract { diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/ui/VideoCallPresenterImpl.java b/android/app/src/main/kotlin/com/hmg/hmgDr/ui/VideoCallPresenterImpl.java index ea2128ba..63cf8c27 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/ui/VideoCallPresenterImpl.java +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/ui/VideoCallPresenterImpl.java @@ -1,8 +1,8 @@ package com.hmg.hmgDr.ui; -import com.hmg.hmgDr.Model.ChangeCallStatusRequestModel; -import com.hmg.hmgDr.Model.GetSessionStatusModel; -import com.hmg.hmgDr.Model.SessionStatusModel; +import com.hmg.hmgDr.model.ChangeCallStatusRequestModel; +import com.hmg.hmgDr.model.GetSessionStatusModel; +import com.hmg.hmgDr.model.SessionStatusModel; import com.hmg.hmgDr.Service.AppRetrofit; import com.hmg.hmgDr.Service.SessionStatusAPI; diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/ui/fragment/VideoCallFragment.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/ui/fragment/VideoCallFragment.kt index 061356ac..b020ed29 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/ui/fragment/VideoCallFragment.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/ui/fragment/VideoCallFragment.kt @@ -11,7 +11,6 @@ import android.graphics.Point import android.graphics.drawable.ColorDrawable import android.opengl.GLSurfaceView import android.os.* -import android.util.DisplayMetrics import android.util.Log import android.view.* import android.widget.* @@ -21,9 +20,9 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.view.GestureDetectorCompat import androidx.fragment.app.DialogFragment -import com.hmg.hmgDr.Model.ChangeCallStatusRequestModel -import com.hmg.hmgDr.Model.GetSessionStatusModel -import com.hmg.hmgDr.Model.SessionStatusModel +import com.hmg.hmgDr.model.ChangeCallStatusRequestModel +import com.hmg.hmgDr.model.GetSessionStatusModel +import com.hmg.hmgDr.model.SessionStatusModel import com.hmg.hmgDr.R import com.hmg.hmgDr.ui.VideoCallContract.VideoCallPresenter import com.hmg.hmgDr.ui.VideoCallContract.VideoCallView diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/util/NotificationUtil.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/util/NotificationUtil.kt new file mode 100644 index 00000000..13d6bd19 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/util/NotificationUtil.kt @@ -0,0 +1,42 @@ +package com.hmg.hmgDr.util + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.graphics.Color +import android.os.Build +import com.hmg.hmgDr.model.NotificationDataModel + +object NotificationUtil { + + fun createNotificationChannel(context: Context, notificationDataModel: NotificationDataModel): String{ + // The id of the channel. + val channelId: String = notificationDataModel.mChannelId + + // NotificationChannels are required for Notifications on O (API 26) and above. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // The user-visible name of the channel. + val channelName: CharSequence = notificationDataModel.mChannelName + // The user-visible description of the channel. + val channelDescription: String = notificationDataModel.mChannelDescription + val channelImportance: Int = notificationDataModel.mChannelImportance + val channelEnableVibrate: Boolean = notificationDataModel.mChannelEnableVibrate + val channelLockscreenVisibility: Int = notificationDataModel.mChannelLockscreenVisibility + + // Initializes NotificationChannel. + val notificationChannel = NotificationChannel(channelId, channelName, channelImportance) + notificationChannel.description = channelDescription + notificationChannel.enableVibration(channelEnableVibrate) + notificationChannel.lightColor = Color.BLUE + notificationChannel.lockscreenVisibility = channelLockscreenVisibility + + // Adds NotificationChannel to system. Attempting to create an existing notification + // channel with its original values performs no operation, so it's safe to perform the + // below sequence. + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(notificationChannel) + } + return channelId + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_arrow_bottom.xml b/android/app/src/main/res/drawable/ic_arrow_bottom.xml new file mode 100644 index 00000000..1aeaa998 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_arrow_bottom.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_arrow_top.xml b/android/app/src/main/res/drawable/ic_arrow_top.xml new file mode 100644 index 00000000..1d112693 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_arrow_top.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_end_call.xml b/android/app/src/main/res/drawable/ic_end_call.xml new file mode 100644 index 00000000..724ce9db --- /dev/null +++ b/android/app/src/main/res/drawable/ic_end_call.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/layout/notifi_video_view.xml b/android/app/src/main/res/layout/notifi_video_view.xml new file mode 100644 index 00000000..3afd925b --- /dev/null +++ b/android/app/src/main/res/layout/notifi_video_view.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From d0f1f9e17fcd11febef179ae59470dcbee43536c Mon Sep 17 00:00:00 2001 From: mosazaid Date: Tue, 6 Jul 2021 17:07:28 +0300 Subject: [PATCH 05/13] notification with timer and bug fixes --- .../main/kotlin/com/hmg/hmgDr/MainActivity.kt | 12 +- .../VideoStreamFloatingWidgetService.kt | 108 +++++++++++------- .../hmg/hmgDr/model/NotificationVideoModel.kt | 2 +- .../src/main/res/layout/notifi_video_view.xml | 9 +- 4 files changed, 80 insertions(+), 51 deletions(-) diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt index e20eef1e..a2c0f742 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt @@ -111,12 +111,14 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, // start service // serviceIntent = Intent(this@MainActivity, VideoStreamContainerService::class.java) - serviceIntent = Intent(this@MainActivity, VideoStreamFloatingWidgetService::class.java) - serviceIntent?.run { - putExtras(arguments) - action = VideoStreamFloatingWidgetService.ACTION_START_CALL + if (videoStreamService == null || videoStreamService?.serviceRunning == false){ + serviceIntent = Intent(this@MainActivity, VideoStreamFloatingWidgetService::class.java) + serviceIntent?.run { + putExtras(arguments) + action = VideoStreamFloatingWidgetService.ACTION_START_CALL + } + checkFloatingWidgetPermission() } - checkFloatingWidgetPermission() } private fun checkFloatingWidgetPermission() { diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt index f79f23a4..96f0ce1f 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt @@ -18,10 +18,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.view.GestureDetectorCompat import com.hmg.hmgDr.R -import com.hmg.hmgDr.model.ChangeCallStatusRequestModel -import com.hmg.hmgDr.model.GetSessionStatusModel -import com.hmg.hmgDr.model.NotificationVideoModel -import com.hmg.hmgDr.model.SessionStatusModel +import com.hmg.hmgDr.model.* import com.hmg.hmgDr.ui.VideoCallContract import com.hmg.hmgDr.ui.VideoCallPresenterImpl import com.hmg.hmgDr.ui.VideoCallResponseListener @@ -115,6 +112,9 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, private var isSpeckerClicked = false private var isMicClicked = false private var elapsedTime: Long = 0 + private var formattedCallTime: String = "00:00" + private lateinit var notificationData: NotificationVideoModel + private var resume = false var isFullScreen: Boolean = true private var isCircle: Boolean = false @@ -125,7 +125,8 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, private val serviceBinder: IBinder = VideoStreamBinder() // Notification variables - private lateinit var mNotificationManagerCompat: NotificationManagerCompat + private var mNotificationManagerCompat: NotificationManagerCompat? = null + private lateinit var notificationCompatBuilder: NotificationCompat.Builder override fun onBind(intent: Intent?): IBinder { return serviceBinder @@ -180,14 +181,21 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, get() = this@VideoStreamFloatingWidgetService } - override fun onDestroy() { mWindowManager?.removeView(floatingWidgetView) // disconnectSession() - cmTimer.stop() super.onDestroy() } + private fun stopService() { + // because problem that timer still continue longer a bit than service so notification called again + mNotificationManagerCompat = null + cmTimer.stop() + stopForeground(true) + stopSelf() + + } + /* Add Floating Widget View to Window Manager */ private fun addFloatingWidgetView(inflater: LayoutInflater) { //Inflate the removing view layout we created @@ -225,7 +233,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, mWindowManager?.addView(floatingWidgetView, windowManagerParams) } - @SuppressLint("ClickableViewAccessibility") + @SuppressLint("ClickableViewAccessibility", "RestrictedApi") private fun init(view: View) { initUI(view) @@ -279,9 +287,22 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, elapsedTime += 1000 } val format = "%1$02d:%2$02d" // two digits + formattedCallTime = String.format(format, minutes, seconds) + arg0?.text = formattedCallTime - arg0?.text = String.format(format, minutes, seconds) - Log.d(TAG, "onChronometerTick: $minutes : $seconds") + try { + notificationCompatBuilder.contentView.setChronometer(R.id.notify_timer, cmTimer.base, null, true) + }catch (e : Exception){} + + // for change notification timer + if (mNotificationManagerCompat != null) { + val bigTextStyle = setNotificationBigStyle() + notificationCompatBuilder.setStyle(bigTextStyle) + mNotificationManagerCompat?.notify( + ONGOING_NOTIFICATION_ID, + notificationCompatBuilder.build() + ) + } } videoCallPresenter = VideoCallPresenterImpl(this, baseUrl) @@ -536,6 +557,9 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, } fun onMinimizedClicked() { + if (isCircle){ + onMiniCircleClicked() + } if (isFullScreen) { windowManagerParams.width = 400 windowManagerParams.height = 600 @@ -759,8 +783,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, private fun disconnectSession() { if (mSession == null) { videoCallResponseListener?.onCallFinished(Activity.RESULT_CANCELED) - stopForeground(true) - stopSelf() + stopService() return } @@ -787,8 +810,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, sessionStatusModel!!.vcid ) ) - stopForeground(true) - stopSelf() + stopService() } private fun subscribeToStream(stream: Stream) { @@ -899,8 +921,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, returnIntent.putExtra("sessionStatusNotRespond", sessionStatusModel) disconnectSession() videoCallResponseListener?.onCallFinished(Activity.RESULT_OK, returnIntent) - stopSelf() - stopForeground(true) + stopService() } } @@ -927,7 +948,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, private fun addForegroundService() { mNotificationManagerCompat = NotificationManagerCompat.from(applicationContext) - val areNotificationsEnabled = mNotificationManagerCompat.areNotificationsEnabled() + val areNotificationsEnabled = mNotificationManagerCompat!!.areNotificationsEnabled() if (!areNotificationsEnabled) { Toast.makeText( this, @@ -941,28 +962,33 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, generateBigTextStyleNotification() } + private fun setNotificationBigStyle(): NotificationCompat.BigTextStyle { + notificationData.mSummaryText = formattedCallTime + + return NotificationCompat.BigTextStyle() // Overrides ContentText in the big form of the template. + .bigText(notificationData.mBigText) // Overrides ContentTitle in the big form of the template. + .setBigContentTitle(notificationData.mBigContentTitle) // Summary line after the detail section in the big form of the template. + // Note: To improve readability, don't overload the user with info. If Summary Text + // doesn't add critical information, you should skip it. + .setSummaryText(notificationData.mSummaryText) + } + private fun generateBigTextStyleNotification() { - val notificationData: NotificationVideoModel = + notificationData = NotificationVideoModel( sessionStatusModel!!.patientName, "Tap to return to call", CHANNEL_DEFAULT_IMPORTANCE, CHANNEL_DEFAULT_NAME, "Video call stream background", - mSummaryText = "timer" + mSummaryText = formattedCallTime ) // 1. Create/Retrieve Notification Channel for O and beyond devices (26+). val notificationChannelId: String = NotificationUtil.createNotificationChannel(this, notificationData) // 2. Build the BIG_TEXT_STYLE. - val bigTextStyle = - NotificationCompat.BigTextStyle() // Overrides ContentText in the big form of the template. - .bigText(notificationData.mBigText) // Overrides ContentTitle in the big form of the template. - .setBigContentTitle(notificationData.mBigContentTitle) // Summary line after the detail section in the big form of the template. - // Note: To improve readability, don't overload the user with info. If Summary Text - // doesn't add critical information, you should skip it. - .setSummaryText(notificationData.mSummaryText) + val bigTextStyle = setNotificationBigStyle() // 3. Set up main Intent for notification. val pendingIntent: PendingIntent = @@ -981,20 +1007,16 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, .let { notificationIntent -> PendingIntent.getService(this, 0, notificationIntent, 0) } -// val endCallAction = NotificationCompat.Action.Builder( -// R.drawable.ic_end_call, -// "End Call", -// endCallPendingIntent -// ) -// .build() - - // 5. Build and issue the notification. - // Notification Channel Id is ignored for Android pre O (26). - + val endCallAction = NotificationCompat.Action.Builder( + R.drawable.ic_end_call, + "End Call", + endCallPendingIntent + ) + .build() // 5. Build and issue the notification. // Notification Channel Id is ignored for Android pre O (26). - val notificationCompatBuilder = notificationChannelId?.let { + notificationCompatBuilder = notificationChannelId.let { NotificationCompat.Builder( applicationContext, it ) @@ -1008,8 +1030,9 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, // notification's content mRemoteViews.setTextViewText(R.id.notify_content, notificationData.mContentText) mRemoteViews.setOnClickPendingIntent(R.id.btn_end, endCallPendingIntent) + mRemoteViews.setChronometer(R.id.notify_timer, SystemClock.elapsedRealtime(), null, false) - val notification: Notification = notificationCompatBuilder + notificationCompatBuilder // BIG_TEXT_STYLE sets title and content for API 16 (4.1 and after). .setStyle(bigTextStyle) // Title for API <16 (4.0 and below) devices. @@ -1033,10 +1056,14 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, .setPriority(notificationData.mPriority) .setVisibility(notificationData.mChannelLockscreenVisibility) // .addAction(endCallAction) - .build() + .setUsesChronometer(true) + .setWhen(elapsedTime) + .setOnlyAlertOnce(true) // This will ensure that updates to an existing notification won't sound/vibrate https://stackoverflow.com/a/52349466/6246772 - mNotificationManagerCompat.notify(ONGOING_NOTIFICATION_ID, notification) + val notification = notificationCompatBuilder + .build() + mNotificationManagerCompat!!.notify(ONGOING_NOTIFICATION_ID, notification) startForeground(ONGOING_NOTIFICATION_ID, notification) /*val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE) @@ -1076,7 +1103,6 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, startActivity(intent) } - /** * OnTouch actions */ diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt index 78c15a91..6b6d9217 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt @@ -18,7 +18,7 @@ class NotificationVideoModel constructor( // Unique data for this Notification.Style: var mBigContentTitle: String = mContentTitle, val mBigText: String = mContentText, - val mSummaryText: String + var mSummaryText: String ) : NotificationDataModel( mContentTitle, diff --git a/android/app/src/main/res/layout/notifi_video_view.xml b/android/app/src/main/res/layout/notifi_video_view.xml index 3afd925b..cdc87a4b 100644 --- a/android/app/src/main/res/layout/notifi_video_view.xml +++ b/android/app/src/main/res/layout/notifi_video_view.xml @@ -32,17 +32,18 @@ android:textColor="@color/white" android:textSize="@dimen/text_size_small" /> - + android:format="MM:SS" + tools:text="25:45" /> Date: Wed, 7 Jul 2021 16:59:33 +0300 Subject: [PATCH 06/13] notification with timer and bug fixes --- .../VideoStreamFloatingWidgetService.kt | 16 +++- .../hmg/hmgDr/model/NotificationVideoModel.kt | 4 +- .../com/hmg/hmgDr/util/NotificationUtil.kt | 4 +- .../src/main/res/layout/notifi_video_view.xml | 24 +---- .../res/layout/notifi_video_view_expand.xml | 93 +++++++++++++++++++ 5 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 android/app/src/main/res/layout/notifi_video_view_expand.xml diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt index 96f0ce1f..02413827 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt @@ -292,7 +292,8 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, try { notificationCompatBuilder.contentView.setChronometer(R.id.notify_timer, cmTimer.base, null, true) - }catch (e : Exception){} + notificationCompatBuilder.bigContentView.setChronometer(R.id.notify_timer, cmTimer.base, null, true) + } catch (e : Exception){} // for change notification timer if (mNotificationManagerCompat != null) { @@ -1029,8 +1030,12 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, mRemoteViews.setTextViewText(R.id.notify_title, notificationData.mContentTitle) // notification's content mRemoteViews.setTextViewText(R.id.notify_content, notificationData.mContentText) - mRemoteViews.setOnClickPendingIntent(R.id.btn_end, endCallPendingIntent) - mRemoteViews.setChronometer(R.id.notify_timer, SystemClock.elapsedRealtime(), null, false) + val mRemoteViewsExpand = RemoteViews(packageName, R.layout.notifi_video_view_expand) + mRemoteViewsExpand.setImageViewResource(R.id.iv_icon, R.mipmap.ic_launcher) + mRemoteViewsExpand.setTextViewText(R.id.notify_title, notificationData.mContentTitle) + mRemoteViewsExpand.setTextViewText(R.id.notify_content, notificationData.mContentText) + mRemoteViewsExpand.setOnClickPendingIntent(R.id.btn_end, endCallPendingIntent) + mRemoteViewsExpand.setChronometer(R.id.notify_timer, SystemClock.elapsedRealtime(), null, false) notificationCompatBuilder // BIG_TEXT_STYLE sets title and content for API 16 (4.1 and after). @@ -1047,9 +1052,9 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, ) ) .setCustomContentView(mRemoteViews) - .setCustomBigContentView(mRemoteViews) + .setCustomBigContentView(mRemoteViewsExpand) .setContentIntent(pendingIntent) - .setDefaults(NotificationCompat.DEFAULT_ALL) +// .setDefaults(NotificationCompat.DEFAULT_ALL) // Set primary color (important for Wear 2.0 Notifications). .setColor(ContextCompat.getColor(applicationContext, R.color.colorPrimary)) .setCategory(Notification.CATEGORY_SERVICE) @@ -1059,6 +1064,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, .setUsesChronometer(true) .setWhen(elapsedTime) .setOnlyAlertOnce(true) // This will ensure that updates to an existing notification won't sound/vibrate https://stackoverflow.com/a/52349466/6246772 + .setSound(null) val notification = notificationCompatBuilder .build() diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt index 6b6d9217..9a0835f3 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/model/NotificationVideoModel.kt @@ -12,8 +12,8 @@ class NotificationVideoModel constructor( mChannelName: CharSequence, mChannelDescription: String, mPriority: Int = NotificationCompat.PRIORITY_DEFAULT, - mChannelImportance: Int = NotificationManager.IMPORTANCE_DEFAULT, - mChannelEnableVibrate: Boolean = false, + mChannelImportance: Int = NotificationManager.IMPORTANCE_LOW, + mChannelEnableVibrate: Boolean = true, mChannelLockscreenVisibility: Int = NotificationCompat.VISIBILITY_PUBLIC, // Unique data for this Notification.Style: var mBigContentTitle: String = mContentTitle, diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/util/NotificationUtil.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/util/NotificationUtil.kt index 13d6bd19..1c55e1b2 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/util/NotificationUtil.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/util/NotificationUtil.kt @@ -26,9 +26,11 @@ object NotificationUtil { // Initializes NotificationChannel. val notificationChannel = NotificationChannel(channelId, channelName, channelImportance) notificationChannel.description = channelDescription - notificationChannel.enableVibration(channelEnableVibrate) notificationChannel.lightColor = Color.BLUE notificationChannel.lockscreenVisibility = channelLockscreenVisibility + // no vibration + notificationChannel.vibrationPattern = longArrayOf(0) + notificationChannel.enableVibration(channelEnableVibrate) // Adds NotificationChannel to system. Attempting to create an existing notification // channel with its original values performs no operation, so it's safe to perform the diff --git a/android/app/src/main/res/layout/notifi_video_view.xml b/android/app/src/main/res/layout/notifi_video_view.xml index cdc87a4b..a2a81be1 100644 --- a/android/app/src/main/res/layout/notifi_video_view.xml +++ b/android/app/src/main/res/layout/notifi_video_view.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="88dp" android:background="@android:color/holo_blue_dark" android:orientation="vertical" android:padding="@dimen/padding_space_medium"> @@ -48,20 +48,19 @@ + android:src="@drawable/ic_arrow_bottom" /> - @@ -73,20 +72,7 @@ android:paddingStart="@dimen/padding_space_small" android:paddingEnd="@dimen/padding_space_small" android:textColor="@color/white" - android:textSize="@dimen/text_size_medium" + android:textSize="@dimen/text_size_small" android:text="Tap to return to call" /> - - \ No newline at end of file diff --git a/android/app/src/main/res/layout/notifi_video_view_expand.xml b/android/app/src/main/res/layout/notifi_video_view_expand.xml new file mode 100644 index 00000000..c06aa9ac --- /dev/null +++ b/android/app/src/main/res/layout/notifi_video_view_expand.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From adab2d47d38efda8a881ce8d4923c827aa832158 Mon Sep 17 00:00:00 2001 From: mosazaid Date: Thu, 8 Jul 2021 16:46:22 +0300 Subject: [PATCH 07/13] video stream bug disconnect fixes --- .../VideoStreamFloatingWidgetService.kt | 81 +++++++++++++++---- .../hmgDr/ui/fragment/VideoCallFragment.kt | 3 + lib/core/service/VideoCallService.dart | 4 +- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt index 02413827..f76104dc 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt @@ -182,8 +182,13 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, } override fun onDestroy() { - mWindowManager?.removeView(floatingWidgetView) -// disconnectSession() + try { + mWindowManager?.removeView(floatingWidgetView) + + } catch (e: Exception) { + Log.e("onDestroyService", "${e.localizedMessage}.") + } + // disconnectSession() super.onDestroy() } @@ -193,7 +198,12 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, cmTimer.stop() stopForeground(true) stopSelf() - + videoCallResponseListener?.minimizeVideoEvent(false) + try { + mWindowManager?.removeView(floatingWidgetView) + } catch (e: Exception) { + Log.e("stopService", "${e.localizedMessage}.") + } } /* Add Floating Widget View to Window Manager */ @@ -291,9 +301,20 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, arg0?.text = formattedCallTime try { - notificationCompatBuilder.contentView.setChronometer(R.id.notify_timer, cmTimer.base, null, true) - notificationCompatBuilder.bigContentView.setChronometer(R.id.notify_timer, cmTimer.base, null, true) - } catch (e : Exception){} + notificationCompatBuilder.contentView.setChronometer( + R.id.notify_timer, + cmTimer.base, + null, + true + ) + notificationCompatBuilder.bigContentView.setChronometer( + R.id.notify_timer, + cmTimer.base, + null, + true + ) + } catch (e: Exception) { + } // for change notification timer if (mNotificationManagerCompat != null) { @@ -321,6 +342,17 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, mSession = Session.Builder(this, apiKey, sessionId).build() mSession!!.setSessionListener(this) mSession!!.connect(token) + + mSession!!.setReconnectionListener(object : Session.ReconnectionListener { + override fun onReconnecting(session: Session?) { + + } + + override fun onReconnected(session: Session?) { + session?.connect(token) + session?.onResume() + } + }) } catch (e: Exception) { e.printStackTrace() } @@ -558,20 +590,26 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, } fun onMinimizedClicked() { - if (isCircle){ + if (isCircle) { onMiniCircleClicked() } if (isFullScreen) { windowManagerParams.width = 400 windowManagerParams.height = 600 - mWindowManager?.updateViewLayout(floatingWidgetView, windowManagerParams) + try { + mWindowManager?.updateViewLayout(floatingWidgetView, windowManagerParams) + } catch (e: Exception) { + } recordContainer.visibility = View.GONE } else { windowManagerParams.width = WindowManager.LayoutParams.MATCH_PARENT windowManagerParams.height = WindowManager.LayoutParams.MATCH_PARENT - mWindowManager?.updateViewLayout(floatingWidgetView, windowManagerParams) + try { + mWindowManager?.updateViewLayout(floatingWidgetView, windowManagerParams) + } catch (e: Exception) { + } if (isRecording) { recordContainer.visibility = View.VISIBLE @@ -637,12 +675,18 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, windowManagerParams.width = 400 windowManagerParams.height = 600 - mWindowManager?.updateViewLayout(floatingWidgetView, windowManagerParams) + try { + mWindowManager?.updateViewLayout(floatingWidgetView, windowManagerParams) + } catch (e: Exception) { + } } else { windowManagerParams.width = 300 windowManagerParams.height = 300 - mWindowManager?.updateViewLayout(floatingWidgetView, windowManagerParams) + try { + mWindowManager?.updateViewLayout(floatingWidgetView, windowManagerParams) + } catch (e: Exception) { + } } isCircle = !isCircle @@ -728,10 +772,10 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, override fun onDisconnected(session: Session) { Log.d(TAG, "onDisconnected: disconnected from session " + session.sessionId) - mSession = null - cmTimer.stop() - disconnectSession() - videoCallResponseListener?.minimizeVideoEvent(false) +// mSession = null +// cmTimer.stop() +// disconnectSession() +// videoCallResponseListener?.minimizeVideoEvent(false) } override fun onError(session: Session, opentokError: OpentokError) { @@ -1035,7 +1079,12 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, mRemoteViewsExpand.setTextViewText(R.id.notify_title, notificationData.mContentTitle) mRemoteViewsExpand.setTextViewText(R.id.notify_content, notificationData.mContentText) mRemoteViewsExpand.setOnClickPendingIntent(R.id.btn_end, endCallPendingIntent) - mRemoteViewsExpand.setChronometer(R.id.notify_timer, SystemClock.elapsedRealtime(), null, false) + mRemoteViewsExpand.setChronometer( + R.id.notify_timer, + SystemClock.elapsedRealtime(), + null, + false + ) notificationCompatBuilder // BIG_TEXT_STYLE sets title and content for API 16 (4.1 and after). diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/ui/fragment/VideoCallFragment.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/ui/fragment/VideoCallFragment.kt index b020ed29..4d649aac 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/ui/fragment/VideoCallFragment.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/ui/fragment/VideoCallFragment.kt @@ -199,6 +199,7 @@ class VideoCallFragment : DialogFragment(), PermissionCallbacks, Session.Session } } + fun setCallListener(videoCallResponseListener: VideoCallResponseListener) { this.videoCallResponseListener = videoCallResponseListener } @@ -354,6 +355,8 @@ class VideoCallFragment : DialogFragment(), PermissionCallbacks, Session.Session checkClientConnected() handleVideoViewHeight(true) + + if (appLang == "ar") { progressBarLayout!!.layoutDirection = View.LAYOUT_DIRECTION_RTL } diff --git a/lib/core/service/VideoCallService.dart b/lib/core/service/VideoCallService.dart index f9693f69..73c31f91 100644 --- a/lib/core/service/VideoCallService.dart +++ b/lib/core/service/VideoCallService.dart @@ -33,8 +33,8 @@ class VideoCallService extends BaseService { // kToken: startCallRes.openTokenID, // kSessionId: startCallRes.openSessionID, // kApiKey:'46209962', - kToken: "T1==cGFydG5lcl9pZD00NzI0Nzk1NCZzaWc9NGIyZDljOTY3YjFiNWU1YzUzNzFmMjIyNjJmNmEzY2Y5NzZjOTdlYzpzZXNzaW9uX2lkPTFfTVg0ME56STBOemsxTkg1LU1UWXlNekEyTlRRMU9EVXhObjVrVFRoMFlVdFJXaXRYTWpadFZGZHFhSGxZVGpOdE1UVi1mZyZjcmVhdGVfdGltZT0xNjIzMDY1NDk1Jm5vbmNlPTAuMjM2Mjk0NTIwMTkyOTA4OTcmcm9sZT1wdWJsaXNoZXImZXhwaXJlX3RpbWU9MTYyNTY1NzQ5NCZpbml0aWFsX2xheW91dF9jbGFzc19saXN0PQ==", - kSessionId: "1_MX40NzI0Nzk1NH5-MTYyMzA2NTQ1ODUxNn5kTTh0YUtRWitXMjZtVFdqaHlYTjNtMTV-fg", + kToken: "T1==cGFydG5lcl9pZD00NzI0Nzk1NCZzaWc9NGUyZjgxMjFlYTFkNzU5NjcxNDY2ZTM2ZjM3YTVhNTI2NGY0NTI2NzpzZXNzaW9uX2lkPTJfTVg0ME56STBOemsxTkg1LU1UWXlOVGN5TmpnMk5qZzNOMzQ1YUhCcGRtcDFXbVpDTDFkNE1qbDRkWFY2TTA4cmIySi1mZyZjcmVhdGVfdGltZT0xNjI1NzI2ODg5Jm5vbmNlPTAuNjc2Nzc4OTQxNjA1MTMxNSZyb2xlPXB1Ymxpc2hlciZleHBpcmVfdGltZT0xNjI4MzE4ODg4JmluaXRpYWxfbGF5b3V0X2NsYXNzX2xpc3Q9", + kSessionId: "2_MX40NzI0Nzk1NH5-MTYyNTcyNjg2Njg3N345aHBpdmp1WmZCL1d4Mjl4dXV6M08rb2J-fg", kApiKey:'47247954', vcId: patient.vcId, isRecording: isRecording, From 8a4dcf23343558454716f5c3e8d8789310a764fa Mon Sep 17 00:00:00 2001 From: mosazaid Date: Mon, 12 Jul 2021 16:13:52 +0300 Subject: [PATCH 08/13] try fixing error that was originally registered here. Are you missing a call to unregisterReceiver()? --- android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt | 2 +- .../com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt index a2c0f742..3c879350 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt @@ -269,7 +269,7 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, private fun unbindService() { if (bound) { - videoStreamService!!.videoCallResponseListener = null // unregister + videoStreamService?.videoCallResponseListener = null // unregister // videoStreamService!!.mActivity = null unbindService(serviceConnection) bound = false diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt index 3c4a5dc1..cdd9ebc0 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt @@ -833,6 +833,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, } if (mSubscriber != null) { + mSubscriber!!.subscribeToAudio = false // mosa added for error that was originally registered here. Are you missing a call to unregisterReceiver()? mSubscriberViewContainer.removeView(mSubscriber!!.view) mSession!!.unsubscribe(mSubscriber) mSubscriber!!.destroy() From 529711d2ce124cd9f81128f99319562a79cdddad Mon Sep 17 00:00:00 2001 From: mosazaid Date: Sun, 25 Jul 2021 12:33:27 +0300 Subject: [PATCH 09/13] video stream crash working to fix --- android/app/build.gradle | 1 + android/app/src/debug/AndroidManifest.xml | 64 --- android/app/src/main/AndroidManifest.xml | 31 +- .../kotlin/com/hmg/hmgDr/AppApplication.kt | 12 + .../main/kotlin/com/hmg/hmgDr/MainActivity.kt | 88 ++- .../VideoStreamFloatingWidgetService.kt | 47 +- .../com/hmg/hmgDr/errorhandler/Action.kt | 7 + .../com/hmg/hmgDr/errorhandler/ActionEntry.kt | 34 ++ .../hmg/hmgDr/errorhandler/ErrorHandler.kt | 505 ++++++++++++++++++ .../hmgDr/errorhandler/ExceptionMatcher.kt | 9 + .../com/hmg/hmgDr/errorhandler/Matcher .kt | 5 + .../hmg/hmgDr/errorhandler/MatcherFactory.kt | 11 + .../errorhandler/UnknownErrorCodeException.kt | 3 + .../hmg/hmgDr/errorhandler/retrofit/Range.kt | 45 ++ .../retrofit/RetrofitMatcherFactory.kt | 48 ++ .../hmg/hmgDr/globalErrorHandler/FileUtil.kt | 37 ++ .../LoggingExceptionHandler.kt | 39 ++ .../globalErrorHandler/UCEDefaultActivity.kt | 6 + .../globalErrorHandler/UCEFileProvider.kt | 6 + .../hmgDr/globalErrorHandler/UCEHandler.kt | 280 ++++++++++ .../hmg/hmgDr/util/audio/CustomAudioDevice.kt | 467 ++++++++++++++++ .../res/layout/default_error_activity.xml | 81 +++ android/app/src/main/res/values/strings.xml | 3 +- .../app/src/main/res/xml/provider_paths.xml | 4 + lib/core/service/VideoCallService.dart | 29 +- .../viewModel/authentication_view_model.dart | 2 +- 26 files changed, 1760 insertions(+), 104 deletions(-) delete mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/AppApplication.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/Action.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ActionEntry.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ErrorHandler.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ExceptionMatcher.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/Matcher .kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/MatcherFactory.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/UnknownErrorCodeException.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/retrofit/Range.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/retrofit/RetrofitMatcherFactory.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/FileUtil.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/LoggingExceptionHandler.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEDefaultActivity.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEFileProvider.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEHandler.kt create mode 100644 android/app/src/main/kotlin/com/hmg/hmgDr/util/audio/CustomAudioDevice.kt create mode 100644 android/app/src/main/res/layout/default_error_activity.xml create mode 100644 android/app/src/main/res/xml/provider_paths.xml diff --git a/android/app/build.gradle b/android/app/build.gradle index 2e124398..c7b2631e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -108,6 +108,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.6.2' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1' + implementation 'com.squareup.retrofit2:adapter-rxjava:2.1.0' } apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 23a5488a..00000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2bd477be..82805ecc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,20 +15,25 @@ - - + + + + - - - - @@ -59,6 +60,22 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/AppApplication.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/AppApplication.kt new file mode 100644 index 00000000..8b5994d7 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/AppApplication.kt @@ -0,0 +1,12 @@ +package com.hmg.hmgDr + +import com.hmg.hmgDr.globalErrorHandler.LoggingExceptionHandler +import io.flutter.app.FlutterApplication + +class AppApplication : FlutterApplication() { + + override fun onCreate() { + super.onCreate() + LoggingExceptionHandler(this, "ErrorFile") + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt index 3c879350..5120cb22 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/MainActivity.kt @@ -1,5 +1,6 @@ package com.hmg.hmgDr +import android.Manifest import android.app.Activity import android.content.ComponentName import android.content.Context @@ -17,16 +18,23 @@ import com.google.gson.GsonBuilder import com.hmg.hmgDr.model.GetSessionStatusModel import com.hmg.hmgDr.model.SessionStatusModel import com.hmg.hmgDr.Service.VideoStreamFloatingWidgetService +import com.hmg.hmgDr.globalErrorHandler.FileUtil +import com.hmg.hmgDr.globalErrorHandler.LoggingExceptionHandler import com.hmg.hmgDr.ui.VideoCallResponseListener +import com.hmg.hmgDr.ui.fragment.VideoCallFragment +import com.opentok.android.Session import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant +import pub.devrel.easypermissions.AfterPermissionGranted +import pub.devrel.easypermissions.AppSettingsDialog +import pub.devrel.easypermissions.EasyPermissions class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, - VideoCallResponseListener { + VideoCallResponseListener, EasyPermissions.PermissionCallbacks { /* Permission request code to draw over other apps */ private val DRAW_OVER_OTHER_APP_PERMISSION_REQUEST_CODE = 1222 @@ -41,6 +49,10 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, private var videoStreamService: VideoStreamFloatingWidgetService? = null private var bound = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) @@ -111,7 +123,7 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, // start service // serviceIntent = Intent(this@MainActivity, VideoStreamContainerService::class.java) - if (videoStreamService == null || videoStreamService?.serviceRunning == false){ + if (videoStreamService == null || videoStreamService?.serviceRunning == false) { serviceIntent = Intent(this@MainActivity, VideoStreamFloatingWidgetService::class.java) serviceIntent?.run { putExtras(arguments) @@ -122,6 +134,8 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, } private fun checkFloatingWidgetPermission() { + requestPermissions() + //Check if the application has draw over other apps permission or not? //This permission is by default available for API<23. But for API > 23 //you have to ask for the permission in runtime. @@ -177,16 +191,20 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, } } else*/ if (requestCode == DRAW_OVER_OTHER_APP_PERMISSION_REQUEST_CODE) { //Check if the permission is granted or not. - if (resultCode == RESULT_OK) - //If permission granted start floating widget service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Settings.canDrawOverlays(this)) { + startFloatingWidgetService() + } else { + //Permission is not available then display toast + Toast.makeText( + this, + "Draw over other app permission not available. App won\\'t work without permission. Please try again.", + Toast.LENGTH_SHORT + ).show() + } + } else { startFloatingWidgetService() - else - //Permission is not available then display toast - Toast.makeText( - this, - "Draw over other app permission not available. App won\\'t work without permission. Please try again.", - Toast.LENGTH_SHORT - ).show() + } } else { super.onActivityResult(requestCode, resultCode, data) } @@ -294,4 +312,52 @@ class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler, } } + + @AfterPermissionGranted(RC_READ_WRITE) + private fun requestPermissions() { + val perms = arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + if (EasyPermissions.hasPermissions(this, *perms)) { + } else { + EasyPermissions.requestPermissions( + this, + getString(R.string.remaining_ar), + RC_READ_WRITE, + *perms + ) + } + } + + override fun onPermissionsGranted(requestCode: Int, perms: MutableList?) { + + } + + override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { + if (EasyPermissions.somePermissionPermanentlyDenied(this@MainActivity, perms)) { + AppSettingsDialog.Builder(this) + .setTitle(getString(R.string.title_settings_dialog)) + .setRationale(getString(R.string.rationale_ask_again)) + .setPositiveButton(getString(R.string.setting)) + .setNegativeButton(getString(R.string.cancel)) + .setRequestCode(RC_SETTINGS_SCREEN_PERM) + .build() + .show() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + companion object{ + private const val RC_READ_WRITE = 1 + private const val RC_SETTINGS_SCREEN_PERM = 123 + } } diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt index cdd9ebc0..fcf86260 100644 --- a/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/Service/VideoStreamFloatingWidgetService.kt @@ -25,6 +25,7 @@ import com.hmg.hmgDr.ui.VideoCallResponseListener import com.hmg.hmgDr.util.DynamicVideoRenderer import com.hmg.hmgDr.util.NotificationUtil import com.hmg.hmgDr.util.ViewsUtil +import com.hmg.hmgDr.util.audio.CustomAudioDevice import com.opentok.android.* import kotlin.math.ceil @@ -45,6 +46,9 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, const val CHANNEL_DEFAULT_NAME = "Video cAll" const val ONGOING_NOTIFICATION_ID = 1 + private const val TEST_DURATION = 20 //test quality duration in seconds + private const val TIME_WINDOW = 3 //3 seconds + private const val TIME_VIDEO_TEST = 15 //time interval to check the video quality in seconds const val ACTION_START_CALL = "com.hmg.hmgDr.Service.action.startCall" const val ACTION_MINIMIZE_CALL = "com.hmg.hmgDr.Service.action.minimizeCall" @@ -115,6 +119,22 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, private var formattedCallTime: String = "00:00" private lateinit var notificationData: NotificationVideoModel + private val mVideoPLRatio = 0.0 + private val mVideoBw: Long = 0 + private val mAudioPLRatio = 0.0 + private val mAudioBw: Long = 0 + private val mPrevVideoPacketsLost: Long = 0 + private val mPrevVideoPacketsRcvd: Long = 0 + private val mPrevVideoTimestamp = 0.0 + private val mPrevVideoBytes: Long = 0 + private val mPrevAudioPacketsLost: Long = 0 + private val mPrevAudioPacketsRcvd: Long = 0 + private val mPrevAudioTimestamp = 0.0 + private val mPrevAudioBytes: Long = 0 + private val mStartTestTime: Long = 0 + + private val audioOnly = false + private var resume = false var isFullScreen: Boolean = true private var isCircle: Boolean = false @@ -184,6 +204,9 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, override fun onDestroy() { try { mWindowManager?.removeView(floatingWidgetView) + floatingWidgetView = null + mSession = null + mWindowManager = null } catch (e: Exception) { Log.e("onDestroyService", "${e.localizedMessage}.") @@ -339,6 +362,15 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, { miniCircleDoubleTap() }) ) try { + // Add a custom audio device before session initialization + + // Add a custom audio device before session initialization + val customAudioDevice = CustomAudioDevice( + this + ) + customAudioDevice.setRendererMute(true) + AudioDeviceManager.setAudioDevice(customAudioDevice) + mSession = Session.Builder(this, apiKey, sessionId).build() mSession!!.setSessionListener(this) mSession!!.connect(token) @@ -755,6 +787,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, // .renderer(ThumbnailCircleVideoRenderer(requireContext())) .build() mPublisher!!.setPublisherListener(this) + mPublisher!!.audioFallbackEnabled = false if (mPublisher!!.view is GLSurfaceView) { (mPublisher!!.view as GLSurfaceView).setZOrderOnTop(true) } @@ -833,7 +866,6 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, } if (mSubscriber != null) { - mSubscriber!!.subscribeToAudio = false // mosa added for error that was originally registered here. Are you missing a call to unregisterReceiver()? mSubscriberViewContainer.removeView(mSubscriber!!.view) mSession!!.unsubscribe(mSubscriber) mSubscriber!!.destroy() @@ -891,6 +923,9 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, */ override fun onVideoDataReceived(subscriberKit: SubscriberKit?) { + if (mSubscriber == null) { + return + } mSubscriber!!.setStyle( BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL @@ -972,7 +1007,7 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, } } - fun onCallChangeCallStatusSuccessful(){ + fun onCallChangeCallStatusSuccessful() { val returnIntent = Intent() returnIntent.putExtra("CallEnd", sessionStatusModel) videoCallResponseListener?.onCallFinished(Activity.RESULT_CANCELED, returnIntent) @@ -1232,13 +1267,13 @@ class VideoStreamFloatingWidgetService : Service(), Session.SessionListener, y_cord_Destination = y_init_margin + y_diff val barHeight: Int = getStatusBarHeight() if (y_cord_Destination < 0) { -// y_cord_Destination = 0 + y_cord_Destination = 0 // y_cord_Destination = // -(szWindow.y - (videoCallContainer.height /*+ barHeight*/)) - y_cord_Destination = -(szWindow.y / 2) +// y_cord_Destination = -(szWindow.y / 2) } else if (y_cord_Destination + (videoCallContainer.height + barHeight) > szWindow.y) { -// y_cord_Destination = szWindow.y - (videoCallContainer.height + barHeight) - y_cord_Destination = (szWindow.y / 2) + y_cord_Destination = szWindow.y - (videoCallContainer.height + barHeight) +// y_cord_Destination = (szWindow.y / 2) } layoutParams.y = y_cord_Destination diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/Action.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/Action.kt new file mode 100644 index 00000000..2b1f3dad --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/Action.kt @@ -0,0 +1,7 @@ +package com.hmg.hmgDr.errorhandler + +/** + * A functional interface representing an action that gets executed + * upon an error by an [ErrorHandler]. + */ +typealias Action = (Throwable, ErrorHandler) -> Unit diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ActionEntry.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ActionEntry.kt new file mode 100644 index 00000000..8fc636dd --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ActionEntry.kt @@ -0,0 +1,34 @@ +package com.hmg.hmgDr.errorhandler + + +/** + * Container to ease passing around a tuple of two objects. This object provides a sensible + * implementation of equals(), returning true if equals() is true on each of the contained + * objects. + */ +class ActionEntry +/** + * Constructor for an ActionEntry. + * + * @param matcher the matcher object in the ActionEntry + * @param action the action object in the ActionEntry + */( + val matcher: Matcher, + val action: Action +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as ActionEntry + return if (matcher != that.matcher) false else action == that.action + } + + /** + * Compute a hash code using the hash codes of the underlying objects + * + * @return a hashcode of the ActionEntry + */ + override fun hashCode(): Int { + return matcher.hashCode() xor action.hashCode() + } +} diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ErrorHandler.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ErrorHandler.kt new file mode 100644 index 00000000..d504d0f4 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ErrorHandler.kt @@ -0,0 +1,505 @@ +package com.hmg.hmgDr.errorhandler + +import java.util.HashMap +import kotlin.reflect.KClass + +/** + * https://github.com/mgray88/kotlin-error-handler + * An ErrorHandler is responsible for handling an error by executing one or more actions, + * instances of [Action], that are found to match the error. + */ + +class ErrorHandler private constructor() { + + private val errorCodeMap = mutableMapOf, MatcherFactory<*>>() + private val actions = mutableListOf() + private val otherwiseActions= mutableListOf() + private val alwaysActions = mutableListOf() + private var localContext: ThreadLocal = object : ThreadLocal() { + override fun initialValue(): Context { + return Context() + } + } + private var parentErrorHandler: ErrorHandler? = null + + /** + * Create a new ErrorHandler with the given one as parent. + * + * @param parentErrorHandler the parent @{link ErrorHandler} + */ + private constructor(parentErrorHandler: ErrorHandler) : this() { + this.parentErrorHandler = parentErrorHandler + } + + /** + * Register `action` to be executed by [.handle], + * if the thrown error matches the `matcher`. + * + * @param matcher a matcher to match the thrown error + * @param action the associated action + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun on( + matcher: Matcher, + action: Action + ): ErrorHandler { + actions.add(ActionEntry(matcher, action)) + return this + } + + /** + * Kotlin <1.4 lambda compatibility for `[.on(Matcher, Action)]` + */ + fun on( + matcher: (Throwable) -> Boolean, + action: Action + ): ErrorHandler { + return on(object : Matcher { + override fun matches(throwable: Throwable): Boolean { + return matcher(throwable) + } + }, action) + } + + /** + * Register `action` to be executed by [.handle], + * if the thrown error is an instance of `exceptionClass`. + * + * @param exceptionClass the class of the error + * @param action the associated action + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun on( + exceptionClass: KClass, + action: Action + ): ErrorHandler { + actions.add(ActionEntry(ExceptionMatcher(exceptionClass), action)) + return this + } + + /** + * Register `action` to be executed by [.handle], + * if the thrown error is bound (associated) to `errorCode`. + * + * + * See [.bindClass] and [.bind] + * on how to associate arbitrary error codes with actual Throwables via [Matcher]. + * + * @param the error code type + * @param errorCode the error code + * @param action the associated action + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun on( + errorCode: T, + action: Action + ): ErrorHandler { + val matcherFactory: MatcherFactory = + getMatcherFactoryForErrorCode(errorCode) + ?: throw UnknownErrorCodeException(errorCode) + actions.add(ActionEntry(matcherFactory.build(errorCode), action)) + return this + } + + /** + * Register `action` to be executed in case no other *conditional* + * action gets executed. + * + * @param action the action + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun otherwise(action: Action): ErrorHandler { + otherwiseActions.add(action) + return this + } + + /** + * Register `action` to be executed on all errors. + * + * @param action the action + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun always(action: Action): ErrorHandler { + alwaysActions.add(action) + return this + } + + /** + * Skip all following actions registered via an `on` method + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun skipFollowing(): ErrorHandler { + localContext.get().skipFollowing = true + return this + } + + /** + * Skip all actions registered via [.always] + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun skipAlways(): ErrorHandler { + localContext.get().skipAlways = true + return this + } + + /** + * Skip the default matching actions if any + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun skipDefaults(): ErrorHandler { + localContext.get().skipDefaults = true + return this + } + + private fun handle( + error: Throwable, + context: ThreadLocal + ) { + localContext = context + val ctx = localContext.get() + for (actionEntry in actions) { + if (ctx.skipFollowing) break + if (actionEntry.matcher.matches(error)) { + actionEntry.action(error, this) + ctx.handled = true + } + } + if (!ctx.handled && otherwiseActions.isNotEmpty()) { + for (action in otherwiseActions) { + action(error, this) + ctx.handled = true + } + } + if (!ctx.skipAlways) { + for (action in alwaysActions) { + action(error, this) + ctx.handled = true + } + } + if (!ctx.skipDefaults) { + parentErrorHandler?.handle(error, localContext) + } + } + + /** + * Run a custom code block and assign current ErrorHandler instance + * to handle a possible exception throw in 'catch'. + * + * @param closure functional interface containing Exception prone code + */ + fun runHandling(closure: () -> Unit) { + try { + closure() + } catch (throwable: Throwable) { + handle(throwable, localContext) + } + } + + /** + * Handle `error` by executing all matching actions. + * + * @param error the error as a [Throwable] + */ + fun handle(error: Throwable) { + this.handle(error, localContext) + } + + /** + * Bind an `errorCode` to a `Matcher`, using a `MatcherFactory`. + * + * + * + * For example, when we need to catch a network timeout it's better to just write "timeout" + * instead of a train-wreck expression. So we need to bind this "timeout" error code to an actual + * condition that will check the actual error when it occurs to see if its a network timeout or not. + * + * + *
+     * ```
+     * ErrorHandler
+     *     .defaultErrorHandler()
+     *     .bind("timeout") { errorCode ->
+     *         Matcher { throwable ->
+     *             return (throwable is SocketTimeoutException) && throwable.message.contains("Read timed out")
+     *         }
+     *     }
+     *
+     * // ...
+     *
+     * ErrorHandler
+     *     .create()
+     *     .on("timeout") { throwable, handler ->
+     *         showOfflineScreen()
+     *     }
+     * ```
+     * 
+ * + * + * @param the error code type + * @param errorCode the errorCode value, can use a primitive for clarity and let it be autoboxed + * @param matcherFactory a factory that given an error code, provides a matcher to match the error against it + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun bind( + errorCode: T, + matcherFactory: MatcherFactory + ): ErrorHandler { + errorCodeMap[ErrorCodeIdentifier(errorCode)] = matcherFactory + return this + } + + /** + * Kotlin <1.4 lambda compatibility for `[.bind(T, MatcherFactory)]` + */ + fun bind( + errorCode: T, + matcherFactory: (T) -> (Throwable) -> Boolean + ): ErrorHandler { + return bind(errorCode, object : MatcherFactory { + override fun build(errorCode: T): Matcher { + val matcher = matcherFactory(errorCode) + return object : Matcher { + override fun matches(throwable: Throwable): Boolean { + return matcher(throwable) + } + } + } + }) + } + + /** + * Bind an `errorCode` `Class` to a `Matcher`, using a `MatcherFactory`. + * + * + * + * For example, when we prefer using plain integers to refer to HTTP errors instead of + * checking the HTTPException status code every time. + * + * + *
+     * ```
+     * ErrorHandler
+     *     .defaultErrorHandler()
+     *     .bindClass(Integer::class) { errorCode ->
+     *         Matcher { throwable ->
+     *             return throwable is HttpException && throwable.code() == errorCode
+     *         }
+     *     }
+     *
+     * // ...
+     *
+     * ErrorHandler
+     *     .create()
+     *     .on(404) { throwable, handler ->
+     *         showResourceNotFoundError()
+     *     }
+     *     .on(500) { throwable, handler ->
+     *         showServerError()
+     *     }
+     * ````
+     * 
+ * + * @param the error code type + * @param errorCodeClass the errorCode class + * @param matcherFactory a factory that given an error code, provides a matcher to match the error against it + * @return the current `ErrorHandler` instance - to use in command chains + */ + fun bindClass( + errorCodeClass: KClass, + matcherFactory: MatcherFactory + ): ErrorHandler { + errorCodeMap[ErrorCodeIdentifier(errorCodeClass)] = matcherFactory + return this + } + + /** + * Kotlin <1.4 lambda compatibility for `[.bindClass(KClass, MatcherFactory)]` + */ + fun bindClass( + errorCodeClass: KClass, + matcherFactory: (T) -> (Throwable) -> Boolean + ): ErrorHandler { + return bindClass(errorCodeClass, object : MatcherFactory { + override fun build(errorCode: T): Matcher { + val matcher = matcherFactory(errorCode) + return object : Matcher { + override fun matches(throwable: Throwable): Boolean { + return matcher(throwable) + } + } + } + }) + } + + @Suppress("UNCHECKED_CAST") + protected fun getMatcherFactoryForErrorCode(errorCode: T): MatcherFactory? { + var matcherFactory: MatcherFactory? + matcherFactory = errorCodeMap[ErrorCodeIdentifier(errorCode)] as? MatcherFactory + if (matcherFactory != null) { + return matcherFactory + } + + matcherFactory = errorCodeMap[ErrorCodeIdentifier(errorCode::class)] as? MatcherFactory + if (matcherFactory != null) { + return matcherFactory + } + return if (parentErrorHandler != null) { + parentErrorHandler?.getMatcherFactoryForErrorCode(errorCode) + } else null + } + + /** + * Clear ErrorHandler instance from all its registered Actions and Matchers. + */ + fun clear() { + actions.clear() + errorCodeMap.clear() + otherwiseActions.clear() + alwaysActions.clear() + localContext.get().clear() + } + + private class Context { + private val keys = HashMap() + var handled = false + var skipDefaults = false + var skipFollowing = false + var skipAlways = false + operator fun get(key: Any?): Any? { + return keys[key] + } + + fun put(key: String, value: Any): Any? { + return keys.put(key, value) + } + + fun remove(key: Any?): Any? { + return keys.remove(key) + } + + fun clear() { + keys.clear() + skipDefaults = false + skipFollowing = false + skipAlways = false + } + } + + /** + * Used to identify an error code either by its "literal" value + * or by its Class. + * + * + * When using custom objects as error codes, + * make sure you implement [Object.equals] to allow ErrorHandler + * perform equality comparisons between instances. + */ + private class ErrorCodeIdentifier { + private val errorCode: T? + private val errorCodeClass: KClass? + + internal constructor(errorCode: T) { + this.errorCode = errorCode + this.errorCodeClass = null + } + + internal constructor(errorCodeClass: KClass) { + this.errorCode = null + this.errorCodeClass = errorCodeClass + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + + val that = other as ErrorCodeIdentifier<*> + + if (if (errorCode != null) errorCode != that.errorCode else that.errorCode != null) return false + return if (errorCodeClass != null) errorCodeClass == that.errorCodeClass else that.errorCodeClass == null + } + + override fun hashCode(): Int { + var result = errorCode?.hashCode() ?: 0 + result = 31 * result + (errorCodeClass?.hashCode() ?: 0) + return result + } + } + + companion object { + private var defaultInstance: ErrorHandler? = null + + /** + * Create a new @{link ErrorHandler}, isolated from the default one. + * + * + * In other words, designed to handle all errors by itself without delegating + * to the default error handler. + * + * @return returns a new `ErrorHandler` instance + */ + @JvmStatic + fun createIsolated(): ErrorHandler { + return ErrorHandler() + } + + /** + * Create a new @{link ErrorHandler}, that delegates to the default one, or the + * parent @{link ErrorHandler} passed in + * + * Any default actions, are always executed after the ones registered on this one. + * + * @param parentErrorHandler `ErrorHandler` to use as the parent + * @return returns a new `ErrorHandler` instance + */ + @JvmStatic + fun create(parentErrorHandler: ErrorHandler? = null): ErrorHandler { + return ErrorHandler(parentErrorHandler ?: defaultErrorHandler()) + } + + /** + * Get the default @{link ErrorHandler}, a singleton object + * to which all other instances by default delegate to. + * + * @return the default @{link ErrorHandler} instance + */ + @JvmStatic + @Synchronized + fun defaultErrorHandler(): ErrorHandler { + if (defaultInstance == null) { + defaultInstance = + ErrorHandler() + } + return defaultInstance!! + } + } +} + +/** + * Wrapper around `[.on(KClass, Action)]` to allow action's `Throwable` parameter + * to be typed to the `Throwable` expected + */ +inline fun ErrorHandler.on( + noinline action: (T, ErrorHandler) -> Unit +): ErrorHandler { + return on(T::class) { throwable, errorHandler -> + action(throwable as T, errorHandler) + } +} + +/** + * Lazy `ErrorHandler` initializer which delegates to a parent, or the `defaultErrorHandler` + * if the parent is not supplied. Uses optional lambda function to add actions and bindings to + * the new `ErrorHandler` + * + * @param parentErrorHandler (optional) error handler to delegate default actions to + * @param apply (optional) apply function for adding actions and binding + * @return lazy initialized `ErrorHandler` + */ +inline fun errorHandler( + parentErrorHandler: ErrorHandler? = null, + noinline apply: (ErrorHandler.() -> Unit)? = null +) = lazy { + val eh = ErrorHandler.create(parentErrorHandler) + apply?.invoke(eh) + return@lazy eh +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ExceptionMatcher.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ExceptionMatcher.kt new file mode 100644 index 00000000..4ab02f8a --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/ExceptionMatcher.kt @@ -0,0 +1,9 @@ +package com.hmg.hmgDr.errorhandler + +import kotlin.reflect.KClass + +class ExceptionMatcher(private val errorClass: KClass) : Matcher { + override fun matches(throwable: Throwable): Boolean { + return errorClass.isInstance(throwable) + } +} diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/Matcher .kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/Matcher .kt new file mode 100644 index 00000000..acf8b3c1 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/Matcher .kt @@ -0,0 +1,5 @@ +package com.hmg.hmgDr.errorhandler + +interface Matcher { + fun matches(throwable: Throwable): Boolean +} diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/MatcherFactory.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/MatcherFactory.kt new file mode 100644 index 00000000..af296213 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/MatcherFactory.kt @@ -0,0 +1,11 @@ +package com.hmg.hmgDr.errorhandler + +interface MatcherFactory { + /** + * Build a [Matcher] to match the given error code against an error + * + * @param errorCode the error code + * @return a new [Matcher] + */ + fun build(errorCode: T): Matcher +} diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/UnknownErrorCodeException.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/UnknownErrorCodeException.kt new file mode 100644 index 00000000..46abc572 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/UnknownErrorCodeException.kt @@ -0,0 +1,3 @@ +package com.hmg.hmgDr.errorhandler + +class UnknownErrorCodeException(val errorCode: Any) : RuntimeException() diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/retrofit/Range.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/retrofit/Range.kt new file mode 100644 index 00000000..e6abc82e --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/retrofit/Range.kt @@ -0,0 +1,45 @@ +package com.hmg.hmgDr.errorhandler.retrofit + +/** + * Range class for HTTP status codes + */ +class Range private constructor(val lowerBound: Int, val upperBound: Int) { + + /** + * Checks if the passed httpStatusCode is contained in given range + * + * @param httpStatusCode the status code to check + * @return true if contains, otherwise false + */ + operator fun contains(httpStatusCode: Int): Boolean { + return httpStatusCode in lowerBound..upperBound + } + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val range = + o as Range + return if (lowerBound != range.lowerBound) false else upperBound == range.upperBound + } + + override fun hashCode(): Int { + var result = lowerBound + result = 31 * result + upperBound + return result + } + + companion object { + /** + * Creates a Range object with lower and upper bound + * @param lowerBound lower limit of Range + * @param upperBound upper limit of Range + * + * @return a Range instance + */ + @JvmStatic + fun of(lowerBound: Int, upperBound: Int): Range { + return Range(lowerBound, upperBound) + } + } +} diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/retrofit/RetrofitMatcherFactory.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/retrofit/RetrofitMatcherFactory.kt new file mode 100644 index 00000000..062fc30e --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/errorhandler/retrofit/RetrofitMatcherFactory.kt @@ -0,0 +1,48 @@ +package com.hmg.hmgDr.errorhandler.retrofit + +import com.hmg.hmgDr.errorhandler.Matcher +import com.hmg.hmgDr.errorhandler.MatcherFactory +import retrofit2.adapter.rxjava.HttpException + +object RetrofitMatcherFactory { + + /** + * Creates a [MatcherFactory] that checks HTTP statuses + * + * @return new MatcherFactory for Retrofit Rx HttpException that works with Integer + */ + @JvmStatic + fun create(): MatcherFactory { + return object : MatcherFactory { + override fun build(errorCode: Int): Matcher { + return object : Matcher { + override fun matches(throwable: Throwable): Boolean { + return throwable is HttpException && + throwable.code() == errorCode + } + } + } + } + } + + /** + * Creates a [MatcherFactory] that checks if HTTP status is in given [Range] + * + * @return new MatcherFactory for Retrofit Rx HttpException that works with Range + */ + @JvmStatic + fun createRange(): MatcherFactory { + return object : MatcherFactory { + override fun build(errorCode: Range): Matcher { + return object : Matcher { + override fun matches(throwable: Throwable): Boolean { + return throwable is HttpException && + errorCode.contains(throwable.code()) + } + } + } + } + } + + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/FileUtil.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/FileUtil.kt new file mode 100644 index 00000000..13fc2dd7 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/FileUtil.kt @@ -0,0 +1,37 @@ +package com.hmg.hmgDr.globalErrorHandler + +import android.os.Environment +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + + +object FileUtil { + + val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS") + + fun pushLog(body: String?) { + try { + val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + val time = SimpleDateFormat("HH:MM:SS", Locale.getDefault()).format(Date()) + val root = + File(Environment.getExternalStorageDirectory(),"error_log_dir") + // if external memory exists and folder with name Notes + if (!root.exists()) { + root.mkdirs() // this will create folder. + } + val oldFile = File(root, "error" + sdf.format(Date()).toString() + ".txt") // old file + if (oldFile.exists()) oldFile.delete() + val filepath = File(root, "error$date.txt") // file path to save + val bufferedWriter = BufferedWriter(FileWriter(filepath, true)) + bufferedWriter.append("\r\n") + bufferedWriter.append("\r\n").append(body).append(" Time : ").append(time) + bufferedWriter.flush() + } catch (e: IOException) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/LoggingExceptionHandler.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/LoggingExceptionHandler.kt new file mode 100644 index 00000000..cfbd26ca --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/LoggingExceptionHandler.kt @@ -0,0 +1,39 @@ +package com.hmg.hmgDr.globalErrorHandler + +import android.content.Context +import android.content.Intent +import com.hmg.hmgDr.MainActivity +import com.hmg.hmgDr.globalErrorHandler.FileUtil.pushLog + + +class LoggingExceptionHandler(private val context: Context, ErrorFile: String) : + Thread.UncaughtExceptionHandler { + private val rootHandler: Thread.UncaughtExceptionHandler + override fun uncaughtException(t: Thread, e: Throwable) { + object : Thread() { + override fun run() { + pushLog("UnCaught Exception is thrown in $error$e") + try { + sleep(500) + val intent = Intent(context, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + context.startActivity(intent) + } catch (e1: Exception) { + e1.printStackTrace() + } + } + }.start() + rootHandler.uncaughtException(t, e) + } + + companion object { + private val TAG = LoggingExceptionHandler::class.java.simpleName + lateinit var error: String + } + + init { + error = "$ErrorFile.error " + rootHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler(this) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEDefaultActivity.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEDefaultActivity.kt new file mode 100644 index 00000000..8ee4bbd0 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEDefaultActivity.kt @@ -0,0 +1,6 @@ +package com.hmg.hmgDr.globalErrorHandler + +import androidx.appcompat.app.AppCompatActivity + +class UCEDefaultActivity : AppCompatActivity() { +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEFileProvider.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEFileProvider.kt new file mode 100644 index 00000000..70376428 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEFileProvider.kt @@ -0,0 +1,6 @@ +package com.hmg.hmgDr.globalErrorHandler + +import androidx.core.content.FileProvider + +class UCEFileProvider : FileProvider() { +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEHandler.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEHandler.kt new file mode 100644 index 00000000..35654d1f --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/globalErrorHandler/UCEHandler.kt @@ -0,0 +1,280 @@ +package com.hmg.hmgDr.globalErrorHandler + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.ref.WeakReference; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayDeque; +import java.util.Date; +import java.util.Deque; +import java.util.Locale; +import kotlin.system.exitProcess + +class UCEHandler(val builder: Builder) { + + val EXTRA_STACK_TRACE = "EXTRA_STACK_TRACE" + val EXTRA_ACTIVITY_LOG = "EXTRA_ACTIVITY_LOG" + private val TAG = "UCEHandler" + private val UCE_HANDLER_PACKAGE_NAME = "com.rohitss.uceh" + private val DEFAULT_HANDLER_PACKAGE_NAME = "com.android.internal.os" + private val MAX_STACK_TRACE_SIZE = 131071 //128 KB - 1 + + private val MAX_ACTIVITIES_IN_LOG = 50 + private val SHARED_PREFERENCES_FILE = "uceh_preferences" + private val SHARED_PREFERENCES_FIELD_TIMESTAMP = "last_crash_timestamp" + private val activityLog: Deque = ArrayDeque(MAX_ACTIVITIES_IN_LOG) + var COMMA_SEPARATED_EMAIL_ADDRESSES: String? = null + + @SuppressLint("StaticFieldLeak") + private var application: Application? = null + private var isInBackground = true + private var isBackgroundMode = false + private var isUCEHEnabled = false + private var isTrackActivitiesEnabled = false + private var lastActivityCreated: WeakReference = WeakReference(null) + + fun UCEHandler(builder: Builder) { + isUCEHEnabled = builder.isUCEHEnabled + isTrackActivitiesEnabled = builder.isTrackActivitiesEnabled + isBackgroundMode = builder.isBackgroundModeEnabled + COMMA_SEPARATED_EMAIL_ADDRESSES = builder.commaSeparatedEmailAddresses + setUCEHandler(builder.context) + } + + private fun setUCEHandler(context: Context?) { + try { + if (context != null) { + val oldHandler = Thread.getDefaultUncaughtExceptionHandler() + if (oldHandler != null && oldHandler.javaClass.name.startsWith( + UCE_HANDLER_PACKAGE_NAME + ) + ) { + Log.e(TAG, "UCEHandler was already installed, doing nothing!") + } else { + if (oldHandler != null && !oldHandler.javaClass.name.startsWith( + DEFAULT_HANDLER_PACKAGE_NAME + ) + ) { + Log.e( + TAG, + "You already have an UncaughtExceptionHandler. If you use a custom UncaughtExceptionHandler, it should be initialized after UCEHandler! Installing anyway, but your original handler will not be called." + ) + } + application = context.getApplicationContext() as Application + //Setup UCE Handler. + Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler { thread, throwable -> + if (isUCEHEnabled) { + Log.e( + TAG, + "App crashed, executing UCEHandler's UncaughtExceptionHandler", + throwable + ) + if (hasCrashedInTheLastSeconds(application!!)) { + Log.e( + TAG, + "App already crashed recently, not starting custom error activity because we could enter a restart loop. Are you sure that your app does not crash directly on init?", + throwable + ) + if (oldHandler != null) { + oldHandler.uncaughtException(thread, throwable) + return@UncaughtExceptionHandler + } + } else { + setLastCrashTimestamp(application!!, Date().getTime()) + if (!isInBackground || isBackgroundMode) { + val intent = Intent(application, UCEDefaultActivity::class.java) + val sw = StringWriter() + val pw = PrintWriter(sw) + throwable.printStackTrace(pw) + var stackTraceString: String = sw.toString() + if (stackTraceString.length > MAX_STACK_TRACE_SIZE) { + val disclaimer = " [stack trace too large]" + stackTraceString = stackTraceString.substring( + 0, + MAX_STACK_TRACE_SIZE - disclaimer.length + ) + disclaimer + } + intent.putExtra(EXTRA_STACK_TRACE, stackTraceString) + if (isTrackActivitiesEnabled) { + val activityLogStringBuilder = StringBuilder() + while (!activityLog.isEmpty()) { + activityLogStringBuilder.append(activityLog.poll()) + } + intent.putExtra( + EXTRA_ACTIVITY_LOG, + activityLogStringBuilder.toString() + ) + } + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + application!!.startActivity(intent) + } else { + if (oldHandler != null) { + oldHandler.uncaughtException(thread, throwable) + return@UncaughtExceptionHandler + } + //If it is null (should not be), we let it continue and kill the process or it will be stuck + } + } + val lastActivity: Activity? = lastActivityCreated.get() + if (lastActivity != null) { + lastActivity.finish() + lastActivityCreated.clear() + } + killCurrentProcess() + } else oldHandler?.uncaughtException(thread, throwable) + }) + application!!.registerActivityLifecycleCallbacks(object : + Application.ActivityLifecycleCallbacks { + val dateFormat: DateFormat = + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) + var currentlyStartedActivities = 0 + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + if (activity.javaClass !== UCEDefaultActivity::class.java) { + lastActivityCreated = WeakReference(activity) + } + if (isTrackActivitiesEnabled) { + activityLog.add( + dateFormat.format(Date()) + .toString() + ": " + activity.javaClass + .getSimpleName() + " created\n" + ) + } + } + + override fun onActivityStarted(activity: Activity) { + currentlyStartedActivities++ + isInBackground = currentlyStartedActivities == 0 + } + + override fun onActivityResumed(activity: Activity) { + if (isTrackActivitiesEnabled) { + activityLog.add( + dateFormat.format(Date()) + .toString() + ": " + activity.javaClass + .simpleName + " resumed\n" + ) + } + } + + override fun onActivityPaused(activity: Activity) { + if (isTrackActivitiesEnabled) { + activityLog.add( + dateFormat.format(Date()) + .toString() + ": " + activity.javaClass + .simpleName + " paused\n" + ) + } + } + + override fun onActivityStopped(activity: Activity) { + currentlyStartedActivities-- + isInBackground = currentlyStartedActivities == 0 + } + + override fun onActivitySaveInstanceState( + activity: Activity, + outState: Bundle + ) {} + override fun onActivityDestroyed(activity: Activity) { + if (isTrackActivitiesEnabled) { + activityLog.add( + dateFormat.format(Date()) + .toString() + ": " + activity.javaClass + .simpleName + " destroyed\n" + ) + } + } + }) + } + Log.i(TAG, "UCEHandler has been installed.") + } else { + Log.e(TAG, "Context can not be null") + } + } catch (throwable: Throwable) { + Log.e( + TAG, + "UCEHandler can not be initialized. Help making it better by reporting this as a bug.", + throwable + ) + } + } + + /** + * INTERNAL method that tells if the app has crashed in the last seconds. + * This is used to avoid restart loops. + * + * @return true if the app has crashed in the last seconds, false otherwise. + */ + private fun hasCrashedInTheLastSeconds(context: Context): Boolean { + val lastTimestamp = getLastCrashTimestamp(context) + val currentTimestamp: Long = Date().getTime() + return lastTimestamp <= currentTimestamp && currentTimestamp - lastTimestamp < 3000 + } + + @SuppressLint("ApplySharedPref") + private fun setLastCrashTimestamp(context: Context, timestamp: Long) { + context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).edit() + .putLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, timestamp).commit() + } + + private fun killCurrentProcess() { +// Process.killProcess(Process.myPid()) + exitProcess(10) + } + + private fun getLastCrashTimestamp(context: Context): Long { + return context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE) + .getLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, -1) + } + + fun closeApplication(activity: Activity) { + activity.finish() + killCurrentProcess() + } + + inner class Builder(context: Context) { + val context: Context + var isUCEHEnabled = true + var commaSeparatedEmailAddresses: String? = null + var isTrackActivitiesEnabled = false + var isBackgroundModeEnabled = true + fun setUCEHEnabled(isUCEHEnabled: Boolean): Builder { + this.isUCEHEnabled = isUCEHEnabled + return this + } + + fun setTrackActivitiesEnabled(isTrackActivitiesEnabled: Boolean): Builder { + this.isTrackActivitiesEnabled = isTrackActivitiesEnabled + return this + } + + fun setBackgroundModeEnabled(isBackgroundModeEnabled: Boolean): Builder { + this.isBackgroundModeEnabled = isBackgroundModeEnabled + return this + } + + fun addCommaSeparatedEmailAddresses(commaSeparatedEmailAddresses: String?): Builder { + this.commaSeparatedEmailAddresses = commaSeparatedEmailAddresses ?: "" + return this + } + + fun build() { + return UCEHandler(this) + } + + init { + this.context = context + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hmg/hmgDr/util/audio/CustomAudioDevice.kt b/android/app/src/main/kotlin/com/hmg/hmgDr/util/audio/CustomAudioDevice.kt new file mode 100644 index 00000000..ef654844 --- /dev/null +++ b/android/app/src/main/kotlin/com/hmg/hmgDr/util/audio/CustomAudioDevice.kt @@ -0,0 +1,467 @@ +package com.hmg.hmgDr.util.audio + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioRecord +import android.media.AudioTrack +import android.media.MediaRecorder.AudioSource +import android.os.Process +import android.util.Log + +import com.opentok.android.BaseAudioDevice + +import java.nio.ByteBuffer +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.ReentrantLock + +class CustomAudioDevice(context: Context) : BaseAudioDevice() { + + private val m_context: Context = context + private var m_audioTrack: AudioTrack? = null + private var m_audioRecord: AudioRecord? = null + + // Capture & render buffers + private var m_playBuffer: ByteBuffer? = null + private var m_recBuffer: ByteBuffer? = null + private val m_tempBufPlay: ByteArray + private val m_tempBufRec: ByteArray + private val m_rendererLock: ReentrantLock = ReentrantLock(true) + private val m_renderEvent: Condition = m_rendererLock.newCondition() + + @Volatile + private var m_isRendering = false + + @Volatile + private var m_shutdownRenderThread = false + private val m_captureLock: ReentrantLock = ReentrantLock(true) + private val m_captureEvent: Condition = m_captureLock.newCondition() + + @Volatile + private var m_isCapturing = false + + @Volatile + private var m_shutdownCaptureThread = false + private val m_captureSettings: AudioSettings + private val m_rendererSettings: AudioSettings + + // Capturing delay estimation + private var m_estimatedCaptureDelay = 0 + + // Rendering delay estimation + private var m_bufferedPlaySamples = 0 + private var m_playPosition = 0 + private var m_estimatedRenderDelay = 0 + private val m_audioManager: AudioManager + private var isRendererMuted = false + + companion object { + private const val LOG_TAG = "opentok-defaultaudio" + private const val SAMPLING_RATE = 44100 + private const val NUM_CHANNELS_CAPTURING = 1 + private const val NUM_CHANNELS_RENDERING = 1 + private const val MAX_SAMPLES = 2 * 480 * 2 // Max 10 ms @ 48 kHz + } + + init { + try { + m_playBuffer = ByteBuffer.allocateDirect(MAX_SAMPLES) + m_recBuffer = ByteBuffer.allocateDirect(MAX_SAMPLES) + } catch (e: Exception) { + Log.e(LOG_TAG, "${e.message}.") + } + m_tempBufPlay = ByteArray(MAX_SAMPLES) + m_tempBufRec = ByteArray(MAX_SAMPLES) + m_captureSettings = AudioSettings( + SAMPLING_RATE, + NUM_CHANNELS_CAPTURING + ) + m_rendererSettings = AudioSettings( + SAMPLING_RATE, + NUM_CHANNELS_RENDERING + ) + m_audioManager = m_context + .getSystemService(Context.AUDIO_SERVICE) as AudioManager + m_audioManager.mode = AudioManager.MODE_IN_COMMUNICATION + } + + override fun initCapturer(): Boolean { + + // get the minimum buffer size that can be used + val minRecBufSize: Int = AudioRecord.getMinBufferSize( + m_captureSettings + .sampleRate, + if (NUM_CHANNELS_CAPTURING == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO, + AudioFormat.ENCODING_PCM_16BIT + ) + + // double size to be more safe + val recBufSize = minRecBufSize * 2 + + // release the object + if (m_audioRecord != null) { + m_audioRecord!!.release() + m_audioRecord = null + } + try { + m_audioRecord = AudioRecord( + AudioSource.VOICE_COMMUNICATION, + m_captureSettings.sampleRate, + if (NUM_CHANNELS_CAPTURING == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO, + AudioFormat.ENCODING_PCM_16BIT, recBufSize + ) + } catch (e: Exception) { + Log.e(LOG_TAG, "${e.message}.") + return false + } + + // check that the audioRecord is ready to be used + if (m_audioRecord!!.state != AudioRecord.STATE_INITIALIZED) { + Log.i( + LOG_TAG, "Audio capture is not initialized " + + m_captureSettings.sampleRate + ) + return false + } + m_shutdownCaptureThread = false + Thread(m_captureThread).start() + return true + } + + override fun destroyCapturer(): Boolean { + m_captureLock.lock() + // release the object + m_audioRecord?.release() + m_audioRecord = null + m_shutdownCaptureThread = true + m_captureEvent.signal() + m_captureLock.unlock() + return true + } + + override fun getEstimatedCaptureDelay(): Int { + return m_estimatedCaptureDelay + } + + override fun startCapturer(): Boolean { + // start recording + try { + m_audioRecord!!.startRecording() + } catch (e: IllegalStateException) { + e.printStackTrace() + return false + } + m_captureLock.lock() + m_isCapturing = true + m_captureEvent.signal() + m_captureLock.unlock() + return true + } + + override fun stopCapturer(): Boolean { + m_captureLock.lock() + try { + // only stop if we are recording + if (m_audioRecord!!.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + // stop recording + try { + m_audioRecord!!.stop() + } catch (e: IllegalStateException) { + e.printStackTrace() + return false + } + } + } finally { + // Ensure we always unlock + m_isCapturing = false + m_captureLock.unlock() + } + return true + } + + private val m_captureThread = Runnable { + val samplesToRec = SAMPLING_RATE / 100 + var samplesRead = 0 + try { + Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO) + } catch (e: Exception) { + e.printStackTrace() + } + while (!m_shutdownCaptureThread) { + m_captureLock.lock() + samplesRead = try { + if (!m_isCapturing) { + m_captureEvent.await() + continue + } else { + if (m_audioRecord == null) { + continue + } + val lengthInBytes = ((samplesToRec shl 1) + * NUM_CHANNELS_CAPTURING) + val readBytes: Int = m_audioRecord!!.read( + m_tempBufRec, 0, + lengthInBytes + ) + m_recBuffer!!.rewind() + m_recBuffer!!.put(m_tempBufRec) + (readBytes shr 1) / NUM_CHANNELS_CAPTURING + } + } catch (e: Exception) { + Log.e(LOG_TAG, "RecordAudio try failed: " + e.message) + continue + } finally { + // Ensure we always unlock + m_captureLock.unlock() + } + audioBus.writeCaptureData(m_recBuffer, samplesRead) + m_estimatedCaptureDelay = samplesRead * 1000 / SAMPLING_RATE + } + } + + override fun initRenderer(): Boolean { + + // get the minimum buffer size that can be used + val minPlayBufSize: Int = AudioTrack.getMinBufferSize( + m_rendererSettings + .sampleRate, + if (NUM_CHANNELS_RENDERING == 1) AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO, + AudioFormat.ENCODING_PCM_16BIT + ) + var playBufSize = minPlayBufSize + if (playBufSize < 6000) { + playBufSize *= 2 + } + + // release the object + if (m_audioTrack != null) { + m_audioTrack!!.release() + m_audioTrack = null + } + try { + m_audioTrack = AudioTrack( + AudioManager.STREAM_VOICE_CALL, + m_rendererSettings.sampleRate, + if (NUM_CHANNELS_RENDERING == 1) AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO, + AudioFormat.ENCODING_PCM_16BIT, playBufSize, + AudioTrack.MODE_STREAM + ) + } catch (e: Exception) { + Log.e(LOG_TAG, "${e.message}.") + return false + } + + // check that the audioRecord is ready to be used + if (m_audioTrack!!.state != AudioTrack.STATE_INITIALIZED) { + Log.i( + LOG_TAG, "Audio renderer not initialized " + + m_rendererSettings.sampleRate + ) + return false + } + m_bufferedPlaySamples = 0 + setOutputMode(OutputMode.SpeakerPhone) + m_shutdownRenderThread = false + Thread(m_renderThread).start() + return true + } + + override fun destroyRenderer(): Boolean { + m_rendererLock.lock() + // release the object + m_audioTrack!!.release() + m_audioTrack = null + m_shutdownRenderThread = true + m_renderEvent.signal() + m_rendererLock.unlock() + unregisterHeadsetReceiver() + m_audioManager.isSpeakerphoneOn = false + m_audioManager.mode = AudioManager.MODE_NORMAL + return true + } + + override fun getEstimatedRenderDelay(): Int { + return m_estimatedRenderDelay + } + + override fun startRenderer(): Boolean { + // start playout + try { + m_audioTrack!!.play() + } catch (e: IllegalStateException) { + e.printStackTrace() + return false + } + m_rendererLock.lock() + m_isRendering = true + m_renderEvent.signal() + m_rendererLock.unlock() + return true + } + + override fun stopRenderer(): Boolean { + m_rendererLock.lock() + try { + // only stop if we are playing + if (m_audioTrack!!.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { + // stop playout + try { + m_audioTrack!!.stop() + } catch (e: IllegalStateException) { + e.printStackTrace() + return false + } + + // flush the buffers + m_audioTrack!!.flush() + } + } finally { + // Ensure we always unlock, both for success, exception or error + // return. + m_isRendering = false + m_rendererLock.unlock() + } + return true + } + + private val m_renderThread = Runnable { + val samplesToPlay = SAMPLING_RATE / 100 + try { + Process + .setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO) + } catch (e: Exception) { + e.printStackTrace() + } + while (!m_shutdownRenderThread) { + m_rendererLock.lock() + try { + if (!m_isRendering) { + m_renderEvent.await() + continue + } else { + m_rendererLock.unlock() + + // Don't lock on audioBus calls + m_playBuffer!!.clear() + val samplesRead: Int = audioBus.readRenderData( + m_playBuffer, samplesToPlay + ) + + // Log.d(LOG_TAG, "Samples read: " + samplesRead); + m_rendererLock.lock() + if (!isRendererMuted) { + // After acquiring the lock again + // we must check if we are still playing + if (m_audioTrack == null + || !m_isRendering + ) { + continue + } + val bytesRead = ((samplesRead shl 1) + * NUM_CHANNELS_RENDERING) + m_playBuffer!!.get(m_tempBufPlay, 0, bytesRead) + val bytesWritten: Int = m_audioTrack!!.write( + m_tempBufPlay, 0, + bytesRead + ) + + // increase by number of written samples + m_bufferedPlaySamples += ((bytesWritten shr 1) + / NUM_CHANNELS_RENDERING) + + // decrease by number of played samples + val pos: Int = m_audioTrack!!.getPlaybackHeadPosition() + if (pos < m_playPosition) { + // wrap or reset by driver + m_playPosition = 0 + } + m_bufferedPlaySamples -= pos - m_playPosition + m_playPosition = pos + + // we calculate the estimated delay based on the + // buffered samples + m_estimatedRenderDelay = (m_bufferedPlaySamples * 1000 + / SAMPLING_RATE) + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "Exception: " + e.message) + e.printStackTrace() + } finally { + m_rendererLock.unlock() + } + } + } + + override fun getCaptureSettings(): AudioSettings { + return m_captureSettings + } + + override fun getRenderSettings(): AudioSettings { + return m_rendererSettings + } + + /** + * Communication modes handling + */ + override fun setOutputMode(mode: OutputMode): Boolean { + super.setOutputMode(mode) + if (mode == OutputMode.Handset) { + unregisterHeadsetReceiver() + m_audioManager.isSpeakerphoneOn = false + } else { + m_audioManager.isSpeakerphoneOn = true + registerHeadsetReceiver() + } + return true + } + + private val m_headsetReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) { + if (intent.action!!.compareTo(Intent.ACTION_HEADSET_PLUG) == 0) { + val state: Int = intent.getIntExtra("state", 0) + m_audioManager.isSpeakerphoneOn = state == 0 + } + } + } + private var m_receiverRegistered = false + private fun registerHeadsetReceiver() { + if (!m_receiverRegistered) { + val receiverFilter = IntentFilter( + Intent.ACTION_HEADSET_PLUG + ) + m_context.registerReceiver(m_headsetReceiver, receiverFilter) + m_receiverRegistered = true + } + } + + private fun unregisterHeadsetReceiver() { + if (m_receiverRegistered) { + try { + m_context.unregisterReceiver(m_headsetReceiver) + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } + m_receiverRegistered = false + } + } + + override fun onPause() { + if (outputMode == OutputMode.SpeakerPhone) { + unregisterHeadsetReceiver() + } + } + + override fun onResume() { + if (outputMode == OutputMode.SpeakerPhone) { + registerHeadsetReceiver() + } + } + + fun setRendererMute(isRendererMuted: Boolean) { + this.isRendererMuted = isRendererMuted + } +} diff --git a/android/app/src/main/res/layout/default_error_activity.xml b/android/app/src/main/res/layout/default_error_activity.xml new file mode 100644 index 00000000..21af70f2 --- /dev/null +++ b/android/app/src/main/res/layout/default_error_activity.xml @@ -0,0 +1,81 @@ + + + + + + + + + +