mirror of
https://github.com/nextcloud/android.git
synced 2024-12-19 15:33:00 +03:00
Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
commit
35b4f6c4b7
8 changed files with 753 additions and 711 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 >= Android 5.0</string>
|
<string name="upload_old_android">O cifrado só é posíbel con >= 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>
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue