Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2024-06-18 02:31:12 +02:00
commit 35b4f6c4b7
8 changed files with 753 additions and 711 deletions

View file

@ -74,7 +74,7 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
private val thumbnailAsyncTasks = mutableListOf<ThumbnailsCacheManager.ThumbnailGenerationTask>() private val thumbnailAsyncTasks = mutableListOf<ThumbnailsCacheManager.ThumbnailGenerationTask>()
interface ResultListener { fun interface ResultListener {
fun onResult(@IdRes actionId: Int) fun onResult(@IdRes actionId: Int)
} }

View file

@ -1,707 +0,0 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2023 TSI-mc
* SPDX-FileCopyrightText: 2023 Parneet Singh <gurayaparneet@gmail.com>
* SPDX-FileCopyrightText: 2020 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2016 ownCloud Inc.
* SPDX-FileCopyrightText: 2013 David A. Velasco <dvelasco@solidgear.es>
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
*/
package com.owncloud.android.ui.preview;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import com.nextcloud.client.account.User;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.jobs.BackgroundJobManager;
import com.nextcloud.client.jobs.download.FileDownloadHelper;
import com.nextcloud.client.media.ExoplayerListener;
import com.nextcloud.client.media.NextcloudExoPlayer;
import com.nextcloud.client.media.PlayerServiceConnection;
import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.common.NextcloudClient;
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
import com.nextcloud.utils.extensions.BundleExtensionsKt;
import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.databinding.FragmentPreviewMediaBinding;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.files.StreamMediaFileOperation;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.activity.DrawerActivity;
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
import com.owncloud.android.ui.fragment.FileFragment;
import com.owncloud.android.utils.MimeTypeUtil;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.annotation.StringRes;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
/**
* This fragment shows a preview of a downloaded media file (audio or video).
* <p>
* Trying to get an instance with NULL {@link OCFile} or ownCloud {@link User} values will produce an
* {@link IllegalStateException}.
* <p>
* By now, if the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on
* instantiation too.
*/
public class PreviewMediaFragment extends FileFragment implements OnTouchListener,
Injectable {
private static final String TAG = PreviewMediaFragment.class.getSimpleName();
public static final String EXTRA_FILE = "FILE";
public static final String EXTRA_USER = "USER";
public static final String EXTRA_AUTOPLAY = "AUTOPLAY";
public static final String EXTRA_START_POSITION = "START_POSITION";
private static final String EXTRA_PLAY_POSITION = "PLAY_POSITION";
private static final String EXTRA_PLAYING = "PLAYING";
private static final double MIN_DENSITY_RATIO = 24.0;
private static final String FILE = "FILE";
private static final String USER = "USER";
private static final String PLAYBACK_POSITION = "PLAYBACK_POSITION";
private static final String AUTOPLAY = "AUTOPLAY";
private static final String IS_LIVE_PHOTO = "IS_LIVE_PHOTO";
private User user;
private long savedPlaybackPosition;
private boolean autoplay;
private boolean isLivePhoto;
private boolean prepared;
private PlayerServiceConnection mediaPlayerServiceConnection;
private Uri videoUri;
@Inject ClientFactory clientFactory;
@Inject UserAccountManager accountManager;
@Inject BackgroundJobManager backgroundJobManager;
FragmentPreviewMediaBinding binding;
private ViewGroup emptyListView;
private ExoPlayer exoPlayer;
private NextcloudClient nextcloudClient;
/**
* Creates a fragment to preview a file.
* <p>
* When 'fileToDetail' or 'user' are null
*
* @param fileToDetail An {@link OCFile} to preview in the fragment
* @param user Currently active user
*/
public static PreviewMediaFragment newInstance(OCFile fileToDetail,
User user,
long startPlaybackPosition,
boolean autoplay,
boolean isLivePhoto) {
PreviewMediaFragment previewMediaFragment = new PreviewMediaFragment();
Bundle bundle = new Bundle();
bundle.putParcelable(FILE, fileToDetail);
bundle.putParcelable(USER, user);
bundle.putLong(PLAYBACK_POSITION, startPlaybackPosition);
bundle.putBoolean(AUTOPLAY, autoplay);
bundle.putBoolean(IS_LIVE_PHOTO, isLivePhoto);
previewMediaFragment.setArguments(bundle);
return previewMediaFragment;
}
/**
* Creates an empty fragment for previews.
* <p/>
* MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically (for instance, when the
* device is turned a aside).
* <p/>
* DO NOT CALL IT: an {@link OCFile} and {@link User} must be provided for a successful construction
*/
public PreviewMediaFragment() {
super();
savedPlaybackPosition = 0;
autoplay = true;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
Bundle bundle = getArguments();
if (bundle != null) {
setFile(BundleExtensionsKt.getParcelableArgument(bundle, FILE, OCFile.class));
user = BundleExtensionsKt.getParcelableArgument(bundle, USER, User.class);
savedPlaybackPosition = bundle.getLong(PLAYBACK_POSITION);
autoplay = bundle.getBoolean(AUTOPLAY);
isLivePhoto = bundle.getBoolean(IS_LIVE_PHOTO);
}
mediaPlayerServiceConnection = new PlayerServiceConnection(requireContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
Log_OC.v(TAG, "onCreateView");
binding = FragmentPreviewMediaBinding.inflate(inflater, container, false);
View view = binding.getRoot();
emptyListView = binding.emptyView.emptyListView;
setLoadingView();
return view;
}
private void setLoadingView() {
binding.progress.setVisibility(View.VISIBLE);
binding.emptyView.emptyListView.setVisibility(View.GONE);
}
private void setVideoErrorMessage(String headline, @StringRes int message) {
binding.emptyView.emptyListViewHeadline.setText(headline);
binding.emptyView.emptyListViewText.setText(message);
binding.emptyView.emptyListIcon.setImageResource(R.drawable.file_movie);
binding.emptyView.emptyListViewText.setVisibility(View.VISIBLE);
binding.emptyView.emptyListIcon.setVisibility(View.VISIBLE);
binding.progress.setVisibility(View.GONE);
binding.emptyView.emptyListView.setVisibility(View.VISIBLE);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log_OC.v(TAG, "onActivityCreated");
OCFile file = getFile();
if (savedInstanceState == null) {
if (file == null) {
throw new IllegalStateException("Instanced with a NULL OCFile");
}
if (user == null) {
throw new IllegalStateException("Instanced with a NULL ownCloud Account");
}
} else {
file = BundleExtensionsKt.getParcelableArgument(savedInstanceState, EXTRA_FILE, OCFile.class);
setFile(file);
user = BundleExtensionsKt.getParcelableArgument(savedInstanceState, EXTRA_USER, User.class);
savedPlaybackPosition = savedInstanceState.getInt(EXTRA_PLAY_POSITION);
autoplay = savedInstanceState.getBoolean(EXTRA_PLAYING);
}
if (file != null) {
if (MimeTypeUtil.isVideo(file)) {
binding.exoplayerView.setVisibility(View.VISIBLE);
binding.imagePreview.setVisibility(View.GONE);
} else {
binding.exoplayerView.setVisibility(View.GONE);
binding.imagePreview.setVisibility(View.VISIBLE);
extractAndSetCoverArt(file);
}
}
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
/**
* tries to read the cover art from the audio file and sets it as cover art.
*
* @param file audio file with potential cover art
*/
private void extractAndSetCoverArt(OCFile file) {
if (MimeTypeUtil.isAudio(file)) {
if (file.getStoragePath() == null) {
setThumbnailForAudio(file);
} else {
try {
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(file.getStoragePath());
byte[] data = mmr.getEmbeddedPicture();
if (data != null) {
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
binding.imagePreview.setImageBitmap(bitmap); //associated cover art in bitmap
} else {
setThumbnailForAudio(file);
}
} catch (Throwable t) {
setGenericThumbnail();
}
}
}
}
private void setThumbnailForAudio(OCFile file) {
Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId());
if (thumbnail != null) {
binding.imagePreview.setImageBitmap(thumbnail);
} else {
setGenericThumbnail();
}
}
/**
* Set generic icon (logo) as placeholder for thumbnail in preview.
*/
private void setGenericThumbnail() {
Drawable logo = AppCompatResources.getDrawable(requireContext(), R.drawable.logo);
if (logo != null) {
if (!getResources().getBoolean(R.bool.is_branded_client)) {
// only colour logo of non-branded client
DrawableCompat.setTint(logo, getResources().getColor(R.color.primary, requireContext().getTheme()));
}
binding.imagePreview.setImageDrawable(logo);
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
Log_OC.v(TAG, "onSaveInstanceState");
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
outState.putParcelable(EXTRA_FILE, getFile());
outState.putParcelable(EXTRA_USER, user);
if (MimeTypeUtil.isVideo(getFile()) && exoPlayer != null) {
savedPlaybackPosition = exoPlayer.getCurrentPosition();
autoplay = exoPlayer.isPlaying();
outState.putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition);
outState.putBoolean(EXTRA_PLAYING, autoplay);
} else if (mediaPlayerServiceConnection != null && mediaPlayerServiceConnection.isConnected()) {
outState.putInt(EXTRA_PLAY_POSITION, mediaPlayerServiceConnection.getCurrentPosition());
outState.putBoolean(EXTRA_PLAYING, mediaPlayerServiceConnection.isPlaying());
}
}
@Override
public void onStart() {
super.onStart();
Log_OC.v(TAG, "onStart");
@NonNull Context context;
if (getContext() != null) {
context = getContext();
} else {
context = MainApp.getAppContext();
}
OCFile file = getFile();
if (file != null) {
// bind to any existing player
mediaPlayerServiceConnection.bind();
if (MimeTypeUtil.isAudio(file)) {
binding.mediaController.setMediaPlayer(mediaPlayerServiceConnection);
binding.mediaController.setVisibility(View.VISIBLE);
mediaPlayerServiceConnection.start(user, file, autoplay, savedPlaybackPosition);
binding.emptyView.emptyListView.setVisibility(View.GONE);
binding.progress.setVisibility(View.GONE);
} else if (MimeTypeUtil.isVideo(file)) {
if (mediaPlayerServiceConnection.isConnected()) {
// always stop player
stopAudio();
}
if (exoPlayer != null) {
playVideo();
} else {
final Handler handler = new Handler(Looper.getMainLooper());
Executors.newSingleThreadExecutor().execute(() -> {
try {
nextcloudClient = clientFactory.createNextcloudClient(accountManager.getUser());
handler.post(() -> {
exoPlayer = NextcloudExoPlayer.createNextcloudExoplayer(context, nextcloudClient);
exoPlayer.addListener(new ExoplayerListener(context, binding.exoplayerView, exoPlayer, () -> {
goBackToLivePhoto();
return null;
}));
playVideo();
});
} catch (ClientFactory.CreationException e) {
handler.post(() -> Log_OC.e(TAG, "error setting up ExoPlayer", e));
}
});
}
}
}
}
private void goBackToLivePhoto() {
if (!isLivePhoto) {
return;
}
showActionBar();
requireActivity().getSupportFragmentManager().popBackStack();
}
private void showActionBar() {
Activity currentActivity = requireActivity();
if (currentActivity instanceof PreviewImageActivity activity) {
activity.toggleActionBarVisibility(false);
}
}
@OptIn(markerClass = UnstableApi.class)
private void setupVideoView() {
binding.exoplayerView.setShowNextButton(false);
binding.exoplayerView.setShowPreviousButton(false);
binding.exoplayerView.setPlayer(exoPlayer);
binding.exoplayerView.setFullscreenButtonClickListener(isFullScreen -> startFullScreenVideo());
}
private void stopAudio() {
mediaPlayerServiceConnection.stop();
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.removeItem(R.id.action_search);
inflater.inflate(R.menu.custom_menu_placeholder, menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.custom_menu_placeholder_item) {
final OCFile file = getFile();
if (containerActivity.getStorageManager() != null && file != null) {
// Update the file
final OCFile updatedFile = containerActivity.getStorageManager().getFileById(file.getFileId());
setFile(updatedFile);
final OCFile fileNew = getFile();
if (fileNew != null) {
showFileActions(fileNew);
}
}
}
return super.onOptionsItemSelected(item);
}
private void showFileActions(OCFile file) {
final List<Integer> additionalFilter = new ArrayList<>(
Arrays.asList(
R.id.action_rename_file,
R.id.action_sync_file,
R.id.action_move_or_copy,
R.id.action_favorite,
R.id.action_unset_favorite,
R.id.action_pin_to_homescreen
));
if (getFile() != null && getFile().isSharedWithMe() && !getFile().canReshare()) {
additionalFilter.add(R.id.action_send_share_file);
}
final FragmentManager fragmentManager = getChildFragmentManager();
FileActionsBottomSheet.newInstance(file, false, additionalFilter)
.setResultListener(fragmentManager, this, this::onFileActionChosen)
.show(fragmentManager, "actions");
}
public void onFileActionChosen(final int itemId) {
if (itemId == R.id.action_send_share_file) {
sendShareFile();
} else if (itemId == R.id.action_open_file_with) {
openFile();
} else if (itemId == R.id.action_remove_file) {
RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile());
dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
} else if (itemId == R.id.action_see_details) {
seeDetails();
} else if (itemId == R.id.action_sync_file) {
containerActivity.getFileOperationsHelper().syncFile(getFile());
} else if (itemId == R.id.action_cancel_sync) {
containerActivity.getFileOperationsHelper().cancelTransference(getFile());
} else if (itemId == R.id.action_stream_media) {
containerActivity.getFileOperationsHelper().streamMediaFile(getFile());
} else if (itemId == R.id.action_export_file) {
ArrayList<OCFile> list = new ArrayList<>();
list.add(getFile());
containerActivity.getFileOperationsHelper().exportFiles(list,
getContext(),
getView(),
backgroundJobManager);
} else if (itemId == R.id.action_download_file) {
FileDownloadHelper.Companion.instance().downloadFileIfNotStartedBefore(user, getFile());
}
}
/**
* Update the file of the fragment with file value
*
* @param file Replaces the held file with a new one
*/
public void updateFile(OCFile file) {
setFile(file);
}
private void seeDetails() {
stopPreview(false);
containerActivity.showDetails(getFile());
}
private void sendShareFile() {
stopPreview(false);
containerActivity.getFileOperationsHelper().sendShareFile(getFile());
}
private void playVideo() {
setupVideoView();
// load the video file in the video player
// when done, VideoHelper#onPrepared() will be called
if (getFile().isDown()) {
playVideoUri(getFile().getStorageUri());
} else {
try {
new LoadStreamUrl(this, user, clientFactory).execute(getFile().getLocalId());
} catch (Exception e) {
Log_OC.e(TAG, "Loading stream url not possible: " + e);
}
}
}
private void playVideoUri(final Uri uri) {
binding.progress.setVisibility(View.GONE);
exoPlayer.setMediaItem(MediaItem.fromUri(uri));
exoPlayer.setPlayWhenReady(autoplay);
exoPlayer.prepare();
if (savedPlaybackPosition >= 0) {
exoPlayer.seekTo(savedPlaybackPosition);
}
// only autoplay video once
autoplay = false;
}
private static class LoadStreamUrl extends AsyncTask<Long, Void, Uri> {
private final ClientFactory clientFactory;
private final User user;
private final WeakReference<PreviewMediaFragment> previewMediaFragmentWeakReference;
public LoadStreamUrl(PreviewMediaFragment previewMediaFragment, User user, ClientFactory clientFactory) {
this.previewMediaFragmentWeakReference = new WeakReference<>(previewMediaFragment);
this.user = user;
this.clientFactory = clientFactory;
}
@Override
protected Uri doInBackground(Long... fileId) {
OwnCloudClient client;
try {
client = clientFactory.create(user);
} catch (ClientFactory.CreationException e) {
Log_OC.e(TAG, "Loading stream url not possible: " + e);
return null;
}
StreamMediaFileOperation sfo = new StreamMediaFileOperation(fileId[0]);
RemoteOperationResult result = sfo.execute(client);
if (!result.isSuccess()) {
return null;
}
return Uri.parse((String) result.getData().get(0));
}
@Override
protected void onPostExecute(Uri uri) {
final PreviewMediaFragment previewMediaFragment = previewMediaFragmentWeakReference.get();
final Context context = previewMediaFragment != null ? previewMediaFragment.getContext() : null;
if (previewMediaFragment != null && previewMediaFragment.binding != null && context != null) {
if (uri != null) {
previewMediaFragment.videoUri = uri;
previewMediaFragment.playVideoUri(uri);
} else {
previewMediaFragment.emptyListView.setVisibility(View.VISIBLE);
previewMediaFragment.setVideoErrorMessage(
previewMediaFragment.getString(R.string.stream_not_possible_headline),
R.string.stream_not_possible_message);
}
} else {
Log_OC.e(TAG, "Error streaming file: no previewMediaFragment!");
}
}
}
@Override
public void onPause() {
Log_OC.v(TAG, "onPause");
super.onPause();
}
@Override
public void onResume() {
super.onResume();
Log_OC.v(TAG, "onResume");
}
@Override
public void onDestroy() {
Log_OC.v(TAG, "onDestroy");
super.onDestroy();
}
@Override
public void onDestroyView() {
Log_OC.v(TAG, "onDestroyView");
super.onDestroyView();
binding = null;
}
@Override
public void onStop() {
Log_OC.v(TAG, "onStop");
final OCFile file = getFile();
if (MimeTypeUtil.isAudio(file) && !mediaPlayerServiceConnection.isPlaying()) {
stopAudio();
} else if (MimeTypeUtil.isVideo(file) && exoPlayer != null && exoPlayer.isPlaying()) {
savedPlaybackPosition = exoPlayer.getCurrentPosition();
exoPlayer.pause();
}
mediaPlayerServiceConnection.unbind();
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_UNLOCKED);
super.onStop();
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN && v.equals(binding.exoplayerView)) {
// added a margin on the left to avoid interfering with gesture to open navigation drawer
if (event.getX() / Resources.getSystem().getDisplayMetrics().density > MIN_DENSITY_RATIO) {
startFullScreenVideo();
}
return true;
}
return false;
}
private void startFullScreenVideo() {
final FragmentActivity activity = getActivity();
if (activity != null) {
new PreviewVideoFullscreenDialog(activity, nextcloudClient, exoPlayer, binding.exoplayerView).show();
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log_OC.v(TAG, "onConfigurationChanged " + this);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log_OC.v(TAG, "onActivityResult " + this);
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
savedPlaybackPosition = data.getLongExtra(PreviewMediaFragment.EXTRA_START_POSITION, 0);
autoplay = data.getBooleanExtra(PreviewMediaFragment.EXTRA_AUTOPLAY, false);
}
}
/**
* Opens the previewed file with an external application.
*/
private void openFile() {
stopPreview(true);
containerActivity.getFileOperationsHelper().openFile(getFile());
}
/**
* Helper method to test if an {@link OCFile} can be passed to a {@link PreviewMediaFragment} to be previewed.
*
* @param file File to test if can be previewed.
* @return 'True' if the file can be handled by the fragment.
*/
public static boolean canBePreviewed(OCFile file) {
return file != null && (MimeTypeUtil.isAudio(file) || MimeTypeUtil.isVideo(file));
}
public void stopPreview(boolean stopAudio) {
if (stopAudio && mediaPlayerServiceConnection != null) {
mediaPlayerServiceConnection.stop();
} else if (exoPlayer != null) {
savedPlaybackPosition = exoPlayer.getCurrentPosition();
exoPlayer.stop();
}
}
public long getPosition() {
if (prepared) {
savedPlaybackPosition = exoPlayer.getCurrentPosition();
}
Log_OC.v(TAG, "getting position: " + savedPlaybackPosition);
return savedPlaybackPosition;
}
private void toggleDrawerLockMode(ContainerActivity containerActivity, int lockMode) {
((DrawerActivity) containerActivity).setDrawerLockMode(lockMode);
}
@Override
public void onDetach() {
if (exoPlayer != null) {
exoPlayer.stop();
exoPlayer.release();
}
super.onDetach();
}
}

View file

@ -0,0 +1,720 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2023 TSI-mc
* SPDX-FileCopyrightText: 2023 Parneet Singh <gurayaparneet@gmail.com>
* SPDX-FileCopyrightText: 2020 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2016 ownCloud Inc.
* SPDX-FileCopyrightText: 2013 David A. Velasco <dvelasco@solidgear.es>
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
*/
package com.owncloud.android.ui.preview
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.jobs.download.FileDownloadHelper.Companion.instance
import com.nextcloud.client.media.ExoplayerListener
import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
import com.nextcloud.client.media.PlayerServiceConnection
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.client.network.ClientFactory.CreationException
import com.nextcloud.common.NextcloudClient
import com.nextcloud.ui.fileactions.FileActionsBottomSheet.Companion.newInstance
import com.nextcloud.utils.extensions.getParcelableArgument
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.databinding.FragmentPreviewMediaBinding
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.files.StreamMediaFileOperation
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.activity.DrawerActivity
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment
import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment
import com.owncloud.android.ui.fragment.FileFragment
import com.owncloud.android.utils.MimeTypeUtil
import java.lang.ref.WeakReference
import java.util.concurrent.Executors
import javax.inject.Inject
/**
* This fragment shows a preview of a downloaded media file (audio or video).
*
*
* Trying to get an instance with NULL [OCFile] or ownCloud [User] values will produce an
* [IllegalStateException].
*
*
* By now, if the [OCFile] passed is not downloaded, an [IllegalStateException] is generated on
* instantiation too.
*/
/**
* Creates an empty fragment for previews.
*
*
* MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically (for instance, when the
* device is turned a aside).
*
*
* DO NOT CALL IT: an [OCFile] and [User] must be provided for a successful construction
*/
@Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod", "TooManyFunctions")
class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
private var user: User? = null
private var savedPlaybackPosition: Long = 0
private var autoplay = true
private var isLivePhoto = false
private val prepared = false
private var mediaPlayerServiceConnection: PlayerServiceConnection? = null
private var videoUri: Uri? = null
@Inject
lateinit var clientFactory: ClientFactory
@Inject
lateinit var accountManager: UserAccountManager
@Inject
lateinit var backgroundJobManager: BackgroundJobManager
lateinit var binding: FragmentPreviewMediaBinding
private var emptyListView: ViewGroup? = null
private var exoPlayer: ExoPlayer? = null
private var nextcloudClient: NextcloudClient? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { bundle ->
file = bundle.getParcelableArgument(FILE, OCFile::class.java)
user = bundle.getParcelableArgument(USER, User::class.java)
savedPlaybackPosition = bundle.getLong(PLAYBACK_POSITION)
autoplay = bundle.getBoolean(AUTOPLAY)
isLivePhoto = bundle.getBoolean(IS_LIVE_PHOTO)
}
mediaPlayerServiceConnection = PlayerServiceConnection(requireContext())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
Log_OC.v(TAG, "onCreateView")
binding = FragmentPreviewMediaBinding.inflate(inflater, container, false)
emptyListView = binding.emptyView.emptyListView
setLoadingView()
return binding.root
}
private fun setLoadingView() {
binding.progress.visibility = View.VISIBLE
binding.emptyView.emptyListView.visibility = View.GONE
}
private fun setVideoErrorMessage(headline: String, @StringRes message: Int = R.string.stream_not_possible_message) {
binding.emptyView.run {
emptyListViewHeadline.text = headline
emptyListViewText.setText(message)
emptyListIcon.setImageResource(R.drawable.file_movie)
emptyListViewText.visibility = View.VISIBLE
emptyListIcon.visibility = View.VISIBLE
emptyListView.visibility = View.VISIBLE
}
binding.progress.visibility = View.GONE
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log_OC.v(TAG, "onActivityCreated")
var file = file
if (savedInstanceState == null) {
checkNotNull(file) { "Instanced with a NULL OCFile" }
checkNotNull(user) { "Instanced with a NULL ownCloud Account" }
} else {
file = savedInstanceState.getParcelableArgument(EXTRA_FILE, OCFile::class.java)
setFile(file)
user = savedInstanceState.getParcelableArgument(EXTRA_USER, User::class.java)
savedPlaybackPosition = savedInstanceState.getInt(EXTRA_PLAY_POSITION).toLong()
autoplay = savedInstanceState.getBoolean(EXTRA_PLAYING)
}
if (file != null) {
if (MimeTypeUtil.isVideo(file)) {
binding.exoplayerView.visibility = View.VISIBLE
binding.imagePreview.visibility = View.GONE
} else {
binding.exoplayerView.visibility = View.GONE
binding.imagePreview.visibility = View.VISIBLE
extractAndSetCoverArt(file)
}
}
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
addMenuHost()
}
/**
* tries to read the cover art from the audio file and sets it as cover art.
*
* @param file audio file with potential cover art
*/
@Suppress("TooGenericExceptionCaught")
private fun extractAndSetCoverArt(file: OCFile) {
if (!MimeTypeUtil.isAudio(file)) return
if (file.storagePath == null) {
setThumbnailForAudio(file)
} else {
try {
val mmr = MediaMetadataRetriever().apply {
setDataSource(file.storagePath)
}
val data = mmr.embeddedPicture
if (data != null) {
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
binding.imagePreview.setImageBitmap(bitmap) // associated cover art in bitmap
} else {
setThumbnailForAudio(file)
}
} catch (t: Throwable) {
setGenericThumbnail()
}
}
}
private fun setThumbnailForAudio(file: OCFile) {
val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
)
if (thumbnail != null) {
binding.imagePreview.setImageBitmap(thumbnail)
} else {
setGenericThumbnail()
}
}
/**
* Set generic icon (logo) as placeholder for thumbnail in preview.
*/
private fun setGenericThumbnail() {
AppCompatResources.getDrawable(requireContext(), R.drawable.logo)?.let { logo ->
if (!resources.getBoolean(R.bool.is_branded_client)) {
// only colour logo of non-branded client
DrawableCompat.setTint(logo, resources.getColor(R.color.primary, requireContext().theme))
}
binding.imagePreview.setImageDrawable(logo)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Log_OC.v(TAG, "onSaveInstanceState")
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
outState.run {
putParcelable(EXTRA_FILE, file)
putParcelable(EXTRA_USER, user)
if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
autoplay = exoPlayer?.isPlaying ?: false
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
putBoolean(EXTRA_PLAYING, autoplay)
} else if (mediaPlayerServiceConnection != null && mediaPlayerServiceConnection?.isConnected == true) {
putInt(EXTRA_PLAY_POSITION, mediaPlayerServiceConnection?.currentPosition ?: 0)
putBoolean(EXTRA_PLAYING, mediaPlayerServiceConnection?.isPlaying ?: false)
}
}
}
@Suppress("TooGenericExceptionCaught")
override fun onStart() {
super.onStart()
Log_OC.v(TAG, "onStart")
val context = if (context != null) {
requireContext()
} else {
MainApp.getAppContext()
}
val file = file
if (file != null) {
// bind to any existing player
mediaPlayerServiceConnection?.bind()
if (MimeTypeUtil.isAudio(file)) {
binding.mediaController.setMediaPlayer(mediaPlayerServiceConnection)
binding.mediaController.visibility = View.VISIBLE
mediaPlayerServiceConnection?.start(user!!, file, autoplay, savedPlaybackPosition)
binding.emptyView.emptyListView.visibility = View.GONE
binding.progress.visibility = View.GONE
} else if (MimeTypeUtil.isVideo(file)) {
if (mediaPlayerServiceConnection?.isConnected == true) {
// always stop player
stopAudio()
}
if (exoPlayer != null) {
playVideo()
} else {
val handler = Handler(Looper.getMainLooper())
Executors.newSingleThreadExecutor().execute {
try {
nextcloudClient = clientFactory.createNextcloudClient(accountManager.user)
handler.post {
exoPlayer = createNextcloudExoplayer(context, nextcloudClient!!)
exoPlayer?.addListener(
ExoplayerListener(
context,
binding.exoplayerView,
exoPlayer!!
) {
goBackToLivePhoto()
}
)
playVideo()
}
} catch (e: CreationException) {
handler.post { Log_OC.e(TAG, "error setting up ExoPlayer", e) }
}
}
}
}
}
}
private fun goBackToLivePhoto() {
if (!isLivePhoto) {
return
}
showActionBar()
requireActivity().supportFragmentManager.popBackStack()
}
private fun showActionBar() {
val currentActivity: Activity = requireActivity()
if (currentActivity is PreviewImageActivity) {
currentActivity.toggleActionBarVisibility(false)
}
}
@OptIn(UnstableApi::class)
private fun setupVideoView() {
binding.exoplayerView.run {
setShowNextButton(false)
setShowPreviousButton(false)
player = exoPlayer
setFullscreenButtonClickListener { startFullScreenVideo() }
}
}
private fun stopAudio() {
mediaPlayerServiceConnection?.stop()
}
private fun addMenuHost() {
val menuHost: MenuHost = requireActivity()
menuHost.addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menu.removeItem(R.id.action_search)
menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.custom_menu_placeholder_item -> {
if (containerActivity.storageManager == null || file == null) return false
val updatedFile = containerActivity.storageManager.getFileById(file.fileId)
file = updatedFile
file?.let { newFile ->
showFileActions(newFile)
}
true
}
else -> false
}
}
},
viewLifecycleOwner,
Lifecycle.State.RESUMED
)
}
private fun showFileActions(file: OCFile) {
val additionalFilter: MutableList<Int> = ArrayList(
listOf(
R.id.action_rename_file,
R.id.action_sync_file,
R.id.action_move_or_copy,
R.id.action_favorite,
R.id.action_unset_favorite,
R.id.action_pin_to_homescreen
)
)
if (getFile() != null && getFile().isSharedWithMe && !getFile().canReshare()) {
additionalFilter.add(R.id.action_send_share_file)
}
newInstance(file, false, additionalFilter)
.setResultListener(childFragmentManager, this) { itemId: Int -> this.onFileActionChosen(itemId) }
.show(childFragmentManager, "actions")
}
private fun onFileActionChosen(itemId: Int) {
when (itemId) {
R.id.action_send_share_file -> {
sendShareFile()
}
R.id.action_open_file_with -> {
openFile()
}
R.id.action_remove_file -> {
val dialog = RemoveFilesDialogFragment.newInstance(file)
dialog.show(requireFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION)
}
R.id.action_see_details -> {
seeDetails()
}
R.id.action_sync_file -> {
containerActivity.fileOperationsHelper.syncFile(file)
}
R.id.action_cancel_sync -> {
containerActivity.fileOperationsHelper.cancelTransference(file)
}
R.id.action_stream_media -> {
containerActivity.fileOperationsHelper.streamMediaFile(file)
}
R.id.action_export_file -> {
val list = ArrayList<OCFile>()
list.add(file)
containerActivity.fileOperationsHelper.exportFiles(
list,
context,
view,
backgroundJobManager
)
}
R.id.action_download_file -> {
instance().downloadFileIfNotStartedBefore(user!!, file)
}
}
}
/**
* Update the file of the fragment with file value
*
* @param file Replaces the held file with a new one
*/
fun updateFile(file: OCFile?) {
setFile(file)
}
private fun seeDetails() {
stopPreview(false)
containerActivity.showDetails(file)
}
private fun sendShareFile() {
stopPreview(false)
containerActivity.fileOperationsHelper.sendShareFile(file)
}
@Suppress("TooGenericExceptionCaught")
private fun playVideo() {
setupVideoView()
// load the video file in the video player
// when done, VideoHelper#onPrepared() will be called
if (file.isDown) {
playVideoUri(file.storageUri)
} else {
try {
LoadStreamUrl(this, user, clientFactory).execute(
file.localId
)
} catch (e: Exception) {
Log_OC.e(TAG, "Loading stream url not possible: $e")
}
}
}
private fun playVideoUri(uri: Uri) {
binding.progress.visibility = View.GONE
exoPlayer?.setMediaItem(MediaItem.fromUri(uri))
exoPlayer?.playWhenReady = autoplay
exoPlayer?.prepare()
if (savedPlaybackPosition >= 0) {
exoPlayer?.seekTo(savedPlaybackPosition)
}
// only autoplay video once
autoplay = false
}
@Suppress("DEPRECATION", "ReturnCount")
private class LoadStreamUrl(
previewMediaFragment: PreviewMediaFragment,
private val user: User?,
private val clientFactory: ClientFactory?
) : AsyncTask<Long?, Void?, Uri?>() {
private val previewMediaFragmentWeakReference = WeakReference(previewMediaFragment)
@Deprecated("Deprecated in Java")
override fun doInBackground(vararg fileId: Long?): Uri? {
val client: OwnCloudClient?
try {
client = clientFactory?.create(user)
} catch (e: CreationException) {
Log_OC.e(TAG, "Loading stream url not possible: $e")
return null
}
val sfo = fileId[0]?.let { StreamMediaFileOperation(it) }
val result = sfo?.execute(client)
if (result?.isSuccess == false) {
return null
}
return Uri.parse(result?.data?.get(0) as String)
}
@Deprecated("Deprecated in Java")
override fun onPostExecute(uri: Uri?) {
val previewMediaFragment = previewMediaFragmentWeakReference.get()
val context = previewMediaFragment?.context
if (previewMediaFragment?.binding == null || context == null) {
Log_OC.e(TAG, "Error streaming file: no previewMediaFragment!")
return
}
previewMediaFragment.run {
if (uri != null) {
videoUri = uri
playVideoUri(uri)
} else {
emptyListView?.visibility = View.VISIBLE
setVideoErrorMessage(getString(R.string.stream_not_possible_headline))
}
}
}
}
override fun onStop() {
Log_OC.v(TAG, "onStop")
val file = file
if (MimeTypeUtil.isAudio(file) && mediaPlayerServiceConnection?.isPlaying == false) {
stopAudio()
} else if (MimeTypeUtil.isVideo(file) && exoPlayer != null && exoPlayer?.isPlaying == true) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
exoPlayer?.pause()
}
mediaPlayerServiceConnection?.unbind()
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_UNLOCKED)
super.onStop()
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN && v == binding.exoplayerView) {
// added a margin on the left to avoid interfering with gesture to open navigation drawer
if (event.x / Resources.getSystem().displayMetrics.density > MIN_DENSITY_RATIO) {
startFullScreenVideo()
}
return true
}
return false
}
private fun startFullScreenVideo() {
activity?.let { activity ->
nextcloudClient?.let { client ->
exoPlayer?.let { player ->
PreviewVideoFullscreenDialog(activity, client, player, binding.exoplayerView).show()
}
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Log_OC.v(TAG, "onConfigurationChanged $this")
}
@Suppress("DEPRECATION")
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log_OC.v(TAG, "onActivityResult $this")
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
savedPlaybackPosition = data?.getLongExtra(EXTRA_START_POSITION, 0) ?: 0L
autoplay = data?.getBooleanExtra(EXTRA_AUTOPLAY, false) ?: false
}
}
/**
* Opens the previewed file with an external application.
*/
private fun openFile() {
stopPreview(true)
containerActivity.fileOperationsHelper.openFile(file)
}
private fun stopPreview(stopAudio: Boolean) {
if (stopAudio && mediaPlayerServiceConnection != null) {
mediaPlayerServiceConnection?.stop()
} else if (exoPlayer != null) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
exoPlayer?.stop()
}
}
val position: Long
get() {
if (prepared) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
}
Log_OC.v(TAG, "getting position: $savedPlaybackPosition")
return savedPlaybackPosition
}
private fun toggleDrawerLockMode(containerActivity: ContainerActivity, lockMode: Int) {
(containerActivity as DrawerActivity).setDrawerLockMode(lockMode)
}
override fun onDetach() {
exoPlayer?.let {
it.stop()
it.release()
}
super.onDetach()
}
companion object {
private val TAG: String = PreviewMediaFragment::class.java.simpleName
const val EXTRA_FILE: String = "FILE"
const val EXTRA_USER: String = "USER"
const val EXTRA_AUTOPLAY: String = "AUTOPLAY"
const val EXTRA_START_POSITION: String = "START_POSITION"
private const val EXTRA_PLAY_POSITION = "PLAY_POSITION"
private const val EXTRA_PLAYING = "PLAYING"
private const val MIN_DENSITY_RATIO = 24.0
private const val FILE = "FILE"
private const val USER = "USER"
private const val PLAYBACK_POSITION = "PLAYBACK_POSITION"
private const val AUTOPLAY = "AUTOPLAY"
private const val IS_LIVE_PHOTO = "IS_LIVE_PHOTO"
/**
* Creates a fragment to preview a file.
*
*
* When 'fileToDetail' or 'user' are null
*
* @param fileToDetail An [OCFile] to preview in the fragment
* @param user Currently active user
*/
@JvmStatic
fun newInstance(
fileToDetail: OCFile?,
user: User?,
startPlaybackPosition: Long,
autoplay: Boolean,
isLivePhoto: Boolean
): PreviewMediaFragment {
val previewMediaFragment = PreviewMediaFragment()
val bundle = Bundle().apply {
putParcelable(FILE, fileToDetail)
putParcelable(USER, user)
putLong(PLAYBACK_POSITION, startPlaybackPosition)
putBoolean(AUTOPLAY, autoplay)
putBoolean(IS_LIVE_PHOTO, isLivePhoto)
}
previewMediaFragment.arguments = bundle
return previewMediaFragment
}
/**
* Helper method to test if an [OCFile] can be passed to a [PreviewMediaFragment] to be previewed.
*
* @param file File to test if can be previewed.
* @return 'True' if the file can be handled by the fragment.
*/
@JvmStatic
fun canBePreviewed(file: OCFile?): Boolean {
return file != null && (MimeTypeUtil.isAudio(file) || MimeTypeUtil.isVideo(file))
}
}
}

View file

@ -633,6 +633,7 @@
<string name="prefs_instant_behaviour_dialogTitle">O ficheiro orixinal vai ser…</string> <string name="prefs_instant_behaviour_dialogTitle">O ficheiro orixinal vai ser…</string>
<string name="prefs_instant_behaviour_title">O ficheiro orixinal vai ser…</string> <string name="prefs_instant_behaviour_title">O ficheiro orixinal vai ser…</string>
<string name="prefs_instant_upload_exclude_hidden_summary">Excluír ficheiros e cartafoles agochados</string> <string name="prefs_instant_upload_exclude_hidden_summary">Excluír ficheiros e cartafoles agochados</string>
<string name="prefs_instant_upload_exclude_hidden_title">Excluír o agochado</string>
<string name="prefs_instant_upload_path_use_date_subfolders_summary">Arquivar en subcartafoles baseados na data</string> <string name="prefs_instant_upload_path_use_date_subfolders_summary">Arquivar en subcartafoles baseados na data</string>
<string name="prefs_instant_upload_path_use_subfolders_title">Usar subcartafoles</string> <string name="prefs_instant_upload_path_use_subfolders_title">Usar subcartafoles</string>
<string name="prefs_instant_upload_subfolder_rule_title">Opcións de subcartafol</string> <string name="prefs_instant_upload_subfolder_rule_title">Opcións de subcartafol</string>
@ -664,6 +665,7 @@
<string name="preview_image_description">Vista previa da imaxe</string> <string name="preview_image_description">Vista previa da imaxe</string>
<string name="preview_image_error_no_local_file">Non hai ficheiro local que ver</string> <string name="preview_image_error_no_local_file">Non hai ficheiro local que ver</string>
<string name="preview_image_error_unknown_format">Non é posíbel amosar a imaxe</string> <string name="preview_image_error_unknown_format">Non é posíbel amosar a imaxe</string>
<string name="preview_media_unhandled_http_code_message">O ficheiro está bloqueado actualmente por outro usuario ou proceso e, polo tanto, non é posíbel eliminalo. Ténteo de novo máis tarde.</string>
<string name="preview_sorry">Desculpe.</string> <string name="preview_sorry">Desculpe.</string>
<string name="privacy">Privacidade</string> <string name="privacy">Privacidade</string>
<string name="public_share_name">Nome novo</string> <string name="public_share_name">Nome novo</string>
@ -671,6 +673,7 @@
<string name="push_notifications_old_login">Non dispón de notificacións automáticas por mor dun acceso á sesión caducado. Considere volver engadir a súa conta.</string> <string name="push_notifications_old_login">Non dispón de notificacións automáticas por mor dun acceso á sesión caducado. Considere volver engadir a súa conta.</string>
<string name="push_notifications_temp_error">Actualmente non están dispoñíbeis as notificacións automáticas.</string> <string name="push_notifications_temp_error">Actualmente non están dispoñíbeis as notificacións automáticas.</string>
<string name="qr_could_not_be_read">Non foi posíbel ler o código QR.</string> <string name="qr_could_not_be_read">Non foi posíbel ler o código QR.</string>
<string name="receive_external_files_activity_start_sync_folder_is_not_exists_message">Non é posíbel atopar o cartafol, a operación de sincronización foi cancelada</string>
<string name="recommend_subject">Probe %1$s no seu dispositivo!</string> <string name="recommend_subject">Probe %1$s no seu dispositivo!</string>
<string name="recommend_text">Quixera convidalo a usar %1$s no seu dispositivo.\nDescargueo aquí: %2$s</string> <string name="recommend_text">Quixera convidalo a usar %1$s no seu dispositivo.\nDescargueo aquí: %2$s</string>
<string name="recommend_urls">%1$s ou %2$s</string> <string name="recommend_urls">%1$s ou %2$s</string>
@ -712,8 +715,12 @@
<string name="screenshot_04_accounts_heading">Todas as súas contas</string> <string name="screenshot_04_accounts_heading">Todas as súas contas</string>
<string name="screenshot_04_accounts_subline">nun só lugar</string> <string name="screenshot_04_accounts_subline">nun só lugar</string>
<string name="screenshot_05_autoUpload_heading">Envío automático</string> <string name="screenshot_05_autoUpload_heading">Envío automático</string>
<string name="screenshot_05_autoUpload_subline">para as súas fotos e vídeos</string>
<string name="screenshot_06_davdroid_heading">Calendario e contactos</string>
<string name="screenshot_06_davdroid_subline">Sincronizar con DAVx5</string> <string name="screenshot_06_davdroid_subline">Sincronizar con DAVx5</string>
<string name="search_error">Produciuse un erro ao obter os resultados da busca</string> <string name="search_error">Produciuse un erro ao obter os resultados da busca</string>
<string name="secure_share_not_set_up">A compartición segura non está definida para este usuario</string>
<string name="secure_share_search">Compartición segura…</string>
<string name="select_all">Seleccionar todo</string> <string name="select_all">Seleccionar todo</string>
<string name="select_media_folder">Estabelecer o cartafol multimedia</string> <string name="select_media_folder">Estabelecer o cartafol multimedia</string>
<string name="select_one_template">Seleccione un modelo</string> <string name="select_one_template">Seleccione un modelo</string>
@ -728,6 +735,7 @@
<string name="set_status_message">Estabelecer a mensaxe de estado</string> <string name="set_status_message">Estabelecer a mensaxe de estado</string>
<string name="setup_e2e">Durante a configuración do cifrado de extremo a extremo, recibirá un mnemotécnico ao chou de 12 palabras, que necesitará para abrir os seus ficheiros noutros dispositivos. Isto só se almacenará neste dispositivo e pódese amosar de novo nesta pantalla. Anóteo nun lugar seguro!</string> <string name="setup_e2e">Durante a configuración do cifrado de extremo a extremo, recibirá un mnemotécnico ao chou de 12 palabras, que necesitará para abrir os seus ficheiros noutros dispositivos. Isto só se almacenará neste dispositivo e pódese amosar de novo nesta pantalla. Anóteo nun lugar seguro!</string>
<string name="share">Compartir</string> <string name="share">Compartir</string>
<string name="share_copy_link">Compartir e copiar a ligazón</string>
<string name="share_dialog_title">Compartindo</string> <string name="share_dialog_title">Compartindo</string>
<string name="share_expiration_date_format">%1$s</string> <string name="share_expiration_date_format">%1$s</string>
<string name="share_expiration_date_label">Caduca o %1$s</string> <string name="share_expiration_date_label">Caduca o %1$s</string>
@ -747,6 +755,7 @@
<string name="share_link_with_label">Compartir ligazón (%1$s)</string> <string name="share_link_with_label">Compartir ligazón (%1$s)</string>
<string name="share_no_expiration_date_label">Estabelecer a data de caducidade</string> <string name="share_no_expiration_date_label">Estabelecer a data de caducidade</string>
<string name="share_no_password_title">Estabelecer o contrasinal</string> <string name="share_no_password_title">Estabelecer o contrasinal</string>
<string name="share_not_allowed_when_file_drop">Non se permite volver compartir durante a entrega segura de ficheiros</string>
<string name="share_password_title">Protexido con contrasinal</string> <string name="share_password_title">Protexido con contrasinal</string>
<string name="share_permission_can_edit">Pode editar</string> <string name="share_permission_can_edit">Pode editar</string>
<string name="share_permission_file_drop">Soltar o ficheiro</string> <string name="share_permission_file_drop">Soltar o ficheiro</string>
@ -770,6 +779,7 @@
<string name="shared_icon_shared_via_link">compartido mediante ligazón</string> <string name="shared_icon_shared_via_link">compartido mediante ligazón</string>
<string name="shared_with_you_by">Compartido con Vde. por %1$s</string> <string name="shared_with_you_by">Compartido con Vde. por %1$s</string>
<string name="sharee_add_failed">Produciuse un fallo ao engadir unha compartición</string> <string name="sharee_add_failed">Produciuse un fallo ao engadir unha compartición</string>
<string name="sharee_already_added_to_file">Produciuse un erro ao engadir o contido compartido. Este ficheiro ou cartafol xa foi compartido con esta persoa ou grupo.</string>
<string name="show_images">Amosar as fotos</string> <string name="show_images">Amosar as fotos</string>
<string name="show_video">Amosar os vídeos</string> <string name="show_video">Amosar os vídeos</string>
<string name="signup_with_provider">Rexistrarse cun provedor</string> <string name="signup_with_provider">Rexistrarse cun provedor</string>
@ -826,6 +836,7 @@
<string name="subject_shared_with_you">«%1$s» foi compartido con Vde.</string> <string name="subject_shared_with_you">«%1$s» foi compartido con Vde.</string>
<string name="subject_user_shared_with_you">%1$s compartiu «%2$s» con Vde.</string> <string name="subject_user_shared_with_you">%1$s compartiu «%2$s» con Vde.</string>
<string name="subtitle_photos_only">Só fotos</string> <string name="subtitle_photos_only">Só fotos</string>
<string name="subtitle_photos_videos">Fotos e vídeos</string>
<string name="subtitle_videos_only">Só vídeos</string> <string name="subtitle_videos_only">Só vídeos</string>
<string name="suggest">Suxerir</string> <string name="suggest">Suxerir</string>
<string name="sync_conflicts_in_favourites_ticker">Atopáronse conflitos</string> <string name="sync_conflicts_in_favourites_ticker">Atopáronse conflitos</string>
@ -883,8 +894,12 @@
<string name="update_link_file_no_exist">Non é posíbel actualizar. Comprobe que o ficheiro existe.</string> <string name="update_link_file_no_exist">Non é posíbel actualizar. Comprobe que o ficheiro existe.</string>
<string name="update_link_forbidden_permissions">para actualizar esta compartición</string> <string name="update_link_forbidden_permissions">para actualizar esta compartición</string>
<string name="updating_share_failed">Produciuse un fallo ao actualizar elementos compartidos</string> <string name="updating_share_failed">Produciuse un fallo ao actualizar elementos compartidos</string>
<string name="upload_action_cancelled_clear">Limpar os envíos cancelados</string>
<string name="upload_action_cancelled_resume">Retomar os envíos cancelados</string>
<string name="upload_action_failed_clear">Limpar os envíos fallados</string> <string name="upload_action_failed_clear">Limpar os envíos fallados</string>
<string name="upload_action_failed_retry">Tentar de novo os envíos fallados</string> <string name="upload_action_failed_retry">Tentar de novo os envíos fallados</string>
<string name="upload_action_global_upload_pause">Pausar todos os envíos</string>
<string name="upload_action_global_upload_resume">Retomar todos os envíos</string>
<string name="upload_cannot_create_file">Non é posíbel crear o ficheiro local</string> <string name="upload_cannot_create_file">Non é posíbel crear o ficheiro local</string>
<string name="upload_chooser_title">Enviar dende…</string> <string name="upload_chooser_title">Enviar dende…</string>
<string name="upload_content_from_other_apps">Enviar contido dende outras aplicacións</string> <string name="upload_content_from_other_apps">Enviar contido dende outras aplicacións</string>
@ -896,6 +911,7 @@
<string name="upload_file_dialog_filetype_snippet_text">Ficheiro de fragmento de texto(.txt)</string> <string name="upload_file_dialog_filetype_snippet_text">Ficheiro de fragmento de texto(.txt)</string>
<string name="upload_file_dialog_title">Introduza o nome e o tipo do ficheiro a enviar</string> <string name="upload_file_dialog_title">Introduza o nome e o tipo do ficheiro a enviar</string>
<string name="upload_files">Enviar ficheiros</string> <string name="upload_files">Enviar ficheiros</string>
<string name="upload_global_pause_title">Todos os envíos están en pausa</string>
<string name="upload_item_action_button">Botón da acción do envío de elemento</string> <string name="upload_item_action_button">Botón da acción do envío de elemento</string>
<string name="upload_list_delete">Eliminar</string> <string name="upload_list_delete">Eliminar</string>
<string name="upload_list_empty_headline">Non hai envíos dispoñíbeis</string> <string name="upload_list_empty_headline">Non hai envíos dispoñíbeis</string>
@ -905,6 +921,7 @@
<string name="upload_local_storage_full">O almacenamento local está cheo</string> <string name="upload_local_storage_full">O almacenamento local está cheo</string>
<string name="upload_local_storage_not_copied">Non foi posíbel copiar o ficheiro no almacenamento local</string> <string name="upload_local_storage_not_copied">Non foi posíbel copiar o ficheiro no almacenamento local</string>
<string name="upload_lock_failed">Produciuse un fallo ao bloquear o cartafol</string> <string name="upload_lock_failed">Produciuse un fallo ao bloquear o cartafol</string>
<string name="upload_notification_manager_start_text">%1$d / %2$d - %3$s</string>
<string name="upload_old_android">O cifrado só é posíbel con &gt;= Android 5.0</string> <string name="upload_old_android">O cifrado só é posíbel con &gt;= Android 5.0</string>
<string name="upload_query_move_foreign_files">Non hai espazo abondo para copiar os ficheiros seleccionados no cartafol %1$s. No canto diso, gustaríalle movelos?</string> <string name="upload_query_move_foreign_files">Non hai espazo abondo para copiar os ficheiros seleccionados no cartafol %1$s. No canto diso, gustaríalle movelos?</string>
<string name="upload_quota_exceeded">Superouse a cota de almacenamento</string> <string name="upload_quota_exceeded">Superouse a cota de almacenamento</string>

View file

@ -45,6 +45,6 @@ android {
} }
dependencies { dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVersion" implementation "androidx.appcompat:appcompat:1.6.1"
implementation "com.github.zynkware:Document-Scanning-Android-SDK:$documentScannerVersion" implementation "com.github.zynkware:Document-Scanning-Android-SDK:$documentScannerVersion"
} }

View file

@ -14,7 +14,7 @@ buildscript {
androidPluginVersion = '8.4.0' androidPluginVersion = '8.4.0'
androidxMediaVersion = '1.3.1' androidxMediaVersion = '1.3.1'
androidxTestVersion = "1.5.0" androidxTestVersion = "1.5.0"
appCompatVersion = '1.6.1' appCompatVersion = '1.7.0'
checkerVersion = "3.21.2" checkerVersion = "3.21.2"
daggerVersion = "2.51.1" daggerVersion = "2.51.1"
documentScannerVersion = "1.1.1" documentScannerVersion = "1.1.1"

View file

@ -239,6 +239,8 @@
<trusted-key id="A5BD02B93E7A40482EB1D66A5F69AD087600B22C" group="org.ow2.asm"/> <trusted-key id="A5BD02B93E7A40482EB1D66A5F69AD087600B22C" group="org.ow2.asm"/>
<trusted-key id="A5F483CD733A4EBAEA378B2AE88979FB9B30ACF2"> <trusted-key id="A5F483CD733A4EBAEA378B2AE88979FB9B30ACF2">
<trusting group="androidx.annotation"/> <trusting group="androidx.annotation"/>
<trusting group="androidx.appcompat"/>
<trusting group="androidx.core"/>
<trusting group="androidx.fragment"/> <trusting group="androidx.fragment"/>
<trusting group="androidx.lifecycle"/> <trusting group="androidx.lifecycle"/>
<trusting group="androidx.webkit" name="webkit" version="1.11.0"/> <trusting group="androidx.webkit" name="webkit" version="1.11.0"/>
@ -1067,6 +1069,11 @@
<sha256 value="2f63fbeda23ca0919738d09e406de661f21bac583d6e04a1797dcb77e3b6ae95" origin="Generated by Gradle"/> <sha256 value="2f63fbeda23ca0919738d09e406de661f21bac583d6e04a1797dcb77e3b6ae95" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="androidx.core" name="core" version="1.13.0">
<artifact name="core-1.13.0.aar">
<sha256 value="1b96c8eb10c4b40283fdd6e9aa74ffff05fae4f15d54f61ba69d517fcd144695" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.2.0"> <component group="androidx.core" name="core" version="1.2.0">
<artifact name="core-1.2.0.aar"> <artifact name="core-1.2.0.aar">
<sha256 value="524b8b88ceb6a74a7e44e6b567a135660f211799904cb218bfee5be1166820b2" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="524b8b88ceb6a74a7e44e6b567a135660f211799904cb218bfee5be1166820b2" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -1601,6 +1608,11 @@
<sha256 value="9951cb91d43d916e1aa298682121c42d49c1f0280061d002f1f36ff6cb5318ee" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="9951cb91d43d916e1aa298682121c42d49c1f0280061d002f1f36ff6cb5318ee" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact> </artifact>
</component> </component>
<component group="androidx.fragment" name="fragment" version="1.5.4">
<artifact name="fragment-1.5.4.module">
<sha256 value="af3260808dceb6532efc2d7215be45872c24a699dada7d77bff738ce3b85a7f0" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.fragment" name="fragment" version="1.6.0"> <component group="androidx.fragment" name="fragment" version="1.6.0">
<artifact name="fragment-1.6.0.aar"> <artifact name="fragment-1.6.0.aar">
<sha256 value="eaad568874cea7e1738beebf3298670a8743e25bb4934546764bee62f6d27f26" origin="Generated by Gradle"/> <sha256 value="eaad568874cea7e1738beebf3298670a8743e25bb4934546764bee62f6d27f26" origin="Generated by Gradle"/>

View file

@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE DO NOT TOUCH; GENERATED BY DRONE
<span class="mdl-layout-title">Lint Report: 3 errors and 69 warnings</span> <span class="mdl-layout-title">Lint Report: 3 errors and 68 warnings</span>