forked from GitHub-Mirror/riotX-android
Merge pull request #261 from vector-im/feature/e2e_file
Encrypt attachment in e2e rooms
This commit is contained in:
@ -5,7 +5,7 @@
|
||||
* 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
|
||||
* 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,
|
||||
@ -14,21 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.glide;
|
||||
package im.vector.riotredesign.core.glide
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.bumptech.glide.load.Option
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
|
||||
import com.bumptech.glide.GlideBuilder;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
const val ElementToDecryptOptionKey = "im.vector.riotx.core.glide.ElementToDecrypt"
|
||||
|
||||
@GlideModule
|
||||
public final class MyAppGlideModule extends AppGlideModule {
|
||||
|
||||
@Override
|
||||
public void applyOptions(Context context, GlideBuilder builder) {
|
||||
builder.setLogLevel(Log.ERROR);
|
||||
}
|
||||
val ELEMENT_TO_DECRYPT = Option.memory(
|
||||
ElementToDecryptOptionKey, ElementToDecrypt("", "", ""))
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import im.vector.riotredesign.core.extensions.vectorComponent
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import java.io.InputStream
|
||||
|
||||
@GlideModule
|
||||
class MyAppGlideModule : AppGlideModule() {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
builder.setLogLevel(Log.ERROR)
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
registry.append(ImageContentRenderer.Data::class.java,
|
||||
InputStream::class.java,
|
||||
VectorGlideModelLoaderFactory(context.vectorComponent().activeSessionHolder()))
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import com.bumptech.glide.signature.ObjectKey
|
||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import com.bumptech.glide.load.engine.Resource as Resource1
|
||||
|
||||
|
||||
class VectorGlideModelLoaderFactory(private val activeSessionHolder: ActiveSessionHolder)
|
||||
: ModelLoaderFactory<ImageContentRenderer.Data, InputStream> {
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageContentRenderer.Data, InputStream> {
|
||||
return VectorGlideModelLoader(activeSessionHolder)
|
||||
}
|
||||
|
||||
override fun teardown() {
|
||||
// Is there something to do here?
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class VectorGlideModelLoader(private val activeSessionHolder: ActiveSessionHolder)
|
||||
: ModelLoader<ImageContentRenderer.Data, InputStream> {
|
||||
override fun handles(model: ImageContentRenderer.Data): Boolean {
|
||||
// Always handle
|
||||
return true
|
||||
}
|
||||
|
||||
override fun buildLoadData(model: ImageContentRenderer.Data, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
|
||||
return ModelLoader.LoadData(ObjectKey(model), VectorGlideDataFetcher(activeSessionHolder, model, width, height))
|
||||
}
|
||||
}
|
||||
|
||||
class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val data: ImageContentRenderer.Data,
|
||||
private val width: Int,
|
||||
private val height: Int)
|
||||
: DataFetcher<InputStream> {
|
||||
|
||||
val client = OkHttpClient()
|
||||
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
private var stream: InputStream? = null
|
||||
|
||||
override fun cleanup() {
|
||||
cancel()
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
// ?
|
||||
return DataSource.REMOTE
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream?.close() // interrupts decode if any
|
||||
stream = null
|
||||
} catch (ignore: IOException) {
|
||||
Timber.e(ignore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
Timber.v("Load data: $data")
|
||||
if (data.isLocalFile()) {
|
||||
val initialFile = File(data.url)
|
||||
callback.onDataReady(FileInputStream(initialFile))
|
||||
return
|
||||
}
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val url = contentUrlResolver.resolveFullSize(data.url)
|
||||
?: return
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val inputStream = response.body()?.byteStream()
|
||||
Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}")
|
||||
if (!response.isSuccessful) {
|
||||
callback.onLoadFailed(IOException("Unexpected code $response"))
|
||||
return
|
||||
}
|
||||
stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) {
|
||||
MXEncryptedAttachments.decryptAttachment(inputStream, data.elementToDecrypt)
|
||||
} else {
|
||||
inputStream
|
||||
}
|
||||
callback.onDataReady(stream)
|
||||
}
|
||||
}
|
@ -21,9 +21,9 @@ import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
|
||||
fun getFilenameFromUri(context: Context, uri: Uri): String? {
|
||||
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||
var result: String? = null
|
||||
if (uri.scheme == "content") {
|
||||
if (context != null && uri.scheme == "content") {
|
||||
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
|
@ -616,11 +616,11 @@ class RoomDetailFragment :
|
||||
}
|
||||
|
||||
override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
|
||||
vectorBaseActivity.notImplemented()
|
||||
vectorBaseActivity.notImplemented("open file")
|
||||
}
|
||||
|
||||
override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {
|
||||
vectorBaseActivity.notImplemented()
|
||||
vectorBaseActivity.notImplemented("open audio file")
|
||||
}
|
||||
|
||||
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@ -36,6 +37,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.intent.getFilenameFromUri
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import im.vector.riotredesign.core.resources.UserPreferencesProvider
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
@ -360,13 +362,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
|
||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||
val attachments = action.mediaFiles.map {
|
||||
val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path))
|
||||
|
||||
ContentAttachmentData(
|
||||
size = it.size,
|
||||
duration = it.duration,
|
||||
date = it.date,
|
||||
height = it.height,
|
||||
width = it.width,
|
||||
name = it.name,
|
||||
name = nameWithExtension ?: it.name,
|
||||
path = it.path,
|
||||
mimeType = it.mimeType,
|
||||
type = ContentAttachmentData.Type.values()[it.mediaType]
|
||||
|
@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.riotredesign.EmojiCompatFontProvider
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
@ -71,7 +72,7 @@ class MessageItemFactory @Inject constructor(
|
||||
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
|
||||
if (event.root.unsignedData?.redactedEvent != null) {
|
||||
if (event.root.isRedacted()) {
|
||||
//message is redacted
|
||||
return buildRedactedItem(informationData, highlight, callback)
|
||||
}
|
||||
@ -179,7 +180,8 @@ class MessageItemFactory @Inject constructor(
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
val data = ImageContentRenderer.Data(
|
||||
filename = messageContent.body,
|
||||
url = messageContent.url,
|
||||
url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
height = messageContent.info?.height,
|
||||
maxHeight = maxHeight,
|
||||
width = messageContent.info?.width,
|
||||
@ -220,10 +222,11 @@ class MessageItemFactory @Inject constructor(
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
val thumbnailData = ImageContentRenderer.Data(
|
||||
filename = messageContent.body,
|
||||
url = messageContent.info?.thumbnailUrl,
|
||||
height = messageContent.info?.height,
|
||||
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
|
||||
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
||||
height = messageContent.videoInfo?.height,
|
||||
maxHeight = maxHeight,
|
||||
width = messageContent.info?.width,
|
||||
width = messageContent.videoInfo?.width,
|
||||
maxWidth = maxWidth
|
||||
)
|
||||
|
||||
|
@ -55,7 +55,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
|
||||
// Crypto
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||
EventType.ENCRYPTED -> {
|
||||
if (event.root.isRedacted()) {
|
||||
// Redacted event, let the MessageItemFactory handle it
|
||||
messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
} else {
|
||||
encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||
}
|
||||
}
|
||||
|
||||
// Unhandled event types (yet)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
|
@ -22,8 +22,8 @@ import android.widget.ImageView
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.github.piasy.biv.view.BigImageView
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||
@ -37,6 +37,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
data class Data(
|
||||
val filename: String,
|
||||
val url: String?,
|
||||
val elementToDecrypt: ElementToDecrypt?,
|
||||
val height: Int?,
|
||||
val maxHeight: Int,
|
||||
val width: Int?,
|
||||
@ -59,17 +60,28 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
val (width, height) = processSize(data, mode)
|
||||
imageView.layoutParams.height = height
|
||||
imageView.layoutParams.width = width
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val resolvedUrl = when (mode) {
|
||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
}
|
||||
//Fallback to base url
|
||||
?: data.url
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(resolvedUrl)
|
||||
val glideRequest = if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(data)
|
||||
} else {
|
||||
// Clear image
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val resolvedUrl = when (mode) {
|
||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
}
|
||||
//Fallback to base url
|
||||
?: data.url
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(resolvedUrl)
|
||||
}
|
||||
|
||||
glideRequest
|
||||
.dontAnimate()
|
||||
.transform(RoundedCorners(dpToPx(8, imageView.context)))
|
||||
.thumbnail(0.3f)
|
||||
@ -81,6 +93,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
||||
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
||||
// TODO DECRYPT_FILE Decrypt file
|
||||
imageView.showImage(
|
||||
Uri.parse(thumbnail),
|
||||
Uri.parse(fullSize)
|
||||
|
@ -20,9 +20,11 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.isVisible
|
||||
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
|
||||
import com.github.piasy.biv.view.GlideImageViewFactory
|
||||
import im.vector.riotredesign.core.di.ScreenComponent
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import kotlinx.android.synthetic.main.activity_image_media_viewer.*
|
||||
import javax.inject.Inject
|
||||
@ -44,9 +46,26 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
|
||||
finish()
|
||||
} else {
|
||||
configureToolbar(imageMediaViewerToolbar, mediaData)
|
||||
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
|
||||
|
||||
if (mediaData.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
imageMediaViewerImageView.isVisible = false
|
||||
encryptedImageView.isVisible = true
|
||||
|
||||
GlideApp
|
||||
.with(this)
|
||||
.load(mediaData)
|
||||
.dontAnimate()
|
||||
.into(encryptedImageView)
|
||||
} else {
|
||||
// Clear image
|
||||
imageMediaViewerImageView.isVisible = true
|
||||
encryptedImageView.isVisible = false
|
||||
|
||||
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ import javax.inject.Inject
|
||||
|
||||
class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
|
||||
|
||||
// TODO DECRYPT_FILE Encrypted data
|
||||
@Parcelize
|
||||
data class Data(
|
||||
val filename: String,
|
||||
|
@ -32,6 +32,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.vector.riotredesign.R
|
||||
@ -46,6 +47,10 @@ import im.vector.riotredesign.core.utils.toast
|
||||
import im.vector.riotredesign.features.MainActivity
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
@ -197,6 +202,18 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||
|
||||
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
notImplemented()
|
||||
|
||||
// TODO DECRYPT_FILE Quick implementation of clear cache, finish this
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
// On UI Thread
|
||||
Glide.get(requireContext()).clearMemory()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
// On BG thread
|
||||
Glide.get(requireContext()).clearDiskCache()
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO
|
||||
displayLoadingView()
|
||||
|
||||
|
@ -119,7 +119,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() {
|
||||
context?.let { context: Context ->
|
||||
AlertDialog.Builder(context)
|
||||
.setSingleChoiceItems(R.array.media_saving_choice,
|
||||
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
|
||||
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
|
||||
PreferencesManager.setSelectedMediasSavingPeriod(activity, n)
|
||||
d.cancel()
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
@ -18,4 +19,11 @@
|
||||
app:failureImageInitScaleType="center"
|
||||
app:optimizeDisplay="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/encryptedImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
Reference in New Issue
Block a user