From 711ecdfab84c26c0f85fc5c2844e6c453bb1e84b Mon Sep 17 00:00:00 2001 From: Hannes Weilharter Date: Wed, 8 Jul 2026 09:21:46 +0200 Subject: [PATCH 1/5] CATROID-1662 add undo functionality for project deletion --- .../catroid/ui/ProjectListActivity.kt | 25 +++- .../ui/fragment/ProjectOptionsFragment.kt | 4 +- .../fragment/ProjectListFragment.kt | 141 +++++++++++++++++- .../main/res/menu/menu_projects_activity.xml | 9 +- catroid/src/main/res/values/strings.xml | 1 + 5 files changed, 167 insertions(+), 13 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectListActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectListActivity.kt index b134fbfd5ad..7d0ba7a19de 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectListActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectListActivity.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -34,9 +34,14 @@ import org.catrobat.catroid.ui.recyclerview.fragment.ProjectListFragment class ProjectListActivity : BaseCastActivity() { private lateinit var binding: ActivityRecyclerBinding + private var isUndoMenuItemVisible = false public override fun onCreate(savedInstanceState: Bundle?) { + if (savedInstanceState != null) { + savedInstanceStateExpected = true + } super.onCreate(savedInstanceState) + isUndoMenuItemVisible = savedInstanceState?.getBoolean(BUNDLE_IS_UNDO_MENU_ITEM_VISIBLE, false) ?: false binding = ActivityRecyclerBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar.toolbar) @@ -73,17 +78,34 @@ class ProjectListActivity : BaseCastActivity() { override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_projects_activity, menu) menu.findItem(R.id.merge).isVisible = BuildConfig.FEATURE_MERGE_ENABLED + menu.findItem(R.id.menu_undo).isVisible = isUndoMenuItemVisible return super.onCreateOptionsMenu(menu) } + fun showUndo(visible: Boolean) { + isUndoMenuItemVisible = visible + try { + optionsMenu.findItem(R.id.menu_undo).isVisible = visible + } catch (_: UninitializedPropertyAccessException) { + invalidateOptionsMenu() + } + } + override fun onBackPressed() { if (supportFragmentManager.backStackEntryCount > 0) { supportFragmentManager.popBackStack() } else { + (supportFragmentManager.findFragmentByTag(ProjectListFragment.TAG) as? ProjectListFragment) + ?.clearDeletedProjectUndo() super.onBackPressed() } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(BUNDLE_IS_UNDO_MENU_ITEM_VISIBLE, isUndoMenuItemVisible) + } + @Suppress("UNUSED_PARAMETER") fun handleAddButton(view: View?) { val dialog = NewProjectDialogFragment() @@ -93,5 +115,6 @@ class ProjectListActivity : BaseCastActivity() { companion object { const val IMPORT_LOCAL_INTENT: String = "merge" val TAG: String = ProjectListActivity::class.java.simpleName + private const val BUNDLE_IS_UNDO_MENU_ITEM_VISIBLE = "isUndoMenuItemVisible" } } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt index f8e62a799ee..acb85e10c54 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt @@ -53,7 +53,6 @@ import org.catrobat.catroid.databinding.FragmentProjectOptionsBinding import org.catrobat.catroid.io.StorageOperations import org.catrobat.catroid.io.XstreamSerializer import org.catrobat.catroid.io.asynctask.ProjectExporter -import org.catrobat.catroid.io.asynctask.ProjectSaver import org.catrobat.catroid.io.asynctask.loadProject import org.catrobat.catroid.io.asynctask.renameProject import org.catrobat.catroid.io.asynctask.saveProjectSerial @@ -109,7 +108,7 @@ class ProjectOptionsFragment : Fragment() { addTextChangedListener(object : NewProjectNameTextWatcher() { override fun afterTextChanged(s: Editable?) { val error = if (s.toString() != project!!.name) { - validateInput(s.toString(), getContext()) + validateInput(s.toString(), context) } else { null } @@ -190,7 +189,6 @@ class ProjectOptionsFragment : Fragment() { ) AlertDialog.Builder(requireContext()) .setTitle(resources.getQuantityString(R.plurals.delete_projects, 1)) - .setMessage(R.string.dialog_confirm_delete) .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> deleteProject( projectData diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt index 7f3bcebf9dc..e65dfe99c90 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt @@ -40,6 +40,7 @@ import android.view.View import androidx.annotation.PluralsRes import androidx.annotation.RequiresApi import androidx.core.graphics.drawable.toDrawable +import androidx.appcompat.app.AlertDialog import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -71,6 +72,7 @@ import org.catrobat.catroid.ui.recyclerview.adapter.RVAdapter import org.catrobat.catroid.ui.recyclerview.adapter.multiselection.MultiSelectionManager import org.catrobat.catroid.ui.recyclerview.viewholder.CheckableViewHolder import org.catrobat.catroid.ui.runtimepermissions.RequiresPermissionTask +import org.catrobat.catroid.utils.FileMetaDataExtractor import org.catrobat.catroid.ui.shortcut.ShortcutHelper import org.catrobat.catroid.utils.ToastUtil import org.koin.android.ext.android.inject @@ -110,6 +112,7 @@ class ProjectListFragment( } actionModeType = IMPORT_LOCAL } + showUndo(hasDeletedProjectUndo()) } private fun onImportProjectFinished(success: Boolean) { @@ -185,6 +188,7 @@ class ProjectListFragment( showPinShortcutDialog(projectName, icon) } + showUndo(hasDeletedProjectUndo()) BottomBar.showBottomBar(requireActivity()) super.onResume() } @@ -211,6 +215,7 @@ class ProjectListFragment( override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { + R.id.menu_undo -> restoreDeletedProject() R.id.import_project -> showImportChooser() R.id.sort_projects -> sortProjects() else -> return super.onOptionsItemSelected(item) @@ -218,6 +223,10 @@ class ProjectListFragment( return true } + private fun showUndo(visible: Boolean) { + (activity as? ProjectListActivity)?.showUndo(visible) + } + private fun sortProjects() { if (adapter != null) { adapter.projectsSorted = !adapter.projectsSorted @@ -387,6 +396,15 @@ class ProjectListFragment( @PluralsRes override fun getDeleteAlertTitleId(): Int = R.plurals.delete_projects + override fun showDeleteAlert(selectedItems: MutableList?) { + AlertDialog.Builder(requireContext()) + .setTitle(resources.getQuantityString(getDeleteAlertTitleId(), selectedItems?.size ?: 0)) + .setPositiveButton(R.string.delete) { _, _ -> deleteItems(selectedItems) } + .setNegativeButton(R.string.cancel, null) + .setCancelable(false) + .show() + } + override fun deleteItems(selectedItems: MutableList?) { setShowProgressBar(true) var deletedItemCount = 0 @@ -394,32 +412,136 @@ class ProjectListFragment( selectedItems ?: return for (item in selectedItems) { item ?: continue + if (!copyProjectForUndoOption(item)) { + ToastUtil.showError(requireContext(), R.string.error_copy_project) + continue + } try { projectManager.deleteDownloadedProjectInformation(item.name) StorageOperations.deleteDir(item.directory) items.remove(item) deletedProjectNames.add(item.name) + adapter.remove(item) + deletedItemCount++ } catch (e: IOException) { Log.e(TAG, Log.getStackTraceString(e)) + clearDeletedProjectUndo() } - adapter.remove(item) - deletedItemCount++ } if (deletedProjectNames.isNotEmpty()) { ShortcutHelper.removeShortcutsForProjects(requireContext(), deletedProjectNames) } - ToastUtil.showSuccess( - requireContext(), resources.getQuantityString( - R.plurals.deleted_projects, - deletedItemCount, - deletedItemCount + if (deletedItemCount > 0) { + ToastUtil.showSuccess( + requireContext(), resources.getQuantityString( + R.plurals.deleted_projects, + deletedItemCount, + deletedItemCount + ) ) - ) + } + showUndo(deletedItemCount > 0 && hasDeletedProjectUndo()) finishActionMode() setAdapterItems(adapter.projectsSorted) checkForEmptyList() } + private fun restoreDeletedProject() { + setShowProgressBar(true) + coroutineScope.launch { + val restored = restoreDeletedProjectFromUndo() + withContext(mainDispatcher) { + if (restored) { + ToastUtil.showSuccess(requireContext(), R.string.restored_project) + showUndo(false) + getLocalProjectListAsync(object: LoadProjectsListener { + override fun onProjectsLoaded() { + setAdapterItems(adapter.projectsSorted) + setShowProgressBar(false) + checkForEmptyList() + } + }) + } else { + ToastUtil.showError(requireContext(), R.string.error_load_project) + showUndo(hasDeletedProjectUndo()) + setShowProgressBar(false) + } + } + } + } + + private fun restoreDeletedProjectFromUndo(): Boolean { + val backupProjectDir = deletedProjectBackupDirectory + val backupCodeFile = File(backupProjectDir, Constants.CODE_XML_FILE_NAME) + if (!backupCodeFile.exists()) { + return false + } + + return try { + val restoredProjectData = ProjectMetaDataParser(backupCodeFile).projectMetaData + val destinationName = uniqueNameProvider.getUniqueNameInNameables( + restoredProjectData.name, + items + ) + val destinationDir = File( + FlavoredConstants.DEFAULT_ROOT_DIRECTORY, + FileMetaDataExtractor.encodeSpecialCharsForFileSystem(destinationName) + ) + StorageOperations.copyDir(backupProjectDir, destinationDir) + if (destinationName != restoredProjectData.name) { + XstreamSerializer.renameProject( + File(destinationDir, Constants.CODE_XML_FILE_NAME), + destinationName + ) + } + projectManager.addNewDownloadedProject(destinationName) + deleteDeletedProjectUndoDirectory() + true + } catch (e: IOException) { + Log.e(TAG, "Cannot restore deleted project.", e) + false + } + } + + private fun copyProjectForUndoOption(projectData: ProjectData): Boolean { + return try { + clearDeletedProjectUndo() + if (!deletedProjectUndoDirectory.mkdirs() && !deletedProjectUndoDirectory.isDirectory) { + return false + } + StorageOperations.copyDir(projectData.directory, deletedProjectBackupDirectory) + true + } catch (e: IOException) { + Log.e(TAG, "Cannot copy project ${projectData.name} for undo.", e) + clearDeletedProjectUndo() + false + } + } + + fun clearDeletedProjectUndo() { + deleteDeletedProjectUndoDirectory() + showUndo(false) + } + + private fun deleteDeletedProjectUndoDirectory() { + try { + if (deletedProjectUndoDirectory.exists()) { + StorageOperations.deleteDir(deletedProjectUndoDirectory) + } + } catch (e: IOException) { + Log.e(TAG, "Cannot clear deleted project undo.", e) + } + } + + private fun hasDeletedProjectUndo(): Boolean = + File(deletedProjectBackupDirectory, Constants.CODE_XML_FILE_NAME).exists() + + private val deletedProjectUndoDirectory: File + get() = File(FlavoredConstants.DEFAULT_ROOT_DIRECTORY, PROJECT_DELETION_UNDO_DIRECTORY) + + private val deletedProjectBackupDirectory: File + get() = File(deletedProjectUndoDirectory, PROJECT_DELETION_BACKUP_DIRECTORY) + fun checkForEmptyList() { if (adapter.items.isEmpty()) { setShowProgressBar(true) @@ -460,6 +582,7 @@ class ProjectListFragment( override fun onLoadFinished(success: Boolean) { if (success) { + clearDeletedProjectUndo() val intent = Intent(requireContext(), ProjectActivity::class.java) intent.putExtra( ProjectActivity.EXTRA_FRAGMENT_POSITION, @@ -620,6 +743,8 @@ class ProjectListFragment( val TAG: String = ProjectListFragment::class.java.simpleName private const val PERMISSIONS_REQUEST_IMPORT_FROM_EXTERNAL_STORAGE = 801 private const val REQUEST_IMPORT_PROJECT = 7 + private const val PROJECT_DELETION_UNDO_DIRECTORY = ".projectDeletionUndo" + private const val PROJECT_DELETION_BACKUP_DIRECTORY = "project" @JvmStatic fun getLocalProjectList(items: MutableList) { diff --git a/catroid/src/main/res/menu/menu_projects_activity.xml b/catroid/src/main/res/menu/menu_projects_activity.xml index 85aaa8fbf76..407f895a42d 100644 --- a/catroid/src/main/res/menu/menu_projects_activity.xml +++ b/catroid/src/main/res/menu/menu_projects_activity.xml @@ -1,7 +1,7 @@ Renamed project + Restored project From c106256e6915a49751de778ea91fc000e8423e0b Mon Sep 17 00:00:00 2001 From: Hannes Weilharter Date: Wed, 8 Jul 2026 19:16:28 +0200 Subject: [PATCH 2/5] CATROID-1662 add testcases --- .../ui/fragment/DeleteProjectTest.java | 88 +++++++++++++------ 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.java b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.java index d27b428e885..95e6ec6561e 100644 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.java +++ b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.java @@ -52,6 +52,7 @@ import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; +import static androidx.test.espresso.Espresso.pressBack; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; @@ -64,10 +65,9 @@ public class DeleteProjectTest { @Rule - public BaseActivityTestRule baseActivityTestRule = new - BaseActivityTestRule<>(ProjectListActivity.class, true, false); + public BaseActivityTestRule baseActivityTestRule = new BaseActivityTestRule<>(ProjectListActivity.class, true, false); - private String projectToDelete = "firstProject"; + private final String projectToDelete = "firstProject"; @Before public void setUp() throws Exception { @@ -91,17 +91,11 @@ public void deleteProjectMultipleElementsListTest() { .inRoot(isDialog()) .check(matches(isDisplayed())); - onView(withText(R.string.dialog_confirm_delete)).inRoot(isDialog()) - .check(matches(isDisplayed())); - - onView(allOf(withId(android.R.id.button2), withText(R.string.cancel))) - .check(matches(isDisplayed())); + onView(allOf(withId(android.R.id.button2), withText(R.string.cancel))).check(matches(isDisplayed())); - onView(allOf(withId(android.R.id.button1), withText(R.string.delete))) - .perform(click()); + onView(allOf(withId(android.R.id.button1), withText(R.string.delete))).perform(click()); - onView(withText(projectToDelete)) - .check(doesNotExist()); + onView(withText(projectToDelete)).check(doesNotExist()); } @Category({Cat.AppUi.class, Level.Smoke.class}) @@ -112,9 +106,7 @@ public void deleteProjectSingleElementListTest() { openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); onView(withText(R.string.delete)).perform(click()); - onView(withText(projectToDelete)) - .check(doesNotExist()); - + onView(withText(projectToDelete)).check(doesNotExist()); } @Category({Cat.AppUi.class, Level.Smoke.class}) @@ -139,8 +131,7 @@ public void cancelDeleteProjectTest() { openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); onView(withText(R.string.delete)).perform(click()); - onRecyclerView().atPosition(1) - .performCheckItemClick(); + onRecyclerView().atPosition(1).performCheckItemClick(); onView(withId(R.id.confirm)).perform(click()); @@ -148,17 +139,64 @@ public void cancelDeleteProjectTest() { .inRoot(isDialog()) .check(matches(isDisplayed())); - onView(withText(R.string.dialog_confirm_delete)).inRoot(isDialog()) - .check(matches(isDisplayed())); + onView(allOf(withId(android.R.id.button1), withText(R.string.delete))).check(matches(isDisplayed())); - onView(allOf(withId(android.R.id.button1), withText(R.string.delete))) - .check(matches(isDisplayed())); + onView(allOf(withId(android.R.id.button2), withText(R.string.cancel))).perform(click()); - onView(allOf(withId(android.R.id.button2), withText(R.string.cancel))) - .perform(click()); + onView(withText(projectToDelete)).check(matches(isDisplayed())); + } - onView(withText(projectToDelete)) - .check(matches(isDisplayed())); + @Category({Cat.AppUi.class, Level.Smoke.class}) + @Test + public void deleteProjectShowUndoButtonTest() { + baseActivityTestRule.launchActivity(null); + + openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); + onView(withText(R.string.delete)).perform(click()); + + onView(withText(projectToDelete)).check(doesNotExist()); + onView(withId(R.id.menu_undo)).check(matches(isDisplayed())); + } + + @Category({Cat.AppUi.class, Level.Smoke.class}) + @Test + public void undoDeletedProjectTest() throws InterruptedException { + baseActivityTestRule.launchActivity(null); + + openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); + onView(withText(R.string.delete)).perform(click()); + + onView(withText(projectToDelete)).check(doesNotExist()); + onView(withId(R.id.menu_undo)).check(matches(isDisplayed())); + + onView(withId(R.id.menu_undo)).perform(click()); + Thread.sleep(1000); + + onView(withText(projectToDelete)).check(matches(isDisplayed())); + onView(withId(R.id.menu_undo)).check(doesNotExist()); + } + + @Category({Cat.AppUi.class, Level.Smoke.class}) + @Test + public void undoOptionNotVisibleAfterActivityChangeTest() throws InterruptedException { + String secondProject = "secondProject"; + createProject(secondProject); + baseActivityTestRule.launchActivity(null); + + openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); + onView(withText(R.string.delete)).perform(click()); + + onRecyclerView().atPosition(1).performCheckItemClick(); + + onView(withId(R.id.confirm)).perform(click()); + onView(allOf(withId(android.R.id.button1), withText(R.string.delete))).perform(click()); + + onView(withText(secondProject)).perform(click()); + Thread.sleep(1000); + pressBack(); + + onView(withId(R.id.menu_undo)).check(doesNotExist()); + onView(withText(projectToDelete)).check(doesNotExist()); } private void createProject(String projectName) { From 4091207c0fd1aa74d869053e4e5f115489d632ae Mon Sep 17 00:00:00 2001 From: Hannes Weilharter Date: Mon, 13 Jul 2026 17:13:46 +0200 Subject: [PATCH 3/5] CATROID-1662 fix detekt warning --- .../catroid/ui/recyclerview/fragment/ProjectListFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt index e65dfe99c90..a74a60dcb97 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt @@ -454,7 +454,7 @@ class ProjectListFragment( if (restored) { ToastUtil.showSuccess(requireContext(), R.string.restored_project) showUndo(false) - getLocalProjectListAsync(object: LoadProjectsListener { + getLocalProjectListAsync(object : LoadProjectsListener { override fun onProjectsLoaded() { setAdapterItems(adapter.projectsSorted) setShowProgressBar(false) From 9d229c3c905ea7b6a8360956c90b9bcfb6b1eb18 Mon Sep 17 00:00:00 2001 From: Hannes Weilharter Date: Tue, 28 Jul 2026 15:21:59 +0200 Subject: [PATCH 4/5] CATROID-1662 refactor DeleteProjectTest to Kotlin --- .../ui/fragment/DeleteProjectTest.java | 215 -------------- .../ui/fragment/DeleteProjectTest.kt | 272 ++++++++++++++++++ 2 files changed, 272 insertions(+), 215 deletions(-) delete mode 100644 catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.java create mode 100644 catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.kt diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.java b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.java deleted file mode 100644 index 95e6ec6561e..00000000000 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2026 The Catrobat Team - * () - * - * 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. - * - * An additional term exception under section 7 of the GNU Affero - * General Public License, version 3, is available at - * http://developer.catrobat.org/license_additional_term - * - * 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 org.catrobat.catroid.uiespresso.ui.fragment; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.common.BrickValues; -import org.catrobat.catroid.content.Project; -import org.catrobat.catroid.content.Script; -import org.catrobat.catroid.content.Sprite; -import org.catrobat.catroid.content.StartScript; -import org.catrobat.catroid.content.bricks.SetXBrick; -import org.catrobat.catroid.formulaeditor.Formula; -import org.catrobat.catroid.io.XstreamSerializer; -import org.catrobat.catroid.testsuites.annotations.Cat; -import org.catrobat.catroid.testsuites.annotations.Level; -import org.catrobat.catroid.ui.ProjectListActivity; -import org.catrobat.catroid.uiespresso.util.UiTestUtils; -import org.catrobat.catroid.uiespresso.util.rules.BaseActivityTestRule; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.junit.runner.RunWith; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; - -import static org.catrobat.catroid.uiespresso.ui.fragment.rvutils.RecyclerViewInteractionWrapper.onRecyclerView; -import static org.hamcrest.Matchers.allOf; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; -import static androidx.test.espresso.Espresso.pressBack; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.RootMatchers.isDialog; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; - -@RunWith(AndroidJUnit4.class) -public class DeleteProjectTest { - - @Rule - public BaseActivityTestRule baseActivityTestRule = new BaseActivityTestRule<>(ProjectListActivity.class, true, false); - - private final String projectToDelete = "firstProject"; - - @Before - public void setUp() throws Exception { - createProject(projectToDelete); - } - - @Category({Cat.AppUi.class, Level.Smoke.class}) - @Test - public void deleteProjectMultipleElementsListTest() { - createProject("secondProject"); - baseActivityTestRule.launchActivity(null); - - openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); - onView(withText(R.string.delete)).perform(click()); - - onView(withText(projectToDelete)).perform(click()); - - onView(withId(R.id.confirm)).perform(click()); - - onView(withText(UiTestUtils.getResources().getQuantityString(R.plurals.delete_projects, 1))) - .inRoot(isDialog()) - .check(matches(isDisplayed())); - - onView(allOf(withId(android.R.id.button2), withText(R.string.cancel))).check(matches(isDisplayed())); - - onView(allOf(withId(android.R.id.button1), withText(R.string.delete))).perform(click()); - - onView(withText(projectToDelete)).check(doesNotExist()); - } - - @Category({Cat.AppUi.class, Level.Smoke.class}) - @Test - public void deleteProjectSingleElementListTest() { - baseActivityTestRule.launchActivity(null); - - openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); - onView(withText(R.string.delete)).perform(click()); - - onView(withText(projectToDelete)).check(doesNotExist()); - } - - @Category({Cat.AppUi.class, Level.Smoke.class}) - @Test - public void selectFragmentToDeleteTest() { - createProject("secondProject"); - baseActivityTestRule.launchActivity(null); - - openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); - onView(withText(R.string.delete)).perform(click()); - - onRecyclerView().atPosition(0).perform(click()); - onRecyclerView().atPosition(0).performCheckItemCheck(); - } - - @Category({Cat.AppUi.class, Level.Smoke.class}) - @Test - public void cancelDeleteProjectTest() { - createProject("secondProject"); - baseActivityTestRule.launchActivity(null); - - openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); - onView(withText(R.string.delete)).perform(click()); - - onRecyclerView().atPosition(1).performCheckItemClick(); - - onView(withId(R.id.confirm)).perform(click()); - - onView(withText(UiTestUtils.getResources().getQuantityString(R.plurals.delete_projects, 1))) - .inRoot(isDialog()) - .check(matches(isDisplayed())); - - onView(allOf(withId(android.R.id.button1), withText(R.string.delete))).check(matches(isDisplayed())); - - onView(allOf(withId(android.R.id.button2), withText(R.string.cancel))).perform(click()); - - onView(withText(projectToDelete)).check(matches(isDisplayed())); - } - - @Category({Cat.AppUi.class, Level.Smoke.class}) - @Test - public void deleteProjectShowUndoButtonTest() { - baseActivityTestRule.launchActivity(null); - - openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); - onView(withText(R.string.delete)).perform(click()); - - onView(withText(projectToDelete)).check(doesNotExist()); - onView(withId(R.id.menu_undo)).check(matches(isDisplayed())); - } - - @Category({Cat.AppUi.class, Level.Smoke.class}) - @Test - public void undoDeletedProjectTest() throws InterruptedException { - baseActivityTestRule.launchActivity(null); - - openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); - onView(withText(R.string.delete)).perform(click()); - - onView(withText(projectToDelete)).check(doesNotExist()); - onView(withId(R.id.menu_undo)).check(matches(isDisplayed())); - - onView(withId(R.id.menu_undo)).perform(click()); - Thread.sleep(1000); - - onView(withText(projectToDelete)).check(matches(isDisplayed())); - onView(withId(R.id.menu_undo)).check(doesNotExist()); - } - - @Category({Cat.AppUi.class, Level.Smoke.class}) - @Test - public void undoOptionNotVisibleAfterActivityChangeTest() throws InterruptedException { - String secondProject = "secondProject"; - createProject(secondProject); - baseActivityTestRule.launchActivity(null); - - openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().getTargetContext()); - onView(withText(R.string.delete)).perform(click()); - - onRecyclerView().atPosition(1).performCheckItemClick(); - - onView(withId(R.id.confirm)).perform(click()); - onView(allOf(withId(android.R.id.button1), withText(R.string.delete))).perform(click()); - - onView(withText(secondProject)).perform(click()); - Thread.sleep(1000); - pressBack(); - - onView(withId(R.id.menu_undo)).check(doesNotExist()); - onView(withText(projectToDelete)).check(doesNotExist()); - } - - private void createProject(String projectName) { - Project project = new Project(ApplicationProvider.getApplicationContext(), projectName); - Sprite sprite = new Sprite("firstSprite"); - - Script script = new StartScript(); - script.addBrick(new SetXBrick(new Formula(BrickValues.X_POSITION))); - script.addBrick(new SetXBrick(new Formula(BrickValues.X_POSITION))); - sprite.addScript(script); - - project.getDefaultScene().addSprite(sprite); - - XstreamSerializer.getInstance().saveProject(project); - } -} diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.kt b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.kt new file mode 100644 index 00000000000..514511ede61 --- /dev/null +++ b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/DeleteProjectTest.kt @@ -0,0 +1,272 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * 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. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * 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 org.catrobat.catroid.uiespresso.ui.fragment + +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.catrobat.catroid.R +import org.catrobat.catroid.common.BrickValues +import org.catrobat.catroid.content.Project +import org.catrobat.catroid.content.Script +import org.catrobat.catroid.content.Sprite +import org.catrobat.catroid.content.StartScript +import org.catrobat.catroid.content.bricks.SetXBrick +import org.catrobat.catroid.formulaeditor.Formula +import org.catrobat.catroid.io.XstreamSerializer +import org.catrobat.catroid.testsuites.annotations.Cat.AppUi +import org.catrobat.catroid.testsuites.annotations.Level.Smoke +import org.catrobat.catroid.ui.ProjectListActivity +import org.catrobat.catroid.uiespresso.ui.fragment.rvutils.RecyclerViewInteractionWrapper +import org.catrobat.catroid.uiespresso.util.UiTestUtils.Companion.resources +import org.catrobat.catroid.uiespresso.util.rules.BaseActivityTestRule +import org.hamcrest.Matchers +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.experimental.categories.Category +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DeleteProjectTest { + + @get:Rule + var baseActivityTestRule: BaseActivityTestRule = + BaseActivityTestRule(ProjectListActivity::class.java, true, false) + + private val projectToDelete = "firstProject" + + @Before + @Throws(Exception::class) + fun setUp() { + createProject(projectToDelete) + } + + @Category(AppUi::class, Smoke::class) + @Test + fun deleteProjectMultipleElementsListTest() { + createProject("secondProject") + baseActivityTestRule.launchActivity(null) + + Espresso.openActionBarOverflowOrOptionsMenu( + InstrumentationRegistry.getInstrumentation().targetContext + ) + Espresso.onView(ViewMatchers.withText(R.string.delete)).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withText(projectToDelete)).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withId(R.id.confirm)).perform(ViewActions.click()) + + Espresso.onView( + ViewMatchers.withText( + resources.getQuantityString( + R.plurals.delete_projects, + 1 + ) + ) + ) + .inRoot(RootMatchers.isDialog()) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView( + Matchers.allOf( + ViewMatchers.withId(android.R.id.button2), ViewMatchers.withText( + R.string.cancel + ) + ) + ).check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView( + Matchers.allOf( + ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText( + R.string.delete + ) + ) + ).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withText(projectToDelete)).check(ViewAssertions.doesNotExist()) + } + + @Category(AppUi::class, Smoke::class) + @Test + fun deleteProjectSingleElementListTest() { + baseActivityTestRule.launchActivity(null) + + Espresso.openActionBarOverflowOrOptionsMenu( + InstrumentationRegistry.getInstrumentation().targetContext + ) + Espresso.onView(ViewMatchers.withText(R.string.delete)).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withText(projectToDelete)).check(ViewAssertions.doesNotExist()) + } + + @Category(AppUi::class, Smoke::class) + @Test + fun selectFragmentToDeleteTest() { + createProject("secondProject") + baseActivityTestRule.launchActivity(null) + + Espresso.openActionBarOverflowOrOptionsMenu( + InstrumentationRegistry.getInstrumentation().targetContext + ) + Espresso.onView(ViewMatchers.withText(R.string.delete)).perform(ViewActions.click()) + + RecyclerViewInteractionWrapper.onRecyclerView().atPosition(0).perform(ViewActions.click()) + RecyclerViewInteractionWrapper.onRecyclerView().atPosition(0).performCheckItemCheck() + } + + @Category(AppUi::class, Smoke::class) + @Test + fun cancelDeleteProjectTest() { + createProject("secondProject") + baseActivityTestRule.launchActivity(null) + + Espresso.openActionBarOverflowOrOptionsMenu( + InstrumentationRegistry.getInstrumentation().targetContext + ) + Espresso.onView(ViewMatchers.withText(R.string.delete)).perform(ViewActions.click()) + + RecyclerViewInteractionWrapper.onRecyclerView().atPosition(1).performCheckItemClick() + + Espresso.onView(ViewMatchers.withId(R.id.confirm)).perform(ViewActions.click()) + + Espresso.onView( + ViewMatchers.withText( + resources.getQuantityString( + R.plurals.delete_projects, + 1 + ) + ) + ) + .inRoot(RootMatchers.isDialog()) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView( + Matchers.allOf( + ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText( + R.string.delete + ) + ) + ).check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView( + Matchers.allOf( + ViewMatchers.withId(android.R.id.button2), ViewMatchers.withText( + R.string.cancel + ) + ) + ).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withText(projectToDelete)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + } + + @Category(AppUi::class, Smoke::class) + @Test + fun deleteProjectShowUndoButtonTest() { + baseActivityTestRule.launchActivity(null) + + Espresso.openActionBarOverflowOrOptionsMenu( + InstrumentationRegistry.getInstrumentation().targetContext + ) + Espresso.onView(ViewMatchers.withText(R.string.delete)).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withText(projectToDelete)).check(ViewAssertions.doesNotExist()) + Espresso.onView(ViewMatchers.withId(R.id.menu_undo)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + } + + @Category(AppUi::class, Smoke::class) + @Test + @Throws(InterruptedException::class) + fun undoDeletedProjectTest() { + baseActivityTestRule.launchActivity(null) + + Espresso.openActionBarOverflowOrOptionsMenu( + InstrumentationRegistry.getInstrumentation().targetContext + ) + Espresso.onView(ViewMatchers.withText(R.string.delete)).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withText(projectToDelete)).check(ViewAssertions.doesNotExist()) + Espresso.onView(ViewMatchers.withId(R.id.menu_undo)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + + Espresso.onView(ViewMatchers.withId(R.id.menu_undo)).perform(ViewActions.click()) + Thread.sleep(1000) + + Espresso.onView(ViewMatchers.withText(projectToDelete)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + Espresso.onView(ViewMatchers.withId(R.id.menu_undo)).check(ViewAssertions.doesNotExist()) + } + + @Category(AppUi::class, Smoke::class) + @Test + @Throws(InterruptedException::class) + fun undoOptionNotVisibleAfterActivityChangeTest() { + val secondProject = "secondProject" + createProject(secondProject) + baseActivityTestRule.launchActivity(null) + + Espresso.openActionBarOverflowOrOptionsMenu( + InstrumentationRegistry.getInstrumentation().targetContext + ) + Espresso.onView(ViewMatchers.withText(R.string.delete)).perform(ViewActions.click()) + + RecyclerViewInteractionWrapper.onRecyclerView().atPosition(1).performCheckItemClick() + + Espresso.onView(ViewMatchers.withId(R.id.confirm)).perform(ViewActions.click()) + Espresso.onView( + Matchers.allOf( + ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText( + R.string.delete + ) + ) + ).perform(ViewActions.click()) + + Espresso.onView(ViewMatchers.withText(secondProject)).perform(ViewActions.click()) + Thread.sleep(1000) + Espresso.pressBack() + + Espresso.onView(ViewMatchers.withId(R.id.menu_undo)).check(ViewAssertions.doesNotExist()) + Espresso.onView(ViewMatchers.withText(projectToDelete)).check(ViewAssertions.doesNotExist()) + } + + private fun createProject(projectName: String?) { + val project = Project(ApplicationProvider.getApplicationContext(), projectName) + val sprite = Sprite("firstSprite") + + val script: Script = StartScript() + script.addBrick(SetXBrick(Formula(BrickValues.X_POSITION))) + script.addBrick(SetXBrick(Formula(BrickValues.X_POSITION))) + sprite.addScript(script) + + project.defaultScene.addSprite(sprite) + + XstreamSerializer.getInstance().saveProject(project) + } +} From 39c093a9f4d568cf8056146095d3038fb9794ca1 Mon Sep 17 00:00:00 2001 From: Hannes Weilharter Date: Fri, 4 Sep 2026 09:20:55 +0200 Subject: [PATCH 5/5] CATROID-1662 refactor Undo implementation --- .../catroid/ui/ProjectListActivity.kt | 16 +++++++--- .../ui/fragment/ProjectOptionsFragment.kt | 2 +- .../fragment/ProjectListFragment.kt | 30 ++++++++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectListActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectListActivity.kt index 7d0ba7a19de..b830a44b37d 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectListActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectListActivity.kt @@ -75,18 +75,26 @@ class ProjectListActivity : BaseCastActivity() { .commit() } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + menu.findItem(R.id.menu_undo).isVisible = isUndoMenuItemVisible + return super.onPrepareOptionsMenu(menu) + } + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_projects_activity, menu) menu.findItem(R.id.merge).isVisible = BuildConfig.FEATURE_MERGE_ENABLED - menu.findItem(R.id.menu_undo).isVisible = isUndoMenuItemVisible return super.onCreateOptionsMenu(menu) } fun showUndo(visible: Boolean) { isUndoMenuItemVisible = visible - try { - optionsMenu.findItem(R.id.menu_undo).isVisible = visible - } catch (_: UninitializedPropertyAccessException) { + if (::binding.isInitialized) { + val menuItem = binding.toolbar.toolbar.menu.findItem(R.id.menu_undo) + menuItem?.isVisible = visible + + binding.toolbar.toolbar.invalidate() + binding.toolbar.toolbar.requestLayout() + } else { invalidateOptionsMenu() } } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt index acb85e10c54..ce691ab246a 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt index a74a60dcb97..9ee7a440b97 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/ProjectListFragment.kt @@ -277,6 +277,7 @@ class ProjectListFragment( }.execute(requireActivity()) } + @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_IMPORT_PROJECT && resultCode == RESULT_OK) { @@ -407,11 +408,15 @@ class ProjectListFragment( override fun deleteItems(selectedItems: MutableList?) { setShowProgressBar(true) + val projectsToDelete = selectedItems.orEmpty().filterNotNull() + if (projectsToDelete.isEmpty()) { + setShowProgressBar(false) + return + } + var deletedItemCount = 0 val deletedProjectNames = mutableListOf() - selectedItems ?: return - for (item in selectedItems) { - item ?: continue + for (item in projectsToDelete) { if (!copyProjectForUndoOption(item)) { ToastUtil.showError(requireContext(), R.string.error_copy_project) continue @@ -440,8 +445,8 @@ class ProjectListFragment( ) ) } - showUndo(deletedItemCount > 0 && hasDeletedProjectUndo()) finishActionMode() + showUndo(deletedItemCount > 0 && hasDeletedProjectUndo()) setAdapterItems(adapter.projectsSorted) checkForEmptyList() } @@ -544,6 +549,9 @@ class ProjectListFragment( fun checkForEmptyList() { if (adapter.items.isEmpty()) { + if (hasDeletedProjectUndo()) { + return + } setShowProgressBar(true) if (projectManager.initializeDefaultProject()) { setAdapterItems(adapter.projectsSorted) @@ -802,7 +810,7 @@ class ProjectListFragment( val nameView = dialogView.findViewById(R.id.shortcut_dialog_project_name) val pinButton = dialogView.findViewById(R.id.shortcut_dialog_pin_button) val cancelButton = dialogView.findViewById(R.id.shortcut_dialog_cancel_button) - val miuiContainer = dialogView.findViewById(R.id.shortcut_dialog_miui_container) + val miuiContainer = dialogView.findViewById(R.id.shortcut_dialog_miui_container) if (icon != null) { iconView.setImageBitmap(icon) @@ -810,8 +818,8 @@ class ProjectListFragment( iconView.setImageResource(R.drawable.ic_launcher_foreground) } nameView.text = projectName - miuiContainer.visibility = android.view.View.GONE - pinButton.visibility = android.view.View.VISIBLE + miuiContainer.visibility = View.GONE + pinButton.visibility = View.VISIBLE val dialog = android.app.AlertDialog.Builder(context, R.style.ShortcutPinDialog) .setView(dialogView) @@ -842,7 +850,7 @@ class ProjectListFragment( val nameView = dialogView.findViewById(R.id.shortcut_dialog_project_name) val pinButton = dialogView.findViewById(R.id.shortcut_dialog_pin_button) val cancelButton = dialogView.findViewById(R.id.shortcut_dialog_cancel_button) - val miuiContainer = dialogView.findViewById(R.id.shortcut_dialog_miui_container) + val miuiContainer = dialogView.findViewById(R.id.shortcut_dialog_miui_container) val settingsButton = dialogView.findViewById(R.id.shortcut_dialog_miui_settings_button) val miuiCancelButton = dialogView.findViewById(R.id.shortcut_dialog_miui_cancel_button) @@ -852,9 +860,9 @@ class ProjectListFragment( iconView.setImageResource(R.drawable.ic_launcher_foreground) } nameView.text = projectName - miuiContainer.visibility = android.view.View.VISIBLE - pinButton.visibility = android.view.View.GONE - cancelButton.visibility = android.view.View.GONE + miuiContainer.visibility = View.VISIBLE + pinButton.visibility = View.GONE + cancelButton.visibility = View.GONE val dialog = android.app.AlertDialog.Builder(context, R.style.ShortcutPinDialog) .setView(dialogView)