diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5e607ef71d..4e44dcd5a3 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@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + 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@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e6664f0039..3559d339b7 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@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + 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 1227b2cf78..df3e9016b7 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/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png index 497107941f..0e9310c7e5 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png index 98f675eec0..cf556b7259 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png index 02e0f1f84e..8ae67d62bf 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png differ diff --git a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt index 9cec5f9039..616fc1bc9b 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/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt b/app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt index bc3bf0e17f..03c453e8ca 100644 --- a/app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt +++ b/app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt @@ -20,14 +20,15 @@ */ package com.owncloud.android.ui.activity +import android.os.Bundle import android.view.View /** * Activity providing information about ways to participate in the app's development. */ class HuaweiCommunityActivity : CommunityActivity() { - override fun setupContent() { - super.setupContent() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) binding.communityReleaseCandidatePlaystore.visibility = View.GONE } } 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 676da368ba..556acc6422 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 abd77b7c73..d8fe465852 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 a5b8b1098c..af248c2c20 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.datamodel.SyncedFolder; 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/nextcloud/utils/extensions/TextViewExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/TextViewExtensions.kt new file mode 100644 index 0000000000..52ad55653b --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/TextViewExtensions.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Android client application + * + * @author Alper Ozturk + * Copyright (C) 2023 Alper Ozturk + * Copyright (C) 2023 Nextcloud GmbH + * + * 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.nextcloud.utils.extensions + +import android.text.method.LinkMovementMethod +import android.widget.TextView +import androidx.core.text.HtmlCompat + +@Suppress("NewLineAtEndOfFile") +fun TextView.setHtmlContent(value: String) { + movementMethod = LinkMovementMethod.getInstance() + text = HtmlCompat.fromHtml(value, HtmlCompat.FROM_HTML_MODE_LEGACY) +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.java deleted file mode 100644 index 05662f63b7..0000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Andy Scherzinger - * @author Tobias Kaminsky - * Copyright (C) 2016 Andy Scherzinger - * Copyright (C) 2016 Nextcloud - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or 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.text.Html; -import android.text.method.LinkMovementMethod; -import android.view.MenuItem; -import android.widget.TextView; - -import com.google.android.material.button.MaterialButton; -import com.owncloud.android.R; -import com.owncloud.android.databinding.CommunityLayoutBinding; -import com.owncloud.android.utils.DisplayUtils; - -/** - * Activity providing information about ways to participate in the app's development. - */ -public class CommunityActivity extends DrawerActivity { - - protected CommunityLayoutBinding binding; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = CommunityLayoutBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - // setup toolbar - setupToolbar(); - - updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_community)); - - // setup drawer - setupDrawer(R.id.nav_community); - - setupContent(); - } - - protected void setupContent() { - binding.communityReleaseCandidateText.setMovementMethod(LinkMovementMethod.getInstance()); - - TextView contributeForumView = binding.communityContributeForumText; - contributeForumView.setMovementMethod(LinkMovementMethod.getInstance()); - contributeForumView.setText(Html.fromHtml(getString(R.string.community_contribute_forum_text) + " " + - getString(R.string.community_contribute_forum_text_link, - viewThemeUtils - .files - .primaryColorToHexString(this), - getString(R.string.help_link), - getString(R.string.community_contribute_forum_forum)))); - - TextView contributeTranslationView = binding.communityContributeTranslateText; - contributeTranslationView.setMovementMethod(LinkMovementMethod.getInstance()); - contributeTranslationView.setText(Html.fromHtml( - getString(R.string.community_contribute_translate_link, - viewThemeUtils.files.primaryColorToHexString(this), - getString(R.string.translation_link), - getString(R.string.community_contribute_translate_translate)) + " " + - getString(R.string.community_contribute_translate_text))); - - TextView contributeGithubView = binding.communityContributeGithubText; - contributeGithubView.setMovementMethod(LinkMovementMethod.getInstance()); - contributeGithubView.setText(Html.fromHtml( - getString(R.string.community_contribute_github_text, - getString(R.string.community_contribute_github_text_link, - viewThemeUtils.files.primaryColorToHexString(this), - getString(R.string.contributing_link))))); - - MaterialButton reportButton = binding.communityTestingReport; - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(reportButton); - reportButton.setOnClickListener(v -> DisplayUtils.startLinkIntent(this, R.string.report_issue_empty_link)); - - binding.communityBetaFdroid.setOnClickListener( - l -> DisplayUtils.startLinkIntent(this, R.string.fdroid_beta_link)); - - binding.communityReleaseCandidateFdroid.setOnClickListener( - l -> DisplayUtils.startLinkIntent(this, R.string.fdroid_link)); - - binding.communityReleaseCandidatePlaystore.setOnClickListener( - l -> DisplayUtils.startLinkIntent(this, R.string.play_store_register_beta)); - - binding.communityBetaApk.setOnClickListener( - l -> DisplayUtils.startLinkIntent(this, R.string.beta_apk_link)); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - boolean retval = true; - if (item.getItemId() == android.R.id.home) { - if (isDrawerOpen()) { - closeDrawer(); - } else { - openDrawer(); - } - } else { - retval = super.onOptionsItemSelected(item); - } - return retval; - } - - @Override - protected void onResume() { - super.onResume(); - - setDrawerMenuItemChecked(R.id.nav_community); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt new file mode 100644 index 0000000000..28dc94c67f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt @@ -0,0 +1,146 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * @author Tobias Kaminsky + * Copyright (C) 2016 Andy Scherzinger + * Copyright (C) 2016 Nextcloud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or 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.text.method.LinkMovementMethod +import android.view.MenuItem +import com.nextcloud.utils.extensions.setHtmlContent +import com.owncloud.android.R +import com.owncloud.android.databinding.CommunityLayoutBinding +import com.owncloud.android.utils.DisplayUtils + +/** + * Activity providing information about ways to participate in the app's development. + */ +open class CommunityActivity : DrawerActivity() { + lateinit var binding: CommunityLayoutBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = CommunityLayoutBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupToolbar() + updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_community)) + + setupDrawer(R.id.nav_community) + binding.communityReleaseCandidateText.movementMethod = LinkMovementMethod.getInstance() + setupContributeForumView() + setupContributeTranslationView() + setupContributeGithubView() + setupReportButton() + setOnClickListeners() + } + + private fun setupContributeForumView() { + val htmlContent = getString(R.string.community_contribute_forum_text) + " " + + getString( + R.string.community_contribute_forum_text_link, + viewThemeUtils.files + .primaryColorToHexString(this), + getString(R.string.help_link), + getString(R.string.community_contribute_forum_forum) + ) + binding.communityContributeForumText.setHtmlContent(htmlContent) + } + + private fun setupContributeTranslationView() { + val htmlContent = getString( + R.string.community_contribute_translate_link, + viewThemeUtils.files.primaryColorToHexString(this), + getString(R.string.translation_link), + getString(R.string.community_contribute_translate_translate) + ) + " " + + getString(R.string.community_contribute_translate_text) + binding.communityContributeTranslateText.setHtmlContent(htmlContent) + } + + private fun setupContributeGithubView() { + val htmlContent = getString( + R.string.community_contribute_github_text, + getString( + R.string.community_contribute_github_text_link, + viewThemeUtils.files.primaryColorToHexString(this), + getString(R.string.contributing_link) + ) + ) + binding.communityContributeGithubText.setHtmlContent(htmlContent) + } + + private fun setupReportButton() { + val reportButton = binding.communityTestingReport + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(reportButton) + reportButton.setOnClickListener { + DisplayUtils.startLinkIntent( + this, + R.string.report_issue_empty_link + ) + } + } + + private fun setOnClickListeners() { + binding.communityBetaFdroid.setOnClickListener { + DisplayUtils.startLinkIntent( + this, + R.string.fdroid_beta_link + ) + } + binding.communityReleaseCandidateFdroid.setOnClickListener { + DisplayUtils.startLinkIntent( + this, + R.string.fdroid_link + ) + } + binding.communityReleaseCandidatePlaystore.setOnClickListener { + DisplayUtils.startLinkIntent( + this, + R.string.play_store_register_beta + ) + } + binding.communityBetaApk.setOnClickListener { + DisplayUtils.startLinkIntent( + this, + R.string.beta_apk_link + ) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + var retval = true + if (item.itemId == android.R.id.home) { + if (isDrawerOpen) { + closeDrawer() + } else { + openDrawer() + } + } else { + retval = super.onOptionsItemSelected(item) + } + return retval + } + + override fun onResume() { + super.onResume() + setDrawerMenuItemChecked(R.id.nav_community) + } +} 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 074de51a10..a819c25fbd 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 com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment; 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 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag } transaction.commit(); } + + getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); } @Override @@ -137,12 +140,14 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag // 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 cb7013cd1c..16478cd5b6 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 @@ public class FileDisplayActivity extends FileActivity setUser(user); if (fileId == null) { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file)); + onFileRequestError(null); return; } @@ -2529,8 +2528,7 @@ public class FileDisplayActivity extends FileActivity setUser(user); if (filepath == null) { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file)); + onFileRequestError(null); return; } @@ -2544,8 +2542,7 @@ public class FileDisplayActivity extends FileActivity 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 @@ public class FileDisplayActivity extends FileActivity 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/FilePickerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt similarity index 55% rename from app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.java rename to app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt index 029bb13a82..7ed93b2e2b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt @@ -18,40 +18,29 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +package com.owncloud.android.ui.activity -package com.owncloud.android.ui.activity; - -import android.os.Bundle; -import android.view.View; - -import com.owncloud.android.R; -import com.owncloud.android.ui.fragment.OCFileListFragment; - -import androidx.fragment.app.FragmentTransaction; +import android.os.Bundle +import com.owncloud.android.R +import com.owncloud.android.ui.fragment.OCFileListFragment /** * File picker of remote files */ -public class FilePickerActivity extends FolderPickerActivity { +class FilePickerActivity : FolderPickerActivity() { - @Override - public void onClick(View v) { - super.onClick(v); - } - - @Override - protected void createFragments() { - OCFileListFragment listOfFiles = new OCFileListFragment(); - Bundle args = new Bundle(); - args.putBoolean(OCFileListFragment.ARG_ONLY_FOLDERS_CLICKABLE, true); - args.putBoolean(OCFileListFragment.ARG_HIDE_FAB, true); - args.putBoolean(OCFileListFragment.ARG_HIDE_ITEM_OPTIONS, true); - args.putBoolean(OCFileListFragment.ARG_SEARCH_ONLY_FOLDER, false); - args.putBoolean(OCFileListFragment.ARG_FILE_SELECTABLE, true); - args.putString(OCFileListFragment.ARG_MIMETYPE, getIntent().getStringExtra(OCFileListFragment.ARG_MIMETYPE)); - listOfFiles.setArguments(args); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.add(R.id.fragment_container, listOfFiles, TAG_LIST_OF_FOLDERS); - transaction.commit(); + override fun createFragments() { + val listOfFiles = OCFileListFragment() + val args = Bundle() + args.putBoolean(OCFileListFragment.ARG_ONLY_FOLDERS_CLICKABLE, true) + args.putBoolean(OCFileListFragment.ARG_HIDE_FAB, true) + args.putBoolean(OCFileListFragment.ARG_HIDE_ITEM_OPTIONS, true) + args.putBoolean(OCFileListFragment.ARG_SEARCH_ONLY_FOLDER, false) + args.putBoolean(OCFileListFragment.ARG_FILE_SELECTABLE, true) + args.putString(OCFileListFragment.ARG_MIMETYPE, intent.getStringExtra(OCFileListFragment.ARG_MIMETYPE)) + listOfFiles.arguments = args + val transaction = supportFragmentManager.beginTransaction() + transaction.add(R.id.fragment_container, listOfFiles, TAG_LIST_OF_FOLDERS) + transaction.commit() } } 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 6e829d547b..fced4e7cda 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() @@ -605,6 +616,7 @@ open class FolderPickerActivity : const val MOVE_OR_COPY = "MOVE_OR_COPY" const val CHOOSE_LOCATION = "CHOOSE_LOCATION" private val TAG = FolderPickerActivity::class.java.simpleName - protected const val TAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS" + + const val TAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS" } } 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 21bc674acc..0000000000 --- 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 0000000000..f0e5ec2cd8 --- /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/PassCodeActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java deleted file mode 100644 index 279b8cd9a0..0000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * ownCloud Android client application - * - * @author Bartek Przybylski - * @author masensio - * @author David A. Velasco - * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2015 ownCloud Inc. - * Copyright (C) 2020 Kwon Yuna - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.owncloud.android.ui.activity; - -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.View; -import android.view.Window; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; - -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.preferences.AppPreferences; -import com.owncloud.android.R; -import com.owncloud.android.authentication.PassCodeManager; -import com.owncloud.android.databinding.PasscodelockBinding; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.ui.components.PassCodeEditText; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.Arrays; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AppCompatActivity; - -public class PassCodeActivity extends AppCompatActivity implements Injectable { - - private static final String TAG = PassCodeActivity.class.getSimpleName(); - private static final String KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS"; - private static final String KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE"; - - public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT"; - public final static String ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT"; - public final static String ACTION_CHECK = "ACTION_CHECK"; - public final static String KEY_PASSCODE = "KEY_PASSCODE"; - public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT"; - - public final static String PREFERENCE_PASSCODE_D = "PrefPinCode"; - public final static String PREFERENCE_PASSCODE_D1 = "PrefPinCode1"; - public final static String PREFERENCE_PASSCODE_D2 = "PrefPinCode2"; - public final static String PREFERENCE_PASSCODE_D3 = "PrefPinCode3"; - public final static String PREFERENCE_PASSCODE_D4 = "PrefPinCode4"; - - @Inject AppPreferences preferences; - @Inject PassCodeManager passCodeManager; - @Inject ViewThemeUtils viewThemeUtils; - private PasscodelockBinding binding; - private final PassCodeEditText[] passCodeEditTexts = new PassCodeEditText[4]; - private String[] passCodeDigits = {"", "", "", ""}; - private boolean confirmingPassCode; - private boolean changed = true; // to control that only one blocks jump - - /** - * Initializes the activity. - *

- * An intent with a valid ACTION is expected; if none is found, an {@link IllegalArgumentException} will be thrown. - * - * @param savedInstanceState Previously saved state - irrelevant in this case - */ - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = PasscodelockBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - viewThemeUtils.platform.colorTextButtons(binding.cancel); - - passCodeEditTexts[0] = binding.txt0; - passCodeEditTexts[1] = binding.txt1; - passCodeEditTexts[2] = binding.txt2; - passCodeEditTexts[3] = binding.txt3; - - for (EditText passCodeEditText : passCodeEditTexts) { - viewThemeUtils.platform.colorEditText(passCodeEditText); - } - - passCodeEditTexts[0].requestFocus(); - - Window window = getWindow(); - if (window != null) { - window.setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); - } - - if (ACTION_CHECK.equals(getIntent().getAction())) { - /// this is a pass code request; the user has to input the right value - binding.header.setText(R.string.pass_code_enter_pass_code); - binding.explanation.setVisibility(View.INVISIBLE); - setCancelButtonEnabled(false); // no option to cancel - - showDelay(); - - } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) { - if (savedInstanceState != null) { - confirmingPassCode = savedInstanceState.getBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE); - passCodeDigits = savedInstanceState.getStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS); - } - if (confirmingPassCode) { - // the app was in the passcode confirmation - requestPassCodeConfirmation(); - } else { - // pass code preference has just been activated in SettingsActivity; - // will receive and confirm pass code value - binding.header.setText(R.string.pass_code_configure_your_pass_code); - - binding.explanation.setVisibility(View.VISIBLE); - } - setCancelButtonEnabled(true); - - } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - // pass code preference has just been disabled in SettingsActivity; - // will confirm user knows pass code, then remove it - binding.header.setText(R.string.pass_code_remove_your_pass_code); - binding.explanation.setVisibility(View.INVISIBLE); - setCancelButtonEnabled(true); - - } else { - throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to " + TAG); - } - - setTextListeners(); - } - - /** - * Enables or disables the cancel button to allow the user interrupt the ACTION requested to the activity. - * - * @param enabled 'True' makes the cancel button available, 'false' hides it. - */ - protected void setCancelButtonEnabled(boolean enabled) { - if (enabled) { - binding.cancel.setVisibility(View.VISIBLE); - binding.cancel.setOnClickListener(v -> finish()); - } else { - binding.cancel.setVisibility(View.INVISIBLE); - binding.cancel.setOnClickListener(null); - } - } - - @VisibleForTesting - public PasscodelockBinding getBinding() { - return binding; - } - - /** - * Binds the appropriate listeners to the input boxes receiving each digit of the pass code. - */ - protected void setTextListeners() { - for (int i = 0; i < passCodeEditTexts.length; i++) { - final PassCodeEditText editText = passCodeEditTexts[i]; - boolean isLast = (i == 3); - - editText.addTextChangedListener(new PassCodeDigitTextWatcher(i, isLast)); - if (i > 0) { - setOnKeyListener(i); - } - - int finalIndex = i; - editText.setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(finalIndex)); - } - } - - private void onPassCodeEditTextFocusChange(final int passCodeIndex) { - for (int i = 0; i < passCodeIndex; i++) { - if (TextUtils.isEmpty(passCodeEditTexts[i].getText())) { - passCodeEditTexts[i].requestFocus(); - break; - } - } - } - - private void setOnKeyListener(final int passCodeIndex) { - passCodeEditTexts[passCodeIndex].setOnKeyListener((v, keyCode, event) -> { - if (keyCode == KeyEvent.KEYCODE_DEL && changed) { - passCodeEditTexts[passCodeIndex - 1].requestFocus(); - if (!confirmingPassCode) { - passCodeDigits[passCodeIndex - 1] = ""; - } - passCodeEditTexts[passCodeIndex - 1].setText(""); - changed = false; - - } else if (!changed) { - changed = true; - } - return false; - }); - } - - /** - * Processes the pass code entered by the user just after the last digit was in. - *

- * Takes into account the action requested to the activity, the currently saved pass code and the previously typed - * pass code, if any. - */ - private void processFullPassCode() { - if (ACTION_CHECK.equals(getIntent().getAction())) { - if (checkPassCode()) { - preferences.resetPinWrongAttempts(); - - /// pass code accepted in request, user is allowed to access the app - passCodeManager.updateLockTimestamp(); - hideSoftKeyboard(); - finish(); - - } else { - preferences.increasePinWrongAttempts(); - - showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE); - } - - } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - if (checkPassCode()) { - passCodeManager.updateLockTimestamp(); - Intent resultIntent = new Intent(); - resultIntent.putExtra(KEY_CHECK_RESULT, true); - setResult(RESULT_OK, resultIntent); - hideSoftKeyboard(); - finish(); - } else { - showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE); - } - - } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) { - /// enabling pass code - if (!confirmingPassCode) { - requestPassCodeConfirmation(); - - } else if (confirmPassCode()) { - /// confirmed: user typed the same pass code twice - savePassCodeAndExit(); - - } else { - showErrorAndRestart(R.string.pass_code_mismatch, R.string.pass_code_configure_your_pass_code, View.VISIBLE); - } - } - } - - private void hideSoftKeyboard() { - View focusedView = getCurrentFocus(); - if (focusedView != null) { - InputMethodManager inputMethodManager = - (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow( - focusedView.getWindowToken(), - 0); - } - } - - private void showErrorAndRestart(int errorMessage, int headerMessage, int explanationVisibility) { - Arrays.fill(passCodeDigits, null); - Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show(); - binding.header.setText(headerMessage); // TODO check if really needed - binding.explanation.setVisibility(explanationVisibility); // TODO check if really needed - clearBoxes(); - - showDelay(); - } - - - /** - * Ask to the user for retyping the pass code just entered before saving it as the current pass code. - */ - protected void requestPassCodeConfirmation() { - clearBoxes(); - binding.header.setText(R.string.pass_code_reenter_your_pass_code); - binding.explanation.setVisibility(View.INVISIBLE); - confirmingPassCode = true; - } - - /** - * Compares pass code entered by the user with the value currently saved in the app. - * - * @return 'True' if entered pass code equals to the saved one. - */ - protected boolean checkPassCode() { - String[] savedPassCodeDigits = preferences.getPassCode(); - - boolean result = true; - for (int i = 0; i < passCodeDigits.length && result; i++) { - result = passCodeDigits[i] != null && passCodeDigits[i].equals(savedPassCodeDigits[i]); - } - return result; - } - - /** - * Compares pass code retyped by the user in the input fields with the value entered just before. - * - * @return 'True' if retyped pass code equals to the entered before. - */ - protected boolean confirmPassCode() { - confirmingPassCode = false; - - for (int i = 0; i < passCodeEditTexts.length; i++) { - Editable passCodeText = passCodeEditTexts[i].getText(); - if (passCodeText == null || !passCodeText.toString().equals(passCodeDigits[i])) { - return false; - } - } - return true; - } - - /** - * Sets the input fields to empty strings and puts the focus on the first one. - */ - protected void clearBoxes() { - for (EditText mPassCodeEditText : passCodeEditTexts) { - mPassCodeEditText.setText(""); - } - passCodeEditTexts[0].requestFocus(); - } - - /** - * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing than - * ACTION_CHECK may be worked around. - * - * @param keyCode Key code of the key that triggered the down event. - * @param event Event triggered. - * @return 'True' when the key event was processed by this method. - */ - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { - if (ACTION_CHECK.equals(getIntent().getAction())) { - moveTaskToBack(true); - finishAndRemoveTask(); - } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction()) || - ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - finish(); - }// else, do nothing, but report that the key was consumed to stay alive - return true; - } - return super.onKeyDown(keyCode, event); - } - - /** - * Saves the pass code input by the user as the current pass code. - */ - protected void savePassCodeAndExit() { - Intent resultIntent = new Intent(); - resultIntent.putExtra(KEY_PASSCODE, - passCodeDigits[0] + passCodeDigits[1] + passCodeDigits[2] + passCodeDigits[3]); - - setResult(RESULT_OK, resultIntent); - - passCodeManager.updateLockTimestamp(); - - finish(); - } - - private void showDelay() { - int delay = preferences.pinBruteForceDelay(); - - if (delay > 0) { - binding.explanation.setText(R.string.brute_force_delay); - binding.explanation.setVisibility(View.VISIBLE); - binding.txt0.setEnabled(false); - binding.txt1.setEnabled(false); - binding.txt2.setEnabled(false); - binding.txt3.setEnabled(false); - - new Thread(new Runnable() { - @Override - public void run() { - try { - Thread.sleep(delay * 1000L); - - runOnUiThread(() -> { - binding.explanation.setVisibility(View.INVISIBLE); - binding.txt0.setEnabled(true); - binding.txt1.setEnabled(true); - binding.txt2.setEnabled(true); - binding.txt3.setEnabled(true); - }); - } catch (InterruptedException e) { - Log_OC.e(this, "Could not delay password input prompt"); - } - } - }).start(); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE, confirmingPassCode); - outState.putStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS, passCodeDigits); - } - - private class PassCodeDigitTextWatcher implements TextWatcher { - - private int mIndex = -1; - private boolean mLastOne; - - /** - * Constructor - * - * @param index Position in the pass code of the input field that will be bound to this watcher. - * @param lastOne 'True' means that watcher corresponds to the last position of the pass code. - */ - PassCodeDigitTextWatcher(int index, boolean lastOne) { - mIndex = index; - mLastOne = lastOne; - - if (mIndex < 0) { - throw new IllegalArgumentException( - "Invalid index in " + PassCodeDigitTextWatcher.class.getSimpleName() + - " constructor" - ); - } - } - - private int next() { - return mLastOne ? 0 : mIndex + 1; - } - - /** - * Performs several actions when the user types a digit in an input field: - saves the input digit to the state - * of the activity; this will allow retyping the pass code to confirm it. - moves the focus automatically to the - * next field - for the last field, triggers the processing of the full pass code - * - * @param s Changed text - */ - @Override - public void afterTextChanged(Editable s) { - if (s.length() > 0) { - if (!confirmingPassCode) { - Editable passCodeText = passCodeEditTexts[mIndex].getText(); - - if (passCodeText != null) { - passCodeDigits[mIndex] = passCodeText.toString(); - } - } - - if (mLastOne) { - processFullPassCode(); - } else { - passCodeEditTexts[next()].requestFocus(); - } - - } else { - Log_OC.d(TAG, "Text box " + mIndex + " was cleaned"); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - } - -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt new file mode 100644 index 0000000000..db7f1f54a2 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt @@ -0,0 +1,437 @@ +/* + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author masensio + * @author David A. Velasco + * Copyright (C) 2011 Bartek Przybylski + * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2020 Kwon Yuna + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.owncloud.android.ui.activity + +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.R +import com.owncloud.android.authentication.PassCodeManager +import com.owncloud.android.databinding.PasscodelockBinding +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.components.PassCodeEditText +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.util.Arrays +import javax.inject.Inject + +@Suppress("TooManyFunctions", "MagicNumber") +class PassCodeActivity : AppCompatActivity(), Injectable { + + companion object { + private val TAG = PassCodeActivity::class.java.simpleName + + private const val KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS" + private const val KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE" + const val ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT" + const val ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT" + const val ACTION_CHECK = "ACTION_CHECK" + const val KEY_PASSCODE = "KEY_PASSCODE" + const val KEY_CHECK_RESULT = "KEY_CHECK_RESULT" + const val PREFERENCE_PASSCODE_D = "PrefPinCode" + const val PREFERENCE_PASSCODE_D1 = "PrefPinCode1" + const val PREFERENCE_PASSCODE_D2 = "PrefPinCode2" + const val PREFERENCE_PASSCODE_D3 = "PrefPinCode3" + const val PREFERENCE_PASSCODE_D4 = "PrefPinCode4" + } + + @JvmField + @Inject + var preferences: AppPreferences? = null + + @JvmField + @Inject + var passCodeManager: PassCodeManager? = null + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + @get:VisibleForTesting + lateinit var binding: PasscodelockBinding + private set + + private val passCodeEditTexts = arrayOfNulls(4) + private var passCodeDigits: Array? = arrayOf("", "", "", "") + private var confirmingPassCode = false + private var changed = true // to control that only one blocks jump + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = PasscodelockBinding.inflate(layoutInflater) + setContentView(binding.root) + + applyTint() + setupPasscodeEditTexts() + setSoftInputMode() + setupUI(savedInstanceState) + setTextListeners() + } + + private fun applyTint() { + viewThemeUtils?.platform?.colorViewBackground(binding.cardViewContent, ColorRole.SURFACE_VARIANT) + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(binding.cancel) + } + + private fun setupPasscodeEditTexts() { + passCodeEditTexts[0] = binding.txt0 + passCodeEditTexts[1] = binding.txt1 + passCodeEditTexts[2] = binding.txt2 + passCodeEditTexts[3] = binding.txt3 + + passCodeEditTexts.forEach { + it?.let { viewThemeUtils?.platform?.colorEditText(it) } + } + + passCodeEditTexts[0]?.requestFocus() + } + + private fun setSoftInputMode() { + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + } + + private fun setupUI(savedInstanceState: Bundle?) { + if (ACTION_CHECK == intent.action) { + // / this is a pass code request; the user has to input the right value + binding.header.setText(R.string.pass_code_enter_pass_code) + binding.explanation.visibility = View.INVISIBLE + setCancelButtonEnabled(false) // no option to cancel + showDelay() + } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { + if (savedInstanceState != null) { + confirmingPassCode = savedInstanceState.getBoolean(KEY_CONFIRMING_PASSCODE) + passCodeDigits = savedInstanceState.getStringArray(KEY_PASSCODE_DIGITS) + } + if (confirmingPassCode) { + // the app was in the passcode confirmation + requestPassCodeConfirmation() + } else { + // pass code preference has just been activated in SettingsActivity; + // will receive and confirm pass code value + binding.header.setText(R.string.pass_code_configure_your_pass_code) + binding.explanation.visibility = View.VISIBLE + } + setCancelButtonEnabled(true) + } else if (ACTION_CHECK_WITH_RESULT == intent.action) { + // pass code preference has just been disabled in SettingsActivity; + // will confirm user knows pass code, then remove it + binding.header.setText(R.string.pass_code_remove_your_pass_code) + binding.explanation.visibility = View.INVISIBLE + setCancelButtonEnabled(true) + } else { + throw IllegalArgumentException("A valid ACTION is needed in the Intent passed to $TAG") + } + } + + private fun setCancelButtonEnabled(enabled: Boolean) { + binding.cancel.visibility = if (enabled) { + View.VISIBLE + } else { + View.INVISIBLE + } + binding.cancel.setOnClickListener { + if (enabled) { + finish() + } + } + } + + private fun setTextListeners() { + for (i in passCodeEditTexts.indices) { + val editText = passCodeEditTexts[i] + val isLast = (i == 3) + + editText?.addTextChangedListener(PassCodeDigitTextWatcher(i, isLast)) + + if (i > 0) { + setOnKeyListener(i) + } + + editText?.onFocusChangeListener = View.OnFocusChangeListener { _: View?, _: Boolean -> + onPassCodeEditTextFocusChange(i) + } + } + } + + private fun onPassCodeEditTextFocusChange(passCodeIndex: Int) { + for (i in 0 until passCodeIndex) { + if (TextUtils.isEmpty(passCodeEditTexts[i]?.text)) { + passCodeEditTexts[i]?.requestFocus() + break + } + } + } + + private fun setOnKeyListener(passCodeIndex: Int) { + passCodeEditTexts[passCodeIndex]?.setOnKeyListener { _: View?, keyCode: Int, _: KeyEvent? -> + if (keyCode == KeyEvent.KEYCODE_DEL && changed) { + passCodeEditTexts[passCodeIndex - 1]?.requestFocus() + + if (!confirmingPassCode) { + passCodeDigits?.set(passCodeIndex - 1, "") + } + + passCodeEditTexts[passCodeIndex - 1]?.setText("") + + changed = false + } else if (!changed) { + changed = true + } + false + } + } + + /** + * Processes the pass code entered by the user just after the last digit was in. + * + * + * Takes into account the action requested to the activity, the currently saved pass code and the previously typed + * pass code, if any. + */ + private fun processFullPassCode() { + if (ACTION_CHECK == intent.action) { + if (checkPassCode()) { + preferences?.resetPinWrongAttempts() + + // / pass code accepted in request, user is allowed to access the app + passCodeManager?.updateLockTimestamp() + hideSoftKeyboard() + finish() + } else { + preferences?.increasePinWrongAttempts() + showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE) + } + } else if (ACTION_CHECK_WITH_RESULT == intent.action) { + if (checkPassCode()) { + passCodeManager?.updateLockTimestamp() + + val resultIntent = Intent() + resultIntent.putExtra(KEY_CHECK_RESULT, true) + setResult(RESULT_OK, resultIntent) + hideSoftKeyboard() + finish() + } else { + showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE) + } + } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { + // / enabling pass code + if (!confirmingPassCode) { + requestPassCodeConfirmation() + } else if (confirmPassCode()) { + // / confirmed: user typed the same pass code twice + savePassCodeAndExit() + } else { + showErrorAndRestart( + R.string.pass_code_mismatch, + R.string.pass_code_configure_your_pass_code, + View.VISIBLE + ) + } + } + } + + private fun hideSoftKeyboard() { + currentFocus?.let { + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow( + it.windowToken, + 0 + ) + } + } + + private fun showErrorAndRestart(errorMessage: Int, headerMessage: Int, explanationVisibility: Int) { + passCodeDigits?.let { Arrays.fill(it, null) } + + Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show() + binding.header.setText(headerMessage) // TODO check if really needed + binding.explanation.visibility = explanationVisibility // TODO check if really needed + clearBoxes() + showDelay() + } + + /** + * Ask to the user for retyping the pass code just entered before saving it as the current pass code. + */ + private fun requestPassCodeConfirmation() { + clearBoxes() + binding.header.setText(R.string.pass_code_reenter_your_pass_code) + binding.explanation.visibility = View.INVISIBLE + confirmingPassCode = true + } + + private fun checkPassCode(): Boolean { + val savedPassCodeDigits = preferences?.passCode + return passCodeDigits?.zip(savedPassCodeDigits.orEmpty()) { input, saved -> + input != null && input == saved + }?.all { it } ?: false + } + + private fun confirmPassCode(): Boolean { + return passCodeEditTexts.indices.all { i -> + passCodeEditTexts[i]?.text.toString() == passCodeDigits!![i] + } + } + + private fun clearBoxes() { + passCodeEditTexts.forEach { it?.text?.clear() } + passCodeEditTexts.firstOrNull()?.requestFocus() + } + + /** + * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing than + * ACTION_CHECK may be worked around. + * + * @param keyCode Key code of the key that triggered the down event. + * @param event Event triggered. + * @return 'True' when the key event was processed by this method. + */ + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) { + if (ACTION_CHECK == intent.action) { + moveTaskToBack(true) + finishAndRemoveTask() + } else if (ACTION_REQUEST_WITH_RESULT == intent.action || ACTION_CHECK_WITH_RESULT == intent.action) { + finish() + } // else, do nothing, but report that the key was consumed to stay alive + return true + } + return super.onKeyDown(keyCode, event) + } + + private fun savePassCodeAndExit() { + val resultIntent = Intent() + resultIntent.putExtra( + KEY_PASSCODE, + passCodeDigits!![0] + passCodeDigits!![1] + passCodeDigits!![2] + passCodeDigits!![3] + ) + setResult(RESULT_OK, resultIntent) + passCodeManager?.updateLockTimestamp() + finish() + } + + private fun showDelay() { + val delay = preferences?.pinBruteForceDelay() ?: 0 + + if (delay <= 0) { + return + } + + binding.explanation.setText(R.string.brute_force_delay) + binding.explanation.visibility = View.VISIBLE + binding.txt0.isEnabled = false + binding.txt1.isEnabled = false + binding.txt2.isEnabled = false + binding.txt3.isEnabled = false + + Thread(object : Runnable { + override fun run() { + try { + Thread.sleep(delay * 1000L) + + runOnUiThread { + binding.explanation.visibility = View.INVISIBLE + binding.txt0.isEnabled = true + binding.txt1.isEnabled = true + binding.txt2.isEnabled = true + binding.txt3.isEnabled = true + } + } catch (e: InterruptedException) { + Log_OC.e(this, "Could not delay password input prompt") + } + } + }).start() + } + + public override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(KEY_CONFIRMING_PASSCODE, confirmingPassCode) + outState.putStringArray(KEY_PASSCODE_DIGITS, passCodeDigits) + } + + private inner class PassCodeDigitTextWatcher(index: Int, lastOne: Boolean) : TextWatcher { + private var mIndex = -1 + private val mLastOne: Boolean + + init { + mIndex = index + mLastOne = lastOne + + require(mIndex >= 0) { + "Invalid index in " + PassCodeDigitTextWatcher::class.java.simpleName + + " constructor" + } + } + + private operator fun next(): Int { + return if (mLastOne) 0 else mIndex + 1 + } + + /** + * Performs several actions when the user types a digit in an input field: - saves the input digit to the state + * of the activity; this will allow retyping the pass code to confirm it. - moves the focus automatically to the + * next field - for the last field, triggers the processing of the full pass code + * + * @param s Changed text + */ + override fun afterTextChanged(s: Editable) { + if (s.isNotEmpty()) { + if (!confirmingPassCode) { + val passCodeText = passCodeEditTexts[mIndex]?.text + + if (passCodeText != null) { + passCodeDigits!![mIndex] = passCodeText.toString() + } + } + + if (mLastOne) { + processFullPassCode() + } else { + passCodeEditTexts[next()]?.requestFocus() + } + } else { + Log_OC.d(TAG, "Text box $mIndex was cleaned") + } + } + + @Suppress("EmptyFunctionBlock") + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + } + + @Suppress("EmptyFunctionBlock") + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java deleted file mode 100644 index 495445c6a0..0000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Tobias Kaminsky - * @author Chris Narkiewicz - * - * Copyright (C) 2018 Tobias Kaminsky - * Copyright (C) 2018 Nextcloud GmbH. - * Copyright (C) 2019 Chris Narkiewicz - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.ui.activity; - -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.webkit.JavascriptInterface; - -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.account.User; -import com.nextcloud.client.network.ClientFactory; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.operations.RichDocumentsCreateAssetOperation; -import com.owncloud.android.ui.asynctasks.PrintAsyncTask; -import com.owncloud.android.ui.asynctasks.RichDocumentsLoadUrlTask; -import com.owncloud.android.ui.fragment.OCFileListFragment; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.FileStorageUtils; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.lang.ref.WeakReference; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/** - * Opens document for editing via Richdocuments app in a web view - */ -public class RichDocumentsEditorWebView extends EditorWebView { - private static final int REQUEST_REMOTE_FILE = 100; - private static final String URL = "URL"; - private static final String HYPERLINK = "Url"; - private static final String TYPE = "Type"; - private static final String PRINT = "print"; - private static final String SLIDESHOW = "slideshow"; - private static final String NEW_NAME = "NewName"; - - @Inject - protected CurrentAccountProvider currentAccountProvider; - - @Inject - protected ClientFactory clientFactory; - - @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT_INTERFACE") - @Override - protected void postOnCreate() { - super.postOnCreate(); - - getWebView().addJavascriptInterface(new RichDocumentsMobileInterface(), "RichDocumentsMobileInterface"); - - // load url in background - loadUrl(getIntent().getStringExtra(EXTRA_URL)); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - } - - private void openFileChooser() { - Intent action = new Intent(this, FilePickerActivity.class); - action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/"); - startActivityForResult(action, REQUEST_REMOTE_FILE); - } - - @Override - protected void handleActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_REMOTE_FILE: - handleRemoteFile(data); - break; - - default: - // unexpected, do nothing - break; - } - - super.handleActivityResult(requestCode, resultCode, data); - } - - private void handleRemoteFile(Intent data) { - OCFile file = data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES); - - new Thread(() -> { - User user = currentAccountProvider.getUser(); - RichDocumentsCreateAssetOperation operation = new RichDocumentsCreateAssetOperation(file.getRemotePath()); - RemoteOperationResult result = operation.execute(user, this); - - if (result.isSuccess()) { - String asset = (String) result.getSingleData(); - - runOnUiThread(() -> getWebView().evaluateJavascript("OCA.RichDocuments.documentsMain.postAsset('" + - file.getFileName() + "', '" + asset + "');", null)); - } else { - runOnUiThread(() -> DisplayUtils.showSnackMessage(this, "Inserting image failed!")); - } - }).start(); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - outState.putString(EXTRA_URL, url); - super.onSaveInstanceState(outState); - } - - @Override - public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - url = savedInstanceState.getString(EXTRA_URL); - super.onRestoreInstanceState(savedInstanceState); - } - - @Override - protected void onResume() { - super.onResume(); - - getWebView().evaluateJavascript("if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " + - "{ OCA.RichDocuments.documentsMain.postGrabFocus(); }", - null); - } - - private void printFile(Uri url) { - OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount(); - - if (account == null) { - DisplayUtils.showSnackMessage(getWebView(), getString(R.string.failed_to_print)); - return; - } - - File targetFile = new File(FileStorageUtils.getTemporalPath(account.getName()) + "/print.pdf"); - - new PrintAsyncTask(targetFile, url.toString(), new WeakReference<>(this)).execute(); - } - - @Override - public void loadUrl(String url) { - if (TextUtils.isEmpty(url)) { - new RichDocumentsLoadUrlTask(this, getUser().get(), getFile()).execute(); - } else { - super.loadUrl(url); - } - } - - private void showSlideShow(Uri url) { - Intent intent = new Intent(this, ExternalSiteWebView.class); - intent.putExtra(ExternalSiteWebView.EXTRA_URL, url.toString()); - intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false); - intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_TOOLBAR, false); - startActivity(intent); - } - - private class RichDocumentsMobileInterface extends MobileInterface { - @JavascriptInterface - public void insertGraphic() { - openFileChooser(); - } - - @JavascriptInterface - public void documentLoaded() { - runOnUiThread(RichDocumentsEditorWebView.this::hideLoading); - } - - @JavascriptInterface - public void downloadAs(String json) { - try { - JSONObject downloadJson = new JSONObject(json); - - Uri url = Uri.parse(downloadJson.getString(URL)); - - switch (downloadJson.getString(TYPE)) { - case PRINT: - printFile(url); - break; - - case SLIDESHOW: - showSlideShow(url); - break; - - default: - downloadFile(url); - break; - } - } catch (JSONException e) { - Log_OC.e(this, "Failed to parse download json message: " + e); - } - } - - @JavascriptInterface - public void fileRename(String renameString) { - // when shared file is renamed in another instance, we will get notified about it - // need to change filename for sharing - try { - JSONObject renameJson = new JSONObject(renameString); - String newName = renameJson.getString(NEW_NAME); - getFile().setFileName(newName); - } catch (JSONException e) { - Log_OC.e(this, "Failed to parse rename json message: " + e); - } - } - - @JavascriptInterface - public void paste() { - // Javascript cannot do this by itself, so help out. - getWebView().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE)); - getWebView().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE)); - } - - @JavascriptInterface - public void hyperlink(String hyperlink) { - try { - String url = new JSONObject(hyperlink).getString(HYPERLINK); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - startActivity(intent); - } catch (JSONException e) { - Log_OC.e(this, "Failed to parse download json message: " + e); - } - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt b/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt new file mode 100644 index 0000000000..c2697bf7cd --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt @@ -0,0 +1,244 @@ +/* + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * @author Chris Narkiewicz + * + * Copyright (C) 2018 Tobias Kaminsky + * Copyright (C) 2018 Nextcloud GmbH. + * Copyright (C) 2019 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.ui.activity + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.view.KeyEvent +import android.webkit.JavascriptInterface +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.client.network.ClientFactory +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RichDocumentsCreateAssetOperation +import com.owncloud.android.ui.asynctasks.PrintAsyncTask +import com.owncloud.android.ui.asynctasks.RichDocumentsLoadUrlTask +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.FileStorageUtils +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.lang.ref.WeakReference +import javax.inject.Inject + +/** + * Opens document for editing via Richdocuments app in a web view + */ +class RichDocumentsEditorWebView : EditorWebView() { + @JvmField + @Inject + var currentAccountProvider: CurrentAccountProvider? = null + + @JvmField + @Inject + var clientFactory: ClientFactory? = null + + private var activityResult: ActivityResultLauncher? = null + + @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT_INTERFACE") + override fun postOnCreate() { + super.postOnCreate() + + webView.addJavascriptInterface(RichDocumentsMobileInterface(), "RichDocumentsMobileInterface") + + intent.getStringExtra(EXTRA_URL)?.let { + loadUrl(it) + } + + registerActivityResult() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + } + + private fun openFileChooser() { + val action = Intent(this, FilePickerActivity::class.java) + action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/") + activityResult?.launch(action) + } + + private fun registerActivityResult() { + activityResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (RESULT_OK == result.resultCode) { + result.data?.let { + handleRemoteFile(it) + } + } + } + } + + private fun handleRemoteFile(data: Intent) { + val file = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES, OCFile::class.java) + } else { + @Suppress("DEPRECATION") + data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES) + } + + Thread { + val user = currentAccountProvider?.user + val operation = RichDocumentsCreateAssetOperation(file?.remotePath) + val result = operation.execute(user, this) + if (result.isSuccess) { + val asset = result.singleData as String + runOnUiThread { + webView.evaluateJavascript( + "OCA.RichDocuments.documentsMain.postAsset('" + + file?.fileName + "', '" + asset + "');", + null + ) + } + } else { + runOnUiThread { DisplayUtils.showSnackMessage(this, "Inserting image failed!") } + } + }.start() + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putString(EXTRA_URL, url) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + url = savedInstanceState.getString(EXTRA_URL) + super.onRestoreInstanceState(savedInstanceState) + } + + override fun onResume() { + super.onResume() + webView.evaluateJavascript( + "if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " + + "{ OCA.RichDocuments.documentsMain.postGrabFocus(); }", + null + ) + } + + private fun printFile(url: Uri) { + val account = accountManager.currentOwnCloudAccount + if (account == null) { + DisplayUtils.showSnackMessage(webView, getString(R.string.failed_to_print)) + return + } + val targetFile = File(FileStorageUtils.getTemporalPath(account.name) + "/print.pdf") + PrintAsyncTask(targetFile, url.toString(), WeakReference(this)).execute() + } + + public override fun loadUrl(url: String) { + if (TextUtils.isEmpty(url)) { + RichDocumentsLoadUrlTask(this, user.get(), file).execute() + } else { + super.loadUrl(url) + } + } + + private fun showSlideShow(url: Uri) { + val intent = Intent(this, ExternalSiteWebView::class.java) + intent.putExtra(EXTRA_URL, url.toString()) + intent.putExtra(EXTRA_SHOW_SIDEBAR, false) + intent.putExtra(EXTRA_SHOW_TOOLBAR, false) + startActivity(intent) + } + + private inner class RichDocumentsMobileInterface : MobileInterface() { + @JavascriptInterface + fun insertGraphic() { + openFileChooser() + } + + @JavascriptInterface + fun documentLoaded() { + runOnUiThread { hideLoading() } + } + + @JavascriptInterface + fun downloadAs(json: String?) { + try { + json ?: return + val downloadJson = JSONObject(json) + val url = Uri.parse(downloadJson.getString(URL)) + when (downloadJson.getString(TYPE)) { + PRINT -> printFile(url) + SLIDESHOW -> showSlideShow(url) + else -> downloadFile(url) + } + } catch (e: JSONException) { + Log_OC.e(this, "Failed to parse download json message: $e") + } + } + + @JavascriptInterface + fun fileRename(renameString: String?) { + // when shared file is renamed in another instance, we will get notified about it + // need to change filename for sharing + try { + renameString ?: return + val renameJson = JSONObject(renameString) + val newName = renameJson.getString(NEW_NAME) + file.fileName = newName + } catch (e: JSONException) { + Log_OC.e(this, "Failed to parse rename json message: $e") + } + } + + @JavascriptInterface + fun paste() { + // Javascript cannot do this by itself, so help out. + webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE)) + webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE)) + } + + @JavascriptInterface + fun hyperlink(hyperlink: String?) { + try { + hyperlink ?: return + val url = JSONObject(hyperlink).getString(HYPERLINK) + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(url) + startActivity(intent) + } catch (e: JSONException) { + Log_OC.e(this, "Failed to parse download json message: $e") + } + } + } + + companion object { + private const val URL = "URL" + private const val HYPERLINK = "Url" + private const val TYPE = "Type" + private const val PRINT = "print" + private const val SLIDESHOW = "slideshow" + private const val NEW_NAME = "NewName" + } +} 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 bff39ae57e..44942b1b3a 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 java.util.List; 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 class UploadFilesActivity extends DrawerActivity implements LocalFileList checkWritableFolder(mCurrentDir); + getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); + Log_OC.d(TAG, "onCreate() end"); } @@ -369,43 +372,45 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList } } - @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 0298ef49bc..821d44ac3c 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.lib.common.utils.Log_OC; 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 class UploadListAdapter extends SectionedRecyclerViewAdapter 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 class UploadListAdapter extends SectionedRecyclerViewAdapter 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 class UploadListAdapter extends SectionedRecyclerViewAdapter. --> - - + android:padding="@dimen/standard_padding"> - - + - + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d74e021324..3874b9b1c1 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -325,7 +325,6 @@ حذف خطأ في استرداد النشاطات للملف حدث خطأ في تحميل التفاصيل - تنزيل \u0020 الملف حفظ قم برفع بعض المحتوى أو زامن مع أجهزتك diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index f8611008a3..179ba7581d 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -50,7 +50,7 @@ Adresa serveru https://… Nesprávný formát adresy serveru Server nenalezen - Žádné síťové spojení + Nepřipojeno k síti Zabezpečené spojení není k dispozici. Nesprávně formulované nastavení pro server Neúspěšné ověření se @@ -163,11 +163,11 @@ Ikona uživatele v seznamu kontaktů Nejsou udělena oprávnění, proto nebylo nic naimportováno. Kontakty - Zálohovat nyní + Zazálohovat nyní Zálohování naplánováno a brzy začne Import naplánován a brzy začne Nenalezen žádný soubor - Nepodařilo se najít vaši poslední zálohu! + Nepodařilo se najít vaši nejaktuálnější zálohu! Zkopírováno do schránky Při pokusu o zkopírování tohoto souboru či složky došlo k chybě Není možné zkopírovat složku do některé z jejích vlastních podsložek @@ -325,7 +325,6 @@ Smazat Při načítání aktivit u souboru došlo k chybě Nepodařilo se načíst podrobnosti. - Stahování \u0020 Soubor Ponechat Nahrajte nějaký obsah, nebo synchronizujte s vašimi zařízeními. @@ -363,7 +362,7 @@ Přesouvání dat… Dokončeno Nahradit - Příprava migrace… + Příprava stěhování… Obnovování nastavení účtu… Ukládání nastavení účtu… Opravdu chcete změnit složku pro ukládání dat na %1$s?\n\nPoznámka: Všechna data bude třeba znovu stáhnout. @@ -598,7 +597,7 @@ Upozorňovat na nově nalezené složky s médii GNU General Public License, verze 2 Nápověda - Imprint + Impresum Původní soubor bude… Původní soubor bude… Ukládat v podsložkách podle data @@ -935,7 +934,7 @@ Adresa E-mail Telefonní číslo - Twitter + X (Twitter) Webové stránky Chyba při načítání informací o uživateli Nezadány žádné osobní údaje diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 5c024cac6f..cf4e85d7e5 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -325,7 +325,6 @@ Slet Fejl ved indlæsning af aktiviteter for fil Fejl ved indlæsning af detaljer - Downloader \u0020 Fil Behold Upload indhold eller synkronisér med dine enheder. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e6c62482f2..fe479e5e48 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -325,7 +325,6 @@ Löschen Fehler beim Abrufen der Aktivitäten für die Datei Fehler beim Laden der Details - Herunterladen \u0020 Datei Behalten Laden Sie Inhalt hoch oder synchronisieren Sie mit Ihren Geräten. diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml index d2e5257b43..a8ab730dfe 100644 --- a/app/src/main/res/values-es-rCL/strings.xml +++ b/app/src/main/res/values-es-rCL/strings.xml @@ -239,6 +239,7 @@ Crear No hay carpetas aquí Seleccionar + Mover No se te permite %s para copiar este archivo para crear este archivo diff --git a/app/src/main/res/values-es-rSV/strings.xml b/app/src/main/res/values-es-rSV/strings.xml index 9652d8bf0e..c4dbc29b1b 100644 --- a/app/src/main/res/values-es-rSV/strings.xml +++ b/app/src/main/res/values-es-rSV/strings.xml @@ -540,6 +540,7 @@ Aguarda un momento… Verificando credenciales almacenadas Copiando el archivo desde almacenamiento privado + Actualizar Imagen de qué es nuevo Omitir Nuevo en %1$s diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml index 845e7deb5e..ddf8681eea 100644 --- a/app/src/main/res/values-et-rEE/strings.xml +++ b/app/src/main/res/values-et-rEE/strings.xml @@ -525,6 +525,7 @@ Lae alla Oota üks hetk… Faili kopeerimine privaatsest salvestusalast + Uuenda Jäta vahele Mis on su staatus? Saada kiri diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index d88f97b2de..0cb6b3e19c 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -393,6 +393,7 @@ Ez dago karpetarik hemen Aukeratu Aukeratu helburuko karpeta + Kopiatu Mugitu Ez daukazu baimenik %s fitxategi hau kopiatzeko @@ -943,6 +944,7 @@ Itxaron momentu bat… Gordetako nortasun-datuak konprobatzen Fitxategia biltegiratze pribatutik kopiatzen + Eguneratu Zer da irudi berria Salto egin Berria %1$s-n diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index bf89788c8a..ef26443935 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -393,6 +393,7 @@ هیچ پوشه ای اینجا وجود ندارد انتخاب کردن پوشهٔ هدف را انتخاب کنید + رونوشت انتقال شما مجاز نیستید%s کپی این فایل @@ -943,6 +944,7 @@ یک لحظه صبر کنید... بررسی اعتبارنامه‌های ذخیره شده کپی کردن فایل از حافظه خصوصی + به‌روز رسانی چه تصویر جدیدی است رد شدن جدید در %1$s diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 0352263ba1..d516221c7b 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -381,6 +381,7 @@ Ei kansioita täällä Valitse Valitse kohdekansio + Kopioi Siirrä Sinulla ei ole oikeutta %s kopioida tämä tiedosto @@ -906,6 +907,7 @@ GNU yleinen lisenssi, versio 2 Odota hetki… Tarkistetaan tallennettuja tilitietoja Kopioidaan tiedostoa yksityisestä tallennustilasta + Päivitä Mitä uutta -kuva Ohita Uutta versiossa %1$s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0c0202b9fc..03e2d5ca0e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -327,7 +327,6 @@ Attention, la suppression est irréversible. Supprimer Erreur lors de la récupération de l’activité du fichier Impossible de charger les détails - Téléchargement de \u0020 Fichier Conserver Déposez du contenu ou synchronisez vos appareils. diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 5117e01527..0c6f74392c 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -325,7 +325,6 @@ Eliminar Produciuse un erro ao recuperar actividades para o ficheiro Produciuse un fallo ao cargar os detalles - Descargando \u0020 Ficheiro Conservar Envíe algún contido ou sincronice cos seus dispositivos. diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 2e77e1d75e..41bd2a80b1 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -18,6 +18,7 @@ Másolás Új mappa Áthelyezés + Áthelyezés vagy Másolás Megnyitás ezzel Keresés Részletek @@ -962,7 +963,9 @@ A Nextcloud itt érhető el: https://nextcloud.com Egy pillanat… Tárolt hitelesítő adatok ellenőrzése Fájl másolása a privát tárolóról + A bejelentkezéshez frissítse az Android System WebView alkalmazást Frissítés + Frissítse az Android System WebView-t Újdonságok kép Kihagyás Új itt: %1$s diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index e33b3fbc76..1671fe22e9 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -18,6 +18,7 @@ Kopier Ny mappe Flytt + Flytt eller kopier Åpne med Søk Detaljer @@ -425,6 +426,13 @@ Roter mot klokken Roter med klokken Kan ikke endre bildet. + Fildetaljer + Betingelser for å ta bilder + ƒ/%s + ISO %s + %s MP + %s mm + %s s i mappen %1$s Last også opp eksisterende filer Bare last opp under lading @@ -505,6 +513,7 @@ Ingen kalender finnes Ingen program for å behandle e-post adresse Ingen gjenstander + Ingen app tilgjengelig for å håndtere kart Kun én konto er tillatt Ingen app tilgjengelig for å håndtere PDF Ingen program for å sende valgt fil @@ -591,7 +600,9 @@ Avtrykk Opprinnelig fil vil bli… Opprinnelig fil vil bli… + Lagre i undermapper basert på dato Bruk undermapper + Alternativer for undermappe Legg til ende-til-ende -kryptering på denne klienten Lisens Appsikkerhet @@ -781,6 +792,8 @@ Intern strøming ikke mulig Vennligst last ned media i stedet, eller bruk ekstern app. Streng modus: ingen HTTP-tilkobling tillatt! + År/Måned/Dag + År/Måned År \"%1$s\" er blitt delt med deg %1$s delte \"%2$s\" med deg @@ -932,7 +945,9 @@ Vent et øyeblikk… Sjekker lagrede påloggingsdetaljer Kopierer fil fra privat lager + Vennligst oppdater Android systemets WebView-app for pålogging Oppdater + Oppdater Android systemets WebView Hva er nytt-bilde Hopp over Nytt i %1$s diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e6198a5a30..bce95d1a08 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 @@ -944,7 +945,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-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c406d92a02..09b3d35379 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -325,7 +325,6 @@ Удалить Ошибка получения истории событий, связанных с файлом Не удалось получить подробные сведения - Скачивание \u0020 Файл Сохранить Добавьте что-нибудь или синхронизируйте со своими устройствами! diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 36ad0cb6c7..295eeef96f 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -325,7 +325,6 @@ Обриши Грешка при добављању активности за фајл Грешка при учитавању детаља - Преузима се \u0020 Фајл Задржи Отпремите неки садржај или синхронизујте са вашим уређајима. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 23cb596699..84ab136db7 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -325,7 +325,6 @@ Ta bort Fel vid hämtning av aktiviteter för fil Kunde inte läsa in detaljer - Laddar ner \u0020 Fil Behåll Ladda upp något eller synkronisera med dina enheter diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3384b39a1c..cc435ed7d9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -325,7 +325,6 @@ Sil Dosya işlemleri alınırken sorun çıktı Ayrıntılar yüklenemedi - İndiriliyor \u0020 Dosya Tut Bazı içerikler yükleyin ya da aygıtlarınızla eşitleyin. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8ed39ee928..c66b1fde90 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -323,7 +323,6 @@ Вилучити Помилка з отриманням дії для файлу Не вдалося завантажити подробиці - Звантаження \u0020 Файл Зберегти Додати дані або синхронізувати з вашими пристроями. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8a2916edba..4651365872 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -325,7 +325,6 @@ 删除 获取文件动态时出错 加载详情失败 - 下载中 \u0020 文件 保留 上传一些内容或与您的设备同步。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 52f42a3065..8f94353c5c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -325,7 +325,6 @@ 刪除 取得檔案活動時發生錯誤 載入詳細資訊失敗 - 正在下載 \u0020 檔案 保留 上傳一些內容或與您的裝置同步。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d552f8c69b..4c293c2b93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -162,7 +162,6 @@ Fetching server version… Waiting to upload %1$s (%2$d) - Downloading \u0020 Downloading… %1$d%% Downloading %2$s Downloaded diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c8383be196..ec14ded2e1 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 5a9380573e..f1f77f43e8 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 diff --git a/gradle.properties b/gradle.properties index 85bc2ba7da..5b47fafb59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,5 +9,10 @@ android.nonTransitiveRClass=false android.nonFinalResIds=false #android.debug.obsoleteApi=true -# Minimum max heap space to get reliable builds -org.gradle.jvmargs=-Xmx1g + +# JVM arguments to optimize heap usage, enable heap dump on out-of-memory errors, and set the file encoding +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +kotlin.daemon.jvmargs=-Xmx4096m +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.configureondemand=true diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index b8212f767a..17a46e7806 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 75 warnings + Lint Report: 74 warnings