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

This commit is contained in:
Tobias Kaminsky 2024-06-29 02:31:17 +02:00
commit 09d71a7340
15 changed files with 1063 additions and 1017 deletions

View file

@ -229,7 +229,7 @@ public abstract class FileActivity extends DrawerActivity
}
public void checkInternetConnection() {
if (connectivityService.isConnected()) {
if (connectivityService != null && connectivityService.isConnected()) {
hideInfoBox();
}
}
@ -790,11 +790,14 @@ public abstract class FileActivity extends DrawerActivity
String link = "";
OCFile file = null;
for (Object object : result.getData()) {
OCShare shareLink = (OCShare) object;
if (TAG_PUBLIC_LINK.equalsIgnoreCase(shareLink.getShareType().name())) {
link = shareLink.getShareLink();
file = getStorageManager().getFileByPath(shareLink.getPath());
break;
if (object instanceof OCShare shareLink) {
ShareType shareType = shareLink.getShareType();
if (shareType != null && TAG_PUBLIC_LINK.equalsIgnoreCase(shareType.name())) {
link = shareLink.getShareLink();
file = getStorageManager().getFileByEncryptedRemotePath(shareLink.getPath());
break;
}
}
}
@ -804,8 +807,12 @@ public abstract class FileActivity extends DrawerActivity
sharingFragment.onUpdateShareInformation(result, file);
}
if (fileListFragment instanceof OCFileListFragment && file != null) {
((OCFileListFragment) fileListFragment).updateOCFile(file);
if (fileListFragment instanceof OCFileListFragment ocFileListFragment && file != null) {
if (ocFileListFragment.getAdapterFiles().contains(file)) {
ocFileListFragment.updateOCFile(file);
} else {
DisplayUtils.showSnackMessage(this, R.string.file_activity_shared_file_cannot_be_updated);
}
}
} else {
// Detect Failure (403) --> maybe needs password

View file

@ -987,131 +987,124 @@ public class OCFileListFragment extends ExtendedListFragment implements
return true;
}
private void folderOnItemClick(OCFile file, int position) {
if (file.isEncrypted()) {
User user = ((FileActivity) mContainerActivity).getUser().orElseThrow(RuntimeException::new);
// check if e2e app is enabled
OCCapability ocCapability = mContainerActivity.getStorageManager()
.getCapability(user.getAccountName());
if (ocCapability.getEndToEndEncryption().isFalse() ||
ocCapability.getEndToEndEncryption().isUnknown()) {
Snackbar.make(getRecyclerView(), R.string.end_to_end_encryption_not_enabled,
Snackbar.LENGTH_LONG).show();
return;
}
// check if keys are stored
if (FileOperationsHelper.isEndToEndEncryptionSetup(requireContext(), user)) {
// update state and view of this fragment
searchFragment = false;
mHideFab = false;
if (mContainerActivity instanceof FolderPickerActivity &&
((FolderPickerActivity) mContainerActivity)
.isDoNotEnterEncryptedFolder()) {
Snackbar.make(getRecyclerView(),
R.string.copy_move_to_encrypted_folder_not_supported,
Snackbar.LENGTH_LONG).show();
} else {
browseToFolder(file, position);
}
} else {
Log_OC.d(TAG, "no public key for " + user.getAccountName());
FragmentManager fragmentManager = getParentFragmentManager();
if (fragmentManager.findFragmentByTag(SETUP_ENCRYPTION_DIALOG_TAG) == null) {
SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position);
dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
dialog.show(fragmentManager, SETUP_ENCRYPTION_DIALOG_TAG);
}
}
} else {
// update state and view of this fragment
searchFragment = false;
setEmptyListLoadingMessage();
browseToFolder(file, position);
}
}
private void fileOnItemClick(OCFile file) {
if (PreviewImageFragment.canBePreviewed(file)) {
// preview image - it handles the download, if needed
if (searchFragment) {
VirtualFolderType type = switch (currentSearchType) {
case FAVORITE_SEARCH -> VirtualFolderType.FAVORITE;
case GALLERY_SEARCH -> VirtualFolderType.GALLERY;
default -> VirtualFolderType.NONE;
};
((FileDisplayActivity) mContainerActivity).startImagePreview(file, type, !file.isDown());
} else {
((FileDisplayActivity) mContainerActivity).startImagePreview(file, !file.isDown());
}
} else if (file.isDown() && MimeTypeUtil.isVCard(file)) {
((FileDisplayActivity) mContainerActivity).startContactListFragment(file);
} else if (file.isDown() && MimeTypeUtil.isPDF(file)) {
((FileDisplayActivity) mContainerActivity).startPdfPreview(file);
} else if (PreviewTextFileFragment.canBePreviewed(file)) {
setFabVisible(false);
((FileDisplayActivity) mContainerActivity).startTextPreview(file, false);
} else if (file.isDown()) {
if (PreviewMediaActivity.Companion.canBePreviewed(file)) {
setFabVisible(false);
((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, false, true);
} else {
mContainerActivity.getFileOperationsHelper().openFile(file);
}
} else {
User account = accountManager.getUser();
OCCapability capability = mContainerActivity.getStorageManager().getCapability(account.getAccountName());
if (PreviewMediaActivity.Companion.canBePreviewed(file) && !file.isEncrypted()) {
setFabVisible(false);
((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true, true);
} else if (editorUtils.isEditorAvailable(accountManager.getUser(), file.getMimeType()) && !file.isEncrypted()) {
mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(file, getContext());
} else if (capability.getRichDocumentsMimeTypeList().contains(file.getMimeType()) &&
capability.getRichDocumentsDirectEditing().isTrue() && !file.isEncrypted()) {
mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(file, getContext());
} else if (mContainerActivity instanceof FileDisplayActivity fileDisplayActivity) {
fileDisplayActivity.startDownloadForPreview(file, mFile);
}
}
}
@Override
@OptIn(markerClass = UnstableApi.class)
public void onItemClicked(OCFile file) {
((FileActivity) mContainerActivity).checkInternetConnection();
if (mContainerActivity != null && mContainerActivity instanceof FileActivity fileActivity) {
fileActivity.checkInternetConnection();
}
if (getCommonAdapter().isMultiSelect()) {
toggleItemToCheckedList(file);
} else {
if (file != null) {
int position = getCommonAdapter().getItemPosition(file);
if (file.isFolder()) {
if (file.isEncrypted()) {
User user = ((FileActivity) mContainerActivity).getUser().orElseThrow(RuntimeException::new);
// check if e2e app is enabled
OCCapability ocCapability = mContainerActivity.getStorageManager()
.getCapability(user.getAccountName());
if (ocCapability.getEndToEndEncryption().isFalse() ||
ocCapability.getEndToEndEncryption().isUnknown()) {
Snackbar.make(getRecyclerView(), R.string.end_to_end_encryption_not_enabled,
Snackbar.LENGTH_LONG).show();
return;
}
// check if keys are stored
if (FileOperationsHelper.isEndToEndEncryptionSetup(requireContext(), user)) {
// update state and view of this fragment
searchFragment = false;
mHideFab = false;
if (mContainerActivity instanceof FolderPickerActivity &&
((FolderPickerActivity) mContainerActivity)
.isDoNotEnterEncryptedFolder()) {
Snackbar.make(getRecyclerView(),
R.string.copy_move_to_encrypted_folder_not_supported,
Snackbar.LENGTH_LONG).show();
} else {
browseToFolder(file, position);
}
} else {
Log_OC.d(TAG, "no public key for " + user.getAccountName());
FragmentManager fragmentManager = getParentFragmentManager();
if (fragmentManager != null &&
fragmentManager.findFragmentByTag(SETUP_ENCRYPTION_DIALOG_TAG) == null) {
SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user,
position);
dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
dialog.show(fragmentManager, SETUP_ENCRYPTION_DIALOG_TAG);
}
}
} else {
// update state and view of this fragment
searchFragment = false;
setEmptyListLoadingMessage();
browseToFolder(file, position);
}
} else if (mFileSelectable) {
Intent intent = new Intent();
intent.putExtra(FolderPickerActivity.EXTRA_FILES, file);
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();
} else if (!mOnlyFoldersClickable) {
// Click on a file
if (PreviewImageFragment.canBePreviewed(file)) {
// preview image - it handles the download, if needed
if (searchFragment) {
VirtualFolderType type;
switch (currentSearchType) {
case FAVORITE_SEARCH:
type = VirtualFolderType.FAVORITE;
break;
case GALLERY_SEARCH:
type = VirtualFolderType.GALLERY;
break;
default:
type = VirtualFolderType.NONE;
break;
}
((FileDisplayActivity) mContainerActivity).startImagePreview(file, type, !file.isDown());
} else {
((FileDisplayActivity) mContainerActivity).startImagePreview(file, !file.isDown());
}
} else if (file.isDown() && MimeTypeUtil.isVCard(file)) {
((FileDisplayActivity) mContainerActivity).startContactListFragment(file);
} else if (file.isDown() && MimeTypeUtil.isPDF(file)) {
((FileDisplayActivity) mContainerActivity).startPdfPreview(file);
} else if (PreviewTextFileFragment.canBePreviewed(file)) {
setFabVisible(false);
((FileDisplayActivity) mContainerActivity).startTextPreview(file, false);
} else if (file.isDown()) {
if (PreviewMediaActivity.Companion.canBePreviewed(file)) {
// media preview
setFabVisible(false);
((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, false, true);
} else {
mContainerActivity.getFileOperationsHelper().openFile(file);
}
} else {
// file not downloaded, check for streaming, remote editing
User account = accountManager.getUser();
OCCapability capability = mContainerActivity.getStorageManager()
.getCapability(account.getAccountName());
if (PreviewMediaActivity.Companion.canBePreviewed(file) && !file.isEncrypted()) {
// stream media preview on >= NC14
setFabVisible(false);
((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true, true);
} else if (editorUtils.isEditorAvailable(accountManager.getUser(),
file.getMimeType()) &&
!file.isEncrypted()) {
mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(file, getContext());
} else if (capability.getRichDocumentsMimeTypeList().contains(file.getMimeType()) &&
capability.getRichDocumentsDirectEditing().isTrue() && !file.isEncrypted()) {
mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(file, getContext());
} else {
// automatic download, preview on finish
((FileDisplayActivity) mContainerActivity).startDownloadForPreview(file, mFile);
}
}
}
} else {
if (file == null) {
Log_OC.d(TAG, "Null object in ListAdapter!");
return;
}
int position = getCommonAdapter().getItemPosition(file);
if (file.isFolder()) {
folderOnItemClick(file, position);
} else if (mFileSelectable) {
Intent intent = new Intent();
intent.putExtra(FolderPickerActivity.EXTRA_FILES, file);
requireActivity().setResult(Activity.RESULT_OK, intent);
requireActivity().finish();
} else if (!mOnlyFoldersClickable) {
fileOnItemClick(file);
}
}
}
@ -1387,9 +1380,19 @@ public class OCFileListFragment extends ExtendedListFragment implements
}
}
public void updateOCFile(OCFile file) {
public List<OCFile> getAdapterFiles() {
return mAdapter.getFiles();
}
public void updateOCFile(@NonNull OCFile file) {
List<OCFile> mFiles = mAdapter.getFiles();
mFiles.set(mFiles.indexOf(file), file);
int index = mFiles.indexOf(file);
if (index == -1) {
Log_OC.d(TAG, "File cannot be found in adapter's files");
return;
}
mFiles.set(index, file);
mAdapter.notifyItemChanged(file);
}

View file

@ -1,40 +0,0 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.owncloud.android.ui.preview;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.owncloud.android.R;
import com.owncloud.android.ui.fragment.FileFragment;
import androidx.annotation.Nullable;
import static com.owncloud.android.ui.activity.FileActivity.EXTRA_FILE;
/**
* A fragment showing an error message.
*/
public class PreviewImageErrorFragment extends FileFragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.preview_image_error_fragment, container, false);
}
public static FileFragment newInstance() {
FileFragment fileFragment = new PreviewImageErrorFragment();
Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_FILE, null);
fileFragment.setArguments(bundle);
return fileFragment;
}
}

View file

@ -0,0 +1,36 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.owncloud.android.ui.preview
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.owncloud.android.R
import com.owncloud.android.ui.activity.FileActivity
import com.owncloud.android.ui.fragment.FileFragment
/**
* A fragment showing an error message.
*/
class PreviewImageErrorFragment : FileFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.preview_image_error_fragment, container, false)
}
companion object {
fun newInstance(): FileFragment {
val fileFragment: FileFragment = PreviewImageErrorFragment()
val bundle = Bundle()
bundle.putParcelable(FileActivity.EXTRA_FILE, null)
fileFragment.arguments = bundle
return fileFragment
}
}
}

View file

@ -1,847 +0,0 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020-2024 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2017-2020 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 2013-2015 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.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.PictureDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Process;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import com.github.chrisbanes.photoview.PhotoView;
import com.google.android.material.snackbar.Snackbar;
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.network.ConnectivityService;
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
import com.nextcloud.utils.extensions.BundleExtensionsKt;
import com.nextcloud.utils.extensions.ExtensionsKt;
import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.databinding.PreviewImageFragmentBinding;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.lib.common.utils.Log_OC;
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.BitmapUtils;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.MimeType;
import com.owncloud.android.utils.MimeTypeUtil;
import com.owncloud.android.utils.theme.ViewThemeUtils;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import pl.droidsonroids.gif.GifDrawable;
import static com.owncloud.android.datamodel.ThumbnailsCacheManager.PREFIX_THUMBNAIL;
/**
* This fragment shows a preview of a downloaded image.
* Trying to get an instance with a NULL {@link OCFile} will produce an {@link IllegalStateException}.
* If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too.
*/
public class PreviewImageFragment extends FileFragment implements Injectable {
private static final String EXTRA_FILE = "FILE";
private static final String EXTRA_ZOOM = "ZOOM";
private static final String ARG_FILE = "FILE";
private static final String ARG_IGNORE_FIRST = "IGNORE_FIRST";
private static final String ARG_SHOW_RESIZED_IMAGE = "SHOW_RESIZED_IMAGE";
private static final String MIME_TYPE_PNG = "image/png";
private static final String MIME_TYPE_GIF = "image/gif";
private static final String MIME_TYPE_SVG = "image/svg+xml";
private Boolean showResizedImage;
private Bitmap bitmap;
private static final String TAG = PreviewImageFragment.class.getSimpleName();
private boolean ignoreFirstSavedState;
private LoadBitmapTask loadBitmapTask;
@Inject ConnectivityService connectivityService;
@Inject UserAccountManager accountManager;
@Inject BackgroundJobManager backgroundJobManager;
@Inject ViewThemeUtils viewThemeUtils;
private PreviewImageFragmentBinding binding;
/**
* Public factory method to create a new fragment that previews an image.
* <p>
* Android strongly recommends keep the empty constructor of fragments as the only public constructor, and use
* {@link #setArguments(Bundle)} to set the needed arguments.
* <p>
* This method hides to client objects the need of doing the construction in two steps.
*
* @param imageFile An {@link OCFile} to preview as an image in the fragment
* @param ignoreFirstSavedState Flag to work around an unexpected behaviour of { FragmentStateAdapter } ;
* TODO better solution
*/
public static PreviewImageFragment newInstance(@NonNull OCFile imageFile,
boolean ignoreFirstSavedState,
boolean showResizedImage) {
PreviewImageFragment frag = new PreviewImageFragment();
frag.showResizedImage = showResizedImage;
Bundle args = new Bundle();
args.putParcelable(ARG_FILE, imageFile);
args.putBoolean(ARG_IGNORE_FIRST, ignoreFirstSavedState);
args.putBoolean(ARG_SHOW_RESIZED_IMAGE, showResizedImage);
frag.setArguments(args);
return frag;
}
/**
* Creates an empty fragment for image previews.
* <p>
* MUST BE KEPT: the system uses it when tries to re-instantiate 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 PreviewImageFragment() {
ignoreFirstSavedState = false;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args == null) {
throw new IllegalArgumentException("Arguments may not be null!");
}
setFile(BundleExtensionsKt.getParcelableArgument(args, ARG_FILE, OCFile.class));
// TODO better in super, but needs to check ALL the class extending FileFragment;
// not right now
ignoreFirstSavedState = args.getBoolean(ARG_IGNORE_FIRST);
showResizedImage = args.getBoolean(ARG_SHOW_RESIZED_IMAGE);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = PreviewImageFragmentBinding.inflate(inflater, container, false);
View view = binding.getRoot();
binding.image.setVisibility(View.GONE);
view.setOnClickListener(v -> togglePreviewImageFullScreen());
binding.image.setOnClickListener(v -> togglePreviewImageFullScreen());
checkLivePhotoAvailability();
setMultiListLoadingMessage();
return view;
}
private void checkLivePhotoAvailability() {
OCFile livePhotoVideo = getFile().livePhotoVideo;
if (livePhotoVideo == null) return;
binding.livePhotoIndicator.setVisibility(View.VISIBLE);
ExtensionsKt.clickWithDebounce(binding.livePhotoIndicator, 4000L, () -> {
playLivePhoto(livePhotoVideo);
return null;
});
}
private void hideActionBar() {
PreviewImageActivity activity = (PreviewImageActivity) requireActivity();
activity.toggleActionBarVisibility(true);
}
private void playLivePhoto(OCFile file) {
if (file == null) {
return;
}
hideActionBar();
Fragment mediaFragment = PreviewMediaFragment.newInstance(file, accountManager.getUser(), 0, true, true);
FragmentManager fragmentManager = requireActivity().getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.top, mediaFragment);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
}
/**
* {@inheritDoc}
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState == null) {
Log_OC.d(TAG, "savedInstanceState is null");
return;
}
if (ignoreFirstSavedState) {
Log_OC.d(TAG, "Saved state ignored");
ignoreFirstSavedState = false;
return;
}
OCFile file = BundleExtensionsKt.getParcelableArgument(savedInstanceState, EXTRA_FILE, OCFile.class);
if (file == null) {
Log_OC.d(TAG, "file cannot be found inside the savedInstanceState");
return;
}
setFile(file);
float maxScale = binding.image.getMaximumScale();
float minScale = binding.image.getMinimumScale();
float savedScale = savedInstanceState.getFloat(EXTRA_ZOOM);
if (savedScale < minScale || savedScale > maxScale) {
Log_OC.d(TAG, "Saved scale " + savedScale + " is out of bounds, setting to default scale.");
savedScale = Math.min(maxScale, Math.max(minScale, savedScale));
}
try {
binding.image.setScale(savedScale);
} catch (IllegalArgumentException e) {
Log_OC.d(TAG, "Error caught at setScale: " + e);
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putFloat(EXTRA_ZOOM, binding.image.getScale());
outState.putParcelable(EXTRA_FILE, getFile());
}
@Override
public void onStart() {
super.onStart();
if (getFile() != null) {
binding.image.setTag(getFile().getFileId());
Point screenSize = DisplayUtils.getScreenSize(getActivity());
int width = screenSize.x;
int height = screenSize.y;
// show thumbnail while loading image
binding.image.setVisibility(View.GONE);
binding.emptyListProgress.setVisibility(View.VISIBLE);
Bitmap thumbnail = getThumbnailBitmap(getFile());
if (thumbnail != null) {
binding.shimmer.setVisibility(View.VISIBLE);
binding.shimmerThumbnail.setImageBitmap(thumbnail);
binding.image.setVisibility(View.GONE);
bitmap = thumbnail;
} else {
thumbnail = ThumbnailsCacheManager.mDefaultImg;
}
if (showResizedImage) {
Bitmap resizedImage = getResizedBitmap(getFile(), width, height);
if (resizedImage != null && !getFile().isUpdateThumbnailNeeded()) {
binding.image.setImageBitmap(resizedImage);
binding.image.setVisibility(View.VISIBLE);
binding.emptyListView.setVisibility(View.GONE);
binding.emptyListProgress.setVisibility(View.GONE);
binding.image.setBackgroundColor(getResources().getColor(R.color.background_color_inverse));
bitmap = resizedImage;
} else {
// generate new resized image
if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(getFile(), binding.image) &&
containerActivity.getStorageManager() != null) {
final ThumbnailsCacheManager.ResizedImageGenerationTask task =
new ThumbnailsCacheManager.ResizedImageGenerationTask(this,
binding.image,
binding.emptyListProgress,
containerActivity.getStorageManager(),
connectivityService,
containerActivity.getStorageManager().getUser(),
getResources().getColor(R.color.background_color_inverse)
);
if (resizedImage == null) {
resizedImage = thumbnail;
}
final ThumbnailsCacheManager.AsyncResizedImageDrawable asyncDrawable =
new ThumbnailsCacheManager.AsyncResizedImageDrawable(
MainApp.getAppContext().getResources(),
resizedImage,
task
);
binding.image.setImageDrawable(asyncDrawable);
task.execute(getFile());
}
}
} else {
loadBitmapTask = new LoadBitmapTask(binding.image, binding.emptyListView, binding.emptyListProgress);
binding.image.setVisibility(View.GONE);
binding.emptyListView.setVisibility(View.GONE);
binding.emptyListProgress.setVisibility(View.VISIBLE);
loadBitmapTask.execute(getFile());
}
} else {
showErrorMessage(R.string.preview_image_error_no_local_file);
}
}
private @Nullable
Bitmap getResizedBitmap(OCFile file, int width, int height) {
Bitmap cachedImage = null;
int scaledWidth = width;
int scaledHeight = height;
for (int i = 0; i < 3 && cachedImage == null; i++) {
try {
cachedImage = ThumbnailsCacheManager.getScaledBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.getRemoteId(),
scaledWidth,
scaledHeight);
} catch (OutOfMemoryError e) {
scaledWidth = scaledWidth / 2;
scaledHeight = scaledHeight / 2;
}
}
return cachedImage;
}
private @Nullable
Bitmap getThumbnailBitmap(OCFile file) {
return ThumbnailsCacheManager.getBitmapFromDiskCache(PREFIX_THUMBNAIL + file.getRemoteId());
}
@Override
public void onStop() {
Log_OC.d(TAG, "onStop starts");
if (loadBitmapTask != null) {
loadBitmapTask.cancel(true);
loadBitmapTask = null;
}
super.onStop();
}
/**
* {@inheritDoc}
*/
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.custom_menu_placeholder, menu);
final MenuItem item = menu.findItem(R.id.custom_menu_placeholder_item);
item.setIcon(viewThemeUtils.platform.colorDrawable(item.getIcon(), ContextCompat.getColor(requireContext(), R.color.white)));
}
@Override
public boolean onOptionsItemSelected(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(file);
}
}
return true;
}
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");
}
/**
* {@inheritDoc}
*/
public void onFileActionChosen(final int itemId) {
if (itemId == R.id.action_send_share_file) {
if (getFile().isSharedWithMe() && !getFile().canReshare()) {
Snackbar.make(requireView(), R.string.resharing_is_not_allowed, Snackbar.LENGTH_LONG).show();
} else {
containerActivity.getFileOperationsHelper().sendShareFile(getFile());
}
} 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_download_file || 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_set_as_wallpaper) {
containerActivity.getFileOperationsHelper().setPictureAs(getFile(), getImageView());
} 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_edit) {
((PreviewImageActivity) requireActivity()).startImageEditor(getFile());
}
}
private void seeDetails() {
containerActivity.showDetails(getFile());
}
@SuppressFBWarnings("Dm")
@Override
public void onDestroy() {
if (bitmap != null) {
bitmap.recycle();
// putting this in onStop() is just the same; the fragment is always destroyed by
// {@link FragmentStatePagerAdapter} when the fragment in swiped further than the
// valid offscreen distance, and onStop() is never called before than that
}
super.onDestroy();
}
/**
* Opens the previewed image with an external application.
*/
private void openFile() {
containerActivity.getFileOperationsHelper().openFile(getFile());
finish();
}
private class LoadBitmapTask extends AsyncTask<OCFile, Void, LoadImage> {
private static final int PARAMS_LENGTH = 1;
/**
* Weak reference to the target {@link ImageView} where the bitmap will be loaded into.
* <p>
* Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load
* finishes.
*/
private final WeakReference<PhotoView> imageViewRef;
private final WeakReference<LinearLayout> infoViewRef;
private final WeakReference<FrameLayout> progressViewRef;
/**
* Error message to show when a load fails.
*/
private int mErrorMessageId;
/**
* Constructor.
*
* @param imageView Target {@link ImageView} where the bitmap will be loaded into.
*/
LoadBitmapTask(PhotoView imageView, LinearLayout infoView, FrameLayout progressView) {
imageViewRef = new WeakReference<>(imageView);
infoViewRef = new WeakReference<>(infoView);
progressViewRef = new WeakReference<>(progressView);
}
@Override
protected LoadImage doInBackground(OCFile... params) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
if (params.length != PARAMS_LENGTH) {
return null;
}
Bitmap bitmapResult = null;
Drawable drawableResult = null;
OCFile ocFile = params[0];
String storagePath = ocFile.getStoragePath();
try {
int maxDownScale = 3; // could be a parameter passed to doInBackground(...)
Point screenSize = DisplayUtils.getScreenSize(getActivity());
int minWidth = screenSize.x;
int minHeight = screenSize.y;
for (int i = 0; i < maxDownScale && bitmapResult == null && drawableResult == null; i++) {
if (MIME_TYPE_SVG.equalsIgnoreCase(ocFile.getMimeType())) {
if (isCancelled()) {
return null;
}
try {
SVG svg = SVG.getFromInputStream(new FileInputStream(storagePath));
drawableResult = new PictureDrawable(svg.renderToPicture());
if (isCancelled()) {
return new LoadImage(null, drawableResult, ocFile);
}
} catch (FileNotFoundException e) {
mErrorMessageId = R.string.common_error_unknown;
Log_OC.e(TAG, "File not found trying to load " + getFile().getStoragePath(), e);
} catch (SVGParseException e) {
mErrorMessageId = R.string.common_error_unknown;
Log_OC.e(TAG, "Couldn't parse SVG " + getFile().getStoragePath(), e);
}
} else {
if (isCancelled()) {
return null;
}
try {
bitmapResult = BitmapUtils.decodeSampledBitmapFromFile(storagePath, minWidth,
minHeight);
if (isCancelled()) {
return new LoadImage(bitmapResult, null, ocFile);
}
if (bitmapResult == null) {
mErrorMessageId = R.string.preview_image_error_unknown_format;
Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
break;
} else {
if (MimeType.JPEG.equalsIgnoreCase(ocFile.getMimeType())) {
// Rotate image, obeying exif tag.
bitmapResult = BitmapUtils.rotateImage(bitmapResult, storagePath);
}
}
} catch (OutOfMemoryError e) {
mErrorMessageId = R.string.common_error_out_memory;
if (i < maxDownScale - 1) {
Log_OC.w(TAG, "Out of memory rendering file " + storagePath + " ; scaling down");
minWidth = minWidth / 2;
minHeight = minHeight / 2;
} else {
Log_OC.w(TAG, "Out of memory rendering file " + storagePath + " ; failing");
}
if (bitmapResult != null) {
bitmapResult.recycle();
}
bitmapResult = null;
}
}
}
} catch (NoSuchFieldError e) {
mErrorMessageId = R.string.common_error_unknown;
Log_OC.e(TAG, "Error from access to non-existing field despite protection; file "
+ storagePath, e);
} catch (Throwable t) {
mErrorMessageId = R.string.common_error_unknown;
Log_OC.e(TAG, "Unexpected error loading " + getFile().getStoragePath(), t);
}
return new LoadImage(bitmapResult, drawableResult, ocFile);
}
@Override
protected void onCancelled(LoadImage result) {
if (result != null && result.bitmap != null) {
result.bitmap.recycle();
}
}
@Override
protected void onPostExecute(LoadImage result) {
if (result.bitmap != null || result.drawable != null) {
showLoadedImage(result);
} else {
showErrorMessage(mErrorMessageId);
}
if (result.bitmap != null && bitmap != result.bitmap) {
// unused bitmap, release it! (just in case)
result.bitmap.recycle();
}
}
private void showLoadedImage(LoadImage result) {
final PhotoView imageView = imageViewRef.get();
Bitmap bitmap = result.bitmap;
Drawable drawable = result.drawable;
if (imageView != null) {
if (bitmap != null) {
Log_OC.d(TAG, "Showing image with resolution " + bitmap.getWidth() + "x" +
bitmap.getHeight());
if (MIME_TYPE_PNG.equalsIgnoreCase(result.ocFile.getMimeType()) ||
MIME_TYPE_GIF.equalsIgnoreCase(result.ocFile.getMimeType())) {
getResources();
imageView.setImageDrawable(generateCheckerboardLayeredDrawable(result, bitmap));
} else {
imageView.setImageBitmap(bitmap);
}
PreviewImageFragment.this.bitmap = bitmap; // needs to be kept for recycling when not useful
} else {
if (drawable != null
&& MIME_TYPE_SVG.equalsIgnoreCase(result.ocFile.getMimeType())) {
getResources();
imageView.setImageDrawable(generateCheckerboardLayeredDrawable(result, null));
}
}
final LinearLayout infoView = infoViewRef.get();
if (infoView != null) {
infoView.setVisibility(View.GONE);
}
final FrameLayout progressView = progressViewRef.get();
if (progressView != null) {
progressView.setVisibility(View.GONE);
}
imageView.setBackgroundColor(getResources().getColor(R.color.background_color_inverse));
imageView.setVisibility(View.VISIBLE);
}
}
}
private LayerDrawable generateCheckerboardLayeredDrawable(LoadImage result, Bitmap bitmap) {
Resources resources = getResources();
Drawable[] layers = new Drawable[2];
layers[0] = ResourcesCompat.getDrawable(resources, R.color.bg_default, null);
Drawable bitmapDrawable;
if (MIME_TYPE_PNG.equalsIgnoreCase(result.ocFile.getMimeType())) {
bitmapDrawable = new BitmapDrawable(resources, bitmap);
} else if (MIME_TYPE_SVG.equalsIgnoreCase(result.ocFile.getMimeType())) {
bitmapDrawable = result.drawable;
} else if (MIME_TYPE_GIF.equalsIgnoreCase(result.ocFile.getMimeType())) {
try {
bitmapDrawable = new GifDrawable(result.ocFile.getStoragePath());
} catch (IOException exception) {
bitmapDrawable = result.drawable;
}
} else {
bitmapDrawable = new BitmapDrawable(resources, bitmap);
}
layers[1] = bitmapDrawable;
LayerDrawable layerDrawable = new LayerDrawable(layers);
Activity activity = getActivity();
if (activity != null) {
int bitmapWidth;
int bitmapHeight;
if (MIME_TYPE_PNG.equalsIgnoreCase(result.ocFile.getMimeType())) {
bitmapWidth = convertDpToPixel(bitmap.getWidth(), getActivity());
bitmapHeight = convertDpToPixel(bitmap.getHeight(), getActivity());
} else {
bitmapWidth = convertDpToPixel(bitmapDrawable.getIntrinsicWidth(), getActivity());
bitmapHeight = convertDpToPixel(bitmapDrawable.getIntrinsicHeight(), getActivity());
}
layerDrawable.setLayerSize(0, bitmapWidth, bitmapHeight);
layerDrawable.setLayerSize(1, bitmapWidth, bitmapHeight);
}
return layerDrawable;
}
private void showErrorMessage(@StringRes int errorMessageId) {
setSorryMessageForMultiList(errorMessageId);
}
private void setMultiListLoadingMessage() {
binding.image.setVisibility(View.GONE);
binding.emptyListView.setVisibility(View.GONE);
binding.emptyListProgress.setVisibility(View.VISIBLE);
}
private void setSorryMessageForMultiList(@StringRes int message) {
binding.emptyListViewHeadline.setText(R.string.preview_sorry);
binding.emptyListViewText.setText(message);
binding.emptyListIcon.setImageResource(R.drawable.file_image);
binding.emptyListView.setBackgroundColor(getResources().getColor(R.color.bg_default));
binding.emptyListViewHeadline.setTextColor(getResources().getColor(R.color.standard_grey));
binding.emptyListViewText.setTextColor(getResources().getColor(R.color.standard_grey));
binding.image.setVisibility(View.GONE);
binding.emptyListView.setVisibility(View.VISIBLE);
binding.emptyListProgress.setVisibility(View.GONE);
}
public void setErrorPreviewMessage() {
try {
if (getActivity() != null) {
Snackbar.make(binding.emptyListView,
R.string.resized_image_not_possible_download,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.common_yes, v -> {
PreviewImageActivity activity = (PreviewImageActivity) getActivity();
if (activity != null) {
activity.requestForDownload(getFile());
} else if (getContext() != null) {
Snackbar.make(binding.emptyListView,
getResources().getString(R.string.could_not_download_image),
Snackbar.LENGTH_INDEFINITE).show();
}
}
).show();
}
} catch (IllegalArgumentException e) {
Log_OC.d(TAG, e.getMessage());
}
}
public void setNoConnectionErrorMessage() {
try {
Snackbar.make(binding.emptyListView, R.string.auth_no_net_conn_title, Snackbar.LENGTH_LONG).show();
} catch (IllegalArgumentException e) {
Log_OC.d(TAG, e.getMessage());
}
}
/**
* Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment} 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.isImage(file);
}
/**
* Finishes the preview
*/
private void finish() {
Activity container = getActivity();
if (container != null) {
container.finish();
}
}
private void togglePreviewImageFullScreen() {
Activity activity = getActivity();
if (activity != null) {
((PreviewImageActivity) activity).toggleFullScreen();
}
toggleImageBackground();
}
private void toggleImageBackground() {
if (getFile() != null && (MIME_TYPE_PNG.equalsIgnoreCase(getFile().getMimeType()) ||
MIME_TYPE_SVG.equalsIgnoreCase(getFile().getMimeType())) && getActivity() != null &&
getActivity() instanceof PreviewImageActivity) {
PreviewImageActivity previewImageActivity = (PreviewImageActivity) getActivity();
if (binding.image.getDrawable() instanceof LayerDrawable) {
LayerDrawable layerDrawable = (LayerDrawable) binding.image.getDrawable();
Drawable layerOne;
if (previewImageActivity.isSystemUIVisible()) {
layerOne = ResourcesCompat.getDrawable(getResources(), R.color.bg_default, null);
} else {
layerOne = ResourcesCompat.getDrawable(getResources(), R.drawable.backrepeat, null);
}
layerDrawable.setDrawableByLayerId(layerDrawable.getId(0), layerOne);
binding.image.setImageDrawable(layerDrawable);
binding.image.invalidate();
}
}
}
private static int convertDpToPixel(float dp, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return (int) (dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
}
public PhotoView getImageView() {
return binding.image;
}
private class LoadImage {
private final Bitmap bitmap;
private final Drawable drawable;
private final OCFile ocFile;
LoadImage(Bitmap bitmap, Drawable drawable, OCFile ocFile) {
this.bitmap = bitmap;
this.drawable = drawable;
this.ocFile = ocFile;
}
}
}

View file

@ -0,0 +1,876 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020-2024 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2017-2020 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 2013-2015 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.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.PictureDrawable
import android.os.AsyncTask
import android.os.Bundle
import android.os.Process
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import com.github.chrisbanes.photoview.PhotoView
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.ui.fileactions.FileActionsBottomSheet.Companion.newInstance
import com.nextcloud.utils.extensions.clickWithDebounce
import com.nextcloud.utils.extensions.getParcelableArgument
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.databinding.PreviewImageFragmentBinding
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncResizedImageDrawable
import com.owncloud.android.datamodel.ThumbnailsCacheManager.ResizedImageGenerationTask
import com.owncloud.android.lib.common.utils.Log_OC
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.ui.preview.PreviewMediaFragment.Companion.newInstance
import com.owncloud.android.utils.BitmapUtils
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.MimeType
import com.owncloud.android.utils.MimeTypeUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import pl.droidsonroids.gif.GifDrawable
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.ref.WeakReference
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
/**
* This fragment shows a preview of a downloaded image.
* Trying to get an instance with a NULL [OCFile] will produce an [IllegalStateException].
* If the [OCFile] passed is not downloaded, an [IllegalStateException] is generated on instantiation too.
*/
/**
* Creates an empty fragment for image previews.
*
*
* MUST BE KEPT: the system uses it when tries to re-instantiate 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("TooManyFunctions")
class PreviewImageFragment : FileFragment(), Injectable {
private var showResizedImage: Boolean? = null
private var bitmap: Bitmap? = null
private var ignoreFirstSavedState = false
private var loadBitmapTask: LoadBitmapTask? = null
@Inject
lateinit var connectivityService: ConnectivityService
@Inject
lateinit var accountManager: UserAccountManager
@Inject
lateinit var backgroundJobManager: BackgroundJobManager
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
private lateinit var binding: PreviewImageFragmentBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = arguments ?: throw IllegalArgumentException("Arguments may not be null!")
file = args.getParcelableArgument(ARG_FILE, OCFile::class.java)
// TODO better in super, but needs to check ALL the class extending FileFragment;
// not right now
ignoreFirstSavedState = args.getBoolean(ARG_IGNORE_FIRST)
showResizedImage = args.getBoolean(ARG_SHOW_RESIZED_IMAGE)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = PreviewImageFragmentBinding.inflate(inflater, container, false)
binding.image.visibility = View.GONE
binding.root.setOnClickListener { togglePreviewImageFullScreen() }
binding.image.setOnClickListener { togglePreviewImageFullScreen() }
checkLivePhotoAvailability()
setMultiListLoadingMessage()
return binding.root
}
@Suppress("MagicNumber")
private fun checkLivePhotoAvailability() {
val livePhotoVideo = file.livePhotoVideo ?: return
binding.livePhotoIndicator.visibility = View.VISIBLE
clickWithDebounce(binding.livePhotoIndicator, 4000L) {
playLivePhoto(livePhotoVideo)
}
}
private fun hideActionBar() {
(requireActivity() as PreviewImageActivity).run {
toggleActionBarVisibility(true)
}
}
private fun playLivePhoto(file: OCFile?) {
if (file == null) {
return
}
hideActionBar()
val mediaFragment: Fragment = newInstance(file, accountManager.user, 0, true, true)
val fragmentManager = requireActivity().supportFragmentManager
fragmentManager.beginTransaction().run {
replace(R.id.top, mediaFragment)
addToBackStack(null)
commit()
}
}
/**
* {@inheritDoc}
*/
@Suppress("ReturnCount")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) {
Log_OC.d(TAG, "savedInstanceState is null")
return
}
if (ignoreFirstSavedState) {
Log_OC.d(TAG, "Saved state ignored")
ignoreFirstSavedState = false
return
}
val file = savedInstanceState.getParcelableArgument(EXTRA_FILE, OCFile::class.java)
if (file == null) {
Log_OC.d(TAG, "file cannot be found inside the savedInstanceState")
return
}
setFile(file)
val maxScale = binding.image.maximumScale
val minScale = binding.image.minimumScale
var savedScale = savedInstanceState.getFloat(EXTRA_ZOOM)
if (savedScale < minScale || savedScale > maxScale) {
Log_OC.d(TAG, "Saved scale $savedScale is out of bounds, setting to default scale.")
savedScale = min(maxScale.toDouble(), max(minScale.toDouble(), savedScale.toDouble()))
.toFloat()
}
try {
binding.image.scale = savedScale
} catch (e: IllegalArgumentException) {
Log_OC.d(TAG, "Error caught at setScale: $e")
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putFloat(EXTRA_ZOOM, binding.image.scale)
outState.putParcelable(EXTRA_FILE, file)
}
override fun onStart() {
super.onStart()
if (file == null) {
showErrorMessage(R.string.preview_image_error_no_local_file)
return
}
binding.image.tag = file.fileId
val screenSize = DisplayUtils.getScreenSize(activity)
val width = screenSize.x
val height = screenSize.y
// show thumbnail while loading image
binding.image.visibility = View.GONE
binding.emptyListProgress.visibility = View.VISIBLE
var thumbnail = getThumbnailBitmap(file)
if (thumbnail != null) {
binding.shimmer.visibility = View.VISIBLE
binding.shimmerThumbnail.setImageBitmap(thumbnail)
binding.image.visibility = View.GONE
bitmap = thumbnail
} else {
thumbnail = ThumbnailsCacheManager.mDefaultImg
}
if (showResizedImage == true) {
adjustResizedImage(thumbnail, width, height)
} else {
loadBitmapTask = LoadBitmapTask(binding.image, binding.emptyListView, binding.emptyListProgress)
binding.image.visibility = View.GONE
binding.emptyListView.visibility = View.GONE
binding.emptyListProgress.visibility = View.VISIBLE
loadBitmapTask?.execute(file)
}
}
private fun adjustResizedImage(thumbnail: Bitmap?, width: Int, height: Int) {
var resizedImage = getResizedBitmap(file, width, height)
if (resizedImage != null && !file.isUpdateThumbnailNeeded) {
binding.image.setImageBitmap(resizedImage)
binding.image.visibility = View.VISIBLE
binding.emptyListView.visibility = View.GONE
binding.emptyListProgress.visibility = View.GONE
binding.image.setBackgroundColor(resources.getColor(R.color.background_color_inverse))
bitmap = resizedImage
} else {
// generate new resized image
if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, binding.image) &&
containerActivity.storageManager != null
) {
val task =
ResizedImageGenerationTask(
this,
binding.image,
binding.emptyListProgress,
containerActivity.storageManager,
connectivityService,
containerActivity.storageManager.user,
resources.getColor(R.color.background_color_inverse)
)
if (resizedImage == null) {
resizedImage = thumbnail
}
val asyncDrawable =
AsyncResizedImageDrawable(
MainApp.getAppContext().resources,
resizedImage,
task
)
binding.image.setImageDrawable(asyncDrawable)
task.execute(file)
}
}
}
@Suppress("MagicNumber")
private fun getResizedBitmap(file: OCFile, width: Int, height: Int): Bitmap? {
var cachedImage: Bitmap? = null
var scaledWidth = width
var scaledHeight = height
var i = 0
while (i < 3 && cachedImage == null) {
try {
cachedImage = ThumbnailsCacheManager.getScaledBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId,
scaledWidth,
scaledHeight
)
} catch (e: OutOfMemoryError) {
scaledWidth /= 2
scaledHeight /= 2
}
i++
}
return cachedImage
}
private fun getThumbnailBitmap(file: OCFile): Bitmap? {
return ThumbnailsCacheManager.getBitmapFromDiskCache(ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId)
}
override fun onStop() {
Log_OC.d(TAG, "onStop starts")
loadBitmapTask?.cancel(true)
loadBitmapTask = null
super.onStop()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val menuHost: MenuHost = requireActivity()
addMenuProvider(menuHost)
}
private fun addMenuProvider(menuHost: MenuHost) {
menuHost.addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
val item = menu.findItem(R.id.custom_menu_placeholder_item)
item.icon?.let {
item.setIcon(
viewThemeUtils.platform.colorDrawable(
it,
ContextCompat.getColor(requireContext(), R.color.white)
)
)
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.custom_menu_placeholder_item -> {
val file = file
if (containerActivity.storageManager != null && file != null) {
// Update the file
val updatedFile = containerActivity.storageManager.getFileById(file.fileId)
setFile(updatedFile)
val fileNew = getFile()
if (fileNew != null) {
showFileActions(file)
}
}
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)
}
val fragmentManager = childFragmentManager
newInstance(file, false, additionalFilter)
.setResultListener(fragmentManager, this) { itemId: Int -> this.onFileActionChosen(itemId) }
.show(fragmentManager, "actions")
}
/**
* {@inheritDoc}
*/
private fun onFileActionChosen(itemId: Int) {
if (itemId == R.id.action_send_share_file) {
if (file.isSharedWithMe && !file.canReshare()) {
Snackbar.make(requireView(), R.string.resharing_is_not_allowed, Snackbar.LENGTH_LONG).show()
} else {
containerActivity.fileOperationsHelper.sendShareFile(file)
}
} else if (itemId == R.id.action_open_file_with) {
openFile()
} else if (itemId == R.id.action_remove_file) {
val dialog = RemoveFilesDialogFragment.newInstance(file)
dialog.show(parentFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)
} else if (itemId == R.id.action_see_details) {
seeDetails()
} else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file) {
containerActivity.fileOperationsHelper.syncFile(file)
} else if (itemId == R.id.action_cancel_sync) {
containerActivity.fileOperationsHelper.cancelTransference(file)
} else if (itemId == R.id.action_set_as_wallpaper) {
containerActivity.fileOperationsHelper.setPictureAs(file, imageView)
} else if (itemId == R.id.action_export_file) {
val list = ArrayList<OCFile>()
list.add(file)
containerActivity.fileOperationsHelper.exportFiles(
list,
context,
view,
backgroundJobManager
)
} else if (itemId == R.id.action_edit) {
(requireActivity() as PreviewImageActivity).startImageEditor(file)
}
}
private fun seeDetails() {
containerActivity.showDetails(file)
}
@SuppressFBWarnings("Dm")
override fun onDestroy() {
bitmap?.recycle()
super.onDestroy()
}
/**
* Opens the previewed image with an external application.
*/
private fun openFile() {
containerActivity.fileOperationsHelper.openFile(file)
finish()
}
@SuppressLint("StaticFieldLeak")
private inner class LoadBitmapTask(imageView: PhotoView, infoView: LinearLayout, progressView: FrameLayout) :
AsyncTask<OCFile?, Void?, LoadImage?>() {
/**
* Weak reference to the target [ImageView] where the bitmap will be loaded into.
*
*
* Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load
* finishes.
*/
private val imageViewRef = WeakReference(imageView)
private val infoViewRef = WeakReference(infoView)
private val progressViewRef = WeakReference(progressView)
/**
* Error message to show when a load fails.
*/
private var mErrorMessageId = 0
private val paramsLength = 1
@Suppress("TooGenericExceptionCaught", "MagicNumber", "ReturnCount", "LongMethod", "NestedBlockDepth")
@Deprecated("Deprecated in Java")
override fun doInBackground(vararg params: OCFile?): LoadImage? {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE)
if (params.size != paramsLength) {
return null
}
var bitmapResult: Bitmap? = null
var drawableResult: Drawable? = null
val ocFile = params[0] ?: return null
val storagePath = ocFile.storagePath
try {
val maxDownScale = 3 // could be a parameter passed to doInBackground(...)
val screenSize = DisplayUtils.getScreenSize(activity)
var minWidth = screenSize.x
var minHeight = screenSize.y
var i = 0
while (i < maxDownScale && bitmapResult == null && drawableResult == null) {
if (MIME_TYPE_SVG.equals(ocFile.mimeType, ignoreCase = true)) {
if (isCancelled) {
return null
}
try {
val svg = SVG.getFromInputStream(FileInputStream(storagePath))
drawableResult = PictureDrawable(svg.renderToPicture())
if (isCancelled) {
return LoadImage(null, drawableResult, ocFile)
}
} catch (e: FileNotFoundException) {
mErrorMessageId = R.string.common_error_unknown
Log_OC.e(TAG, "File not found trying to load " + file.storagePath, e)
} catch (e: SVGParseException) {
mErrorMessageId = R.string.common_error_unknown
Log_OC.e(TAG, "Couldn't parse SVG " + file.storagePath, e)
}
} else {
if (isCancelled) {
return null
}
try {
bitmapResult = BitmapUtils.decodeSampledBitmapFromFile(
storagePath,
minWidth,
minHeight
)
if (isCancelled) {
return LoadImage(bitmapResult, null, ocFile)
}
if (bitmapResult == null) {
mErrorMessageId = R.string.preview_image_error_unknown_format
Log_OC.e(TAG, "File could not be loaded as a bitmap: $storagePath")
break
} else {
if (MimeType.JPEG.equals(ocFile.mimeType, ignoreCase = true)) {
// Rotate image, obeying exif tag.
bitmapResult = BitmapUtils.rotateImage(bitmapResult, storagePath)
}
}
} catch (e: OutOfMemoryError) {
mErrorMessageId = R.string.common_error_out_memory
if (i < maxDownScale - 1) {
Log_OC.w(TAG, "Out of memory rendering file $storagePath ; scaling down")
minWidth /= 2
minHeight /= 2
} else {
Log_OC.w(TAG, "Out of memory rendering file $storagePath ; failing")
}
bitmapResult?.recycle()
bitmapResult = null
}
}
i++
}
} catch (e: NoSuchFieldError) {
mErrorMessageId = R.string.common_error_unknown
Log_OC.e(
TAG,
"Error from access to non-existing field despite protection; file " +
storagePath,
e
)
} catch (t: Throwable) {
mErrorMessageId = R.string.common_error_unknown
Log_OC.e(TAG, "Unexpected error loading " + file.storagePath, t)
}
return LoadImage(bitmapResult, drawableResult, ocFile)
}
@Deprecated("Deprecated in Java")
override fun onCancelled(result: LoadImage?) {
result?.bitmap?.recycle()
}
@Deprecated("Deprecated in Java")
override fun onPostExecute(result: LoadImage?) {
if (result?.bitmap != null || result?.drawable != null) {
showLoadedImage(result)
} else {
showErrorMessage(mErrorMessageId)
}
if (result?.bitmap != null && bitmap != result.bitmap) {
// unused bitmap, release it! (just in case)
result.bitmap.recycle()
}
}
private fun showLoadedImage(result: LoadImage?) {
val imageView = imageViewRef.get()
val bitmap = result?.bitmap
val drawable = result?.drawable
if (imageView == null) {
return
}
if (bitmap != null) {
Log_OC.d(
TAG,
"Showing image with resolution " + bitmap.width + "x" +
bitmap.height
)
if (MIME_TYPE_PNG.equals(result.ocFile.mimeType, ignoreCase = true) ||
MIME_TYPE_GIF.equals(result.ocFile.mimeType, ignoreCase = true)
) {
resources
imageView.setImageDrawable(generateCheckerboardLayeredDrawable(result, bitmap))
} else {
imageView.setImageBitmap(bitmap)
}
this@PreviewImageFragment.bitmap = bitmap // needs to be kept for recycling when not useful
} else {
if (drawable != null &&
MIME_TYPE_SVG.equals(result.ocFile.mimeType, ignoreCase = true)
) {
resources
imageView.setImageDrawable(generateCheckerboardLayeredDrawable(result, null))
}
}
val infoView = infoViewRef.get()
infoView?.visibility = View.GONE
val progressView = progressViewRef.get()
progressView?.visibility = View.GONE
imageView.setBackgroundColor(resources.getColor(R.color.background_color_inverse))
imageView.visibility = View.VISIBLE
}
}
@Suppress("ReturnCount")
private fun generateCheckerboardLayeredDrawable(result: LoadImage, bitmap: Bitmap?): LayerDrawable {
val resources = resources
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ResourcesCompat.getDrawable(resources, R.color.bg_default, null)
val bitmapDrawable =
if (MIME_TYPE_PNG.equals(result.ocFile.mimeType, ignoreCase = true)) {
BitmapDrawable(resources, bitmap)
} else if (MIME_TYPE_SVG.equals(result.ocFile.mimeType, ignoreCase = true)) {
result.drawable
} else if (MIME_TYPE_GIF.equals(result.ocFile.mimeType, ignoreCase = true)) {
try {
GifDrawable(result.ocFile.storagePath)
} catch (exception: IOException) {
result.drawable
}
} else {
BitmapDrawable(resources, bitmap)
}
layers[1] = bitmapDrawable
val layerDrawable = LayerDrawable(layers)
val activity: Activity? = activity
if (activity != null) {
val bitmapWidth: Int
val bitmapHeight: Int
if (MIME_TYPE_PNG.equals(result.ocFile.mimeType, ignoreCase = true)) {
if (bitmap == null) {
return layerDrawable
}
bitmapWidth = convertDpToPixel(bitmap.width.toFloat(), getActivity())
bitmapHeight = convertDpToPixel(bitmap.height.toFloat(), getActivity())
} else {
if (bitmapDrawable == null) {
return layerDrawable
}
bitmapWidth = convertDpToPixel(bitmapDrawable.intrinsicWidth.toFloat(), getActivity())
bitmapHeight = convertDpToPixel(bitmapDrawable.intrinsicHeight.toFloat(), getActivity())
}
layerDrawable.setLayerSize(0, bitmapWidth, bitmapHeight)
layerDrawable.setLayerSize(1, bitmapWidth, bitmapHeight)
}
return layerDrawable
}
private fun showErrorMessage(@StringRes errorMessageId: Int) {
setSorryMessageForMultiList(errorMessageId)
}
private fun setMultiListLoadingMessage() {
binding.image.visibility = View.GONE
binding.emptyListView.visibility = View.GONE
binding.emptyListProgress.visibility = View.VISIBLE
}
private fun setSorryMessageForMultiList(@StringRes message: Int) {
binding.emptyListViewHeadline.setText(R.string.preview_sorry)
binding.emptyListViewText.setText(message)
binding.emptyListIcon.setImageResource(R.drawable.file_image)
binding.emptyListView.setBackgroundColor(resources.getColor(R.color.bg_default))
binding.emptyListViewHeadline.setTextColor(resources.getColor(R.color.standard_grey))
binding.emptyListViewText.setTextColor(resources.getColor(R.color.standard_grey))
binding.image.visibility = View.GONE
binding.emptyListView.visibility = View.VISIBLE
binding.emptyListProgress.visibility = View.GONE
}
fun setErrorPreviewMessage() {
try {
if (activity != null) {
Snackbar.make(
binding.emptyListView,
R.string.resized_image_not_possible_download,
Snackbar.LENGTH_INDEFINITE
)
.setAction(
R.string.common_yes
) { v: View? ->
val activity = activity as PreviewImageActivity?
if (activity != null) {
activity.requestForDownload(file)
} else if (context != null) {
Snackbar.make(
binding.emptyListView,
resources.getString(R.string.could_not_download_image),
Snackbar.LENGTH_INDEFINITE
).show()
}
}.show()
}
} catch (e: IllegalArgumentException) {
Log_OC.d(TAG, e.message)
}
}
fun setNoConnectionErrorMessage() {
try {
Snackbar.make(binding.emptyListView, R.string.auth_no_net_conn_title, Snackbar.LENGTH_LONG).show()
} catch (e: IllegalArgumentException) {
Log_OC.d(TAG, e.message)
}
}
/**
* Finishes the preview
*/
private fun finish() {
val container: Activity? = activity
container?.finish()
}
private fun togglePreviewImageFullScreen() {
val activity: Activity? = activity
if (activity != null) {
(activity as PreviewImageActivity).toggleFullScreen()
}
toggleImageBackground()
}
@Suppress("ComplexCondition")
private fun toggleImageBackground() {
if (file != null && (
MIME_TYPE_PNG.equals(
file.mimeType,
ignoreCase = true
) ||
MIME_TYPE_SVG.equals(file.mimeType, ignoreCase = true)
) && activity != null &&
activity is PreviewImageActivity
) {
val previewImageActivity = activity as PreviewImageActivity?
if (binding.image.drawable is LayerDrawable) {
val layerDrawable = binding.image.drawable as LayerDrawable
val layerOne = if (previewImageActivity?.isSystemUIVisible == true) {
ResourcesCompat.getDrawable(resources, R.color.bg_default, null)
} else {
ResourcesCompat.getDrawable(
resources,
R.drawable.backrepeat,
null
)
}
layerDrawable.setDrawableByLayerId(layerDrawable.getId(0), layerOne)
binding.image.setImageDrawable(layerDrawable)
binding.image.invalidate()
}
}
}
val imageView: PhotoView
get() = binding.image
private inner class LoadImage(val bitmap: Bitmap?, val drawable: Drawable?, val ocFile: OCFile)
companion object {
private const val EXTRA_FILE = "FILE"
private const val EXTRA_ZOOM = "ZOOM"
private const val ARG_FILE = "FILE"
private const val ARG_IGNORE_FIRST = "IGNORE_FIRST"
private const val ARG_SHOW_RESIZED_IMAGE = "SHOW_RESIZED_IMAGE"
private const val MIME_TYPE_PNG = "image/png"
private const val MIME_TYPE_GIF = "image/gif"
private const val MIME_TYPE_SVG = "image/svg+xml"
private val TAG: String = PreviewImageFragment::class.java.simpleName
/**
* Public factory method to create a new fragment that previews an image.
*
*
* Android strongly recommends keep the empty constructor of fragments as the only public constructor, and use
* [.setArguments] to set the needed arguments.
*
*
* This method hides to client objects the need of doing the construction in two steps.
*
* @param imageFile An [OCFile] to preview as an image in the fragment
* @param ignoreFirstSavedState Flag to work around an unexpected behaviour of { FragmentStateAdapter } ;
* TODO better solution
*/
fun newInstance(
imageFile: OCFile,
ignoreFirstSavedState: Boolean,
showResizedImage: Boolean
): PreviewImageFragment {
val args = Bundle().apply {
putParcelable(ARG_FILE, imageFile)
putBoolean(ARG_IGNORE_FIRST, ignoreFirstSavedState)
putBoolean(ARG_SHOW_RESIZED_IMAGE, showResizedImage)
}
return PreviewImageFragment().apply {
this.showResizedImage = showResizedImage
arguments = args
}
}
/**
* Helper method to test if an [OCFile] can be passed to a [PreviewImageFragment] 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.isImage(file)
}
private fun convertDpToPixel(dp: Float, context: Context?): Int {
val resources = context?.resources ?: return 0
val metrics = resources.displayMetrics
return (dp * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
}
}
}

View file

@ -92,6 +92,7 @@
<string name="auth_unsupported_multiaccount">%1$s لا يدعم الحسابات المتعددة </string>
<string name="auth_wrong_connection_title">لا يمكن إنشاء إتصال</string>
<string name="authenticator_activity_cancel_login">إلغاء الدخول</string>
<string name="authenticator_activity_login_error">حدث إشكال عند معالجة طلبك للدخول. يُرجى المحاولة مرةً أخرى.</string>
<string name="authenticator_activity_please_complete_login_process">رجاءً، قم بإكمال عملية الدخول في مستعرض الوب عندك</string>
<string name="auto_upload_file_behaviour_kept_in_folder">ترك في مجلد الأصل، بسبب كونه للقرائة فقط</string>
<string name="auto_upload_on_wifi">قم بالرفع عبر شبكة لاسلكية غير محدودة البيانات فقط</string>

View file

@ -882,6 +882,7 @@ Enheds legitimationsoplysninger er sat op
<string name="upload_local_storage_full">Lokalt lager fuldt</string>
<string name="upload_local_storage_not_copied">Filen kunne ikke kopieres til lokalt lager</string>
<string name="upload_lock_failed">Lås på mappe fejlede</string>
<string name="upload_manually_cancelled">Upload var annulleret af bruger</string>
<string name="upload_old_android">Kryptering kun mulig med &gt;= Android 5.0</string>
<string name="upload_query_move_foreign_files">Utilstrækkelig plads til, at kopiere de valgte filer ind i %1$s mappen. Vil du flytte dem i stedet?</string>
<string name="upload_scan_doc_upload">Scan dokument fra kamera</string>

View file

@ -92,6 +92,7 @@
<string name="auth_unsupported_multiaccount">%1$s unterstützt nicht mehrere Benutzerkonten</string>
<string name="auth_wrong_connection_title">Verbindung konnte nicht hergestellt werden</string>
<string name="authenticator_activity_cancel_login">Anmelden abbrechen</string>
<string name="authenticator_activity_login_error">Fehler bei der Verarbeitung Ihrer Anmeldeanforderung. Bitte versuchen Sie es später erneut.</string>
<string name="authenticator_activity_please_complete_login_process">Bitte schließen Sie den Anmeldevorgang in Ihrem Browser ab</string>
<string name="auto_upload_file_behaviour_kept_in_folder">im Original-Verzeichnis belassen, da nur lesbar</string>
<string name="auto_upload_on_wifi">Nur über gebührenfreies WLAN hochladen</string>

View file

@ -92,6 +92,7 @@
<string name="auth_unsupported_multiaccount">Ní thacaíonn %1$s le cuntais iolracha</string>
<string name="auth_wrong_connection_title">Níorbh fhéidir ceangal a bhunú</string>
<string name="authenticator_activity_cancel_login">Cealaigh Logáil Isteach</string>
<string name="authenticator_activity_login_error">Bhí fadhb ann d\'iarratas logáil isteach a phróiseáil. Bain triail eile as ar ball le do thoil.</string>
<string name="authenticator_activity_please_complete_login_process">Críochnaigh an próiseas logáil isteach i do bhrabhsálaí le do thoil</string>
<string name="auto_upload_file_behaviour_kept_in_folder">coinnithe sa bhunfhillteán, mar atá sé inléite amháin</string>
<string name="auto_upload_on_wifi">Uaslódáil ar Wi-Fi neamh-mhéadraithe amháin</string>

View file

@ -92,6 +92,7 @@
<string name="auth_unsupported_multiaccount">%1$s non admite contas múltipes</string>
<string name="auth_wrong_connection_title">Non foi posíbel estabelecer a conexión</string>
<string name="authenticator_activity_cancel_login">Cancelar o acceso</string>
<string name="authenticator_activity_login_error">Houbo un problema ao procesar a súa solicitude de acceso. Ténteo de novo máis tarde.</string>
<string name="authenticator_activity_please_complete_login_process">Complete o proceso de acceso no seu navegador</string>
<string name="auto_upload_file_behaviour_kept_in_folder">mantense no cartafol orixinal, xa que é de só lectura</string>
<string name="auto_upload_on_wifi">Enviar só con wifi sen límite de datos</string>
@ -99,7 +100,7 @@
<string name="autoupload_configure">Configurar</string>
<string name="autoupload_create_new_custom_folder">Crear un cartafol personalizado novo</string>
<string name="autoupload_custom_folder">Configurar un cartafol personalizado</string>
<string name="autoupload_disable_power_save_check">Desactivar a verificación de aforro de enerxía</string>
<string name="autoupload_disable_power_save_check">Desactivar a comprobación de aforro de enerxía</string>
<string name="autoupload_hide_folder">Agochar o cartafol</string>
<string name="autoupload_worker_foreground_info">Preparando o envío automático</string>
<string name="avatar">Avatar</string>
@ -599,7 +600,7 @@
<string name="placeholder_timestamp">18/05/2012 12:23 PM</string>
<string name="player_stop">parar</string>
<string name="player_toggle">alternar</string>
<string name="power_save_check_dialog_message">A desactivación da verificación de aforro de enerxía pode provocar o envío de ficheiros cando estea coa batería baixa.</string>
<string name="power_save_check_dialog_message">A desactivación da comprobación de aforro de enerxía pode provocar o envío de ficheiros cando estea coa batería baixa.</string>
<string name="pref_behaviour_entries_delete_file">eliminado</string>
<string name="pref_behaviour_entries_keep_file">mantense no cartafol orixinal</string>
<string name="pref_behaviour_entries_move">movido cara ao cartafol da aplicación</string>

View file

@ -92,6 +92,7 @@
<string name="auth_unsupported_multiaccount">%1$s støtter ikke flere kontoer</string>
<string name="auth_wrong_connection_title">Klarte ikke å opprette tilkobling</string>
<string name="authenticator_activity_cancel_login">Avbryt pålogging</string>
<string name="authenticator_activity_login_error">Det oppstod et problem under behandling av påloggingsforespørselen. Prøv igjen senere.</string>
<string name="authenticator_activity_please_complete_login_process">Fullfør påloggingsprosessen i nettleseren din</string>
<string name="auto_upload_file_behaviour_kept_in_folder">beholdt i opprinnelig mappe da den kun har leserettigheter</string>
<string name="auto_upload_on_wifi">Kun last opp på ubegrenset Wi-Fi</string>

View file

@ -92,6 +92,7 @@
<string name="auth_unsupported_multiaccount">%1$s 不支援多個帳號</string>
<string name="auth_wrong_connection_title">無法建立連線</string>
<string name="authenticator_activity_cancel_login">取消登入</string>
<string name="authenticator_activity_login_error">處理您的登入請求時出現問題。請稍後再試。</string>
<string name="authenticator_activity_please_complete_login_process">請在瀏覽器中完成登入流程</string>
<string name="auto_upload_file_behaviour_kept_in_folder">以唯讀模式保留在原本的資料夾</string>
<string name="auto_upload_on_wifi">僅在非計量收費的 Wi-Fi 上傳</string>
@ -321,6 +322,7 @@
<string name="error_showing_encryption_dialog">顯示加密設定對話方塊時發生錯誤!</string>
<string name="error_starting_direct_camera_upload">無法啟動相機</string>
<string name="error_starting_doc_scan">開始文件掃描時發生錯誤</string>
<string name="error_uploading_direct_camera_upload">上傳拍攝的媒體失敗</string>
<string name="etm_accounts">帳號</string>
<string name="etm_background_execution_count">時段執行於48小時內</string>
<string name="etm_background_job_created">已建立</string>
@ -904,7 +906,9 @@
<string name="upload_chooser_title">上傳自……</string>
<string name="upload_content_from_other_apps">從其它應用程式上傳</string>
<string name="upload_direct_camera_photo">照片</string>
<string name="upload_direct_camera_promt">您想要拍攝照片或影片?</string>
<string name="upload_direct_camera_upload">從相機上傳</string>
<string name="upload_direct_camera_video">影片</string>
<string name="upload_file_dialog_filename">檔案名稱</string>
<string name="upload_file_dialog_filetype">檔案類型</string>
<string name="upload_file_dialog_filetype_googlemap_shortcut">Google 地圖捷徑 (%s)</string>

View file

@ -1154,6 +1154,7 @@
<string name="check_back_later_or_reload">Check back later or reload.</string>
<string name="e2e_not_yet_setup">E2E not yet setup</string>
<string name="error_file_actions">Error showing file actions</string>
<string name="file_activity_shared_file_cannot_be_updated">Shared file cannot be updated</string>
<string name="pin_home">Pin to Home screen</string>
<string name="pin_shortcut_label">Open %1$s</string>
<string name="displays_mnemonic">Displays your 12 word passphrase</string>

View file

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