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

This commit is contained in:
Tobias Kaminsky 2024-07-05 03:37:20 +02:00
commit 5c86b142d8
17 changed files with 768 additions and 771 deletions

View file

@ -9,12 +9,14 @@
name: REUSE Compliance Check
on: pull_request
on: [pull_request]
jobs:
reuse-compliance-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: REUSE Compliance Check
uses: fsfe/reuse-action@a46482ca367aef4454a87620aa37c2be4b2f8106 # v3.0.0
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0

View file

@ -299,7 +299,7 @@ dependencies {
implementation 'com.googlecode.ez-vcard:ez-vcard:0.12.1'
implementation 'org.lukhnos:nnio:0.3.1'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'com.github.nextcloud-deps:sectioned-recyclerview:0.6.1'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.28'
@ -310,7 +310,7 @@ dependencies {
}
implementation 'com.caverock:androidsvg:1.4'
implementation 'androidx.annotation:annotation:1.8.0'
implementation 'com.vanniktech:emoji-google:0.18.0'
implementation 'com.vanniktech:emoji-google:0.21.0'
implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido:$fidoVersion"
implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido2:$fidoVersion"
@ -378,11 +378,11 @@ dependencies {
// dependencies for instrumented tests
// JUnit4 Rules
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation "androidx.test:rules:$androidxTestVersion"
// Android JUnit Runner
androidTestImplementation "androidx.test:runner:1.5.2"
androidTestUtil "androidx.test:orchestrator:1.4.2"
androidTestImplementation "androidx.test:runner:1.6.1"
androidTestUtil "androidx.test:orchestrator:1.5.0"
androidTestImplementation "androidx.test:core-ktx:$androidxTestVersion"
// Espresso

View file

@ -182,7 +182,7 @@ class FilesSyncWork(
} else {
// Check every file in synced folder for changes and update
// filesystemDataProvider database (potentially needs a long time)
FilesSyncHelper.insertAllDBEntries(syncedFolder, powerManagementService)
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder)
}
}

View file

@ -12,9 +12,11 @@ import android.content.Context
import android.content.Intent
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.device.BatteryStatus
import com.nextcloud.client.device.PowerManagementService
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.currentUploadFileOperation
import com.nextcloud.client.network.Connectivity
import com.nextcloud.client.network.ConnectivityService
import com.owncloud.android.MainApp
import com.owncloud.android.datamodel.OCFile
@ -28,9 +30,9 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
import com.owncloud.android.lib.resources.files.model.RemoteFile
import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.utils.FileUtil
import java.io.File
import java.util.Optional
import javax.inject.Inject
@Suppress("TooManyFunctions")
@ -117,34 +119,44 @@ class FileUploadHelper {
failedUploads: Array<OCUpload>
): Boolean {
var showNotExistMessage = false
val (gotNetwork, _, gotWifi) = connectivityService.connectivity
val isOnline = checkConnectivity(connectivityService)
val connectivity = connectivityService.connectivity
val batteryStatus = powerManagementService.battery
val charging = batteryStatus.isCharging || batteryStatus.isFull
val isPowerSaving = powerManagementService.isPowerSavingEnabled
var uploadUser = Optional.empty<User>()
val accountNames = accountManager.accounts.filter { account ->
accountManager.getUser(account.name).isPresent
}.map { account ->
account.name
}.toHashSet()
for (failedUpload in failedUploads) {
val isDeleted = !File(failedUpload.localPath).exists()
if (isDeleted) {
showNotExistMessage = true
if (!accountNames.contains(failedUpload.accountName)) {
uploadsStorageManager.removeUpload(failedUpload)
continue
}
// 2A. for deleted files, mark as permanently failed
if (failedUpload.lastResult != UploadResult.FILE_NOT_FOUND) {
failedUpload.lastResult = UploadResult.FILE_NOT_FOUND
val uploadResult =
checkUploadConditions(failedUpload, connectivity, batteryStatus, powerManagementService, isOnline)
if (uploadResult != UploadResult.UPLOADED) {
if (failedUpload.lastResult != uploadResult) {
failedUpload.lastResult = uploadResult
uploadsStorageManager.updateUpload(failedUpload)
}
} else if (!isPowerSaving && gotNetwork &&
canUploadBeRetried(failedUpload, gotWifi, charging) && !connectivityService.isInternetWalled
) {
// 2B. for existing local files, try restarting it if possible
failedUpload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
uploadsStorageManager.updateUpload(failedUpload)
if (uploadResult == UploadResult.FILE_NOT_FOUND) {
showNotExistMessage = true
}
continue
}
failedUpload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
uploadsStorageManager.updateUpload(failedUpload)
}
accountManager.accounts.forEach {
val user = accountManager.getUser(it.name)
if (user.isPresent) backgroundJobManager.startFilesUploadJob(user.get())
accountNames.forEach { accountName ->
val user = accountManager.getUser(accountName)
if (user.isPresent) {
backgroundJobManager.startFilesUploadJob(user.get())
}
}
return showNotExistMessage
@ -216,11 +228,50 @@ class FileUploadHelper {
return upload.uploadStatus == UploadStatus.UPLOAD_IN_PROGRESS
}
private fun canUploadBeRetried(upload: OCUpload, gotWifi: Boolean, isCharging: Boolean): Boolean {
val file = File(upload.localPath)
val needsWifi = upload.isUseWifiOnly
val needsCharging = upload.isWhileChargingOnly
return file.exists() && (!needsWifi || gotWifi) && (!needsCharging || isCharging)
private fun checkConnectivity(connectivityService: ConnectivityService): Boolean {
// check that connection isn't walled off and that the server is reachable
return connectivityService.getConnectivity().isConnected && !connectivityService.isInternetWalled()
}
/**
* Dupe of [UploadFileOperation.checkConditions], needed to check if the upload should even be scheduled
* @return [UploadResult.UPLOADED] if the upload should be scheduled, otherwise the reason why it shouldn't
*/
private fun checkUploadConditions(
upload: OCUpload,
connectivity: Connectivity,
battery: BatteryStatus,
powerManagementService: PowerManagementService,
hasGeneralConnection: Boolean
): UploadResult {
var conditions = UploadResult.UPLOADED
// check that internet is available
if (!hasGeneralConnection) {
conditions = UploadResult.NETWORK_CONNECTION
}
// check that local file exists; skip the upload otherwise
if (!File(upload.localPath).exists()) {
conditions = UploadResult.FILE_NOT_FOUND
}
// check that connectivity conditions are met; delay upload otherwise
if (upload.isUseWifiOnly && (!connectivity.isWifi || connectivity.isMetered)) {
conditions = UploadResult.DELAYED_FOR_WIFI
}
// check if charging conditions are met; delay upload otherwise
if (upload.isWhileChargingOnly && !battery.isCharging && !battery.isFull) {
conditions = UploadResult.DELAYED_FOR_CHARGING
}
// check that device is not in power save mode; delay upload otherwise
if (powerManagementService.isPowerSavingEnabled) {
conditions = UploadResult.DELAYED_IN_POWER_SAVE_MODE
}
return conditions
}
@Suppress("ReturnCount")

View file

@ -130,7 +130,7 @@ class FileUploadWorker(
@Suppress("ReturnCount")
private fun retrievePagesBySortingUploadsByID(): Result {
val accountName = inputData.getString(ACCOUNT) ?: return Result.failure()
var uploadsPerPage = uploadsStorageManager.getCurrentAndPendingUploadsForAccountPageAscById(-1, accountName)
var uploadsPerPage = uploadsStorageManager.getCurrentUploadsForAccountPageAscById(-1, accountName)
val totalUploadSize = uploadsStorageManager.getTotalUploadSize(accountName)
Log_OC.d(TAG, "Total upload size: $totalUploadSize")
@ -148,7 +148,7 @@ class FileUploadWorker(
val lastId = uploadsPerPage.last().uploadId
uploadFiles(totalUploadSize, uploadsPerPage, accountName)
uploadsPerPage =
uploadsStorageManager.getCurrentAndPendingUploadsForAccountPageAscById(lastId, accountName)
uploadsStorageManager.getCurrentUploadsForAccountPageAscById(lastId, accountName)
}
if (isStopped) {

View file

@ -471,7 +471,7 @@ public class UploadsStorageManager extends Observable {
return getUploadPage(QUERY_PAGE_SIZE, afterId, true, selection, selectionArgs);
}
private String getInProgressUploadsSelection() {
private String getInProgressAndDelayedUploadsSelection() {
return "( " + ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_IN_PROGRESS.value +
OR + ProviderTableMeta.UPLOADS_LAST_RESULT +
EQUAL + UploadResult.DELAYED_FOR_WIFI.getValue() +
@ -485,7 +485,8 @@ public class UploadsStorageManager extends Observable {
}
public int getTotalUploadSize(@Nullable String... selectionArgs) {
final String selection = getInProgressUploadsSelection();
final String selection = ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_IN_PROGRESS.value +
AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL;
int totalSize = 0;
Cursor cursor = getDB().query(
@ -605,17 +606,29 @@ public class UploadsStorageManager extends Observable {
}
public OCUpload[] getCurrentAndPendingUploadsForAccount(final @NonNull String accountName) {
String inProgressUploadsSelection = getInProgressUploadsSelection();
String inProgressUploadsSelection = getInProgressAndDelayedUploadsSelection();
return getUploads(inProgressUploadsSelection, accountName);
}
public OCUpload[] getCurrentUploadsForAccount(final @NonNull String accountName) {
return getUploads(ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_IN_PROGRESS.value + AND +
ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, accountName);
}
public List<OCUpload> getCurrentUploadsForAccountPageAscById(final long afterId, final @NonNull String accountName) {
final String selection = ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_IN_PROGRESS.value +
AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL;
return getUploadPage(QUERY_PAGE_SIZE, afterId, false, selection, accountName);
}
/**
* Gets a page of uploads after <code>afterId</code>, where uploads are sorted by ascending upload id.
* <p>
* If <code>afterId</code> is -1, returns the first page
*/
public List<OCUpload> getCurrentAndPendingUploadsForAccountPageAscById(final long afterId, final @NonNull String accountName) {
final String selection = getInProgressUploadsSelection();
final String selection = getInProgressAndDelayedUploadsSelection();
return getUploadPage(QUERY_PAGE_SIZE, afterId, false, selection, accountName);
}

View file

@ -150,7 +150,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
outState.putInt(EXTRA_LOCAL_BEHAVIOUR, localBehaviour)
}
override fun conflictDecisionMade(decision: Decision) {
override fun conflictDecisionMade(decision: Decision?) {
listener?.conflictDecisionMade(decision)
}
@ -205,10 +205,10 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
if (prev != null) {
fragmentTransaction.remove(prev)
}
if (existingFile != null && storageManager.fileExists(remotePath)) {
if (existingFile != null && storageManager.fileExists(remotePath) && newFile != null) {
val dialog = ConflictsResolveDialog.newInstance(
existingFile,
newFile,
newFile!!,
userOptional.get()
)
dialog.show(fragmentTransaction, "conflictDialog")

View file

@ -1,276 +0,0 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 2012 Bartosz Przybylski <bart.p.pl@gmail.com>
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
*/
package com.owncloud.android.ui.dialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.nextcloud.client.account.User;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.utils.extensions.BundleExtensionsKt;
import com.nextcloud.utils.extensions.FileExtensionsKt;
import com.owncloud.android.R;
import com.owncloud.android.databinding.ConflictResolveDialogBinding;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.SyncedFolderProvider;
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.adapter.LocalFileListAdapter;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.theme.ViewThemeUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
/**
* Dialog which will be displayed to user upon keep-in-sync file conflict.
*/
public class ConflictsResolveDialog extends DialogFragment implements Injectable {
private ConflictResolveDialogBinding binding;
private OCFile existingFile;
private File newFile;
public OnConflictDecisionMadeListener listener;
private User user;
private final List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks = new ArrayList<>();
private MaterialButton positiveButton;
@Inject ViewThemeUtils viewThemeUtils;
@Inject SyncedFolderProvider syncedFolderProvider;
private static final String TAG = "ConflictsResolveDialog";
private static final String KEY_NEW_FILE = "file";
private static final String KEY_EXISTING_FILE = "ocfile";
private static final String KEY_USER = "user";
public enum Decision {
CANCEL,
KEEP_BOTH,
KEEP_LOCAL,
KEEP_SERVER,
}
public static ConflictsResolveDialog newInstance(OCFile existingFile, OCFile newFile, User user) {
ConflictsResolveDialog dialog = new ConflictsResolveDialog();
Bundle args = new Bundle();
args.putParcelable(KEY_EXISTING_FILE, existingFile);
File file = new File(newFile.getStoragePath());
FileExtensionsKt.logFileSize(file, TAG);
args.putSerializable(KEY_NEW_FILE, file);
args.putParcelable(KEY_USER, user);
dialog.setArguments(args);
return dialog;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
listener = (OnConflictDecisionMadeListener) context;
} catch (ClassCastException e) {
throw new ClassCastException("Activity of this dialog must implement OnConflictDecisionMadeListener");
}
}
@Override
public void onStart() {
super.onStart();
AlertDialog alertDialog = (AlertDialog) getDialog();
if (alertDialog == null) {
Toast.makeText(getContext(), "Failed to create conflict dialog", Toast.LENGTH_LONG).show();
return;
}
positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton);
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton);
positiveButton.setEnabled(false);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
existingFile = BundleExtensionsKt.getParcelableArgument(savedInstanceState, KEY_EXISTING_FILE, OCFile.class);
newFile = BundleExtensionsKt.getSerializableArgument(savedInstanceState, KEY_NEW_FILE, File.class);
user = BundleExtensionsKt.getParcelableArgument(savedInstanceState, KEY_USER, User.class);
} else if (getArguments() != null) {
existingFile = BundleExtensionsKt.getParcelableArgument(getArguments(), KEY_EXISTING_FILE, OCFile.class);
newFile = BundleExtensionsKt.getSerializableArgument(getArguments(), KEY_NEW_FILE, File.class);
user = BundleExtensionsKt.getParcelableArgument(getArguments(), KEY_USER, User.class);
} else {
Toast.makeText(getContext(), "Failed to create conflict dialog", Toast.LENGTH_LONG).show();
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
FileExtensionsKt.logFileSize(existingFile, TAG);
FileExtensionsKt.logFileSize(newFile, TAG);
outState.putParcelable(KEY_EXISTING_FILE, existingFile);
outState.putSerializable(KEY_NEW_FILE, newFile);
outState.putParcelable(KEY_USER, user);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Inflate the layout for the dialog
binding = ConflictResolveDialogBinding.inflate(requireActivity().getLayoutInflater());
viewThemeUtils.platform.themeCheckbox(binding.newCheckbox);
viewThemeUtils.platform.themeCheckbox(binding.existingCheckbox);
// Build the dialog
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
builder.setView(binding.getRoot())
.setPositiveButton(R.string.common_ok, (dialog, which) -> {
if (listener != null) {
if (binding.newCheckbox.isChecked() && binding.existingCheckbox.isChecked()) {
listener.conflictDecisionMade(Decision.KEEP_BOTH);
} else if (binding.newCheckbox.isChecked()) {
listener.conflictDecisionMade(Decision.KEEP_LOCAL);
} else if (binding.existingCheckbox.isChecked()) {
listener.conflictDecisionMade(Decision.KEEP_SERVER);
} // else do nothing
}
})
.setNegativeButton(R.string.common_cancel, (dialog, which) -> {
if (listener != null) {
listener.conflictDecisionMade(Decision.CANCEL);
}
})
.setTitle(String.format(getString(R.string.conflict_file_headline), existingFile.getFileName()));
File parentFile = new File(existingFile.getRemotePath()).getParentFile();
if (parentFile != null) {
binding.in.setText(String.format(getString(R.string.in_folder), parentFile.getAbsolutePath()));
} else {
binding.in.setVisibility(View.GONE);
}
// set info for new file
binding.newSize.setText(DisplayUtils.bytesToHumanReadable(newFile.length()));
binding.newTimestamp.setText(DisplayUtils.getRelativeTimestamp(getContext(), newFile.lastModified()));
binding.newThumbnail.setTag(newFile.hashCode());
LocalFileListAdapter.setThumbnail(newFile,
binding.newThumbnail,
getContext(),
viewThemeUtils);
// set info for existing file
binding.existingSize.setText(DisplayUtils.bytesToHumanReadable(existingFile.getFileLength()));
binding.existingTimestamp.setText(DisplayUtils.getRelativeTimestamp(getContext(),
existingFile.getModificationTimestamp()));
binding.existingThumbnail.setTag(existingFile.getFileId());
DisplayUtils.setThumbnail(existingFile,
binding.existingThumbnail,
user,
new FileDataStorageManager(user,
requireContext().getContentResolver()),
asyncTasks,
false,
getContext(),
null,
syncedFolderProvider.getPreferences(),
viewThemeUtils,
syncedFolderProvider);
View.OnClickListener checkBoxClickListener = v ->
positiveButton.setEnabled(binding.newCheckbox.isChecked() || binding.existingCheckbox.isChecked());
binding.newCheckbox.setOnClickListener(checkBoxClickListener);
binding.existingCheckbox.setOnClickListener(checkBoxClickListener);
binding.newFileContainer.setOnClickListener(v -> {
binding.newCheckbox.setChecked(!binding.newCheckbox.isChecked());
positiveButton.setEnabled(binding.newCheckbox.isChecked() || binding.existingCheckbox.isChecked());
});
binding.existingFileContainer.setOnClickListener(v -> {
binding.existingCheckbox.setChecked(!binding.existingCheckbox.isChecked());
positiveButton.setEnabled(binding.newCheckbox.isChecked() || binding.existingCheckbox.isChecked());
});
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.existingFileContainer.getContext(), builder);
return builder.create();
}
public void showDialog(AppCompatActivity activity) {
Fragment prev = activity.getSupportFragmentManager().findFragmentByTag("dialog");
FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction();
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack(null);
this.show(ft, "dialog");
}
@Override
public void onCancel(@NonNull DialogInterface dialog) {
if (listener != null) {
listener.conflictDecisionMade(Decision.CANCEL);
}
}
public interface OnConflictDecisionMadeListener {
void conflictDecisionMade(Decision decision);
}
@Override
public void onStop() {
super.onStop();
for (ThumbnailsCacheManager.ThumbnailGenerationTask task : asyncTasks) {
if (task != null) {
task.cancel(true);
if (task.getGetMethod() != null) {
Log_OC.d(this, "cancel: abort get method directly");
task.getGetMethod().abort();
}
}
}
asyncTasks.clear();
}
}

View file

@ -0,0 +1,270 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 2012 Bartosz Przybylski <bart.p.pl@gmail.com>
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
*/
package com.owncloud.android.ui.dialog
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.client.account.User
import com.nextcloud.client.di.Injectable
import com.nextcloud.utils.extensions.getParcelableArgument
import com.nextcloud.utils.extensions.getSerializableArgument
import com.nextcloud.utils.extensions.logFileSize
import com.owncloud.android.R
import com.owncloud.android.databinding.ConflictResolveDialogBinding
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.adapter.LocalFileListAdapter
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.theme.ViewThemeUtils
import java.io.File
import javax.inject.Inject
/**
* Dialog which will be displayed to user upon keep-in-sync file conflict.
*/
class ConflictsResolveDialog : DialogFragment(), Injectable {
private lateinit var binding: ConflictResolveDialogBinding
private var existingFile: OCFile? = null
private var newFile: File? = null
var listener: OnConflictDecisionMadeListener? = null
private var user: User? = null
private val asyncTasks: MutableList<ThumbnailGenerationTask> = ArrayList()
private var positiveButton: MaterialButton? = null
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var syncedFolderProvider: SyncedFolderProvider
enum class Decision {
CANCEL,
KEEP_BOTH,
KEEP_LOCAL,
KEEP_SERVER
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as OnConflictDecisionMadeListener
} catch (e: ClassCastException) {
throw ClassCastException("Activity of this dialog must implement OnConflictDecisionMadeListener")
}
}
override fun onStart() {
super.onStart()
val alertDialog = dialog as AlertDialog?
if (alertDialog == null) {
Toast.makeText(context, "Failed to create conflict dialog", Toast.LENGTH_LONG).show()
return
}
positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton
val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton
positiveButton?.let {
viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it)
}
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton)
positiveButton?.isEnabled = false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
existingFile = savedInstanceState.getParcelableArgument(KEY_EXISTING_FILE, OCFile::class.java)
newFile = savedInstanceState.getSerializableArgument(KEY_NEW_FILE, File::class.java)
user = savedInstanceState.getParcelableArgument(KEY_USER, User::class.java)
} else if (arguments != null) {
existingFile = arguments.getParcelableArgument(KEY_EXISTING_FILE, OCFile::class.java)
newFile = arguments.getSerializableArgument(KEY_NEW_FILE, File::class.java)
user = arguments.getParcelableArgument(KEY_USER, User::class.java)
} else {
Toast.makeText(context, "Failed to create conflict dialog", Toast.LENGTH_LONG).show()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
existingFile.logFileSize(TAG)
newFile.logFileSize(TAG)
outState.putParcelable(KEY_EXISTING_FILE, existingFile)
outState.putSerializable(KEY_NEW_FILE, newFile)
outState.putParcelable(KEY_USER, user)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = ConflictResolveDialogBinding.inflate(requireActivity().layoutInflater)
viewThemeUtils.platform.themeCheckbox(binding.newCheckbox)
viewThemeUtils.platform.themeCheckbox(binding.existingCheckbox)
val builder = MaterialAlertDialogBuilder(requireActivity())
builder.setView(binding.root)
.setPositiveButton(R.string.common_ok) { _: DialogInterface?, _: Int ->
if (binding.newCheckbox.isChecked && binding.existingCheckbox.isChecked) {
listener?.conflictDecisionMade(Decision.KEEP_BOTH)
} else if (binding.newCheckbox.isChecked) {
listener?.conflictDecisionMade(Decision.KEEP_LOCAL)
} else if (binding.existingCheckbox.isChecked) {
listener?.conflictDecisionMade(Decision.KEEP_SERVER)
}
}
.setNegativeButton(R.string.common_cancel) { _: DialogInterface?, _: Int ->
listener?.conflictDecisionMade(Decision.CANCEL)
}
.setTitle(String.format(getString(R.string.conflict_file_headline), existingFile?.fileName))
setupUI()
setOnClickListeners()
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.existingFileContainer.context, builder)
return builder.create()
}
private fun setupUI() {
val parentFile = existingFile?.remotePath?.let { File(it).parentFile }
if (parentFile != null) {
binding.`in`.text = String.format(getString(R.string.in_folder), parentFile.absolutePath)
} else {
binding.`in`.visibility = View.GONE
}
// set info for new file
binding.newSize.text = newFile?.length()?.let { DisplayUtils.bytesToHumanReadable(it) }
binding.newTimestamp.text = newFile?.lastModified()?.let { DisplayUtils.getRelativeTimestamp(context, it) }
binding.newThumbnail.tag = newFile.hashCode()
LocalFileListAdapter.setThumbnail(
newFile,
binding.newThumbnail,
context,
viewThemeUtils
)
// set info for existing file
binding.existingSize.text = existingFile?.fileLength?.let { DisplayUtils.bytesToHumanReadable(it) }
binding.existingTimestamp.text = existingFile?.modificationTimestamp?.let {
DisplayUtils.getRelativeTimestamp(
context,
it
)
}
binding.existingThumbnail.tag = existingFile?.fileId
DisplayUtils.setThumbnail(
existingFile,
binding.existingThumbnail,
user,
FileDataStorageManager(
user,
requireContext().contentResolver
),
asyncTasks,
false,
context,
null,
syncedFolderProvider.preferences,
viewThemeUtils,
syncedFolderProvider
)
}
private fun setOnClickListeners() {
val checkBoxClickListener = View.OnClickListener {
positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
}
binding.newCheckbox.setOnClickListener(checkBoxClickListener)
binding.existingCheckbox.setOnClickListener(checkBoxClickListener)
binding.newFileContainer.setOnClickListener {
binding.newCheckbox.isChecked = !binding.newCheckbox.isChecked
positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
}
binding.existingFileContainer.setOnClickListener {
binding.existingCheckbox.isChecked = !binding.existingCheckbox.isChecked
positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
}
}
fun showDialog(activity: AppCompatActivity) {
val prev = activity.supportFragmentManager.findFragmentByTag("dialog")
val ft = activity.supportFragmentManager.beginTransaction()
if (prev != null) {
ft.remove(prev)
}
ft.addToBackStack(null)
show(ft, "dialog")
}
override fun onCancel(dialog: DialogInterface) {
listener?.conflictDecisionMade(Decision.CANCEL)
}
fun interface OnConflictDecisionMadeListener {
fun conflictDecisionMade(decision: Decision?)
}
override fun onStop() {
super.onStop()
for (task in asyncTasks) {
task.cancel(true)
Log_OC.d(this, "cancel: abort get method directly")
task.getMethod?.abort()
}
asyncTasks.clear()
}
companion object {
private const val TAG = "ConflictsResolveDialog"
private const val KEY_NEW_FILE = "file"
private const val KEY_EXISTING_FILE = "ocfile"
private const val KEY_USER = "user"
@JvmStatic
fun newInstance(existingFile: OCFile?, newFile: OCFile, user: User?): ConflictsResolveDialog {
val file = File(newFile.storagePath)
file.logFileSize(TAG)
val bundle = Bundle().apply {
putParcelable(KEY_EXISTING_FILE, existingFile)
putSerializable(KEY_NEW_FILE, file)
putParcelable(KEY_USER, user)
}
return ConflictsResolveDialog().apply {
arguments = bundle
}
}
}
}

View file

@ -1,201 +0,0 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2018 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2018 Jessie Chatham Spencer <jessie@teainspace.com>
* SPDX-FileCopyrightText: 2016-2022 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 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.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.ActionMode;
import com.google.android.material.button.MaterialButton;
import com.nextcloud.client.di.Injectable;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.ui.activity.ComponentsGetter;
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import java.util.ArrayList;
import java.util.Collection;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
/**
* Dialog requiring confirmation before removing a collection of given OCFiles.
* Triggers the removal according to the user response.
*/
public class RemoveFilesDialogFragment extends ConfirmationDialogFragment implements
ConfirmationDialogFragmentListener, Injectable {
private static final int SINGLE_SELECTION = 1;
private static final String ARG_TARGET_FILES = "TARGET_FILES";
private Collection<OCFile> mTargetFiles;
private ActionMode actionMode;
/**
* Public factory method to create new RemoveFilesDialogFragment instances.
*
* @param files Files to remove.
* @param actionMode ActionMode to finish on confirmation
* @return Dialog ready to show.
*/
public static RemoveFilesDialogFragment newInstance(ArrayList<OCFile> files, ActionMode actionMode) {
RemoveFilesDialogFragment dialogFragment = newInstance(files);
dialogFragment.setActionMode(actionMode);
return dialogFragment;
}
/**
* Public factory method to create new RemoveFilesDialogFragment instances.
*
* @param files Files to remove.
* @return Dialog ready to show.
*/
public static RemoveFilesDialogFragment newInstance(ArrayList<OCFile> files) {
RemoveFilesDialogFragment frag = new RemoveFilesDialogFragment();
Bundle args = new Bundle();
int messageStringId;
boolean containsFolder = false;
boolean containsDown = false;
for (OCFile file: files) {
containsFolder |= file.isFolder();
containsDown |= file.isDown();
}
if (files.size() == SINGLE_SELECTION) {
// choose message for a single file
OCFile file = files.get(0);
messageStringId = file.isFolder() ?
R.string.confirmation_remove_folder_alert :
R.string.confirmation_remove_file_alert;
} else {
// choose message for more than one file
messageStringId = containsFolder ?
R.string.confirmation_remove_folders_alert :
R.string.confirmation_remove_files_alert;
}
args.putInt(ARG_MESSAGE_RESOURCE_ID, messageStringId);
if (files.size() == SINGLE_SELECTION) {
args.putStringArray(ARG_MESSAGE_ARGUMENTS, new String[] { files.get(0).getFileName() } );
}
args.putInt(ARG_POSITIVE_BTN_RES, R.string.file_delete);
if (containsFolder || containsDown) {
args.putInt(ARG_NEGATIVE_BTN_RES, R.string.confirmation_remove_local);
}
args.putInt(ARG_NEUTRAL_BTN_RES, R.string.file_keep);
args.putParcelableArrayList(ARG_TARGET_FILES, files);
frag.setArguments(args);
return frag;
}
/**
* Convenience factory method to create new RemoveFilesDialogFragment instances for a single file
*
* @param file File to remove.
* @return Dialog ready to show.
*/
public static RemoveFilesDialogFragment newInstance(OCFile file) {
ArrayList<OCFile> list = new ArrayList<>();
list.add(file);
return newInstance(list);
}
@Override
public void onStart() {
super.onStart();
AlertDialog alertDialog = (AlertDialog) getDialog();
if (alertDialog != null) {
MaterialButton positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton);
MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton);
MaterialButton neutralButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
if (neutralButton != null) {
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(neutralButton);
}
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
Bundle arguments = getArguments();
if (arguments == null) {
return dialog;
}
mTargetFiles = arguments.getParcelableArrayList(ARG_TARGET_FILES);
setOnConfirmationListener(this);
return dialog;
}
/**
* Performs the removal of the target file, both locally and in the server and
* finishes the supplied ActionMode if one was given.
*/
@Override
public void onConfirmation(String callerTag) {
removeFiles(false);
}
/**
* Performs the removal of the local copy of the target file
*/
@Override
public void onCancel(String callerTag) {
removeFiles(true);
}
private void removeFiles(boolean onlyLocalCopy) {
ComponentsGetter cg = (ComponentsGetter) getActivity();
if (cg != null) {
cg.getFileOperationsHelper().removeFiles(mTargetFiles, onlyLocalCopy, false);
}
finishActionMode();
}
@Override
public void onNeutral(String callerTag) {
// nothing to do here
}
private void setActionMode(ActionMode actionMode) {
this.actionMode = actionMode;
}
/**
* This is used when finishing an actionMode,
* for example if we want to exit the selection mode
* after deleting the target files.
*/
private void finishActionMode() {
if (actionMode != null) {
actionMode.finish();
}
}
}

View file

@ -0,0 +1,175 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2018 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2018 Jessie Chatham Spencer <jessie@teainspace.com>
* SPDX-FileCopyrightText: 2016-2022 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 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.dialog
import android.app.Dialog
import android.os.Bundle
import android.view.ActionMode
import androidx.appcompat.app.AlertDialog
import com.google.android.material.button.MaterialButton
import com.nextcloud.client.di.Injectable
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener
/**
* Dialog requiring confirmation before removing a collection of given OCFiles.
* Triggers the removal according to the user response.
*/
class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDialogFragmentListener, Injectable {
private var mTargetFiles: Collection<OCFile>? = null
private var actionMode: ActionMode? = null
override fun onStart() {
super.onStart()
val alertDialog = dialog as AlertDialog? ?: return
val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton
positiveButton?.let {
viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
}
val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton
negativeButton?.let {
viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
}
val neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) as? MaterialButton
neutralButton?.let {
viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(neutralButton)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
val arguments = arguments ?: return dialog
mTargetFiles = arguments.getParcelableArrayList(ARG_TARGET_FILES)
setOnConfirmationListener(this)
return dialog
}
/**
* Performs the removal of the target file, both locally and in the server and
* finishes the supplied ActionMode if one was given.
*/
override fun onConfirmation(callerTag: String?) {
removeFiles(false)
}
/**
* Performs the removal of the local copy of the target file
*/
override fun onCancel(callerTag: String?) {
removeFiles(true)
}
private fun removeFiles(onlyLocalCopy: Boolean) {
val cg = activity as ComponentsGetter?
cg?.fileOperationsHelper?.removeFiles(mTargetFiles, onlyLocalCopy, false)
finishActionMode()
}
override fun onNeutral(callerTag: String?) {
// nothing to do here
}
private fun setActionMode(actionMode: ActionMode?) {
this.actionMode = actionMode
}
/**
* This is used when finishing an actionMode,
* for example if we want to exit the selection mode
* after deleting the target files.
*/
private fun finishActionMode() {
actionMode?.finish()
}
companion object {
private const val SINGLE_SELECTION = 1
private const val ARG_TARGET_FILES = "TARGET_FILES"
@JvmStatic
fun newInstance(files: ArrayList<OCFile>, actionMode: ActionMode?): RemoveFilesDialogFragment {
return newInstance(files).apply {
setActionMode(actionMode)
}
}
@JvmStatic
fun newInstance(files: ArrayList<OCFile>): RemoveFilesDialogFragment {
val messageStringId: Int
var containsFolder = false
var containsDown = false
for (file in files) {
containsFolder = containsFolder or file.isFolder
containsDown = containsDown or file.isDown
}
if (files.size == SINGLE_SELECTION) {
val file = files[0]
messageStringId =
if (file.isFolder) {
R.string.confirmation_remove_folder_alert
} else {
R.string.confirmation_remove_file_alert
}
} else {
messageStringId =
if (containsFolder) {
R.string.confirmation_remove_folders_alert
} else {
R.string.confirmation_remove_files_alert
}
}
val bundle = Bundle().apply {
putInt(ARG_MESSAGE_RESOURCE_ID, messageStringId)
if (files.size == SINGLE_SELECTION) {
putStringArray(
ARG_MESSAGE_ARGUMENTS,
arrayOf(
files[0].fileName
)
)
}
putInt(ARG_POSITIVE_BTN_RES, R.string.file_delete)
if (containsFolder || containsDown) {
putInt(ARG_NEGATIVE_BTN_RES, R.string.confirmation_remove_local)
}
putInt(ARG_NEUTRAL_BTN_RES, R.string.file_keep)
putParcelableArrayList(ARG_TARGET_FILES, files)
}
return RemoveFilesDialogFragment().apply {
arguments = bundle
}
}
@JvmStatic
fun newInstance(file: OCFile): RemoveFilesDialogFragment {
val list = ArrayList<OCFile>().apply {
add(file)
}
return newInstance(list)
}
}
}

View file

@ -1,171 +0,0 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2018 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 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.dialog;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.nextcloud.utils.extensions.BundleExtensionsKt;
import com.owncloud.android.R;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.activity.CopyToClipboardActivity;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
/**
* Dialog showing a list activities able to resolve a given Intent,
* filtering out the activities matching give package names.
*/
public class ShareLinkToDialog extends DialogFragment {
private final static String TAG = ShareLinkToDialog.class.getSimpleName();
private final static String ARG_INTENT = ShareLinkToDialog.class.getSimpleName() +
".ARG_INTENT";
private final static String ARG_PACKAGES_TO_EXCLUDE = ShareLinkToDialog.class.getSimpleName() +
".ARG_PACKAGES_TO_EXCLUDE";
private ActivityAdapter mAdapter;
private Intent mIntent;
public static ShareLinkToDialog newInstance(Intent intent, String... packagesToExclude) {
ShareLinkToDialog f = new ShareLinkToDialog();
Bundle args = new Bundle();
args.putParcelable(ARG_INTENT, intent);
args.putStringArray(ARG_PACKAGES_TO_EXCLUDE, packagesToExclude);
f.setArguments(args);
return f;
}
public ShareLinkToDialog() {
super();
Log_OC.d(TAG, "constructor");
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
mIntent = BundleExtensionsKt.getParcelableArgument(getArguments(), ARG_INTENT, Intent.class);
String[] packagesToExclude = getArguments().getStringArray(ARG_PACKAGES_TO_EXCLUDE);
List<String> packagesToExcludeList = Arrays.asList(packagesToExclude != null ?
packagesToExclude : new String[0]);
PackageManager pm = getActivity().getPackageManager();
List<ResolveInfo> activities = pm.queryIntentActivities(mIntent, PackageManager.MATCH_DEFAULT_ONLY);
Iterator<ResolveInfo> it = activities.iterator();
ResolveInfo resolveInfo;
while (it.hasNext()) {
resolveInfo = it.next();
if (packagesToExcludeList.contains(resolveInfo.activityInfo.packageName.toLowerCase(Locale.ROOT))) {
it.remove();
}
}
boolean sendAction = mIntent.getBooleanExtra(Intent.ACTION_SEND, false);
if (!sendAction) {
// add activity for copy to clipboard
Intent copyToClipboardIntent = new Intent(getActivity(), CopyToClipboardActivity.class);
List<ResolveInfo> copyToClipboard = pm.queryIntentActivities(copyToClipboardIntent, 0);
if (!copyToClipboard.isEmpty()) {
activities.add(copyToClipboard.get(0));
}
}
Collections.sort(activities, new ResolveInfo.DisplayNameComparator(pm));
mAdapter = new ActivityAdapter(getActivity(), pm, activities);
return createSelector(sendAction);
}
private AlertDialog createSelector(final boolean sendAction) {
int titleId;
if (sendAction) {
titleId = R.string.activity_chooser_send_file_title;
} else {
titleId = R.string.activity_chooser_title;
}
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
.setTitle(titleId)
.setAdapter(mAdapter, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Add the information of the chosen activity to the intent to send
ResolveInfo chosen = mAdapter.getItem(which);
ActivityInfo actInfo = chosen.activityInfo;
ComponentName name=new ComponentName(
actInfo.applicationInfo.packageName,
actInfo.name);
mIntent.setComponent(name);
// Send the file
getActivity().startActivity(mIntent);
}
});
return builder.create();
}
class ActivityAdapter extends ArrayAdapter<ResolveInfo> {
private PackageManager mPackageManager;
ActivityAdapter(Context context, PackageManager pm, List<ResolveInfo> apps) {
super(context, R.layout.activity_row, apps);
this.mPackageManager = pm;
}
@Override
public @NonNull View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = convertView;
if (view == null) {
view = newView(parent);
}
bindView(position, view);
return view;
}
private View newView(ViewGroup parent) {
return((LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE)).
inflate(R.layout.activity_row, parent, false);
}
private void bindView(int position, View row) {
TextView label = row.findViewById(R.id.title);
label.setText(getItem(position).loadLabel(mPackageManager));
ImageView icon = row.findViewById(R.id.icon);
icon.setImageDrawable(getItem(position).loadIcon(mPackageManager));
}
}
}

View file

@ -0,0 +1,150 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2018 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 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.dialog
import android.app.Dialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.utils.extensions.getParcelableArgument
import com.owncloud.android.R
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.activity.CopyToClipboardActivity
import java.util.Collections
/**
* Dialog showing a list activities able to resolve a given Intent,
* filtering out the activities matching give package names.
*/
class ShareLinkToDialog : DialogFragment() {
private var mAdapter: ActivityAdapter? = null
private var mIntent: Intent? = null
init {
Log_OC.d(TAG, "constructor")
}
@Suppress("SpreadOperator")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
mIntent = arguments.getParcelableArgument(ARG_INTENT, Intent::class.java) ?: throw NullPointerException()
val packagesToExclude = arguments?.getStringArray(ARG_PACKAGES_TO_EXCLUDE)
val packagesToExcludeList = listOf(*packagesToExclude ?: arrayOfNulls(0))
val pm = activity?.packageManager ?: throw NullPointerException()
val activities = pm.queryIntentActivities(mIntent!!, PackageManager.MATCH_DEFAULT_ONLY)
val it = activities.iterator()
var resolveInfo: ResolveInfo
while (it.hasNext()) {
resolveInfo = it.next()
if (packagesToExcludeList.contains(resolveInfo.activityInfo.packageName.lowercase())) {
it.remove()
}
}
val sendAction = mIntent?.getBooleanExtra(Intent.ACTION_SEND, false)
if (sendAction == false) {
// add activity for copy to clipboard
val copyToClipboardIntent = Intent(requireActivity(), CopyToClipboardActivity::class.java)
val copyToClipboard = pm.queryIntentActivities(copyToClipboardIntent, 0)
if (copyToClipboard.isNotEmpty()) {
activities.add(copyToClipboard[0])
}
}
Collections.sort(activities, ResolveInfo.DisplayNameComparator(pm))
mAdapter = ActivityAdapter(requireActivity(), pm, activities)
return createSelector(sendAction ?: false)
}
private fun createSelector(sendAction: Boolean): AlertDialog {
val titleId = if (sendAction) {
R.string.activity_chooser_send_file_title
} else {
R.string.activity_chooser_title
}
return MaterialAlertDialogBuilder(requireActivity())
.setTitle(titleId)
.setAdapter(mAdapter) { _, which ->
// Add the information of the chosen activity to the intent to send
val chosen = mAdapter?.getItem(which)
val actInfo = chosen?.activityInfo ?: return@setAdapter
val name = ComponentName(
actInfo.applicationInfo.packageName,
actInfo.name
)
mIntent?.setComponent(name)
activity?.startActivity(mIntent)
}
.create()
}
internal inner class ActivityAdapter(
context: Context,
private val mPackageManager: PackageManager,
apps: List<ResolveInfo>
) : ArrayAdapter<ResolveInfo?>(context, R.layout.activity_row, apps) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: newView(parent)
bindView(position, view)
return view
}
private fun newView(parent: ViewGroup): View {
return (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.activity_row, parent, false)
}
private fun bindView(position: Int, row: View) {
row.findViewById<TextView>(R.id.title).run {
text = getItem(position)?.loadLabel(mPackageManager)
}
row.findViewById<ImageView>(R.id.icon).run {
setImageDrawable(getItem(position)?.loadIcon(mPackageManager))
}
}
}
companion object {
private val TAG: String = ShareLinkToDialog::class.java.simpleName
private val ARG_INTENT = ShareLinkToDialog::class.java.simpleName +
".ARG_INTENT"
private val ARG_PACKAGES_TO_EXCLUDE = ShareLinkToDialog::class.java.simpleName +
".ARG_PACKAGES_TO_EXCLUDE"
@JvmStatic
fun newInstance(intent: Intent?, vararg packagesToExclude: String?): ShareLinkToDialog {
val bundle = Bundle().apply {
putParcelable(ARG_INTENT, intent)
putStringArray(ARG_PACKAGES_TO_EXCLUDE, packagesToExclude)
}
return ShareLinkToDialog().apply {
arguments = bundle
}
}
}
}

View file

@ -108,7 +108,7 @@ public final class FilesSyncHelper {
}
}
private static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder) {
public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder) {
final Context context = MainApp.getAppContext();
final ContentResolver contentResolver = context.getContentResolver();
@ -146,18 +146,6 @@ public final class FilesSyncHelper {
}
}
public static void insertAllDBEntries(SyncedFolder syncedFolder,
PowerManagementService powerManagementService) {
if (syncedFolder.isEnabled() &&
!(syncedFolder.isChargingOnly() &&
!powerManagementService.getBattery().isCharging() &&
!powerManagementService.getBattery().isFull()
)
) {
insertAllDBEntriesForSyncedFolder(syncedFolder);
}
}
public static void insertChangedEntries(SyncedFolder syncedFolder,
String[] changedFiles) {
final ContentResolver contentResolver = MainApp.getAppContext().getContentResolver();
@ -248,66 +236,13 @@ public final class FilesSyncHelper {
final UserAccountManager accountManager,
final ConnectivityService connectivityService,
final PowerManagementService powerManagementService) {
boolean accountExists;
boolean whileChargingOnly = true;
boolean useWifiOnly = true;
// Make all in progress downloads failed to restart upload worker
uploadsStorageManager.failInProgressUploads(UploadResult.SERVICE_INTERRUPTED);
OCUpload[] failedUploads = uploadsStorageManager.getFailedUploads();
for (OCUpload failedUpload : failedUploads) {
accountExists = false;
if (!failedUpload.isWhileChargingOnly()) {
whileChargingOnly = false;
}
if (!failedUpload.isUseWifiOnly()) {
useWifiOnly = false;
}
// check if accounts still exists
for (Account account : accountManager.getAccounts()) {
if (account.name.equals(failedUpload.getAccountName())) {
accountExists = true;
break;
}
}
if (!accountExists) {
uploadsStorageManager.removeUpload(failedUpload);
}
}
failedUploads = uploadsStorageManager.getFailedUploads();
if (failedUploads.length == 0) {
//nothing to do
return;
}
if (whileChargingOnly) {
final BatteryStatus batteryStatus = powerManagementService.getBattery();
final boolean charging = batteryStatus.isCharging() || batteryStatus.isFull();
if (!charging) {
//all uploads requires charging
return;
}
}
if (useWifiOnly && !connectivityService.getConnectivity().isWifi()) {
//all uploads requires wifi
return;
}
new Thread(() -> {
if (connectivityService.getConnectivity().isConnected()) {
FileUploadHelper.Companion.instance().retryFailedUploads(
uploadsStorageManager,
connectivityService,
accountManager,
powerManagementService);
}
FileUploadHelper.Companion.instance().retryFailedUploads(
uploadsStorageManager,
connectivityService,
accountManager,
powerManagementService);
}).start();
}

View file

@ -146,7 +146,7 @@
<string name="common_ok">Aceptar</string>
<string name="common_pending">Pendente</string>
<string name="common_remove">Eliminar</string>
<string name="common_rename">Renomear</string>
<string name="common_rename">Cambiar o nome</string>
<string name="common_save">Gardar</string>
<string name="common_send">Enviar</string>
<string name="common_share">Compartir</string>
@ -407,14 +407,14 @@
<string name="file_migration_waiting_for_unfinished_sync">Agardando que rematen as sincronizacións…</string>
<string name="file_not_found">Non se atopou o ficheiro</string>
<string name="file_not_synced">Non foi posíbel sincronizar o ficheiro. Amosase a última versión dispoñíbel.</string>
<string name="file_rename">Renomear</string>
<string name="file_rename">Cambiar o nome</string>
<string name="file_upload_worker_same_file_already_exists">Xa existe o mesmo ficheiro, non se detectou ningún conflito</string>
<string name="file_version_restored_error">Produciuse un erro ao restaurar a versión do ficheiro</string>
<string name="file_version_restored_successfully">Versión do ficheiro restaurada satisfactoriamente.</string>
<string name="filedetails_details">Detalles</string>
<string name="filedetails_download">Descargar</string>
<string name="filedetails_export">Exportar</string>
<string name="filedetails_renamed_in_upload_msg">O ficheiro foi renomeado a %1$s durante o envío</string>
<string name="filedetails_renamed_in_upload_msg">Cambiouselle o nome do ficheiro a %1$s durante o envío</string>
<string name="filedetails_sync_file">Sincronizar</string>
<string name="filedisplay_no_file_selected">Non seleccionou ningún ficheiro</string>
<string name="filename_empty">O nome de ficheiro non pode estar baleiro</string>
@ -437,7 +437,7 @@
<string name="forbidden_permissions_create">para crear este ficheiro</string>
<string name="forbidden_permissions_delete">para eliminar este ficheiro</string>
<string name="forbidden_permissions_move">para mover este ficheiro</string>
<string name="forbidden_permissions_rename">para renomear este ficheiro</string>
<string name="forbidden_permissions_rename">para cambiarlle o nome a este ficheiro</string>
<string name="foreground_service_upload">Enviando ficheiros…</string>
<string name="foreign_files_fail">Non foi posíbel mover algúns ficheiros</string>
<string name="foreign_files_local_text">Local: %1$s</string>
@ -693,8 +693,8 @@
<string name="remove_push_notification">Retirar</string>
<string name="remove_success_msg">Eliminado</string>
<string name="rename_dialog_title">Introduza un nome novo</string>
<string name="rename_local_fail_msg">Non foi posíbel renomear a copia local, ténteo cun un nome diferente</string>
<string name="rename_server_fail_msg">Non foi posíbel renomear, o nome xa está ocupado</string>
<string name="rename_local_fail_msg">Non foi posíbel cambiarlle o nome a copia local, ténteo cun un nome diferente</string>
<string name="rename_server_fail_msg">Non foi posíbel cambiarlle o nome, o nome xa está ocupado</string>
<string name="request_account_deletion">Solicitar a eliminación da conta </string>
<string name="request_account_deletion_button">Solicitat a eliminación</string>
<string name="request_account_deletion_details">Solicitar a eliminación definitiva da conta polo provedor de servizos</string>

View file

@ -13,12 +13,12 @@ buildscript {
androidLibraryVersion ="50ef03422e17bbcbdbbdc868a27749f548488ec6"
androidPluginVersion = '8.5.0'
androidxMediaVersion = '1.3.1'
androidxTestVersion = "1.5.0"
androidxTestVersion = "1.6.1"
appCompatVersion = '1.7.0'
checkerVersion = "3.21.2"
daggerVersion = "2.51.1"
documentScannerVersion = "1.1.1"
espressoVersion = "3.5.1"
espressoVersion = "3.6.1"
fidoVersion = "4.1.0-patch2"
jacoco_version = '0.8.12'
kotlin_version = '2.0.0'

View file

@ -248,6 +248,7 @@
<trusting group="androidx.transition" name="transition" version="1.5.0"/>
<trusting group="androidx.webkit" name="webkit" version="1.11.0"/>
<trusting group="^androidx[.]compose($|([.].*))" regex="true"/>
<trusting group="^androidx[.]test($|([.].*))" regex="true"/>
<trusting group="^com[.]android($|([.].*))" regex="true"/>
</trusted-key>
<trusted-key id="A6D6C97108B8585F91B158748671A8DF71296252" group="^com[.]squareup($|([.].*))" regex="true"/>
@ -379,6 +380,14 @@
<sha256 value="b7730754793e2fa510ddb10b7514e65f8706e4ec4b100acf7e4215f0bd5519b4" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity" version="1.2.4">
<artifact name="activity-1.2.4.aar">
<sha256 value="ae8e9c7de57e387d2ad90e73f3a5a5dfd502bd4f034c1dccfdb3506d1d2df81a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="activity-1.2.4.module">
<sha256 value="20f5634f76717910e5b4299909d6998a8078f106bdb9e15b2dd75fcfc1bd42b5" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity" version="1.5.1">
<artifact name="activity-1.5.1.module">
<sha256 value="c4317fb95ce2716b88f1f4a5334795efda084097a3f2447ffccb10a412c85be4" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -595,6 +604,14 @@
<sha256 value="2670902fc6c26047c42e0f60ee34656fa071841db370de958198413b5ab58fc3" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.3.1">
<artifact name="appcompat-1.3.1.aar">
<sha256 value="959b1daefe40d5e7eed1022f97730b22bc5fd52e6a6083eba284fa86c2971303" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="appcompat-1.3.1.module">
<sha256 value="a1cfeb760e51aa3363a72796ed5bed7ba501d94df342f0a9160b67d4f213cc5e" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.6.1">
<artifact name="appcompat-1.6.1.aar">
<sha256 value="7ea5573b93ababd3bd32312451c6ea48a662b03a140dda81aebe75776a20a422" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -619,6 +636,14 @@
<sha256 value="75d9865bc6b0f779043b4f6523a944bd2ccb7526f5c1f5ef24624ac78dec3bd3" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat-resources" version="1.3.1">
<artifact name="appcompat-resources-1.3.1.aar">
<sha256 value="e3306cd3e9a19a28a5de5ec3b379580f237c4d81c15c3d795404be9291890a75" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="appcompat-resources-1.3.1.module">
<sha256 value="757bb47d84171d055fe67cf6b88efc7a42c05bdb673b8a89a1a3c25a589d4848" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat-resources" version="1.6.1">
<artifact name="appcompat-resources-1.6.1.aar">
<sha256 value="db915dbf49357863de1669ff9fdd8e9008d65fe357af6cce9ae63043ad5f6617" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -1004,6 +1029,14 @@
<sha256 value="77639a0b051e22510bad93affcea0ebd781ef124bf9b7621a95749937bcfcdfd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.concurrent" name="concurrent-futures-ktx" version="1.1.0">
<artifact name="concurrent-futures-ktx-1.1.0.jar">
<sha256 value="1968bf52039e38636aa6f114cd17d7256919d1e8997417716fef9d1da1f24d85" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="concurrent-futures-ktx-1.1.0.module">
<sha256 value="69b79724566d49140846700690b8d2165231c577e93e66726a443e8f976bbe19" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.constraintlayout" name="constraintlayout" version="2.0.1">
<artifact name="constraintlayout-2.0.1.aar">
<sha256 value="ec15b5d4a2eff07888bc1499ce2e2c6efe24c0ed60cc57b08c9dc4b6fd3c2189" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -1112,6 +1145,14 @@
<sha256 value="988f820899d5a4982e5c878ca1cd417970ace332ea2ff72f5be19b233fa0e788" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.8.0">
<artifact name="core-1.8.0.aar">
<sha256 value="48c64a15ec3eb11bfb33339e5ceb70ec7f821bd2dfa2eb8675ebd5353317e792" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-1.8.0.module">
<sha256 value="505f1838869611519d65ec7feb650e88856e3682e37e42a2a24b73e655a98d74" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.9.0">
<artifact name="core-1.9.0.aar">
<sha256 value="8bda3ee3a88887d54f6679fb6b6cd788629f73234ac91c8bbed924e721ec85b8" origin="Generated by Gradle"/>
@ -2936,6 +2977,14 @@
<sha256 value="fc8b21ebe5fa3a7c96ee098bcdcd00f077ebce73f243fa858e2b0671615f75d8" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.tracing" name="tracing" version="1.1.0">
<artifact name="tracing-1.1.0.aar">
<sha256 value="5b78e2c618fc10b3d14decc01df76158f15954ad746aacf0607766721da081f6" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="tracing-1.1.0.module">
<sha256 value="b1fed4309623b6f20bc817d8fbd70e4ea7085e40647694cd399ae58d2f0049e3" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.transition" name="transition" version="1.2.0">
<artifact name="transition-1.2.0.aar">
<sha256 value="a1e059b3bc0b43a58dec0efecdcaa89c82d2bca552ea5bacf6656c46e853157e" origin="Generated by Gradle" reason="Artifact is not signed"/>