mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 19:49:33 +01:00
Merge pull request #1946 from nextcloud/feature/noid/media-overview
media overview
This commit is contained in:
commit
df62c87b57
@ -332,6 +332,10 @@ dependencies {
|
||||
|
||||
gplayImplementation 'com.google.android.gms:play-services-base:18.0.1'
|
||||
gplayImplementation "com.google.firebase:firebase-messaging:23.0.0"
|
||||
|
||||
// TODO: Define variable for version
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
|
||||
// implementation 'androidx.activity:activity-ktx:1.4.0'
|
||||
}
|
||||
|
||||
task installGitHooks(type: Copy, group: "development") {
|
||||
|
@ -167,6 +167,10 @@
|
||||
android:theme="@style/TakePhotoTheme"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.SharedItemsActivity"
|
||||
android:theme="@style/AppTheme"/>
|
||||
|
||||
<receiver android:name=".receivers.PackageReplacedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
|
@ -118,6 +118,15 @@ class MainActivity : BaseActivity(), ActionBarProvider {
|
||||
if (userUtils.anyUserExists()) {
|
||||
setDefaultRootController()
|
||||
} else {
|
||||
launchLoginScreen()
|
||||
}
|
||||
} else {
|
||||
launchLoginScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchLoginScreen() {
|
||||
if (!TextUtils.isEmpty(resources.getString(R.string.weblogin_url))) {
|
||||
router!!.pushController(
|
||||
RouterTransaction.with(
|
||||
@ -134,25 +143,6 @@ class MainActivity : BaseActivity(), ActionBarProvider {
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!TextUtils.isEmpty(resources.getString(R.string.weblogin_url))) {
|
||||
router!!.pushController(
|
||||
RouterTransaction.with(
|
||||
WebViewLoginController(resources.getString(R.string.weblogin_url), false)
|
||||
)
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
)
|
||||
} else {
|
||||
router!!.setRoot(
|
||||
RouterTransaction.with(ServerSelectionController())
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
Log.d(TAG, "onStart: Activity: " + System.identityHashCode(this).toString())
|
||||
|
@ -0,0 +1,180 @@
|
||||
package com.nextcloud.talk.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.adapters.SharedItemsGridAdapter
|
||||
import com.nextcloud.talk.adapters.SharedItemsListAdapter
|
||||
import com.nextcloud.talk.databinding.ActivitySharedItemsBinding
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.repositories.SharedItemType
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
|
||||
import com.nextcloud.talk.viewmodels.SharedItemsViewModel
|
||||
|
||||
class SharedItemsActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivitySharedItemsBinding
|
||||
private lateinit var viewModel: SharedItemsViewModel
|
||||
private lateinit var currentTab: SharedItemType
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
currentTab = SharedItemType.MEDIA
|
||||
|
||||
val roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)!!
|
||||
val conversationName = intent.getStringExtra(KEY_CONVERSATION_NAME)
|
||||
val userEntity = intent.getParcelableExtra<UserEntity>(KEY_USER_ENTITY)!!
|
||||
|
||||
binding = ActivitySharedItemsBinding.inflate(layoutInflater)
|
||||
setSupportActionBar(binding.sharedItemsToolbar)
|
||||
setContentView(binding.root)
|
||||
|
||||
DisplayUtils.applyColorToStatusBar(
|
||||
this,
|
||||
ResourcesCompat.getColor(
|
||||
resources, R.color.appbar, null
|
||||
)
|
||||
)
|
||||
DisplayUtils.applyColorToNavigationBar(
|
||||
this.window,
|
||||
ResourcesCompat.getColor(resources, R.color.bg_default, null)
|
||||
)
|
||||
|
||||
supportActionBar?.title = conversationName
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
SharedItemsViewModel.Factory(userEntity, roomToken, currentTab)
|
||||
).get(SharedItemsViewModel::class.java)
|
||||
|
||||
viewModel.sharedItemType.observe(this) {
|
||||
initTabs(it)
|
||||
}
|
||||
|
||||
viewModel.sharedItems.observe(this) {
|
||||
Log.d(TAG, "Items received: $it")
|
||||
|
||||
if (currentTab == SharedItemType.MEDIA) {
|
||||
val adapter = SharedItemsGridAdapter()
|
||||
adapter.items = it.items
|
||||
adapter.authHeader = it.authHeader
|
||||
binding.imageRecycler.adapter = adapter
|
||||
|
||||
val layoutManager = GridLayoutManager(this, SPAN_COUNT)
|
||||
binding.imageRecycler.layoutManager = layoutManager
|
||||
} else {
|
||||
val adapter = SharedItemsListAdapter()
|
||||
adapter.items = it.items
|
||||
adapter.authHeader = it.authHeader
|
||||
binding.imageRecycler.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
layoutManager.orientation = LinearLayoutManager.VERTICAL
|
||||
binding.imageRecycler.layoutManager = layoutManager
|
||||
}
|
||||
}
|
||||
|
||||
binding.imageRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
viewModel.loadNextItems()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun updateItems(type: SharedItemType) {
|
||||
currentTab = type
|
||||
viewModel.loadItems(type)
|
||||
}
|
||||
|
||||
private fun initTabs(sharedItemTypes: Set<SharedItemType>) {
|
||||
|
||||
if (sharedItemTypes.contains(SharedItemType.MEDIA)) {
|
||||
val tabMedia: TabLayout.Tab = binding.sharedItemsTabs.newTab()
|
||||
tabMedia.tag = SharedItemType.MEDIA
|
||||
tabMedia.setText(R.string.shared_items_media)
|
||||
binding.sharedItemsTabs.addTab(tabMedia)
|
||||
}
|
||||
|
||||
if (sharedItemTypes.contains(SharedItemType.FILE)) {
|
||||
val tabFile: TabLayout.Tab = binding.sharedItemsTabs.newTab()
|
||||
tabFile.tag = SharedItemType.FILE
|
||||
tabFile.setText(R.string.shared_items_file)
|
||||
binding.sharedItemsTabs.addTab(tabFile)
|
||||
}
|
||||
|
||||
if (sharedItemTypes.contains(SharedItemType.AUDIO)) {
|
||||
val tabAudio: TabLayout.Tab = binding.sharedItemsTabs.newTab()
|
||||
tabAudio.tag = SharedItemType.AUDIO
|
||||
tabAudio.setText(R.string.shared_items_audio)
|
||||
binding.sharedItemsTabs.addTab(tabAudio)
|
||||
}
|
||||
|
||||
if (sharedItemTypes.contains(SharedItemType.VOICE)) {
|
||||
val tabVoice: TabLayout.Tab = binding.sharedItemsTabs.newTab()
|
||||
tabVoice.tag = SharedItemType.VOICE
|
||||
tabVoice.setText(R.string.shared_items_voice)
|
||||
binding.sharedItemsTabs.addTab(tabVoice)
|
||||
}
|
||||
|
||||
// if(sharedItemTypes.contains(SharedItemType.LOCATION)) {
|
||||
// val tabLocation: TabLayout.Tab = binding.sharedItemsTabs.newTab()
|
||||
// tabLocation.tag = SharedItemType.LOCATION
|
||||
// tabLocation.text = "location"
|
||||
// binding.sharedItemsTabs.addTab(tabLocation)
|
||||
// }
|
||||
|
||||
// if(sharedItemTypes.contains(SharedItemType.DECKCARD)) {
|
||||
// val tabDeckCard: TabLayout.Tab = binding.sharedItemsTabs.newTab()
|
||||
// tabDeckCard.tag = SharedItemType.DECKCARD
|
||||
// tabDeckCard.text = "deckcard"
|
||||
// binding.sharedItemsTabs.addTab(tabDeckCard)
|
||||
// }
|
||||
|
||||
// if(sharedItemTypes.contains(SharedItemType.OTHER)) {
|
||||
// val tabOther: TabLayout.Tab = binding.sharedItemsTabs.newTab()
|
||||
// tabOther.tag = SharedItemType.OTHER
|
||||
// tabOther.setText(R.string.shared_items_other)
|
||||
// binding.sharedItemsTabs.addTab(tabOther)
|
||||
// }
|
||||
|
||||
binding.sharedItemsTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
updateItems(tab.tag as SharedItemType)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = SharedItemsActivity::class.simpleName
|
||||
const val SPAN_COUNT: Int = 4
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.drawee.controller.BaseControllerListener
|
||||
import com.facebook.drawee.controller.ControllerListener
|
||||
import com.facebook.drawee.interfaces.DraweeController
|
||||
import com.facebook.drawee.view.SimpleDraweeView
|
||||
import com.facebook.imagepipeline.common.RotationOptions
|
||||
import com.facebook.imagepipeline.image.ImageInfo
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
||||
import com.nextcloud.talk.databinding.SharedItemGridBinding
|
||||
import com.nextcloud.talk.repositories.SharedItem
|
||||
import com.nextcloud.talk.utils.DrawableUtils
|
||||
import com.nextcloud.talk.utils.FileViewerUtils
|
||||
|
||||
class SharedItemsGridAdapter : RecyclerView.Adapter<SharedItemsGridAdapter.ViewHolder>() {
|
||||
|
||||
companion object {
|
||||
private val TAG = SharedItemsGridAdapter::class.simpleName
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: SharedItemGridBinding, itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
var authHeader: Map<String, String> = emptyMap()
|
||||
var items: List<SharedItem> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = SharedItemGridBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding, binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
|
||||
val currentItem = items[position]
|
||||
|
||||
if (currentItem.previewAvailable) {
|
||||
val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(currentItem.previewLink))
|
||||
.setProgressiveRenderingEnabled(true)
|
||||
.setRotationOptions(RotationOptions.autoRotate())
|
||||
.disableDiskCache()
|
||||
.setHeaders(authHeader)
|
||||
.build()
|
||||
|
||||
val listener: ControllerListener<ImageInfo?> = object : BaseControllerListener<ImageInfo?>() {
|
||||
override fun onFailure(id: String, e: Throwable) {
|
||||
Log.w(TAG, "Failed to load image. A static mimetype image will be used", e)
|
||||
setStaticMimetypeImage(currentItem, holder)
|
||||
}
|
||||
}
|
||||
|
||||
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
|
||||
.setOldController(holder.binding.image.controller)
|
||||
.setAutoPlayAnimations(true)
|
||||
.setImageRequest(imageRequest)
|
||||
.setControllerListener(listener)
|
||||
.build()
|
||||
holder.binding.image.controller = draweeController
|
||||
} else {
|
||||
setStaticMimetypeImage(currentItem, holder)
|
||||
}
|
||||
|
||||
val fileViewerUtils = FileViewerUtils(holder.binding.image.context, currentItem.userEntity)
|
||||
|
||||
holder.binding.image.setOnClickListener {
|
||||
fileViewerUtils.openFile(
|
||||
FileViewerUtils.FileInfo(currentItem.id, currentItem.name, currentItem.fileSize),
|
||||
currentItem.path,
|
||||
currentItem.link,
|
||||
currentItem.mimeType,
|
||||
FileViewerUtils.ProgressUi(
|
||||
holder.binding.progressBar,
|
||||
null,
|
||||
it as SimpleDraweeView
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fileViewerUtils.resumeToUpdateViewsByProgress(
|
||||
currentItem.name,
|
||||
currentItem.id,
|
||||
currentItem.mimeType,
|
||||
FileViewerUtils.ProgressUi(holder.binding.progressBar, null, holder.binding.image)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setStaticMimetypeImage(
|
||||
currentItem: SharedItem,
|
||||
holder: ViewHolder
|
||||
) {
|
||||
val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(currentItem.mimeType)
|
||||
val drawable = ContextCompat.getDrawable(holder.binding.image.context, drawableResourceId)
|
||||
holder.binding.image.hierarchy.setPlaceholderImage(drawable)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.drawee.controller.BaseControllerListener
|
||||
import com.facebook.drawee.controller.ControllerListener
|
||||
import com.facebook.drawee.interfaces.DraweeController
|
||||
import com.facebook.imagepipeline.common.RotationOptions
|
||||
import com.facebook.imagepipeline.image.ImageInfo
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
||||
import com.nextcloud.talk.databinding.SharedItemListBinding
|
||||
import com.nextcloud.talk.repositories.SharedItem
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.DrawableUtils
|
||||
import com.nextcloud.talk.utils.FileViewerUtils
|
||||
import com.nextcloud.talk.utils.FileViewerUtils.ProgressUi
|
||||
|
||||
class SharedItemsListAdapter : RecyclerView.Adapter<SharedItemsListAdapter.ViewHolder>() {
|
||||
|
||||
companion object {
|
||||
private val TAG = SharedItemsListAdapter::class.simpleName
|
||||
private const val ONE_SECOND_IN_MILLIS = 1000
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: SharedItemListBinding, itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
var authHeader: Map<String, String> = emptyMap()
|
||||
var items: List<SharedItem> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = SharedItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding, binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
|
||||
val currentItem = items[position]
|
||||
|
||||
holder.binding.fileName.text = currentItem.name
|
||||
holder.binding.fileSize.text = Formatter.formatShortFileSize(
|
||||
holder.binding.fileSize.context,
|
||||
currentItem.fileSize.toLong()
|
||||
)
|
||||
holder.binding.fileDate.text = DateUtils.getLocalDateTimeStringFromTimestamp(
|
||||
currentItem.date * ONE_SECOND_IN_MILLIS
|
||||
)
|
||||
|
||||
if (currentItem.previewAvailable) {
|
||||
val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(currentItem.previewLink))
|
||||
.setProgressiveRenderingEnabled(true)
|
||||
.setRotationOptions(RotationOptions.autoRotate())
|
||||
.disableDiskCache()
|
||||
.setHeaders(authHeader)
|
||||
.build()
|
||||
|
||||
val listener: ControllerListener<ImageInfo?> = object : BaseControllerListener<ImageInfo?>() {
|
||||
override fun onFailure(id: String, e: Throwable) {
|
||||
Log.w(TAG, "Failed to load image. A static mimetype image will be used", e)
|
||||
setStaticMimetypeImage(currentItem, holder)
|
||||
}
|
||||
}
|
||||
|
||||
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
|
||||
.setOldController(holder.binding.fileImage.controller)
|
||||
.setAutoPlayAnimations(true)
|
||||
.setImageRequest(imageRequest)
|
||||
.setControllerListener(listener)
|
||||
.build()
|
||||
holder.binding.fileImage.controller = draweeController
|
||||
} else {
|
||||
setStaticMimetypeImage(currentItem, holder)
|
||||
}
|
||||
|
||||
val fileViewerUtils = FileViewerUtils(holder.binding.fileImage.context, currentItem.userEntity)
|
||||
|
||||
holder.binding.fileItem.setOnClickListener {
|
||||
fileViewerUtils.openFile(
|
||||
FileViewerUtils.FileInfo(currentItem.id, currentItem.name, currentItem.fileSize),
|
||||
currentItem.path,
|
||||
currentItem.link,
|
||||
currentItem.mimeType,
|
||||
ProgressUi(
|
||||
holder.binding.progressBar,
|
||||
null,
|
||||
holder.binding.fileImage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fileViewerUtils.resumeToUpdateViewsByProgress(
|
||||
currentItem.name,
|
||||
currentItem.id,
|
||||
currentItem.mimeType,
|
||||
ProgressUi(holder.binding.progressBar, null, holder.binding.fileImage)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setStaticMimetypeImage(
|
||||
currentItem: SharedItem,
|
||||
holder: ViewHolder
|
||||
) {
|
||||
val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(currentItem.mimeType)
|
||||
val drawable = ContextCompat.getDrawable(holder.binding.fileImage.context, drawableResourceId)
|
||||
holder.binding.fileImage.hierarchy.setPlaceholderImage(drawable)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
}
|
@ -27,13 +27,11 @@
|
||||
package com.nextcloud.talk.adapters.messages;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
@ -43,44 +41,31 @@ import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.nextcloud.talk.R;
|
||||
import com.nextcloud.talk.activities.FullScreenImageActivity;
|
||||
import com.nextcloud.talk.activities.FullScreenMediaActivity;
|
||||
import com.nextcloud.talk.activities.FullScreenTextViewerActivity;
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
||||
import com.nextcloud.talk.components.filebrowser.models.BrowserFile;
|
||||
import com.nextcloud.talk.components.filebrowser.models.DavResponse;
|
||||
import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
|
||||
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
|
||||
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker;
|
||||
import com.nextcloud.talk.models.database.CapabilitiesUtil;
|
||||
import com.nextcloud.talk.models.database.UserEntity;
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessage;
|
||||
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet;
|
||||
import com.nextcloud.talk.utils.AccountUtils;
|
||||
import com.nextcloud.talk.utils.DisplayUtils;
|
||||
import com.nextcloud.talk.utils.DrawableUtils;
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys;
|
||||
import com.nextcloud.talk.utils.FileViewerUtils;
|
||||
import com.stfalcon.chatkit.messages.MessageHolders;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.appcompat.view.ContextThemeWrapper;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.emoji.widget.EmojiTextView;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
import androidx.work.WorkManager;
|
||||
import autodagger.AutoInjector;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleObserver;
|
||||
@ -114,6 +99,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
|
||||
|
||||
ReactionsInsideMessageBinding reactionsBinding;
|
||||
|
||||
FileViewerUtils fileViewerUtils;
|
||||
|
||||
View clickView;
|
||||
|
||||
ReactionsInterface reactionsInterface;
|
||||
@ -163,6 +150,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
|
||||
|
||||
if (message.getMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) {
|
||||
|
||||
fileViewerUtils = new FileViewerUtils(context, message.activeUser);
|
||||
|
||||
String fileName = message.getSelectedIndividualHashMap().get(KEY_NAME);
|
||||
getMessageText().setText(fileName);
|
||||
if (message.getSelectedIndividualHashMap().containsKey(KEY_CONTACT_NAME)) {
|
||||
@ -192,51 +181,27 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
|
||||
}
|
||||
|
||||
if (message.activeUser != null && message.activeUser.getUsername() != null && message.activeUser.getBaseUrl() != null) {
|
||||
String accountString =
|
||||
message.activeUser.getUsername() + "@" +
|
||||
message.activeUser.getBaseUrl()
|
||||
.replace("https://", "")
|
||||
.replace("http://", "");
|
||||
|
||||
clickView.setOnClickListener(v -> {
|
||||
String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
|
||||
if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileName)) {
|
||||
openOrDownloadFile(message);
|
||||
} else {
|
||||
openFileInFilesApp(message, accountString);
|
||||
}
|
||||
});
|
||||
clickView.setOnClickListener(v ->
|
||||
fileViewerUtils.openFile(
|
||||
message,
|
||||
new FileViewerUtils.ProgressUi(progressBar, getMessageText(), image)
|
||||
)
|
||||
);
|
||||
|
||||
clickView.setOnLongClickListener(l -> {
|
||||
onMessageViewLongClick(message, accountString);
|
||||
onMessageViewLongClick(message);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
Log.e(TAG, "failed to set click listener because activeUser, username or baseUrl were null");
|
||||
}
|
||||
|
||||
fileViewerUtils.resumeToUpdateViewsByProgress(
|
||||
Objects.requireNonNull(message.getSelectedIndividualHashMap().get(MagicPreviewMessageViewHolder.KEY_NAME)),
|
||||
Objects.requireNonNull(message.getSelectedIndividualHashMap().get(MagicPreviewMessageViewHolder.KEY_ID)),
|
||||
Objects.requireNonNull(message.getSelectedIndividualHashMap().get(MagicPreviewMessageViewHolder.KEY_MIMETYPE)),
|
||||
new FileViewerUtils.ProgressUi(progressBar, getMessageText(), image));
|
||||
|
||||
// check if download worker is already running
|
||||
String fileId = message.getSelectedIndividualHashMap().get(KEY_ID);
|
||||
ListenableFuture<List<WorkInfo>> workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId);
|
||||
|
||||
try {
|
||||
for (WorkInfo workInfo : workers.get()) {
|
||||
if (workInfo.getState() == WorkInfo.State.RUNNING ||
|
||||
workInfo.getState() == WorkInfo.State.ENQUEUED) {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
|
||||
String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.getWorkInfoByIdLiveData(workInfo.getId())
|
||||
.observeForever(info -> updateViewsByProgress(fileName, mimetype, info));
|
||||
}
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e);
|
||||
}
|
||||
} else if (message.getMessageType() == ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) {
|
||||
getMessageText().setText("GIPHY");
|
||||
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", getMessageText());
|
||||
@ -287,151 +252,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public abstract EmojiTextView getMessageText();
|
||||
|
||||
public abstract ProgressBar getProgressBar();
|
||||
|
||||
public abstract SimpleDraweeView getImage();
|
||||
|
||||
public abstract View getPreviewContainer();
|
||||
|
||||
public abstract View getPreviewContactContainer();
|
||||
|
||||
public abstract SimpleDraweeView getPreviewContactPhoto();
|
||||
|
||||
public abstract EmojiTextView getPreviewContactName();
|
||||
|
||||
public abstract ProgressBar getPreviewContactProgressBar();
|
||||
|
||||
public abstract ReactionsInsideMessageBinding getReactionsBinding();
|
||||
|
||||
private void openOrDownloadFile(ChatMessage message) {
|
||||
String filename = message.getSelectedIndividualHashMap().get(KEY_NAME);
|
||||
String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
|
||||
File file = new File(context.getCacheDir(), filename);
|
||||
if (file.exists()) {
|
||||
openFile(filename, mimetype);
|
||||
} else {
|
||||
downloadFileToCache(message);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSupportedForInternalViewer(String mimetype){
|
||||
switch (mimetype) {
|
||||
case "image/png":
|
||||
case "image/jpeg":
|
||||
case "image/gif":
|
||||
case "audio/mpeg":
|
||||
case "audio/wav":
|
||||
case "audio/ogg":
|
||||
case "video/mp4":
|
||||
case "video/quicktime":
|
||||
case "video/ogg":
|
||||
case "text/markdown":
|
||||
case "text/plain":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void openFile(String filename, String mimetype) {
|
||||
switch (mimetype) {
|
||||
case "audio/mpeg":
|
||||
case "audio/wav":
|
||||
case "audio/ogg":
|
||||
case "video/mp4":
|
||||
case "video/quicktime":
|
||||
case "video/ogg":
|
||||
openMediaView(filename, mimetype);
|
||||
break;
|
||||
case "image/png":
|
||||
case "image/jpeg":
|
||||
case "image/gif":
|
||||
openImageView(filename, mimetype);
|
||||
break;
|
||||
case "text/markdown":
|
||||
case "text/plain":
|
||||
openTextView(filename, mimetype);
|
||||
break;
|
||||
default:
|
||||
openFileByExternalApp(filename, mimetype);
|
||||
}
|
||||
}
|
||||
|
||||
private void openFileByExternalApp(String fileName, String mimetype) {
|
||||
String path = context.getCacheDir().getAbsolutePath() + "/" + fileName;
|
||||
File file = new File(path);
|
||||
Intent intent;
|
||||
if (Build.VERSION.SDK_INT < 24) {
|
||||
intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.fromFile(file), mimetype);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||
} else {
|
||||
intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
Uri pdfURI = FileProvider.getUriForFile(context, context.getPackageName(), file);
|
||||
intent.setDataAndType(pdfURI, mimetype);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}
|
||||
|
||||
try {
|
||||
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
} else {
|
||||
Log.e(TAG, "No Application found to open the file. This should have been handled beforehand!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error while opening file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canBeHandledByExternalApp(String mimetype, String fileName) {
|
||||
String path = context.getCacheDir().getAbsolutePath() + "/" + fileName;
|
||||
File file = new File(path);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.fromFile(file), mimetype);
|
||||
return intent.resolveActivity(context.getPackageManager()) != null;
|
||||
}
|
||||
|
||||
private void openImageView(String filename, String mimetype) {
|
||||
Intent fullScreenImageIntent = new Intent(context, FullScreenImageActivity.class);
|
||||
fullScreenImageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
fullScreenImageIntent.putExtra("FILE_NAME", filename);
|
||||
fullScreenImageIntent.putExtra("IS_GIF", isGif(mimetype));
|
||||
context.startActivity(fullScreenImageIntent);
|
||||
}
|
||||
|
||||
private void openFileInFilesApp(ChatMessage message, String accountString) {
|
||||
if (AccountUtils.INSTANCE.canWeOpenFilesApp(context, accountString)) {
|
||||
Intent filesAppIntent = new Intent(Intent.ACTION_VIEW, null);
|
||||
final ComponentName componentName = new ComponentName(
|
||||
context.getString(R.string.nc_import_accounts_from),
|
||||
"com.owncloud.android.ui.activity.FileDisplayActivity"
|
||||
);
|
||||
filesAppIntent.setComponent(componentName);
|
||||
filesAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
filesAppIntent.setPackage(context.getString(R.string.nc_import_accounts_from));
|
||||
filesAppIntent.putExtra(BundleKeys.INSTANCE.getKEY_ACCOUNT(), accountString);
|
||||
filesAppIntent.putExtra(
|
||||
BundleKeys.INSTANCE.getKEY_FILE_ID(),
|
||||
message.getSelectedIndividualHashMap().get(KEY_ID)
|
||||
);
|
||||
context.startActivity(filesAppIntent);
|
||||
} else {
|
||||
Intent browserIntent = new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(message.getSelectedIndividualHashMap().get("link"))
|
||||
);
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(browserIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMessageViewLongClick(ChatMessage message, String accountString) {
|
||||
if (isSupportedForInternalViewer(message.getSelectedIndividualHashMap().get(KEY_MIMETYPE))) {
|
||||
private void onMessageViewLongClick(ChatMessage message) {
|
||||
if (fileViewerUtils.isSupportedForInternalViewer(message.getSelectedIndividualHashMap().get(KEY_MIMETYPE))) {
|
||||
previewMessageInterface.onPreviewMessageLongClick(message);
|
||||
return;
|
||||
}
|
||||
@ -452,132 +274,17 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
|
||||
popupMenu.inflate(R.menu.chat_preview_message_menu);
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(item -> {
|
||||
openFileInFilesApp(message, accountString);
|
||||
if (item.getItemId()== R.id.openInFiles){
|
||||
String keyID = message.getSelectedIndividualHashMap().get(KEY_ID);
|
||||
String link = message.getSelectedIndividualHashMap().get("link");
|
||||
fileViewerUtils.openFileInFilesApp(link, keyID);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
private void downloadFileToCache(ChatMessage message) {
|
||||
|
||||
String baseUrl = message.activeUser.getBaseUrl();
|
||||
String userId = message.activeUser.getUserId();
|
||||
String attachmentFolder = CapabilitiesUtil.getAttachmentFolder(message.activeUser);
|
||||
|
||||
String fileName = message.getSelectedIndividualHashMap().get(KEY_NAME);
|
||||
String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
|
||||
|
||||
String size = message.getSelectedIndividualHashMap().get("size");
|
||||
|
||||
if (size == null) {
|
||||
size = "-1";
|
||||
}
|
||||
Integer fileSize = Integer.valueOf(size);
|
||||
|
||||
String fileId = message.getSelectedIndividualHashMap().get(KEY_ID);
|
||||
String path = message.getSelectedIndividualHashMap().get(KEY_PATH);
|
||||
|
||||
// check if download worker is already running
|
||||
ListenableFuture<List<WorkInfo>> workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId);
|
||||
|
||||
try {
|
||||
for (WorkInfo workInfo : workers.get()) {
|
||||
if (workInfo.getState() == WorkInfo.State.RUNNING || workInfo.getState() == WorkInfo.State.ENQUEUED) {
|
||||
Log.d(TAG, "Download worker for " + fileId + " is already running or " +
|
||||
"scheduled");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.e(TAG, "Error when checking if worker already exsists", e);
|
||||
}
|
||||
|
||||
Data data;
|
||||
OneTimeWorkRequest downloadWorker;
|
||||
|
||||
data = new Data.Builder()
|
||||
.putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
|
||||
.putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
|
||||
.putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
|
||||
.putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
|
||||
.putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
|
||||
.putInt(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
|
||||
.build();
|
||||
|
||||
downloadWorker = new OneTimeWorkRequest.Builder(DownloadFileToCacheWorker.class)
|
||||
.setInputData(data)
|
||||
.addTag(fileId)
|
||||
.build();
|
||||
|
||||
WorkManager.getInstance().enqueue(downloadWorker);
|
||||
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
|
||||
WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.getId()).observeForever(workInfo -> {
|
||||
updateViewsByProgress(fileName, mimetype, workInfo);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateViewsByProgress(String fileName, String mimetype, WorkInfo workInfo) {
|
||||
switch (workInfo.getState()) {
|
||||
case RUNNING:
|
||||
int progress = workInfo.getProgress().getInt(DownloadFileToCacheWorker.PROGRESS, -1);
|
||||
if (progress > -1) {
|
||||
getMessageText().setText(String.format(context.getResources().getString(R.string.filename_progress), fileName, progress));
|
||||
}
|
||||
break;
|
||||
|
||||
case SUCCEEDED:
|
||||
if (image.isShown()) {
|
||||
openFile(fileName, mimetype);
|
||||
} else {
|
||||
Log.d(TAG, "file " + fileName +
|
||||
" was downloaded but it's not opened because view is not shown on screen");
|
||||
}
|
||||
getMessageText().setText(fileName);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
break;
|
||||
|
||||
case FAILED:
|
||||
getMessageText().setText(fileName);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void openMediaView(String filename, String mimetype) {
|
||||
Intent fullScreenMediaIntent = new Intent(context, FullScreenMediaActivity.class);
|
||||
fullScreenMediaIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
fullScreenMediaIntent.putExtra("FILE_NAME", filename);
|
||||
fullScreenMediaIntent.putExtra("AUDIO_ONLY", isAudioOnly(mimetype));
|
||||
context.startActivity(fullScreenMediaIntent);
|
||||
}
|
||||
|
||||
private void openTextView(String filename, String mimetype) {
|
||||
Intent fullScreenTextViewerIntent = new Intent(context, FullScreenTextViewerActivity.class);
|
||||
fullScreenTextViewerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
fullScreenTextViewerIntent.putExtra("FILE_NAME", filename);
|
||||
fullScreenTextViewerIntent.putExtra("IS_MARKDOWN", isMarkdown(mimetype));
|
||||
context.startActivity(fullScreenTextViewerIntent);
|
||||
}
|
||||
|
||||
private boolean isGif(String mimetype) {
|
||||
return ("image/gif").equals(mimetype);
|
||||
}
|
||||
|
||||
private boolean isMarkdown(String mimetype) {
|
||||
return ("text/markdown").equals(mimetype);
|
||||
}
|
||||
|
||||
private boolean isAudioOnly(String mimetype) {
|
||||
return mimetype.startsWith("audio");
|
||||
}
|
||||
|
||||
private void fetchFileInformation(String url, UserEntity activeUser) {
|
||||
Single.fromCallable(new Callable<ReadFilesystemOperation>() {
|
||||
@Override
|
||||
@ -622,4 +329,22 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
|
||||
public void assignPreviewMessageInterface(PreviewMessageInterface previewMessageInterface) {
|
||||
this.previewMessageInterface = previewMessageInterface;
|
||||
}
|
||||
|
||||
public abstract EmojiTextView getMessageText();
|
||||
|
||||
public abstract ProgressBar getProgressBar();
|
||||
|
||||
public abstract SimpleDraweeView getImage();
|
||||
|
||||
public abstract View getPreviewContainer();
|
||||
|
||||
public abstract View getPreviewContactContainer();
|
||||
|
||||
public abstract SimpleDraweeView getPreviewContactPhoto();
|
||||
|
||||
public abstract EmojiTextView getPreviewContactName();
|
||||
|
||||
public abstract ProgressBar getPreviewContactProgressBar();
|
||||
|
||||
public abstract ReactionsInsideMessageBinding getReactionsBinding();
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ package com.nextcloud.talk.api;
|
||||
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverall;
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage;
|
||||
import com.nextcloud.talk.models.json.chat.ChatShareOverall;
|
||||
import com.nextcloud.talk.models.json.chat.ChatShareOverviewOverall;
|
||||
import com.nextcloud.talk.models.json.conversations.RoomOverall;
|
||||
import com.nextcloud.talk.models.json.conversations.RoomsOverall;
|
||||
import com.nextcloud.talk.models.json.generic.GenericOverall;
|
||||
@ -338,6 +340,18 @@ public interface NcApi {
|
||||
@Field("actorDisplayName") String actorDisplayName,
|
||||
@Field("replyTo") Integer replyTo);
|
||||
|
||||
@GET
|
||||
Observable<Response<ChatShareOverall>> getSharedItems(@Header("Authorization") String authorization, @Url String url,
|
||||
@Query("objectType") String objectType,
|
||||
@Nullable @Query("lastKnownMessageId") Integer lastKnownMessageId,
|
||||
@Nullable @Query("limit") Integer limit);
|
||||
|
||||
@GET
|
||||
Observable<Response<ChatShareOverviewOverall>> getSharedItemsOverview(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Nullable @Query("limit") Integer limit);
|
||||
|
||||
|
||||
@GET
|
||||
Observable<MentionOverall> getMentionAutocompleteSuggestions(@Header("Authorization") String authorization,
|
||||
@Url String url, @Query("search") String query,
|
||||
|
@ -46,6 +46,7 @@ import android.os.Build
|
||||
import android.os.Build.VERSION_CODES.O
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Parcelable
|
||||
import android.os.SystemClock
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
@ -99,6 +100,7 @@ import com.nextcloud.talk.BuildConfig
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.CallActivity
|
||||
import com.nextcloud.talk.activities.MainActivity
|
||||
import com.nextcloud.talk.activities.SharedItemsActivity
|
||||
import com.nextcloud.talk.activities.TakePhotoActivity
|
||||
import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
|
||||
import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
|
||||
@ -156,6 +158,7 @@ import com.nextcloud.talk.utils.NotificationUtils
|
||||
import com.nextcloud.talk.utils.UriUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
|
||||
@ -188,9 +191,7 @@ import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.ArrayList
|
||||
import java.util.Date
|
||||
import java.util.HashMap
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.ExecutionException
|
||||
import javax.inject.Inject
|
||||
@ -253,6 +254,7 @@ class ChatController(args: Bundle) :
|
||||
var conversationInfoMenuItem: MenuItem? = null
|
||||
var conversationVoiceCallMenuItem: MenuItem? = null
|
||||
var conversationVideoMenuItem: MenuItem? = null
|
||||
var conversationSharedItemsItem: MenuItem? = null
|
||||
|
||||
var magicWebSocketInstance: MagicWebSocketInstance? = null
|
||||
|
||||
@ -1464,7 +1466,7 @@ class ChatController(args: Bundle) :
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap<BrowserController.BrowserType>(browserType))
|
||||
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap<UserEntity>(conversationUser))
|
||||
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
|
||||
bundle.putString(KEY_ROOM_TOKEN, roomToken)
|
||||
router.pushController(
|
||||
RouterTransaction.with(BrowserForSharingController(bundle))
|
||||
.pushChangeHandler(VerticalChangeHandler())
|
||||
@ -1476,7 +1478,7 @@ class ChatController(args: Bundle) :
|
||||
Log.d(TAG, "showShareLocationScreen")
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
|
||||
bundle.putString(KEY_ROOM_TOKEN, roomToken)
|
||||
router.pushController(
|
||||
RouterTransaction.with(LocationPickerController(bundle))
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
@ -1487,7 +1489,7 @@ class ChatController(args: Bundle) :
|
||||
private fun showConversationInfoScreen() {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser)
|
||||
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
|
||||
bundle.putString(KEY_ROOM_TOKEN, roomToken)
|
||||
bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, inOneToOneCall())
|
||||
router.pushController(
|
||||
RouterTransaction.with(ConversationInfoController(bundle))
|
||||
@ -2300,6 +2302,12 @@ class ChatController(args: Bundle) :
|
||||
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
|
||||
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
|
||||
|
||||
if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "rich-object-list-media")) {
|
||||
conversationSharedItemsItem = menu.findItem(R.id.shared_items)
|
||||
} else {
|
||||
menu.removeItem(R.id.shared_items)
|
||||
}
|
||||
|
||||
loadAvatarForStatusBar()
|
||||
}
|
||||
}
|
||||
@ -2337,10 +2345,22 @@ class ChatController(args: Bundle) :
|
||||
showConversationInfoScreen()
|
||||
return true
|
||||
}
|
||||
R.id.shared_items -> {
|
||||
showSharedItems()
|
||||
return true
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSharedItems() {
|
||||
val intent = Intent(activity, SharedItemsActivity::class.java)
|
||||
intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
|
||||
intent.putExtra(KEY_ROOM_TOKEN, roomToken)
|
||||
intent.putExtra(KEY_USER_ENTITY, conversationUser as Parcelable)
|
||||
activity!!.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
|
||||
val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
|
||||
val chatMessageIterator = chatMessageMap.iterator()
|
||||
@ -2402,7 +2422,7 @@ class ChatController(args: Bundle) :
|
||||
bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
|
||||
bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
|
||||
bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl)
|
||||
bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, it.displayName)
|
||||
bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
|
||||
|
||||
if (isVoiceOnlyCall) {
|
||||
bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true)
|
||||
|
@ -27,9 +27,11 @@
|
||||
package com.nextcloud.talk.controllers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
@ -49,6 +51,7 @@ import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.SharedItemsActivity
|
||||
import com.nextcloud.talk.adapters.items.ParticipantItem
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
@ -88,11 +91,8 @@ import io.reactivex.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.util.ArrayList
|
||||
import java.util.Calendar
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
import java.util.HashMap
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -176,9 +176,25 @@ class ConversationInfoController(args: Bundle) :
|
||||
binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog(null) }
|
||||
binding.addParticipantsAction.setOnClickListener { addParticipants() }
|
||||
|
||||
if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "rich-object-list-media")) {
|
||||
binding.showSharedItemsAction.setOnClickListener { showSharedItems() }
|
||||
} else {
|
||||
binding.categorySharedItems.visibility = View.GONE
|
||||
}
|
||||
|
||||
fetchRoomInfo()
|
||||
}
|
||||
|
||||
private fun showSharedItems() {
|
||||
val intent = Intent(activity, SharedItemsActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.putExtra(BundleKeys.KEY_CONVERSATION_NAME, conversation?.displayName)
|
||||
intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
|
||||
intent.putExtra(BundleKeys.KEY_USER_ENTITY, conversationUser as Parcelable)
|
||||
activity!!.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onViewBound(view: View) {
|
||||
super.onViewBound(view)
|
||||
|
||||
|
@ -156,6 +156,9 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
|
||||
if (MessageDigest.isEqual(
|
||||
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
|
||||
("file").getBytes(Charsets.UTF_8))) {
|
||||
|
||||
// TODO: this selectedIndividualHashMap stuff needs to be analyzed and most likely be refactored!
|
||||
// it just feels wrong to fill this here inside getImageUrl()
|
||||
selectedIndividualHashMap = individualHashMap;
|
||||
if (!isVoiceMessage()) {
|
||||
if (getActiveUser() != null && getActiveUser().getBaseUrl() != null) {
|
||||
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Mario Danic
|
||||
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.talk.models.json.chat;
|
||||
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField;
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject;
|
||||
import com.nextcloud.talk.models.json.generic.GenericOCS;
|
||||
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
@Parcel
|
||||
@JsonObject
|
||||
public class ChatShareOCS {
|
||||
@JsonField(name = "data")
|
||||
public HashMap<String, ChatMessage> data;
|
||||
|
||||
public HashMap<String, ChatMessage> getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public void setData(HashMap<String, ChatMessage> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public boolean equals(final Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ChatShareOCS)) {
|
||||
return false;
|
||||
}
|
||||
final ChatShareOCS other = (ChatShareOCS) o;
|
||||
if (!other.canEqual(this)) {
|
||||
return false;
|
||||
}
|
||||
final Object this$data = this.getData();
|
||||
final Object other$data = other.getData();
|
||||
|
||||
return Objects.equals(this$data, other$data);
|
||||
}
|
||||
|
||||
protected boolean canEqual(final Object other) {
|
||||
return other instanceof ChatShareOCS;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
final int PRIME = 59;
|
||||
int result = 1;
|
||||
final Object $data = this.getData();
|
||||
return result * PRIME + ($data == null ? 43 : $data.hashCode());
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "ChatShareOCS(data=" + this.getData() + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Mario Danic
|
||||
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.models.json.chat;
|
||||
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField;
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject;
|
||||
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Parcel
|
||||
@JsonObject
|
||||
public class ChatShareOverall {
|
||||
@JsonField(name = "ocs")
|
||||
public ChatShareOCS ocs;
|
||||
|
||||
public ChatShareOCS getOcs() {
|
||||
return this.ocs;
|
||||
}
|
||||
|
||||
public void setOcs(ChatShareOCS ocs) {
|
||||
this.ocs = ocs;
|
||||
}
|
||||
|
||||
public boolean equals(final Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ChatShareOverall)) {
|
||||
return false;
|
||||
}
|
||||
final ChatShareOverall other = (ChatShareOverall) o;
|
||||
if (!other.canEqual(this)) {
|
||||
return false;
|
||||
}
|
||||
final Object this$ocs = this.getOcs();
|
||||
final Object other$ocs = other.getOcs();
|
||||
|
||||
return Objects.equals(this$ocs, other$ocs);
|
||||
}
|
||||
|
||||
protected boolean canEqual(final Object other) {
|
||||
return other instanceof ChatShareOverall;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
final int PRIME = 59;
|
||||
int result = 1;
|
||||
final Object $ocs = this.getOcs();
|
||||
return result * PRIME + ($ocs == null ? 43 : $ocs.hashCode());
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "ChatShareOverall(ocs=" + this.getOcs() + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Mario Danic
|
||||
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.talk.models.json.chat;
|
||||
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField;
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject;
|
||||
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@Parcel
|
||||
@JsonObject
|
||||
public class ChatShareOverviewOCS {
|
||||
@JsonField(name = "data")
|
||||
public HashMap<String, List<Object>> data;
|
||||
|
||||
public HashMap<String, List<Object>> getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public void setData(HashMap<String, List<Object>> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public boolean equals(final Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ChatShareOverviewOCS)) {
|
||||
return false;
|
||||
}
|
||||
final ChatShareOverviewOCS other = (ChatShareOverviewOCS) o;
|
||||
if (!other.canEqual(this)) {
|
||||
return false;
|
||||
}
|
||||
final Object this$data = this.getData();
|
||||
final Object other$data = other.getData();
|
||||
|
||||
return Objects.equals(this$data, other$data);
|
||||
}
|
||||
|
||||
protected boolean canEqual(final Object other) {
|
||||
return other instanceof ChatShareOverviewOCS;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
final int PRIME = 59;
|
||||
int result = 1;
|
||||
final Object $data = this.getData();
|
||||
return result * PRIME + ($data == null ? 43 : $data.hashCode());
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "ChatShareOverviewOCS(data=" + this.getData() + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Mario Danic
|
||||
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.models.json.chat;
|
||||
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField;
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject;
|
||||
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Parcel
|
||||
@JsonObject
|
||||
public class ChatShareOverviewOverall {
|
||||
@JsonField(name = "ocs")
|
||||
public ChatShareOverviewOCS ocs;
|
||||
|
||||
public ChatShareOverviewOCS getOcs() {
|
||||
return this.ocs;
|
||||
}
|
||||
|
||||
public void setOcs(ChatShareOverviewOCS ocs) {
|
||||
this.ocs = ocs;
|
||||
}
|
||||
|
||||
public boolean equals(final Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ChatShareOverviewOverall)) {
|
||||
return false;
|
||||
}
|
||||
final ChatShareOverviewOverall other = (ChatShareOverviewOverall) o;
|
||||
if (!other.canEqual(this)) {
|
||||
return false;
|
||||
}
|
||||
final Object this$ocs = this.getOcs();
|
||||
final Object other$ocs = other.getOcs();
|
||||
|
||||
return Objects.equals(this$ocs, other$ocs);
|
||||
}
|
||||
|
||||
protected boolean canEqual(final Object other) {
|
||||
return other instanceof ChatShareOverviewOverall;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
final int PRIME = 59;
|
||||
int result = 1;
|
||||
final Object $ocs = this.getOcs();
|
||||
return result * PRIME + ($ocs == null ? 43 : $ocs.hashCode());
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "ChatShareOverviewOverall(ocs=" + this.getOcs() + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.nextcloud.talk.repositories
|
||||
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
|
||||
data class SharedItem(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val fileSize: Int,
|
||||
val date: Long,
|
||||
val path: String,
|
||||
val link: String,
|
||||
val mimeType: String,
|
||||
val previewAvailable: Boolean,
|
||||
val previewLink: String,
|
||||
val userEntity: UserEntity,
|
||||
)
|
@ -0,0 +1,18 @@
|
||||
package com.nextcloud.talk.repositories
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
enum class SharedItemType {
|
||||
|
||||
AUDIO,
|
||||
FILE,
|
||||
MEDIA,
|
||||
VOICE,
|
||||
LOCATION,
|
||||
DECKCARD,
|
||||
OTHER;
|
||||
|
||||
companion object {
|
||||
fun typeFor(name: String) = valueOf(name.uppercase(Locale.ROOT))
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package com.nextcloud.talk.repositories
|
||||
|
||||
import autodagger.AutoInjector
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.models.json.chat.ChatShareOverall
|
||||
import com.nextcloud.talk.models.json.chat.ChatShareOverviewOverall
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import io.reactivex.Observable
|
||||
import retrofit2.Response
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class SharedItemsRepository {
|
||||
|
||||
var parameters: Parameters? = null
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
|
||||
init {
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
}
|
||||
|
||||
fun media(type: SharedItemType): Observable<Response<ChatShareOverall>>? {
|
||||
return media(type, null)
|
||||
}
|
||||
|
||||
fun media(type: SharedItemType, lastKnownMessageId: Int?): Observable<Response<ChatShareOverall>>? {
|
||||
val credentials = ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)
|
||||
|
||||
return ncApi.getSharedItems(
|
||||
credentials,
|
||||
ApiUtils.getUrlForChatSharedItems(1, parameters!!.baseUrl, parameters!!.roomToken),
|
||||
type.toString().lowercase(Locale.ROOT),
|
||||
lastKnownMessageId,
|
||||
BATCH_SIZE
|
||||
)
|
||||
}
|
||||
|
||||
fun availableTypes(): Observable<Response<ChatShareOverviewOverall>>? {
|
||||
val credentials = ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)
|
||||
|
||||
return ncApi.getSharedItemsOverview(
|
||||
credentials,
|
||||
ApiUtils.getUrlForChatSharedItemsOverview(1, parameters!!.baseUrl, parameters!!.roomToken),
|
||||
1
|
||||
)
|
||||
}
|
||||
|
||||
fun authHeader(): Map<String, String> {
|
||||
return mapOf(Pair("Authorization", ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)))
|
||||
}
|
||||
|
||||
fun previewLink(fileId: String?): String {
|
||||
return ApiUtils.getUrlForFilePreviewWithFileId(
|
||||
parameters!!.baseUrl,
|
||||
fileId,
|
||||
sharedApplication!!.resources.getDimensionPixelSize(R.dimen.maximum_file_preview_size)
|
||||
)
|
||||
}
|
||||
|
||||
data class Parameters(
|
||||
val userName: String,
|
||||
val userToken: String,
|
||||
val baseUrl: String,
|
||||
val userEntity: UserEntity,
|
||||
val roomToken: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val BATCH_SIZE: Int = 28
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.nextcloud.talk.repositories
|
||||
|
||||
class SharedMediaItems(
|
||||
val type: SharedItemType,
|
||||
val items: MutableList<SharedItem>,
|
||||
var lastSeenId: Int?,
|
||||
var moreItemsExisting: Boolean,
|
||||
val authHeader: Map<String, String>
|
||||
)
|
@ -261,6 +261,14 @@ public class ApiUtils {
|
||||
return getUrlForChat(version, baseUrl, token) + "/" + messageId;
|
||||
}
|
||||
|
||||
public static String getUrlForChatSharedItems(int version, String baseUrl, String token) {
|
||||
return getUrlForChat(version, baseUrl, token) + "/share";
|
||||
}
|
||||
|
||||
public static String getUrlForChatSharedItemsOverview(int version, String baseUrl, String token) {
|
||||
return getUrlForChatSharedItems(version, baseUrl, token) + "/overview";
|
||||
}
|
||||
|
||||
public static String getUrlForSignaling(int version, String baseUrl) {
|
||||
return getUrlForApi(version, baseUrl) + "/signaling";
|
||||
}
|
||||
|
393
app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
Normal file
393
app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
Normal file
@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Marcel Hibbe
|
||||
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.emoji.widget.EmojiTextView
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import com.facebook.drawee.view.SimpleDraweeView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.FullScreenImageActivity
|
||||
import com.nextcloud.talk.activities.FullScreenMediaActivity
|
||||
import com.nextcloud.talk.activities.FullScreenTextViewerActivity
|
||||
import com.nextcloud.talk.adapters.messages.MagicPreviewMessageViewHolder
|
||||
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
|
||||
import com.nextcloud.talk.models.database.CapabilitiesUtil
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessage
|
||||
import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
class FileViewerUtils(private val context: Context, private val userEntity: UserEntity) {
|
||||
|
||||
fun openFile(
|
||||
message: ChatMessage,
|
||||
progressUi: ProgressUi
|
||||
) {
|
||||
val fileName = message.getSelectedIndividualHashMap()[MagicPreviewMessageViewHolder.KEY_NAME]!!
|
||||
val mimetype = message.getSelectedIndividualHashMap()[MagicPreviewMessageViewHolder.KEY_MIMETYPE]!!
|
||||
val link = message.getSelectedIndividualHashMap()["link"]!!
|
||||
|
||||
val fileId = message.getSelectedIndividualHashMap()[MagicPreviewMessageViewHolder.KEY_ID]!!
|
||||
val path = message.getSelectedIndividualHashMap()[MagicPreviewMessageViewHolder.KEY_PATH]!!
|
||||
|
||||
var size = message.getSelectedIndividualHashMap()["size"]
|
||||
if (size == null) {
|
||||
size = "-1"
|
||||
}
|
||||
val fileSize = Integer.valueOf(size)
|
||||
|
||||
openFile(
|
||||
FileInfo(fileId, fileName, fileSize),
|
||||
path,
|
||||
link,
|
||||
mimetype,
|
||||
progressUi
|
||||
)
|
||||
}
|
||||
|
||||
fun openFile(
|
||||
fileInfo: FileInfo,
|
||||
path: String,
|
||||
link: String,
|
||||
mimetype: String,
|
||||
progressUi: ProgressUi
|
||||
) {
|
||||
if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileInfo.fileName)) {
|
||||
openOrDownloadFile(
|
||||
fileInfo,
|
||||
path,
|
||||
mimetype,
|
||||
progressUi
|
||||
)
|
||||
} else {
|
||||
openFileInFilesApp(link, fileInfo.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun canBeHandledByExternalApp(mimetype: String, fileName: String): Boolean {
|
||||
val path: String = context.cacheDir.absolutePath + "/" + fileName
|
||||
val file = File(path)
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.fromFile(file), mimetype)
|
||||
return intent.resolveActivity(context.packageManager) != null
|
||||
}
|
||||
|
||||
private fun openOrDownloadFile(
|
||||
fileInfo: FileInfo,
|
||||
path: String,
|
||||
mimetype: String,
|
||||
progressUi: ProgressUi
|
||||
) {
|
||||
val file = File(context.cacheDir, fileInfo.fileName)
|
||||
if (file.exists()) {
|
||||
openFileByMimetype(fileInfo.fileName!!, mimetype!!)
|
||||
} else {
|
||||
downloadFileToCache(
|
||||
fileInfo,
|
||||
path,
|
||||
mimetype,
|
||||
progressUi
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFileByMimetype(filename: String, mimetype: String) {
|
||||
when (mimetype) {
|
||||
"audio/mpeg",
|
||||
"audio/wav",
|
||||
"audio/ogg",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/ogg"
|
||||
-> openMediaView(filename, mimetype)
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif"
|
||||
-> openImageView(filename, mimetype)
|
||||
"text/markdown",
|
||||
"text/plain"
|
||||
-> openTextView(filename, mimetype)
|
||||
else
|
||||
-> openFileByExternalApp(filename, mimetype)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
private fun openFileByExternalApp(fileName: String, mimetype: String) {
|
||||
val path = context.cacheDir.absolutePath + "/" + fileName
|
||||
val file = File(path)
|
||||
val intent: Intent
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.fromFile(file), mimetype)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
} else {
|
||||
intent = Intent()
|
||||
intent.action = Intent.ACTION_VIEW
|
||||
val pdfURI = FileProvider.getUriForFile(context, context.packageName, file)
|
||||
intent.setDataAndType(pdfURI, mimetype)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
try {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
Log.e(TAG, "No Application found to open the file. This should have been handled beforehand!")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error while opening file", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun openFileInFilesApp(link: String, keyID: String) {
|
||||
val accountString = userEntity.username + "@" +
|
||||
userEntity.baseUrl
|
||||
.replace("https://", "")
|
||||
.replace("http://", "")
|
||||
|
||||
if (canWeOpenFilesApp(context, accountString)) {
|
||||
val filesAppIntent = Intent(Intent.ACTION_VIEW, null)
|
||||
val componentName = ComponentName(
|
||||
context.getString(R.string.nc_import_accounts_from),
|
||||
"com.owncloud.android.ui.activity.FileDisplayActivity"
|
||||
)
|
||||
filesAppIntent.component = componentName
|
||||
filesAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
filesAppIntent.setPackage(context.getString(R.string.nc_import_accounts_from))
|
||||
filesAppIntent.putExtra(KEY_ACCOUNT, accountString)
|
||||
filesAppIntent.putExtra(KEY_FILE_ID, keyID)
|
||||
context.startActivity(filesAppIntent)
|
||||
} else {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(link)
|
||||
)
|
||||
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openImageView(filename: String, mimetype: String) {
|
||||
val fullScreenImageIntent = Intent(context, FullScreenImageActivity::class.java)
|
||||
fullScreenImageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
fullScreenImageIntent.putExtra("FILE_NAME", filename)
|
||||
fullScreenImageIntent.putExtra("IS_GIF", isGif(mimetype))
|
||||
context.startActivity(fullScreenImageIntent)
|
||||
}
|
||||
|
||||
private fun openMediaView(filename: String, mimetype: String) {
|
||||
val fullScreenMediaIntent = Intent(context, FullScreenMediaActivity::class.java)
|
||||
fullScreenMediaIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
fullScreenMediaIntent.putExtra("FILE_NAME", filename)
|
||||
fullScreenMediaIntent.putExtra("AUDIO_ONLY", isAudioOnly(mimetype))
|
||||
context.startActivity(fullScreenMediaIntent)
|
||||
}
|
||||
|
||||
private fun openTextView(filename: String, mimetype: String) {
|
||||
val fullScreenTextViewerIntent = Intent(context, FullScreenTextViewerActivity::class.java)
|
||||
fullScreenTextViewerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
fullScreenTextViewerIntent.putExtra("FILE_NAME", filename)
|
||||
fullScreenTextViewerIntent.putExtra("IS_MARKDOWN", isMarkdown(mimetype))
|
||||
context.startActivity(fullScreenTextViewerIntent)
|
||||
}
|
||||
|
||||
fun isSupportedForInternalViewer(mimetype: String?): Boolean {
|
||||
return when (mimetype) {
|
||||
"image/png", "image/jpeg",
|
||||
"image/gif", "audio/mpeg",
|
||||
"audio/wav", "audio/ogg",
|
||||
"video/mp4", "video/quicktime",
|
||||
"video/ogg", "text/markdown",
|
||||
"text/plain" -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isGif(mimetype: String): Boolean {
|
||||
return "image/gif" == mimetype
|
||||
}
|
||||
|
||||
private fun isMarkdown(mimetype: String): Boolean {
|
||||
return "text/markdown" == mimetype
|
||||
}
|
||||
|
||||
private fun isAudioOnly(mimetype: String): Boolean {
|
||||
return mimetype.startsWith("audio")
|
||||
}
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
private fun downloadFileToCache(
|
||||
fileInfo: FileInfo,
|
||||
path: String,
|
||||
mimetype: String,
|
||||
progressUi: ProgressUi
|
||||
) {
|
||||
// check if download worker is already running
|
||||
val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileInfo.fileId!!)
|
||||
try {
|
||||
for (workInfo in workers.get()) {
|
||||
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
Log.d(TAG, "Download worker for $fileInfo.fileId is already running or scheduled")
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
}
|
||||
val downloadWorker: OneTimeWorkRequest
|
||||
val data: Data = Data.Builder()
|
||||
.putString(DownloadFileToCacheWorker.KEY_BASE_URL, userEntity.baseUrl)
|
||||
.putString(DownloadFileToCacheWorker.KEY_USER_ID, userEntity.userId)
|
||||
.putString(
|
||||
DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER,
|
||||
CapabilitiesUtil.getAttachmentFolder(userEntity)
|
||||
)
|
||||
.putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileInfo.fileName)
|
||||
.putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
|
||||
.putInt(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileInfo.fileSize)
|
||||
.build()
|
||||
downloadWorker = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
|
||||
.setInputData(data)
|
||||
.addTag(fileInfo.fileId)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(downloadWorker)
|
||||
progressUi.progressBar?.visibility = View.VISIBLE
|
||||
WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
|
||||
.observeForever { workInfo: WorkInfo? ->
|
||||
updateViewsByProgress(
|
||||
fileInfo.fileName,
|
||||
mimetype,
|
||||
workInfo!!,
|
||||
progressUi
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewsByProgress(
|
||||
fileName: String,
|
||||
mimetype: String,
|
||||
workInfo: WorkInfo,
|
||||
progressUi: ProgressUi
|
||||
) {
|
||||
when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
val progress = workInfo.progress.getInt(DownloadFileToCacheWorker.PROGRESS, -1)
|
||||
if (progress > -1) {
|
||||
progressUi.messageText?.text = String.format(
|
||||
context.resources.getString(R.string.filename_progress),
|
||||
fileName,
|
||||
progress
|
||||
)
|
||||
}
|
||||
}
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
if (progressUi.previewImage.isShown) {
|
||||
openFileByMimetype(fileName, mimetype)
|
||||
} else {
|
||||
Log.d(
|
||||
TAG,
|
||||
"file " + fileName +
|
||||
" was downloaded but it's not opened because view is not shown on screen"
|
||||
)
|
||||
}
|
||||
progressUi.messageText?.text = fileName
|
||||
progressUi.progressBar?.visibility = View.GONE
|
||||
}
|
||||
WorkInfo.State.FAILED -> {
|
||||
progressUi.messageText?.text = fileName
|
||||
progressUi.progressBar?.visibility = View.GONE
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeToUpdateViewsByProgress(
|
||||
fileName: String,
|
||||
fileId: String,
|
||||
mimeType: String,
|
||||
progressUi: ProgressUi
|
||||
) {
|
||||
val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId)
|
||||
|
||||
try {
|
||||
for (workInfo in workers.get()) {
|
||||
if (workInfo.state == WorkInfo.State.RUNNING ||
|
||||
workInfo.state == WorkInfo.State.ENQUEUED
|
||||
) {
|
||||
progressUi.progressBar?.visibility = View.VISIBLE
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.getWorkInfoByIdLiveData(workInfo.id)
|
||||
.observeForever { info: WorkInfo? ->
|
||||
updateViewsByProgress(
|
||||
fileName,
|
||||
mimeType,
|
||||
info!!,
|
||||
progressUi
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
}
|
||||
}
|
||||
|
||||
data class ProgressUi(
|
||||
val progressBar: ProgressBar?,
|
||||
val messageText: EmojiTextView?,
|
||||
val previewImage: SimpleDraweeView
|
||||
)
|
||||
|
||||
data class FileInfo(
|
||||
val fileId: String,
|
||||
val fileName: String,
|
||||
val fileSize: Int
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val TAG = FileViewerUtils::class.simpleName
|
||||
const val KEY_ID = "id"
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
package com.nextcloud.talk.viewmodels
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.models.json.chat.ChatShareOverall
|
||||
import com.nextcloud.talk.models.json.chat.ChatShareOverviewOverall
|
||||
import com.nextcloud.talk.repositories.SharedItem
|
||||
import com.nextcloud.talk.repositories.SharedItemType
|
||||
import com.nextcloud.talk.repositories.SharedItemsRepository
|
||||
import com.nextcloud.talk.repositories.SharedMediaItems
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import retrofit2.Response
|
||||
|
||||
class SharedItemsViewModel(private val repository: SharedItemsRepository, private val initialType: SharedItemType) :
|
||||
ViewModel() {
|
||||
|
||||
private val _sharedItemType: MutableLiveData<Set<SharedItemType>> by lazy {
|
||||
MutableLiveData<Set<SharedItemType>>().also {
|
||||
availableTypes()
|
||||
}
|
||||
}
|
||||
|
||||
private val _sharedItems: MutableLiveData<SharedMediaItems> by lazy {
|
||||
MutableLiveData<SharedMediaItems>().also {
|
||||
loadItems(initialType)
|
||||
}
|
||||
}
|
||||
|
||||
val sharedItemType: LiveData<Set<SharedItemType>>
|
||||
get() = _sharedItemType
|
||||
|
||||
val sharedItems: LiveData<SharedMediaItems>
|
||||
get() = _sharedItems
|
||||
|
||||
fun loadNextItems() {
|
||||
val currentSharedItems = sharedItems.value!!
|
||||
|
||||
if (currentSharedItems.moreItemsExisting) {
|
||||
repository.media(currentSharedItems.type, currentSharedItems.lastSeenId)?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(observer(currentSharedItems.type, false))
|
||||
}
|
||||
}
|
||||
|
||||
fun loadItems(type: SharedItemType) {
|
||||
repository.media(type)?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(observer(type, true))
|
||||
}
|
||||
|
||||
private fun observer(type: SharedItemType, initModel: Boolean): Observer<Response<ChatShareOverall>> {
|
||||
return object : Observer<Response<ChatShareOverall>> {
|
||||
|
||||
var chatLastGiven: Int? = null
|
||||
val items = mutableMapOf<String, SharedItem>()
|
||||
|
||||
override fun onSubscribe(d: Disposable) = Unit
|
||||
|
||||
override fun onNext(response: Response<ChatShareOverall>) {
|
||||
|
||||
if (response.headers()["x-chat-last-given"] != null) {
|
||||
chatLastGiven = response.headers()["x-chat-last-given"]!!.toInt()
|
||||
}
|
||||
|
||||
val mediaItems = response.body()!!.ocs!!.data
|
||||
if (mediaItems != null) {
|
||||
for (it in mediaItems) {
|
||||
if (it.value.messageParameters.containsKey("file")) {
|
||||
val fileParameters = it.value.messageParameters["file"]!!
|
||||
|
||||
val previewAvailable =
|
||||
"yes".equals(fileParameters["preview-available"]!!, ignoreCase = true)
|
||||
|
||||
items[it.value.id] = SharedItem(
|
||||
fileParameters["id"]!!,
|
||||
fileParameters["name"]!!,
|
||||
fileParameters["size"]!!.toInt(),
|
||||
it.value.timestamp,
|
||||
fileParameters["path"]!!,
|
||||
fileParameters["link"]!!,
|
||||
fileParameters["mimetype"]!!,
|
||||
previewAvailable,
|
||||
repository.previewLink(fileParameters["id"]),
|
||||
repository.parameters!!.userEntity
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "location and deckcard are not yet supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.d(TAG, "An error occurred: $e")
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
|
||||
val sortedMutableItems = items.toSortedMap().values.toList().reversed().toMutableList()
|
||||
val moreItemsExisting = items.count() == BATCH_SIZE
|
||||
|
||||
if (initModel) {
|
||||
this@SharedItemsViewModel._sharedItems.value =
|
||||
SharedMediaItems(
|
||||
type,
|
||||
sortedMutableItems,
|
||||
chatLastGiven,
|
||||
moreItemsExisting,
|
||||
repository.authHeader()
|
||||
)
|
||||
} else {
|
||||
val oldItems = this@SharedItemsViewModel._sharedItems.value!!.items
|
||||
this@SharedItemsViewModel._sharedItems.value =
|
||||
SharedMediaItems(
|
||||
type,
|
||||
(oldItems.toMutableList() + sortedMutableItems) as MutableList<SharedItem>,
|
||||
chatLastGiven,
|
||||
moreItemsExisting,
|
||||
repository.authHeader()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun availableTypes() {
|
||||
repository.availableTypes()?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(object : Observer<Response<ChatShareOverviewOverall>> {
|
||||
|
||||
val types = mutableSetOf<SharedItemType>()
|
||||
|
||||
override fun onSubscribe(d: Disposable) = Unit
|
||||
|
||||
override fun onNext(response: Response<ChatShareOverviewOverall>) {
|
||||
val typeMap = response.body()!!.ocs!!.data
|
||||
for (it in typeMap) {
|
||||
if (it.value.size > 0) {
|
||||
try {
|
||||
types += SharedItemType.typeFor(it.key)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.w(TAG, "Server responds an unknown shared item type: ${it.key}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.d(TAG, "An error occurred: $e")
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
this@SharedItemsViewModel._sharedItemType.value = types
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
class Factory(val userEntity: UserEntity, val roomToken: String, private val initialType: SharedItemType) :
|
||||
ViewModelProvider
|
||||
.Factory {
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(SharedItemsViewModel::class.java)) {
|
||||
|
||||
val repository = SharedItemsRepository()
|
||||
repository.parameters = SharedItemsRepository.Parameters(
|
||||
userEntity.userId,
|
||||
userEntity.token,
|
||||
userEntity.baseUrl,
|
||||
userEntity,
|
||||
roomToken
|
||||
)
|
||||
|
||||
return SharedItemsViewModel(repository, initialType) as T
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = SharedItemsViewModel::class.simpleName
|
||||
const val BATCH_SIZE: Int = 28
|
||||
}
|
||||
}
|
25
app/src/main/res/drawable/ic_folder_multiple_image.xml
Normal file
25
app/src/main/res/drawable/ic_folder_multiple_image.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<!--
|
||||
@author Google LLC
|
||||
Copyright (C) 2022 Google LLC
|
||||
|
||||
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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M7,15L11.5,9L15,13.5L17.5,10.5L21,15M22,4H14L12,2H6A2,2 0 0,0 4,4V16A2,2 0 0,0 6,18H22A2,2 0 0,0 24,16V6A2,2 0 0,0 22,4M2,6H0V11H0V20A2,2 0 0,0 2,22H20V20H2V6Z" />
|
||||
</vector>
|
66
app/src/main/res/layout/activity_shared_items.xml
Normal file
66
app/src/main/res/layout/activity_shared_items.xml
Normal file
@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Tim Krüger
|
||||
~ @author Andy Scherzinger
|
||||
~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
|
||||
~ Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:background="@color/bg_default"
|
||||
tools:context=".activities.SharedItemsActivity">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/shared_items_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/appbar"
|
||||
android:theme="?attr/actionBarPopupTheme"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_scrollFlags="enterAlwaysCollapsed|noScroll"
|
||||
app:navigationIconTint="@color/fontAppbar"
|
||||
app:popupTheme="@style/appActionBarPopupMenu"
|
||||
app:titleTextColor="@color/fontAppbar"
|
||||
tools:title="@string/nc_app_product_name" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/shared_items_tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/min_size_clickable_area"
|
||||
android:background="@color/appbar"
|
||||
app:layout_constraintTop_toBottomOf="@id/shared_items_toolbar"
|
||||
app:tabGravity="fill"
|
||||
app:tabMode="fixed"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabTextAppearance="@style/TextAppearanceTab" />
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/image_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/shared_items_tabs"
|
||||
android:layout_marginTop="@dimen/double_margin_between_elements"
|
||||
tools:listitem="@layout/shared_item_grid" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -129,7 +129,7 @@
|
||||
android:id="@+id/participants_list_category"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/settings"
|
||||
android:layout_below="@+id/category_shared_items"
|
||||
android:visibility="gone"
|
||||
apc:cardBackgroundColor="@color/bg_default"
|
||||
apc:cardElevation="0dp"
|
||||
@ -213,6 +213,28 @@
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<com.yarolegovich.mp.MaterialPreferenceCategory
|
||||
android:id="@+id/category_shared_items"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/settings"
|
||||
android:animateLayoutChanges="true"
|
||||
apc:cardBackgroundColor="@color/bg_default"
|
||||
apc:cardElevation="0dp"
|
||||
apc:mpc_title="@string/nc_shared_items">
|
||||
|
||||
<com.yarolegovich.mp.MaterialStandardPreference
|
||||
android:id="@+id/show_shared_items_action"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
apc:mp_icon="@drawable/ic_folder_multiple_image"
|
||||
apc:mp_icon_tint="@color/grey_600"
|
||||
apc:mp_title="@string/nc_shared_items_description" />
|
||||
|
||||
</com.yarolegovich.mp.MaterialPreferenceCategory>
|
||||
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
</RelativeLayout>
|
||||
|
@ -73,7 +73,7 @@
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/textColorMaxContrast"
|
||||
android:textSize="12sp"
|
||||
android:textSize="14sp"
|
||||
tools:text="3 minutes ago" />
|
||||
|
||||
<TextView
|
||||
|
61
app/src/main/res/layout/shared_item_grid.xml
Normal file
61
app/src/main/res/layout/shared_item_grid.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Tim Krüger
|
||||
~ @author Marcel Hibbe
|
||||
~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
|
||||
~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:fresco="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/preview_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:adjustViewBounds="true"
|
||||
app:layout_alignSelf="flex_start"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_flexGrow="1"
|
||||
app:layout_wrapBefore="true">
|
||||
|
||||
<com.facebook.drawee.view.SimpleDraweeView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="4dp"
|
||||
app:placeholderImageScaleType="fitCenter"
|
||||
fresco:actualImageScaleType="centerCrop"
|
||||
fresco:roundedCornerRadius="4dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
125
app/src/main/res/layout/shared_item_list.xml
Normal file
125
app/src/main/res/layout/shared_item_list.xml
Normal file
@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Tim Krüger
|
||||
~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<RelativeLayout 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:id="@+id/file_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/standard_margin"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
android:layout_marginBottom="@dimen/standard_half_margin"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/preview_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
android:adjustViewBounds="true"
|
||||
app:layout_alignSelf="flex_start"
|
||||
app:layout_flexGrow="1"
|
||||
app:layout_wrapBefore="true">
|
||||
|
||||
<com.facebook.drawee.view.SimpleDraweeView
|
||||
xmlns:fresco="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/file_image"
|
||||
android:layout_width="@dimen/mediatab_file_icon_size"
|
||||
android:layout_height="@dimen/mediatab_file_icon_size"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:placeholderImageScaleType="fitCenter"
|
||||
fresco:actualImageScaleType="centerCrop"
|
||||
fresco:roundedCornerRadius="4dp"
|
||||
tools:src="@drawable/ic_call_black_24dp"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@id/preview_container">
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/file_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/ListItem"
|
||||
android:textSize="@dimen/two_line_primary_text_size"
|
||||
tools:text="Filename.md" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/file_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/textColorMaxContrast"
|
||||
android:textSize="14sp"
|
||||
tools:text="11 KB" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/standard_quarter_margin"
|
||||
android:textColor="@color/textColorMaxContrast"
|
||||
android:textSize="14sp"
|
||||
android:text="|"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/file_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/standard_quarter_margin"
|
||||
android:textColor="@color/textColorMaxContrast"
|
||||
android:textSize="14sp"
|
||||
tools:text="04-05-2022 21:16"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -37,8 +37,13 @@
|
||||
|
||||
<item
|
||||
android:id="@+id/conversation_info"
|
||||
android:icon="@drawable/ic_info_white_24dp"
|
||||
android:orderInCategory="1"
|
||||
android:title="@string/nc_conversation_menu_conversation_info"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/shared_items"
|
||||
android:orderInCategory="1"
|
||||
android:title="Shared Items"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
|
@ -70,4 +70,6 @@
|
||||
<dimen name="activity_row_layout_height">48dp</dimen>
|
||||
<dimen name="reaction_bottom_sheet_layout_size">40dp</dimen>
|
||||
<dimen name="standard_eighth_margin">2dp</dimen>
|
||||
|
||||
<dimen name="mediatab_file_icon_size">50dp</dimen>
|
||||
</resources>
|
||||
|
@ -89,7 +89,6 @@
|
||||
<string name="nc_settings_server_eol">The server version is too old and not supported by this version of the Android app</string>
|
||||
<string name="nc_settings_server_almost_eol">The server version is very old and will not be supported in the next release!</string>
|
||||
<string name="nc_settings_warning">Warning</string>
|
||||
<string name="nc_add">Add</string>
|
||||
<string name="nc_settings_wrong_account">Only current account can be reauthorized</string>
|
||||
<string name="nc_settings_no_talk_installed">Talk app is not installed on the server you tried to authenticate against</string>
|
||||
<string name="nc_settings_account_updated">Your already existing account was updated, instead of adding a new one</string>
|
||||
@ -118,7 +117,6 @@
|
||||
<string name="nc_settings_screen_lock_desc">Lock %1$s with Android screen lock or supported biometric method</string>
|
||||
<string name="nc_settings_screen_lock_key" translatable="false">screen_lock</string>
|
||||
<string name="nc_settings_screen_lock_timeout_title">Screen lock inactivity timeout</string>
|
||||
<string name="nc_none">None</string>
|
||||
<string name="nc_settings_screen_lock_timeout_key" translatable="false">screen_lock_timeout</string>
|
||||
<string name="nc_settings_screen_security_title">Screen security</string>
|
||||
<string name="nc_settings_screen_security_desc">Prevents screenshots in the recent list and inside the app</string>
|
||||
@ -205,8 +203,6 @@
|
||||
<string name="nc_call_incoming">INCOMING</string>
|
||||
<string name="nc_call_ringing">RINGING</string>
|
||||
<string name="nc_connecting_call">Connecting…</string>
|
||||
<string name="nc_calling">Calling…</string>
|
||||
<string name="nc_incoming_call">Incoming call from</string>
|
||||
<string name="nc_nick_guest">Guest</string>
|
||||
<string name="nc_public_call">New public conversation</string>
|
||||
<string name="nc_public_call_explanation">Public conversations let you invite people from outside through a specially crafted link.</string>
|
||||
@ -338,7 +334,6 @@
|
||||
<!-- Empty states -->
|
||||
<string name="nc_conversations_empty">Join a conversation or start a new one</string>
|
||||
<string name="nc_conversations_empty_details">Say hi to your friends and colleagues!</string>
|
||||
<string name="nc_hello">Hello</string>
|
||||
|
||||
<!-- Other -->
|
||||
<string name="nc_limit_hit">%s characters limit has been hit</string>
|
||||
@ -379,14 +374,6 @@
|
||||
<string name="nc_lobby_start_soon">The meeting will start soon</string>
|
||||
<string name="nc_manual">Not set</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="nc_no_connection_error">No connection</string>
|
||||
<string name="nc_bad_response_error">Bad response</string>
|
||||
<string name="nc_timeout_error">Timeout</string>
|
||||
<string name="nc_empty_response_error">Empty response</string>
|
||||
<string name="nc_not_defined_error">Unknown error</string>
|
||||
<string name="nc_unauthorized_error">Unauthorized</string>
|
||||
|
||||
<string name="nc_allow_guests">Allow guests</string>
|
||||
<string name="nc_last_moderator_title">Could not leave conversation</string>
|
||||
<string name="nc_last_moderator">You need to promote a new moderator before you can leave %1$s.</string>
|
||||
@ -430,6 +417,10 @@
|
||||
<string name="nc_share_contact">Share contact</string>
|
||||
<string name="nc_share_contact_permission">Permission to read contacts is required</string>
|
||||
|
||||
<!-- shared items -->
|
||||
<string name="nc_shared_items">Shared items</string>
|
||||
<string name="nc_shared_items_description">Images, files, voice messages…</string>
|
||||
|
||||
<!-- voice messages -->
|
||||
<string name="nc_voice_message_filename">Talk recording from %1$s (%2$s)</string>
|
||||
<string name="nc_voice_message_hold_to_record_info">Hold to record, release to send.</string>
|
||||
@ -498,6 +489,7 @@
|
||||
<string name="nc_dialog_invalid_password">Invalid password</string>
|
||||
<string name="nc_dialog_reauth_or_delete">Do you want to reauthorize or delete this account?</string>
|
||||
|
||||
<!-- Take photo -->
|
||||
<string name="take_photo">Take a photo</string>
|
||||
<string name="take_photo_switch_camera">Switch camera</string>
|
||||
<string name="take_photo_retake_photo">Re-take photo</string>
|
||||
@ -507,12 +499,23 @@
|
||||
<string name="take_photo_send">Send</string>
|
||||
<string name="take_photo_error_deleting_picture">Error taking picture</string>
|
||||
<string name="take_photo_permission">Taking a photo is not possible without permissions</string>
|
||||
|
||||
<!-- Audio selection -->
|
||||
<string name="audio_output_bluetooth">Bluetooth</string>
|
||||
<string name="audio_output_speaker">Speaker</string>
|
||||
<string name="audio_output_phone">Phone</string>
|
||||
<string name="audio_output_dialog_headline">Audio output</string>
|
||||
<string name="audio_output_wired_headset">Wired headset</string>
|
||||
|
||||
<!-- Shared items -->
|
||||
<string name="shared_items_media">Media</string>
|
||||
<string name="shared_items_file">File</string>
|
||||
<string name="shared_items_audio">Audio</string>
|
||||
<string name="shared_items_voice">Voice</string>
|
||||
<string name="shared_items_other">Other</string>
|
||||
|
||||
<string name="title_attachments">Attachments</string>
|
||||
|
||||
<string name="reactions_tab_all">All</string>
|
||||
|
||||
</resources>
|
||||
|
@ -257,4 +257,10 @@
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearanceTab" parent="TextAppearance.Design.Tab">
|
||||
<item name="textAllCaps">false</item>
|
||||
<item name="android:textAllCaps">false</item>
|
||||
</style>
|
||||
|
||||
|
||||
</resources>
|
||||
|
@ -1,2 +1,2 @@
|
||||
DO NOT TOUCH; GENERATED BY DRONE
|
||||
<span class="mdl-layout-title">Lint Report: 1 error and 156 warnings</span>
|
||||
<span class="mdl-layout-title">Lint Report: 1 error and 150 warnings</span>
|
||||
|
Loading…
Reference in New Issue
Block a user