diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c73090..a729486 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools"> + { override fun getId() = roomSummary.roomId - override fun getDialogPhoto() = roomSummary.avatarUrl + override fun getDialogPhoto() = roomSummary.avatarUrl.ifEmpty { roomSummary.toSDNItem().firstLetterOfDisplayName() } override fun getDialogName() = roomSummary.displayName diff --git a/app/src/main/java/org/sdn/android/sdk/sample/data/TimelineEventMessageWrapper.kt b/app/src/main/java/org/sdn/android/sdk/sample/data/TimelineEventMessageWrapper.kt index efe161e..58a85ea 100644 --- a/app/src/main/java/org/sdn/android/sdk/sample/data/TimelineEventMessageWrapper.kt +++ b/app/src/main/java/org/sdn/android/sdk/sample/data/TimelineEventMessageWrapper.kt @@ -45,4 +45,8 @@ data class TimelineEventMessageWrapper(private val timelineEvent: TimelineEvent) override fun getUser() = TimelineEventSenderWrapper(timelineEvent.senderInfo) override fun getCreatedAt() = Date(timelineEvent.root.originServerTs ?: 0) + + fun getTimelineEvent() : TimelineEvent { + return timelineEvent + } } diff --git a/app/src/main/java/org/sdn/android/sdk/sample/data/TimelineEventSenderWrapper.kt b/app/src/main/java/org/sdn/android/sdk/sample/data/TimelineEventSenderWrapper.kt index 5ded1e6..9d34c8b 100644 --- a/app/src/main/java/org/sdn/android/sdk/sample/data/TimelineEventSenderWrapper.kt +++ b/app/src/main/java/org/sdn/android/sdk/sample/data/TimelineEventSenderWrapper.kt @@ -24,5 +24,5 @@ data class TimelineEventSenderWrapper(private val senderInfo: SenderInfo) : IUse override fun getName() = senderInfo.disambiguatedDisplayName - override fun getAvatar() = senderInfo.avatarUrl + override fun getAvatar() = if (!senderInfo.avatarUrl.isNullOrEmpty()) senderInfo.avatarUrl else "https://static.sending.me/beam/70/${senderInfo.userId}?colors=FC774B,FFB197,B27AFF,DAC2FB,F0E7FD&square=true" } diff --git a/app/src/main/java/org/sdn/android/sdk/sample/ui/RoomDetailFragment.kt b/app/src/main/java/org/sdn/android/sdk/sample/ui/RoomDetailFragment.kt index 6d5d0a1..26159f7 100644 --- a/app/src/main/java/org/sdn/android/sdk/sample/ui/RoomDetailFragment.kt +++ b/app/src/main/java/org/sdn/android/sdk/sample/ui/RoomDetailFragment.kt @@ -17,6 +17,7 @@ package org.sdn.android.sdk.sample.ui import android.content.Context +import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -41,12 +42,19 @@ import org.sdn.android.sdk.sample.databinding.FragmentRoomDetailBinding import org.sdn.android.sdk.sample.utils.* import org.sdn.android.sdk.api.meet.SdnMeetActivity -import android.util.Log -import kotlinx.coroutines.GlobalScope +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.widget.Toast +import androidx.appcompat.app.AlertDialog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -//import com.github.zhanghai.android.kotlin.BaseEncoding import org.apache.commons.codec.binary.Base32 +import org.sdn.android.sdk.api.session.events.model.toModel +import org.sdn.android.sdk.api.session.room.model.message.MessageContent +import org.sdn.android.sdk.sample.R +import org.sdn.android.sdk.sample.data.TimelineEventMessageWrapper +import org.sdn.android.sdk.sample.ui.dialog.PasswordDialogFragment class RoomDetailFragment : Fragment(), Timeline.Listener, ToolbarConfigurable { @@ -112,6 +120,18 @@ class RoomDetailFragment : Fragment(), Timeline.Listener, ToolbarConfigurable { } }) + adapter.setOnMessageLongClickListener { + val event = (it as TimelineEventMessageWrapper).getTimelineEvent() + with(AlertDialog.Builder(requireContext())) { +// setTitle("Androidly Alert") + setMessage("Request decryption key?") + setPositiveButton(R.string.ok) { _: DialogInterface, _: Int -> + session.cryptoService().reRequestRoomKeyForEvent(event.root) + } + setNegativeButton(R.string.cancel, null) + show() + } + } views.timelineEventList.setAdapter(adapter) views.timelineEventList.itemAnimator = null views.timelineEventList.addOnScrollListener(RecyclerScrollMoreListener(views.timelineEventList.layoutManager as LinearLayoutManager) { @@ -149,11 +169,39 @@ class RoomDetailFragment : Fragment(), Timeline.Listener, ToolbarConfigurable { } } - views.toolbarBtnVideo.setOnClickListener { - Log.d("getMeeting","start") - GlobalScope.launch { - joinRoomMeeting(context!!, roomID) + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.room_detail_options, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.invite -> { + val dlg = PasswordDialogFragment.newInstance("") + dlg.show(childFragmentManager, "") + true } + R.id.meeting -> { + room?.roomId?.let { + lifecycleScope.launch { + joinRoomMeeting(requireContext(), it) + } + } + true + } + R.id.leave -> { + room?.roomId?.let { + lifecycleScope.launch { + session.roomService().leaveRoom(it) + showRoomList() + } + } + true + } + + else -> super.onOptionsItemSelected(item) } } @@ -215,5 +263,11 @@ class RoomDetailFragment : Fragment(), Timeline.Listener, ToolbarConfigurable { } } - + private fun showRoomList() { + (activity as MainActivity).supportFragmentManager + .beginTransaction() + .addToBackStack(null) + .replace(R.id.fragmentContainer, RoomListFragment()) + .commit() + } } diff --git a/app/src/main/java/org/sdn/android/sdk/sample/ui/RoomListFragment.kt b/app/src/main/java/org/sdn/android/sdk/sample/ui/RoomListFragment.kt index 0aa8edf..aadd339 100644 --- a/app/src/main/java/org/sdn/android/sdk/sample/ui/RoomListFragment.kt +++ b/app/src/main/java/org/sdn/android/sdk/sample/ui/RoomListFragment.kt @@ -16,9 +16,9 @@ package org.sdn.android.sdk.sample.ui -import android.content.DialogInterface -import android.content.res.Resources +import android.annotation.SuppressLint import android.os.Bundle +import android.os.Environment import android.view.* import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -26,7 +26,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.stfalcon.chatkit.commons.ImageLoader import com.stfalcon.chatkit.dialogs.DialogsListAdapter -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.sdn.android.sdk.api.session.room.model.Membership import org.sdn.android.sdk.api.session.room.model.RoomSummary @@ -38,18 +37,47 @@ import org.sdn.android.sdk.sample.SessionHolder import org.sdn.android.sdk.sample.data.RoomSummaryDialogWrapper import org.sdn.android.sdk.sample.databinding.FragmentRoomListBinding import org.sdn.android.sdk.sample.formatter.RoomListDateFormatter +import org.sdn.android.sdk.sample.ui.dialog.PasswordDialogFragment import org.sdn.android.sdk.sample.utils.AvatarRenderer import org.sdn.android.sdk.sample.utils.SDNItemColorProvider +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream class RoomListFragment : Fragment(), ToolbarConfigurable { private val session = SessionHolder.currentSession!! + private val exportListenerKey = "export" + private val importListenerKey = "import" + private val e2eBackupDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + private val e2eBackupFile = "e2e_xx_keys.txt" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + childFragmentManager.setFragmentResultListener(exportListenerKey, this) { _, bundle -> + val password = bundle.getString("password") ?: "default" + lifecycleScope.launch { + val exportedBytes = session.cryptoService().exportRoomKeys(password) + saveFileToExternalStorage(e2eBackupFile, exportedBytes) + } + } + + childFragmentManager.setFragmentResultListener(importListenerKey, this) { _, bundle -> + val password = bundle.getString("password") ?: "default" + lifecycleScope.launch { + val importedBytes = readFileFromExternalStorage(e2eBackupFile) + val result = session.cryptoService().importRoomKeys(importedBytes, password, null) + Timber.i("totalNumberOfKeys: ${result.totalNumberOfKeys}, successfullyNumberOfImportedKeys: ${result.successfullyNumberOfImportedKeys}") + } + } + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { + ): View { _views = FragmentRoomListBinding.inflate(inflater, container, false) return views.root } @@ -63,19 +91,24 @@ class RoomListFragment : Fragment(), ToolbarConfigurable { } private val imageLoader = ImageLoader { imageView, url, _ -> - avatarRenderer.render(url, imageView) + avatarRenderer.renderDrawable(url, imageView) } private val roomAdapter = DialogsListAdapter(imageLoader) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) configureToolbar(views.toolbar, displayBack = false) -11 + views.createRoomButton.setOnClickListener { val userId = views.otherUserIdField.text.toString().trim() viewLifecycleOwner.lifecycleScope.launch { - session.roomService().createDirectRoom(otherUserId = userId) +// session.roomService().createDirectRoom(otherUserId = userId) + session.roomService().createRoom(CreateRoomParams() + .apply { + invitedUserIds.add(userId) + enableEncryption() + }) } // GlobalScope.launch { // println("contact-signOut out") @@ -99,13 +132,13 @@ class RoomListFragment : Fragment(), ToolbarConfigurable { builder.setMessage("Do you want to join this room?") builder.setPositiveButton("Join") { _, _ -> viewLifecycleOwner.lifecycleScope.launch { - session.roomService().joinRoom(it.roomSummary.roomId); + session.roomService().joinRoom(it.roomSummary.roomId) showRoomDetail(it.roomSummary) } } builder.setNegativeButton("Cancel", null) val dialog = builder.create() - dialog.setOnShowListener { _ -> + dialog.setOnShowListener { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(resources.getColor(R.color.dark_gray)) dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(resources.getColor(R.color.dark_gray)) } @@ -141,6 +174,16 @@ class RoomListFragment : Fragment(), ToolbarConfigurable { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { + R.id.export_key -> { + val dlg = PasswordDialogFragment.newInstance(exportListenerKey) + dlg.show(childFragmentManager, "export") + true + } + R.id.import_key -> { + val dlg = PasswordDialogFragment.newInstance(importListenerKey) + dlg.show(childFragmentManager, "import") + true + } R.id.logout -> { signOut() true @@ -152,7 +195,7 @@ class RoomListFragment : Fragment(), ToolbarConfigurable { private fun signOut() { lifecycleScope.launch { try { - session.signOutService().signOut(true) + session.signOutService().signOut(signOutFromHomeserver = true, deleteCrypto = false) } catch (failure: Throwable) { activity?.let { Toast.makeText(it, "Failure: $failure", Toast.LENGTH_SHORT).show() @@ -186,4 +229,20 @@ class RoomListFragment : Fragment(), ToolbarConfigurable { } roomAdapter.setItems(sortedRoomSummaryList) } + + @SuppressLint("SetWorldReadable") + private fun saveFileToExternalStorage(fileName: String, data: ByteArray) { + val targetFile = File(e2eBackupDir, fileName) + targetFile.setReadable(true, false) + data.inputStream().use { input -> + FileOutputStream(targetFile).use { output -> + input.copyTo(output) + } + } + } + + private fun readFileFromExternalStorage(fileName: String): ByteArray { + val targetFile = File(e2eBackupDir, fileName) + return targetFile.readBytes() + } } diff --git a/app/src/main/java/org/sdn/android/sdk/sample/ui/SimpleLoginFragment.kt b/app/src/main/java/org/sdn/android/sdk/sample/ui/SimpleLoginFragment.kt index 22360e5..3e939d8 100644 --- a/app/src/main/java/org/sdn/android/sdk/sample/ui/SimpleLoginFragment.kt +++ b/app/src/main/java/org/sdn/android/sdk/sample/ui/SimpleLoginFragment.kt @@ -16,6 +16,7 @@ package org.sdn.android.sdk.sample.ui +import android.content.Context.MODE_PRIVATE import android.content.Intent import android.net.Uri import android.os.Bundle @@ -106,8 +107,16 @@ class SimpleLoginFragment : Fragment() { val ecKeyPair: ECKeyPair = ECKeyPair.create(privateKey.decodeHex().toByteArray()) val authService = SampleApp.getSDNClient(requireContext()).authenticationService() + val sp = requireContext().getSharedPreferences("device_data", MODE_PRIVATE) + var deviceIdKey = "device_id_$address" + var deviceId = "" try { - val loginDidMsg = authService.didPreLogin(edgeNodeConnectionConfig, address) + val fedInfo = authService.getFedInfo(edgeNodeConnectionConfig) + edgeNodeConnectionConfig.peerId = fedInfo.peer + deviceIdKey = "device_id_${fedInfo.peer}_$address" + deviceId = sp.getString(deviceIdKey, "") ?: "" + + val loginDidMsg = authService.didPreLogin(edgeNodeConnectionConfig, address, deviceId) if (loginDidMsg.message is String) { Log.d("loginLoginDidMsg", loginDidMsg.message) } @@ -127,6 +136,11 @@ class SimpleLoginFragment : Fragment() { Toast.makeText(requireContext(), "Failure: $failure", Toast.LENGTH_SHORT).show() null }?.let { + val retDeviceId = it.sessionParams.deviceId + if (retDeviceId != deviceId) { + Timber.tag("login").i("get new device id: $retDeviceId") + sp.edit().putString(deviceIdKey, retDeviceId).apply() + } SessionHolder.currentSession = it it.open() it.syncService().startSync(true) diff --git a/app/src/main/java/org/sdn/android/sdk/sample/ui/dialog/PasswordDialogFragment.kt b/app/src/main/java/org/sdn/android/sdk/sample/ui/dialog/PasswordDialogFragment.kt new file mode 100644 index 0000000..e51e57a --- /dev/null +++ b/app/src/main/java/org/sdn/android/sdk/sample/ui/dialog/PasswordDialogFragment.kt @@ -0,0 +1,63 @@ +package org.sdn.android.sdk.sample.ui.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.EditText +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import org.sdn.android.sdk.sample.R +import org.sdn.android.sdk.sample.databinding.FragmentDlgPasswordBinding + +class PasswordDialogFragment : DialogFragment() { + + private var _views: FragmentDlgPasswordBinding? = null + private val views get() = _views!! + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, 0) + } + + override fun onStart() { + val params = dialog!!.window!!.attributes + params.width = ViewGroup.LayoutParams.MATCH_PARENT + dialog!!.window!!.attributes = params as WindowManager.LayoutParams + super.onStart() + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _views = FragmentDlgPasswordBinding.inflate(inflater, container, false) + return views.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val inputPassword: EditText = view.findViewById(R.id.input_password) + inputPassword.requestFocus() + dialog!!.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + super.onViewCreated(view, savedInstanceState) + + val requestKey = arguments?.getString("requestKey") ?: "" + views.btnOk.setOnClickListener { + val password = views.inputPassword.text.toString() + parentFragmentManager.setFragmentResult(requestKey, bundleOf(Pair("password", password))) + dismiss() + } + views.btnCancel.setOnClickListener { + dismiss() + } + } + + companion object { + fun newInstance(requestKey: String): PasswordDialogFragment { + // Supply num input as an argument. + val args = Bundle().also { it.putString("requestKey", requestKey) } + return PasswordDialogFragment().also { it.arguments = args } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sdn/android/sdk/sample/utils/AvatarRenderer.kt b/app/src/main/java/org/sdn/android/sdk/sample/utils/AvatarRenderer.kt index 847546e..8824cb4 100644 --- a/app/src/main/java/org/sdn/android/sdk/sample/utils/AvatarRenderer.kt +++ b/app/src/main/java/org/sdn/android/sdk/sample/utils/AvatarRenderer.kt @@ -41,6 +41,27 @@ class AvatarRenderer(private val frag: Fragment, private val sdnItemColorProvide GlideApp.with(frag).load(resolvedUrl).fallback(R.drawable.default_avatar).into(imageView) } + fun renderDrawable(letter: String?, imageView: ImageView) { + + if (letter?.startsWith("http") == true) { + render(letter, imageView) + return + } + + val color = sdnItemColorProvider.getColorForRoom(letter ?: "") + val drawable = TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRoundRect(letter, color, 5) + val nullStr: String? = null + Picasso.get() + .load(nullStr) + .placeholder(drawable) + .error(R.drawable.default_avatar) + .into(imageView) + } + fun render(SDNItem: SDNItem, imageView: ImageView) { val resolvedUrl = resolvedUrl(SDNItem.avatarUrl) val placeholder = getPlaceholderDrawable(SDNItem) diff --git a/app/src/main/java/org/sdn/android/sdk/sample/utils/MatrixItemColorProvider.kt b/app/src/main/java/org/sdn/android/sdk/sample/utils/MatrixItemColorProvider.kt index 6047f10..fcad82b 100644 --- a/app/src/main/java/org/sdn/android/sdk/sample/utils/MatrixItemColorProvider.kt +++ b/app/src/main/java/org/sdn/android/sdk/sample/utils/MatrixItemColorProvider.kt @@ -41,6 +41,15 @@ class SDNItemColorProvider(private val context: Context) { } } + fun getColorForRoom(id: String): Int { + return cache.getOrPut(id) { + ContextCompat.getColor( + context, + getColorFromRoomId(id) + ) + } + } + companion object { @ColorRes @VisibleForTesting @@ -63,10 +72,13 @@ class SDNItemColorProvider(private val context: Context) { @ColorRes private fun getColorFromRoomId(roomId: String?): Int { - return when ((roomId?.toList()?.sumOf { it.code } ?: 0) % 3) { - 1 -> R.color.avatar_fill_2 - 2 -> R.color.avatar_fill_3 - else -> R.color.avatar_fill_1 + return when ((roomId?.toList()?.sumOf { it.code } ?: 0) % 6) { + 1 -> R.color.avatar_fill_1 + 2 -> R.color.avatar_fill_2 + 3 -> R.color.avatar_fill_3 + 4 -> R.color.avatar_fill_4 + 5 -> R.color.avatar_fill_5 + else -> R.color.avatar_fill_6 } } } diff --git a/app/src/main/java/org/sdn/android/sdk/sample/utils/TimelineEventListProcessor.kt b/app/src/main/java/org/sdn/android/sdk/sample/utils/TimelineEventListProcessor.kt index ffbea6a..6d2d0e2 100644 --- a/app/src/main/java/org/sdn/android/sdk/sample/utils/TimelineEventListProcessor.kt +++ b/app/src/main/java/org/sdn/android/sdk/sample/utils/TimelineEventListProcessor.kt @@ -48,6 +48,7 @@ class TimelineEventListProcessor(private val adapter: MessagesListAdapter + + +