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 <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2021-04-05 22:18:10 +02:00
parent 8a978c726b
commit 3f6f492143
No known key found for this signature in database
GPG key ID: C793F8B59F43CE7B
23 changed files with 1155 additions and 58 deletions

View file

@ -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

View file

@ -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'

View file

@ -108,6 +108,24 @@
android:configChanges="orientation|screenSize"
android:launchMode="singleTask" />
<activity
android:name=".activities.FullScreenImageActivity"
android:theme="@style/FullScreenImageTheme"
android:configChanges="orientation|keyboardHidden|screenSize">
</activity>
<activity
android:name=".activities.FullScreenMediaActivity"
android:theme="@style/FullScreenMediaTheme"
android:configChanges="orientation|keyboardHidden|screenSize">
</activity>
<activity
android:name=".activities.FullScreenTextViewerActivity"
android:theme="@style/FullScreenTextTheme"
android:configChanges="orientation|keyboardHidden|screenSize">
</activity>
<receiver android:name=".receivers.PackageReplacedReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />

View file

@ -0,0 +1,138 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* @author Dariusz Olszewski
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
* 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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -0,0 +1,147 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2021 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.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)
}
}

View file

@ -0,0 +1,93 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2021 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.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)
}

View file

@ -2,7 +2,9 @@
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
* Copyright (C) 2021 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
@ -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<ChatMessage> {
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);
}
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);
image.setOnClickListener(v -> {
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<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("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<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("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<ReadFilesystemOperation>() {
@Override

View file

@ -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 <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
@ -374,6 +376,10 @@ public interface NcApi {
@Url String url,
@Body RequestBody body);
@GET
Call<ResponseBody> downloadFile(@Header("Authorization") String authorization,
@Url String url);
@DELETE
Observable<ChatOverallSingleMessage> deleteChatMessage(@Header("Authorization") String authorization,
@Url String url);

View file

@ -2,7 +2,9 @@
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
* Copyright (C) 2021 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
@ -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) {

View file

@ -0,0 +1,157 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2021 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.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"
}
}

View file

@ -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<String> = ArrayList()
paths.add("$ncTargetpath/$filename")

View file

@ -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;
}

View file

@ -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) {

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ @author Dariusz Olszewski
~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
~ 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 <http://www.gnu.org/licenses/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/image_wrapper_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:fitsSystemWindows="true"
tools:context=".activities.FullScreenImageActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/imageview_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
<pl.droidsonroids.gif.GifImageView
android:id="@+id/gif_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
/>
</FrameLayout>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ Copyright (C) 2021 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/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:fitsSystemWindows="true"
tools:context=".activities.FullScreenMediaActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/mediaview_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:show_buffering="when_playing"
app:show_shuffle_button="true"/>
</FrameLayout>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ Copyright (C) 2021 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/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fitsSystemWindows="true"
tools:context=".activities.FullScreenTextViewerActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/textview_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Light"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
tools:text="Lorem Ipsum"/>
</FrameLayout>

View file

@ -2,7 +2,9 @@
~ Nextcloud Talk application
~
~ @author Mario Danic
~ @author Marcel Hibbe
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~ Copyright (C) 2021 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
@ -48,16 +50,28 @@
app:flexWrap="wrap"
app:justifyContent="flex_end">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
app:layout_alignSelf="flex_start"
android:adjustViewBounds="true">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
app:layout_alignSelf="flex_start"
tools:src="@tools:sample/backgrounds/scenic"/>
tools:src="@drawable/ic_call_black_24dp" />
<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.emoji.widget.EmojiTextView
android:id="@id/messageText"

View file

@ -2,7 +2,9 @@
~ Nextcloud Talk application
~
~ @author Mario Danic
~ @author Marcel Hibbe
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~ Copyright (C) 2021 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
@ -40,17 +42,28 @@
app:flexWrap="wrap"
app:justifyContent="flex_end">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
app:layout_alignSelf="flex_start"
android:adjustViewBounds="true">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
app:layout_alignSelf="flex_start"
app:actualImageScaleType="fitCenter"
tools:src="@tools:sample/backgrounds/scenic"/>
tools:src="@drawable/ic_call_black_24dp" />
<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.emoji.widget.EmojiTextView
android:id="@id/messageText"
@ -63,7 +76,8 @@
android:textSize="12sp"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true" />
app:layout_wrapBefore="true"
tools:text="Message" />
<TextView
android:id="@id/messageTime"
@ -72,7 +86,8 @@
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:textColor="@color/warm_grey_four"
app:layout_alignSelf="center" />
app:layout_alignSelf="center"
tools:text="12:34:56" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Tobias Kaminski
~ Copyright (C) 2021 Tobias Kaminski <tobias@kaminsky.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/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/openInFiles"
android:title="@string/open_in_files_app" />
</menu>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Tobias Kaminski
~ Copyright (C) 2021 Tobias Kaminski <tobias@kaminsky.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/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/share"
android:title="@string/share" />
</menu>

View file

@ -341,6 +341,10 @@
<string name="nc_delete_message">Delete</string>
<string name="nc_delete_message_leaked_to_matterbridge">Message deleted successfully, but it might have been leaked to other services</string>
<string name="share">Share</string>
<string name="send_to">Send to</string>
<string name="open_in_files_app">Open in Files app</string>
<!-- Upload -->
<string name="nc_upload_local_file">Upload local file</string>
<string name="nc_upload_from_cloud">Share from %1$s</string>
@ -403,4 +407,5 @@
<string name="nc_action_open_main_menu">Open main menu</string>
<string name="failed_to_save">Failed to save %1$s</string>
<string name="selected_list_item">selected</string>
<string name="filename_progress">%1$s (%2$d)</string>
</resources>

View file

@ -133,6 +133,26 @@
<item name="android:textColor">@color/white</item>
</style>
<style name="FullScreenImageTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:navigationBarColor">@color/black</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
<style name="FullScreenMediaTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:navigationBarColor">@color/black</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
<style name="FullScreenTextTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:navigationBarColor">@color/black</item>
</style>
<!-- Launch screen -->
<style name="AppTheme.Launcher">
<item name="android:windowBackground">@drawable/launch_screen</item>

View file

@ -19,5 +19,10 @@
-->
<paths>
<files-path name="files" path="/" />
<files-path
name="files"
path="/" />
<cache-path
name="cachedFiles"
path="/" />
</paths>