diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ecf1ae4b7c67..4e44dcd5a3ae 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,7 +32,7 @@ jobs: with: swap-size-gb: 10 - name: Initialize CodeQL - uses: github/codeql-action/init@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 + uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 with: languages: ${{ matrix.language }} - name: Set up JDK 17 @@ -46,4 +46,4 @@ jobs: echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" ./gradlew assembleDebug - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 + uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index cf5828e1d3d4..3559d339b79d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,6 +37,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@689fdc5193eeb735ecb2e52e819e3382876f93f4 # v2.22.6 + uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 with: sarif_file: results.sarif diff --git a/app/build.gradle b/app/build.gradle index 1227b2cf78ad..df3e9016b7f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,8 +84,8 @@ android { defaultConfig { minSdkVersion 24 - targetSdkVersion 33 - compileSdk 33 + targetSdkVersion 34 + compileSdk 34 buildConfigField 'boolean', 'CI', ciBuild.toString() buildConfigField 'boolean', 'RUNTIME_PERF_ANALYSIS', perfAnalysis.toString() @@ -246,7 +246,7 @@ dependencies { implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2 implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.10.0' implementation 'com.jakewharton:disklrucache:2.0.2' implementation "androidx.appcompat:appcompat:$appCompatVersion" implementation 'androidx.webkit:webkit:1.7.0' diff --git a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt index 9cec5f9039f1..616fc1bc9be5 100644 --- a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt @@ -174,7 +174,7 @@ class ActivitiesActivityIT : AbstractIT() { sut.dismissSnackbar() } - shortSleep() + longSleep() waitForIdleSync() screenshot(sut) diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 676da368baa0..556acc642266 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -92,7 +92,7 @@ abstract class NextcloudDatabase : RoomDatabase() { INSTANCE = Room .databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME) .allowMainThreadQueries() - .addLegacyMigrations(clock) + .addLegacyMigrations(clock, context) .addMigrations(RoomMigration()) .addMigrations(Migration67to68()) .addMigrations(Migration70to71()) diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt b/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt index abd77b7c734f..d8fe46585220 100644 --- a/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt +++ b/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt @@ -22,6 +22,7 @@ package com.nextcloud.client.database.migrations +import android.content.Context import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase @@ -36,12 +37,13 @@ private const val MIN_SUPPORTED_DB_VERSION = 24 class LegacyMigration( private val from: Int, private val to: Int, - private val clock: Clock + private val clock: Clock, + private val context: Context ) : Migration(from, to) { override fun migrate(database: SupportSQLiteDatabase) { - LegacyMigrationHelper(clock) - .onUpgrade(database, from, to) + LegacyMigrationHelper(clock, context) + .tryUpgrade(database, from, to) } } @@ -52,10 +54,11 @@ class LegacyMigration( */ @Suppress("ForEachOnRange") fun RoomDatabase.Builder.addLegacyMigrations( - clock: Clock + clock: Clock, + context: Context ): RoomDatabase.Builder { (MIN_SUPPORTED_DB_VERSION until NextcloudDatabase.FIRST_ROOM_DB_VERSION - 1) - .map { from -> LegacyMigration(from, from + 1, clock) } + .map { from -> LegacyMigration(from, from + 1, clock, context) } .forEach { migration -> this.addMigrations(migration) } return this } diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java b/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java index a5b8b1098c5a..af248c2c207e 100644 --- a/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java +++ b/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java @@ -22,6 +22,7 @@ package com.nextcloud.client.database.migrations; +import android.app.ActivityManager; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; @@ -31,11 +32,11 @@ import com.owncloud.android.db.ProviderMeta; import com.owncloud.android.files.services.NameCollisionPolicy; import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.providers.FileContentProvider; import java.util.Locale; import androidx.sqlite.db.SupportSQLiteDatabase; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public class LegacyMigrationHelper { @@ -52,12 +53,29 @@ public class LegacyMigrationHelper { private static final String UPGRADE_VERSION_MSG = "OUT of the ADD in onUpgrade; oldVersion == %d, newVersion == %d"; private final Clock clock; + private final Context context; - public LegacyMigrationHelper(Clock clock) { + public LegacyMigrationHelper(Clock clock, Context context) { this.clock = clock; + this.context = context; } - public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { + public void tryUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { + try { + upgrade(db, oldVersion, newVersion); + } catch (Throwable t) { + Log_OC.i(TAG, "Migration upgrade failed due to " + t); + clearStorage(); + } + } + + @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE") + private void clearStorage() { + context.getCacheDir().delete(); + ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).clearApplicationUserData(); + } + + private void upgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { Log_OC.i(TAG, "Entering in onUpgrade"); boolean upgraded = false; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java index 074de51a1069..a819c25fbdff 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java @@ -36,6 +36,7 @@ import javax.inject.Inject; +import androidx.activity.OnBackPressedCallback; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; @@ -115,6 +116,8 @@ protected void onCreate(Bundle savedInstanceState) { } transaction.commit(); } + + getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); } @Override @@ -137,12 +140,14 @@ public void onTransferStateChanged(OCFile file, boolean downloading, boolean upl // not needed } - @Override - public void onBackPressed() { - if (getSupportFragmentManager().findFragmentByTag(BackupListFragment.TAG) != null) { - getSupportFragmentManager().popBackStack(BACKUP_TO_LIST, FragmentManager.POP_BACK_STACK_INCLUSIVE); - } else { - finish(); + private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (getSupportFragmentManager().findFragmentByTag(BackupListFragment.TAG) != null) { + getSupportFragmentManager().popBackStack(BACKUP_TO_LIST, FragmentManager.POP_BACK_STACK_INCLUSIVE); + } else { + finish(); + } } - } + }; } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index cb7013cd1ce2..16478cd5b628 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -2507,8 +2507,7 @@ private void openFile(User user, String fileId) { setUser(user); if (fileId == null) { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file)); + onFileRequestError(null); return; } @@ -2529,8 +2528,7 @@ private void openFileByPath(User user, String filepath) { setUser(user); if (filepath == null) { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file)); + onFileRequestError(null); return; } @@ -2544,8 +2542,7 @@ private void openFileByPath(User user, String filepath) { try { client = clientFactory.create(user); } catch (ClientFactory.CreationException e) { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file)); + onFileRequestError(null); return; } @@ -2554,9 +2551,17 @@ private void openFileByPath(User user, String filepath) { client, storageManager, user); - asyncRunner.postQuickTask(getRemoteFileTask, this::onFileRequestResult, null); + asyncRunner.postQuickTask(getRemoteFileTask, this::onFileRequestResult, this::onFileRequestError); + } + + private Unit onFileRequestError(Throwable throwable) { + dismissLoadingDialog(); + DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file)); + Log_OC.e(TAG, "Requesting file from remote failed!", throwable); + return null; } + private Unit onFileRequestResult(GetRemoteFileTask.Result result) { dismissLoadingDialog(); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index 6e829d547b1e..97f7a2467801 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -32,6 +32,7 @@ import android.view.ActionMode import android.view.Menu import android.view.MenuItem import android.view.View +import androidx.activity.OnBackPressedCallback import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.button.MaterialButton import com.nextcloud.client.di.Injectable @@ -125,9 +126,33 @@ open class FolderPickerActivity : // sets message for empty list of folders setBackgroundText() + + handleOnBackPressed() + Log_OC.d(TAG, "onCreate() end") } + private fun handleOnBackPressed() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val listOfFiles = listOfFilesFragment + if (listOfFiles != null) { + // should never be null, indeed + val levelsUp = listOfFiles.onBrowseUp() + if (levelsUp == 0) { + finish() + return + } + file = listOfFiles.currentFile + updateUiElements() + } + } + } + ) + } + override fun onActionModeStarted(mode: ActionMode) { super.onActionModeStarted(mode) if (account != null) { @@ -321,20 +346,6 @@ open class FolderPickerActivity : } } - override fun onBackPressed() { - val listOfFiles = listOfFilesFragment - if (listOfFiles != null) { - // should never be null, indeed - val levelsUp = listOfFiles.onBrowseUp() - if (levelsUp == 0) { - finish() - return - } - file = listOfFiles.currentFile - updateUiElements() - } - } - private fun updateUiElements() { toggleChooseEnabled() updateNavigationElementsInActionBar() diff --git a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java deleted file mode 100644 index 21bc674acc86..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java +++ /dev/null @@ -1,373 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Andy Scherzinger - * @author Mario Danic - * @author Chris Narkiewicz - * Copyright (C) 2017 Andy Scherzinger - * Copyright (C) 2017 Mario Danic - * Copyright (C) 2020 Chris Narkiewicz - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.ui.activity; - -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.jobs.NotificationWork; -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.java.util.Optional; -import com.owncloud.android.R; -import com.owncloud.android.databinding.NotificationsLayoutBinding; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation; -import com.owncloud.android.lib.resources.notifications.models.Notification; -import com.owncloud.android.ui.adapter.NotificationListAdapter; -import com.owncloud.android.ui.asynctasks.DeleteAllNotificationsTask; -import com.owncloud.android.ui.notifications.NotificationsContract; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.PushUtils; - -import java.util.List; - -import javax.inject.Inject; - -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.LinearLayoutManager; - -/** - * Activity displaying all server side stored notification items. - */ -public class NotificationsActivity extends DrawerActivity implements NotificationsContract.View { - - private static final String TAG = NotificationsActivity.class.getSimpleName(); - - private NotificationsLayoutBinding binding; - private NotificationListAdapter adapter; - private Snackbar snackbar; - private OwnCloudClient client; - private Optional optionalUser; - - @Inject ClientFactory clientFactory; - - @Override - protected void onCreate(Bundle savedInstanceState) { - Log_OC.v(TAG, "onCreate() start"); - super.onCreate(savedInstanceState); - - binding = NotificationsLayoutBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - optionalUser = getUser(); - - // use account from intent (opened via android notification can have a different account than current one) - if (getIntent() != null && getIntent().getExtras() != null) { - String accountName = getIntent().getExtras().getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT); - if (accountName != null && optionalUser.isPresent()) { - User user = optionalUser.get(); - if (user.getAccountName().equalsIgnoreCase(accountName)) { - accountManager.setCurrentOwnCloudAccount(accountName); - setUser(getUserAccountManager().getUser()); - optionalUser = getUser(); - } - } - } - - // setup toolbar - setupToolbar(); - - updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications)); - - viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList); - viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingEmpty); - - // setup drawer - setupDrawer(R.id.nav_notifications); - - if (!optionalUser.isPresent()) { - // show error - runOnUiThread(() -> setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.account_not_found)) - ); - return; - } - - binding.swipeContainingList.setOnRefreshListener(() -> { - setLoadingMessage(); - binding.swipeContainingList.setRefreshing(true); - fetchAndSetData(); - }); - - binding.swipeContainingEmpty.setOnRefreshListener(() -> { - setLoadingMessageEmpty(); - fetchAndSetData(); - }); - - setupPushWarning(); - setupContent(); - } - - private void setupPushWarning() { - if (!getResources().getBoolean(R.bool.show_push_warning)) { - return; - } - if (snackbar != null) { - if (!snackbar.isShown()) { - snackbar.show(); - } - } else { - String pushUrl = getResources().getString(R.string.push_server_url); - - if (pushUrl.isEmpty()) { - snackbar = Snackbar.make(binding.emptyList.emptyListView, - R.string.push_notifications_not_implemented, - Snackbar.LENGTH_INDEFINITE); - } else { - final ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(this); - final String accountName = optionalUser.isPresent() ? optionalUser.get().getAccountName() : ""; - final boolean usesOldLogin = arbitraryDataProvider.getBooleanValue(accountName, - UserAccountManager.ACCOUNT_USES_STANDARD_PASSWORD); - - if (usesOldLogin) { - snackbar = Snackbar.make(binding.emptyList.emptyListView, - R.string.push_notifications_old_login, - Snackbar.LENGTH_INDEFINITE); - } else { - String pushValue = arbitraryDataProvider.getValue(accountName, PushUtils.KEY_PUSH); - - if (pushValue == null || pushValue.isEmpty()) { - snackbar = Snackbar.make(binding.emptyList.emptyListView, - R.string.push_notifications_temp_error, - Snackbar.LENGTH_INDEFINITE); - } - } - } - - if (snackbar != null && !snackbar.isShown()) { - snackbar.show(); - } - } - } - - @Override - public void openDrawer() { - super.openDrawer(); - - if (snackbar != null && snackbar.isShown()) { - snackbar.dismiss(); - } - } - - @Override - public void closeDrawer() { - super.closeDrawer(); - - setupPushWarning(); - } - - /** - * sets up the UI elements and loads all notification items. - */ - private void setupContent() { - binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification); - setLoadingMessageEmpty(); - - LinearLayoutManager layoutManager = new LinearLayoutManager(this); - - binding.list.setLayoutManager(layoutManager); - - fetchAndSetData(); - } - - @VisibleForTesting - public void populateList(List notifications) { - initializeAdapter(); - adapter.setNotificationItems(notifications); - binding.loadingContent.setVisibility(View.GONE); - - if (notifications.size() > 0) { - binding.swipeContainingEmpty.setVisibility(View.GONE); - binding.swipeContainingList.setVisibility(View.VISIBLE); - } else { - setEmptyContent( - getString(R.string.notifications_no_results_headline), - getString(R.string.notifications_no_results_message) - ); - binding.swipeContainingList.setVisibility(View.GONE); - binding.swipeContainingEmpty.setVisibility(View.VISIBLE); - } - } - - private void fetchAndSetData() { - Thread t = new Thread(() -> { - initializeAdapter(); - - GetNotificationsRemoteOperation getRemoteNotificationOperation = new GetNotificationsRemoteOperation(); - final RemoteOperationResult> result = getRemoteNotificationOperation.execute(client); - - if (result.isSuccess() && result.getResultData() != null) { - runOnUiThread(() -> populateList(result.getResultData())); - } else { - Log_OC.d(TAG, result.getLogMessage()); - // show error - runOnUiThread(() -> setEmptyContent(getString(R.string.notifications_no_results_headline), result.getLogMessage())); - } - - hideRefreshLayoutLoader(); - }); - - t.start(); - } - - private void initializeClient() { - if (client == null && optionalUser.isPresent()) { - try { - User user = optionalUser.get(); - client = clientFactory.create(user); - } catch (ClientFactory.CreationException e) { - Log_OC.e(TAG, "Error initializing client", e); - } - } - } - - private void initializeAdapter() { - initializeClient(); - if (adapter == null) { - adapter = new NotificationListAdapter(client, this, viewThemeUtils); - binding.list.setAdapter(adapter); - } - } - - private void hideRefreshLayoutLoader() { - runOnUiThread(() -> { - binding.swipeContainingList.setRefreshing(false); - binding.swipeContainingEmpty.setRefreshing(false); - }); - } - - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_notifications, menu); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - boolean retval = true; - - int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - if (isDrawerOpen()) { - closeDrawer(); - } else { - openDrawer(); - } - } else if (itemId == R.id.action_empty_notifications) { - new DeleteAllNotificationsTask(client, this).execute(); - } else { - retval = super.onOptionsItemSelected(item); - } - - return retval; - } - - private void setLoadingMessage() { - binding.swipeContainingEmpty.setVisibility(View.GONE); - } - - @VisibleForTesting - public void setLoadingMessageEmpty() { - binding.swipeContainingList.setVisibility(View.GONE); - binding.emptyList.emptyListView.setVisibility(View.GONE); - binding.loadingContent.setVisibility(View.VISIBLE); - } - - @VisibleForTesting - public void setEmptyContent(String headline, String message) { - binding.swipeContainingList.setVisibility(View.GONE); - binding.loadingContent.setVisibility(View.GONE); - binding.swipeContainingEmpty.setVisibility(View.VISIBLE); - binding.emptyList.emptyListView.setVisibility(View.VISIBLE); - - binding.emptyList.emptyListViewHeadline.setText(headline); - binding.emptyList.emptyListViewText.setText(message); - binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification); - - binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE); - binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE); - } - - @Override - protected void onResume() { - super.onResume(); - setDrawerMenuItemChecked(R.id.nav_notifications); - } - - @Override - public void onRemovedNotification(boolean isSuccess) { - if (!isSuccess) { - DisplayUtils.showSnackMessage(this, getString(R.string.remove_notification_failed)); - fetchAndSetData(); - } - } - - @Override - public void removeNotification(NotificationListAdapter.NotificationViewHolder holder) { - adapter.removeNotification(holder); - - if (adapter.getItemCount() == 0) { - setEmptyContent(getString(R.string.notifications_no_results_headline), getString(R.string.notifications_no_results_message)); - binding.swipeContainingList.setVisibility(View.GONE); - binding.loadingContent.setVisibility(View.GONE); - binding.swipeContainingEmpty.setVisibility(View.VISIBLE); - } - } - - @Override - public void onRemovedAllNotifications(boolean isSuccess) { - if (isSuccess) { - adapter.removeAllNotifications(); - setEmptyContent(getString(R.string.notifications_no_results_headline), getString(R.string.notifications_no_results_message)); - binding.loadingContent.setVisibility(View.GONE); - binding.swipeContainingList.setVisibility(View.GONE); - binding.swipeContainingEmpty.setVisibility(View.VISIBLE); - } else { - DisplayUtils.showSnackMessage(this, getString(R.string.clear_notifications_failed)); - } - } - - @Override - public void onActionCallback(boolean isSuccess, - Notification notification, - NotificationListAdapter.NotificationViewHolder holder) { - if (isSuccess) { - adapter.removeNotification(holder); - } else { - adapter.setButtons(holder, notification); - DisplayUtils.showSnackMessage(this, getString(R.string.notification_action_failed)); - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt new file mode 100644 index 000000000000..f0e5ec2cd874 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt @@ -0,0 +1,376 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * @author Mario Danic + * @author Chris Narkiewicz + * Copyright (C) 2017 Andy Scherzinger + * Copyright (C) 2017 Mario Danic + * Copyright (C) 2020 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.ui.activity + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.NotificationWork +import com.nextcloud.client.network.ClientFactory.CreationException +import com.nextcloud.java.util.Optional +import com.owncloud.android.R +import com.owncloud.android.databinding.NotificationsLayoutBinding +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation +import com.owncloud.android.lib.resources.notifications.models.Notification +import com.owncloud.android.ui.adapter.NotificationListAdapter +import com.owncloud.android.ui.adapter.NotificationListAdapter.NotificationViewHolder +import com.owncloud.android.ui.asynctasks.DeleteAllNotificationsTask +import com.owncloud.android.ui.notifications.NotificationsContract +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.PushUtils + +/** + * Activity displaying all server side stored notification items. + */ +class NotificationsActivity : DrawerActivity(), NotificationsContract.View { + + private lateinit var binding: NotificationsLayoutBinding + + private var adapter: NotificationListAdapter? = null + private var snackbar: Snackbar? = null + private var client: OwnCloudClient? = null + private var optionalUser: Optional? = null + + override fun onCreate(savedInstanceState: Bundle?) { + Log_OC.v(TAG, "onCreate() start") + + super.onCreate(savedInstanceState) + + binding = NotificationsLayoutBinding.inflate(layoutInflater) + setContentView(binding.root) + + optionalUser = user + + intent?.let { + it.extras?.let { bundle -> + setupUser(bundle) + } + } + + setupToolbar() + updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications)) + setupDrawer(R.id.nav_notifications) + + if (optionalUser?.isPresent == false) { + showError() + } + + setupContainingList() + setupPushWarning() + setupContent() + } + + private fun setupContainingList() { + viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList) + viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingEmpty) + binding.swipeContainingList.setOnRefreshListener { + setLoadingMessage() + binding.swipeContainingList.isRefreshing = true + fetchAndSetData() + } + binding.swipeContainingEmpty.setOnRefreshListener { + setLoadingMessageEmpty() + fetchAndSetData() + } + } + + private fun setupUser(bundle: Bundle) { + val accountName = bundle.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT) + + if (accountName != null && optionalUser?.isPresent == true) { + val user = optionalUser?.get() + if (user?.accountName.equals(accountName, ignoreCase = true)) { + accountManager.setCurrentOwnCloudAccount(accountName) + setUser(userAccountManager.user) + optionalUser = getUser() + } + } + } + + private fun showError() { + runOnUiThread { + setEmptyContent( + getString(R.string.notifications_no_results_headline), + getString(R.string.account_not_found) + ) + } + return + } + + private fun setupPushWarning() { + if (!resources.getBoolean(R.bool.show_push_warning)) { + return + } + + if (snackbar != null) { + if (snackbar?.isShown == false) { + snackbar?.show() + } + } else { + val pushUrl = resources.getString(R.string.push_server_url) + if (pushUrl.isEmpty()) { + snackbar = Snackbar.make( + binding.emptyList.emptyListView, + R.string.push_notifications_not_implemented, + Snackbar.LENGTH_INDEFINITE + ) + } else { + val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(this) + val accountName: String = if (optionalUser?.isPresent == true) { + optionalUser?.get()?.accountName ?: "" + } else { + "" + } + val usesOldLogin = arbitraryDataProvider.getBooleanValue( + accountName, + UserAccountManager.ACCOUNT_USES_STANDARD_PASSWORD + ) + + if (usesOldLogin) { + snackbar = Snackbar.make( + binding.emptyList.emptyListView, + R.string.push_notifications_old_login, + Snackbar.LENGTH_INDEFINITE + ) + } else { + val pushValue = arbitraryDataProvider.getValue(accountName, PushUtils.KEY_PUSH) + if (pushValue.isEmpty()) { + snackbar = Snackbar.make( + binding.emptyList.emptyListView, + R.string.push_notifications_temp_error, + Snackbar.LENGTH_INDEFINITE + ) + } + } + } + + if (snackbar != null && snackbar?.isShown == false) { + snackbar?.show() + } + } + } + + override fun openDrawer() { + super.openDrawer() + if (snackbar != null && snackbar?.isShown == true) { + snackbar?.dismiss() + } + } + + override fun closeDrawer() { + super.closeDrawer() + setupPushWarning() + } + + /** + * sets up the UI elements and loads all notification items. + */ + private fun setupContent() { + binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) + setLoadingMessageEmpty() + val layoutManager = LinearLayoutManager(this) + binding.list.layoutManager = layoutManager + fetchAndSetData() + } + + @VisibleForTesting + fun populateList(notifications: List?) { + initializeAdapter() + adapter?.setNotificationItems(notifications) + binding.loadingContent.visibility = View.GONE + + if (notifications?.isNotEmpty() == true) { + binding.swipeContainingEmpty.visibility = View.GONE + binding.swipeContainingList.visibility = View.VISIBLE + } else { + setEmptyContent( + getString(R.string.notifications_no_results_headline), + getString(R.string.notifications_no_results_message) + ) + binding.swipeContainingList.visibility = View.GONE + binding.swipeContainingEmpty.visibility = View.VISIBLE + } + } + + private fun fetchAndSetData() { + val t = Thread { + initializeAdapter() + val getRemoteNotificationOperation = GetNotificationsRemoteOperation() + val result = getRemoteNotificationOperation.execute(client) + if (result.isSuccess && result.resultData != null) { + runOnUiThread { populateList(result.resultData) } + } else { + Log_OC.d(TAG, result.logMessage) + // show error + runOnUiThread { + setEmptyContent( + getString(R.string.notifications_no_results_headline), + result.logMessage + ) + } + } + hideRefreshLayoutLoader() + } + t.start() + } + + private fun initializeClient() { + if (client == null && optionalUser?.isPresent == true) { + try { + val user = optionalUser?.get() + client = clientFactory.create(user) + } catch (e: CreationException) { + Log_OC.e(TAG, "Error initializing client", e) + } + } + } + + private fun initializeAdapter() { + initializeClient() + if (adapter == null) { + adapter = NotificationListAdapter(client, this, viewThemeUtils) + binding.list.adapter = adapter + } + } + + private fun hideRefreshLayoutLoader() { + runOnUiThread { + binding.swipeContainingList.isRefreshing = false + binding.swipeContainingEmpty.isRefreshing = false + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_notifications, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + var retval = true + val itemId = item.itemId + if (itemId == android.R.id.home) { + if (isDrawerOpen) { + closeDrawer() + } else { + openDrawer() + } + } else if (itemId == R.id.action_empty_notifications) { + DeleteAllNotificationsTask(client, this).execute() + } else { + retval = super.onOptionsItemSelected(item) + } + return retval + } + + private fun setLoadingMessage() { + binding.swipeContainingEmpty.visibility = View.GONE + } + + @VisibleForTesting + fun setLoadingMessageEmpty() { + binding.swipeContainingList.visibility = View.GONE + binding.emptyList.emptyListView.visibility = View.GONE + binding.loadingContent.visibility = View.VISIBLE + } + + @VisibleForTesting + fun setEmptyContent(headline: String?, message: String?) { + binding.swipeContainingList.visibility = View.GONE + binding.loadingContent.visibility = View.GONE + binding.swipeContainingEmpty.visibility = View.VISIBLE + binding.emptyList.emptyListView.visibility = View.VISIBLE + binding.emptyList.emptyListViewHeadline.text = headline + binding.emptyList.emptyListViewText.text = message + binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification) + binding.emptyList.emptyListViewText.visibility = View.VISIBLE + binding.emptyList.emptyListIcon.visibility = View.VISIBLE + } + + override fun onResume() { + super.onResume() + setDrawerMenuItemChecked(R.id.nav_notifications) + } + + override fun onRemovedNotification(isSuccess: Boolean) { + if (!isSuccess) { + DisplayUtils.showSnackMessage(this, getString(R.string.remove_notification_failed)) + fetchAndSetData() + } + } + + override fun removeNotification(holder: NotificationViewHolder) { + adapter?.removeNotification(holder) + if (adapter?.itemCount == 0) { + setEmptyContent( + getString(R.string.notifications_no_results_headline), + getString(R.string.notifications_no_results_message) + ) + binding.swipeContainingList.visibility = View.GONE + binding.loadingContent.visibility = View.GONE + binding.swipeContainingEmpty.visibility = View.VISIBLE + } + } + + override fun onRemovedAllNotifications(isSuccess: Boolean) { + if (isSuccess) { + adapter?.removeAllNotifications() + setEmptyContent( + getString(R.string.notifications_no_results_headline), + getString(R.string.notifications_no_results_message) + ) + binding.loadingContent.visibility = View.GONE + binding.swipeContainingList.visibility = View.GONE + binding.swipeContainingEmpty.visibility = View.VISIBLE + } else { + DisplayUtils.showSnackMessage(this, getString(R.string.clear_notifications_failed)) + } + } + + override fun onActionCallback( + isSuccess: Boolean, + notification: Notification, + holder: NotificationViewHolder + ) { + if (isSuccess) { + adapter?.removeNotification(holder) + } else { + adapter?.setButtons(holder, notification) + DisplayUtils.showSnackMessage(this, getString(R.string.notification_action_failed)) + } + } + + companion object { + private val TAG = NotificationsActivity::class.java.simpleName + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java index bff39ae57e0e..44942b1b3a63 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java @@ -61,6 +61,7 @@ import javax.inject.Inject; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.ActionBar; @@ -258,6 +259,8 @@ public void onNothingSelected(AdapterView parent) { checkWritableFolder(mCurrentDir); + getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); + Log_OC.d(TAG, "onCreate() end"); } @@ -369,43 +372,45 @@ private boolean isSearchOpen() { } } - @Override - public void onBackPressed() { - if (isSearchOpen() && mSearchView != null) { - mSearchView.setQuery("", false); - mFileListFragment.onClose(); - mSearchView.onActionViewCollapsed(); - setDrawerIndicatorEnabled(isDrawerIndicatorAvailable()); - } else { - if (mDirectories.getCount() <= SINGLE_DIR) { - finish(); - return; - } + private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (isSearchOpen() && mSearchView != null) { + mSearchView.setQuery("", false); + mFileListFragment.onClose(); + mSearchView.onActionViewCollapsed(); + setDrawerIndicatorEnabled(isDrawerIndicatorAvailable()); + } else { + if (mDirectories.getCount() <= SINGLE_DIR) { + finish(); + return; + } - File parentFolder = mCurrentDir.getParentFile(); - if (!parentFolder.canRead()) { - checkLocalStoragePathPickerPermission(); - return; - } + File parentFolder = mCurrentDir.getParentFile(); + if (!parentFolder.canRead()) { + checkLocalStoragePathPickerPermission(); + return; + } - popDirname(); - mFileListFragment.onNavigateUp(); - mCurrentDir = mFileListFragment.getCurrentDirectory(); - checkWritableFolder(mCurrentDir); + popDirname(); + mFileListFragment.onNavigateUp(); + mCurrentDir = mFileListFragment.getCurrentDirectory(); + checkWritableFolder(mCurrentDir); - if (mCurrentDir.getParentFile() == null) { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(false); + if (mCurrentDir.getParentFile() == null) { + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(false); + } } - } - // invalidate checked state when navigating directories - if (!mLocalFolderPickerMode) { - setSelectAllMenuItem(mOptionsMenu.findItem(R.id.action_select_all), false); + // invalidate checked state when navigating directories + if (!mLocalFolderPickerMode) { + setSelectAllMenuItem(mOptionsMenu.findItem(R.id.action_select_all), false); + } } } - } + }; @Override protected void onSaveInstanceState(@NonNull Bundle outState) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java index 0298ef49bc0e..821d44ac3c7a 100755 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java @@ -62,6 +62,8 @@ import com.owncloud.android.operations.RefreshFolderOperation; import com.owncloud.android.ui.activity.ConflictsResolveActivity; import com.owncloud.android.ui.activity.FileActivity; +import com.owncloud.android.ui.activity.FileDisplayActivity; +import com.owncloud.android.ui.preview.PreviewImageFragment; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -346,12 +348,15 @@ public void onBindViewHolder(SectionedViewHolder holder, int section, int relati itemViewHolder.binding.uploadRightButton.setOnClickListener(v -> removeUpload(item)); } itemViewHolder.binding.uploadRightButton.setVisibility(View.VISIBLE); - } else { // UploadStatus.UPLOAD_SUCCESS + } else { // UploadStatus.UPLOAD_SUCCEEDED itemViewHolder.binding.uploadRightButton.setVisibility(View.INVISIBLE); } itemViewHolder.binding.uploadListItemLayout.setOnClickListener(null); + // Set icon or thumbnail + itemViewHolder.binding.thumbnail.setImageResource(R.drawable.file); + // click on item if (item.getUploadStatus() == UploadStatus.UPLOAD_FAILED) { final UploadResult uploadResult = item.getLastResult(); @@ -381,12 +386,15 @@ public void onBindViewHolder(SectionedViewHolder holder, int section, int relati ); } }); - } else { - itemViewHolder.binding.uploadListItemLayout.setOnClickListener(v -> onUploadItemClick(item)); + } else if (item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED){ + itemViewHolder.binding.uploadListItemLayout.setOnClickListener(v -> onUploadedItemClick(item)); } - // Set icon or thumbnail - itemViewHolder.binding.thumbnail.setImageResource(R.drawable.file); + + // click on thumbnail to open locally + if (item.getUploadStatus() != UploadStatus.UPLOAD_SUCCEEDED){ + itemViewHolder.binding.thumbnail.setOnClickListener(v -> onUploadingItemClick(item)); + } /* * Cancellation needs do be checked and done before changing the drawable in fileIcon, or @@ -738,7 +746,10 @@ public final void loadUploadItemsFromDb() { notifyDataSetChanged(); } - private void onUploadItemClick(OCUpload file) { + /** + * Open local file. + */ + private void onUploadingItemClick(OCUpload file) { File f = new File(file.getLocalPath()); if (!f.exists()) { DisplayUtils.showSnackMessage(parentActivity, R.string.local_file_not_found_message); @@ -747,6 +758,30 @@ private void onUploadItemClick(OCUpload file) { } } + /** + * Open remote file. + */ + private void onUploadedItemClick(OCUpload upload) { + final OCFile file = parentActivity.getStorageManager().getFileByEncryptedRemotePath(upload.getRemotePath()); + if (file == null){ + DisplayUtils.showSnackMessage(parentActivity, R.string.error_retrieving_file); + Log_OC.i(TAG, "Could not find uploaded file on remote."); + return; + } + + if (PreviewImageFragment.canBePreviewed(file)){ + //show image preview and stay in uploads tab + Intent intent = FileDisplayActivity.openFileIntent(parentActivity, parentActivity.getUser().get(), file); + parentActivity.startActivity(intent); + }else{ + Intent intent = new Intent(parentActivity, FileDisplayActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(FileDisplayActivity.KEY_FILE_PATH, upload.getRemotePath()); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + parentActivity.startActivity(intent); + } + } + /** * Open file with app associates with its MIME type. If MIME type unknown, show list with all apps. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e6198a5a30bf..60b032fda9e8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -18,6 +18,7 @@ Kopiuj Nowy katalog Przenieś + Przenieś lub kopiuj Otwórz za pomocą Wyszukaj Szczegóły @@ -324,6 +325,7 @@ Usuń Błąd podczas pobierania aktywności dla pliku Nie udało się załadować szczegółów + Pobieranie \u0020 Plik Zachowaj Wyślij lub zsynchronizuj pliki z urządzeniami. @@ -944,7 +946,9 @@ Proszę czekać… Sprawdzanie danych Kopiowanie pliku z prywatnego magazynu + Aby się zalogować, zaktualizuj aplikację WebView systemu Android Aktualizuj + Zaktualizuj WebView systemu Android Jaki jest nowy obraz Pomiń Co nowego w %1$s diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c8383be19632..ec14ded2e15b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -70,7 +70,7 @@ @color/text_color @color/text_color @color/hwSecurityRed - @style/Widget.MaterialComponents.TextInputEditText.OutlinedBox + @style/Widget.Material3.TextInputEditText.OutlinedBox - @@ -363,7 +363,7 @@ @@ -371,9 +371,9 @@ @color/white @color/white @color/hwSecurityRed - @style/TextAppearance.MaterialComponents.Subtitle1 - @style/TextAppearance.MaterialComponents.Caption - @style/Widget.MaterialComponents.TextInputEditText.OutlinedBox + @style/TextAppearance.Material3.BodyLarge + @style/TextAppearance.Material3.BodySmall + @style/Widget.Material3.TextInputEditText.OutlinedBox - - diff --git a/app/src/test/java/com/nextcloud/client/core/LocalConnectionTest.kt b/app/src/test/java/com/nextcloud/client/core/LocalConnectionTest.kt index 5a9380573ed0..f1f77f43e845 100644 --- a/app/src/test/java/com/nextcloud/client/core/LocalConnectionTest.kt +++ b/app/src/test/java/com/nextcloud/client/core/LocalConnectionTest.kt @@ -65,7 +65,7 @@ class LocalConnectionTest { // THEN // no binding is performed - verify(exactly = 0) { context.bindService(any(), any(), any()) } + verify(exactly = 0) { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) } } @Test @@ -76,12 +76,12 @@ class LocalConnectionTest { // WHEN // bind requested - every { context.bindService(mockIntent, any(), any()) } returns true + every { context.bindService(mockIntent!!, any(), Context.BIND_AUTO_CREATE) } returns true connection.bind() // THEN // service bound - verify { context.bindService(mockIntent, any(), any()) } + verify { context.bindService(mockIntent!!, any(), Context.BIND_AUTO_CREATE) } } @Test