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
09d71a7340
15 changed files with 1063 additions and 1017 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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 >= 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue