From 3f6f492143a7fe9607ecbf4883b2da217a1c87ad Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 5 Apr 2021 22:18:10 +0200 Subject: [PATCH] open files inside app Open files directly inside the app. Download file into cache beforehand if not already done. supported file types: jpg, .png, .gif, .mp3, .mp4, .mov, .wav, .txt, .md thanks to @tobiasKaminsky and @starypatyk Signed-off-by: Marcel Hibbe --- CHANGELOG.md | 1 + app/build.gradle | 15 +- app/src/main/AndroidManifest.xml | 20 +- .../activities/FullScreenImageActivity.kt | 138 ++++++++ .../activities/FullScreenMediaActivity.kt | 147 ++++++++ .../FullScreenTextViewerActivity.kt | 93 +++++ .../MagicPreviewMessageViewHolder.java | 317 ++++++++++++++++-- .../java/com/nextcloud/talk/api/NcApi.java | 6 + .../talk/controllers/ChatController.kt | 5 +- .../talk/jobs/DownloadFileToCacheWorker.kt | 157 +++++++++ .../talk/jobs/UploadAndShareFilesWorker.kt | 24 +- .../com/nextcloud/talk/utils/ApiUtils.java | 4 + .../java/com/nextcloud/talk/utils/UriUtils.kt | 2 +- .../res/layout/activity_full_screen_image.xml | 55 +++ .../res/layout/activity_full_screen_media.xml | 45 +++ .../res/layout/activity_full_screen_text.xml | 45 +++ .../item_custom_incoming_preview_message.xml | 24 +- .../item_custom_outcoming_preview_message.xml | 31 +- .../res/menu/chat_preview_message_menu.xml | 25 ++ app/src/main/res/menu/menu_preview.xml | 25 ++ app/src/main/res/values/strings.xml | 7 +- app/src/main/res/values/styles.xml | 20 ++ app/src/main/res/xml/file_provider_paths.xml | 7 +- 23 files changed, 1155 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt create mode 100644 app/src/main/res/layout/activity_full_screen_image.xml create mode 100644 app/src/main/res/layout/activity_full_screen_media.xml create mode 100644 app/src/main/res/layout/activity_full_screen_text.xml create mode 100644 app/src/main/res/menu/chat_preview_message_menu.xml create mode 100644 app/src/main/res/menu/menu_preview.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index fcbf9c874..6090d60f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Types of changes can be: Added/Changed/Deprecated/Removed/Fixed/Security ## [UNRELEASED] ### Added +- open files inside app (jpg, .png, .gif, .mp3, .mp4, .mov, .wav, .txt, .md) - edit profile information and privacy settings ### Changed diff --git a/app/build.gradle b/app/build.gradle index 072d9f7ce..0a3a6ab32 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -128,7 +128,8 @@ android { ext { daggerVersion = "2.34.1" powermockVersion = "2.0.9" - workVersion = "1.0.1" + workVersion = "2.3.0" + markwonVersion = "4.6.2" } @@ -147,10 +148,10 @@ dependencies { implementation 'com.github.vanniktech:Emoji:0.6.0' implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.1.0' implementation 'org.michaelevans.colorart:library:0.0.3' - implementation "android.arch.work:work-runtime:${workVersion}" - implementation "android.arch.work:work-rxjava2:${workVersion}" + implementation "androidx.work:work-runtime:${workVersion}" + implementation "androidx.work:work-rxjava2:${workVersion}" + androidTestImplementation "androidx.work:work-testing:${workVersion}" implementation 'com.google.android:flexbox:1.1.1' - androidTestImplementation "android.arch.work:work-testing:${workVersion}" implementation ('com.gitlab.bitfireAT:dav4jvm:f2078bc846', { exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser }) @@ -242,6 +243,12 @@ dependencies { implementation 'com.afollestad.material-dialogs:lifecycle:3.1.0' implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.google.android.exoplayer:exoplayer:2.13.3' + + implementation 'com.github.chrisbanes:PhotoView:2.0.0' + implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.23' + + implementation "io.noties.markwon:core:$markwonVersion" //implementation 'com.github.dhaval2404:imagepicker:1.8' implementation 'com.github.tobiaskaminsky:ImagePicker:extraFile-SNAPSHOT' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 51bfa3869..8ceb5add2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,7 +54,7 @@ - + @@ -108,6 +108,24 @@ android:configChanges="orientation|screenSize" android:launchMode="singleTask" /> + + + + + + + + + diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt new file mode 100644 index 000000000..55c4537e9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt @@ -0,0 +1,138 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * @author Dariusz Olszewski + * Copyright (C) 2021 Marcel Hibbe + * Copyright (C) 2021 Dariusz Olszewski + * + * 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 . + */ + +package com.nextcloud.talk.activities + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import com.github.chrisbanes.photoview.PhotoView +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import pl.droidsonroids.gif.GifDrawable +import pl.droidsonroids.gif.GifImageView +import java.io.File + + +class FullScreenImageActivity : AppCompatActivity() { + + private lateinit var path: String + private lateinit var imageWrapperView: FrameLayout + private lateinit var photoView: PhotoView + private lateinit var gifView: GifImageView + + private var showFullscreen = false + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_preview, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == R.id.share) { + val shareUri = FileProvider.getUriForFile(this, + BuildConfig.APPLICATION_ID, + File(path)) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, shareUri) + type = "image/*" + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) + + true + } else { + super.onOptionsItemSelected(item) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_full_screen_image) + setSupportActionBar(findViewById(R.id.imageview_toolbar)) + supportActionBar?.setDisplayShowTitleEnabled(false); + + imageWrapperView = findViewById(R.id.image_wrapper_view) + photoView = findViewById(R.id.photo_view) + gifView = findViewById(R.id.gif_view) + + photoView.setOnPhotoTapListener{ view, x, y -> + toggleFullscreen() + } + photoView.setOnOutsidePhotoTapListener{ + toggleFullscreen() + } + gifView.setOnClickListener{ + toggleFullscreen() + } + + val fileName = intent.getStringExtra("FILE_NAME") + val isGif = intent.getBooleanExtra("IS_GIF", false) + + path = applicationContext.cacheDir.absolutePath + "/" + fileName + if (isGif) { + photoView.visibility = View.INVISIBLE + gifView.visibility = View.VISIBLE + val gifFromUri = GifDrawable(path) + gifView.setImageDrawable(gifFromUri) + } else { + gifView.visibility = View.INVISIBLE + photoView.visibility = View.VISIBLE + photoView.setImageURI(Uri.parse(path)) + } + } + + private fun toggleFullscreen(){ + showFullscreen = !showFullscreen; + if (showFullscreen){ + hideSystemUI() + supportActionBar?.hide() + } else{ + showSystemUI() + supportActionBar?.show() + } + } + + private fun hideSystemUI() { + window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE + or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_FULLSCREEN) + } + + private fun showSystemUI() { + window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt new file mode 100644 index 000000000..640f581e6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt @@ -0,0 +1,147 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * 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 . + */ + +package com.nextcloud.talk.activities + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.WindowManager +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import autodagger.AutoInjector +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.SimpleExoPlayer +import com.google.android.exoplayer2.ui.PlayerControlView +import com.google.android.exoplayer2.ui.StyledPlayerView +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import java.io.File + +@AutoInjector(NextcloudTalkApplication::class) +class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener { + + private lateinit var path: String + private lateinit var playerView: StyledPlayerView + private lateinit var player: SimpleExoPlayer + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_preview, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == R.id.share) { + val shareUri = FileProvider.getUriForFile(this, + BuildConfig.APPLICATION_ID, + File(path)) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, shareUri) + type = "video/*" + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) + + true + } else { + super.onOptionsItemSelected(item) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val fileName = intent.getStringExtra("FILE_NAME") + val isAudioOnly = intent.getBooleanExtra("AUDIO_ONLY", false) + + path = applicationContext.cacheDir.absolutePath + "/" + fileName + + setContentView(R.layout.activity_full_screen_media) + setSupportActionBar(findViewById(R.id.mediaview_toolbar)) + supportActionBar?.setDisplayShowTitleEnabled(false); + + playerView = findViewById(R.id.player_view) + + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + playerView.showController() + if (isAudioOnly) { + playerView.controllerShowTimeoutMs = 0 + } + + playerView.setControllerVisibilityListener { v -> + if (v != 0) { + hideSystemUI() + supportActionBar?.hide() + } else { + showSystemUI() + supportActionBar?.show() + } + } + } + + override fun onStart() { + super.onStart() + initializePlayer() + + val mediaItem: MediaItem = MediaItem.fromUri(path) + player.setMediaItem(mediaItem) + player.prepare() + player.play() + } + + override fun onStop() { + super.onStop() + releasePlayer() + } + + private fun initializePlayer() { + player = SimpleExoPlayer.Builder(applicationContext).build() + playerView.player = player; + player.playWhenReady = true + player.addListener(this) + } + + private fun releasePlayer() { + player.release() + } + + private fun hideSystemUI() { + window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE + or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_FULLSCREEN) + } + + private fun showSystemUI() { + window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt new file mode 100644 index 000000000..c550c49dc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * 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 . + */ + +package com.nextcloud.talk.activities + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import autodagger.AutoInjector +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import io.noties.markwon.Markwon +import java.io.File + + +@AutoInjector(NextcloudTalkApplication::class) +class FullScreenTextViewerActivity : AppCompatActivity() { + + private lateinit var path: String + private lateinit var textView: TextView + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_preview, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == R.id.share) { + val shareUri = FileProvider.getUriForFile(this, + BuildConfig.APPLICATION_ID, + File(path)) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, shareUri) + type = "text/*" + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) + + true + } else { + super.onOptionsItemSelected(item) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_full_screen_text) + setSupportActionBar(findViewById(R.id.textview_toolbar)) + supportActionBar?.setDisplayShowTitleEnabled(false); + + textView = findViewById(R.id.text_view) + + val fileName = intent.getStringExtra("FILE_NAME") + val isMarkdown = intent.getBooleanExtra("IS_MARKDOWN", false) + path = applicationContext.cacheDir.absolutePath + "/" + fileName + var text = readFile(path) + + if (isMarkdown) { + val markwon = Markwon.create(applicationContext); + markwon.setMarkdown(textView, text); + } else { + textView.text = text + } + } + + private fun readFile(fileName: String) = File(fileName).inputStream().readBytes().toString(Charsets.UTF_8) + +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java index 826611432..7e5eb51b1 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java @@ -2,7 +2,9 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Marcel Hibbe * Copyright (C) 2017-2018 Mario Danic + * Copyright (C) 2021 Marcel Hibbe * * 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 @@ -28,19 +30,21 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Handler; +import android.util.Log; +import android.view.Gravity; import android.view.View; -import android.widget.TextView; +import android.widget.PopupMenu; -import androidx.emoji.widget.EmojiTextView; - -import autodagger.AutoInjector; -import butterknife.BindView; -import butterknife.ButterKnife; +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.jobs.DownloadFileToCacheWorker; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.json.chat.ChatMessage; import com.nextcloud.talk.utils.AccountUtils; @@ -48,22 +52,39 @@ import com.nextcloud.talk.utils.DisplayUtils; import com.nextcloud.talk.utils.DrawableUtils; import com.nextcloud.talk.utils.bundle.BundleKeys; import com.stfalcon.chatkit.messages.MessageHolders; + +import java.io.File; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import javax.inject.Inject; + +import androidx.core.content.ContextCompat; +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 butterknife.BindView; +import butterknife.ButterKnife; import io.reactivex.Single; import io.reactivex.SingleObserver; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import okhttp3.OkHttpClient; -import javax.inject.Inject; -import java.util.List; -import java.util.concurrent.Callable; - @AutoInjector(NextcloudTalkApplication.class) public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageMessageViewHolder { + private static String TAG = "MagicPreviewMessageViewHolder"; + @BindView(R.id.messageText) EmojiTextView messageText; + View progressBar; + @Inject Context context; @@ -73,6 +94,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM public MagicPreviewMessageViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); + progressBar = itemView.findViewById(R.id.progress_bar); NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); } @@ -102,35 +124,54 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM } if (message.getMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) { - // it's a preview for a Nextcloud share - messageText.setText(message.getSelectedIndividualHashMap().get("name")); - DisplayUtils.setClickableString(message.getSelectedIndividualHashMap().get("name"), message.getSelectedIndividualHashMap().get("link"), messageText); + String fileName = message.getSelectedIndividualHashMap().get("name"); + messageText.setText(fileName); if (message.getSelectedIndividualHashMap().containsKey("mimetype")) { - image.getHierarchy().setPlaceholderImage(context.getDrawable(DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(message.getSelectedIndividualHashMap().get("mimetype")))); + String mimetype = message.getSelectedIndividualHashMap().get("mimetype"); + int drawableResourceId = DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(mimetype); + Drawable drawable = ContextCompat.getDrawable(context, drawableResourceId); + image.getHierarchy().setPlaceholderImage(drawable); } else { fetchFileInformation("/" + message.getSelectedIndividualHashMap().get("path"), message.activeUser); } + String accountString = + message.activeUser.getUsername() + "@" + message.activeUser.getBaseUrl().replace("https://", "").replace("http://", ""); + image.setOnClickListener(v -> { - - String accountString = - message.activeUser.getUsername() + "@" + message.activeUser.getBaseUrl().replace("https://", "").replace("http://", ""); - - 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("id")); - context.startActivity(filesAppIntent); + String mimetype = message.getSelectedIndividualHashMap().get("mimetype"); + if (isSupportedMimetype(mimetype)) { + openOrDownloadFile(message); } else { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(message.getSelectedIndividualHashMap().get("link"))); - browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(browserIntent); + openFileInFilesApp(message, accountString); } }); + + image.setOnLongClickListener(l -> { + onMessageViewLongClick(message, accountString); + return true; + }); + + // check if download worker is already running + String fileId = message.getSelectedIndividualHashMap().get("id"); + ListenableFuture> 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("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) { messageText.setText("GIPHY"); DisplayUtils.setClickableString("GIPHY", "https://giphy.com", messageText); @@ -151,6 +192,222 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM } } + public boolean isSupportedMimetype(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 openOrDownloadFile(ChatMessage message) { + String filename = message.getSelectedIndividualHashMap().get("name"); + String mimetype = message.getSelectedIndividualHashMap().get("mimetype"); + File file = new File(context.getCacheDir(), filename); + if (file.exists()) { + openFile(filename, mimetype); + } else { + String size = message.getSelectedIndividualHashMap().get("size"); + + if (size == null) { + size = "-1"; + } + Integer fileSize = Integer.valueOf(size); + + String fileId = message.getSelectedIndividualHashMap().get("id"); + String path = message.getSelectedIndividualHashMap().get("path"); + downloadFileToCache( + message.activeUser.getBaseUrl(), + message.activeUser.getUserId(), + message.activeUser.getAttachmentFolder(), + filename, + path, + mimetype, + fileSize, + fileId + ); + } + } + + 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: + Log.w(TAG, "no method defined for mimetype: " + mimetype); + } + } + + 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("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 (isSupportedMimetype(message.getSelectedIndividualHashMap().get("mimetype"))) { + return; + } + + PopupMenu popupMenu = new PopupMenu(this.context, itemView, Gravity.START); + popupMenu.inflate(R.menu.chat_preview_message_menu); + + popupMenu.setOnMenuItemClickListener(item -> { + openFileInFilesApp(message, accountString); + return true; + }); + + popupMenu.show(); + } + + private void downloadFileToCache(String baseUrl, + String userId, + String attachmentFolder, + String fileName, + String path, + String mimetype, + Integer size, + String fileId) { + + // check if download worker is already running + ListenableFuture> 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("Download", "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, size) + .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) { + messageText.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, "image " + fileName + " was downloaded but it's not opened (view is not shown)"); + } + messageText.setText(fileName); + progressBar.setVisibility(View.GONE); + break; + + case FAILED: + messageText.setText(fileName); + progressBar.setVisibility(View.GONE); + 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() { @Override diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 821a02a6d..8600ed610 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -3,7 +3,9 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Marcel Hibbe * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) + * Copyright (C) 2021 Marcel Hibbe * * 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 @@ -374,6 +376,10 @@ public interface NcApi { @Url String url, @Body RequestBody body); + @GET + Call downloadFile(@Header("Authorization") String authorization, + @Url String url); + @DELETE Observable deleteChatMessage(@Header("Authorization") String authorization, @Url String url); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 07ea1c994..5c601918a 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -2,7 +2,9 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Marcel Hibbe * Copyright (C) 2017-2019 Mario Danic + * Copyright (C) 2021 Marcel Hibbe * * 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 @@ -435,8 +437,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { if (newMessagesCount != 0 && layoutManager != null) { - if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < - newMessagesCount) { + if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) { newMessagesCount = 0 if (popupBubble != null && popupBubble!!.isShown) { diff --git a/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt new file mode 100644 index 000000000..a01046c6e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt @@ -0,0 +1,157 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * 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 . + */ + +package com.nextcloud.talk.jobs + +import android.content.Context +import android.util.Log +import androidx.work.Data +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import okhttp3.ResponseBody +import java.io.* +import javax.inject.Inject + + +@AutoInjector(NextcloudTalkApplication::class) +class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerParameters) : + Worker(context, workerParameters) { + + private var totalFileSize: Int = -1 + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userUtils: UserUtils + + @Inject + lateinit var appPreferences: AppPreferences + + override fun doWork(): Result { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + if (totalFileSize > -1) { + setProgressAsync(Data.Builder().putInt(PROGRESS, 0).build()) + } + + try { + val currentUser = userUtils.currentUser + val baseUrl = inputData.getString(KEY_BASE_URL) + val userId = inputData.getString(KEY_USER_ID) + val attachmentFolder = inputData.getString(KEY_ATTACHMENT_FOLDER) + val fileName = inputData.getString(KEY_FILE_NAME) + val remotePath = inputData.getString(KEY_FILE_PATH) + totalFileSize = (inputData.getInt(KEY_FILE_SIZE, -1)) + + checkNotNull(currentUser) + checkNotNull(baseUrl) + checkNotNull(userId) + checkNotNull(attachmentFolder) + checkNotNull(fileName) + checkNotNull(remotePath) + + val url = ApiUtils.getUrlForFileDownload(baseUrl, userId, remotePath) + + return downloadFile(currentUser, url, fileName) + } catch (e: IllegalStateException) { + Log.e(javaClass.simpleName, "Something went wrong when trying to download file", e) + return Result.failure() + } + } + + private fun downloadFile(currentUser: UserEntity, url: String, fileName: String): Result { + val downloadCall = ncApi.downloadFile( + ApiUtils.getCredentials(currentUser.username, currentUser.token), + url) + + return executeDownload(downloadCall.execute().body(), fileName) + } + + private fun executeDownload(body: ResponseBody?, fileName: String): Result { + if (body == null) { + Log.e(TAG, "Response body when downloading $fileName is null!") + return Result.failure() + } + + var count: Int + val data = ByteArray(1024 * 4) + val bis: InputStream = BufferedInputStream(body.byteStream(), 1024 * 8) + val outputFile = File(context.cacheDir, fileName + "_") + val output: OutputStream = FileOutputStream(outputFile) + var total: Long = 0 + val startTime = System.currentTimeMillis() + var timeCount = 1 + + count = bis.read(data) + + while (count != -1) { + if (totalFileSize > -1) { + total += count.toLong() + val progress = (total * 100 / totalFileSize).toInt() + val currentTime = System.currentTimeMillis() - startTime + if (currentTime > 50 * timeCount) { + setProgressAsync(Data.Builder().putInt(PROGRESS, progress).build()) + timeCount++ + } + } + output.write(data, 0, count) + count = bis.read(data) + } + + output.flush() + output.close() + bis.close() + + return onDownloadComplete(fileName) + } + + private fun onDownloadComplete(fileName: String): Result { + val tempFile = File(context.cacheDir, fileName + "_") + val targetFile = File(context.cacheDir, fileName) + + return if (tempFile.renameTo(targetFile)) { + setProgressAsync(Data.Builder().putBoolean(SUCCESS, true).build()) + Result.success() + } else { + Result.failure() + } + } + + companion object { + const val TAG = "DownloadFileToCache" + const val KEY_BASE_URL = "KEY_BASE_URL" + const val KEY_USER_ID = "KEY_USER_ID" + const val KEY_ATTACHMENT_FOLDER = "KEY_ATTACHMENT_FOLDER" + const val KEY_FILE_NAME = "KEY_FILE_NAME" + const val KEY_FILE_PATH = "KEY_FILE_PATH" + const val KEY_FILE_SIZE = "KEY_FILE_SIZE" + const val PROGRESS = "PROGRESS" + const val SUCCESS = "SUCCESS" + + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt index bbc13c299..33a97451a 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt @@ -21,9 +21,7 @@ package com.nextcloud.talk.jobs import android.content.Context -import android.database.Cursor import android.net.Uri -import android.provider.OpenableColumns import android.util.Log import androidx.work.* import autodagger.AutoInjector @@ -45,6 +43,8 @@ import io.reactivex.schedulers.Schedulers import okhttp3.MediaType import okhttp3.RequestBody import retrofit2.Response +import java.io.File +import java.io.FileOutputStream import java.io.InputStream import java.util.* import javax.inject.Inject @@ -80,9 +80,9 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa for (index in sourcefiles.indices) { val sourcefileUri = Uri.parse(sourcefiles[index]) - var filename = UriUtils.getFileName(sourcefileUri, context) + val filename = UriUtils.getFileName(sourcefileUri, context) val requestBody = createRequestBody(sourcefileUri) - uploadFile(currentUser, ncTargetpath, filename, roomToken, requestBody) + uploadFile(currentUser, ncTargetpath, filename, roomToken, requestBody, sourcefileUri) } } catch (e: IllegalStateException) { Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) @@ -107,7 +107,8 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa return requestBody } - private fun uploadFile(currentUser: UserEntity, ncTargetpath: String?, filename: String?, roomToken: String?, requestBody: RequestBody?) { + private fun uploadFile(currentUser: UserEntity, ncTargetpath: String?, filename: String, roomToken: String?, + requestBody: RequestBody?, sourcefileUri: Uri) { ncApi.uploadFile( ApiUtils.getCredentials(currentUser.username, currentUser.token), ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetpath, filename), @@ -128,10 +129,23 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa override fun onComplete() { shareFile(roomToken, currentUser, ncTargetpath, filename) + copyFileToCache(sourcefileUri, filename) } }) } + private fun copyFileToCache(sourceFileUri: Uri, filename: String) { + val cachedFile = File(context.cacheDir, filename) + val outputStream = FileOutputStream(cachedFile) + val inputStream: InputStream = context.contentResolver.openInputStream(sourceFileUri)!! + + inputStream.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + } + private fun shareFile(roomToken: String?, currentUser: UserEntity, ncTargetpath: String?, filename: String?) { val paths: MutableList = ArrayList() paths.add("$ncTargetpath/$filename") diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index d7f7bd18f..14eecdf4a 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -299,6 +299,10 @@ public class ApiUtils { return baseUrl + "/remote.php/dav/files/" + user + attachmentFolder + "/" + filename; } + public static String getUrlForFileDownload(String baseUrl, String user, String remotePath) { + return baseUrl + "/remote.php/dav/files/" + user + "/" + remotePath; + } + public static String getUrlForMessageDeletion(String baseUrl, String token, String messageId) { return baseUrl + ocsApiVersion + spreedApiVersion + "/chat/" + token + "/" + messageId; } diff --git a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt index 5191036d0..a4db61cf3 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt @@ -42,7 +42,7 @@ object UriUtils { } } if (filename == null) { - Log.e(UploadAndShareFilesWorker.TAG, "failed to get DISPLAY_NAME from uri. using fallback.") + Log.e("UriUtils", "failed to get DISPLAY_NAME from uri. using fallback.") filename = uri.path val lastIndexOfSlash = filename!!.lastIndexOf('/') if (lastIndexOfSlash != -1) { diff --git a/app/src/main/res/layout/activity_full_screen_image.xml b/app/src/main/res/layout/activity_full_screen_image.xml new file mode 100644 index 000000000..47d2d59d9 --- /dev/null +++ b/app/src/main/res/layout/activity_full_screen_image.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_full_screen_media.xml b/app/src/main/res/layout/activity_full_screen_media.xml new file mode 100644 index 000000000..44935af75 --- /dev/null +++ b/app/src/main/res/layout/activity_full_screen_media.xml @@ -0,0 +1,45 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_full_screen_text.xml b/app/src/main/res/layout/activity_full_screen_text.xml new file mode 100644 index 000000000..7e728f9e3 --- /dev/null +++ b/app/src/main/res/layout/activity_full_screen_text.xml @@ -0,0 +1,45 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_incoming_preview_message.xml b/app/src/main/res/layout/item_custom_incoming_preview_message.xml index d3f0b4c16..031d78c7d 100644 --- a/app/src/main/res/layout/item_custom_incoming_preview_message.xml +++ b/app/src/main/res/layout/item_custom_incoming_preview_message.xml @@ -2,7 +2,9 @@ ~ Nextcloud Talk application ~ ~ @author Mario Danic + ~ @author Marcel Hibbe ~ Copyright (C) 2017-2018 Mario Danic + ~ Copyright (C) 2021 Marcel Hibbe ~ ~ 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 @@ -48,16 +50,28 @@ app:flexWrap="wrap" app:justifyContent="flex_end"> - + android:adjustViewBounds="true"> + + + + + + ~ Copyright (C) 2021 Marcel Hibbe ~ ~ 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 @@ -40,17 +42,28 @@ app:flexWrap="wrap" app:justifyContent="flex_end"> - + android:adjustViewBounds="true"> + + + + + + app:layout_wrapBefore="true" + tools:text="Message" /> + app:layout_alignSelf="center" + tools:text="12:34:56" /> diff --git a/app/src/main/res/menu/chat_preview_message_menu.xml b/app/src/main/res/menu/chat_preview_message_menu.xml new file mode 100644 index 000000000..89df47bd4 --- /dev/null +++ b/app/src/main/res/menu/chat_preview_message_menu.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/menu/menu_preview.xml b/app/src/main/res/menu/menu_preview.xml new file mode 100644 index 000000000..5388e67c1 --- /dev/null +++ b/app/src/main/res/menu/menu_preview.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6b24ee83..8633770d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -341,6 +341,10 @@ Delete Message deleted successfully, but it might have been leaked to other services + Share + Send to + Open in Files app + Upload local file Share from %1$s @@ -393,7 +397,7 @@ Published Synchronize to trusted servers and the global and public address book Scope toggle - + Search in %s @@ -403,4 +407,5 @@ Open main menu Failed to save %1$s selected + %1$s (%2$d) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 76653f6c7..36b21b75c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -133,6 +133,26 @@ @color/white + + + + + +