Import keys: WIP

This commit is contained in:
Benoit Marty 2019-06-14 12:27:29 +02:00
parent 99d2e8388a
commit 907a1d1a4b
6 changed files with 374 additions and 88 deletions

View File

@ -0,0 +1,152 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotredesign.core.intent

import android.content.ClipData
import android.content.ClipDescription
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.core.util.PatternsCompat.WEB_URL
import java.util.*

/**
* Inspired from Riot code: RoomMediaMessage.java
*/
sealed class ExternalIntentData {
/**
* Constructor for a text message.
*
* @param text the text
* @param htmlText the HTML text
* @param format the formatted text format
*/
data class IntentDataText(
val text: CharSequence? = null,
val htmlText: String? = null,
val format: String? = null,
val clipDataItem: ClipData.Item = ClipData.Item(text, htmlText),
val mimeType: String? = if (null == htmlText) ClipDescription.MIMETYPE_TEXT_PLAIN else format
) : ExternalIntentData()

/**
* Clip data
*/
data class IntentDataClipData(
val clipDataItem: ClipData.Item,
val mimeType: String?
) : ExternalIntentData()

/**
* Constructor from a media Uri/
*
* @param uri the media uri
* @param filename the media file name
*/
data class IntentDataUri(
val uri: Uri,
val filename: String? = null
) : ExternalIntentData()
}


fun analyseIntent(intent: Intent): List<ExternalIntentData> {
val externalIntentDataList = ArrayList<ExternalIntentData>()


// chrome adds many items when sharing an web page link
// so, test first the type
if (TextUtils.equals(intent.type, ClipDescription.MIMETYPE_TEXT_PLAIN)) {
var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT)

if (null == message) {
val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
if (null != sequence) {
message = sequence.toString()
}
}

val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)

if (!TextUtils.isEmpty(subject)) {
if (TextUtils.isEmpty(message)) {
message = subject
} else if (WEB_URL.matcher(message!!).matches()) {
message = subject + "\n" + message
}
}

if (!TextUtils.isEmpty(message)) {
externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type))
return externalIntentDataList
}
}

var clipData: ClipData? = null
var mimetypes: MutableList<String>? = null

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
clipData = intent.clipData
}

// multiple data
if (null != clipData) {
if (null != clipData.description) {
if (0 != clipData.description.mimeTypeCount) {
mimetypes = ArrayList()

for (i in 0 until clipData.description.mimeTypeCount) {
mimetypes.add(clipData.description.getMimeType(i))
}

// if the filter is "accept anything" the mimetype does not make sense
if (1 == mimetypes.size) {
if (mimetypes[0].endsWith("/*")) {
mimetypes = null
}
}
}
}

val count = clipData.itemCount

for (i in 0 until count) {
val item = clipData.getItemAt(i)
var mimetype: String? = null

if (null != mimetypes) {
if (i < mimetypes.size) {
mimetype = mimetypes[i]
} else {
mimetype = mimetypes[0]
}

// uris list is not a valid mimetype
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
mimetype = null
}
}

externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype))
}
} else if (null != intent.data) {
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))
}

return externalIntentDataList
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotredesign.core.intent

import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns

fun getFilenameFromUri(context: Context, uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
} finally {
cursor?.close()
}
}
if (result == null) {
result = uri.path
val cut = result.lastIndexOf('/')
if (cut != -1) {
result = result.substring(cut + 1)
}
}
return result
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotredesign.core.intent

import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import im.vector.riotredesign.core.utils.getFileExtension
import timber.log.Timber

/**
* Returns the mimetype from a uri.
*
* @param context the context
* @return the mimetype
*/
fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
var mimeType: String? = null

try {
mimeType = context.contentResolver.getType(uri)

// try to find the mimetype from the filename
if (null == mimeType) {
val extension = getFileExtension(uri.toString())
if (extension != null) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
}
}

if (null != mimeType) {
// the mimetype is sometimes in uppercase.
mimeType = mimeType.toLowerCase()
}
} catch (e: Exception) {
Timber.e(e, "Failed to open resource input stream")
}

return mimeType
}

View File

@ -49,18 +49,24 @@ import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.extensions.withArgs
import im.vector.riotredesign.core.intent.ExternalIntentData
import im.vector.riotredesign.core.intent.analyseIntent
import im.vector.riotredesign.core.intent.getFilenameFromUri
import im.vector.riotredesign.core.intent.getMimeTypeFromUri
import im.vector.riotredesign.core.platform.SimpleTextWatcher
import im.vector.riotredesign.core.platform.VectorPreferenceFragment
import im.vector.riotredesign.core.preference.BingRule
import im.vector.riotredesign.core.preference.ProgressBarPreference
import im.vector.riotredesign.core.preference.UserAvatarPreference
import im.vector.riotredesign.core.preference.VectorPreference
import im.vector.riotredesign.core.resources.openResource
import im.vector.riotredesign.core.utils.*
import im.vector.riotredesign.features.MainActivity
import im.vector.riotredesign.features.configuration.VectorConfiguration
@ -2643,100 +2649,115 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
return
}

notImplemented()
val sharedDataItems = analyseIntent(intent)
val thisActivity = activity

/*
val sharedDataItems = ArrayList(RoomMediaMessage.listRoomMediaMessages(intent))
val thisActivity = activity
if (sharedDataItems.isNotEmpty() && thisActivity != null) {
val sharedDataItem = sharedDataItems[0]

if (sharedDataItems.isNotEmpty() && thisActivity != null) {
val sharedDataItem = sharedDataItems[0]
val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null)
val builder = AlertDialog.Builder(thisActivity)
.setTitle(R.string.encryption_import_room_keys)
.setView(dialogLayout)

val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button)

passPhraseEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {

}

override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text)
}

override fun afterTextChanged(s: Editable) {

}
})

val importDialog = builder.show()
val appContext = thisActivity.applicationContext

importButton.setOnClickListener(View.OnClickListener {
val password = passPhraseEditText.text.toString()
val resource = openResource(appContext, sharedDataItem.uri, sharedDataItem.getMimeType(appContext))

if (resource?.mContentStream == null) {
appContext.toast("Error")

return@OnClickListener
}

val data: ByteArray

try {
data = ByteArray(resource.mContentStream!!.available())
resource!!.mContentStream!!.read(data)
resource!!.mContentStream!!.close()
} catch (e: Exception) {
try {
resource!!.mContentStream!!.close()
} catch (e2: Exception) {
Timber.e(e2, "## importKeys()")
val uri = when (sharedDataItem) {
is ExternalIntentData.IntentDataUri -> sharedDataItem.uri
is ExternalIntentData.IntentDataClipData -> sharedDataItem.clipDataItem.uri
else -> null
}

appContext.toast(e.localizedMessage)
val mimetype = when (sharedDataItem) {
is ExternalIntentData.IntentDataClipData -> sharedDataItem.mimeType
else -> null
}

return@OnClickListener
if (uri == null) {
return
}

val appContext = thisActivity.applicationContext

val filename = getFilenameFromUri(appContext, uri)

val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null)

val textView = dialogLayout.findViewById<TextView>(R.id.dialog_e2e_keys_passphrase_filename)

if (filename.isNullOrBlank()) {
textView.isVisible = false
} else {
textView.isVisible = true
textView.text = getString(R.string.import_e2e_keys_from_file, filename)
}

val builder = AlertDialog.Builder(thisActivity)
.setTitle(R.string.encryption_import_room_keys)
.setView(dialogLayout)

val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button)

passPhraseEditText.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text)
}
})

val importDialog = builder.show()

importButton.setOnClickListener(View.OnClickListener {
val password = passPhraseEditText.text.toString()
val resource = openResource(appContext, uri, mimetype ?: getMimeTypeFromUri(appContext, uri))

if (resource?.mContentStream == null) {
appContext.toast("Error")

return@OnClickListener
}

val data: ByteArray
// TODO BG
try {
data = ByteArray(resource.mContentStream!!.available())
resource.mContentStream!!.read(data)
resource.mContentStream!!.close()
} catch (e: Exception) {
try {
resource.mContentStream!!.close()
} catch (e2: Exception) {
Timber.e(e2, "## importKeys()")
}

appContext.toast(e.localizedMessage)

return@OnClickListener
}

displayLoadingView()

mSession.importRoomKeys(data,
password,
null,
object : MatrixCallback<ImportRoomKeysResult> {
override fun onSuccess(data: ImportRoomKeysResult) {
if (!isAdded) {
return
}

hideLoadingView()

AlertDialog.Builder(thisActivity)
.setMessage(getString(R.string.encryption_import_room_keys_success,
data.successfullyNumberOfImportedKeys,
data.totalNumberOfKeys))
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
.show()
}

override fun onFailure(failure: Throwable) {
appContext.toast(failure.localizedMessage)
hideLoadingView()
}
})

importDialog.dismiss()
})
}

displayLoadingView()

session.importRoomKeys(data,
password,
null,
object : MatrixCallback<ImportRoomKeysResult> {
override fun onSuccess(info: ImportRoomKeysResult) {
if (!isAdded) {
return
}

hideLoadingView()

info?.let {
AlertDialog.Builder(thisActivity)
.setMessage(getString(R.string.encryption_import_room_keys_success,
it.successfullyNumberOfImportedKeys,
it.totalNumberOfKeys))
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
.show()
}
}

override fun onFailure(failure: Throwable) {
appContext.toast(failure.localizedMessage)
hideLoadingView()
}
})

importDialog.dismiss()
})
}
*/
}

//==============================================================================================================

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -11,6 +12,18 @@
android:paddingRight="?dialogPreferredPadding"
android:paddingBottom="12dp">

<TextView
android:id="@+id/dialog_e2e_keys_passphrase_filename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
android:visibility="gone"
tools:text="filename.txt"
tools:visibility="visible" />

<com.google.android.material.textfield.TextInputLayout
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"

View File

@ -4,4 +4,6 @@
<!-- Strings not defined in Riot -->


<string name="import_e2e_keys_from_file">"Import e2e keys from file \"%1$s\"."</string>

</resources>