diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/retrofittesting/CatroidWebServerProjectUploadTest.kt b/catroid/src/androidTest/java/org/catrobat/catroid/retrofittesting/CatroidWebServerProjectUploadTest.kt new file mode 100644 index 00000000000..98f877cd7c8 --- /dev/null +++ b/catroid/src/androidTest/java/org/catrobat/catroid/retrofittesting/CatroidWebServerProjectUploadTest.kt @@ -0,0 +1,193 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2023 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.retrofittesting + +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import android.util.Log +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.catrobat.catroid.ProjectManager +import org.catrobat.catroid.common.Constants +import org.catrobat.catroid.common.FlavoredConstants +import org.catrobat.catroid.content.Project +import org.catrobat.catroid.io.StorageOperations +import org.catrobat.catroid.io.asynctask.saveProjectSerial +import org.catrobat.catroid.retrofit.WebService +import org.catrobat.catroid.retrofit.models.RegisterUser +import org.catrobat.catroid.testsuites.annotations.Cat.OutgoingNetworkTests +import org.catrobat.catroid.utils.ProjectZipper +import org.catrobat.catroid.utils.Utils +import org.catrobat.catroid.web.ServerAuthenticationConstants +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.experimental.categories.Category +import org.junit.runner.RunWith +import org.koin.java.KoinJavaComponent +import org.koin.test.KoinTest +import org.koin.test.inject +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner +import java.io.File +import java.net.HttpURLConnection + +@RunWith(MockitoJUnitRunner::class) +@Category(OutgoingNetworkTests::class) +class CatroidWebServerProjectUploadTest : KoinTest { + + companion object { + private val TAG = CatroidWebServerProjectUploadTest::class.java.simpleName + private val PROJECT_NAME: String = CatroidWebServerProjectUploadTest::class.java.simpleName + private val PASSWORD = "sEcR3tPassw0rD" + } + + val projectManager: ProjectManager by KoinJavaComponent.inject(ProjectManager::class.java) + private var project: Project? = null + + private lateinit var context: Context + private lateinit var sharedPreferences: SharedPreferences + + private lateinit var newEmail: String + private lateinit var newUserName: String + + private val webServer: WebService by inject() + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + MockitoAnnotations.initMocks(this) + project = Project( + ApplicationProvider.getApplicationContext(), + PROJECT_NAME + ) + saveProjectSerial(project, ApplicationProvider.getApplicationContext()) + projectManager.currentProject = project + + // Register Test User + newUserName = "APIUser" + System.currentTimeMillis() + newEmail = "$newUserName@api.at" + + val response = + webServer.register("", RegisterUser(true, newEmail, newUserName, PASSWORD)).execute() + val responseBody = response.body() + assertNotNull(responseBody) + assertNotNull(responseBody!!.token) + + val sharedPreferencesEditor = sharedPreferences.edit() + sharedPreferencesEditor.putString(Constants.TOKEN, responseBody.token) + sharedPreferencesEditor.apply() + Log.d(TAG, "Login Token Set") + + // Start Mock Server + } + + @After + fun tearDown() { + deleteUser() + StorageOperations.deleteDir(File(FlavoredConstants.DEFAULT_ROOT_DIRECTORY, PROJECT_NAME)) + } + + @Test + @Throws(Exception::class) + fun testUploadProjectAndCheckResponseCode201Returned() { + assertNotNull(project) + + val projectZip = ProjectZipper.zipProjectToArchive( + File(project?.directory!!.absolutePath), + File(Constants.CACHE_DIR, "upload${Constants.CATROBAT_EXTENSION}") + ) + + assertNotNull(projectZip!!) + + val checksum = Utils.md5Checksum(projectZip) + + val map: HashMap = HashMap() + map["checksum"] = RequestBody.create(MultipartBody.FORM, checksum) + + val requestBody = RequestBody.create( + MediaType.parse("multipart/form-data"), + projectZip + ) + val body = MultipartBody.Part.createFormData("file", projectZip.name, requestBody) + + val token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN) + val response = webServer.uploadProject("Bearer $token", map, body).execute() + + assertEquals( + response.code(), + HttpURLConnection.HTTP_CREATED + ) + } + + @Test + @Throws(Exception::class) + fun testUploadProjectReturnsProjectName() { + assertNotNull(project) + + val projectZip = ProjectZipper.zipProjectToArchive( + File(project?.directory!!.absolutePath), + File(Constants.CACHE_DIR, "upload${Constants.CATROBAT_EXTENSION}") + ) + + assertNotNull(projectZip!!) + + val checksum = Utils.md5Checksum(projectZip) + + val map: HashMap = HashMap() + map["checksum"] = RequestBody.create(MultipartBody.FORM, checksum) + + val requestBody = RequestBody.create( + MediaType.parse("multipart/form-data"), + projectZip + ) + val body = MultipartBody.Part.createFormData("file", projectZip.name, requestBody) + + val token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN) + val response = webServer.uploadProject("Bearer $token", map, body).execute().body() + + assertNotNull(response!!) + + Log.d(TAG, "Response project name is " + response.name) + + assertEquals( + response.name, + PROJECT_NAME + ) + } + + private fun deleteUser() { + val token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN) + val response = webServer.deleteUser("Bearer $token").execute() + assertEquals(response.code(), ServerAuthenticationConstants.SERVER_RESPONSE_USER_DELETED) + Log.d(TAG, "Deleted test user") + } +} diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReplaceApiKeyDialogTest.java b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReplaceApiKeyDialogTest.java deleted file mode 100644 index 53501a3b358..00000000000 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReplaceApiKeyDialogTest.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.dialog; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -import com.google.common.base.Charsets; -import com.google.common.io.Files; - -import org.catrobat.catroid.ProjectManager; -import org.catrobat.catroid.R; -import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.content.Project; -import org.catrobat.catroid.content.Scene; -import org.catrobat.catroid.content.Script; -import org.catrobat.catroid.content.Sprite; -import org.catrobat.catroid.content.StartScript; -import org.catrobat.catroid.content.bricks.BackgroundRequestBrick; -import org.catrobat.catroid.uiespresso.ui.activity.ProjectUploadDialogTest; -import org.catrobat.catroid.uiespresso.util.rules.BaseActivityTestRule; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; - -import androidx.test.core.app.ApplicationProvider; - -import static org.catrobat.catroid.common.Constants.CODE_XML_FILE_NAME; -import static org.catrobat.catroid.common.SharedPreferenceKeys.AGREED_TO_PRIVACY_POLICY_VERSION; -import static org.catrobat.catroid.io.asynctask.ProjectSaverKt.saveProjectSerial; -import static org.catrobat.catroid.ui.ProjectUploadActivityKt.PROJECT_DIR; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.matcher.ViewMatchers.withText; - -public class ReplaceApiKeyDialogTest { - - private static final String TAG = ReplaceApiKeyDialogTest.class.getSimpleName(); - - @Rule - public BaseActivityTestRule activityTestRule = - new BaseActivityTestRule<>(ProjectUploadDialogTest.ProjectUploadTestActivity.class, false, false); - - private int bufferedPrivacyPolicyPreferenceSetting; - - Project dummyProject; - String apikey = "AIzaas98d7f9a0sdf07ad0sf8a7sd09fASDf97asd9f"; - String linkapikey = "https://catrobat.at/joke?x=AIzaas98d7f9a0sdf07ad0sf8a7sd09fASDf97asd9f/"; - @Before - public void before() { - SharedPreferences sharedPreferences = PreferenceManager - .getDefaultSharedPreferences(ApplicationProvider.getApplicationContext()); - - bufferedPrivacyPolicyPreferenceSetting = sharedPreferences - .getInt(AGREED_TO_PRIVACY_POLICY_VERSION, 0); - - sharedPreferences - .edit() - .putInt(AGREED_TO_PRIVACY_POLICY_VERSION, Constants.CATROBAT_TERMS_OF_USE_ACCEPTED) - .commit(); - } - - public void createProject(String secret) { - dummyProject = new Project(ApplicationProvider.getApplicationContext(), "ApiProject"); - Scene dummyScene = new Scene("scene", dummyProject); - ProjectManager.getInstance().setCurrentProject(dummyProject); - Sprite sprite = new Sprite("sprite"); - Script firstScript = new StartScript(); - firstScript.addBrick(new BackgroundRequestBrick(secret)); - dummyScene.addSprite(sprite); - sprite.addScript(firstScript); - dummyProject.addScene(dummyScene); - saveProjectSerial(dummyProject, ApplicationProvider.getApplicationContext()); - - Intent intent = new Intent(); - intent.putExtra(PROJECT_DIR, dummyProject.getDirectory()); - - activityTestRule.launchActivity(intent); - } - - public void createProject(String secret1, String secret2) { - dummyProject = new Project(ApplicationProvider.getApplicationContext(), "ApiProject"); - Scene dummyScene = new Scene("scene", dummyProject); - ProjectManager.getInstance().setCurrentProject(dummyProject); - Sprite sprite = new Sprite("sprite"); - Script firstScript = new StartScript(); - firstScript.addBrick(new BackgroundRequestBrick(secret1)); - firstScript.addBrick(new BackgroundRequestBrick(secret2)); - dummyScene.addSprite(sprite); - sprite.addScript(firstScript); - dummyProject.addScene(dummyScene); - saveProjectSerial(dummyProject, ApplicationProvider.getApplicationContext()); - - Intent intent = new Intent(); - intent.putExtra(PROJECT_DIR, dummyProject.getDirectory()); - - activityTestRule.launchActivity(intent); - } - - @After - public void tearDown() throws Exception { - PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext()) - .edit() - .putInt(AGREED_TO_PRIVACY_POLICY_VERSION, - bufferedPrivacyPolicyPreferenceSetting) - .commit(); - } - - @Test - public void replaceApiKeyTestAPI() { - createProject(apikey); - File beforeReplace = new File(dummyProject.getDirectory(), CODE_XML_FILE_NAME); - String beforeReplaceCode = ""; - try { - beforeReplaceCode = - Files.asCharSource(beforeReplace, Charsets.UTF_8).read(); - } catch (IOException exception) { - Log.e(TAG, Log.getStackTraceString(exception)); - } - assertTrue(beforeReplaceCode.contains(apikey)); - onView(withText(R.string.api_replacement_dialog_accept)).perform(click()); - File afterReplace = new File(dummyProject.getDirectory(), CODE_XML_FILE_NAME); - String afterReplaceCode = ""; - try { - afterReplaceCode = Files.asCharSource(afterReplace, Charsets.UTF_8).read(); - } catch (IOException exception) { - Log.e(TAG, Log.getStackTraceString(exception)); - } - assertFalse(afterReplaceCode.contains(apikey)); - } - - @Test - public void replaceApiKeyTestLinkAPI() { - createProject(linkapikey); - File beforeReplace = new File(dummyProject.getDirectory(), CODE_XML_FILE_NAME); - String beforeReplaceCode = ""; - try { - beforeReplaceCode = Files.asCharSource(beforeReplace, Charsets.UTF_8).read(); - } catch (IOException exception) { - Log.e(TAG, Log.getStackTraceString(exception)); - } - assertTrue(beforeReplaceCode.contains(linkapikey)); - onView(withText(R.string.api_replacement_dialog_accept)).perform(click()); - File afterReplace = new File(dummyProject.getDirectory(), CODE_XML_FILE_NAME); - String afterReplaceCode = ""; - try { - afterReplaceCode = Files.asCharSource(afterReplace, Charsets.UTF_8).read(); - } catch (IOException exception) { - Log.e(TAG, Log.getStackTraceString(exception)); - } - assertFalse(afterReplaceCode.contains(linkapikey)); - } - - @Test - public void replaceApiKeyTestLoadBackup() { - createProject(linkapikey, apikey); - - File beforeReplace = new File(dummyProject.getDirectory(), CODE_XML_FILE_NAME); - String beforeReplaceCode = ""; - try { - beforeReplaceCode = Files.asCharSource(beforeReplace, Charsets.UTF_8).read(); - } catch (IOException exception) { - Log.e(TAG, Log.getStackTraceString(exception)); - } - assertTrue(beforeReplaceCode.contains(linkapikey)); - onView(withText(R.string.api_replacement_dialog_accept)).perform(click()); - File afterReplace = new File(dummyProject.getDirectory(), CODE_XML_FILE_NAME); - String afterReplaceCode = ""; - try { - afterReplaceCode = - Files.asCharSource(afterReplace, Charsets.UTF_8).read(); - } catch (IOException exception) { - Log.e(TAG, Log.getStackTraceString(exception)); - } - assertFalse(afterReplaceCode.contains(linkapikey)); - onView(withText(R.string.cancel)).perform(click()); - - File reloaded = new File(dummyProject.getDirectory(), CODE_XML_FILE_NAME); - String reloadedCode = ""; - try { - reloadedCode = Files.asCharSource(reloaded, Charsets.UTF_8).read(); - } catch (IOException exception) { - Log.e(TAG, Log.getStackTraceString(exception)); - } - assertTrue(reloadedCode.contains(linkapikey)); - } -} diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReuploadProjectDialogTest.kt b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReuploadProjectDialogTest.kt deleted file mode 100644 index 28e86e6057b..00000000000 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReuploadProjectDialogTest.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.dialog - -import android.content.Intent -import androidx.test.core.app.ApplicationProvider -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withText -import org.catrobat.catroid.ProjectManager -import org.catrobat.catroid.R -import org.catrobat.catroid.content.Project -import org.catrobat.catroid.content.Scene -import org.catrobat.catroid.content.Script -import org.catrobat.catroid.content.Sprite -import org.catrobat.catroid.content.StartScript -import org.catrobat.catroid.formulaeditor.UserVariable -import org.catrobat.catroid.io.XstreamSerializer -import org.catrobat.catroid.io.asynctask.saveProjectSerial -import org.catrobat.catroid.ui.PROJECT_DIR -import org.catrobat.catroid.uiespresso.ui.activity.ProjectUploadDialogTest.ProjectUploadTestActivity -import org.catrobat.catroid.uiespresso.util.rules.BaseActivityTestRule -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.koin.test.KoinTest -import org.koin.test.inject - -class ReuploadProjectDialogTest : KoinTest { - @get:Rule - var activityTestRule = BaseActivityTestRule( - ProjectUploadTestActivity::class.java, false, false) - - lateinit var dummyProject: Project - var projectName = "reUploadedProject" - private val projectManager: ProjectManager by inject() - - fun createDownloadedProject(name: String?) { - dummyProject = Project( - ApplicationProvider.getApplicationContext(), - name - ) - val dummyScene = Scene("scene", dummyProject) - projectManager.currentProject = dummyProject - val sprite = Sprite("sprite") - val firstScript: Script = StartScript() - dummyScene.addSprite(sprite) - sprite.addScript(firstScript) - dummyProject.addScene(dummyScene) - saveProjectSerial(dummyProject, ApplicationProvider.getApplicationContext()) - val intent = Intent() - intent.putExtra(PROJECT_DIR, dummyProject.directory) - activityTestRule.launchActivity(intent) - } - - @Before - fun setup() { - projectManager.loadDownloadedProjects() - projectManager.deleteDownloadedProjectInformation(projectName) - projectManager.addNewDownloadedProject(projectName) - createDownloadedProject(projectName) - } - - @After - @Throws(Exception::class) - fun tearDown() { - projectManager.deleteDownloadedProjectInformation(projectName) - } - - @Test - fun showUploadWarningForUnchangedProjectTest() { - onView(withText(R.string.warning)) - .check(matches(isDisplayed())) - onView(withText(R.string.ok)) - .perform(click()) - } - - @Test - fun notShowUploadWarningForChangedProjectTest() { - onView(withText(R.string.warning)) - .check(matches(isDisplayed())) - onView(withText(R.string.ok)) - .perform(click()) - - val currentProject = projectManager.currentProject - val newScene = Scene("scene", currentProject) - currentProject.addScene(newScene) - XstreamSerializer.getInstance().saveProject(currentProject) - saveProjectSerial(currentProject, ApplicationProvider.getApplicationContext()) - val intent = Intent() - intent.putExtra(PROJECT_DIR, currentProject.directory) - activityTestRule.launchActivity(intent) - - onView(withText(R.string.main_menu_upload)) - .check(matches(isDisplayed())) - } - - @Test - fun notShowUploadWarningForAddedVariableProjectTest() { - onView(withText(R.string.warning)) - .check(matches(isDisplayed())) - onView(withText(R.string.ok)) - .perform(click()) - - val currentProject = projectManager.currentProject - val userVariable = UserVariable("uservariable") - currentProject.addUserVariable(userVariable) - XstreamSerializer.getInstance().saveProject(currentProject) - saveProjectSerial(currentProject, ApplicationProvider.getApplicationContext()) - val intent = Intent() - intent.putExtra(PROJECT_DIR, currentProject.directory) - activityTestRule.launchActivity(intent) - - onView(withText(R.string.main_menu_upload)) - .check(matches(isDisplayed())) - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt b/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt index f0de4f3693c..62700236f53 100644 --- a/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt +++ b/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt @@ -44,6 +44,7 @@ import org.catrobat.catroid.sync.ProjectsCategoriesSync import org.catrobat.catroid.transfers.GetUserProjectsTask import org.catrobat.catroid.transfers.LoginViewModel import org.catrobat.catroid.transfers.OAuthTask +import org.catrobat.catroid.transfers.ProjectUploadTask import org.catrobat.catroid.transfers.RegistrationViewModel import org.catrobat.catroid.transfers.TagsTask import org.catrobat.catroid.transfers.TokenTask @@ -78,6 +79,9 @@ val componentsModules = module(createdAtStart = true, override = false) { single { OAuthTask(get()) } + single { + ProjectUploadTask(get()) + } single { TokenTask(get()) } diff --git a/catroid/src/main/java/org/catrobat/catroid/retrofit/RetrofitWebServer.kt b/catroid/src/main/java/org/catrobat/catroid/retrofit/RetrofitWebServer.kt index a59b6c82749..e113dac507b 100644 --- a/catroid/src/main/java/org/catrobat/catroid/retrofit/RetrofitWebServer.kt +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/RetrofitWebServer.kt @@ -24,7 +24,9 @@ package org.catrobat.catroid.retrofit import okhttp3.ConnectionSpec +import okhttp3.MultipartBody import okhttp3.OkHttpClient +import okhttp3.RequestBody import okhttp3.logging.HttpLoggingInterceptor import org.catrobat.catroid.common.Constants.CURRENT_CATROBAT_LANGUAGE_VERSION import org.catrobat.catroid.common.Constants.RETROFIT_WRITE_TIMEOUT @@ -34,6 +36,7 @@ import org.catrobat.catroid.retrofit.models.FeaturedProject import org.catrobat.catroid.retrofit.models.LoginResponse import org.catrobat.catroid.retrofit.models.LoginUser import org.catrobat.catroid.retrofit.models.OAuthLogin +import org.catrobat.catroid.retrofit.models.ProjectUploadResponseApi import org.catrobat.catroid.retrofit.models.ProjectResponse import org.catrobat.catroid.retrofit.models.ProjectsCategoryApi import org.catrobat.catroid.retrofit.models.RefreshToken @@ -46,7 +49,10 @@ import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.PartMap import retrofit2.http.Query import java.util.Locale import java.util.concurrent.TimeUnit @@ -114,6 +120,14 @@ interface WebService { @GET("projects/tags") fun getTags(): Call> + @Multipart + @POST("projects") + fun uploadProject( + @Header("Authorization") bearerToken: String, + @PartMap partMap: Map, + @Part projectFile: MultipartBody.Part + ) : Call + @GET("projects/user") fun getUserProjects( @Header("Authorization") bearerToken: String, diff --git a/catroid/src/main/java/org/catrobat/catroid/retrofit/models/ProjectDataClasses.kt b/catroid/src/main/java/org/catrobat/catroid/retrofit/models/ProjectDataClasses.kt index 57fbc209119..10199e0126d 100644 --- a/catroid/src/main/java/org/catrobat/catroid/retrofit/models/ProjectDataClasses.kt +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/models/ProjectDataClasses.kt @@ -27,6 +27,7 @@ import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.Relation +import java.io.File @SuppressWarnings("ConstructorParameterNaming") @Entity(tableName = "featured_project") @@ -40,6 +41,27 @@ data class FeaturedProject( val featured_image: String ) +@SuppressWarnings("ConstructorParameterNaming") +data class ProjectUploadResponseApi( + val id: String, + val name: String, + val author: String, + val description: String, + val version: String, + val views: Int, + val download: Int, + val private: Boolean, + val flavor: String, + val tags: List, + val uploaded: Long, + val uploaded_string: String, + val screenshot_large: String, + val screenshot_small: String, + val project_url: String, + val download_url: String, + val filesize: Double +) + @Entity(tableName = "project_response", primaryKeys = ["id", "categoryType"]) data class ProjectResponse( var id: String, @@ -102,26 +124,5 @@ data class ProjectCategoryWithResponses( data class ProjectsCategoryApi( val type: String, val name: String, - val projectsList: List -) - -@SuppressWarnings("ConstructorParameterNaming") -data class ProjectResponseApi( - val id: String, - val name: String, - val author: String, - val description: String, - val version: String, - val views: Int, - val download: Int, - val private: Boolean, - val flavor: String, - val tags: List, - val uploaded: Long, - val uploaded_string: String, - val screenshot_large: String, - val screenshot_small: String, - val project_url: String, - val download_url: String, - val filesize: Double + val projectsList: List ) diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/ProjectUploadTask.kt b/catroid/src/main/java/org/catrobat/catroid/transfers/ProjectUploadTask.kt new file mode 100644 index 00000000000..5f11d994303 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/transfers/ProjectUploadTask.kt @@ -0,0 +1,121 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2023 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.transfers + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.catrobat.catroid.retrofit.WebService +import org.catrobat.catroid.retrofit.models.ProjectUploadResponseApi +import org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_REGISTER_OK +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.File + +class ProjectUploadTask(private val webServer: WebService) { + private val projectUploadResponse = MutableLiveData() + fun getProjectUploadResponse(): LiveData = projectUploadResponse + + private var errorMessage: String = String() + fun getErrorMessage(): String = errorMessage + + fun uploadProject( + projectZip: File, checksum: String, idToken: String, flavor: String? = null, isPrivate: + Boolean? = null + ) { + Log.d(TAG, "Starting project upload") + + val requestBody = RequestBody.create( + MediaType.parse("multipart/form-data"), + projectZip + ) + + val body = MultipartBody.Part.createFormData("file", projectZip.name, requestBody) + + val map: HashMap = HashMap() + map["checksum"] = RequestBody.create(MultipartBody.FORM, checksum) + + if (isPrivate != null) + { + map["private"] = RequestBody.create(MultipartBody.FORM, isPrivate.toString()) + } + + if (flavor != null) + { + map["flavor"] = RequestBody.create(MultipartBody.FORM, flavor) + } + + val uploadProjectCall: Call = webServer.uploadProject( + "Bearer $idToken", + map, + body + ) + + uploadProjectCall.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + when (val statusCode = response.code()) { + SERVER_RESPONSE_REGISTER_OK -> { + val tokenReceived = response.body()?.id + tokenReceived?.let { + projectUploadResponse.postValue(response.body()) + } ?: run { + errorMessage = "Project Upload failed" + projectUploadResponse.postValue(null) + } + } + else -> { + Log.e( + TAG, + "Not accepted StatusCode: $statusCode on project upload; Server " + + "Answer: ${response.body()}" + ) + errorMessage = "Project could not be uploaded!" + projectUploadResponse.postValue(null) + } + } + } + + override fun onFailure(call: Call, throwable: Throwable) { + Log.e(TAG, "onFailure $throwable.message") + errorMessage = throwable.message.orEmpty() + projectUploadResponse.postValue(null) + } + }) + } + + fun clear() { + projectUploadResponse.value = null + } + + companion object { + private val TAG = ProjectUploadTask::class.java.simpleName + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUpload.kt b/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUpload.kt index ba60b2d060b..e69de29bb2d 100644 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUpload.kt +++ b/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUpload.kt @@ -1,137 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.transfers.project - -import android.content.SharedPreferences -import android.util.Log -import org.catrobat.catroid.common.Constants -import org.catrobat.catroid.common.Constants.DEVICE_VARIABLE_JSON_FILE_NAME -import org.catrobat.catroid.common.Constants.UPLOAD_IMAGE_SCALE_HEIGHT -import org.catrobat.catroid.common.Constants.UPLOAD_IMAGE_SCALE_WIDTH -import org.catrobat.catroid.io.ProjectAndSceneScreenshotLoader -import org.catrobat.catroid.io.ZipArchiver -import org.catrobat.catroid.utils.ImageEditing -import org.catrobat.catroid.web.ServerCalls -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.util.Locale - -typealias UploadProjectSuccessCallback = (projectId: String) -> Unit -typealias UploadProjectErrorCallback = (errorCode: Int, errorMessage: String) -> Unit - -class ProjectUpload( - private val projectDirectory: File, - private val projectName: String, - private val projectDescription: String, - private val userEmail: String, - private val sceneNames: Array?, - private val archiveDirectory: File, - private val zipArchiver: ZipArchiver, - private val screenshotLoader: ProjectAndSceneScreenshotLoader, - private val sharedPreferences: SharedPreferences, - private val serverCalls: ServerCalls -) { - - fun start( - successCallback: UploadProjectSuccessCallback, - errorCallback: UploadProjectErrorCallback - ) { - val projectArchive = zipProjectToArchive(projectDirectory, archiveDirectory) - if (projectArchive == null) { - errorCallback(UPLOAD_ZIP_ERROR, UPLOAD_ZIP_ERROR_MESSAGE) - return - } - - val projectUploadData = createUploadData(projectArchive) - - scaleSceneScreenshots(projectName, sceneNames) - - serverCalls.uploadProject( - projectUploadData, - { projectId, successUsername, successToken -> - sharedPreferences.edit() - .putString(Constants.TOKEN, successToken) - .putString(Constants.USERNAME, successUsername) - .apply() - - successCallback(projectId) - projectArchive.delete() - }, - { errorCode, errorMessage -> - errorCallback( - errorCode, - errorMessage - ) - } - ) - } - - private fun createUploadData(projectArchive: File): ProjectUploadData { - val token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN) - val username = sharedPreferences.getString(Constants.USERNAME, Constants.NO_USERNAME) - - return ProjectUploadData( - projectName = projectName, - projectDescription = projectDescription, - projectArchive = projectArchive, - userEmail = userEmail, - language = Locale.getDefault().language, - token = token ?: Constants.NO_TOKEN, - username = username ?: Constants.NO_USERNAME - ) - } - - private fun scaleSceneScreenshots(projectName: String, sceneNames: Array?) { - sceneNames?.mapNotNull { screenshotLoader.getScreenshotFile(projectName, it, false) } - ?.filter { it.exists() && it.length() > 0 } - ?.forEach { - try { - ImageEditing.scaleImageFile(it, UPLOAD_IMAGE_SCALE_WIDTH, UPLOAD_IMAGE_SCALE_HEIGHT) - } catch (ex: FileNotFoundException) { - Log.e(TAG, Log.getStackTraceString(ex)) - } - } - } - - private fun zipProjectToArchive(projectDirectory: File, archiveDirectory: File): File? { - return try { - val fileList = projectDirectory.listFiles() - val filteredFileList = fileList.filter { file -> file.name != DEVICE_VARIABLE_JSON_FILE_NAME } - - zipArchiver.zip(archiveDirectory, filteredFileList.toTypedArray()) - archiveDirectory - } catch (ioException: IOException) { - Log.e(TAG, Log.getStackTraceString(ioException)) - archiveDirectory.delete() - null - } - } - - companion object { - private val TAG = ProjectUpload::class.java.simpleName - const val UPLOAD_ZIP_ERROR = 32_202 - const val UPLOAD_ZIP_ERROR_MESSAGE = "Failed to zip directory for upload" - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadData.kt b/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadData.kt deleted file mode 100644 index 6ad0fc47dd6..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadData.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.transfers.project - -import java.io.File - -data class ProjectUploadData( - val projectName: String, - val projectDescription: String, - val projectArchive: File, - val userEmail: String, - val language: String, - val token: String, - val username: String -) diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadService.kt b/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadService.kt deleted file mode 100644 index 5b48916254b..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadService.kt +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.transfers.project - -import android.app.IntentService -import android.app.Notification -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.os.ResultReceiver -import android.preference.PreferenceManager -import android.util.Log -import androidx.core.app.NotificationCompat -import org.catrobat.catroid.R -import org.catrobat.catroid.common.Constants -import org.catrobat.catroid.common.Constants.CATROBAT_EXTENSION -import org.catrobat.catroid.common.Constants.EMAIL -import org.catrobat.catroid.common.Constants.EXTRA_LANGUAGE -import org.catrobat.catroid.common.Constants.EXTRA_PROJECT_DESCRIPTION -import org.catrobat.catroid.common.Constants.EXTRA_PROJECT_NAME -import org.catrobat.catroid.common.Constants.EXTRA_PROJECT_PATH -import org.catrobat.catroid.common.Constants.EXTRA_PROVIDER -import org.catrobat.catroid.common.Constants.EXTRA_RESULT_RECEIVER -import org.catrobat.catroid.common.Constants.EXTRA_SCENE_NAMES -import org.catrobat.catroid.common.Constants.EXTRA_UPLOAD_NAME -import org.catrobat.catroid.common.Constants.EXTRA_USER_EMAIL -import org.catrobat.catroid.common.Constants.GOOGLE_EMAIL -import org.catrobat.catroid.common.Constants.GOOGLE_PROVIDER -import org.catrobat.catroid.common.Constants.MAX_PERCENT -import org.catrobat.catroid.common.Constants.NO_EMAIL -import org.catrobat.catroid.common.Constants.NO_GOOGLE_EMAIL -import org.catrobat.catroid.common.Constants.UPLOAD_RESULT_RECEIVER_RESULT_CODE -import org.catrobat.catroid.io.ProjectAndSceneScreenshotLoader -import org.catrobat.catroid.io.ZipArchiver -import org.catrobat.catroid.ui.MainMenuActivity -import org.catrobat.catroid.utils.DeviceSettingsProvider -import org.catrobat.catroid.utils.ToastUtil -import org.catrobat.catroid.utils.Utils -import org.catrobat.catroid.utils.notifications.StatusBarNotificationManager -import org.catrobat.catroid.web.CatrobatWebClient -import org.catrobat.catroid.web.ServerCalls -import java.io.File -import java.util.Locale - -const val UPLOAD_FILE_NAME = "upload$CATROBAT_EXTENSION" - -class ProjectUploadService : IntentService("ProjectUploadService") { - - override fun onHandleIntent(projectUploadIntent: Intent?) { - val intent = projectUploadIntent - ?: return logWarning("Called ProjectUploadService with null intent!") - - val projectPath = intent.getStringExtra(EXTRA_PROJECT_PATH) - ?: return logWarning("Called ProjectUploadService without project path!") - - val projectDirectory = File(projectPath) - if (projectDirectory.listFiles().isEmpty()) { - return logWarning("Called ProjectUploadService with empty project directory!") - } - - val resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER) as? ResultReceiver - ?: return logWarning("Called ProjectUploadService without resultReceiver!") - - val projectName = intent.getStringExtra(EXTRA_UPLOAD_NAME) - ?: return logWarning("Called ProjectUploadService with empty project name!") - - val notificationID = StatusBarNotificationManager.getNextNotificationID() - startForeground( - notificationID, - createUploadNotification(projectName) - ) - - val reUploadBundle = Bundle().apply { - putString(EXTRA_PROJECT_NAME, projectName) - putString(EXTRA_PROJECT_DESCRIPTION, intent.getStringExtra(EXTRA_PROJECT_DESCRIPTION)) - putString(EXTRA_PROJECT_PATH, projectPath) - putStringArray(EXTRA_SCENE_NAMES, intent.getStringArrayExtra(EXTRA_SCENE_NAMES)) - putString(EXTRA_USER_EMAIL, getUserEmail(intent.getStringExtra(EXTRA_PROVIDER))) - putString(EXTRA_LANGUAGE, Locale.getDefault().language) - } - - ProjectUpload( - projectDirectory = projectDirectory, - projectName = projectName, - projectDescription = intent.getStringExtra(EXTRA_PROJECT_DESCRIPTION) ?: "", - userEmail = getUserEmail(intent.getStringExtra(EXTRA_PROVIDER)), - sceneNames = intent.getStringArrayExtra(EXTRA_SCENE_NAMES), - archiveDirectory = File(cacheDir, UPLOAD_FILE_NAME), - zipArchiver = ZipArchiver(), - screenshotLoader = ProjectAndSceneScreenshotLoader( - applicationContext.resources.getDimensionPixelSize(R.dimen.project_thumbnail_width), - applicationContext.resources.getDimensionPixelSize(R.dimen.project_thumbnail_height) - ), - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this), - serverCalls = ServerCalls(CatrobatWebClient.client) - ).start( - successCallback = { projectId -> - Log.v(TAG, "Upload successful") - stopForeground(true) - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager - notificationManager?.notify(notificationID, createUploadFinishedNotification(projectName)) - - ToastUtil.showSuccess(this, R.string.notification_upload_finished) - val result = Bundle().apply { putString(Constants.EXTRA_PROJECT_ID, projectId) } - resultReceiver.send(UPLOAD_RESULT_RECEIVER_RESULT_CODE, result) - }, - errorCallback = { errorCode, errorMessage -> - Log.e(TAG, errorMessage) - stopForeground(true) - ToastUtil.showError(this, resources.getString(R.string.error_project_upload) + " " + errorMessage) - StatusBarNotificationManager(applicationContext) - .createUploadRejectedNotification(applicationContext, errorCode, errorMessage, reUploadBundle) - resultReceiver.send(0, null) - } - ) - } - - override fun onDestroy() { - Utils.invalidateLoginTokenIfUserRestricted(applicationContext) - super.onDestroy() - } - - private fun createUploadNotification(programName: String): Notification { - StatusBarNotificationManager(applicationContext).createNotificationChannel(applicationContext) - - var uploadIntent = Intent(applicationContext, MainMenuActivity::class.java) - uploadIntent.action = Intent.ACTION_MAIN - uploadIntent = uploadIntent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) - val pendingIntent = PendingIntent.getActivity( - applicationContext, - StatusBarNotificationManager.UPLOAD_PENDING_INTENT_REQUEST_CODE, - uploadIntent, PendingIntent.FLAG_CANCEL_CURRENT - ) - - return NotificationCompat.Builder(applicationContext, StatusBarNotificationManager.CHANNEL_ID) - .setContentIntent(pendingIntent) - .setSmallIcon(R.drawable.ic_stat) - .setContentTitle( - applicationContext.getString(R.string.notification_upload_title_pending) + " " + programName - ) - .setContentText(applicationContext.getString(R.string.notification_upload_pending)) - .setOngoing(true) - .setProgress(MAX_PERCENT, 0, true) - .build() - } - - private fun createUploadFinishedNotification(programName: String): Notification { - var uploadIntent = Intent(applicationContext, MainMenuActivity::class.java) - uploadIntent.action = Intent.ACTION_MAIN - uploadIntent = uploadIntent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) - val pendingIntent = PendingIntent.getActivity( - applicationContext, - StatusBarNotificationManager.UPLOAD_PENDING_INTENT_REQUEST_CODE, - uploadIntent, PendingIntent.FLAG_CANCEL_CURRENT - ) - - return NotificationCompat.Builder(applicationContext, StatusBarNotificationManager.CHANNEL_ID) - .setContentIntent(pendingIntent) - .setSmallIcon(R.drawable.ic_stat) - .setContentTitle( - applicationContext.getString(R.string.notification_upload_title_finished) + " " + programName - ) - .setContentText(applicationContext.getString(R.string.notification_upload_finished)) - .setAutoCancel(true) - .setOngoing(false) - .setProgress(0, 0, false) - .build() - } - - private fun getUserEmail(provider: String?): String { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) - - val email = when (provider) { - GOOGLE_PROVIDER -> sharedPreferences.getString(GOOGLE_EMAIL, NO_GOOGLE_EMAIL) - else -> sharedPreferences.getString(EMAIL, NO_EMAIL) - } - - val result = if (email == NO_EMAIL) { - DeviceSettingsProvider.getUserEmail(this) - } else { - email - } - - return result ?: "" - } - - private fun logWarning(warningMessage: String) { - Log.w(TAG, warningMessage) - } - - companion object { - private val TAG = ProjectUploadService::class.java.simpleName - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ResultReceiverWrapper.kt b/catroid/src/main/java/org/catrobat/catroid/transfers/project/ResultReceiverWrapper.kt deleted file mode 100644 index a9e704fc524..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ResultReceiverWrapper.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.transfers.project - -import android.os.Bundle -import android.os.Handler -import android.os.ResultReceiver -import java.lang.ref.WeakReference - -class ResultReceiverWrapper( - resultReceiverWrapperInterface: ResultReceiverWrapperInterface, - handler: Handler -) : ResultReceiver(handler) { - val resultReceiverWrapperInterface = WeakReference(resultReceiverWrapperInterface) - - override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { - resultReceiverWrapperInterface.get()?.onReceiveResult(resultCode, resultData) - } -} - -interface ResultReceiverWrapperInterface { - fun onReceiveResult(resultCode: Int, resultData: Bundle?) -} diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectUploadActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectUploadActivity.kt index 8395d1815b0..c59c459a233 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectUploadActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectUploadActivity.kt @@ -28,7 +28,6 @@ import android.content.Intent import android.content.SharedPreferences import android.net.Uri import android.os.Bundle -import android.os.Handler import android.preference.PreferenceManager import android.text.Editable import android.text.Html @@ -50,9 +49,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.catrobat.catroid.ProjectManager import org.catrobat.catroid.R import org.catrobat.catroid.common.Constants +import org.catrobat.catroid.common.Constants.CATROBAT_EXTENSION import org.catrobat.catroid.common.FlavoredConstants import org.catrobat.catroid.content.Project import org.catrobat.catroid.databinding.ActivityUploadBinding @@ -60,6 +61,9 @@ import org.catrobat.catroid.databinding.DialogReplaceApiKeyBinding import org.catrobat.catroid.databinding.DialogUploadUnchangedProjectBinding import org.catrobat.catroid.exceptions.ProjectException import org.catrobat.catroid.io.ProjectAndSceneScreenshotLoader +import org.catrobat.catroid.io.asynctask.ProjectLoadTask.ProjectLoadListener +import org.catrobat.catroid.retrofit.models.ProjectUploadResponseApi +import org.catrobat.catroid.transfers.ProjectUploadTask import org.catrobat.catroid.io.asynctask.ProjectLoader.ProjectLoadListener import org.catrobat.catroid.io.asynctask.loadProject import org.catrobat.catroid.io.asynctask.renameProject @@ -74,6 +78,7 @@ import org.catrobat.catroid.ui.recyclerview.dialog.TextInputDialog import org.catrobat.catroid.ui.recyclerview.dialog.textwatcher.InputWatcher import org.catrobat.catroid.utils.FileMetaDataExtractor import org.catrobat.catroid.utils.NetworkConnectionMonitor +import org.catrobat.catroid.utils.ProjectZipper import org.catrobat.catroid.utils.ToastUtil import org.catrobat.catroid.utils.Utils import org.catrobat.catroid.web.ServerAuthenticationConstants.DEPRECATED_TOKEN_LENGTH @@ -104,9 +109,7 @@ const val SIGN_IN_CODE = 42 const val NUMBER_OF_UPLOADED_PROJECTS = "number_of_uploaded_projects" open class ProjectUploadActivity : BaseActivity(), - ProjectLoadListener, - ResultReceiverWrapperInterface, - ProjectUploadInterface { + ProjectLoadListener { private lateinit var project: Project private lateinit var xmlFile: File @@ -116,7 +119,6 @@ open class ProjectUploadActivity : BaseActivity(), private lateinit var apiMatcher: Matcher private var uploadProgressDialog: AlertDialog? = null - private lateinit var uploadResultReceiver: ResultReceiverWrapper private val nameInputTextWatcher = NameInputTextWatcher() private var enableNextButton = true @@ -130,12 +132,16 @@ open class ProjectUploadActivity : BaseActivity(), private lateinit var dialogReplaceApiKeyBinding: DialogReplaceApiKeyBinding private var tags: List = ArrayList() + private lateinit var sharedPreferences: SharedPreferences + private val tokenTask: TokenTask by inject() private val tagsTask: TagsTask by inject() - private lateinit var sharedPreferences: SharedPreferences - @JvmField - protected var projectUploadController: ProjectUploadController? = null + // Used for uploading callback to stay consistent with API calls + private val projectUploadTask: ProjectUploadTask by inject() + + // Used for zipping and uploading in background + private var projectUploadJob: Job? = null private val getUserProjectsTask: GetUserProjectsTask by inject() @@ -157,9 +163,18 @@ open class ProjectUploadActivity : BaseActivity(), notesAndCreditsScreen = false setShowProgressBar(true) - uploadResultReceiver = ResultReceiverWrapper(this, Handler()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + projectUploadTask.clear() + projectUploadTask.getProjectUploadResponse() + .observe(this) { projectUploadResponse -> + projectUploadResponse?.let { + showSuccessDialog(projectUploadResponse) + } ?: run { + showErrorDialog(projectUploadTask.getErrorMessage()) + } + } + getUserProjectsTask.clear() getUserProjectsTask.getUserProjectsResponse() .observe(this) { getUserProjectsResponse -> @@ -178,9 +193,6 @@ open class ProjectUploadActivity : BaseActivity(), loadProjectActivity() } - protected open fun createProjectUploadController(): ProjectUploadController? = - ProjectUploadController(this) - override fun onLoadFinished(success: Boolean) { if (success) { loadProjectActivity() @@ -194,7 +206,6 @@ open class ProjectUploadActivity : BaseActivity(), private fun loadProjectActivity() { getTags() project = projectManager.currentProject - projectUploadController = createProjectUploadController() verifyUserIdentity() getAllUserProjects() } @@ -258,6 +269,7 @@ open class ProjectUploadActivity : BaseActivity(), if (uploadProgressDialog?.isShowing == true) { uploadProgressDialog?.dismiss() } + projectUploadJob?.cancel() getUserProjectsJob?.cancel() extractProjectNamesFromResponseJob?.cancel() super.onDestroy() @@ -456,7 +468,7 @@ open class ProjectUploadActivity : BaseActivity(), ) { dialog: DialogInterface, indexSelected: Int, isChecked: Boolean -> if (isChecked) { if (checkedTags.size >= Constants.MAX_NUMBER_OF_CHECKED_TAGS) { - ToastUtil.showError(getContext(), R.string.upload_tags_maximum_error) + ToastUtil.showError(this, R.string.upload_tags_maximum_error) (dialog as AlertDialog).listView.setItemChecked(indexSelected, false) } else { checkedTags.add(availableTags[indexSelected]) @@ -467,10 +479,7 @@ open class ProjectUploadActivity : BaseActivity(), } .setPositiveButton(getText(R.string.next)) { _, _ -> project.tags = checkedTags - projectUploadController?.startUpload( - projectName, projectDescription, - notesAndCredits, project - ) + startProjectUpload() } .setNegativeButton(getText(R.string.cancel)) { dialog, which -> Utils.invalidateLoginTokenIfUserRestricted(this) @@ -481,6 +490,34 @@ open class ProjectUploadActivity : BaseActivity(), .show() } + private fun startProjectUpload() { + Log.d(TAG, "Starting project upload process") + showUploadDialog() + projectUploadJob = GlobalScope.launch(Dispatchers.Main) { + // run asynchronous + val projectZipped = withContext(Dispatchers.Default) { + ProjectZipper.zipProjectToArchive( + File( + project.directory.absolutePath + ), File(cacheDir, "upload$CATROBAT_EXTENSION") + ) + } + + if (projectZipped == null) { + // Maybe change error message on zipping error? + showErrorDialog("Could not pack project to zip file") + Log.d(TAG, "Could not pack project to zip file") + return@launch + } + + projectUploadTask.uploadProject( + projectZipped, + Utils.md5Checksum(projectZipped), + sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN).orEmpty(), + ) + } + } + private fun checkIfProjectNameAlreadyExists(name: String) : Boolean { return projectNamesOfUser.contains(name) } @@ -555,28 +592,7 @@ open class ProjectUploadActivity : BaseActivity(), binding.inputProjectDescription.visibility = visibility } - private val projectName: String - get() { - val name = binding.inputProjectName.editText?.text.toString().trim { it <= ' ' } - if (project.name != name) { - val renamedDirectory = renameProject(project.directory, name) - if (renamedDirectory == null) { - Log.e(TAG, "Creating renamed directory failed!") - return name - } - loadProject(renamedDirectory, applicationContext) - project = projectManager.currentProject - } - return name - } - - private val projectDescription: String - get() = binding.inputProjectDescription.editText?.text.toString().trim { it <= ' ' } - - private val notesAndCredits: String - get() = binding.inputProjectNotesAndCredits.editText?.text.toString().trim { it <= ' ' } - - fun showUploadDialog() { + private fun showUploadDialog() { if (MainMenuActivity.surveyCampaign != null) { MainMenuActivity.surveyCampaign?.uploadFlag = true } @@ -601,36 +617,22 @@ open class ProjectUploadActivity : BaseActivity(), uploadProgressDialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = false } - override fun getResultReceiverWrapper() = uploadResultReceiver - - override fun getContext() = this@ProjectUploadActivity - - override fun startUploadService(intent: Intent?) { - showUploadDialog() - startService(intent) + private fun showErrorDialog(errorMessage: String) { + uploadProgressDialog?.findViewById(R.id.dialog_upload_progress_progressbar)?.visibility = + View.GONE + uploadProgressDialog?.findViewById(R.id.dialog_upload_message_failed)?.visibility = + View.VISIBLE + val image = + uploadProgressDialog?.findViewById(R.id.dialog_upload_progress_image) + image?.setImageResource(R.drawable.ic_upload_failed) + image?.visibility = View.VISIBLE } - override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { - - if (resultCode != Constants.UPLOAD_RESULT_RECEIVER_RESULT_CODE || resultData == null || uploadProgressDialog?.isShowing == false) { - uploadProgressDialog?.findViewById(R.id.dialog_upload_progress_progressbar)?.visibility = - View.GONE - uploadProgressDialog?.findViewById(R.id.dialog_upload_message_failed)?.visibility = - View.VISIBLE - val image = - uploadProgressDialog?.findViewById(R.id.dialog_upload_progress_image) - image?.setImageResource(R.drawable.ic_upload_failed) - image?.visibility = View.VISIBLE - return - } - - val projectId = resultData.getString(Constants.EXTRA_PROJECT_ID) + private fun showSuccessDialog(projectMetaData: ProjectUploadResponseApi) { val positiveButton = uploadProgressDialog?.getButton(DialogInterface.BUTTON_POSITIVE) - positiveButton?.setOnClickListener { - val projectUrl = Constants.SHARE_PROJECT_URL + projectId val intent = Intent(this, WebViewActivity::class.java) - intent.putExtra(WebViewActivity.INTENT_PARAMETER_URL, projectUrl) + intent.putExtra(WebViewActivity.INTENT_PARAMETER_URL, projectMetaData.project_url) startActivity(intent) loadBackup() projectManager.resetChangedFlag(project) @@ -695,11 +697,20 @@ open class ProjectUploadActivity : BaseActivity(), if (isValid) { onCreateView() } else { - val refreshToken = sharedPreferences.getString(Constants.REFRESH_TOKEN, Constants.NO_TOKEN).orEmpty() + val refreshToken = sharedPreferences.getString( + Constants.REFRESH_TOKEN, + Constants.NO_TOKEN + ).orEmpty() when { - token.length == DEPRECATED_TOKEN_LENGTH -> checkDeprecatedToken(token) - refreshToken != Constants.NO_TOKEN -> checkRefreshToken(token, refreshToken) + token.length == DEPRECATED_TOKEN_LENGTH -> checkRefreshToken( + token, + refreshToken + ) + refreshToken != Constants.NO_TOKEN -> checkRefreshToken( + token, + refreshToken + ) else -> verifyUserIdentityFailed() } } @@ -731,6 +742,7 @@ open class ProjectUploadActivity : BaseActivity(), tokenTask.refreshToken(token, refreshToken) } + @Deprecated("Use new API call instead", ReplaceWith("checkRefreshToken(token, refreshToken)")) private fun checkDeprecatedToken(token: String) { tokenTask.getUpgradeTokenResponse().observe(this, Observer { upgradeResponse -> upgradeResponse?.let { @@ -753,7 +765,7 @@ open class ProjectUploadActivity : BaseActivity(), startSignInWorkflow() } - fun startSignInWorkflow() { + private fun startSignInWorkflow() { startActivityForResult(Intent(this, SignInActivity::class.java), SIGN_IN_CODE) } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/controller/ProjectUploadController.kt b/catroid/src/main/java/org/catrobat/catroid/ui/controller/ProjectUploadController.kt index 959974a690e..e69de29bb2d 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/controller/ProjectUploadController.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/controller/ProjectUploadController.kt @@ -1,73 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.ui.controller - -import android.content.Context -import android.content.Intent -import org.catrobat.catroid.common.Constants -import org.catrobat.catroid.content.Project -import org.catrobat.catroid.io.asynctask.saveProjectSerial -import org.catrobat.catroid.transfers.project.ProjectUploadService -import org.catrobat.catroid.transfers.project.ResultReceiverWrapper - -open class ProjectUploadController(private val projectUploadInterface: ProjectUploadInterface) { - interface ProjectUploadInterface { - fun getResultReceiverWrapper(): ResultReceiverWrapper - fun getContext(): Context - fun startUploadService(intent: Intent?) - } - - private val context: Context = projectUploadInterface.getContext() - private fun createUploadIntent( - projectName: String, - projectDescription: String, - project: Project - ): Intent { - val intent = Intent(context, ProjectUploadService::class.java) - val sceneNames = project.sceneNames.toTypedArray() - val resultReceiverWrapper = projectUploadInterface.getResultReceiverWrapper() - return intent.apply { - putExtra(Constants.EXTRA_RESULT_RECEIVER, resultReceiverWrapper) - putExtra(Constants.EXTRA_UPLOAD_NAME, projectName) - putExtra(Constants.EXTRA_PROJECT_DESCRIPTION, projectDescription) - putExtra(Constants.EXTRA_PROJECT_PATH, project.directory.absolutePath) - putExtra(Constants.EXTRA_PROVIDER, Constants.NO_OAUTH_PROVIDER) - putExtra(Constants.EXTRA_SCENE_NAMES, sceneNames) - } - } - - fun startUpload( - projectName: String, - projectDescription: String, - projectNotesAndCredits: String?, - project: Project - ) { - project.description = projectDescription - project.notesAndCredits = projectNotesAndCredits - project.setDeviceData(context) - project.setListeningLanguageTag() - saveProjectSerial(project, context) - val uploadIntent = createUploadIntent(projectName, projectDescription, project) - projectUploadInterface.startUploadService(uploadIntent) - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/utils/Extensions.kt b/catroid/src/main/java/org/catrobat/catroid/utils/Extensions.kt index d1ef2456a62..102669ee826 100644 --- a/catroid/src/main/java/org/catrobat/catroid/utils/Extensions.kt +++ b/catroid/src/main/java/org/catrobat/catroid/utils/Extensions.kt @@ -33,7 +33,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions import org.catrobat.catroid.retrofit.models.ProjectCategoryWithResponses import org.catrobat.catroid.retrofit.models.ProjectResponse -import org.catrobat.catroid.retrofit.models.ProjectResponseApi +import org.catrobat.catroid.retrofit.models.ProjectUploadResponseApi import org.catrobat.catroid.retrofit.models.ProjectsCategory import org.catrobat.catroid.retrofit.models.ProjectsCategoryApi @@ -62,7 +62,7 @@ fun View.setVisibleOrGone(show: Boolean) { } } -fun List.toProjectResponsesList(projectType: String): List { +fun List.toProjectResponsesList(projectType: String): List { return this.map { src -> ProjectResponse( id = src.id, diff --git a/catroid/src/main/java/org/catrobat/catroid/utils/ProjectZipper.kt b/catroid/src/main/java/org/catrobat/catroid/utils/ProjectZipper.kt new file mode 100644 index 00000000000..d7b53af7f4a --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/utils/ProjectZipper.kt @@ -0,0 +1,55 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2023 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.utils + +import android.util.Log +import org.catrobat.catroid.common.Constants.DEVICE_VARIABLE_JSON_FILENAME +import org.catrobat.catroid.io.ZipArchiver +import java.io.File +import java.io.IOException + +/** + * This class converts a Catroid project to a zip file + */ +object ProjectZipper { + + private val TAG = ProjectZipper::class.java.simpleName + + fun zipProjectToArchive(projectDirectory: File, archiveDirectory: File): File? { + Log.d(TAG, "Zipping project...") + + return try { + val fileList = projectDirectory.listFiles() + val filteredFileList = + fileList.filter { file -> file.name != DEVICE_VARIABLE_JSON_FILENAME } + ZipArchiver().zip(archiveDirectory, filteredFileList.toTypedArray()) + Log.d(TAG, "Zipping done") + archiveDirectory + } catch (ioException: IOException) { + Log.e(TAG, Log.getStackTraceString(ioException)) + archiveDirectory.delete() + null + } + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/utils/notifications/StatusBarNotificationManager.java b/catroid/src/main/java/org/catrobat/catroid/utils/notifications/StatusBarNotificationManager.java index d082bb12c13..5260d2e5732 100644 --- a/catroid/src/main/java/org/catrobat/catroid/utils/notifications/StatusBarNotificationManager.java +++ b/catroid/src/main/java/org/catrobat/catroid/utils/notifications/StatusBarNotificationManager.java @@ -35,13 +35,11 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.ResultReceiver; import android.provider.MediaStore; import android.util.Log; import org.catrobat.catroid.R; import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.transfers.project.ProjectUploadService; import org.catrobat.catroid.ui.MainMenuActivity; import org.catrobat.catroid.utils.ToastUtil; @@ -49,19 +47,12 @@ import androidx.core.app.NotificationCompat; import kotlin.jvm.Synchronized; -import static org.catrobat.catroid.common.Constants.EXTRA_PROJECT_NAME; -import static org.catrobat.catroid.common.Constants.MAX_PERCENT; +import static org.catrobat.catroid.common.Constants.*; public final class StatusBarNotificationManager { private static final String TAG = StatusBarNotificationManager.class.getSimpleName(); - private static final String ACTION_UPDATE_POCKET_CODE_VERSION = "update_pocket_code_version"; - private static final String ACTION_RETRY_UPLOAD = "retry_upload"; - private static final String ACTION_CANCEL_UPLOAD = "cancel_upload"; public static final String CHANNEL_ID = "pocket_code_notification_channel_id"; - private static final int NOTIFICATION_PENDING_INTENT_REQUEST_CODE = 1; - public static final int UPLOAD_PENDING_INTENT_REQUEST_CODE = 0xFFFF; - private static int notificationId = 1; private NotificationManager notificationManager; @@ -168,135 +159,6 @@ public void abortProgressNotificationWithMessage(Context context, NotificationDa showOrUpdateNotification(context, notificationData, MAX_PERCENT, null); } - public Notification createUploadRejectedNotification(Context context, int statusCode, String serverAnswer, Bundle bundle) { - Uri alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(context.getResources().getString(R.string.notification_upload_rejected)) - .setContentText(serverAnswer) - .setTicker(context.getResources().getString(R.string.notification_upload_rejected)) - .setSound(alarmSound) - .setStyle(new NotificationCompat.BigTextStyle().bigText(serverAnswer)) - .setProgress(0, 0, false) - .setAutoCancel(true) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setOngoing(false); - - switch (statusCode) { - case Constants.STATUS_CODE_INTERNAL_SERVER_ERROR: - case Constants.STATUS_CODE_UPLOAD_MISSING_DATA: - case Constants.STATUS_CODE_UPLOAD_INVALID_CHECKSUM: - case Constants.STATUS_CODE_UPLOAD_COPY_FAILED: - case Constants.STATUS_CODE_UPLOAD_UNZIP_FAILED: - case Constants.STATUS_CODE_UPLOAD_MISSING_XML: - case Constants.STATUS_CODE_UPLOAD_RENAME_FAILED: - case Constants.STATUS_CODE_UPLOAD_SAVE_THUMBNAIL_FAILED: - Intent actionIntentRetryUpload = new Intent(context, NotificationActionService.class) - .setAction(ACTION_RETRY_UPLOAD); - actionIntentRetryUpload.putExtra("bundle", bundle); - - PendingIntent actionPendingIntentRetryUpload = PendingIntent.getService(context, NOTIFICATION_PENDING_INTENT_REQUEST_CODE, - actionIntentRetryUpload, PendingIntent.FLAG_CANCEL_CURRENT); - builder.addAction(new NotificationCompat.Action(android.R.drawable.ic_popup_sync, - context.getResources().getString(R.string.notification_upload_retry), actionPendingIntentRetryUpload)); - - Intent actionIntentCancelUpload = new Intent(context, NotificationActionService.class) - .setAction(ACTION_CANCEL_UPLOAD); - actionIntentCancelUpload.putExtra("bundle", bundle); - PendingIntent actionPendingIntentCancelUpload = PendingIntent.getService(context, NOTIFICATION_PENDING_INTENT_REQUEST_CODE, - actionIntentCancelUpload, PendingIntent.FLAG_ONE_SHOT); - builder.addAction(new NotificationCompat.Action(android.R.drawable.ic_menu_close_clear_cancel, - context.getResources().getString(R.string.cancel), actionPendingIntentCancelUpload)); - - break; - case Constants.STATUS_CODE_UPLOAD_MISSING_CHECKSUM: - case Constants.STATUS_CODE_UPLOAD_OLD_CATROBAT_LANGUAGE: - case Constants.STATUS_CODE_UPLOAD_OLD_CATROBAT_VERSION: - Intent actionIntentUpdatePocketCodeVersion = new Intent(context, NotificationActionService.class) - .setAction(ACTION_UPDATE_POCKET_CODE_VERSION) - .putExtra("notificationId", NOTIFICATION_PENDING_INTENT_REQUEST_CODE); - PendingIntent actionPendingIntentUpdatePocketCodeVersion = PendingIntent.getService(context, NOTIFICATION_PENDING_INTENT_REQUEST_CODE, - actionIntentUpdatePocketCodeVersion, PendingIntent.FLAG_ONE_SHOT); - builder.addAction(new NotificationCompat.Action(R.drawable.pc_toolbar_icon, - context.getResources().getString(R.string.notification_open_play_store), actionPendingIntentUpdatePocketCodeVersion)); - break; - - default: - Intent openIntent = new Intent(context, MainMenuActivity.class); - openIntent.setAction(Intent.ACTION_MAIN).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(EXTRA_PROJECT_NAME, bundle.getString("projectName")); - - PendingIntent pendingIntent = PendingIntent.getActivity(context, notificationId, openIntent, - PendingIntent.FLAG_CANCEL_CURRENT); - builder.setContentIntent(pendingIntent); - break; - } - - return builder.build(); - } - - public static class NotificationActionService extends IntentService { - public NotificationActionService() { - super(NotificationActionService.class.getSimpleName()); - } - - @Override - protected void onHandleIntent(Intent intent) { - String action = intent.getAction(); - Log.d(TAG, "Received notification, action is: " + action); - - if (ACTION_UPDATE_POCKET_CODE_VERSION.equals(action)) { - final String appPackageName = getPackageName(); - - try { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appPackageName)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); - } catch (android.content.ActivityNotFoundException anfe) { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + appPackageName))); - } - closeNotificationBar(); - } - if (ACTION_RETRY_UPLOAD.equals(action)) { - Intent reuploadIntent = prepareReuploadIntent(intent); - String projectName = intent.getBundleExtra("bundle").getString("projectName"); - - NotificationData notificationData = new StatusBarNotificationManager(getApplicationContext()) - .createAndShowUploadNotification(getApplicationContext(), projectName); - int notificationId = notificationData == null ? -1 : notificationData.getNotificationID(); - reuploadIntent.putExtra("notificationId", notificationId); - getApplicationContext().startService(reuploadIntent); - } - - if (ACTION_CANCEL_UPLOAD.equals(action)) { - closeNotificationBar(); - } - } - - private Intent prepareReuploadIntent(Intent intent) { - String projectName = intent.getBundleExtra("bundle").getString("projectName"); - String projectDescription = intent.getBundleExtra("bundle").getString("projectDescription"); - String projectPath = intent.getBundleExtra("bundle").getString("projectPath"); - String[] sceneNames = intent.getBundleExtra("bundle").getStringArray("sceneNames"); - String token = intent.getBundleExtra("bundle").getString("token"); - String username = intent.getBundleExtra("bundle").getString("username"); - ResultReceiver receiver = intent.getBundleExtra("bundle").getParcelable("receiver"); - - Intent reuploadIntent = new Intent(getApplicationContext(), ProjectUploadService.class); - reuploadIntent.putExtra("receiver", receiver); - reuploadIntent.putExtra("uploadName", projectName); - reuploadIntent.putExtra("projectDescription", projectDescription); - reuploadIntent.putExtra("projectPath", projectPath); - reuploadIntent.putExtra("username", username); - reuploadIntent.putExtra("token", token); - reuploadIntent.putExtra("sceneNames", sceneNames); - return reuploadIntent; - } - - private void closeNotificationBar() { - Intent it = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - sendBroadcast(it); - } - } - public void createNotificationChannel(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = context.getResources().getString(R.string.app_name); diff --git a/catroid/src/main/java/org/catrobat/catroid/web/ServerAuthenticationConstants.java b/catroid/src/main/java/org/catrobat/catroid/web/ServerAuthenticationConstants.java index 6d11f7444a2..47128c448ed 100644 --- a/catroid/src/main/java/org/catrobat/catroid/web/ServerAuthenticationConstants.java +++ b/catroid/src/main/java/org/catrobat/catroid/web/ServerAuthenticationConstants.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 The Catrobat Team + * Copyright (C) 2010-2023 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -27,8 +27,8 @@ public final class ServerAuthenticationConstants { public static final Integer SERVER_RESPONSE_REGISTER_OK = 201; - public static final Integer SERVER_RESPONSE_TOKEN_OK = 200; public static final Integer SERVER_RESPONSE_USER_DELETED = 204; + public static final Integer SERVER_RESPONSE_TOKEN_OK = 200; public static final Integer SERVER_RESPONSE_INVALID_UPLOAD_TOKEN = 401; public static final Integer SERVER_RESPONSE_REGISTER_UNPROCESSABLE_ENTITY = 422; public static final int DEPRECATED_TOKEN_LENGTH = 32; diff --git a/catroid/src/main/java/org/catrobat/catroid/web/ServerCalls.java b/catroid/src/main/java/org/catrobat/catroid/web/ServerCalls.java index 4fb78120d03..27ddc8d14a5 100644 --- a/catroid/src/main/java/org/catrobat/catroid/web/ServerCalls.java +++ b/catroid/src/main/java/org/catrobat/catroid/web/ServerCalls.java @@ -28,15 +28,11 @@ import android.util.Log; import com.google.android.gms.common.images.WebImage; -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; import org.catrobat.catroid.common.Constants; import org.catrobat.catroid.common.ScratchProgramData; import org.catrobat.catroid.common.ScratchSearchResult; import org.catrobat.catroid.common.ScratchVisibilityState; -import org.catrobat.catroid.transfers.project.ProjectUploadData; -import org.catrobat.catroid.web.requests.HttpRequestsKt; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -64,16 +60,12 @@ import okio.BufferedSink; import okio.Okio; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.DEPRECATED_TOKEN_LENGTH; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_TOKEN_OK; - public final class ServerCalls implements ScratchDataFetcher { public static final String BASE_URL_TEST_HTTPS = "https://catroid-test.catrob.at/pocketcode/"; public static final String TAG = ServerCalls.class.getSimpleName(); public static boolean useTestUrl = false; private final OkHttpClient okHttpClient; private String resultString; - private String projectId; public ServerCalls(OkHttpClient httpClient) { okHttpClient = httpClient; @@ -279,48 +271,6 @@ private List extractScratchProgramDataListFromJson(final JSO return programDataList; } - public void uploadProject(ProjectUploadData uploadData, UploadSuccessCallback successCallback, - UploadErrorCallback errorCallback) { - - executeUploadCall( - HttpRequestsKt.createUploadRequest(uploadData), - (uploadResponse) -> { - String newToken = uploadResponse.token; - projectId = uploadResponse.projectId; - - if (uploadResponse.statusCode != SERVER_RESPONSE_TOKEN_OK) { - errorCallback.onError(uploadResponse.statusCode, "Upload failed! JSON Response was " + uploadResponse.statusCode); - } else if (newToken.length() != DEPRECATED_TOKEN_LENGTH) { - errorCallback.onError(uploadResponse.statusCode, uploadResponse.answer); - } else { - successCallback.onSuccess(projectId, uploadData.getUsername(), newToken); - } - }, - errorCallback - ); - } - - private void executeUploadCall(Request request, UploadCallSuccessCallback successCallback, UploadErrorCallback errorCallback) { - Response response; - UploadResponse uploadResponse; - try { - response = okHttpClient.newCall(request).execute(); - if (response.isSuccessful()) { - uploadResponse = new Gson().fromJson(response.body().string(), UploadResponse.class); - successCallback.onSuccess(uploadResponse); - } else { - Log.v(TAG, "Upload not successful"); - errorCallback.onError(response.code(), "Upload failed! HTTP Status code was " + response.code()); - } - } catch (IOException ioException) { - Log.e(TAG, Log.getStackTraceString(ioException)); - errorCallback.onError(WebconnectionException.ERROR_NETWORK, "I/O Exception"); - } catch (JsonSyntaxException jsonSyntaxException) { - Log.e(TAG, Log.getStackTraceString(jsonSyntaxException)); - errorCallback.onError(WebconnectionException.ERROR_JSON, "JsonSyntaxException"); - } - } - public void downloadMedia(final String url, final String filePath, final ResultReceiver receiver) throws IOException, WebconnectionException { @@ -379,23 +329,4 @@ private String getRequestInterruptable(String url) throws WebconnectionException throw new WebconnectionException(WebconnectionException.ERROR_NETWORK, Log.getStackTraceString(e)); } } - - static class UploadResponse { - String projectId; - int statusCode; - String answer; - String token; - } - - public interface UploadSuccessCallback { - void onSuccess(String projectId, String username, String token); - } - - public interface UploadErrorCallback { - void onError(int statusCode, String errorMessage); - } - - private interface UploadCallSuccessCallback { - void onSuccess(UploadResponse response); - } } diff --git a/catroid/src/main/java/org/catrobat/catroid/web/requests/HttpRequests.kt b/catroid/src/main/java/org/catrobat/catroid/web/requests/HttpRequests.kt index 03cb182a4cb..e69de29bb2d 100644 --- a/catroid/src/main/java/org/catrobat/catroid/web/requests/HttpRequests.kt +++ b/catroid/src/main/java/org/catrobat/catroid/web/requests/HttpRequests.kt @@ -1,72 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.web.requests - -import android.util.Log -import okhttp3.MediaType -import okhttp3.MultipartBody -import okhttp3.Request -import okhttp3.RequestBody -import org.catrobat.catroid.common.Constants -import org.catrobat.catroid.common.FlavoredConstants -import org.catrobat.catroid.transfers.project.ProjectUploadData -import org.catrobat.catroid.transfers.project.UPLOAD_FILE_NAME -import org.catrobat.catroid.utils.Utils -import org.catrobat.catroid.web.ServerCalls - -private const val FILE_UPLOAD_TAG = "upload" -private const val PROJECT_NAME_TAG = "projectTitle" -private const val PROJECT_DESCRIPTION_TAG = "projectDescription" -private const val PROJECT_CHECKSUM_TAG = "fileChecksum" -private const val USER_EMAIL = "userEmail" -private const val DEVICE_LANGUAGE = "deviceLanguage" -private val MEDIA_TYPE_ZIPFILE = MediaType.parse("application/zip") -private const val FILE_UPLOAD_URL = FlavoredConstants.BASE_UPLOAD_URL + "api/upload/upload.json" - -fun createUploadRequest( - uploadData: ProjectUploadData -): Request { - - Log.v(ServerCalls.TAG, "Building request to upload to: $FILE_UPLOAD_URL") - - val requestBody = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart( - FILE_UPLOAD_TAG, UPLOAD_FILE_NAME, - RequestBody.create(MEDIA_TYPE_ZIPFILE, uploadData.projectArchive) - ) - .addFormDataPart(PROJECT_NAME_TAG, uploadData.projectName) - .addFormDataPart(PROJECT_DESCRIPTION_TAG, uploadData.projectDescription) - .addFormDataPart(USER_EMAIL, uploadData.userEmail) - .addFormDataPart(PROJECT_CHECKSUM_TAG, Utils.md5Checksum(uploadData.projectArchive)) - .addFormDataPart(Constants.TOKEN, uploadData.token) - .addFormDataPart(Constants.USERNAME, uploadData.username) - .addFormDataPart(DEVICE_LANGUAGE, uploadData.language) - .build() - - return Request.Builder() - .url(FILE_UPLOAD_URL) - .post(requestBody) - .build() -} diff --git a/catroid/src/test/java/org/catrobat/catroid/test/transfers/ProjectUploadTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/transfers/ProjectUploadTest.kt deleted file mode 100644 index c292955d186..00000000000 --- a/catroid/src/test/java/org/catrobat/catroid/test/transfers/ProjectUploadTest.kt +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2022 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.test.transfers - -import android.content.SharedPreferences -import org.catrobat.catroid.common.Constants -import org.catrobat.catroid.io.ProjectAndSceneScreenshotLoader -import org.catrobat.catroid.io.ZipArchiver -import org.catrobat.catroid.transfers.project.ProjectUpload -import org.catrobat.catroid.transfers.project.ProjectUploadData -import org.catrobat.catroid.web.ServerCalls -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Assert.fail -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.`when` -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.verification.VerificationMode -import org.powermock.api.mockito.PowerMockito.mock -import org.powermock.core.classloader.annotations.PowerMockIgnore -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner -import java.io.File -import java.io.IOException -import java.util.Locale - -private const val defaultToken = "TOKEN_1234" -private const val defaultUsername = "USER MC_USER" -private const val userEmail = "user@catrobat.com" -private const val projectName = "testproject" -private const val projectDescription = "testproject description" - -@PowerMockIgnore("javax.net.ssl.*") -@RunWith(PowerMockRunner::class) -@PrepareForTest(ServerCalls::class) -class ProjectUploadTest { - private lateinit var sharedPreferences: SharedPreferences - private lateinit var sharedPrefsEditor: SharedPreferences.Editor - private lateinit var serverCalls: ServerCalls - private lateinit var screenshotLoader: ProjectAndSceneScreenshotLoader - private lateinit var projectDirectory: File - private lateinit var projectDirectoryFiles: Array - private lateinit var projectDirectoryFilesFiltered: Array - private lateinit var archiveDirectory: File - private lateinit var zipArchiver: ZipArchiver - - @Before - fun setup() { - sharedPreferences = mock(SharedPreferences::class.java) - sharedPrefsEditor = mock(SharedPreferences.Editor::class.java) - serverCalls = mock(ServerCalls::class.java) - screenshotLoader = mock(ProjectAndSceneScreenshotLoader::class.java) - projectDirectory = mock(File::class.java) - val deviceVariableFile = mock(File::class.java) - `when`(deviceVariableFile.getName()).thenReturn(Constants.DEVICE_VARIABLE_JSON_FILE_NAME) - projectDirectoryFilesFiltered = arrayOf(mock(File::class.java), mock(File::class.java), mock(File::class.java)) - projectDirectoryFiles = projectDirectoryFilesFiltered + arrayOf(deviceVariableFile) - archiveDirectory = mock(File::class.java) - zipArchiver = mock(ZipArchiver::class.java) - - `when`(projectDirectory.listFiles()).thenReturn(projectDirectoryFiles) - - `when`(sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN)).thenReturn(defaultToken) - `when`(sharedPreferences.getString(Constants.USERNAME, Constants.NO_USERNAME)).thenReturn(defaultUsername) - `when`(sharedPreferences.edit()).thenReturn(sharedPrefsEditor) - } - - @Test - fun testProjectUploadDataPassedToServerCalls() { - `when`(zipArchiver.zip(archiveDirectory, projectDirectoryFiles)).then {} - - `when`( - serverCalls.uploadProject( - any(ProjectUploadData::class.java), - any(ServerCalls.UploadSuccessCallback::class.java), - any(ServerCalls.UploadErrorCallback::class.java) - ) - ).then { - val projectUploadData = it.arguments[0] as? ProjectUploadData - assertEquals( - ProjectUploadData( - projectName = projectName, - projectDescription = projectDescription, - projectArchive = archiveDirectory, - userEmail = userEmail, - language = Locale.getDefault().language, - token = defaultToken, - username = defaultUsername - ), - projectUploadData - ) - } - - createProjectUpload().start( - successCallback = {}, - errorCallback = { _, _ -> } - ) - - didCallUploadProject() - } - - @Test - fun testProjectUploadSuccess() { - val projectId = "1234" - `when`(zipArchiver.zip(archiveDirectory, projectDirectoryFiles)).then {} - `when`(sharedPrefsEditor.putString(ArgumentMatchers.eq(Constants.TOKEN), any())) - .thenReturn(sharedPrefsEditor) - `when`(sharedPrefsEditor.putString(ArgumentMatchers.eq(Constants.USERNAME), any())) - .thenReturn(sharedPrefsEditor) - - `when`( - serverCalls.uploadProject( - any(ProjectUploadData::class.java), - any(ServerCalls.UploadSuccessCallback::class.java), - any(ServerCalls.UploadErrorCallback::class.java) - ) - ).then { - val successCallback = it.arguments[1] as? ServerCalls.UploadSuccessCallback - successCallback?.onSuccess(projectId, "username", "token") - } - - var returnedProjectId = "-1" - createProjectUpload().start( - successCallback = { returnedProjectId = it }, - errorCallback = { _, _ -> fail("Error callback must not be invoked in this test") } - ) - - assertEquals(projectId, returnedProjectId) - } - - @Test - fun testSetSharedPreferencesOnSuccess() { - var token: String? = null - var username: String? = null - val callbackToken = "catroid_testtoken" - val callbackUsername = "catroid_testuser" - var successCallbackCalled = false - - `when`(sharedPrefsEditor.putString(ArgumentMatchers.eq(Constants.TOKEN), any())) - .then { - token = it.arguments[1] as? String - return@then sharedPrefsEditor - } - - `when`(sharedPrefsEditor.putString(ArgumentMatchers.eq(Constants.USERNAME), any())) - .then { - username = it.arguments[1] as? String - return@then sharedPrefsEditor - } - - `when`( - serverCalls.uploadProject( - any(ProjectUploadData::class.java), - any(ServerCalls.UploadSuccessCallback::class.java), - any(ServerCalls.UploadErrorCallback::class.java) - ) - ).then { - val successCallback = it.arguments[1] as? ServerCalls.UploadSuccessCallback - successCallback?.onSuccess("123", callbackUsername, callbackToken) - } - - createProjectUpload().start( - successCallback = { successCallbackCalled = true }, - errorCallback = { _, _ -> fail("Error callback must not be invoked in this test") } - ) - - assertTrue(successCallbackCalled) - assertEquals(callbackToken, token) - assertEquals(callbackUsername, username) - } - - @Test - fun testProjectUploadError() { - val errorCode = 32_202 - val errorMessage = "An error occured during the project Upload" - var receivedErrorMessage = "" - var receivedErrorCode = -1 - - `when`(zipArchiver.zip(archiveDirectory, projectDirectoryFiles)).then {} - - `when`( - serverCalls.uploadProject( - any(ProjectUploadData::class.java), - any(ServerCalls.UploadSuccessCallback::class.java), - any(ServerCalls.UploadErrorCallback::class.java) - ) - ).then { - val errorCallback = it.arguments[2] as? ServerCalls.UploadErrorCallback - errorCallback?.onError(errorCode, errorMessage) - } - - createProjectUpload().start( - successCallback = { fail("Success callback must not be invoked in this test") }, - errorCallback = { eCode, eMessage -> - receivedErrorCode = eCode - receivedErrorMessage = eMessage - } - ) - - assertEquals(errorCode, receivedErrorCode) - assertEquals(errorMessage, receivedErrorMessage) - } - - @Test - fun testNoUploadOnZipError() { - var receivedErrorCode = -1 - var receivedErrorMessage = "" - - `when`(zipArchiver.zip(archiveDirectory, projectDirectoryFilesFiltered)) - .thenThrow(IOException("Failed to zip project")) - - createProjectUpload().start( - successCallback = { fail("Success callback must not be invoked in this test") }, - errorCallback = { errorCode, errorMessage -> - receivedErrorCode = errorCode - receivedErrorMessage = errorMessage - } - ) - - assertEquals(ProjectUpload.UPLOAD_ZIP_ERROR, receivedErrorCode) - assertEquals(ProjectUpload.UPLOAD_ZIP_ERROR_MESSAGE, receivedErrorMessage) - - didCallUploadProject(never()) - } - - @Test - fun testOneUploadCallPerUploadStart() { - `when`( - serverCalls.uploadProject( - any(ProjectUploadData::class.java), - any(ServerCalls.UploadSuccessCallback::class.java), - any(ServerCalls.UploadErrorCallback::class.java) - ) - ).then {} - - val upload = createProjectUpload() - val timesUploadStartCalled = 3 - repeat(timesUploadStartCalled) { upload.start({}, { _, _ -> }) } - didCallUploadProject(times(timesUploadStartCalled)) - } - - @Test - fun testDeviceVariableFileRemoved() { - createProjectUpload().start( - successCallback = { }, - errorCallback = { _, _ -> } - ) - verify(zipArchiver).zip(archiveDirectory, projectDirectoryFilesFiltered) - } - - private fun createProjectUpload(): ProjectUpload { - return ProjectUpload( - projectDirectory = projectDirectory, - projectName = projectName, - projectDescription = projectDescription, - userEmail = userEmail, - sceneNames = arrayOf("scene1", "scene2", "scene3"), - archiveDirectory = archiveDirectory, - zipArchiver = zipArchiver, - screenshotLoader = screenshotLoader, - sharedPreferences = sharedPreferences, - serverCalls = serverCalls - ) - } - - private fun didCallUploadProject(mode: VerificationMode = times(1)) { - verify(serverCalls, mode).uploadProject( - any(ProjectUploadData::class.java), - any(ServerCalls.UploadSuccessCallback::class.java), - any(ServerCalls.UploadErrorCallback::class.java) - ) - } -}