mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Import keys: WIP
This commit is contained in:
parent
99d2e8388a
commit
907a1d1a4b
6 changed files with 374 additions and 88 deletions
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue