diff --git a/catroid/build.gradle b/catroid/build.gradle index 6ec3ce14092..2da52ccff94 100644 --- a/catroid/build.gradle +++ b/catroid/build.gradle @@ -127,7 +127,7 @@ android { manifestPlaceholders += [appName: manifestAppName, appIcon: manifestAppIcon, intentFilterScheme: "https", - intentFilterHost: "share.catrob.at", + intentFilterHost: "share.catrobat.org", intentFilterPathPattern: "/.*/project/.*"] buildConfigField "String", "GIT_COMMIT_INFO", "\"${getGitCommitInfo()}\"" @@ -397,6 +397,8 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' // retrofit moshi converter implementation 'com.squareup.retrofit2:converter-moshi:2.7.1' + // Moshi Kotlin support (reflection adapter for Kotlin data classes) + implementation 'com.squareup.moshi:moshi-kotlin:1.15.1' // Glide (Images downloader) implementation 'com.github.bumptech.glide:glide:4.11.0' diff --git a/catroid/src/androidTest/assets/featured_projects_success_response.json b/catroid/src/androidTest/assets/featured_projects_success_response.json index b4b63992dc5..2e12b690d4f 100644 --- a/catroid/src/androidTest/assets/featured_projects_success_response.json +++ b/catroid/src/androidTest/assets/featured_projects_success_response.json @@ -1,26 +1,69 @@ -[ - { - "id": "58", - "project_id": "74758", - "project_url": "https://share.catrob.at/app/project/74758", - "name": "Palmina and the Pirates", - "author": "silverLining", - "featured_image": "https://share.catrob.at/resources/featured/featured_58.png" - }, - { - "id": "45", - "project_id": "48404", - "project_url": "https://share.catrob.at/app/project/48404", - "name": "Magic and More", - "author": "silverLining", - "featured_image": "https://share.catrob.at/resources/featured/featured_45.png" - }, - { - "id": "48", - "project_id": "53658", - "project_url": "https://share.catrob.at/app/project/53658", - "name": "CatWalk", - "author": "silverLining", - "featured_image": "https://share.catrob.at/resources/featured/featured_48.png" - } -] +{ + "data": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "project_id": "f1e2d3c4-b5a6-7890-abcd-ef0987654321", + "project_url": "https://share.catrobat.org/app/project/f1e2d3c4-b5a6-7890-abcd-ef0987654321", + "name": "Palmina and the Pirates", + "author": "silverLining", + "featured_image": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_a1b2-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_a1b2-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_a1b2-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_a1b2-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_a1b2-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_a1b2-detail@2x.webp" + } + } + }, + { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "project_id": "e2d3c4b5-a6f7-8901-bcde-f09876543210", + "project_url": "https://share.catrobat.org/app/project/e2d3c4b5-a6f7-8901-bcde-f09876543210", + "name": "Magic and More", + "author": "silverLining", + "featured_image": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_b2c3-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_b2c3-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_b2c3-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_b2c3-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_b2c3-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_b2c3-detail@2x.webp" + } + } + }, + { + "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", + "project_id": "d3c4b5a6-f7e8-9012-cdef-098765432109", + "project_url": "https://share.catrobat.org/app/project/d3c4b5a6-f7e8-9012-cdef-098765432109", + "name": "CatWalk", + "author": "silverLining", + "featured_image": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_c3d4-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_c3d4-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_c3d4-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_c3d4-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/featured/featured_c3d4-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/featured/featured_c3d4-detail@2x.webp" + } + } + } + ], + "next_cursor": null, + "has_more": false +} diff --git a/catroid/src/androidTest/assets/projects_categories_response.json b/catroid/src/androidTest/assets/projects_categories_response.json index 223f3e9b99d..c0d346b2b90 100644 --- a/catroid/src/androidTest/assets/projects_categories_response.json +++ b/catroid/src/androidTest/assets/projects_categories_response.json @@ -1,1715 +1,2597 @@ -[ - { - "type": "recent", - "name": "Newest projects", - "projectsList": [ - { - "id": "312f3f37-d07e-11eb-ae11-005056a36f47", - "name": "Zombie Attack (3D effect) by PrydeYT", - "author": "Matt3389", - "description": "Eu apenas fiz algumas mudanças,CRÉDITOS para PrydeYT!", - "version": "1.0.3", - "views": 3, - "download": 2, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624052597, - "uploaded_string": "7 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_312f3f37-d07e-11eb-ae11-005056a36f47.png?t=1624052597", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_312f3f37-d07e-11eb-ae11-005056a36f47.png?t=1624052597", - "project_url": "https://share.catrob.at/app/project/312f3f37-d07e-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/312f3f37-d07e-11eb-ae11-005056a36f47.catrobat", - "filesize": 1.2048263549804688 - }, - { - "id": "0f41ac95-d07e-11eb-ae11-005056a36f47", - "name": "Frinday night funkin mod José gamer 7.777", - "author": "Josgamer7.777 ", - "description": "", - "version": "1.0.3", - "views": 6, - "download": 4, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624052540, - "uploaded_string": "8 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_0f41ac95-d07e-11eb-ae11-005056a36f47.png?t=1624052540", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_0f41ac95-d07e-11eb-ae11-005056a36f47.png?t=1624052540", - "project_url": "https://share.catrob.at/app/project/0f41ac95-d07e-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/0f41ac95-d07e-11eb-ae11-005056a36f47.catrobat", - "filesize": 11.018264770507812 - }, - { - "id": "e10a5d5a-d07d-11eb-ae11-005056a36f47", - "name": "Geometria de bola contra blocos", - "author": "Dreafou Games BR", - "description": "Jogo com fisica de bola sob gravidade e reflexão contra objetos geometricos. Ainda em desenvolvimento", - "version": "1.0.3", - "views": 3, - "download": 0, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Experimental", - "Tutorial" - ], - "uploaded": 1624052462, - "uploaded_string": "10 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_e10a5d5a-d07d-11eb-ae11-005056a36f47.png?t=1624052463", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_e10a5d5a-d07d-11eb-ae11-005056a36f47.png?t=1624052463", - "project_url": "https://share.catrob.at/app/project/e10a5d5a-d07d-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/e10a5d5a-d07d-11eb-ae11-005056a36f47.catrobat", - "filesize": 4.98129940032959 - }, - { - "id": "fa75f950-d07c-11eb-ae11-005056a36f47", - "name": "fnafnico", - "author": "Nicolas da jogos", - "description": "UCN FNAF", - "version": "1.0.3", - "views": 6, - "download": 4, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624052076, - "uploaded_string": "16 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_fa75f950-d07c-11eb-ae11-005056a36f47.png?t=1624052076", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_fa75f950-d07c-11eb-ae11-005056a36f47.png?t=1624052076", - "project_url": "https://share.catrob.at/app/project/fa75f950-d07c-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/fa75f950-d07c-11eb-ae11-005056a36f47.catrobat", - "filesize": 34.17116165161133 - }, - { - "id": "5299ec47-d07c-11eb-ae11-005056a36f47", - "name": "windows 10", - "author": "SamuelPlayGamedav (ŤM)", - "description": "1.0", - "version": "1.0.3", - "views": 5, - "download": 5, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1624051794, - "uploaded_string": "21 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_5299ec47-d07c-11eb-ae11-005056a36f47.png?t=1624051794", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_5299ec47-d07c-11eb-ae11-005056a36f47.png?t=1624051794", - "project_url": "https://share.catrob.at/app/project/5299ec47-d07c-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/5299ec47-d07c-11eb-ae11-005056a36f47.catrobat", - "filesize": 1.0948543548583984 - }, - { - "id": "8afd9a2b-d07a-11eb-ae11-005056a36f47", - "name": "parkour", - "author": "Nicolas da jogos", - "description": "", - "version": "1.0.3", - "views": 5, - "download": 4, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Art", - "Experimental" - ], - "uploaded": 1624051030, - "uploaded_string": "34 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_8afd9a2b-d07a-11eb-ae11-005056a36f47.png?t=1624051030", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_8afd9a2b-d07a-11eb-ae11-005056a36f47.png?t=1624051030", - "project_url": "https://share.catrob.at/app/project/8afd9a2b-d07a-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/8afd9a2b-d07a-11eb-ae11-005056a36f47.catrobat", - "filesize": 30.92966651916504 - }, - { - "id": "a3e78f1f-d079-11eb-ae11-005056a36f47", - "name": "jh", - "author": "Oukseto", - "description": "hhh", - "version": "1.0.3", - "views": 5, - "download": 4, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1624050642, - "uploaded_string": "40 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_a3e78f1f-d079-11eb-ae11-005056a36f47.png?t=1624050642", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_a3e78f1f-d079-11eb-ae11-005056a36f47.png?t=1624050642", - "project_url": "https://share.catrob.at/app/project/a3e78f1f-d079-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/a3e78f1f-d079-11eb-ae11-005056a36f47.catrobat", - "filesize": 0.42807483673095703 - }, - { - "id": "b57d4885-d078-11eb-ae11-005056a36f47", - "name": "CoinsMachine", - "author": "SM35020", - "description": "Number Randomizer(Coins Machine)", - "version": "1.0.3", - "views": 12, - "download": 0, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624050242, - "uploaded_string": "47 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_b57d4885-d078-11eb-ae11-005056a36f47.png?t=1624052858", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_b57d4885-d078-11eb-ae11-005056a36f47.png?t=1624052858", - "project_url": "https://share.catrob.at/app/project/b57d4885-d078-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/b57d4885-d078-11eb-ae11-005056a36f47.catrobat", - "filesize": 0.09321308135986328 - }, - { - "id": "51a24562-d078-11eb-ae11-005056a36f47", - "name": "Mit", - "author": "Thiago appp", - "description": "ajude nós", - "version": "1.0.3", - "views": 9, - "download": 3, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation" - ], - "uploaded": 1624050233, - "uploaded_string": "47 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_51a24562-d078-11eb-ae11-005056a36f47.png?t=1624050233", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_51a24562-d078-11eb-ae11-005056a36f47.png?t=1624050233", - "project_url": "https://share.catrob.at/app/project/51a24562-d078-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/51a24562-d078-11eb-ae11-005056a36f47.catrobat", - "filesize": 0.30908966064453125 - }, - { - "id": "8c0bd1d5-d078-11eb-ae11-005056a36f47", - "name": "tiles!", - "author": "Nicolas da jogos", - "description": "", - "version": "1.0.3", - "views": 10, - "download": 14, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Art", - "Experimental" - ], - "uploaded": 1624050172, - "uploaded_string": "48 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_8c0bd1d5-d078-11eb-ae11-005056a36f47.png?t=1624050172", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_8c0bd1d5-d078-11eb-ae11-005056a36f47.png?t=1624050172", - "project_url": "https://share.catrob.at/app/project/8c0bd1d5-d078-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/8c0bd1d5-d078-11eb-ae11-005056a36f47.catrobat", - "filesize": 0.7816238403320312 - }, - { - "id": "5c65637a-d078-11eb-ae11-005056a36f47", - "name": "tiles", - "author": "Nicolas da jogos", - "description": "", - "version": "1.0.3", - "views": 4, - "download": 0, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624050092, - "uploaded_string": "49 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_5c65637a-d078-11eb-ae11-005056a36f47.png?t=1624050093", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_5c65637a-d078-11eb-ae11-005056a36f47.png?t=1624050093", - "project_url": "https://share.catrob.at/app/project/5c65637a-d078-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/5c65637a-d078-11eb-ae11-005056a36f47.catrobat", - "filesize": 1.4275531768798828 - }, - { - "id": "739311dc-d077-11eb-ae11-005056a36f47", - "name": "legends of universe", - "author": "agente Gabriel", - "description": "beta 0.1", - "version": "1.0.3", - "views": 10, - "download": 6, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Experimental" - ], - "uploaded": 1624049702, - "uploaded_string": "56 minutes ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_739311dc-d077-11eb-ae11-005056a36f47.png?t=1624049702", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_739311dc-d077-11eb-ae11-005056a36f47.png?t=1624049702", - "project_url": "https://share.catrob.at/app/project/739311dc-d077-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/739311dc-d077-11eb-ae11-005056a36f47.catrobat", - "filesize": 17.63987922668457 - }, - { - "id": "86dcf517-d076-11eb-ae11-005056a36f47", - "name": "Friday Night Funkin DEMO v0.02", - "author": "SamuelPlayGamedav (ŤM)", - "description": "", - "version": "1.0.3", - "views": 30, - "download": 32, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1624049305, - "uploaded_string": "1 hour ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_86dcf517-d076-11eb-ae11-005056a36f47.png?t=1624049305", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_86dcf517-d076-11eb-ae11-005056a36f47.png?t=1624049305", - "project_url": "https://share.catrob.at/app/project/86dcf517-d076-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/86dcf517-d076-11eb-ae11-005056a36f47.catrobat", - "filesize": 13.40373706817627 - }, - { - "id": "cb181513-d075-11eb-ae11-005056a36f47", - "name": "One Night At G7 Demo:1.8", - "author": "Petherson", - "description": "COMEÇA,EXTRAS,MODOHARD,HORAS,ANIMATRONICS,SELIVRADOSANIMATRONICS,ETC,", - "version": "1.0.3", - "views": 9, - "download": 9, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624048990, - "uploaded_string": "1 hour ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_cb181513-d075-11eb-ae11-005056a36f47.png?t=1624049315", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_cb181513-d075-11eb-ae11-005056a36f47.png?t=1624049315", - "project_url": "https://share.catrob.at/app/project/cb181513-d075-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/cb181513-d075-11eb-ae11-005056a36f47.catrobat", - "filesize": 2.023141860961914 - }, - { - "id": "3425d023-92a2-11eb-a92d-005056a36f47", - "name": "Among Us", - "author": "Paulinhocastrogame@gmail.com", - "description": "Junte-se a seus companheiros de equipe em um jogo multiplayer de trabalho em equipe e traição!\n☆☆☆☆☆ novas coisas seram adicionadas ao jogos em cada versões", - "version": "1.0.3", - "views": 171, - "download": 210, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624048829, - "uploaded_string": "1 hour ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_3425d023-92a2-11eb-a92d-005056a36f47.png?t=1624048829", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_3425d023-92a2-11eb-a92d-005056a36f47.png?t=1624048829", - "project_url": "https://share.catrob.at/app/project/3425d023-92a2-11eb-a92d-005056a36f47", - "download_url": "https://share.catrob.at/app/download/3425d023-92a2-11eb-a92d-005056a36f47.catrobat", - "filesize": 17.401137351989746 - }, - { - "id": "ddb90ddc-c711-11eb-abc2-005056a36f47", - "name": "big boll bola na rede FCº Ruan", - "author": "Ruan003", - "description": "FC RUAN", - "version": "1.0.3", - "views": 8, - "download": 12, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624048659, - "uploaded_string": "1 hour ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_ddb90ddc-c711-11eb-abc2-005056a36f47.png?t=1624048659", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_ddb90ddc-c711-11eb-abc2-005056a36f47.png?t=1624048659", - "project_url": "https://share.catrob.at/app/project/ddb90ddc-c711-11eb-abc2-005056a36f47", - "download_url": "https://share.catrob.at/app/download/ddb90ddc-c711-11eb-abc2-005056a36f47.catrobat", - "filesize": 0.4646759033203125 - }, - { - "id": "40c2f65a-b056-11eb-abc2-005056a36f47", - "name": "Lazer wars BETA", - "author": "Vitamot studio 0fficial", - "description": "", - "version": "1.0.3", - "views": 179, - "download": 159, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1624048649, - "uploaded_string": "1 hour ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_40c2f65a-b056-11eb-abc2-005056a36f47.png?t=1624048649", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_40c2f65a-b056-11eb-abc2-005056a36f47.png?t=1624048649", - "project_url": "https://share.catrob.at/app/project/40c2f65a-b056-11eb-abc2-005056a36f47", - "download_url": "https://share.catrob.at/app/download/40c2f65a-b056-11eb-abc2-005056a36f47.catrobat", - "filesize": 4.236872673034668 - }, - { - "id": "f9e43c4d-d074-11eb-ae11-005056a36f47", - "name": "BATALHAS E CHEFÕES GAMERTY ATUALIZAÇÃO", - "author": "studiogamet123", - "description": "GAMERTY ATUALIZAÇÃO!", - "version": "1.0.3", - "views": 8, - "download": 2, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Story" - ], - "uploaded": 1624048639, - "uploaded_string": "1 hour ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_f9e43c4d-d074-11eb-ae11-005056a36f47.png?t=1624048639", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_f9e43c4d-d074-11eb-ae11-005056a36f47.png?t=1624048639", - "project_url": "https://share.catrob.at/app/project/f9e43c4d-d074-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/f9e43c4d-d074-11eb-ae11-005056a36f47.catrobat", - "filesize": 23.095444679260254 - }, - { - "id": "4a335e6e-cb66-11eb-99b1-005056a36f47", - "name": "One Night In Among Us", - "author": "Rekky_yt", - "description": "yo c'est rekky la team des français!", - "version": "1.0.3", - "views": 57, - "download": 61, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Experimental" - ], - "uploaded": 1624048037, - "uploaded_string": "1 hour ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_4a335e6e-cb66-11eb-99b1-005056a36f47.png?t=1624048037", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_4a335e6e-cb66-11eb-99b1-005056a36f47.png?t=1624048037", - "project_url": "https://share.catrob.at/app/project/4a335e6e-cb66-11eb-99b1-005056a36f47", - "download_url": "https://share.catrob.at/app/download/4a335e6e-cb66-11eb-99b1-005056a36f47.catrobat", - "filesize": 12.279342651367188 - }, - { - "id": "12e40e09-cfc1-11eb-ae11-005056a36f47", - "name": "Castle escape", - "author": "Switzeer", - "description": "Escape do castelo! Game in demo", - "version": "1.0.3", - "views": 10, - "download": 11, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Experimental" - ], - "uploaded": 1624047921, - "uploaded_string": "1 hour ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_12e40e09-cfc1-11eb-ae11-005056a36f47.png?t=1624047921", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_12e40e09-cfc1-11eb-ae11-005056a36f47.png?t=1624047921", - "project_url": "https://share.catrob.at/app/project/12e40e09-cfc1-11eb-ae11-005056a36f47", - "download_url": "https://share.catrob.at/app/download/12e40e09-cfc1-11eb-ae11-005056a36f47.catrobat", - "filesize": 0.36594486236572266 - } - ] - }, - { - "type": "most_viewed", - "name": "Most viewed", - "projectsList": [ - { - "id": "5680", - "name": "Minecraft", - "author": "XxJHKanalxX", - "description": ">New Minecraft Update!\n\n >Version 3.0\n +Fix Bugs\n\n >Version 3.1\n +New Color\n +New Skin\n +Add Musik on \"Audio\"\n\n >Version 3.2\n +Fix Bugs\n\n>Check my Social Media\n \n *Instagram:XxJHKanalxX\n *YouTube: XxJHKanalxX\n\nVersion 3.2\n\n#GalaxyGameJam", - "version": "0.9.31", - "views": 232617, - "download": 222807, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Experimental" - ], - "uploaded": 1510846570, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_5680.png?t=1598101073", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_5680.png?t=1598163731", - "project_url": "https://share.catrob.at/app/project/5680", - "download_url": "https://share.catrob.at/app/download/5680.catrobat", - "filesize": 67.80170440673828 - }, - { - "id": "817", - "name": "Tic-Tac-Toe Master", - "author": "hej-wickie-hej", - "description": "Three in one win. However, the computer is a master strategist! \n\nThe text was composed using PicSay - http://catrob.at/PicSay as explained on http://catrob.at/WordBalloonExplanation\n\n#GalaxyGameJam", - "version": "0.9.6", - "views": 107733, - "download": 121606, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1366619431, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_817.png?t=1598101039", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_817.png?t=1598163723", - "project_url": "https://share.catrob.at/app/project/817", - "download_url": "https://share.catrob.at/app/download/817.catrobat", - "filesize": 0.2409677505493164 - }, - { - "id": "37890", - "name": "Sonic Mania Android Version 0.2", - "author": "KyleOfBlades", - "description": "Update of MY port,I've been seeing ALOT of remixes of my port so chill with em,don't completely copy my port.\n\nChangelog:Level Select,Intro,Studiopolis,and finally Auto Save Screen", - "version": "0.9.29", - "views": 79777, - "download": 59988, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1505866788, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_37890.png?t=1598101082", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_37890.png?t=1598163732", - "project_url": "https://share.catrob.at/app/project/37890", - "download_url": "https://share.catrob.at/app/download/37890.catrobat", - "filesize": 70.83020114898682 - }, - { - "id": "719", - "name": "Galaxy War", - "author": "DavidR", - "description": "Control the ship by tilting the phone and shoot down as many enemies as possible.", - "version": "0.7.0beta", - "views": 72175, - "download": 81990, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1365151308, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_719.png?t=1598101059", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_719.png?t=1598163728", - "project_url": "https://share.catrob.at/app/project/719", - "download_url": "https://share.catrob.at/app/download/719.catrobat", - "filesize": 3.0423545837402344 - }, - { - "id": "1581", - "name": "Flappy_v3.0", - "author": "mj7007", - "description": "A simple game similar to the popular Flappy Bird, completely made using Pocket Code. Game not tested on multiple devices yet, which might lead to minor issues. Please let me know about any issues and always welcome to remix this game. ", - "version": "0.9.", - "views": 65664, - "download": 78953, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1394965883, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_1581.png?t=1598101002", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_1581.png?t=1598163713", - "project_url": "https://share.catrob.at/app/project/1581", - "download_url": "https://share.catrob.at/app/download/1581.catrobat", - "filesize": 0.10238456726074219 - }, - { - "id": "45868", - "name": "Iso-motion", - "author": "headhunter", - "description": "To play this game, cast your screen to a TV or a PC and use the device as a motion controller to move your character.\n***********************\nMade for GCI task \nAny and All assets used in the program are made by me and are without any previous copyrights, feel free to use them in your programs.\nCalibration code taken from task- https://share.catrob.at/pocketcode/program/38660\n************************\nUpdate 2\n- added death counter\nUpdate 1\n- changed Level 1 orb position.\n- More levels coming soon...", - "version": "0.9.33", - "views": 44948, - "download": 44009, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Experimental" - ], - "uploaded": 1515837610, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_45868.png?t=1598101048", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_45868.png?t=1598163726", - "project_url": "https://share.catrob.at/app/project/45868", - "download_url": "https://share.catrob.at/app/download/45868.catrobat", - "filesize": 30.878884315490723 - }, - { - "id": "20065", - "name": "Geometry Dash v1.5", - "author": "VenomX", - "description": "\nMade with Scratch2Catrobat Converter version 0.7.2.\nOriginal Scratch project => https://scratch.mit.edu/projects/105500895", - "version": "0.9.27", - "views": 44238, - "download": 39256, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Music" - ], - "uploaded": 1481385824, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_20065.png?t=1598100995", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_20065.png?t=1598163709", - "project_url": "https://share.catrob.at/app/project/20065", - "download_url": "https://share.catrob.at/app/download/20065.catrobat", - "filesize": 16.550339698791504 - }, - { - "id": "43252", - "name": "Fnaf Pizzeria Simulator", - "author": "KyleOfBlades", - "description": "My Port of Fnaf 6! Enjoy! Please subscribe! My Channel is Jack Øf Blades", - "version": "0.9.32", - "views": 42732, - "download": 38356, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1512901593, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_43252.png?t=1598100993", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_43252.png?t=1598163708", - "project_url": "https://share.catrob.at/app/project/43252", - "download_url": "https://share.catrob.at/app/download/43252.catrobat", - "filesize": 12.229867935180664 - }, - { - "id": "46b43efe-fb9e-11ea-a92d-005056a36f47", - "name": "Pocket Moon v1.0", - "author": "Jude Birch", - "description": "Farming game made for Google Summer of Code.\n\nPlay as the owner of a farm and grow different crops over the seasons. Sell crops to buy more seeds and tools at the shop, and unlock more areas! You can also explore the world and discover the 21 different fish in the game!\n\nGoogle Summer of Code Page: https://summerofcode.withgoogle.com/projects/5782435888889856\nGameplay: https://youtu.be/oMPMTiVN1Vs\n\nThanks to Jordan Smith for mentoring and supporting the project.", - "version": "0.9.74", - "views": 39021, - "download": 28992, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1603881513, - "uploaded_string": "8 months ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_46b43efe-fb9e-11ea-a92d-005056a36f47.png?t=1603881513", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_46b43efe-fb9e-11ea-a92d-005056a36f47.png?t=1603881513", - "project_url": "https://share.catrob.at/app/project/46b43efe-fb9e-11ea-a92d-005056a36f47", - "download_url": "https://share.catrob.at/app/download/46b43efe-fb9e-11ea-a92d-005056a36f47.catrobat", - "filesize": 4.47955322265625 - }, - { - "id": "42921", - "name": "Math Quest! v2", - "author": "Jude Birch", - "description": "Here's an educational binary themed RPG for Google code-in!", - "version": "0.9.32", - "views": 33784, - "download": 25156, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1512510832, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_42921.png?t=1598101029", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_42921.png?t=1598163721", - "project_url": "https://share.catrob.at/app/project/42921", - "download_url": "https://share.catrob.at/app/download/42921.catrobat", - "filesize": 36.889180183410645 - }, - { - "id": "83613", - "name": "R-Type", - "author": "Stroy562", - "description": "Управляй космическим аппаратом.", - "version": "0.9.70", - "views": 31095, - "download": 3019, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1592770087, - "uploaded_string": "1 year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_83613.png?t=1598101027", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_83613.png?t=1598163720", - "project_url": "https://share.catrob.at/app/project/83613", - "download_url": "https://share.catrob.at/app/download/83613.catrobat", - "filesize": 62.580196380615234 - }, - { - "id": "1450", - "name": "Minions ", - "author": "kestapa", - "description": "", - "version": "0.9.4", - "views": 28599, - "download": 32713, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1383107039, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_1450.png?t=1598101010", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_1450.png?t=1598163717", - "project_url": "https://share.catrob.at/app/project/1450", - "download_url": "https://share.catrob.at/app/download/1450.catrobat", - "filesize": 0.9740276336669922 - }, - { - "id": "49860", - "name": "Undertale: rood fight", - "author": "Powerpoop", - "description": "No", - "version": "0.9.33", - "views": 27617, - "download": 28961, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Story" - ], - "uploaded": 1520904439, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_49860.png?t=1598101093", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_49860.png?t=1598163735", - "project_url": "https://share.catrob.at/app/project/49860", - "download_url": "https://share.catrob.at/app/download/49860.catrobat", - "filesize": 15.183897018432617 - }, - { - "id": "965", - "name": "Air fight 0.5", - "author": "hej-wickie-hej", - "description": "Shoot down your opponent! Steer with your device's inclination. Shoot by tapping or holding the screen. \r\n\r\nUpdate 2016-11-2: now with object grouping, new Catrobat language version 0.992 bricks, as well as unlimited bullets (through clone bricks).", - "version": "0.9.27", - "views": 27399, - "download": 30626, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1478104990, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_965.png?t=1598101092", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_965.png?t=1598163735", - "project_url": "https://share.catrob.at/app/project/965", - "download_url": "https://share.catrob.at/app/download/965.catrobat", - "filesize": 5.546684265136719 - }, - { - "id": "72798", - "name": "CS:GO", - "author": "GameCompani7", - "description": "Дата релиза! (12.31.18!) \n\nОбновление! \n-Крутое обновление! \n- Обновил оружия теперь крутые! \n-Добавил Музыку в меню! \n-Поменял карты сделал красивыми\nи 3D все изменил! \n-Изменил Фон Меню! \n-Добавил загрузку карты! \n\n\nCSGO. От компании GameKompani7\n\nВы играете В роли Спецназа или же Терориста! \nУбейте всех за 40-45 секунд! \nМного оружие скины кейсы 4 карты! \n(Dast inferno Nyke office) \nДостигни Глобала! \nИ стань лучшим! \n Оцените нас! \nВидь разработка была долгой и трудной! \nПерезарядка всё тут есть ну почти! \nВозможно через 7 дней будит обновление так что\nЖдите! \nПрава/ GameKompani7®", - "version": "0.9.44", - "views": 22475, - "download": 56214, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1546881392, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_72798.png?t=1598101072", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_72798.png?t=1598163731", - "project_url": "https://share.catrob.at/app/project/72798", - "download_url": "https://share.catrob.at/app/download/72798.catrobat", - "filesize": 19.330181121826172 - }, - { - "id": "32577", - "name": "Air Hockey", - "author": "Kingsimon", - "description": "Air Hockey", - "version": "0.9.74", - "views": 20334, - "download": 23830, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Music", - "Experimental" - ], - "uploaded": 1602605578, - "uploaded_string": "8 months ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_32577.png?t=1602605578", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_32577.png?t=1602605578", - "project_url": "https://share.catrob.at/app/project/32577", - "download_url": "https://share.catrob.at/app/download/32577.catrobat", - "filesize": 23.37629508972168 - }, - { - "id": "44987", - "name": "Slendytubbies 2D Part 2", - "author": "Felix Filip", - "description": "V1.3 (Last Update) 12.02.18\n\nOBJECTIVE:\nyou need to collect all 5 - 25 Custards while trying to avoid the Monsters\n\nThis game is proven to be the best and scariest game on Pocket Code, If you don't believe me just play the game\n\n- 14 Maps with different Monsters\n\n- Choose Between an Flashlight or an Night Vision Camera\n\n- Animations\n\n- All maps are Large\n\n- You can change the Custard level from 5 to 25 Custards\n\n-There's an Energy bar for the Flashlight and the Night Vision Camera so be careful and  don't use it to much\n\nYouTube: Felix Filip\nGame by: Felix Filip\nTwitter: @FelixFilipYT\nRoblox: FelixFilipYT\nMCPE: Felix Filip AC", - "version": "0.9.33", - "views": 20125, - "download": 24661, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1518476424, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_44987.png?t=1598101087", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_44987.png?t=1598163734", - "project_url": "https://share.catrob.at/app/project/44987", - "download_url": "https://share.catrob.at/app/download/44987.catrobat", - "filesize": 47.748464584350586 - }, - { - "id": "58961", - "name": "ultimate custon night fnaf6", - "author": "joelio", - "description": "UCN FNAF", - "version": "0.9.33", - "views": 19557, - "download": 10021, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1530544175, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_58961.png?t=1598101006", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_58961.png?t=1598163715", - "project_url": "https://share.catrob.at/app/project/58961", - "download_url": "https://share.catrob.at/app/download/58961.catrobat", - "filesize": 26.86823558807373 - }, - { - "id": "1719", - "name": "Don't touch the white tiles!", - "author": "hej-wickie-hej", - "description": "You have 25 seconds to tap as many of the black tiles as possible, starting from the bottom. Of course, don't touch the white ones!\n\nNote that, although you must tap the black tiles one after the other starting from the lower ones, it is nevertheless possible to tap the next tiles already while they are still moving. Doing so gives exponentially more points, depending on how high the tiles were on the screen when you tapped them. \n\nRemark: All numbers and other text in images have been made with PicSay, available on Google Play, see http://catrob.at/PicSay and http://catrob.at/WordBalloonExplanation ", - "version": "0.9.9", - "views": 17981, - "download": 22867, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1404682455, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_1719.png?t=1598101072", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_1719.png?t=1598163730", - "project_url": "https://share.catrob.at/app/project/1719", - "download_url": "https://share.catrob.at/app/download/1719.catrobat", - "filesize": 0.1950674057006836 - }, - { - "id": "45034", - "name": "MasterMind Game ", - "author": "Aditya R Nair", - "description": "Mastermind created in PocketCode.", - "version": "0.9.33", - "views": 16771, - "download": 8012, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1514851170, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_45034.png?t=1598101076", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_45034.png?t=1598163731", - "project_url": "https://share.catrob.at/app/project/45034", - "download_url": "https://share.catrob.at/app/download/45034.catrobat", - "filesize": 8.764063835144043 - } - ] - }, - { - "type": "most_downloaded", - "name": "Most downloaded", - "projectsList": [ - { - "id": "5680", - "name": "Minecraft", - "author": "XxJHKanalxX", - "description": ">New Minecraft Update!\n\n >Version 3.0\n +Fix Bugs\n\n >Version 3.1\n +New Color\n +New Skin\n +Add Musik on \"Audio\"\n\n >Version 3.2\n +Fix Bugs\n\n>Check my Social Media\n \n *Instagram:XxJHKanalxX\n *YouTube: XxJHKanalxX\n\nVersion 3.2\n\n#GalaxyGameJam", - "version": "0.9.31", - "views": 232617, - "download": 222807, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Experimental" - ], - "uploaded": 1510846570, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_5680.png?t=1598101073", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_5680.png?t=1598163731", - "project_url": "https://share.catrob.at/app/project/5680", - "download_url": "https://share.catrob.at/app/download/5680.catrobat", - "filesize": 67.80170440673828 - }, - { - "id": "817", - "name": "Tic-Tac-Toe Master", - "author": "hej-wickie-hej", - "description": "Three in one win. However, the computer is a master strategist! \n\nThe text was composed using PicSay - http://catrob.at/PicSay as explained on http://catrob.at/WordBalloonExplanation\n\n#GalaxyGameJam", - "version": "0.9.6", - "views": 107733, - "download": 121606, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1366619431, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_817.png?t=1598101039", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_817.png?t=1598163723", - "project_url": "https://share.catrob.at/app/project/817", - "download_url": "https://share.catrob.at/app/download/817.catrobat", - "filesize": 0.2409677505493164 - }, - { - "id": "719", - "name": "Galaxy War", - "author": "DavidR", - "description": "Control the ship by tilting the phone and shoot down as many enemies as possible.", - "version": "0.7.0beta", - "views": 72175, - "download": 81990, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1365151308, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_719.png?t=1598101059", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_719.png?t=1598163728", - "project_url": "https://share.catrob.at/app/project/719", - "download_url": "https://share.catrob.at/app/download/719.catrobat", - "filesize": 3.0423545837402344 - }, - { - "id": "1581", - "name": "Flappy_v3.0", - "author": "mj7007", - "description": "A simple game similar to the popular Flappy Bird, completely made using Pocket Code. Game not tested on multiple devices yet, which might lead to minor issues. Please let me know about any issues and always welcome to remix this game. ", - "version": "0.9.", - "views": 65664, - "download": 78953, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1394965883, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_1581.png?t=1598101002", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_1581.png?t=1598163713", - "project_url": "https://share.catrob.at/app/project/1581", - "download_url": "https://share.catrob.at/app/download/1581.catrobat", - "filesize": 0.10238456726074219 - }, - { - "id": "37890", - "name": "Sonic Mania Android Version 0.2", - "author": "KyleOfBlades", - "description": "Update of MY port,I've been seeing ALOT of remixes of my port so chill with em,don't completely copy my port.\n\nChangelog:Level Select,Intro,Studiopolis,and finally Auto Save Screen", - "version": "0.9.29", - "views": 79777, - "download": 59988, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1505866788, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_37890.png?t=1598101082", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_37890.png?t=1598163732", - "project_url": "https://share.catrob.at/app/project/37890", - "download_url": "https://share.catrob.at/app/download/37890.catrobat", - "filesize": 70.83020114898682 - }, - { - "id": "72798", - "name": "CS:GO", - "author": "GameCompani7", - "description": "Дата релиза! (12.31.18!) \n\nОбновление! \n-Крутое обновление! \n- Обновил оружия теперь крутые! \n-Добавил Музыку в меню! \n-Поменял карты сделал красивыми\nи 3D все изменил! \n-Изменил Фон Меню! \n-Добавил загрузку карты! \n\n\nCSGO. От компании GameKompani7\n\nВы играете В роли Спецназа или же Терориста! \nУбейте всех за 40-45 секунд! \nМного оружие скины кейсы 4 карты! \n(Dast inferno Nyke office) \nДостигни Глобала! \nИ стань лучшим! \n Оцените нас! \nВидь разработка была долгой и трудной! \nПерезарядка всё тут есть ну почти! \nВозможно через 7 дней будит обновление так что\nЖдите! \nПрава/ GameKompani7®", - "version": "0.9.44", - "views": 22475, - "download": 56214, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1546881392, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_72798.png?t=1598101072", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_72798.png?t=1598163731", - "project_url": "https://share.catrob.at/app/project/72798", - "download_url": "https://share.catrob.at/app/download/72798.catrobat", - "filesize": 19.330181121826172 - }, - { - "id": "f0ddcde2-9ca2-11eb-a92d-005056a36f47", - "name": "Beste game in ganz Pocket code?!", - "author": "Tap Tap Inc.", - "description": "Vlt...", - "version": "1.0.3", - "views": 281, - "download": 55104, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Experimental", - "Tutorial" - ], - "uploaded": 1623626420, - "uploaded_string": "5 days ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_f0ddcde2-9ca2-11eb-a92d-005056a36f47.png?t=1620830433", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_f0ddcde2-9ca2-11eb-a92d-005056a36f47.png?t=1620830433", - "project_url": "https://share.catrob.at/app/project/f0ddcde2-9ca2-11eb-a92d-005056a36f47", - "download_url": "https://share.catrob.at/app/download/f0ddcde2-9ca2-11eb-a92d-005056a36f47.catrobat", - "filesize": 0.15716552734375 - }, - { - "id": "45868", - "name": "Iso-motion", - "author": "headhunter", - "description": "To play this game, cast your screen to a TV or a PC and use the device as a motion controller to move your character.\n***********************\nMade for GCI task \nAny and All assets used in the program are made by me and are without any previous copyrights, feel free to use them in your programs.\nCalibration code taken from task- https://share.catrob.at/pocketcode/program/38660\n************************\nUpdate 2\n- added death counter\nUpdate 1\n- changed Level 1 orb position.\n- More levels coming soon...", - "version": "0.9.33", - "views": 44948, - "download": 44009, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Experimental" - ], - "uploaded": 1515837610, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_45868.png?t=1598101048", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_45868.png?t=1598163726", - "project_url": "https://share.catrob.at/app/project/45868", - "download_url": "https://share.catrob.at/app/download/45868.catrobat", - "filesize": 30.878884315490723 - }, - { - "id": "20065", - "name": "Geometry Dash v1.5", - "author": "VenomX", - "description": "\nMade with Scratch2Catrobat Converter version 0.7.2.\nOriginal Scratch project => https://scratch.mit.edu/projects/105500895", - "version": "0.9.27", - "views": 44238, - "download": 39256, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Music" - ], - "uploaded": 1481385824, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_20065.png?t=1598100995", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_20065.png?t=1598163709", - "project_url": "https://share.catrob.at/app/project/20065", - "download_url": "https://share.catrob.at/app/download/20065.catrobat", - "filesize": 16.550339698791504 - }, - { - "id": "43252", - "name": "Fnaf Pizzeria Simulator", - "author": "KyleOfBlades", - "description": "My Port of Fnaf 6! Enjoy! Please subscribe! My Channel is Jack Øf Blades", - "version": "0.9.32", - "views": 42732, - "download": 38356, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1512901593, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_43252.png?t=1598100993", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_43252.png?t=1598163708", - "project_url": "https://share.catrob.at/app/project/43252", - "download_url": "https://share.catrob.at/app/download/43252.catrobat", - "filesize": 12.229867935180664 - }, - { - "id": "1450", - "name": "Minions ", - "author": "kestapa", - "description": "", - "version": "0.9.4", - "views": 28599, - "download": 32713, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1383107039, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_1450.png?t=1598101010", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_1450.png?t=1598163717", - "project_url": "https://share.catrob.at/app/project/1450", - "download_url": "https://share.catrob.at/app/download/1450.catrobat", - "filesize": 0.9740276336669922 - }, - { - "id": "965", - "name": "Air fight 0.5", - "author": "hej-wickie-hej", - "description": "Shoot down your opponent! Steer with your device's inclination. Shoot by tapping or holding the screen. \r\n\r\nUpdate 2016-11-2: now with object grouping, new Catrobat language version 0.992 bricks, as well as unlimited bullets (through clone bricks).", - "version": "0.9.27", - "views": 27399, - "download": 30626, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1478104990, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_965.png?t=1598101092", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_965.png?t=1598163735", - "project_url": "https://share.catrob.at/app/project/965", - "download_url": "https://share.catrob.at/app/download/965.catrobat", - "filesize": 5.546684265136719 - }, - { - "id": "46b43efe-fb9e-11ea-a92d-005056a36f47", - "name": "Pocket Moon v1.0", - "author": "Jude Birch", - "description": "Farming game made for Google Summer of Code.\n\nPlay as the owner of a farm and grow different crops over the seasons. Sell crops to buy more seeds and tools at the shop, and unlock more areas! You can also explore the world and discover the 21 different fish in the game!\n\nGoogle Summer of Code Page: https://summerofcode.withgoogle.com/projects/5782435888889856\nGameplay: https://youtu.be/oMPMTiVN1Vs\n\nThanks to Jordan Smith for mentoring and supporting the project.", - "version": "0.9.74", - "views": 39021, - "download": 28992, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1603881513, - "uploaded_string": "8 months ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_46b43efe-fb9e-11ea-a92d-005056a36f47.png?t=1603881513", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_46b43efe-fb9e-11ea-a92d-005056a36f47.png?t=1603881513", - "project_url": "https://share.catrob.at/app/project/46b43efe-fb9e-11ea-a92d-005056a36f47", - "download_url": "https://share.catrob.at/app/download/46b43efe-fb9e-11ea-a92d-005056a36f47.catrobat", - "filesize": 4.47955322265625 - }, - { - "id": "49860", - "name": "Undertale: rood fight", - "author": "Powerpoop", - "description": "No", - "version": "0.9.33", - "views": 27617, - "download": 28961, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Story" - ], - "uploaded": 1520904439, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_49860.png?t=1598101093", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_49860.png?t=1598163735", - "project_url": "https://share.catrob.at/app/project/49860", - "download_url": "https://share.catrob.at/app/download/49860.catrobat", - "filesize": 15.183897018432617 - }, - { - "id": "7eb7c370-de0c-11ea-a5c3-005056a36f47", - "name": "flying bird", - "author": "Gampley", - "description": "When clik Bird seems sounrs", - "version": "0.9.75", - "views": 5222, - "download": 27122, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Animation" - ], - "uploaded": 1611489958, - "uploaded_string": "5 months ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_7eb7c370-de0c-11ea-a5c3-005056a36f47.png?t=1611489958", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_7eb7c370-de0c-11ea-a5c3-005056a36f47.png?t=1611489958", - "project_url": "https://share.catrob.at/app/project/7eb7c370-de0c-11ea-a5c3-005056a36f47", - "download_url": "https://share.catrob.at/app/download/7eb7c370-de0c-11ea-a5c3-005056a36f47.catrobat", - "filesize": 1.6682100296020508 - }, - { - "id": "42921", - "name": "Math Quest! v2", - "author": "Jude Birch", - "description": "Here's an educational binary themed RPG for Google code-in!", - "version": "0.9.32", - "views": 33784, - "download": 25156, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1512510832, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_42921.png?t=1598101029", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_42921.png?t=1598163721", - "project_url": "https://share.catrob.at/app/project/42921", - "download_url": "https://share.catrob.at/app/download/42921.catrobat", - "filesize": 36.889180183410645 - }, - { - "id": "44987", - "name": "Slendytubbies 2D Part 2", - "author": "Felix Filip", - "description": "V1.3 (Last Update) 12.02.18\n\nOBJECTIVE:\nyou need to collect all 5 - 25 Custards while trying to avoid the Monsters\n\nThis game is proven to be the best and scariest game on Pocket Code, If you don't believe me just play the game\n\n- 14 Maps with different Monsters\n\n- Choose Between an Flashlight or an Night Vision Camera\n\n- Animations\n\n- All maps are Large\n\n- You can change the Custard level from 5 to 25 Custards\n\n-There's an Energy bar for the Flashlight and the Night Vision Camera so be careful and  don't use it to much\n\nYouTube: Felix Filip\nGame by: Felix Filip\nTwitter: @FelixFilipYT\nRoblox: FelixFilipYT\nMCPE: Felix Filip AC", - "version": "0.9.33", - "views": 20125, - "download": 24661, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1518476424, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_44987.png?t=1598101087", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_44987.png?t=1598163734", - "project_url": "https://share.catrob.at/app/project/44987", - "download_url": "https://share.catrob.at/app/download/44987.catrobat", - "filesize": 47.748464584350586 - }, - { - "id": "32577", - "name": "Air Hockey", - "author": "Kingsimon", - "description": "Air Hockey", - "version": "0.9.74", - "views": 20334, - "download": 23830, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Music", - "Experimental" - ], - "uploaded": 1602605578, - "uploaded_string": "8 months ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_32577.png?t=1602605578", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_32577.png?t=1602605578", - "project_url": "https://share.catrob.at/app/project/32577", - "download_url": "https://share.catrob.at/app/download/32577.catrobat", - "filesize": 23.37629508972168 - }, - { - "id": "1719", - "name": "Don't touch the white tiles!", - "author": "hej-wickie-hej", - "description": "You have 25 seconds to tap as many of the black tiles as possible, starting from the bottom. Of course, don't touch the white ones!\n\nNote that, although you must tap the black tiles one after the other starting from the lower ones, it is nevertheless possible to tap the next tiles already while they are still moving. Doing so gives exponentially more points, depending on how high the tiles were on the screen when you tapped them. \n\nRemark: All numbers and other text in images have been made with PicSay, available on Google Play, see http://catrob.at/PicSay and http://catrob.at/WordBalloonExplanation ", - "version": "0.9.9", - "views": 17981, - "download": 22867, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1404682455, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_1719.png?t=1598101072", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_1719.png?t=1598163730", - "project_url": "https://share.catrob.at/app/project/1719", - "download_url": "https://share.catrob.at/app/download/1719.catrobat", - "filesize": 0.1950674057006836 - }, - { - "id": "1976", - "name": "Skydiving Steve", - "author": "PocketCodeTeam", - "description": "Download this program to start with the One Hour of Code tutorial at https://pocketcode.org/hourOfCode", - "version": "0.9.10", - "views": 15617, - "download": 19520, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1410514368, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_1976.png?t=1598101087", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_1976.png?t=1598163734", - "project_url": "https://share.catrob.at/app/project/1976", - "download_url": "https://share.catrob.at/app/download/1976.catrobat", - "filesize": 1.7407207489013672 - } - ] - }, - { - "type": "scratch", - "name": "Scratch remixes", - "projectsList": [ - { - "id": "20065", - "name": "Geometry Dash v1.5", - "author": "VenomX", - "description": "\nMade with Scratch2Catrobat Converter version 0.7.2.\nOriginal Scratch project => https://scratch.mit.edu/projects/105500895", - "version": "0.9.27", - "views": 44238, - "download": 39256, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Music" - ], - "uploaded": 1481385824, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_20065.png?t=1598100995", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_20065.png?t=1598163709", - "project_url": "https://share.catrob.at/app/project/20065", - "download_url": "https://share.catrob.at/app/download/20065.catrobat", - "filesize": 16.550339698791504 - }, - { - "id": "49860", - "name": "Undertale: rood fight", - "author": "Powerpoop", - "description": "No", - "version": "0.9.33", - "views": 27617, - "download": 28961, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Story" - ], - "uploaded": 1520904439, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_49860.png?t=1598101093", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_49860.png?t=1598163735", - "project_url": "https://share.catrob.at/app/project/49860", - "download_url": "https://share.catrob.at/app/download/49860.catrobat", - "filesize": 15.183897018432617 - }, - { - "id": "18600", - "name": "Super Mario Bros (version 10)", - "author": "Ayrat", - "description": "\n----------------------------------------\nInstructions:\nIt's finally here... version 10! It has got 5 brand new levels! And more are coming soon... Use arrow keys to move and jump. Collect the coins. Jump upon Goomba enemies (twice!) to beat them. Here's a walkthrough: https://www.youtube.com/watch?v=ONtHRrN-cDQ\n----------------------------------------\nDescription:\nI'm trying to make a Super Mario Bros game. It's not gonna be a remake. I just use the sprites and sounds. In version 10 there is a new enemy: Bullet Bill! And.. there's also music now! Feel free to remix and build a level yourself! Future plans: - add power ups - other enemies - and more... Credits for the sprites and sounds are for Nintendo. I found them here: http://spriters-resource.com/nes/supermariobros/ and http://themushroomkingdom.net/media/smb/wav\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.7.2.\nOriginal Scratch project => https://scratch.mit.edu/projects/49905542", - "version": "0.9.27", - "views": 13814, - "download": 15864, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Music" - ], - "uploaded": 1479594310, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_18600.png?t=1598100992", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_18600.png?t=1598163708", - "project_url": "https://share.catrob.at/app/project/18600", - "download_url": "https://share.catrob.at/app/download/18600.catrobat", - "filesize": 5.398584365844727 - }, - { - "id": "27621", - "name": "CUSTOM Undertale: Asriel Mini Battle", - "author": "Nintendo", - "description": "\n----------------------------------------\nInstructions:\nDefeat Asriel Dreemurr! Use the arrow keys to dodge his attacks. Since Asriel is so powerful; if you get hit once, you die! WARNING! Very laggy on fullscreen! ANOTHER WARNING! May spoil the game!\n----------------------------------------\nDescription:\nThanks to @Werkaec's Undertale: Memories of the Underground V3 (Rememberance) game with some of the coding. The fantastic Undertale game was made by Toby Fox!\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.8.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/92196198\n\n\nThe Soul May Glitch out of the box", - "version": "0.9.28", - "views": 7922, - "download": 8112, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1490752176, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_27621.png?t=1598101050", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_27621.png?t=1598163727", - "project_url": "https://share.catrob.at/app/project/27621", - "download_url": "https://share.catrob.at/app/download/27621.catrobat", - "filesize": 5.910174369812012 - }, - { - "id": "32246", - "name": "One Night At Freddi FezzBurs Version 1.3.0", - "author": "KottaTheCat", - "description": "\n----------------------------------------\nInstructions:\nThe Official One Night At Freddi FezzBurs Game! Now On Gamejolt: http://gamejolt.com/games/one-night-at-freddi-fezzburs/88454 Update 1.3.0 What's New! -Added Time -Added Jumpscare -Added Freddi at The Door Game Made By Brosheb Crisp Inspired By Scott Cawthon's Five Nights At Freddy's More Updates Soon\n----------------------------------------\nDescription:\nNote: DO NOT REMIX! Game Made By Brosheb Crisp Inspired By Scott Cawthon's Five Nights At Freddy's Game Not Yet Done =P\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.8.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/73342440", - "version": "0.9.28", - "views": 4998, - "download": 5331, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Experimental" - ], - "uploaded": 1497372023, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_32246.png?t=1598101008", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_32246.png?t=1598163715", - "project_url": "https://share.catrob.at/app/project/32246", - "download_url": "https://share.catrob.at/app/download/32246.catrobat", - "filesize": 7.9396772384643555 - }, - { - "id": "41333", - "name": "CupHead Boss Fight", - "author": "FuriousFreddy", - "description": "\n----------------------------------------\nInstructions:\nARROW KEYS = MOVE SPACE BAR = SHOOT (Or hold for rapid fire) --------------------------------------------------------- ★ Full screen not recommended Press the green flag until it runs smoother (if necessary) Wally Warbles (from Cuphead) Boss fight\n----------------------------------------\nDescription:\nMusic: Cuphead - Aviary Action ★ This is possible to beat, many people defeated it Sorry, I couldn't do the entire fight, with the son, and the final phase. Credits: Thanks @20105118 for some of the scripts Thanks @Mistystone for the wing animation (I changed it a bit) Thanks @Kurt_Colwell for the explosion And can't forget about MDHR Studios!\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.9.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/178944041", - "version": "0.9.31", - "views": 4076, - "download": 3706, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1510504394, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_41333.png?t=1598101009", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_41333.png?t=1598163716", - "project_url": "https://share.catrob.at/app/project/41333", - "download_url": "https://share.catrob.at/app/download/41333.catrobat", - "filesize": 7.461623191833496 - }, - { - "id": "46885", - "name": "Youtubers life 0.1 ", - "author": "FurbyLisa", - "description": "\n----------------------------------------\nInstructions:\nclick the computer for shop, video, and video maker click bed to boost sleep click burger to boost hunger click door to work or move click your head to change outfit\n----------------------------------------\nDescription:\nSort of like the original Youtubers Life\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.9.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/129293842", - "version": "0.9.33", - "views": 3605, - "download": 3962, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1517076849, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_46885.png?t=1598100990", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_46885.png?t=1598163705", - "project_url": "https://share.catrob.at/app/project/46885", - "download_url": "https://share.catrob.at/app/download/46885.catrobat", - "filesize": 0.3914451599121094 - }, - { - "id": "00cb9482-3fac-11ea-8273-000c292a0f49", - "name": "Sonic 3 Special Stage Test", - "author": "Spinel (Steven Universe) ", - "description": "Hmmmmm", - "version": "0.9.67", - "views": 2738, - "download": 3114, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Experimental" - ], - "uploaded": 1579981848, - "uploaded_string": "1 year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_00cb9482-3fac-11ea-8273-000c292a0f49.png?t=1598101231", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_00cb9482-3fac-11ea-8273-000c292a0f49.png?t=1598163802", - "project_url": "https://share.catrob.at/app/project/00cb9482-3fac-11ea-8273-000c292a0f49", - "download_url": "https://share.catrob.at/app/download/00cb9482-3fac-11ea-8273-000c292a0f49.catrobat", - "filesize": 5.727170944213867 - }, - { - "id": "85485", - "name": "одна ночь с пародиями 5", - "author": "romanade", - "description": "зеленый парень в нутри аниматроника вы сможете прожить 1 ночь или нет?.", - "version": "0.9.56", - "views": 2488, - "download": 1962, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Story" - ], - "uploaded": 1555848213, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_85485.png?t=1598101149", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_85485.png?t=1598163748", - "project_url": "https://share.catrob.at/app/project/85485", - "download_url": "https://share.catrob.at/app/download/85485.catrobat", - "filesize": 8.277359962463379 - }, - { - "id": "112335", - "name": "Counter Strike 1.6 (Порт на Pocket Code)", - "author": "Нокат", - "description": "0.1 Beta", - "version": "0.9.64", - "views": 2113, - "download": 2549, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Story" - ], - "uploaded": 1572197322, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_112335.png?t=1598101175", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_112335.png?t=1598163759", - "project_url": "https://share.catrob.at/app/project/112335", - "download_url": "https://share.catrob.at/app/download/112335.catrobat", - "filesize": 0.9216365814208984 - }, - { - "id": "81606", - "name": "fnati simulator (android version) v1.0", - "author": "tasos9paok", - "description": "fnati Simulator v1.0", - "version": "0.9.54", - "views": 1989, - "download": 1801, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Art" - ], - "uploaded": 1552767790, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_81606.png?t=1598101056", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_81606.png?t=1598163728", - "project_url": "https://share.catrob.at/app/project/81606", - "download_url": "https://share.catrob.at/app/download/81606.catrobat", - "filesize": 20.946385383605957 - }, - { - "id": "85e6d6f3-4d0e-11ea-8d1e-000c292a0f49", - "name": "Sonic Mania Plus Mobile", - "author": "Sonic Channel ", - "description": "----------------------------------------\nInstructions:\nThis is in Beta! Move with the arrow keys! Down and space to spindash! ( Its very buggy!)\n----------------------------------------\nDescription:\nFeel free to use this WITH credit. Credit to @Shyguydude for the sprites!\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.9.1.\nOriginal Scratch project => https://scratch.mit.edu/projects/25103782", - "version": "0.9.68", - "views": 1946, - "download": 1856, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1581453535, - "uploaded_string": "1 year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_85e6d6f3-4d0e-11ea-8d1e-000c292a0f49.png?t=1598101249", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_85e6d6f3-4d0e-11ea-8d1e-000c292a0f49.png?t=1598163805", - "project_url": "https://share.catrob.at/app/project/85e6d6f3-4d0e-11ea-8d1e-000c292a0f49", - "download_url": "https://share.catrob.at/app/download/85e6d6f3-4d0e-11ea-8d1e-000c292a0f49.catrobat", - "filesize": 49.56886863708496 - }, - { - "id": "49251", - "name": "Five Nights At Mario's", - "author": "austin14", - "description": "\n----------------------------------------\nInstructions:\nScary Game In Years. How To Play:Do Not Waste Your Power. You Have Cameras 1A-7 Do Not Get Jumpscared By:Mario. Luigi. Peach. Yoshi. And Golden Mario. And Survive From 12 am to 6 am. Stay Safe. Check Lights. Close A Door If You Need To. It Takes 70 Secs For A Next Hour. The Camera Change Sound Is The Same Sound With Five Nights At Freddy's Camera Sound. You Have 7 Nights To Survive. Five Nights At Mario's 2 Coming Soon.\n----------------------------------------\nDescription:\nNone\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.9.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/92798731", - "version": "0.9.33", - "views": 1908, - "download": 1926, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1520217921, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_49251.png?t=1598101029", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_49251.png?t=1598163721", - "project_url": "https://share.catrob.at/app/project/49251", - "download_url": "https://share.catrob.at/app/download/49251.catrobat", - "filesize": 54.85420036315918 - }, - { - "id": "56286", - "name": "谷歌中国", - "author": "FurbyLisa", - "description": "\n----------------------------------------\nInstructions:\n点击google徽标下方的搜索栏,然后输入... -Skype -刮 -图片 -youtube 如果你不输入其中的一个,它会说,没有结果! Diǎnjī google huībiāo xiàfāng de sōusuǒ lán, ránhòu shūrù... -Skype -guā -túpiàn -youtube rúguǒ nǐ bù shūrù qízhōng de yīgè, tā huì shuō, méiyǒu jiéguǒ! 由于谷歌在中国的审查制度,我决定为了纪念谷歌中国而成立这个项目。\n----------------------------------------\nDescription:\nTo do the china parts,i had to transtlle them with google.\n----------------------------------------\n\n\n Desktop Version => https://scratch.mit.edu/projects/226034878", - "version": "0.9.33", - "views": 1853, - "download": 1555, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1527640683, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_56286.png?t=1598100997", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_56286.png?t=1598163710", - "project_url": "https://share.catrob.at/app/project/56286", - "download_url": "https://share.catrob.at/app/download/56286.catrobat", - "filesize": 0.7806711196899414 - }, - { - "id": "30197", - "name": "Need For Speed Ultra City: Car Select", - "author": "shai", - "description": "\n----------------------------------------\nInstructions:\nFull Title: (FAKE) Need For Speed Ultra City: Car Select Platforms: Playstation 4, Xbox One, Playstation Vita, Nintendo 3DS and Nintendo Switch\n----------------------------------------\nDescription:\nCredits to all NOTE: This game is fake. So don't report!\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.8.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/147913114", - "version": "0.9.28", - "views": 1532, - "download": 1857, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1494702412, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_30197.png?t=1598101075", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_30197.png?t=1598163731", - "project_url": "https://share.catrob.at/app/project/30197", - "download_url": "https://share.catrob.at/app/download/30197.catrobat", - "filesize": 6.56934928894043 - }, - { - "id": "67811", - "name": "Sonic The Hedgehog™ 4", - "author": "FurbyLisa", - "description": "----------------------------------------\nInstructions:\nPress space to show Sonic or jump and press the arrow keys to move.\n----------------------------------------\nDescription:\n:P\n---------------------------------------", - "version": "0.9.40", - "views": 1457, - "download": 1603, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1539810724, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_67811.png?t=1598101079", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_67811.png?t=1598163732", - "project_url": "https://share.catrob.at/app/project/67811", - "download_url": "https://share.catrob.at/app/download/67811.catrobat", - "filesize": 6.96649169921875 - }, - { - "id": "112352", - "name": "Tiny World Generator", - "author": "PIXcY", - "description": "only another random pocketworld-generator", - "version": "0.9.64", - "views": 1198, - "download": 1062, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Animation", - "Art", - "Experimental" - ], - "uploaded": 1572507068, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_112352.png?t=1598100985", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_112352.png?t=1598163698", - "project_url": "https://share.catrob.at/app/project/112352", - "download_url": "https://share.catrob.at/app/download/112352.catrobat", - "filesize": 0.0979766845703125 - }, - { - "id": "1808", - "name": "DJ Scratch Cat remix", - "author": "chwt", - "description": "Made with ScratchToCatrobat Converter version 2014.08.05. Original Scratch project => http://scratch.mit.edu/projects/17828107", - "version": "0.9.9", - "views": 1173, - "download": 942, - "private": false, - "flavor": "pocketcode", - "tags": [], - "uploaded": 1407474809, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_1808.png?t=1598100992", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_1808.png?t=1598163707", - "project_url": "https://share.catrob.at/app/project/1808", - "download_url": "https://share.catrob.at/app/download/1808.catrobat", - "filesize": 0.3998870849609375 - }, - { - "id": "30240", - "name": "Terraria Sprites", - "author": "SansCucumber2", - "description": "\nTerraria Sptires", - "version": "0.9.28", - "views": 1047, - "download": 748, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game", - "Animation", - "Art" - ], - "uploaded": 1494760847, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_30240.png?t=1598101066", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_30240.png?t=1598163729", - "project_url": "https://share.catrob.at/app/project/30240", - "download_url": "https://share.catrob.at/app/download/30240.catrobat", - "filesize": 71.6375150680542 - }, - { - "id": "44573", - "name": "Five Nights at Tankloop's", - "author": "FurbyLisa", - "description": "Credit to @baty on deavinet for the Batman bendy", - "version": "0.9.33", - "views": 839, - "download": 901, - "private": false, - "flavor": "pocketcode", - "tags": [ - "Game" - ], - "uploaded": 1514241510, - "uploaded_string": "more than one year ago", - "screenshot_large": "https://share.catrob.at/resources/screenshots/screen_44573.png?t=1598101072", - "screenshot_small": "https://share.catrob.at/resources/thumbnails/screen_44573.png?t=1598163730", - "project_url": "https://share.catrob.at/app/project/44573", - "download_url": "https://share.catrob.at/app/download/44573.catrobat", - "filesize": 55.191046714782715 - } - ] - } -] \ No newline at end of file +{ + "data": [ + { + "type": "recent", + "name": "Newest projects", + "projects_list": [ + { + "id": "312f3f37-d07e-11eb-ae11-005056a36f47", + "name": "Zombie Attack (3D effect) by PrydeYT", + "author": "Matt3389", + "description": "Eu apenas fiz algumas mudan\u00e7as,CR\u00c9DITOS para PrydeYT!", + "version": "1.0.3", + "views": 3, + "downloads": 2, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T21:43:17+00:00", + "uploaded_string": "7 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_312f3f37-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_312f3f37-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_312f3f37-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_312f3f37-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_312f3f37-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_312f3f37-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/312f3f37-d07e-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/312f3f37-d07e-11eb-ae11-005056a36f47/catrobat", + "filesize": 1.2048263549804688 + }, + { + "id": "0f41ac95-d07e-11eb-ae11-005056a36f47", + "name": "Frinday night funkin mod Jos\u00e9 gamer 7.777", + "author": "Josgamer7.777 ", + "description": "", + "version": "1.0.3", + "views": 6, + "downloads": 4, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T21:42:20+00:00", + "uploaded_string": "8 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_0f41ac95-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_0f41ac95-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_0f41ac95-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_0f41ac95-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_0f41ac95-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_0f41ac95-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/0f41ac95-d07e-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/0f41ac95-d07e-11eb-ae11-005056a36f47/catrobat", + "filesize": 11.018264770507812 + }, + { + "id": "e10a5d5a-d07d-11eb-ae11-005056a36f47", + "name": "Geometria de bola contra blocos", + "author": "Dreafou Games BR", + "description": "Jogo com fisica de bola sob gravidade e reflex\u00e3o contra objetos geometricos. Ainda em desenvolvimento", + "version": "1.0.3", + "views": 3, + "downloads": 0, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "experimental": "Experimental", + "tutorial": "Tutorial" + }, + "uploaded_at": "2021-06-18T21:41:02+00:00", + "uploaded_string": "10 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_e10a5d5a-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_e10a5d5a-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_e10a5d5a-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_e10a5d5a-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_e10a5d5a-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_e10a5d5a-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/e10a5d5a-d07d-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/e10a5d5a-d07d-11eb-ae11-005056a36f47/catrobat", + "filesize": 4.98129940032959 + }, + { + "id": "fa75f950-d07c-11eb-ae11-005056a36f47", + "name": "fnafnico", + "author": "Nicolas da jogos", + "description": "UCN FNAF", + "version": "1.0.3", + "views": 6, + "downloads": 4, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T21:34:36+00:00", + "uploaded_string": "16 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_fa75f950-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_fa75f950-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_fa75f950-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_fa75f950-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_fa75f950-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_fa75f950-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/fa75f950-d07c-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/fa75f950-d07c-11eb-ae11-005056a36f47/catrobat", + "filesize": 34.17116165161133 + }, + { + "id": "5299ec47-d07c-11eb-ae11-005056a36f47", + "name": "windows 10", + "author": "SamuelPlayGamedav (\u0164M)", + "description": "1.0", + "version": "1.0.3", + "views": 5, + "downloads": 5, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2021-06-18T21:29:54+00:00", + "uploaded_string": "21 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5299ec47-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5299ec47-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5299ec47-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5299ec47-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5299ec47-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5299ec47-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/5299ec47-d07c-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/5299ec47-d07c-11eb-ae11-005056a36f47/catrobat", + "filesize": 1.0948543548583984 + }, + { + "id": "8afd9a2b-d07a-11eb-ae11-005056a36f47", + "name": "parkour", + "author": "Nicolas da jogos", + "description": "", + "version": "1.0.3", + "views": 5, + "downloads": 4, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "art": "Art", + "experimental": "Experimental" + }, + "uploaded_at": "2021-06-18T21:17:10+00:00", + "uploaded_string": "34 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_8afd9a2b-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_8afd9a2b-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_8afd9a2b-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_8afd9a2b-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_8afd9a2b-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_8afd9a2b-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/8afd9a2b-d07a-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/8afd9a2b-d07a-11eb-ae11-005056a36f47/catrobat", + "filesize": 30.92966651916504 + }, + { + "id": "a3e78f1f-d079-11eb-ae11-005056a36f47", + "name": "jh", + "author": "Oukseto", + "description": "hhh", + "version": "1.0.3", + "views": 5, + "downloads": 4, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2021-06-18T21:10:42+00:00", + "uploaded_string": "40 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_a3e78f1f-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_a3e78f1f-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_a3e78f1f-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_a3e78f1f-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_a3e78f1f-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_a3e78f1f-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/a3e78f1f-d079-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/a3e78f1f-d079-11eb-ae11-005056a36f47/catrobat", + "filesize": 0.42807483673095703 + }, + { + "id": "b57d4885-d078-11eb-ae11-005056a36f47", + "name": "CoinsMachine", + "author": "SM35020", + "description": "Number Randomizer(Coins Machine)", + "version": "1.0.3", + "views": 12, + "downloads": 0, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T21:04:02+00:00", + "uploaded_string": "47 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_b57d4885-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_b57d4885-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_b57d4885-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_b57d4885-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_b57d4885-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_b57d4885-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/b57d4885-d078-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/b57d4885-d078-11eb-ae11-005056a36f47/catrobat", + "filesize": 0.09321308135986328 + }, + { + "id": "51a24562-d078-11eb-ae11-005056a36f47", + "name": "Mit", + "author": "Thiago appp", + "description": "ajude n\u00f3s", + "version": "1.0.3", + "views": 9, + "downloads": 3, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation" + }, + "uploaded_at": "2021-06-18T21:03:53+00:00", + "uploaded_string": "47 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_51a24562-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_51a24562-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_51a24562-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_51a24562-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_51a24562-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_51a24562-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/51a24562-d078-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/51a24562-d078-11eb-ae11-005056a36f47/catrobat", + "filesize": 0.30908966064453125 + }, + { + "id": "8c0bd1d5-d078-11eb-ae11-005056a36f47", + "name": "tiles!", + "author": "Nicolas da jogos", + "description": "", + "version": "1.0.3", + "views": 10, + "downloads": 14, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "art": "Art", + "experimental": "Experimental" + }, + "uploaded_at": "2021-06-18T21:02:52+00:00", + "uploaded_string": "48 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_8c0bd1d5-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_8c0bd1d5-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_8c0bd1d5-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_8c0bd1d5-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_8c0bd1d5-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_8c0bd1d5-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/8c0bd1d5-d078-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/8c0bd1d5-d078-11eb-ae11-005056a36f47/catrobat", + "filesize": 0.7816238403320312 + }, + { + "id": "5c65637a-d078-11eb-ae11-005056a36f47", + "name": "tiles", + "author": "Nicolas da jogos", + "description": "", + "version": "1.0.3", + "views": 4, + "downloads": 0, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T21:01:32+00:00", + "uploaded_string": "49 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5c65637a-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5c65637a-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5c65637a-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5c65637a-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5c65637a-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5c65637a-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/5c65637a-d078-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/5c65637a-d078-11eb-ae11-005056a36f47/catrobat", + "filesize": 1.4275531768798828 + }, + { + "id": "739311dc-d077-11eb-ae11-005056a36f47", + "name": "legends of universe", + "author": "agente Gabriel", + "description": "beta 0.1", + "version": "1.0.3", + "views": 10, + "downloads": 6, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "experimental": "Experimental" + }, + "uploaded_at": "2021-06-18T20:55:02+00:00", + "uploaded_string": "56 minutes ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_739311dc-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_739311dc-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_739311dc-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_739311dc-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_739311dc-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_739311dc-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/739311dc-d077-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/739311dc-d077-11eb-ae11-005056a36f47/catrobat", + "filesize": 17.63987922668457 + }, + { + "id": "86dcf517-d076-11eb-ae11-005056a36f47", + "name": "Friday Night Funkin DEMO v0.02", + "author": "SamuelPlayGamedav (\u0164M)", + "description": "", + "version": "1.0.3", + "views": 30, + "downloads": 32, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2021-06-18T20:48:25+00:00", + "uploaded_string": "1 hour ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_86dcf517-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_86dcf517-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_86dcf517-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_86dcf517-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_86dcf517-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_86dcf517-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/86dcf517-d076-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/86dcf517-d076-11eb-ae11-005056a36f47/catrobat", + "filesize": 13.40373706817627 + }, + { + "id": "cb181513-d075-11eb-ae11-005056a36f47", + "name": "One Night At G7 Demo:1.8", + "author": "Petherson", + "description": "COME\u00c7A,EXTRAS,MODOHARD,HORAS,ANIMATRONICS,SELIVRADOSANIMATRONICS,ETC,", + "version": "1.0.3", + "views": 9, + "downloads": 9, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T20:43:10+00:00", + "uploaded_string": "1 hour ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_cb181513-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_cb181513-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_cb181513-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_cb181513-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_cb181513-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_cb181513-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/cb181513-d075-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/cb181513-d075-11eb-ae11-005056a36f47/catrobat", + "filesize": 2.023141860961914 + }, + { + "id": "3425d023-92a2-11eb-a92d-005056a36f47", + "name": "Among Us", + "author": "Paulinhocastrogame@gmail.com", + "description": "Junte-se a seus companheiros de equipe em um jogo multiplayer de trabalho em equipe e trai\u00e7\u00e3o!\n\u2606\u2606\u2606\u2606\u2606 novas coisas seram adicionadas ao jogos em cada vers\u00f5es", + "version": "1.0.3", + "views": 171, + "downloads": 210, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T20:40:29+00:00", + "uploaded_string": "1 hour ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_3425d023-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_3425d023-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_3425d023-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_3425d023-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_3425d023-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_3425d023-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/3425d023-92a2-11eb-a92d-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/3425d023-92a2-11eb-a92d-005056a36f47/catrobat", + "filesize": 17.401137351989746 + }, + { + "id": "ddb90ddc-c711-11eb-abc2-005056a36f47", + "name": "big boll bola na rede FC\u00ba Ruan", + "author": "Ruan003", + "description": "FC RUAN", + "version": "1.0.3", + "views": 8, + "downloads": 12, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T20:37:39+00:00", + "uploaded_string": "1 hour ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_ddb90ddc-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_ddb90ddc-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_ddb90ddc-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_ddb90ddc-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_ddb90ddc-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_ddb90ddc-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/ddb90ddc-c711-11eb-abc2-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/ddb90ddc-c711-11eb-abc2-005056a36f47/catrobat", + "filesize": 0.4646759033203125 + }, + { + "id": "40c2f65a-b056-11eb-abc2-005056a36f47", + "name": "Lazer wars BETA", + "author": "Vitamot studio 0fficial", + "description": "", + "version": "1.0.3", + "views": 179, + "downloads": 159, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2021-06-18T20:37:29+00:00", + "uploaded_string": "1 hour ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_40c2f65a-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_40c2f65a-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_40c2f65a-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_40c2f65a-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_40c2f65a-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_40c2f65a-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/40c2f65a-b056-11eb-abc2-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/40c2f65a-b056-11eb-abc2-005056a36f47/catrobat", + "filesize": 4.236872673034668 + }, + { + "id": "f9e43c4d-d074-11eb-ae11-005056a36f47", + "name": "BATALHAS E CHEF\u00d5ES GAMERTY ATUALIZA\u00c7\u00c3O", + "author": "studiogamet123", + "description": "GAMERTY ATUALIZA\u00c7\u00c3O!", + "version": "1.0.3", + "views": 8, + "downloads": 2, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "story": "Story" + }, + "uploaded_at": "2021-06-18T20:37:19+00:00", + "uploaded_string": "1 hour ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_f9e43c4d-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_f9e43c4d-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_f9e43c4d-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_f9e43c4d-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_f9e43c4d-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_f9e43c4d-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/f9e43c4d-d074-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/f9e43c4d-d074-11eb-ae11-005056a36f47/catrobat", + "filesize": 23.095444679260254 + }, + { + "id": "4a335e6e-cb66-11eb-99b1-005056a36f47", + "name": "One Night In Among Us", + "author": "Rekky_yt", + "description": "yo c'est rekky la team des fran\u00e7ais!", + "version": "1.0.3", + "views": 57, + "downloads": 61, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "experimental": "Experimental" + }, + "uploaded_at": "2021-06-18T20:27:17+00:00", + "uploaded_string": "1 hour ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_4a335e6e-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_4a335e6e-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_4a335e6e-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_4a335e6e-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_4a335e6e-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_4a335e6e-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/4a335e6e-cb66-11eb-99b1-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/4a335e6e-cb66-11eb-99b1-005056a36f47/catrobat", + "filesize": 12.279342651367188 + }, + { + "id": "12e40e09-cfc1-11eb-ae11-005056a36f47", + "name": "Castle escape", + "author": "Switzeer", + "description": "Escape do castelo! Game in demo", + "version": "1.0.3", + "views": 10, + "downloads": 11, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "experimental": "Experimental" + }, + "uploaded_at": "2021-06-18T20:25:21+00:00", + "uploaded_string": "1 hour ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_12e40e09-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_12e40e09-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_12e40e09-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_12e40e09-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_12e40e09-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_12e40e09-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/12e40e09-cfc1-11eb-ae11-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/12e40e09-cfc1-11eb-ae11-005056a36f47/catrobat", + "filesize": 0.36594486236572266 + } + ] + }, + { + "type": "most_viewed", + "name": "Most viewed", + "projects_list": [ + { + "id": "5680", + "name": "Minecraft", + "author": "XxJHKanalxX", + "description": ">New Minecraft Update!\n\n >Version 3.0\n +Fix Bugs\n\n >Version 3.1\n +New Color\n +New Skin\n +Add Musik on \"Audio\"\n\n >Version 3.2\n +Fix Bugs\n\n>Check my Social Media\n \n *Instagram:XxJHKanalxX\n *YouTube: XxJHKanalxX\n\nVersion 3.2\n\n#GalaxyGameJam", + "version": "0.9.31", + "views": 232617, + "downloads": 222807, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "experimental": "Experimental" + }, + "uploaded_at": "2017-11-16T15:36:10+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5680-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5680-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5680-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5680-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5680-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5680-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/5680", + "download_url": "https://share.catrobat.org/api/projects/5680/catrobat", + "filesize": 67.80170440673828 + }, + { + "id": "817", + "name": "Tic-Tac-Toe Master", + "author": "hej-wickie-hej", + "description": "Three in one win. However, the computer is a master strategist! \n\nThe text was composed using PicSay - http://catrob.at/PicSay as explained on http://catrob.at/WordBalloonExplanation\n\n#GalaxyGameJam", + "version": "0.9.6", + "views": 107733, + "downloads": 121606, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2013-04-22T08:30:31+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_817-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_817-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_817-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_817-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_817-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_817-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/817", + "download_url": "https://share.catrobat.org/api/projects/817/catrobat", + "filesize": 0.2409677505493164 + }, + { + "id": "37890", + "name": "Sonic Mania Android Version 0.2", + "author": "KyleOfBlades", + "description": "Update of MY port,I've been seeing ALOT of remixes of my port so chill with em,don't completely copy my port.\n\nChangelog:Level Select,Intro,Studiopolis,and finally Auto Save Screen", + "version": "0.9.29", + "views": 79777, + "downloads": 59988, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-09-20T00:19:48+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_37890-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_37890-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_37890-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_37890-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_37890-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_37890-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/37890", + "download_url": "https://share.catrobat.org/api/projects/37890/catrobat", + "filesize": 70.83020114898682 + }, + { + "id": "719", + "name": "Galaxy War", + "author": "DavidR", + "description": "Control the ship by tilting the phone and shoot down as many enemies as possible.", + "version": "0.7.0beta", + "views": 72175, + "downloads": 81990, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2013-04-05T08:41:48+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_719-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_719-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_719-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_719-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_719-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_719-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/719", + "download_url": "https://share.catrobat.org/api/projects/719/catrobat", + "filesize": 3.0423545837402344 + }, + { + "id": "1581", + "name": "Flappy_v3.0", + "author": "mj7007", + "description": "A simple game similar to the popular Flappy Bird, completely made using Pocket Code. Game not tested on multiple devices yet, which might lead to minor issues. Please let me know about any issues and always welcome to remix this game. ", + "version": "0.9.", + "views": 65664, + "downloads": 78953, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2014-03-16T10:31:23+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1581-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1581-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1581-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1581-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1581-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1581-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/1581", + "download_url": "https://share.catrobat.org/api/projects/1581/catrobat", + "filesize": 0.10238456726074219 + }, + { + "id": "45868", + "name": "Iso-motion", + "author": "headhunter", + "description": "To play this game, cast your screen to a TV or a PC and use the device as a motion controller to move your character.\n***********************\nMade for GCI task \nAny and All assets used in the program are made by me and are without any previous copyrights, feel free to use them in your programs.\nCalibration code taken from task- https://share.catrob.at/pocketcode/program/38660\n************************\nUpdate 2\n- added death counter\nUpdate 1\n- changed Level 1 orb position.\n- More levels coming soon...", + "version": "0.9.33", + "views": 44948, + "downloads": 44009, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "experimental": "Experimental" + }, + "uploaded_at": "2018-01-13T10:00:10+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45868-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45868-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45868-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45868-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45868-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45868-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/45868", + "download_url": "https://share.catrobat.org/api/projects/45868/catrobat", + "filesize": 30.878884315490723 + }, + { + "id": "20065", + "name": "Geometry Dash v1.5", + "author": "VenomX", + "description": "\nMade with Scratch2Catrobat Converter version 0.7.2.\nOriginal Scratch project => https://scratch.mit.edu/projects/105500895", + "version": "0.9.27", + "views": 44238, + "downloads": 39256, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "music": "Music" + }, + "uploaded_at": "2016-12-10T16:03:44+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/20065", + "download_url": "https://share.catrobat.org/api/projects/20065/catrobat", + "filesize": 16.550339698791504 + }, + { + "id": "43252", + "name": "Fnaf Pizzeria Simulator", + "author": "KyleOfBlades", + "description": "My Port of Fnaf 6! Enjoy! Please subscribe! My Channel is Jack \u00d8f Blades", + "version": "0.9.32", + "views": 42732, + "downloads": 38356, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-12-10T10:26:33+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_43252-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_43252-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_43252-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_43252-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_43252-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_43252-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/43252", + "download_url": "https://share.catrobat.org/api/projects/43252/catrobat", + "filesize": 12.229867935180664 + }, + { + "id": "46b43efe-fb9e-11ea-a92d-005056a36f47", + "name": "Pocket Moon v1.0", + "author": "Jude Birch", + "description": "Farming game made for Google Summer of Code.\n\nPlay as the owner of a farm and grow different crops over the seasons. Sell crops to buy more seeds and tools at the shop, and unlock more areas! You can also explore the world and discover the 21 different fish in the game!\n\nGoogle Summer of Code Page: https://summerofcode.withgoogle.com/projects/5782435888889856\nGameplay: https://youtu.be/oMPMTiVN1Vs\n\nThanks to Jordan Smith for mentoring and supporting the project.", + "version": "0.9.74", + "views": 39021, + "downloads": 28992, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2020-10-28T10:38:33+00:00", + "uploaded_string": "8 months ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/46b43efe-fb9e-11ea-a92d-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/46b43efe-fb9e-11ea-a92d-005056a36f47/catrobat", + "filesize": 4.47955322265625 + }, + { + "id": "42921", + "name": "Math Quest! v2", + "author": "Jude Birch", + "description": "Here's an educational binary themed RPG for Google code-in!", + "version": "0.9.32", + "views": 33784, + "downloads": 25156, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-12-05T21:53:52+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_42921-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_42921-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_42921-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_42921-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_42921-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_42921-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/42921", + "download_url": "https://share.catrobat.org/api/projects/42921/catrobat", + "filesize": 36.889180183410645 + }, + { + "id": "83613", + "name": "R-Type", + "author": "Stroy562", + "description": "\u0423\u043f\u0440\u0430\u0432\u043b\u044f\u0439 \u043a\u043e\u0441\u043c\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u0430\u043f\u043f\u0430\u0440\u0430\u0442\u043e\u043c.", + "version": "0.9.70", + "views": 31095, + "downloads": 3019, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2020-06-21T20:08:07+00:00", + "uploaded_string": "1 year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_83613-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_83613-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_83613-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_83613-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_83613-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_83613-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/83613", + "download_url": "https://share.catrobat.org/api/projects/83613/catrobat", + "filesize": 62.580196380615234 + }, + { + "id": "1450", + "name": "Minions ", + "author": "kestapa", + "description": "", + "version": "0.9.4", + "views": 28599, + "downloads": 32713, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2013-10-30T04:23:59+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1450-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1450-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1450-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1450-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1450-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1450-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/1450", + "download_url": "https://share.catrobat.org/api/projects/1450/catrobat", + "filesize": 0.9740276336669922 + }, + { + "id": "49860", + "name": "Undertale: rood fight", + "author": "Powerpoop", + "description": "No", + "version": "0.9.33", + "views": 27617, + "downloads": 28961, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "story": "Story" + }, + "uploaded_at": "2018-03-13T01:27:19+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/49860", + "download_url": "https://share.catrobat.org/api/projects/49860/catrobat", + "filesize": 15.183897018432617 + }, + { + "id": "965", + "name": "Air fight 0.5", + "author": "hej-wickie-hej", + "description": "Shoot down your opponent! Steer with your device's inclination. Shoot by tapping or holding the screen. \r\n\r\nUpdate 2016-11-2: now with object grouping, new Catrobat language version 0.992 bricks, as well as unlimited bullets (through clone bricks).", + "version": "0.9.27", + "views": 27399, + "downloads": 30626, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2016-11-02T16:43:10+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_965-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_965-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_965-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_965-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_965-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_965-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/965", + "download_url": "https://share.catrobat.org/api/projects/965/catrobat", + "filesize": 5.546684265136719 + }, + { + "id": "72798", + "name": "CS:GO", + "author": "GameCompani7", + "description": "\u0414\u0430\u0442\u0430 \u0440\u0435\u043b\u0438\u0437\u0430! (12.31.18!) \n\n\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435! \n-\u041a\u0440\u0443\u0442\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435! \n- \u041e\u0431\u043d\u043e\u0432\u0438\u043b \u043e\u0440\u0443\u0436\u0438\u044f \u0442\u0435\u043f\u0435\u0440\u044c \u043a\u0440\u0443\u0442\u044b\u0435! \n-\u0414\u043e\u0431\u0430\u0432\u0438\u043b \u041c\u0443\u0437\u044b\u043a\u0443 \u0432 \u043c\u0435\u043d\u044e! \n-\u041f\u043e\u043c\u0435\u043d\u044f\u043b \u043a\u0430\u0440\u0442\u044b \u0441\u0434\u0435\u043b\u0430\u043b \u043a\u0440\u0430\u0441\u0438\u0432\u044b\u043c\u0438\n\u0438 3D \u0432\u0441\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u043b! \n-\u0418\u0437\u043c\u0435\u043d\u0438\u043b \u0424\u043e\u043d \u041c\u0435\u043d\u044e! \n-\u0414\u043e\u0431\u0430\u0432\u0438\u043b \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043a\u0430\u0440\u0442\u044b! \n\n\nCSGO. \u041e\u0442 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 GameKompani7\n\n\u0412\u044b \u0438\u0433\u0440\u0430\u0435\u0442\u0435 \u0412 \u0440\u043e\u043b\u0438 \u0421\u043f\u0435\u0446\u043d\u0430\u0437\u0430 \u0438\u043b\u0438 \u0436\u0435 \u0422\u0435\u0440\u043e\u0440\u0438\u0441\u0442\u0430! \n\u0423\u0431\u0435\u0439\u0442\u0435 \u0432\u0441\u0435\u0445 \u0437\u0430 40-45 \u0441\u0435\u043a\u0443\u043d\u0434! \n\u041c\u043d\u043e\u0433\u043e \u043e\u0440\u0443\u0436\u0438\u0435 \u0441\u043a\u0438\u043d\u044b \u043a\u0435\u0439\u0441\u044b 4 \u043a\u0430\u0440\u0442\u044b! \n(Dast inferno Nyke office) \n\u0414\u043e\u0441\u0442\u0438\u0433\u043d\u0438 \u0413\u043b\u043e\u0431\u0430\u043b\u0430! \n\u0418 \u0441\u0442\u0430\u043d\u044c \u043b\u0443\u0447\u0448\u0438\u043c! \n \u041e\u0446\u0435\u043d\u0438\u0442\u0435 \u043d\u0430\u0441! \n\u0412\u0438\u0434\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0431\u044b\u043b\u0430 \u0434\u043e\u043b\u0433\u043e\u0439 \u0438 \u0442\u0440\u0443\u0434\u043d\u043e\u0439! \n\u041f\u0435\u0440\u0435\u0437\u0430\u0440\u044f\u0434\u043a\u0430 \u0432\u0441\u0451 \u0442\u0443\u0442 \u0435\u0441\u0442\u044c \u043d\u0443 \u043f\u043e\u0447\u0442\u0438! \n\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0447\u0435\u0440\u0435\u0437 7 \u0434\u043d\u0435\u0439 \u0431\u0443\u0434\u0438\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0442\u0430\u043a \u0447\u0442\u043e\n\u0416\u0434\u0438\u0442\u0435! \n\u041f\u0440\u0430\u0432\u0430/ GameKompani7\u00ae", + "version": "0.9.44", + "views": 22475, + "downloads": 56214, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2019-01-07T17:16:32+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_72798-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_72798-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_72798-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_72798-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_72798-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_72798-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/72798", + "download_url": "https://share.catrobat.org/api/projects/72798/catrobat", + "filesize": 19.330181121826172 + }, + { + "id": "32577", + "name": "Air Hockey", + "author": "Kingsimon", + "description": "Air Hockey", + "version": "0.9.74", + "views": 20334, + "downloads": 23830, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "music": "Music", + "experimental": "Experimental" + }, + "uploaded_at": "2020-10-13T16:12:58+00:00", + "uploaded_string": "8 months ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32577-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32577-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32577-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32577-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32577-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32577-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/32577", + "download_url": "https://share.catrobat.org/api/projects/32577/catrobat", + "filesize": 23.37629508972168 + }, + { + "id": "44987", + "name": "Slendytubbies 2D Part 2", + "author": "Felix Filip", + "description": "V1.3 (Last Update) 12.02.18\n\nOBJECTIVE:\nyou need to collect all 5 - 25 Custards while trying to avoid the Monsters\n\nThis game is proven to be the best and scariest game on Pocket Code, If you don't believe me just play the game\n\n- 14 Maps with different Monsters\n\n- Choose Between an Flashlight or an Night Vision Camera\n\n- Animations\n\n- All maps are Large\n\n- You can change the Custard level from 5 to 25 Custards\n\n-There's an Energy bar for the Flashlight and the Night Vision Camera so be careful and \u00a0don't use it to much\n\nYouTube: Felix Filip\nGame by: Felix Filip\nTwitter: @FelixFilipYT\nRoblox: FelixFilipYT\nMCPE: Felix Filip AC", + "version": "0.9.33", + "views": 20125, + "downloads": 24661, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2018-02-12T23:00:24+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44987-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44987-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44987-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44987-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44987-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44987-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/44987", + "download_url": "https://share.catrobat.org/api/projects/44987/catrobat", + "filesize": 47.748464584350586 + }, + { + "id": "58961", + "name": "ultimate custon night fnaf6", + "author": "joelio", + "description": "UCN FNAF", + "version": "0.9.33", + "views": 19557, + "downloads": 10021, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2018-07-02T15:09:35+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_58961-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_58961-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_58961-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_58961-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_58961-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_58961-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/58961", + "download_url": "https://share.catrobat.org/api/projects/58961/catrobat", + "filesize": 26.86823558807373 + }, + { + "id": "1719", + "name": "Don't touch the white tiles!", + "author": "hej-wickie-hej", + "description": "You have 25 seconds to tap as many of the black tiles as possible, starting from the bottom. Of course, don't touch the white ones!\n\nNote that, although you must tap the black tiles one after the other starting from the lower ones, it is nevertheless possible to tap the next tiles already while they are still moving. Doing so gives exponentially more points, depending on how high the tiles were on the screen when you tapped them. \n\nRemark: All numbers and other text in images have been made with PicSay, available on Google Play, see http://catrob.at/PicSay and http://catrob.at/WordBalloonExplanation ", + "version": "0.9.9", + "views": 17981, + "downloads": 22867, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2014-07-06T21:34:15+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1719-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1719-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1719-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1719-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1719-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1719-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/1719", + "download_url": "https://share.catrobat.org/api/projects/1719/catrobat", + "filesize": 0.1950674057006836 + }, + { + "id": "45034", + "name": "MasterMind Game ", + "author": "Aditya R Nair", + "description": "Mastermind created in PocketCode.", + "version": "0.9.33", + "views": 16771, + "downloads": 8012, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2018-01-01T23:59:30+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45034-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45034-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45034-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45034-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45034-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45034-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/45034", + "download_url": "https://share.catrobat.org/api/projects/45034/catrobat", + "filesize": 8.764063835144043 + } + ] + }, + { + "type": "most_downloaded", + "name": "Most downloaded", + "projects_list": [ + { + "id": "5680", + "name": "Minecraft", + "author": "XxJHKanalxX", + "description": ">New Minecraft Update!\n\n >Version 3.0\n +Fix Bugs\n\n >Version 3.1\n +New Color\n +New Skin\n +Add Musik on \"Audio\"\n\n >Version 3.2\n +Fix Bugs\n\n>Check my Social Media\n \n *Instagram:XxJHKanalxX\n *YouTube: XxJHKanalxX\n\nVersion 3.2\n\n#GalaxyGameJam", + "version": "0.9.31", + "views": 232617, + "downloads": 222807, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "experimental": "Experimental" + }, + "uploaded_at": "2017-11-16T15:36:10+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5680-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5680-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5680-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5680-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_5680-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_5680-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/5680", + "download_url": "https://share.catrobat.org/api/projects/5680/catrobat", + "filesize": 67.80170440673828 + }, + { + "id": "817", + "name": "Tic-Tac-Toe Master", + "author": "hej-wickie-hej", + "description": "Three in one win. However, the computer is a master strategist! \n\nThe text was composed using PicSay - http://catrob.at/PicSay as explained on http://catrob.at/WordBalloonExplanation\n\n#GalaxyGameJam", + "version": "0.9.6", + "views": 107733, + "downloads": 121606, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2013-04-22T08:30:31+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_817-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_817-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_817-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_817-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_817-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_817-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/817", + "download_url": "https://share.catrobat.org/api/projects/817/catrobat", + "filesize": 0.2409677505493164 + }, + { + "id": "719", + "name": "Galaxy War", + "author": "DavidR", + "description": "Control the ship by tilting the phone and shoot down as many enemies as possible.", + "version": "0.7.0beta", + "views": 72175, + "downloads": 81990, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2013-04-05T08:41:48+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_719-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_719-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_719-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_719-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_719-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_719-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/719", + "download_url": "https://share.catrobat.org/api/projects/719/catrobat", + "filesize": 3.0423545837402344 + }, + { + "id": "1581", + "name": "Flappy_v3.0", + "author": "mj7007", + "description": "A simple game similar to the popular Flappy Bird, completely made using Pocket Code. Game not tested on multiple devices yet, which might lead to minor issues. Please let me know about any issues and always welcome to remix this game. ", + "version": "0.9.", + "views": 65664, + "downloads": 78953, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2014-03-16T10:31:23+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1581-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1581-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1581-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1581-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1581-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1581-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/1581", + "download_url": "https://share.catrobat.org/api/projects/1581/catrobat", + "filesize": 0.10238456726074219 + }, + { + "id": "37890", + "name": "Sonic Mania Android Version 0.2", + "author": "KyleOfBlades", + "description": "Update of MY port,I've been seeing ALOT of remixes of my port so chill with em,don't completely copy my port.\n\nChangelog:Level Select,Intro,Studiopolis,and finally Auto Save Screen", + "version": "0.9.29", + "views": 79777, + "downloads": 59988, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-09-20T00:19:48+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_37890-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_37890-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_37890-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_37890-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_37890-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_37890-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/37890", + "download_url": "https://share.catrobat.org/api/projects/37890/catrobat", + "filesize": 70.83020114898682 + }, + { + "id": "72798", + "name": "CS:GO", + "author": "GameCompani7", + "description": "\u0414\u0430\u0442\u0430 \u0440\u0435\u043b\u0438\u0437\u0430! (12.31.18!) \n\n\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435! \n-\u041a\u0440\u0443\u0442\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435! \n- \u041e\u0431\u043d\u043e\u0432\u0438\u043b \u043e\u0440\u0443\u0436\u0438\u044f \u0442\u0435\u043f\u0435\u0440\u044c \u043a\u0440\u0443\u0442\u044b\u0435! \n-\u0414\u043e\u0431\u0430\u0432\u0438\u043b \u041c\u0443\u0437\u044b\u043a\u0443 \u0432 \u043c\u0435\u043d\u044e! \n-\u041f\u043e\u043c\u0435\u043d\u044f\u043b \u043a\u0430\u0440\u0442\u044b \u0441\u0434\u0435\u043b\u0430\u043b \u043a\u0440\u0430\u0441\u0438\u0432\u044b\u043c\u0438\n\u0438 3D \u0432\u0441\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u043b! \n-\u0418\u0437\u043c\u0435\u043d\u0438\u043b \u0424\u043e\u043d \u041c\u0435\u043d\u044e! \n-\u0414\u043e\u0431\u0430\u0432\u0438\u043b \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043a\u0430\u0440\u0442\u044b! \n\n\nCSGO. \u041e\u0442 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 GameKompani7\n\n\u0412\u044b \u0438\u0433\u0440\u0430\u0435\u0442\u0435 \u0412 \u0440\u043e\u043b\u0438 \u0421\u043f\u0435\u0446\u043d\u0430\u0437\u0430 \u0438\u043b\u0438 \u0436\u0435 \u0422\u0435\u0440\u043e\u0440\u0438\u0441\u0442\u0430! \n\u0423\u0431\u0435\u0439\u0442\u0435 \u0432\u0441\u0435\u0445 \u0437\u0430 40-45 \u0441\u0435\u043a\u0443\u043d\u0434! \n\u041c\u043d\u043e\u0433\u043e \u043e\u0440\u0443\u0436\u0438\u0435 \u0441\u043a\u0438\u043d\u044b \u043a\u0435\u0439\u0441\u044b 4 \u043a\u0430\u0440\u0442\u044b! \n(Dast inferno Nyke office) \n\u0414\u043e\u0441\u0442\u0438\u0433\u043d\u0438 \u0413\u043b\u043e\u0431\u0430\u043b\u0430! \n\u0418 \u0441\u0442\u0430\u043d\u044c \u043b\u0443\u0447\u0448\u0438\u043c! \n \u041e\u0446\u0435\u043d\u0438\u0442\u0435 \u043d\u0430\u0441! \n\u0412\u0438\u0434\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0431\u044b\u043b\u0430 \u0434\u043e\u043b\u0433\u043e\u0439 \u0438 \u0442\u0440\u0443\u0434\u043d\u043e\u0439! \n\u041f\u0435\u0440\u0435\u0437\u0430\u0440\u044f\u0434\u043a\u0430 \u0432\u0441\u0451 \u0442\u0443\u0442 \u0435\u0441\u0442\u044c \u043d\u0443 \u043f\u043e\u0447\u0442\u0438! \n\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0447\u0435\u0440\u0435\u0437 7 \u0434\u043d\u0435\u0439 \u0431\u0443\u0434\u0438\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0442\u0430\u043a \u0447\u0442\u043e\n\u0416\u0434\u0438\u0442\u0435! \n\u041f\u0440\u0430\u0432\u0430/ GameKompani7\u00ae", + "version": "0.9.44", + "views": 22475, + "downloads": 56214, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2019-01-07T17:16:32+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_72798-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_72798-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_72798-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_72798-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_72798-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_72798-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/72798", + "download_url": "https://share.catrobat.org/api/projects/72798/catrobat", + "filesize": 19.330181121826172 + }, + { + "id": "f0ddcde2-9ca2-11eb-a92d-005056a36f47", + "name": "Beste game in ganz Pocket code?!", + "author": "Tap Tap Inc.", + "description": "Vlt...", + "version": "1.0.3", + "views": 281, + "downloads": 55104, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "experimental": "Experimental", + "tutorial": "Tutorial" + }, + "uploaded_at": "2021-06-13T23:20:20+00:00", + "uploaded_string": "5 days ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_f0ddcde2-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_f0ddcde2-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_f0ddcde2-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_f0ddcde2-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_f0ddcde2-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_f0ddcde2-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/f0ddcde2-9ca2-11eb-a92d-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/f0ddcde2-9ca2-11eb-a92d-005056a36f47/catrobat", + "filesize": 0.15716552734375 + }, + { + "id": "45868", + "name": "Iso-motion", + "author": "headhunter", + "description": "To play this game, cast your screen to a TV or a PC and use the device as a motion controller to move your character.\n***********************\nMade for GCI task \nAny and All assets used in the program are made by me and are without any previous copyrights, feel free to use them in your programs.\nCalibration code taken from task- https://share.catrob.at/pocketcode/program/38660\n************************\nUpdate 2\n- added death counter\nUpdate 1\n- changed Level 1 orb position.\n- More levels coming soon...", + "version": "0.9.33", + "views": 44948, + "downloads": 44009, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "experimental": "Experimental" + }, + "uploaded_at": "2018-01-13T10:00:10+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45868-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45868-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45868-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45868-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_45868-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_45868-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/45868", + "download_url": "https://share.catrobat.org/api/projects/45868/catrobat", + "filesize": 30.878884315490723 + }, + { + "id": "20065", + "name": "Geometry Dash v1.5", + "author": "VenomX", + "description": "\nMade with Scratch2Catrobat Converter version 0.7.2.\nOriginal Scratch project => https://scratch.mit.edu/projects/105500895", + "version": "0.9.27", + "views": 44238, + "downloads": 39256, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "music": "Music" + }, + "uploaded_at": "2016-12-10T16:03:44+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/20065", + "download_url": "https://share.catrobat.org/api/projects/20065/catrobat", + "filesize": 16.550339698791504 + }, + { + "id": "43252", + "name": "Fnaf Pizzeria Simulator", + "author": "KyleOfBlades", + "description": "My Port of Fnaf 6! Enjoy! Please subscribe! My Channel is Jack \u00d8f Blades", + "version": "0.9.32", + "views": 42732, + "downloads": 38356, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-12-10T10:26:33+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_43252-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_43252-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_43252-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_43252-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_43252-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_43252-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/43252", + "download_url": "https://share.catrobat.org/api/projects/43252/catrobat", + "filesize": 12.229867935180664 + }, + { + "id": "1450", + "name": "Minions ", + "author": "kestapa", + "description": "", + "version": "0.9.4", + "views": 28599, + "downloads": 32713, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2013-10-30T04:23:59+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1450-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1450-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1450-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1450-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1450-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1450-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/1450", + "download_url": "https://share.catrobat.org/api/projects/1450/catrobat", + "filesize": 0.9740276336669922 + }, + { + "id": "965", + "name": "Air fight 0.5", + "author": "hej-wickie-hej", + "description": "Shoot down your opponent! Steer with your device's inclination. Shoot by tapping or holding the screen. \r\n\r\nUpdate 2016-11-2: now with object grouping, new Catrobat language version 0.992 bricks, as well as unlimited bullets (through clone bricks).", + "version": "0.9.27", + "views": 27399, + "downloads": 30626, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2016-11-02T16:43:10+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_965-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_965-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_965-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_965-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_965-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_965-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/965", + "download_url": "https://share.catrobat.org/api/projects/965/catrobat", + "filesize": 5.546684265136719 + }, + { + "id": "46b43efe-fb9e-11ea-a92d-005056a36f47", + "name": "Pocket Moon v1.0", + "author": "Jude Birch", + "description": "Farming game made for Google Summer of Code.\n\nPlay as the owner of a farm and grow different crops over the seasons. Sell crops to buy more seeds and tools at the shop, and unlock more areas! You can also explore the world and discover the 21 different fish in the game!\n\nGoogle Summer of Code Page: https://summerofcode.withgoogle.com/projects/5782435888889856\nGameplay: https://youtu.be/oMPMTiVN1Vs\n\nThanks to Jordan Smith for mentoring and supporting the project.", + "version": "0.9.74", + "views": 39021, + "downloads": 28992, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2020-10-28T10:38:33+00:00", + "uploaded_string": "8 months ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46b43efe-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/46b43efe-fb9e-11ea-a92d-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/46b43efe-fb9e-11ea-a92d-005056a36f47/catrobat", + "filesize": 4.47955322265625 + }, + { + "id": "49860", + "name": "Undertale: rood fight", + "author": "Powerpoop", + "description": "No", + "version": "0.9.33", + "views": 27617, + "downloads": 28961, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "story": "Story" + }, + "uploaded_at": "2018-03-13T01:27:19+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/49860", + "download_url": "https://share.catrobat.org/api/projects/49860/catrobat", + "filesize": 15.183897018432617 + }, + { + "id": "7eb7c370-de0c-11ea-a5c3-005056a36f47", + "name": "flying bird", + "author": "Gampley", + "description": "When clik Bird seems sounrs", + "version": "0.9.75", + "views": 5222, + "downloads": 27122, + "flavor": "pocketcode", + "tags": { + "animation": "Animation" + }, + "uploaded_at": "2021-01-24T12:05:58+00:00", + "uploaded_string": "5 months ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_7eb7c370-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_7eb7c370-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_7eb7c370-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_7eb7c370-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_7eb7c370-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_7eb7c370-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/7eb7c370-de0c-11ea-a5c3-005056a36f47", + "download_url": "https://share.catrobat.org/api/projects/7eb7c370-de0c-11ea-a5c3-005056a36f47/catrobat", + "filesize": 1.6682100296020508 + }, + { + "id": "42921", + "name": "Math Quest! v2", + "author": "Jude Birch", + "description": "Here's an educational binary themed RPG for Google code-in!", + "version": "0.9.32", + "views": 33784, + "downloads": 25156, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-12-05T21:53:52+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_42921-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_42921-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_42921-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_42921-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_42921-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_42921-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/42921", + "download_url": "https://share.catrobat.org/api/projects/42921/catrobat", + "filesize": 36.889180183410645 + }, + { + "id": "44987", + "name": "Slendytubbies 2D Part 2", + "author": "Felix Filip", + "description": "V1.3 (Last Update) 12.02.18\n\nOBJECTIVE:\nyou need to collect all 5 - 25 Custards while trying to avoid the Monsters\n\nThis game is proven to be the best and scariest game on Pocket Code, If you don't believe me just play the game\n\n- 14 Maps with different Monsters\n\n- Choose Between an Flashlight or an Night Vision Camera\n\n- Animations\n\n- All maps are Large\n\n- You can change the Custard level from 5 to 25 Custards\n\n-There's an Energy bar for the Flashlight and the Night Vision Camera so be careful and \u00a0don't use it to much\n\nYouTube: Felix Filip\nGame by: Felix Filip\nTwitter: @FelixFilipYT\nRoblox: FelixFilipYT\nMCPE: Felix Filip AC", + "version": "0.9.33", + "views": 20125, + "downloads": 24661, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2018-02-12T23:00:24+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44987-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44987-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44987-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44987-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44987-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44987-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/44987", + "download_url": "https://share.catrobat.org/api/projects/44987/catrobat", + "filesize": 47.748464584350586 + }, + { + "id": "32577", + "name": "Air Hockey", + "author": "Kingsimon", + "description": "Air Hockey", + "version": "0.9.74", + "views": 20334, + "downloads": 23830, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "music": "Music", + "experimental": "Experimental" + }, + "uploaded_at": "2020-10-13T16:12:58+00:00", + "uploaded_string": "8 months ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32577-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32577-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32577-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32577-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32577-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32577-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/32577", + "download_url": "https://share.catrobat.org/api/projects/32577/catrobat", + "filesize": 23.37629508972168 + }, + { + "id": "1719", + "name": "Don't touch the white tiles!", + "author": "hej-wickie-hej", + "description": "You have 25 seconds to tap as many of the black tiles as possible, starting from the bottom. Of course, don't touch the white ones!\n\nNote that, although you must tap the black tiles one after the other starting from the lower ones, it is nevertheless possible to tap the next tiles already while they are still moving. Doing so gives exponentially more points, depending on how high the tiles were on the screen when you tapped them. \n\nRemark: All numbers and other text in images have been made with PicSay, available on Google Play, see http://catrob.at/PicSay and http://catrob.at/WordBalloonExplanation ", + "version": "0.9.9", + "views": 17981, + "downloads": 22867, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2014-07-06T21:34:15+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1719-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1719-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1719-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1719-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1719-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1719-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/1719", + "download_url": "https://share.catrobat.org/api/projects/1719/catrobat", + "filesize": 0.1950674057006836 + }, + { + "id": "1976", + "name": "Skydiving Steve", + "author": "PocketCodeTeam", + "description": "Download this program to start with the One Hour of Code tutorial at https://pocketcode.org/hourOfCode", + "version": "0.9.10", + "views": 15617, + "downloads": 19520, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2014-09-12T09:32:48+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1976-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1976-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1976-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1976-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1976-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1976-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/1976", + "download_url": "https://share.catrobat.org/api/projects/1976/catrobat", + "filesize": 1.7407207489013672 + } + ] + }, + { + "type": "scratch", + "name": "Scratch remixes", + "projects_list": [ + { + "id": "20065", + "name": "Geometry Dash v1.5", + "author": "VenomX", + "description": "\nMade with Scratch2Catrobat Converter version 0.7.2.\nOriginal Scratch project => https://scratch.mit.edu/projects/105500895", + "version": "0.9.27", + "views": 44238, + "downloads": 39256, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "music": "Music" + }, + "uploaded_at": "2016-12-10T16:03:44+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_20065-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_20065-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/20065", + "download_url": "https://share.catrobat.org/api/projects/20065/catrobat", + "filesize": 16.550339698791504 + }, + { + "id": "49860", + "name": "Undertale: rood fight", + "author": "Powerpoop", + "description": "No", + "version": "0.9.33", + "views": 27617, + "downloads": 28961, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "story": "Story" + }, + "uploaded_at": "2018-03-13T01:27:19+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49860-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49860-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/49860", + "download_url": "https://share.catrobat.org/api/projects/49860/catrobat", + "filesize": 15.183897018432617 + }, + { + "id": "18600", + "name": "Super Mario Bros (version 10)", + "author": "Ayrat", + "description": "\n----------------------------------------\nInstructions:\nIt's finally here... version 10! It has got 5 brand new levels! And more are coming soon... Use arrow keys to move and jump. Collect the coins. Jump upon Goomba enemies (twice!) to beat them. Here's a walkthrough: https://www.youtube.com/watch?v=ONtHRrN-cDQ\n----------------------------------------\nDescription:\nI'm trying to make a Super Mario Bros game. It's not gonna be a remake. I just use the sprites and sounds. In version 10 there is a new enemy: Bullet Bill! And.. there's also music now! Feel free to remix and build a level yourself! Future plans: - add power ups - other enemies - and more... Credits for the sprites and sounds are for Nintendo. I found them here: http://spriters-resource.com/nes/supermariobros/ and http://themushroomkingdom.net/media/smb/wav\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.7.2.\nOriginal Scratch project => https://scratch.mit.edu/projects/49905542", + "version": "0.9.27", + "views": 13814, + "downloads": 15864, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "music": "Music" + }, + "uploaded_at": "2016-11-19T22:25:10+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_18600-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_18600-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_18600-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_18600-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_18600-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_18600-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/18600", + "download_url": "https://share.catrobat.org/api/projects/18600/catrobat", + "filesize": 5.398584365844727 + }, + { + "id": "27621", + "name": "CUSTOM Undertale: Asriel Mini Battle", + "author": "Nintendo", + "description": "\n----------------------------------------\nInstructions:\nDefeat Asriel Dreemurr! Use the arrow keys to dodge his attacks. Since Asriel is so powerful; if you get hit once, you die! WARNING! Very laggy on fullscreen! ANOTHER WARNING! May spoil the game!\n----------------------------------------\nDescription:\nThanks to @Werkaec's Undertale: Memories of the Underground V3 (Rememberance) game with some of the coding. The fantastic Undertale game was made by Toby Fox!\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.8.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/92196198\n\n\nThe Soul May Glitch out of the box", + "version": "0.9.28", + "views": 7922, + "downloads": 8112, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-03-29T01:49:36+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_27621-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_27621-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_27621-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_27621-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_27621-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_27621-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/27621", + "download_url": "https://share.catrobat.org/api/projects/27621/catrobat", + "filesize": 5.910174369812012 + }, + { + "id": "32246", + "name": "One Night At Freddi FezzBurs Version 1.3.0", + "author": "KottaTheCat", + "description": "\n----------------------------------------\nInstructions:\nThe Official One Night At Freddi FezzBurs Game! Now On Gamejolt: http://gamejolt.com/games/one-night-at-freddi-fezzburs/88454 Update 1.3.0 What's New! -Added Time -Added Jumpscare -Added Freddi at The Door Game Made By Brosheb Crisp Inspired By Scott Cawthon's Five Nights At Freddy's More Updates Soon\n----------------------------------------\nDescription:\nNote: DO NOT REMIX! Game Made By Brosheb Crisp Inspired By Scott Cawthon's Five Nights At Freddy's Game Not Yet Done =P\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.8.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/73342440", + "version": "0.9.28", + "views": 4998, + "downloads": 5331, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "experimental": "Experimental" + }, + "uploaded_at": "2017-06-13T16:40:23+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32246-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32246-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32246-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32246-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_32246-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_32246-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/32246", + "download_url": "https://share.catrobat.org/api/projects/32246/catrobat", + "filesize": 7.9396772384643555 + }, + { + "id": "41333", + "name": "CupHead Boss Fight", + "author": "FuriousFreddy", + "description": "\n----------------------------------------\nInstructions:\nARROW KEYS = MOVE SPACE BAR = SHOOT (Or hold for rapid fire) --------------------------------------------------------- \u2605 Full screen not recommended Press the green flag until it runs smoother (if necessary) Wally Warbles (from Cuphead) Boss fight\n----------------------------------------\nDescription:\nMusic: Cuphead - Aviary Action \u2605 This is possible to beat, many people defeated it Sorry, I couldn't do the entire fight, with the son, and the final phase. Credits: Thanks @20105118 for some of the scripts Thanks @Mistystone for the wing animation (I changed it a bit) Thanks @Kurt_Colwell for the explosion And can't forget about MDHR Studios!\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.9.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/178944041", + "version": "0.9.31", + "views": 4076, + "downloads": 3706, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-11-12T16:33:14+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_41333-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_41333-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_41333-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_41333-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_41333-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_41333-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/41333", + "download_url": "https://share.catrobat.org/api/projects/41333/catrobat", + "filesize": 7.461623191833496 + }, + { + "id": "46885", + "name": "Youtubers life 0.1 ", + "author": "FurbyLisa", + "description": "\n----------------------------------------\nInstructions:\nclick the computer for shop, video, and video maker click bed to boost sleep click burger to boost hunger click door to work or move click your head to change outfit\n----------------------------------------\nDescription:\nSort of like the original Youtubers Life\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.9.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/129293842", + "version": "0.9.33", + "views": 3605, + "downloads": 3962, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2018-01-27T18:14:09+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46885-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46885-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46885-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46885-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_46885-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_46885-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/46885", + "download_url": "https://share.catrobat.org/api/projects/46885/catrobat", + "filesize": 0.3914451599121094 + }, + { + "id": "00cb9482-3fac-11ea-8273-000c292a0f49", + "name": "Sonic 3 Special Stage Test", + "author": "Spinel (Steven Universe) ", + "description": "Hmmmmm", + "version": "0.9.67", + "views": 2738, + "downloads": 3114, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "experimental": "Experimental" + }, + "uploaded_at": "2020-01-25T19:50:48+00:00", + "uploaded_string": "1 year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_00cb9482-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_00cb9482-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_00cb9482-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_00cb9482-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_00cb9482-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_00cb9482-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/00cb9482-3fac-11ea-8273-000c292a0f49", + "download_url": "https://share.catrobat.org/api/projects/00cb9482-3fac-11ea-8273-000c292a0f49/catrobat", + "filesize": 5.727170944213867 + }, + { + "id": "85485", + "name": "\u043e\u0434\u043d\u0430 \u043d\u043e\u0447\u044c \u0441 \u043f\u0430\u0440\u043e\u0434\u0438\u044f\u043c\u0438 5", + "author": "romanade", + "description": "\u0437\u0435\u043b\u0435\u043d\u044b\u0439 \u043f\u0430\u0440\u0435\u043d\u044c \u0432 \u043d\u0443\u0442\u0440\u0438 \u0430\u043d\u0438\u043c\u0430\u0442\u0440\u043e\u043d\u0438\u043a\u0430 \u0432\u044b \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u0436\u0438\u0442\u044c 1 \u043d\u043e\u0447\u044c \u0438\u043b\u0438 \u043d\u0435\u0442?.", + "version": "0.9.56", + "views": 2488, + "downloads": 1962, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "story": "Story" + }, + "uploaded_at": "2019-04-21T12:03:33+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_85485-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_85485-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_85485-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_85485-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_85485-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_85485-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/85485", + "download_url": "https://share.catrobat.org/api/projects/85485/catrobat", + "filesize": 8.277359962463379 + }, + { + "id": "112335", + "name": "Counter Strike 1.6 (\u041f\u043e\u0440\u0442 \u043d\u0430 Pocket Code)", + "author": "\u041d\u043e\u043a\u0430\u0442", + "description": "0.1 Beta", + "version": "0.9.64", + "views": 2113, + "downloads": 2549, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "story": "Story" + }, + "uploaded_at": "2019-10-27T17:28:42+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_112335-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_112335-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_112335-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_112335-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_112335-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_112335-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/112335", + "download_url": "https://share.catrobat.org/api/projects/112335/catrobat", + "filesize": 0.9216365814208984 + }, + { + "id": "81606", + "name": "fnati simulator (android version) v1.0", + "author": "tasos9paok", + "description": "fnati Simulator v1.0", + "version": "0.9.54", + "views": 1989, + "downloads": 1801, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "art": "Art" + }, + "uploaded_at": "2019-03-16T20:23:10+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_81606-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_81606-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_81606-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_81606-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_81606-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_81606-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/81606", + "download_url": "https://share.catrobat.org/api/projects/81606/catrobat", + "filesize": 20.946385383605957 + }, + { + "id": "85e6d6f3-4d0e-11ea-8d1e-000c292a0f49", + "name": "Sonic Mania Plus Mobile", + "author": "Sonic Channel ", + "description": "----------------------------------------\nInstructions:\nThis is in Beta! Move with the arrow keys! Down and space to spindash! ( Its very buggy!)\n----------------------------------------\nDescription:\nFeel free to use this WITH credit. Credit to @Shyguydude for the sprites!\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.9.1.\nOriginal Scratch project => https://scratch.mit.edu/projects/25103782", + "version": "0.9.68", + "views": 1946, + "downloads": 1856, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2020-02-11T20:38:55+00:00", + "uploaded_string": "1 year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_85e6d6f3-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_85e6d6f3-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_85e6d6f3-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_85e6d6f3-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_85e6d6f3-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_85e6d6f3-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/85e6d6f3-4d0e-11ea-8d1e-000c292a0f49", + "download_url": "https://share.catrobat.org/api/projects/85e6d6f3-4d0e-11ea-8d1e-000c292a0f49/catrobat", + "filesize": 49.56886863708496 + }, + { + "id": "49251", + "name": "Five Nights At Mario's", + "author": "austin14", + "description": "\n----------------------------------------\nInstructions:\nScary Game In Years. How To Play:Do Not Waste Your Power. You Have Cameras 1A-7 Do Not Get Jumpscared By:Mario. Luigi. Peach. Yoshi. And Golden Mario. And Survive From 12 am to 6 am. Stay Safe. Check Lights. Close A Door If You Need To. It Takes 70 Secs For A Next Hour. The Camera Change Sound Is The Same Sound With Five Nights At Freddy's Camera Sound. You Have 7 Nights To Survive. Five Nights At Mario's 2 Coming Soon.\n----------------------------------------\nDescription:\nNone\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.9.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/92798731", + "version": "0.9.33", + "views": 1908, + "downloads": 1926, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2018-03-05T02:45:21+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49251-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49251-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49251-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49251-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_49251-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_49251-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/49251", + "download_url": "https://share.catrobat.org/api/projects/49251/catrobat", + "filesize": 54.85420036315918 + }, + { + "id": "56286", + "name": "\u8c37\u6b4c\u4e2d\u56fd", + "author": "FurbyLisa", + "description": "\n----------------------------------------\nInstructions:\n\u70b9\u51fbgoogle\u5fbd\u6807\u4e0b\u65b9\u7684\u641c\u7d22\u680f\uff0c\u7136\u540e\u8f93\u5165... -Skype -\u522e -\u56fe\u7247 -youtube \u5982\u679c\u4f60\u4e0d\u8f93\u5165\u5176\u4e2d\u7684\u4e00\u4e2a\uff0c\u5b83\u4f1a\u8bf4\uff0c\u6ca1\u6709\u7ed3\u679c\uff01 Di\u01cenj\u012b google hu\u012bbi\u0101o xi\u00e0f\u0101ng de s\u014dusu\u01d2 l\u00e1n, r\u00e1nh\u00f2u sh\u016br\u00f9... -Skype -gu\u0101 -t\u00fapi\u00e0n -youtube r\u00fagu\u01d2 n\u01d0 b\u00f9 sh\u016br\u00f9 q\u00edzh\u014dng de y\u012bg\u00e8, t\u0101 hu\u00ec shu\u014d, m\u00e9iy\u01d2u ji\u00e9gu\u01d2! \u7531\u4e8e\u8c37\u6b4c\u5728\u4e2d\u56fd\u7684\u5ba1\u67e5\u5236\u5ea6\uff0c\u6211\u51b3\u5b9a\u4e3a\u4e86\u7eaa\u5ff5\u8c37\u6b4c\u4e2d\u56fd\u800c\u6210\u7acb\u8fd9\u4e2a\u9879\u76ee\u3002\n----------------------------------------\nDescription:\nTo do the china parts,i had to transtlle them with google.\n----------------------------------------\n\n\n Desktop Version => https://scratch.mit.edu/projects/226034878", + "version": "0.9.33", + "views": 1853, + "downloads": 1555, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2018-05-30T00:38:03+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_56286-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_56286-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_56286-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_56286-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_56286-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_56286-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/56286", + "download_url": "https://share.catrobat.org/api/projects/56286/catrobat", + "filesize": 0.7806711196899414 + }, + { + "id": "30197", + "name": "Need For Speed Ultra City: Car Select", + "author": "shai", + "description": "\n----------------------------------------\nInstructions:\nFull Title: (FAKE) Need For Speed Ultra City: Car Select Platforms: Playstation 4, Xbox One, Playstation Vita, Nintendo 3DS and Nintendo Switch\n----------------------------------------\nDescription:\nCredits to all NOTE: This game is fake. So don't report!\n----------------------------------------\n\nMade with Scratch2Catrobat Converter version 0.8.0.\nOriginal Scratch project => https://scratch.mit.edu/projects/147913114", + "version": "0.9.28", + "views": 1532, + "downloads": 1857, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2017-05-13T19:06:52+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_30197-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_30197-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_30197-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_30197-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_30197-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_30197-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/30197", + "download_url": "https://share.catrobat.org/api/projects/30197/catrobat", + "filesize": 6.56934928894043 + }, + { + "id": "67811", + "name": "Sonic The Hedgehog\u2122 4", + "author": "FurbyLisa", + "description": "----------------------------------------\nInstructions:\nPress space to show Sonic or jump and press the arrow keys to move.\n----------------------------------------\nDescription:\n:P\n---------------------------------------", + "version": "0.9.40", + "views": 1457, + "downloads": 1603, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2018-10-17T21:12:04+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_67811-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_67811-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_67811-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_67811-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_67811-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_67811-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/67811", + "download_url": "https://share.catrobat.org/api/projects/67811/catrobat", + "filesize": 6.96649169921875 + }, + { + "id": "112352", + "name": "Tiny World Generator", + "author": "PIXcY", + "description": "only another random pocketworld-generator", + "version": "0.9.64", + "views": 1198, + "downloads": 1062, + "flavor": "pocketcode", + "tags": { + "animation": "Animation", + "art": "Art", + "experimental": "Experimental" + }, + "uploaded_at": "2019-10-31T07:31:08+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_112352-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_112352-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_112352-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_112352-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_112352-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_112352-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/112352", + "download_url": "https://share.catrobat.org/api/projects/112352/catrobat", + "filesize": 0.0979766845703125 + }, + { + "id": "1808", + "name": "DJ Scratch Cat remix", + "author": "chwt", + "description": "Made with ScratchToCatrobat Converter version 2014.08.05. Original Scratch project => http://scratch.mit.edu/projects/17828107", + "version": "0.9.9", + "views": 1173, + "downloads": 942, + "flavor": "pocketcode", + "tags": {}, + "uploaded_at": "2014-08-08T05:13:29+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1808-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1808-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1808-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1808-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_1808-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_1808-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/1808", + "download_url": "https://share.catrobat.org/api/projects/1808/catrobat", + "filesize": 0.3998870849609375 + }, + { + "id": "30240", + "name": "Terraria Sprites", + "author": "SansCucumber2", + "description": "\nTerraria Sptires", + "version": "0.9.28", + "views": 1047, + "downloads": 748, + "flavor": "pocketcode", + "tags": { + "game": "Game", + "animation": "Animation", + "art": "Art" + }, + "uploaded_at": "2017-05-14T11:20:47+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_30240-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_30240-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_30240-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_30240-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_30240-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_30240-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/30240", + "download_url": "https://share.catrobat.org/api/projects/30240/catrobat", + "filesize": 71.6375150680542 + }, + { + "id": "44573", + "name": "Five Nights at Tankloop's", + "author": "FurbyLisa", + "description": "Credit to @baty on deavinet for the Batman bendy", + "version": "0.9.33", + "views": 839, + "downloads": 901, + "flavor": "pocketcode", + "tags": { + "game": "Game" + }, + "uploaded_at": "2017-12-25T22:38:30+00:00", + "uploaded_string": "more than one year ago", + "screenshot": { + "thumb": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44573-thumb@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44573-thumb@2x.webp" + }, + "card": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44573-card@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44573-card@2x.webp" + }, + "detail": { + "webp_1x": "https://share.catrobat.org/resources/screenshots/screen_44573-detail@1x.webp", + "webp_2x": "https://share.catrobat.org/resources/screenshots/screen_44573-detail@2x.webp" + } + }, + "project_url": "https://share.catrobat.org/app/project/44573", + "download_url": "https://share.catrobat.org/api/projects/44573/catrobat", + "filesize": 55.191046714782715 + } + ] + } + ] +} diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/retrofittesting/CatroidWebServerTest.kt b/catroid/src/androidTest/java/org/catrobat/catroid/retrofittesting/CatroidWebServerTest.kt index ca5a3fcfe65..c9034eba27a 100644 --- a/catroid/src/androidTest/java/org/catrobat/catroid/retrofittesting/CatroidWebServerTest.kt +++ b/catroid/src/androidTest/java/org/catrobat/catroid/retrofittesting/CatroidWebServerTest.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -29,35 +29,45 @@ import androidx.test.platform.app.InstrumentationRegistry import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue +import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.catrobat.catroid.common.Constants import org.catrobat.catroid.common.FlavoredConstants +import org.catrobat.catroid.retrofit.CatroidWebServer import org.catrobat.catroid.retrofit.WebService +import org.catrobat.catroid.retrofit.models.getCardUrl +import org.catrobat.catroid.retrofit.models.getDetailUrl import org.catrobat.catroid.testsuites.annotations.Cat.OutgoingNetworkTests 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.test.KoinTest -import org.koin.test.inject import org.mockito.MockitoAnnotations import org.mockito.junit.MockitoJUnitRunner +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory import java.net.HttpURLConnection @RunWith(MockitoJUnitRunner::class) @Category(OutgoingNetworkTests::class) -class CatroidWebServerTest : KoinTest { +class CatroidWebServerTest { companion object { - private const val SUCCESS_RESPONSE_FILE_NAME = "featured_projects_success_response.json" + private const val FEATURED_PROJECTS_FIXTURE = "featured_projects_success_response.json" + private const val PROJECTS_CATEGORIES_FIXTURE = "projects_categories_response.json" } private lateinit var mockWebServer: MockWebServer private lateinit var context: Context - private val webServer: WebService by inject() + // Points at MockWebServer (HTTP). Used for tests that execute() the call. + private lateinit var mockWebService: WebService + + // Points at the production API base URL. Used only to assert URL formatting + // / HTTPS — never executed against the network. + private lateinit var productionWebService: WebService @Before fun setUp() { @@ -65,6 +75,9 @@ class CatroidWebServerTest : KoinTest { MockitoAnnotations.initMocks(this) mockWebServer = MockWebServer() mockWebServer.start() + + mockWebService = buildCleartextWebService(mockWebServer.url("/").toString()) + productionWebService = CatroidWebServer.getWebService(Constants.API_BASE_URL) } @After @@ -74,158 +87,157 @@ class CatroidWebServerTest : KoinTest { @Test fun testReadSampleSuccessJsonFile() { - val reader = MockResponseFileReader(context, SUCCESS_RESPONSE_FILE_NAME) + val reader = MockResponseFileReader(context, FEATURED_PROJECTS_FIXTURE) assertNotNull(reader.content) } @Test @Throws(Exception::class) fun testFetchFeaturedProjectsAndCheckResponseCode200Returned() { - val response = MockResponse() - .setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(MockResponseFileReader(context, SUCCESS_RESPONSE_FILE_NAME).content) - mockWebServer.enqueue(response) + enqueueOk(FEATURED_PROJECTS_FIXTURE) - val actualResponse = webServer.getFeaturedProjects().execute() + val actualResponse = mockWebService.getFeaturedProjects().execute() - assertEquals( - response.toString().containsOkHttpCode(), - actualResponse.code().toString().containsOkHttpCode() - ) + assertEquals(HttpURLConnection.HTTP_OK, actualResponse.code()) } @Test @Throws(Exception::class) fun testFeaturedProjectsCallUsesHTTPS() { - assertTrue(webServer.getFeaturedProjects().request().isHttps) + assertTrue(productionWebService.getFeaturedProjects().request().isHttps) } @Test @Throws(Exception::class) fun testFeaturedProjectsResponseHasCorrectStructure() { - val response = webServer.getFeaturedProjects().execute().body() + enqueueOk(FEATURED_PROJECTS_FIXTURE) + + val response = mockWebService.getFeaturedProjects().execute().body() assertNotNull(response) - webServer.getFeaturedProjects() - .execute() - .body() - ?.forEach { - assertNotNull(it) - assertNotNull(it.id) - assertNotNull(it.author) - assertNotNull(it.featured_image) - assertNotNull(it.name) - assertNotNull(it.project_id) - assertNotNull(it.project_url) - } + response?.data?.forEach { + assertNotNull(it) + assertNotNull(it.id) + assertNotNull(it.author) + assertNotNull(it.featuredImage) + assertNotNull(it.name) + assertNotNull(it.projectId) + assertNotNull(it.projectUrl) + } } @Test @Throws(Exception::class) fun testFeaturedProjectsResponseHasValidProjectUrls() { - val response = webServer.getFeaturedProjects().execute().body() + enqueueOk(FEATURED_PROJECTS_FIXTURE) + + val response = mockWebService.getFeaturedProjects().execute().body() assertNotNull(response) - webServer.getFeaturedProjects() - .execute() - .body() - ?.forEach { - assertTrue(it.name.isNotEmpty()) - assertTrue(Patterns.WEB_URL.matcher(it.project_url).matches()) - } + response?.data?.forEach { + assertTrue(it.name.isNotEmpty()) + assertTrue(Patterns.WEB_URL.matcher(it.projectUrl).matches()) + } } @Test @Throws(Exception::class) - fun testFeaturedProjectsResponseHasValidImagesFormat() { - val response = webServer.getFeaturedProjects().execute().body() + fun testFeaturedProjectsResponseHasValidImages() { + enqueueOk(FEATURED_PROJECTS_FIXTURE) + + val response = mockWebService.getFeaturedProjects().execute().body() assertNotNull(response) - webServer.getFeaturedProjects() - .execute() - .body() - ?.forEach { - assertTrue(it.name.isNotEmpty()) - assertTrue(Patterns.WEB_URL.matcher(it.featured_image).matches()) - assertTrue(it.featured_image.contains(Constants.DEFAULT_IMAGE_EXTENSION)) - } + response?.data?.forEach { + assertTrue(it.name.isNotEmpty()) + val imageUrl = it.featuredImage?.getDetailUrl() ?: it.featuredImage?.getCardUrl() + assertNotNull(imageUrl) + assertTrue(Patterns.WEB_URL.matcher(imageUrl!!).matches()) + } } @Test @Throws(Exception::class) fun testFeaturedProjectsEmptyFlavorNameReturnsEmptyList() { - val response = webServer.getFeaturedProjects(flavor = "") - .execute() - .body() + mockWebServer.enqueue( + MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody("""{"data":[]}""") + ) + + val response = mockWebService.getFeaturedProjects(flavor = "").execute().body() assertNotNull(response) - assertTrue(response!!.isEmpty()) + assertTrue(response!!.data.isEmpty()) } @Test @Throws(Exception::class) fun testFeaturedProjectsCallWithDefaultValues() { - val expectedRawResponse = - "Response{protocol=h2, code=200, message=," + - " url=https://share.catrob.at/api/projects/featured?" + - "max_version=${Constants.CURRENT_CATROBAT_LANGUAGE_VERSION}" + - "&flavor=${FlavoredConstants.FLAVOR_NAME}&platform=android&limit=20&offset=0}" - val rawResponse = webServer.getFeaturedProjects().execute().raw().toString() - assertEquals(expectedRawResponse, rawResponse) + val expectedUrl = "${Constants.API_BASE_URL}projects/featured?" + + "max_version=${Constants.CURRENT_CATROBAT_LANGUAGE_VERSION}" + + "&flavor=${FlavoredConstants.FLAVOR_NAME}&platform=android&limit=20" + val actualUrl = productionWebService.getFeaturedProjects().request().url().toString() + assertEquals(expectedUrl, actualUrl) } @Test @Throws(Exception::class) fun testProjectCategoriesCallUsesHTTPS() { - assertTrue(webServer.getProjectCategories().request().isHttps) + assertTrue(productionWebService.getProjectCategories().request().isHttps) } @Test @Throws(Exception::class) fun testProjectCategoriesCallWithDefaultValues() { - val expectedRawResponse = - "Response{protocol=h2, code=200, message=," + - " url=https://share.catrob.at/api/projects/categories?" + - "max_version=${Constants.CURRENT_CATROBAT_LANGUAGE_VERSION}" + - "&flavor=${FlavoredConstants.FLAVOR_NAME}}" - val rawResponse = webServer.getProjectCategories().execute().raw().toString() - assertEquals(expectedRawResponse, rawResponse) + val expectedUrl = "${Constants.API_BASE_URL}projects/categories?" + + "max_version=${Constants.CURRENT_CATROBAT_LANGUAGE_VERSION}" + + "&flavor=${FlavoredConstants.FLAVOR_NAME}" + val actualUrl = productionWebService.getProjectCategories().request().url().toString() + assertEquals(expectedUrl, actualUrl) } @Test @Throws(Exception::class) fun testProjectCategoriesResponseHasCorrectStructure() { - val response = webServer.getProjectCategories().execute().body() + enqueueOk(PROJECTS_CATEGORIES_FIXTURE) + + val response = mockWebService.getProjectCategories().execute().body() assertNotNull(response) - webServer.getProjectCategories() - .execute() - .body() - ?.forEach { - assertNotNull(it) - assertNotNull(it.name) - assertNotNull(it.type) - assertNotNull(it.projectsList) - it.projectsList.forEach { projectResponse -> - assertNotNull(projectResponse.id) - assertNotNull(projectResponse.name) - assertNotNull(projectResponse.author) - assertNotNull(projectResponse.description) - assertNotNull(projectResponse.version) - assertNotNull(projectResponse.views) - assertNotNull(projectResponse.download) - assertNotNull(projectResponse.private) - assertNotNull(projectResponse.flavor) - assertNotNull(projectResponse.tags) - assertNotNull(projectResponse.uploaded) - assertNotNull(projectResponse.uploaded_string) - assertNotNull(projectResponse.screenshot_small) - assertTrue(projectResponse.screenshot_small.contains(Constants.DEFAULT_IMAGE_EXTENSION)) - assertNotNull(projectResponse.screenshot_large) - assertTrue(projectResponse.screenshot_large.contains(Constants.DEFAULT_IMAGE_EXTENSION)) - assertNotNull(projectResponse.project_url) - assertTrue(Patterns.WEB_URL.matcher(projectResponse.project_url).matches()) - assertNotNull(projectResponse.download_url) - assertTrue(Patterns.WEB_URL.matcher(projectResponse.download_url).matches()) - assertNotNull(projectResponse.filesize) - } + response?.data?.forEach { + assertNotNull(it) + assertNotNull(it.name) + assertNotNull(it.type) + assertNotNull(it.projectsList) + it.projectsList.forEach { projectResponse -> + assertNotNull(projectResponse.id) + assertNotNull(projectResponse.name) + assertNotNull(projectResponse.author) + assertNotNull(projectResponse.description) + assertNotNull(projectResponse.version) + assertNotNull(projectResponse.views) + assertNotNull(projectResponse.downloads) + assertNotNull(projectResponse.flavor) + assertNotNull(projectResponse.uploadedString) + assertNotNull(projectResponse.screenshot) + assertNotNull(projectResponse.projectUrl) + assertTrue(Patterns.WEB_URL.matcher(projectResponse.projectUrl).matches()) + assertNotNull(projectResponse.downloadUrl) + assertTrue(Patterns.WEB_URL.matcher(projectResponse.downloadUrl).matches()) + assertNotNull(projectResponse.filesize) } + } + } + + private fun enqueueOk(fixtureFile: String) { + mockWebServer.enqueue( + MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(MockResponseFileReader(context, fixtureFile).content) + ) } - private fun String.containsOkHttpCode() = contains(200.toString()) + private fun buildCleartextWebService(baseUrl: String): WebService = + Retrofit.Builder() + .baseUrl(baseUrl) + .client(OkHttpClient.Builder().build()) + .addConverterFactory(MoshiConverterFactory.create(CatroidWebServer.moshi)) + .build() + .create(WebService::class.java) } diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/test/web/AuthenticationCallsTest.java b/catroid/src/androidTest/java/org/catrobat/catroid/test/web/AuthenticationCallsTest.java deleted file mode 100644 index 9e1b7b2133c..00000000000 --- a/catroid/src/androidTest/java/org/catrobat/catroid/test/web/AuthenticationCallsTest.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.web; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.runner.Flaky; -import org.catrobat.catroid.test.utils.TestUtils; -import org.catrobat.catroid.testsuites.annotations.Cat; -import org.catrobat.catroid.transfers.DeleteTestUserTask; -import org.catrobat.catroid.web.CatrobatServerCalls; -import org.catrobat.catroid.web.CatrobatWebClient; -import org.catrobat.catroid.web.ServerAuthenticator; -import org.catrobat.catroid.web.ServerAuthenticator.TaskListener; -import org.catrobat.catroid.web.WebConnectionException; -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.mockito.Mockito; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import static junit.framework.Assert.assertTrue; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@Category(Cat.OutgoingNetworkTests.class) -@RunWith(AndroidJUnit4.class) -public class AuthenticationCallsTest implements DeleteTestUserTask.OnDeleteTestUserCompleteListener { - - private static final int STATUS_CODE_USER_PASSWORD_TOO_SHORT = 753; - private static final int STATUS_CODE_USER_ADD_EMAIL_EXISTS = 757; - private static final int STATUS_CODE_USER_EMAIL_INVALID = 765; - private static final int STATUS_CODE_AUTHENTICATION_FAILED = 601; - private static final int STATUS_CODE_TOKEN_FAILED = 1001; - private static final int STATUS_CODE_USERNAME_NOT_FOUND = 803; - private static final String BASE_URL_TEST_HTTPS = "https://develop-web.catrobat.ist.tugraz.at/app/"; - - private ServerAuthenticator authenticator; - private String token = Constants.NO_TOKEN; - private String testUser; - private String testEmail; - private TaskListener listenerMock; - private SharedPreferences sharedPreferences; - - @Before - public void setUp() throws Exception { - testUser = "testUser" + System.currentTimeMillis(); - testEmail = testUser + "@gmail.com"; - String testPassword = "pwspws"; - listenerMock = Mockito.mock(TaskListener.class); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext()); - authenticator = new ServerAuthenticator(testUser, testPassword, token, CatrobatWebClient.INSTANCE.getClient(), - BASE_URL_TEST_HTTPS, sharedPreferences, listenerMock); - } - - @After - public void tearDown() throws Exception { - TestUtils.deleteProjects("uploadtestProject"); - } - - @Test - @Flaky - public void testRegistrationOk() throws WebConnectionException { - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, never()).onError(anyInt(), anyString()); - verify(listenerMock, atLeastOnce()).onSuccess(); - - token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN); - assertTrue(new CatrobatServerCalls().checkToken(token, testUser, BASE_URL_TEST_HTTPS)); - } - - @Test - @Flaky - public void testRegisterWithExistingUser() { - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, never()).onError(anyInt(), anyString()); - verify(listenerMock, atLeastOnce()).onSuccess(); - - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, times(1)).onError(eq(STATUS_CODE_USER_ADD_EMAIL_EXISTS), Mockito.matches(".+")); - } - - @Test - @Flaky - public void testRegisterAndLogin() { - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, never()).onError(anyInt(), anyString()); - verify(listenerMock, atLeastOnce()).onSuccess(); - - authenticator.performCatrobatLogin(); - verify(listenerMock, never()).onError(anyInt(), anyString()); - } - - @Test - @Flaky - public void testLoginWithNotExistingUser() { - authenticator.performCatrobatLogin(); - verify(listenerMock, times(1)).onError(eq(STATUS_CODE_USERNAME_NOT_FOUND), Mockito.matches(".+")); - verify(listenerMock, never()).onSuccess(); - } - - @Test - @Flaky - public void testRegisterWithExistingUserAndLoginWithWrongPassword() { - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, never()).onError(anyInt(), anyString()); - verify(listenerMock, atLeastOnce()).onSuccess(); - - authenticator.setPassword("wrongPassword"); - authenticator.performCatrobatLogin(); - verify(listenerMock, times(1)).onError(eq(STATUS_CODE_AUTHENTICATION_FAILED), Mockito.matches(".+")); - } - - @Test - @Flaky - public void testRegisterWithNewUserButExistingEmail() { - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, never()).onError(anyInt(), anyString()); - verify(listenerMock, atLeastOnce()).onSuccess(); - - String newUser = "testUser" + System.currentTimeMillis(); - authenticator.setUsername(newUser); - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, times(1)).onError(eq(STATUS_CODE_USER_ADD_EMAIL_EXISTS), Mockito.matches(".+")); - } - - @Test - @Flaky - public void testRegisterWithTooShortPassword() { - authenticator.setPassword("short"); - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, times(1)).onError(eq(STATUS_CODE_USER_PASSWORD_TOO_SHORT), Mockito.matches(".+")); - verify(listenerMock, never()).onSuccess(); - } - - @Test - @Flaky - public void testRegisterWithInvalidEmail() { - authenticator.performCatrobatRegister("invalidEmail", "de", "at"); - verify(listenerMock, times(1)).onError(eq(STATUS_CODE_USER_EMAIL_INVALID), Mockito.matches(".+")); - verify(listenerMock, never()).onSuccess(); - } - - @Test - @Flaky - public void testCheckTokenWrong() { - String wrongToken = "blub"; - String username = "badUser"; - try { - boolean tokenOk = new CatrobatServerCalls().checkToken(wrongToken, username, BASE_URL_TEST_HTTPS); - - assertFalse(tokenOk); - } catch (WebConnectionException e) { - assertEquals(STATUS_CODE_TOKEN_FAILED, e.getStatusCode()); - assertNotNull(e.getMessage()); - assertThat(e.getMessage().length(), is(greaterThan(0))); - } - } - - @Test - @Flaky - public void testCheckTokenOk() throws WebConnectionException { - authenticator.performCatrobatRegister(testEmail, "de", "at"); - verify(listenerMock, never()).onError(anyInt(), anyString()); - verify(listenerMock, atLeastOnce()).onSuccess(); - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext()); - token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN); - assertTrue(new CatrobatServerCalls().checkToken(token, testUser, BASE_URL_TEST_HTTPS)); - } - - @Override - public void onDeleteTestUserComplete(Boolean deleted) { - assertTrue(deleted); - } -} diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/OpenFromShareLinkTest.java b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/OpenFromShareLinkTest.java index ab19b39c4c6..a349dbf4295 100644 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/OpenFromShareLinkTest.java +++ b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/OpenFromShareLinkTest.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -72,12 +72,12 @@ public class OpenFromShareLinkTest { @Parameterized.Parameters(name = "{0}") public static Iterable data() { return Arrays.asList(new Object[][] { - {"https://share.catrob.at/pocketcode/"}, - {"https://share.catrob.at/pocketcode/program/817?rec_by_page_id=1&rec_user_specific=0"}, - {"https://share.catrob.at/pocketcode/program/817"}, - {"https://share.catrob.at/luna/"}, - {"https://share.catrob.at/luna/program/817?rec_by_page_id=1&rec_user_specific=0"}, - {"https://share.catrob.at/luna/program/817"}, + {"https://share.catrobat.org/pocketcode/"}, + {"https://share.catrobat.org/pocketcode/program/817?rec_by_page_id=1&rec_user_specific=0"}, + {"https://share.catrobat.org/pocketcode/program/817"}, + {"https://share.catrobat.org/luna/"}, + {"https://share.catrobat.org/luna/program/817?rec_by_page_id=1&rec_user_specific=0"}, + {"https://share.catrobat.org/luna/program/817"}, }); } diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReplaceExistingProjectDialogTest.java b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReplaceExistingProjectDialogTest.java index 7784cf717b2..ebf0146c030 100644 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReplaceExistingProjectDialogTest.java +++ b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/dialog/ReplaceExistingProjectDialogTest.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -65,7 +65,7 @@ public class ReplaceExistingProjectDialogTest { String[] projectNames = {"Project1", "Project2", "Project3"}; - private static final String URL = "https://share.catrob.at/pocketcode/download/71489.catrobat?fname=Pet%20Simulator"; + private static final String URL = "https://share.catrobat.org/pocketcode/download/71489.catrobat?fname=Pet%20Simulator"; private ProjectDownloader.ProjectDownloadQueue queueMock = null; diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/MainMenuFragmentTest.kt b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/MainMenuFragmentTest.kt index ad728d80a2f..caca662b1c5 100644 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/MainMenuFragmentTest.kt +++ b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/fragment/MainMenuFragmentTest.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -45,11 +45,22 @@ import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest import org.catrobat.catroid.ProjectManager import org.catrobat.catroid.R import org.catrobat.catroid.common.Constants.CATROBAT_TERMS_OF_USE_ACCEPTED import org.catrobat.catroid.common.SharedPreferenceKeys.AGREED_TO_PRIVACY_POLICY_VERSION import org.catrobat.catroid.db.AppDatabase +import org.catrobat.catroid.retrofit.CatroidWebServer +import org.catrobat.catroid.retrofit.WebService +import org.catrobat.catroid.retrofittesting.MockResponseFileReader +import org.catrobat.catroid.sync.DefaultFeaturedProjectSync +import org.catrobat.catroid.sync.DefaultProjectsCategoriesSync import org.catrobat.catroid.sync.FeaturedProjectsSync import org.catrobat.catroid.sync.ProjectsCategoriesSync import org.catrobat.catroid.test.utils.TestUtils @@ -69,13 +80,35 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.koin.core.context.loadKoinModules +import org.koin.core.context.unloadKoinModules +import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.inject +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import java.net.HttpURLConnection @RunWith(AndroidJUnit4::class) class MainMenuFragmentTest : KoinTest { + + companion object { + private const val FEATURED_PROJECTS_FIXTURE = "featured_projects_success_response.json" + private const val PROJECTS_CATEGORIES_FIXTURE = "projects_categories_response.json" + } + private var privacyPreferenceSetting: Int = 0 private lateinit var applicationContext: Context + private lateinit var mockWebServer: MockWebServer + private val testKoinModule = module { + single(override = true) { buildCleartextWebService() } + single(override = true) { + DefaultFeaturedProjectSync(get(), get(), get()) + } + single(override = true) { + DefaultProjectsCategoriesSync(get(), get(), get()) + } + } private val connectionMonitor: NetworkConnectionMonitor by inject() private val appDatabase: AppDatabase by inject() @@ -105,11 +138,16 @@ class MainMenuFragmentTest : KoinTest { CATROBAT_TERMS_OF_USE_ACCEPTED ).commit() + startMockServer() + loadKoinModules(testKoinModule) + createProject() } @After fun tearDown() { + unloadKoinModules(testKoinModule) + mockWebServer.shutdown() TestUtils.deleteProjects(javaClass.simpleName) PreferenceManager.getDefaultSharedPreferences(applicationContext) .edit() @@ -130,15 +168,15 @@ class MainMenuFragmentTest : KoinTest { @Test fun testCatrobatCommunitySectionIsDisplayed() { syncBeforeLaunch() - onView(withId(R.id.featuredProjectsTextView)) + onView(withId(R.id.exploreShareTextView)) .check(matches(isDisplayed())) .check(matches(isClickable())) + assumeTrue("no featured projects available", featuredProjectsAdapter.itemCount > 0) + onView(withId(R.id.featuredProjectsRecyclerView)) .perform(scrollTo()) .check(matches(isDisplayed())) - - assumeTrue("seems there is no internet connection", featuredProjectsAdapter.itemCount > 0) } @Test @@ -225,9 +263,42 @@ class MainMenuFragmentTest : KoinTest { private fun syncBeforeLaunch(triggerSync: Boolean = true) { if (triggerSync) { + connectionMonitor.setValueTo(true) featuredProjectsSync.sync(true) projectsCategoriesSync.sync(true) } baseActivityTestRule.launchActivity(null) } + + private fun startMockServer() { + mockWebServer = MockWebServer() + mockWebServer.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + val path = request.path.orEmpty() + return when { + path.startsWith("/projects/featured") -> + okResponse(FEATURED_PROJECTS_FIXTURE) + path.startsWith("/projects/categories") -> + okResponse(PROJECTS_CATEGORIES_FIXTURE) + else -> MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + } + } + mockWebServer.start() + } + + private fun okResponse(fixtureFile: String): MockResponse { + val instrumentationContext = InstrumentationRegistry.getInstrumentation().context + return MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(MockResponseFileReader(instrumentationContext, fixtureFile).content) + } + + private fun buildCleartextWebService(): WebService = + Retrofit.Builder() + .baseUrl(mockWebServer.url("/").toString()) + .client(OkHttpClient.Builder().build()) + .addConverterFactory(MoshiConverterFactory.create(CatroidWebServer.moshi)) + .build() + .create(WebService::class.java) } diff --git a/catroid/src/catroid/java/org/catrobat/catroid/common/FlavoredConstants.java b/catroid/src/catroid/java/org/catrobat/catroid/common/FlavoredConstants.java index c03685f80d6..43c230285a4 100644 --- a/catroid/src/catroid/java/org/catrobat/catroid/common/FlavoredConstants.java +++ b/catroid/src/catroid/java/org/catrobat/catroid/common/FlavoredConstants.java @@ -30,37 +30,29 @@ import java.io.File; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; -import static org.catrobat.catroid.common.Constants.UPLOAD_URL; public final class FlavoredConstants { // Web: public static final String BASE_URL_HTTPS = "https://catrobat.org/docs/"; - - public static final String BASE_UPLOAD_URL = UPLOAD_URL + "/pocketcode/"; - public static final String CATROBAT_HELP_URL = "https://catrobat.org/docs/"; - public static final String CATEGORY_URL = BASE_URL_HTTPS + "#home-projects__"; + public static final String FLAVOR_NAME = "pocketcode"; - public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "Pocket Code"; + public static final String CATEGORY_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/#home-projects__"; - public static final String FLAVOR_NAME = "pocketcode"; + public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "Pocket Code"; public static final File DEFAULT_ROOT_DIRECTORY = CatroidApplication.getAppContext().getFilesDir(); public static final File EXTERNAL_STORAGE_ROOT_DIRECTORY = new File( Environment.getExternalStorageDirectory(), POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME); - // TODO: Delete these and every usage, when the Catrobat share server completely closes - // Deprecated Media Library: - public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/app/download-media/"; - - // Media Library: - public static final String CATROBAT_BASE_URL = "https://catrobat.org/"; - public static final String CATROBAT_CONTENT_DOWNLOAD_URL = CATROBAT_BASE_URL + "wp-content/"; - public static final String CATROBAT_CONTENT_LOOKS_URL = CATROBAT_BASE_URL + "figures-download/"; - public static final String CATROBAT_CONTENT_SOUNDS_URL = CATROBAT_BASE_URL + "sounds-download/"; - public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = CATROBAT_BASE_URL + "backgrounds-download/"; + // Media Library (via share.catrobat.org): + public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/media-library"; + public static final String CATROBAT_CONTENT_DOWNLOAD_URL = MAIN_URL_HTTPS + "/resources/media/"; + public static final String CATROBAT_CONTENT_LOOKS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE"; + public static final String CATROBAT_CONTENT_SOUNDS_URL = LIBRARY_BASE_URL + "?file_type=SOUND"; + public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE&category=backgrounds"; public static final String PRIVACY_POLICY_URL = "https://developer.catrobat.org/pages/legal/policies/privacy/"; private FlavoredConstants() { diff --git a/catroid/src/createAtSchool/java/org/catrobat/catroid/common/FlavoredConstants.java b/catroid/src/createAtSchool/java/org/catrobat/catroid/common/FlavoredConstants.java index ea2103bce5c..c4c0dd447e5 100644 --- a/catroid/src/createAtSchool/java/org/catrobat/catroid/common/FlavoredConstants.java +++ b/catroid/src/createAtSchool/java/org/catrobat/catroid/common/FlavoredConstants.java @@ -30,34 +30,29 @@ import java.io.File; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; -import static org.catrobat.catroid.common.Constants.UPLOAD_URL; public final class FlavoredConstants { // Web: public static final String BASE_URL_HTTPS = "https://catrobat.org/docs/"; - - public static final String BASE_UPLOAD_URL = UPLOAD_URL + "/create@school/"; - public static final String CATROBAT_HELP_URL = "https://catrobat.org/docs/"; - public static final String CATEGORY_URL = BASE_URL_HTTPS + "#home-projects__"; + public static final String FLAVOR_NAME = "create@school"; + + public static final String CATEGORY_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/#home-projects__"; public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "Create@School"; - public static final String FLAVOR_NAME = "create@school"; - public static final File DEFAULT_ROOT_DIRECTORY = CatroidApplication.getAppContext().getFilesDir(); public static final File EXTERNAL_STORAGE_ROOT_DIRECTORY = new File( Environment.getExternalStorageDirectory().getAbsolutePath(), POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME); - // Media Library: - public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/app/download-media/"; - public static final String CATROBAT_BASE_URL = "https://catrobat.org/"; - public static final String CATROBAT_CONTENT_DOWNLOAD_URL = CATROBAT_BASE_URL + "wp-content/"; - public static final String CATROBAT_CONTENT_LOOKS_URL = CATROBAT_BASE_URL + "figures-download/"; - public static final String CATROBAT_CONTENT_SOUNDS_URL = CATROBAT_BASE_URL + "sounds-download/"; - public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = CATROBAT_BASE_URL + "backgrounds-download/"; + // Media Library (via share.catrobat.org): + public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/media-library"; + public static final String CATROBAT_CONTENT_DOWNLOAD_URL = MAIN_URL_HTTPS + "/resources/media/"; + public static final String CATROBAT_CONTENT_LOOKS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE"; + public static final String CATROBAT_CONTENT_SOUNDS_URL = LIBRARY_BASE_URL + "?file_type=SOUND"; + public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE&category=backgrounds"; public static final String PRIVACY_POLICY_URL = "https://developer.catrobat.org/pages/legal/policies/privacy/"; private FlavoredConstants() { diff --git a/catroid/src/debug/java/org/catrobat/catroid/ui/ProjectUploadTestActivity.kt b/catroid/src/debug/java/org/catrobat/catroid/ui/ProjectUploadTestActivity.kt index 53fca1e438e..5b884f70f99 100644 --- a/catroid/src/debug/java/org/catrobat/catroid/ui/ProjectUploadTestActivity.kt +++ b/catroid/src/debug/java/org/catrobat/catroid/ui/ProjectUploadTestActivity.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2024 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -33,7 +33,7 @@ class ProjectUploadTestActivity : ProjectUploadActivity() { } override fun verifyUserIdentity() { - onTokenCheckComplete(true, false) + onCreateView() } fun projectUploadController(): ProjectUploadController? = projectUploadController diff --git a/catroid/src/embroideryDesigner/java/org/catrobat/catroid/common/FlavoredConstants.java b/catroid/src/embroideryDesigner/java/org/catrobat/catroid/common/FlavoredConstants.java index 997a66c9dcb..909240d4b28 100644 --- a/catroid/src/embroideryDesigner/java/org/catrobat/catroid/common/FlavoredConstants.java +++ b/catroid/src/embroideryDesigner/java/org/catrobat/catroid/common/FlavoredConstants.java @@ -30,34 +30,29 @@ import java.io.File; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; -import static org.catrobat.catroid.common.Constants.UPLOAD_URL; public final class FlavoredConstants { // Web: public static final String BASE_URL_HTTPS = "https://catrobat.org/docs/"; - - public static final String BASE_UPLOAD_URL = UPLOAD_URL + "/embroidery/"; - public static final String CATROBAT_HELP_URL = "https://catrobat.org/docs/"; - public static final String CATEGORY_URL = BASE_URL_HTTPS + "#home-projects__"; + public static final String FLAVOR_NAME = "embroidery"; + + public static final String CATEGORY_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/#home-projects__"; public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "EmbroideryDesigner"; - public static final String FLAVOR_NAME = "embroidery"; - public static final File DEFAULT_ROOT_DIRECTORY = CatroidApplication.getAppContext().getFilesDir(); public static final File EXTERNAL_STORAGE_ROOT_DIRECTORY = new File( Environment.getExternalStorageDirectory().getAbsolutePath(), POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME); - // Media Library: - public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/app/download-media/"; - public static final String CATROBAT_BASE_URL = "https://catrobat.org/"; - public static final String CATROBAT_CONTENT_DOWNLOAD_URL = CATROBAT_BASE_URL + "wp-content/"; - public static final String CATROBAT_CONTENT_LOOKS_URL = CATROBAT_BASE_URL + "figures-download/"; - public static final String CATROBAT_CONTENT_SOUNDS_URL = CATROBAT_BASE_URL + "sounds-download/"; - public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = CATROBAT_BASE_URL + "backgrounds-download/"; + // Media Library (via share.catrobat.org): + public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/media-library"; + public static final String CATROBAT_CONTENT_DOWNLOAD_URL = MAIN_URL_HTTPS + "/resources/media/"; + public static final String CATROBAT_CONTENT_LOOKS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE"; + public static final String CATROBAT_CONTENT_SOUNDS_URL = LIBRARY_BASE_URL + "?file_type=SOUND"; + public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE&category=backgrounds"; public static final String PRIVACY_POLICY_URL = "https://developer.catrobat.org/pages/legal/policies/privacy/"; private FlavoredConstants() { diff --git a/catroid/src/lunaAndCat/java/org/catrobat/catroid/common/FlavoredConstants.java b/catroid/src/lunaAndCat/java/org/catrobat/catroid/common/FlavoredConstants.java index ee8914d2b30..785fb8e3c67 100644 --- a/catroid/src/lunaAndCat/java/org/catrobat/catroid/common/FlavoredConstants.java +++ b/catroid/src/lunaAndCat/java/org/catrobat/catroid/common/FlavoredConstants.java @@ -30,34 +30,29 @@ import java.io.File; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; -import static org.catrobat.catroid.common.Constants.UPLOAD_URL; public final class FlavoredConstants { // Web: public static final String BASE_URL_HTTPS = "https://catrobat.org/docs/"; - - public static final String BASE_UPLOAD_URL = UPLOAD_URL + "/luna/"; - public static final String CATROBAT_HELP_URL = "https://catrobat.org/docs/"; - public static final String CATEGORY_URL = BASE_URL_HTTPS + "#home-projects__"; + public static final String FLAVOR_NAME = "luna"; + + public static final String CATEGORY_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/#home-projects__"; public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "Luna&Cat"; - public static final String FLAVOR_NAME = "luna"; - public static final File DEFAULT_ROOT_DIRECTORY = CatroidApplication.getAppContext().getFilesDir(); public static final File EXTERNAL_STORAGE_ROOT_DIRECTORY = new File( Environment.getExternalStorageDirectory().getAbsolutePath(), POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME); - // Media Library: - public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/app/download-media/"; - public static final String CATROBAT_BASE_URL = "https://catrobat.org/"; - public static final String CATROBAT_CONTENT_DOWNLOAD_URL = CATROBAT_BASE_URL + "wp-content/"; - public static final String CATROBAT_CONTENT_LOOKS_URL = CATROBAT_BASE_URL + "figures-download/"; - public static final String CATROBAT_CONTENT_SOUNDS_URL = CATROBAT_BASE_URL + "sounds-download/"; - public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = CATROBAT_BASE_URL + "backgrounds-download/"; + // Media Library (via share.catrobat.org): + public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/media-library"; + public static final String CATROBAT_CONTENT_DOWNLOAD_URL = MAIN_URL_HTTPS + "/resources/media/"; + public static final String CATROBAT_CONTENT_LOOKS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE"; + public static final String CATROBAT_CONTENT_SOUNDS_URL = LIBRARY_BASE_URL + "?file_type=SOUND"; + public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE&category=backgrounds"; public static final String PRIVACY_POLICY_URL = "https://developer.catrobat.org/pages/legal/policies/privacy/"; private FlavoredConstants() { diff --git a/catroid/src/main/java/org/catrobat/catroid/ProjectManager.java b/catroid/src/main/java/org/catrobat/catroid/ProjectManager.java index c8c62e5591e..15b084ff644 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ProjectManager.java +++ b/catroid/src/main/java/org/catrobat/catroid/ProjectManager.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -698,9 +698,8 @@ public void saveDownloadedProjects() { public void loadDownloadedProjects() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); Gson gson = new Gson(); - String json = null; if (sharedPreferences != null) { - json = sharedPreferences.getString(downloadedProjectsName, null); + String json = sharedPreferences.getString(downloadedProjectsName, null); if (json != null) { Type type = new TypeToken>() { }.getType(); diff --git a/catroid/src/main/java/org/catrobat/catroid/common/Constants.java b/catroid/src/main/java/org/catrobat/catroid/common/Constants.java index b52634e04e4..3d6f695c867 100644 --- a/catroid/src/main/java/org/catrobat/catroid/common/Constants.java +++ b/catroid/src/main/java/org/catrobat/catroid/common/Constants.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -37,7 +37,6 @@ import androidx.annotation.IntDef; import androidx.exifinterface.media.ExifInterface; -import static org.catrobat.catroid.common.FlavoredConstants.BASE_URL_HTTPS; import static org.catrobat.catroid.common.FlavoredConstants.DEFAULT_ROOT_DIRECTORY; public final class Constants { @@ -48,7 +47,8 @@ public final class Constants { public static final int CAST_NOT_SEEING_DEVICE_TIMEOUT = 3000; //in public static final long PROGESSIVE_INPUT_DELAY = 400; public static final long PROGESSIVE_INPUT_COUNTDOWN_INTERVALL = 500; - public static final long RETROFIT_WRITE_TIMEOUT = 15; + public static final long RETROFIT_WRITE_TIMEOUT = 300; + public static final long RETROFIT_CONNECT_TIMEOUT = 15; public static final String PLATFORM_NAME = "Android"; public static final int APPLICATION_BUILD_NUMBER = 0; // updated from jenkins nightly/release build @@ -126,15 +126,14 @@ public final class Constants { public static final String TEXT_TO_SPEECH_TMP_PATH = TMP_PATH + "/textToSpeech"; // Web: - private static final String MAIN_URL_PRODUCTION = "https://share.catrob.at"; - public static final String UPLOAD_URL = "https://upload.catrob.at"; + private static final String MAIN_URL_PRODUCTION = "https://share.catrobat.org"; private static final String WEB_TEST_URL = BuildConfig.WEB_TEST_URL; public static final String MAIN_URL_HTTPS = BuildConfig.WEB_TEST_FLAG ? WEB_TEST_URL : MAIN_URL_PRODUCTION; - // Default "flavor" in the web which equals "pocketcode" - public static final String BASE_APP_URL_HTTPS = MAIN_URL_HTTPS + "/app/"; + public static final String API_BASE_URL = MAIN_URL_HTTPS + "/api/"; + public static final String BASE_APP_URL_HTTPS = MAIN_URL_HTTPS + "/" + FlavoredConstants.FLAVOR_NAME + "/"; - public static final String SHARE_PROJECT_URL = BASE_URL_HTTPS + "/project/"; + public static final String SHARE_PROJECT_URL = BASE_APP_URL_HTTPS + "project/"; public static final String CATROBAT_ABOUT_URL = "https://www.catrobat.org/"; public static final String CATROBAT_FORMULA_WIKI_URL = "https://catrobat.org/docs/"; @@ -146,7 +145,7 @@ public final class Constants { public static final String CATROBAT_LOGIC_WIKI_URL = "https://catrobat.org/docs/"; public static final String CATROBAT_SENSORS_WIKI_URL = "https://catrobat.org/docs/"; public static final String CATROBAT_OBJECT_WIKI_URL = "https://catrobat.org/docs/"; - public static final String CATROBAT_DELETE_ACCOUNT_URL = BASE_URL_HTTPS + "profile/edit"; + public static final String CATROBAT_DELETE_ACCOUNT_URL = BASE_APP_URL_HTTPS + "profile/edit"; public static final String CATROBAT_TERMS_OF_USE_TOKEN_FLAVOR_URL = "?flavorName="; public static final String CATROBAT_TERMS_OF_USE_TOKEN_VERSION_URL = "&versionCode="; public static final int CATROBAT_TERMS_OF_USE_ACCEPTED = 1; @@ -154,9 +153,6 @@ public final class Constants { public static final String PLAY_STORE_PAGE_LINK = "https://play.google.com/store/apps/details?id="; public static final String HUAWEI_APP_GALLERY_LINK = "https://catrob.at/HuaweiAppGallery"; - public static final String USERNAME_COOKIE_NAME = "CATRO_LOGIN_USER"; - public static final String TOKEN_COOKIE_NAME = "CATRO_LOGIN_TOKEN"; - public static final String USER_AGENT = "Mozilla/5.0 (compatible; Catrobatbot/1.0; +https://catrob.at/bot)"; // HTTP status codes: @@ -267,6 +263,8 @@ public final class Constants { public static final String EXTRA_UPLOAD_NAME = "uploadName"; public static final int UPLOAD_RESULT_RECEIVER_RESULT_CODE = 1; + public static final int UPLOAD_PROGRESS_RESULT_CODE = 2; + public static final String EXTRA_UPLOAD_PROGRESS = "uploadProgress"; //Various: public static final int BUFFER_8K = 8 * 1024; @@ -299,6 +297,7 @@ public final class Constants { public static final int UPLOAD_IMAGE_SCALE_WIDTH = 480; public static final int UPLOAD_IMAGE_SCALE_HEIGHT = 480; + public static final long UPLOAD_MAX_SIZE_BYTES = 100L * 1024 * 1024; // 100 MB public static final int TEXT_FROM_CAMERA_SENSOR_HASHCODE = 1613638780; diff --git a/catroid/src/main/java/org/catrobat/catroid/common/Survey.java b/catroid/src/main/java/org/catrobat/catroid/common/Survey.java index a3d0640ea13..39f67e464ae 100644 --- a/catroid/src/main/java/org/catrobat/catroid/common/Survey.java +++ b/catroid/src/main/java/org/catrobat/catroid/common/Survey.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -27,22 +27,25 @@ import android.content.Intent; import android.preference.PreferenceManager; import android.text.format.DateUtils; +import androidx.annotation.VisibleForTesting; -import org.catrobat.catroid.transfers.GetSurveyTask; +import org.catrobat.catroid.retrofit.WebService; +import org.catrobat.catroid.retrofit.models.SurveyResponse; import org.catrobat.catroid.ui.WebViewActivity; import org.catrobat.catroid.utils.Utils; import java.util.Date; -import androidx.annotation.VisibleForTesting; +import kotlin.Lazy; import static org.catrobat.catroid.common.SharedPreferenceKeys.LAST_USED_DATE_KEY; import static org.catrobat.catroid.common.SharedPreferenceKeys.SHOW_SURVEY_KEY; import static org.catrobat.catroid.common.SharedPreferenceKeys.SURVEY_URL1_HASH_KEY; import static org.catrobat.catroid.common.SharedPreferenceKeys.SURVEY_URL2_HASH_KEY; import static org.catrobat.catroid.common.SharedPreferenceKeys.TIME_SPENT_IN_APP_IN_SECONDS_KEY; +import static org.koin.java.KoinJavaComponent.inject; -public class Survey implements GetSurveyTask.SurveyResponseListener { +public class Survey { @VisibleForTesting public static final int MINIMUM_TIME_SPENT_IN_APP_IN_SECONDS = 60 * 60; @@ -77,7 +80,9 @@ public void startAppTime(Context context) { public void endAppTime(Context context) { if (!fulfilledSurveyRequirements) { - sessionTimeSpentInIdeInSeconds = (System.currentTimeMillis() - sessionStartTimeInMilliseconds - sessionTimeSpentInStageInMilliseconds) / 1000; + long elapsed = System.currentTimeMillis() - sessionStartTimeInMilliseconds + - sessionTimeSpentInStageInMilliseconds; + sessionTimeSpentInIdeInSeconds = elapsed / 1000; sessionStartTimeInMilliseconds = 0; long oldTimeSpentInApp = PreferenceManager.getDefaultSharedPreferences(context) @@ -145,12 +150,23 @@ public void showSurvey(Context context) { @VisibleForTesting public void getSurvey(Context context) { - GetSurveyTask getSurveyTask = new GetSurveyTask(context); - getSurveyTask.setOnSurveyResponseListener(this); - getSurveyTask.execute(); + Lazy webServiceLazy = inject(WebService.class); + String langCode = java.util.Locale.getDefault().getLanguage(); + new Thread(() -> { + try { + retrofit2.Response response = + webServiceLazy.getValue().getSurvey(langCode, "android").execute(); + if (response.isSuccessful() && response.body() != null && response.body().getUrl() != null) { + String surveyUrl = response.body().getUrl(); + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> + onSurveyReceived(context, surveyUrl)); + } + } catch (Exception ignored) { + android.util.Log.d("Survey", "Survey fetch skipped", ignored); + } + }).start(); } - @Override public void onSurveyReceived(Context context, String surveyUrl) { if (isUrlNew(context, surveyUrl)) { Intent intent = new Intent(context, WebViewActivity.class); diff --git a/catroid/src/main/java/org/catrobat/catroid/content/XmlHeader.java b/catroid/src/main/java/org/catrobat/catroid/content/XmlHeader.java index 4116210a771..8f07384ee19 100644 --- a/catroid/src/main/java/org/catrobat/catroid/content/XmlHeader.java +++ b/catroid/src/main/java/org/catrobat/catroid/content/XmlHeader.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -55,7 +55,7 @@ public class XmlHeader implements Serializable { private String listeningLanguageTag = ""; //============================================================================================== - // mutable fields only used by Catroweb (share.catrob.at website) so far + // mutable fields only used by Catroweb (share.catrobat.org website) so far //============================================================================================== private String applicationBuildName = ""; private int applicationBuildNumber = 0; @@ -75,7 +75,7 @@ public class XmlHeader implements Serializable { // *** CATROBAT REMIX SPECIFICATION REQUIREMENT *** // // Keep in mind that the remixGrandparentsUrlString-field (respectively remixOf-XML-field) - // (see below) is used by Catroweb's web application "share.catrob.at" only. + // (see below) is used by Catroweb's web application "share.catrobat.org" only. // Once new Catrobat programs get uploaded, Catroweb automatically updates this XML-field // and sets the program as being remixed! // In order to do so, Catroweb takes the value from the url-XML-field (see above) and assigns 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 f62f5de834c..bf6f666625c 100644 --- a/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt +++ b/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt @@ -31,6 +31,8 @@ import androidx.work.WorkManager import org.catrobat.catroid.ProjectManager import org.catrobat.catroid.db.AppDatabase import org.catrobat.catroid.db.DatabaseMigrations +import org.catrobat.catroid.retrofit.AuthInterceptor +import org.catrobat.catroid.retrofit.AuthService import org.catrobat.catroid.retrofit.CatroidWebServer import org.catrobat.catroid.stage.HmsSpeechRecognitionHolder import org.catrobat.catroid.stage.SpeechRecognitionHolder @@ -51,6 +53,10 @@ import org.catrobat.catroid.ui.recyclerview.repository.ProjectCategoriesReposito import org.catrobat.catroid.ui.recyclerview.viewmodel.MainFragmentViewModel import org.catrobat.catroid.utils.MobileServiceAvailability import org.catrobat.catroid.utils.NetworkConnectionMonitor +import org.catrobat.catroid.web.CatrobatWebClient +import org.catrobat.catroid.web.DownloadClient +import org.catrobat.catroid.web.JwtTokenStore +import org.catrobat.catroid.web.LoginRepository import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.androidx.viewmodel.dsl.viewModel @@ -59,14 +65,32 @@ import org.koin.core.logger.Level import org.koin.core.module.Module import org.koin.dsl.module +private val API_BASE_URL = org.catrobat.catroid.common.Constants.API_BASE_URL + val componentsModules = module(createdAtStart = true, override = false) { single { Room.databaseBuilder(androidContext(), AppDatabase::class.java, "app_database") - .addMigrations(DatabaseMigrations.MIGRATION_1_2) - .addMigrations(DatabaseMigrations.MIGRATION_2_3) + .addMigrations(DatabaseMigrations.MIGRATION_1_2, DatabaseMigrations.MIGRATION_2_3) .build() } - single { CatroidWebServer.getWebService("https://share.catrob.at/api/") } + + single { JwtTokenStore(androidContext()) } + + // Auth service (no auth interceptor — avoids refresh loop) + single { CatroidWebServer.getAuthService(API_BASE_URL) } + + // Auth interceptor + single { AuthInterceptor(get(), API_BASE_URL) } + + // Login repository + single { LoginRepository(get(), get()) } + + // WebService with auth interceptor for authenticated API calls + single { CatroidWebServer.getWebService(API_BASE_URL, listOf(get())) } + + // Download client (uses base OkHttp — no auth needed for public downloads) + single { DownloadClient(CatrobatWebClient.client) } + factory { WorkManager.getInstance(androidContext()) } single { ProjectManager(androidContext()) } single { NetworkConnectionMonitor(androidContext()) } diff --git a/catroid/src/main/java/org/catrobat/catroid/retrofit/AuthInterceptor.kt b/catroid/src/main/java/org/catrobat/catroid/retrofit/AuthInterceptor.kt new file mode 100644 index 00000000000..7be58ae0c10 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/AuthInterceptor.kt @@ -0,0 +1,130 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.retrofit + +import android.util.Log +import okhttp3.Interceptor +import okhttp3.Response +import org.catrobat.catroid.retrofit.models.AuthResponse +import org.catrobat.catroid.retrofit.models.RefreshRequest +import org.catrobat.catroid.web.JwtTokenStore +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.http.Body +import retrofit2.http.POST +import java.io.IOException + +class AuthInterceptor( + private val tokenStore: JwtTokenStore, + private val baseUrl: String +) : Interceptor { + + private val lock = Any() + + private interface SyncRefreshService { + @POST("authentication/refresh") + fun refreshToken(@Body body: RefreshRequest): Call + } + + private val refreshService: SyncRefreshService by lazy { + Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(MoshiConverterFactory.create(CatroidWebServer.moshi)) + .build() + .create(SyncRefreshService::class.java) + } + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + val token = tokenStore.getAccessToken() + val request = if (token != null) { + originalRequest.newBuilder() + .header(HEADER_AUTHORIZATION, bearer(token)) + .build() + } else { + originalRequest + } + + val response = chain.proceed(request) + + if (response.code() != HTTP_UNAUTHORIZED || token == null) { + return response + } + + synchronized(lock) { + val currentToken = tokenStore.getAccessToken() + if (currentToken != null && currentToken != token) { + response.close() + val retryRequest = originalRequest.newBuilder() + .header(HEADER_AUTHORIZATION, bearer(currentToken)) + .build() + return chain.proceed(retryRequest) + } + if (currentToken == null) { + tokenStore.clearTokens() + return response + } + + val refreshToken = tokenStore.getRefreshToken() ?: run { + tokenStore.clearTokens() + return response + } + + return try { + val refreshResponse = refreshService + .refreshToken(RefreshRequest(refreshToken)) + .execute() + + if (refreshResponse.isSuccessful && refreshResponse.body() != null) { + val body = refreshResponse.body()!! + tokenStore.setTokens(body.token, body.refreshToken) + + response.close() + val retryRequest = originalRequest.newBuilder() + .header(HEADER_AUTHORIZATION, bearer(body.token)) + .build() + chain.proceed(retryRequest) + } else { + Log.w(TAG, "Token refresh returned ${refreshResponse.code()}") + tokenStore.clearTokens() + response + } + } catch (e: IOException) { + Log.w(TAG, "Token refresh failed", e) + tokenStore.clearTokens() + response + } + } + } + + companion object { + private const val TAG = "AuthInterceptor" + private const val HTTP_UNAUTHORIZED = 401 + private const val HEADER_AUTHORIZATION = "Authorization" + private const val BEARER_PREFIX = "Bearer " + private fun bearer(token: String) = BEARER_PREFIX + token + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/retrofit/AuthService.kt b/catroid/src/main/java/org/catrobat/catroid/retrofit/AuthService.kt new file mode 100644 index 00000000000..1111c1d846c --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/AuthService.kt @@ -0,0 +1,53 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.retrofit + +import org.catrobat.catroid.retrofit.models.AuthResponse +import org.catrobat.catroid.retrofit.models.LoginRequest +import org.catrobat.catroid.retrofit.models.OAuthLoginRequest +import org.catrobat.catroid.retrofit.models.RefreshRequest +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +interface AuthService { + + @POST("authentication") + suspend fun login(@Body body: LoginRequest): AuthResponse + + @POST("authentication/refresh") + suspend fun refreshToken(@Body body: RefreshRequest): AuthResponse + + @POST("authentication/oauth") + suspend fun oauthLogin(@Body body: OAuthLoginRequest): AuthResponse + + @GET("authentication") + suspend fun checkToken(@Header("Authorization") bearer: String): Response + + @DELETE("authentication") + suspend fun logout(@Header("Authorization") bearer: String): Response +} diff --git a/catroid/src/main/java/org/catrobat/catroid/retrofit/ErrorInterceptor.kt b/catroid/src/main/java/org/catrobat/catroid/retrofit/ErrorInterceptor.kt index e77ee06ba69..39bd76cc5ef 100644 --- a/catroid/src/main/java/org/catrobat/catroid/retrofit/ErrorInterceptor.kt +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/ErrorInterceptor.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -34,7 +34,7 @@ class ErrorInterceptor : Interceptor { if (response.isSuccessful.not() and response.isRedirect.not()) { val contentType = response.body()?.contentType() - val body = response.body()?.toString() ?: "" + val body = response.body()?.string() ?: "" return response.newBuilder() .body(ResponseBody.create(contentType, body)) diff --git a/catroid/src/main/java/org/catrobat/catroid/retrofit/FlexibleMapAdapter.kt b/catroid/src/main/java/org/catrobat/catroid/retrofit/FlexibleMapAdapter.kt new file mode 100644 index 00000000000..8839c5abbe4 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/FlexibleMapAdapter.kt @@ -0,0 +1,95 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.retrofit + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonQualifier +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import java.lang.reflect.Type + +/** + * Qualifier for Map fields that may arrive as [] (empty JSON array) instead of {} + * from the Catroweb PHP API. PHP's json_encode returns [] for empty associative arrays + * and {"key":"value"} for non-empty ones. + */ +@Retention(AnnotationRetention.RUNTIME) +@JsonQualifier +annotation class FlexibleMap + +class FlexibleMapAdapter : JsonAdapter?>() { + + override fun fromJson(reader: JsonReader): Map? { + return when (reader.peek()) { + JsonReader.Token.BEGIN_ARRAY -> { + reader.skipValue() + emptyMap() + } + JsonReader.Token.BEGIN_OBJECT -> { + val result = mutableMapOf() + reader.beginObject() + while (reader.hasNext()) { + result[reader.nextName()] = reader.nextString() + } + reader.endObject() + result + } + JsonReader.Token.NULL -> { + reader.nextNull() + null + } + else -> { + reader.skipValue() + null + } + } + } + + override fun toJson(writer: JsonWriter, value: Map?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for ((k, v) in value) { + writer.name(k).value(v) + } + writer.endObject() + } + + companion object { + val FACTORY = object : Factory { + override fun create( + type: Type, + annotations: Set, + moshi: Moshi + ): JsonAdapter<*>? { + Types.nextAnnotations(annotations, FlexibleMap::class.java) ?: return null + return FlexibleMapAdapter() + } + } + } +} 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 d5286cf8fc7..f483e2d9ace 100644 --- a/catroid/src/main/java/org/catrobat/catroid/retrofit/RetrofitWebServer.kt +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/RetrofitWebServer.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -26,14 +26,30 @@ package org.catrobat.catroid.retrofit import okhttp3.ConnectionSpec import okhttp3.OkHttpClient import org.catrobat.catroid.common.Constants.CURRENT_CATROBAT_LANGUAGE_VERSION +import org.catrobat.catroid.common.Constants.RETROFIT_CONNECT_TIMEOUT import org.catrobat.catroid.common.Constants.RETROFIT_WRITE_TIMEOUT import org.catrobat.catroid.common.FlavoredConstants.FLAVOR_NAME -import org.catrobat.catroid.retrofit.models.FeaturedProject -import org.catrobat.catroid.retrofit.models.ProjectsCategoryApi +import org.catrobat.catroid.retrofit.models.CursorPaginatedResponse +import org.catrobat.catroid.retrofit.models.FeaturedProjectApi +import org.catrobat.catroid.retrofit.models.ProjectResponseApi +import org.catrobat.catroid.retrofit.models.ProjectUploadResponse +import org.catrobat.catroid.retrofit.models.ProjectsCategoryListResponse +import org.catrobat.catroid.retrofit.models.MediaAssetResponse +import org.catrobat.catroid.retrofit.models.MediaCategoryPreview +import org.catrobat.catroid.retrofit.models.SurveyResponse +import org.catrobat.catroid.retrofit.models.TagsResponse import retrofit2.Call import retrofit2.Retrofit +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.Path import retrofit2.http.Query import java.util.Locale import java.util.concurrent.TimeUnit @@ -45,21 +61,86 @@ interface WebService { @Query("flavor") flavor: String = FLAVOR_NAME, @Query("platform") platform: String = "android", @Query("limit") limit: Int = 20, - @Query("offset") offset: Int = 0 - ): Call> + @Query("cursor") cursor: String? = null + ): Call> @GET("projects/categories") fun getProjectCategories( @Query("max_version") maxVersion: String = CURRENT_CATROBAT_LANGUAGE_VERSION.toString(), @Query("flavor") flavor: String = FLAVOR_NAME - ): Call> + ): Call + + @GET("projects") + suspend fun getProjectsByCategory( + @Query("category") category: String, + @Query("max_version") maxVersion: String = CURRENT_CATROBAT_LANGUAGE_VERSION.toString(), + @Query("flavor") flavor: String = FLAVOR_NAME, + @Query("limit") limit: Int = 20, + @Query("cursor") cursor: String? = null + ): CursorPaginatedResponse + + @GET("projects/search") + suspend fun searchProjects( + @Query("query") query: String, + @Query("max_version") maxVersion: String = CURRENT_CATROBAT_LANGUAGE_VERSION.toString(), + @Query("flavor") flavor: String = FLAVOR_NAME, + @Query("limit") limit: Int = 20, + @Query("cursor") cursor: String? = null + ): CursorPaginatedResponse + + @Multipart + @POST("projects") + fun uploadProject( + @Part file: MultipartBody.Part, + @Part("checksum") checksum: RequestBody, + @Part("flavor") flavor: RequestBody? = null, + @Part("private") private: RequestBody? = null, + @Part("project_id") projectId: RequestBody? = null + ): Call + + @GET("projects/tags") + fun getProjectTags(): Call + + @GET("survey/{langCode}") + fun getSurvey( + @Path("langCode") langCode: String, + @Query("platform") platform: String = "android" + ): Call + + @GET("media/library") + @Suppress("LongParameterList") + suspend fun getMediaLibrary( + @Query("file_type") fileType: String? = null, + @Query("flavor") flavor: String? = null, + @Query("search") search: String? = null, + @Query("assets_per_category") assetsPerCategory: Int = 5, + @Query("limit") limit: Int = 20, + @Query("cursor") cursor: String? = null + ): CursorPaginatedResponse + + @GET("media/assets") + @Suppress("LongParameterList") + suspend fun getMediaAssets( + @Query("category_id") categoryId: String? = null, + @Query("file_type") fileType: String? = null, + @Query("flavor") flavor: String? = null, + @Query("search") search: String? = null, + @Query("limit") limit: Int = 20, + @Query("cursor") cursor: String? = null + ): CursorPaginatedResponse } class CatroidWebServer private constructor() { companion object { - @JvmStatic - fun getWebService(baseUrl: String): WebService { - val okHttpClient = OkHttpClient.Builder() + val moshi: Moshi = Moshi.Builder() + .add(FlexibleMapAdapter.FACTORY) + .addLast(KotlinJsonAdapterFactory()) + .build() + + private fun baseHttpClientBuilder(): OkHttpClient.Builder = + OkHttpClient.Builder() + .connectTimeout(RETROFIT_CONNECT_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(RETROFIT_WRITE_TIMEOUT, TimeUnit.SECONDS) .writeTimeout(RETROFIT_WRITE_TIMEOUT, TimeUnit.SECONDS) .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) .addInterceptor { chain -> @@ -70,15 +151,34 @@ class CatroidWebServer private constructor() { .build() chain.proceed(request) } - .addInterceptor(ErrorInterceptor()) - .build() + + @JvmStatic + fun getWebService( + baseUrl: String, + additionalInterceptors: List = emptyList() + ): WebService { + val builder = baseHttpClientBuilder() + + additionalInterceptors.forEach { builder.addInterceptor(it) } + + builder.addInterceptor(ErrorInterceptor()) return Retrofit.Builder() .baseUrl(baseUrl) - .client(okHttpClient) - .addConverterFactory(MoshiConverterFactory.create()) + .client(builder.build()) + .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() .create(WebService::class.java) } + + @JvmStatic + fun getAuthService(baseUrl: String): AuthService { + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(baseHttpClientBuilder().build()) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(AuthService::class.java) + } } } diff --git a/catroid/src/main/java/org/catrobat/catroid/retrofit/models/AuthModels.kt b/catroid/src/main/java/org/catrobat/catroid/retrofit/models/AuthModels.kt new file mode 100644 index 00000000000..2cea6aca9d8 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/models/AuthModels.kt @@ -0,0 +1,62 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.retrofit.models + +import com.squareup.moshi.Json + +data class LoginRequest( + val username: String, + val password: String +) + +data class RefreshRequest( + @Json(name = "refresh_token") val refreshToken: String +) + +data class OAuthLoginRequest( + @Json(name = "id_token") val idToken: String, + @Json(name = "resource_owner") val resourceOwner: String +) + +data class AuthResponse( + val token: String, + @Json(name = "refresh_token") val refreshToken: String, + val username: String? = null +) + +data class ApiErrorResponse( + val error: ApiErrorDetail +) + +data class ApiErrorDetail( + val code: Int, + val type: String, + val message: String, + val details: List? = null +) + +data class ApiFieldError( + val field: String, + val message: String +) diff --git a/catroid/src/main/java/org/catrobat/catroid/retrofit/models/Models.kt b/catroid/src/main/java/org/catrobat/catroid/retrofit/models/Models.kt index 64bab98130a..7eb1901ce7b 100644 --- a/catroid/src/main/java/org/catrobat/catroid/retrofit/models/Models.kt +++ b/catroid/src/main/java/org/catrobat/catroid/retrofit/models/Models.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -23,21 +23,82 @@ package org.catrobat.catroid.retrofit.models +import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.Relation +import com.squareup.moshi.Json +import org.catrobat.catroid.retrofit.FlexibleMap -@SuppressWarnings("ConstructorParameterNaming") +// --- Cursor pagination envelope --- + +data class CursorPaginatedResponse( + val data: List, + @Json(name = "next_cursor") val nextCursor: String? = null, + @Json(name = "has_more") val hasMore: Boolean = false +) + +// --- Image variants (responsive images from API v2) --- + +data class ImageVariantSet( + @Json(name = "avif_1x") val avif1x: String? = null, + @Json(name = "avif_2x") val avif2x: String? = null, + @Json(name = "webp_1x") val webp1x: String? = null, + @Json(name = "webp_2x") val webp2x: String? = null +) + +fun ImageVariantSet.getBestUrl(): String? = webp2x ?: webp1x ?: avif2x ?: avif1x + +data class ImageVariants( + val thumb: ImageVariantSet? = null, + val card: ImageVariantSet? = null, + val detail: ImageVariantSet? = null, + val width: Int? = null, + val height: Int? = null +) + +fun ImageVariants.getCardUrl(): String? = card?.getBestUrl() ?: thumb?.getBestUrl() +fun ImageVariants.getDetailUrl(): String? = detail?.getBestUrl() ?: card?.getBestUrl() +fun ImageVariants.getThumbUrl(): String? = thumb?.getBestUrl() ?: card?.getBestUrl() + +// --- Featured projects (from /api/projects/featured) --- + +data class FeaturedProjectApi( + val id: String, + @Json(name = "project_id") val projectId: String, + @Json(name = "project_url") val projectUrl: String, + val url: String? = null, + val name: String, + val author: String, + @Json(name = "featured_image") val featuredImage: ImageVariants? = null +) + +fun FeaturedProjectApi.toRoomEntity(): FeaturedProject = FeaturedProject( + id = id, + projectId = projectId, + projectUrl = projectUrl, + name = name, + author = author, + featuredImage = featuredImage?.getDetailUrl() ?: featuredImage?.getCardUrl() ?: "" +) + +// Room entity (stores best image URL as string) @Entity(tableName = "featured_project") data class FeaturedProject( @PrimaryKey val id: String, - val project_id: String, - val project_url: String, + @ColumnInfo(name = "project_id") val projectId: String, + @ColumnInfo(name = "project_url") val projectUrl: String, val name: String, val author: String, - val featured_image: String + @ColumnInfo(name = "featured_image") val featuredImage: String +) + +// --- Project categories (from /api/projects/categories) --- + +data class ProjectsCategoryListResponse( + val data: List ) @Entity(tableName = "project_response", primaryKeys = ["id", "categoryType"]) @@ -49,7 +110,8 @@ data class ProjectResponse( var version: String, var views: Int, var download: Int, - var private: Boolean, + @ColumnInfo(name = "private") + var isPrivate: Boolean, var flavor: String, var tags: List, var uploaded: Long, @@ -102,26 +164,89 @@ data class ProjectCategoryWithResponses( data class ProjectsCategoryApi( val type: String, val name: String, - val projectsList: List + @Json(name = "projects_list") val projectsList: List ) -@SuppressWarnings("ConstructorParameterNaming") +// --- Project response (from /api/projects endpoints) --- + 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 + @Json(name = "author_id") val authorId: String? = null, + @Json(name = "scratch_id") val scratchId: Int? = null, + val description: String = "", + val credits: String = "", + val version: String = "", + val views: Int = 0, + val downloads: Int = 0, + val reactions: Int = 0, + val comments: Int = 0, + @Json(name = "private") + val isPrivate: Boolean = false, + val flavor: String = "", + @FlexibleMap val tags: Map? = null, + @FlexibleMap val extensions: Map? = null, + @Json(name = "uploaded_at") val uploadedAt: String? = null, + @Json(name = "uploaded_string") val uploadedString: String = "", + val screenshot: ImageVariants? = null, + @Json(name = "project_url") val projectUrl: String = "", + @Json(name = "download_url") val downloadUrl: String = "", + val filesize: Double = 0.0, + @Json(name = "not_for_kids") val notForKids: Int = 0, + @Json(name = "retention_days") val retentionDays: Int? = null, + @Json(name = "retention_expiry") val retentionExpiry: String? = null +) + +fun ProjectResponseApi.getScreenshotUrl(): String? = screenshot?.getCardUrl() + +// --- Upload response (minimal — avoids PHP empty-array vs object mismatch on tags) --- + +data class ProjectUploadResponse( + val id: String = "" +) + +// --- Tags & Survey responses --- + +data class TagsResponse( + val data: List = emptyList() +) + +data class TagItem( + val id: String, + val text: String +) + +data class SurveyResponse( + val url: String? = null +) + +// --- Media Library responses --- + +data class MediaCategoryPreview( + val id: String, + val name: String, + val description: String? = null, + val priority: Int = 0, + @Json(name = "assets_count") val assetsCount: Int = 0, + @Json(name = "preview_assets") val previewAssets: List = emptyList() +) + +data class MediaAssetResponse( + val id: String, + val name: String, + val description: String? = null, + @Json(name = "file_type") val fileType: String = "", + val extension: String = "", + val size: Int = 0, + val author: String? = null, + val downloads: Int = 0, + val active: Boolean = true, + @Json(name = "created_at") val createdAt: String? = null, + @Json(name = "updated_at") val updatedAt: String? = null, + @Json(name = "category_id") val categoryId: String? = null, + @Json(name = "category_name") val categoryName: String? = null, + val flavors: List = emptyList(), + @Json(name = "download_url") val downloadUrl: String = "", + val thumbnail: ImageVariants? = null ) diff --git a/catroid/src/main/java/org/catrobat/catroid/sync/FeaturedProjectsSync.kt b/catroid/src/main/java/org/catrobat/catroid/sync/FeaturedProjectsSync.kt index 8e0c2bd7b44..ea4e433c15e 100644 --- a/catroid/src/main/java/org/catrobat/catroid/sync/FeaturedProjectsSync.kt +++ b/catroid/src/main/java/org/catrobat/catroid/sync/FeaturedProjectsSync.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -28,6 +28,7 @@ import androidx.annotation.WorkerThread import org.catrobat.catroid.db.AppDatabase import org.catrobat.catroid.retrofit.WebService import org.catrobat.catroid.retrofit.models.FeaturedProject +import org.catrobat.catroid.retrofit.models.toRoomEntity import org.catrobat.catroid.ui.recyclerview.repository.LocalHashVersionRepository interface FeaturedProjectsSync { @@ -49,17 +50,19 @@ class DefaultFeaturedProjectSync( Log.d(javaClass.simpleName, "local stored hash version: $localHashVersion") Log.d(javaClass.simpleName, "server hash version: $serverHashVersion") if (requireUpdate(localHashVersion, serverHashVersion) || force) { - update(response.body()) + val body = response.body() + val projects = body?.data?.map { it.toRoomEntity() } + update(projects) localHashVersionRepository.setFeaturedProjectsHashVersion(serverHashVersion) } else { Log.d(javaClass.simpleName, "no update needed! you've latest version :)") } } - private fun update(body: List?) { + private fun update(projects: List?) { Log.d(javaClass.simpleName, "updating feature projects") - Log.d(javaClass.simpleName, "$body") - body?.let { + Log.d(javaClass.simpleName, "$projects") + projects?.let { appDatabase.featuredProjectDao().deleteAll() appDatabase.featuredProjectDao().insertFeaturedProjects(it) } diff --git a/catroid/src/main/java/org/catrobat/catroid/sync/ProjectsCategoriesSync.kt b/catroid/src/main/java/org/catrobat/catroid/sync/ProjectsCategoriesSync.kt index d8ff58212f1..1193812e2c2 100644 --- a/catroid/src/main/java/org/catrobat/catroid/sync/ProjectsCategoriesSync.kt +++ b/catroid/src/main/java/org/catrobat/catroid/sync/ProjectsCategoriesSync.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -51,18 +51,19 @@ class DefaultProjectsCategoriesSync( Log.d(javaClass.simpleName, "local stored hash version: $localHashVersion") Log.d(javaClass.simpleName, "server hash version: $serverHashVersion") if (requireUpdate(localHashVersion, serverHashVersion) || force) { - update(response.body()) + val body = response.body() + update(body?.data) localHashVersionRepository.setProjectsCategoriesHashVersion(serverHashVersion) } else { Log.d(javaClass.simpleName, "no update needed! you've latest version :)") } } - private fun update(body: List?) { + private fun update(categories: List?) { Log.d(javaClass.simpleName, "updating projectsCategories") - Log.d(javaClass.simpleName, "$body") + Log.d(javaClass.simpleName, "$categories") - body?.toProjectCategoryWithResponsesList()?.let { + categories?.toProjectCategoryWithResponsesList()?.let { appDatabase.projectCategoryDao().nukeAll() appDatabase.projectCategoryDao().insertProjectCategoriesWithResponses(it) } diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/CheckEmailAvailableTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/CheckEmailAvailableTask.java deleted file mode 100644 index 9644ff9fcff..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/CheckEmailAvailableTask.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.web.CatrobatServerCalls; -import org.catrobat.catroid.web.WebConnectionException; - -public class CheckEmailAvailableTask extends AsyncTask { - private static final String TAG = CheckEmailAvailableTask.class.getSimpleName(); - - private String email; - private String provider; - - private Boolean emailAvailable; - - private OnCheckEmailAvailableCompleteListener onCheckEmailAvailableCompleteListener; - - public CheckEmailAvailableTask(String email, String provider) { - this.email = email; - this.provider = provider; - } - - public void setOnCheckEmailAvailableCompleteListener(OnCheckEmailAvailableCompleteListener listener) { - onCheckEmailAvailableCompleteListener = listener; - } - - @Override - protected Boolean doInBackground(String... params) { - try { - emailAvailable = new CatrobatServerCalls().isEMailAvailable(email); - return true; - } catch (WebConnectionException webconnectionException) { - Log.e(TAG, Log.getStackTraceString(webconnectionException)); - } - return false; - } - - @Override - protected void onPostExecute(Boolean success) { - super.onPostExecute(success); - - if (onCheckEmailAvailableCompleteListener != null) { - onCheckEmailAvailableCompleteListener.onCheckEmailAvailableComplete(emailAvailable, provider); - } - } - - public interface OnCheckEmailAvailableCompleteListener { - void onCheckEmailAvailableComplete(Boolean emailAvailable, String provider); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/CheckOAuthTokenTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/CheckOAuthTokenTask.java deleted file mode 100644 index 108a4042c8a..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/CheckOAuthTokenTask.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.app.Activity; -import android.app.ProgressDialog; -import android.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.utils.Utils; -import org.catrobat.catroid.web.CatrobatServerCalls; -import org.catrobat.catroid.web.WebConnectionException; - -import androidx.appcompat.app.AlertDialog; - -public class CheckOAuthTokenTask extends AsyncTask { - private static final String TAG = CheckOAuthTokenTask.class.getSimpleName(); - - private Activity activity; - private ProgressDialog progressDialog; - private String id; - private String provider; - - private Boolean tokenAvailable; - - private WebConnectionException exception; - - private OnCheckOAuthTokenCompleteListener onCheckOAuthTokenCompleteListener; - - public CheckOAuthTokenTask(Activity activity, String id, String provider) { - this.activity = activity; - this.id = id; - this.provider = provider; - } - - public void setOnCheckOAuthTokenCompleteListener(OnCheckOAuthTokenCompleteListener listener) { - onCheckOAuthTokenCompleteListener = listener; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (activity == null) { - return; - } - String title = activity.getString(R.string.please_wait); - String message = activity.getString(R.string.loading_check_oauth_token); - progressDialog = ProgressDialog.show(activity, title, message); - } - - @Override - protected Boolean doInBackground(String... params) { - try { - if (!Utils.isNetworkAvailable(activity)) { - exception = new WebConnectionException(WebConnectionException.ERROR_NETWORK, "Network not available!"); - return false; - } - - tokenAvailable = new CatrobatServerCalls().checkOAuthToken(id, provider, activity); - return true; - } catch (WebConnectionException webconnectionException) { - Log.e(TAG, Log.getStackTraceString(webconnectionException)); - exception = webconnectionException; - } - return false; - } - - @Override - protected void onPostExecute(Boolean success) { - super.onPostExecute(success); - - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - - if (Utils.checkForNetworkError(exception)) { - showDialog(R.string.error_internet_connection); - return; - } - - if (!success && exception != null) { - showDialog(R.string.sign_in_error); - return; - } - - if (onCheckOAuthTokenCompleteListener != null) { - onCheckOAuthTokenCompleteListener.onCheckOAuthTokenComplete(tokenAvailable, provider); - } - } - - private void showDialog(int messageId) { - if (activity == null) { - return; - } - if (exception.getMessage() == null) { - new AlertDialog.Builder(activity).setMessage(messageId).setPositiveButton(R.string.ok, null) - .show(); - } else { - new AlertDialog.Builder(activity).setMessage(exception.getMessage()) - .setPositiveButton(R.string.ok, null).show(); - } - } - - public interface OnCheckOAuthTokenCompleteListener { - void onCheckOAuthTokenComplete(Boolean tokenAvailable, String provider); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/CheckTokenTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/CheckTokenTask.java deleted file mode 100644 index e10ef19ace1..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/CheckTokenTask.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.common.FlavoredConstants; -import org.catrobat.catroid.web.CatrobatServerCalls; -import org.catrobat.catroid.web.WebConnectionException; - -public class CheckTokenTask extends AsyncTask { - - private static final String TAG = CheckTokenTask.class.getSimpleName(); - private TokenCheckListener onCheckTokenCompleteListener; - - public CheckTokenTask(TokenCheckListener onCheckTokenCompleteListener) { - this.onCheckTokenCompleteListener = onCheckTokenCompleteListener; - } - - @Override - protected Boolean[] doInBackground(String... arg0) { - try { - return new Boolean[]{new CatrobatServerCalls().checkToken(arg0[0], arg0[1], FlavoredConstants.BASE_URL_HTTPS), false}; - } catch (WebConnectionException e) { - Log.e(TAG, Log.getStackTraceString(e)); - return new Boolean[]{false, true}; - } - } - - @Override - protected void onPostExecute(Boolean[] b) { - boolean tokenValid = b[0]; - boolean connectionFailed = b[1]; - onCheckTokenCompleteListener.onTokenCheckComplete(tokenValid, connectionFailed); - } - - public interface TokenCheckListener { - - void onTokenCheckComplete(boolean tokenValid, boolean connectionFailed); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/CheckUserNameAvailableTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/CheckUserNameAvailableTask.java deleted file mode 100644 index 05e8782f593..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/CheckUserNameAvailableTask.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.web.CatrobatServerCalls; -import org.catrobat.catroid.web.WebConnectionException; - -public class CheckUserNameAvailableTask extends AsyncTask { - private static final String TAG = CheckUserNameAvailableTask.class.getSimpleName(); - private String username; - - private Boolean userNameAvailable; - - private OnCheckUserNameAvailableCompleteListener onCheckUserNameAvailableCompleteListener; - - public CheckUserNameAvailableTask(String username) { - this.username = username; - } - - public void setOnCheckUserNameAvailableCompleteListener(OnCheckUserNameAvailableCompleteListener listener) { - onCheckUserNameAvailableCompleteListener = listener; - } - - @Override - protected Boolean doInBackground(Void... params) { - try { - userNameAvailable = new CatrobatServerCalls().isUserNameAvailable(username); - return true; - } catch (WebConnectionException webconnectionException) { - Log.e(TAG, Log.getStackTraceString(webconnectionException)); - } - return false; - } - - @Override - protected void onPostExecute(Boolean success) { - super.onPostExecute(success); - - if (onCheckUserNameAvailableCompleteListener != null) { - onCheckUserNameAvailableCompleteListener.onCheckUserNameAvailableComplete(userNameAvailable, username); - } - } - - public interface OnCheckUserNameAvailableCompleteListener { - void onCheckUserNameAvailableComplete(Boolean userNameAvailable, String username); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/DeleteTestUserTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/DeleteTestUserTask.java deleted file mode 100644 index 28a4669f557..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/DeleteTestUserTask.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.content.Context; -import android.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.utils.Utils; -import org.catrobat.catroid.web.CatrobatServerCalls; -import org.catrobat.catroid.web.WebConnectionException; - -import androidx.appcompat.app.AlertDialog; - -public class DeleteTestUserTask extends AsyncTask { - private static final String TAG = DeleteTestUserTask.class.getSimpleName(); - - private Context context; - - private WebConnectionException exception; - - private OnDeleteTestUserCompleteListener onDeleteTestUserCompleteListener; - - public DeleteTestUserTask(Context context) { - this.context = context; - } - - public void setOnDeleteTestUserCompleteListener(OnDeleteTestUserCompleteListener listener) { - onDeleteTestUserCompleteListener = listener; - } - - @Override - protected Boolean doInBackground(Void... params) { - try { - if (!Utils.isNetworkAvailable(context)) { - exception = new WebConnectionException(WebConnectionException.ERROR_NETWORK, "Network not available!"); - return false; - } - - return new CatrobatServerCalls().deleteTestUserAccountsOnServer(); - } catch (WebConnectionException webconnectionException) { - Log.e(TAG, Log.getStackTraceString(webconnectionException)); - exception = webconnectionException; - } - return false; - } - - @Override - protected void onPostExecute(Boolean deleted) { - super.onPostExecute(deleted); - - if (Utils.checkForNetworkError(exception)) { - showDialog(R.string.error_internet_connection); - return; - } - - if (onDeleteTestUserCompleteListener != null) { - onDeleteTestUserCompleteListener.onDeleteTestUserComplete(deleted); - } - } - - private void showDialog(int messageId) { - if (context == null) { - return; - } - if (exception.getMessage() == null) { - new AlertDialog.Builder(context).setMessage(messageId).setPositiveButton(R.string.ok, null) - .show(); - } else { - new AlertDialog.Builder(context).setMessage(exception.getMessage()) - .setPositiveButton(R.string.ok, null).show(); - } - } - - public interface OnDeleteTestUserCompleteListener { - void onDeleteTestUserComplete(Boolean deleted); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/GetSurveyTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/GetSurveyTask.java deleted file mode 100644 index 40ab11e2634..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/GetSurveyTask.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.content.Context; -import android.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.web.CatrobatServerCalls; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Locale; - -import javax.net.ssl.HttpsURLConnection; - -import androidx.annotation.VisibleForTesting; - -public class GetSurveyTask extends AsyncTask { - - private static final String TAG = GetSurveyTask.class.getSimpleName(); - private static final String SURVEY_URL_JSON_KEY = "url"; - private SurveyResponseListener onSurveyResponseListener; - private final WeakReference contextWeakReference; - - public GetSurveyTask(Context context) { - this.contextWeakReference = new WeakReference<>(context); - } - - public void setOnSurveyResponseListener(SurveyResponseListener listener) { - onSurveyResponseListener = listener; - } - - @Override - protected String doInBackground(String... arg0) { - String jsonString = new CatrobatServerCalls().getSurvey(Locale.getDefault().getLanguage()); - String surveyUrl = null; - - if (!jsonString.isEmpty()) { - try { - surveyUrl = parseSurvey(jsonString); - surveyUrl = isUrlStatusCodeOk(surveyUrl) ? surveyUrl : null; - } catch (Exception e) { - Log.e(TAG, "Failed to get survey url", e); - } - } - - return surveyUrl; - } - - @Override - protected void onPostExecute(String response) { - Context context = contextWeakReference.get(); - if (context == null) { - return; - } - - if (response != null) { - onSurveyResponseListener.onSurveyReceived(context, response); - } - } - - @VisibleForTesting - public String parseSurvey(String response) throws JSONException { - JSONObject json = new JSONObject(response); - String surveyUrl = json.getString(SURVEY_URL_JSON_KEY); - - return surveyUrl; - } - - private boolean isUrlStatusCodeOk(String surveyUrl) throws IOException { - HttpsURLConnection connection = (HttpsURLConnection) (new URL(surveyUrl)).openConnection(); - connection.connect(); - int status = connection.getResponseCode(); - connection.disconnect(); - - return status == HttpURLConnection.HTTP_OK; - } - - public interface SurveyResponseListener { - void onSurveyReceived(Context context, String surveyUrl); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/GetTagsTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/GetTagsTask.java deleted file mode 100644 index cbbdad42197..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/GetTagsTask.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.web.CatrobatServerCalls; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -public class GetTagsTask extends AsyncTask { - - private static final String TAG = GetTagsTask.class.getSimpleName(); - private static final String TAGS_JSON_KEY = "constantTags"; - private TagResponseListener onTagsResponseListener; - - public void setOnTagsResponseListener(TagResponseListener listener) { - onTagsResponseListener = listener; - } - - @Override - protected String doInBackground(String... arg0) { - return new CatrobatServerCalls().getTags(Locale.getDefault().getLanguage()); - } - - @Override - protected void onPostExecute(String response) { - try { - onTagsResponseListener.onTagsReceived(parseTags(response)); - } catch (JSONException e) { - Log.e(TAG, "Failed to parse tags json", e); - } - } - - private List parseTags(String response) throws JSONException { - List tags = new ArrayList<>(); - JSONObject json = new JSONObject(response); - JSONArray tagsJson = json.getJSONArray(TAGS_JSON_KEY); - for (int i = 0; i < tagsJson.length(); i++) { - tags.add(tagsJson.getString(i)); - } - return Collections.unmodifiableList(tags); - } - - public interface TagResponseListener { - - void onTagsReceived(List tags); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleExchangeCodeTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleExchangeCodeTask.java deleted file mode 100644 index f1f4bc8c62c..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleExchangeCodeTask.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.app.Activity; -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.utils.Utils; -import org.catrobat.catroid.web.CatrobatWebClient; -import org.catrobat.catroid.web.ServerCalls; -import org.catrobat.catroid.web.WebConnectionException; - -import androidx.appcompat.app.AlertDialog; - -public class GoogleExchangeCodeTask extends AsyncTask { - - private static final String TAG = GoogleExchangeCodeTask.class.getSimpleName(); - - private Context context; - private ProgressDialog progressDialog; - private String code; - private String mail; - private String username; - private String id; - private String locale; - private final String idToken; - private String message; - private OnGoogleExchangeCodeCompleteListener onGoogleExchangeCodeCompleteListener; - private WebConnectionException exception; - private boolean tokenExchanged; - - public GoogleExchangeCodeTask(Activity activity, String code, String mail, String username, String id, String - locale, String idToken) { - this.code = code; - this.context = activity; - this.mail = mail; - this.username = username; - this.id = id; - this.locale = locale; - this.idToken = idToken; - } - - public void setOnGoogleExchangeCodeCompleteListener(OnGoogleExchangeCodeCompleteListener listener) { - onGoogleExchangeCodeCompleteListener = listener; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (context == null) { - return; - } - String title = context.getString(R.string.please_wait); - String message = context.getString(R.string.loading_google_exchange_code); - progressDialog = ProgressDialog.show(context, title, message); - } - - @Override - protected Boolean doInBackground(Void... arg0) { - try { - if (!Utils.isNetworkAvailable(context)) { - exception = new WebConnectionException(WebConnectionException.ERROR_NETWORK, "Network not available!"); - return false; - } - - tokenExchanged = new ServerCalls(CatrobatWebClient.INSTANCE.getClient()).googleExchangeCode(code, id, username, mail, locale, idToken); - return true; - } catch (WebConnectionException webconnectionException) { - Log.e(TAG, Log.getStackTraceString(webconnectionException)); - message = webconnectionException.getMessage(); - } - return false; - } - - @Override - protected void onPostExecute(Boolean success) { - super.onPostExecute(success); - - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - - if (Utils.checkForNetworkError(exception)) { - showDialog(R.string.error_internet_connection); - return; - } - - if ((!success && exception != null) || !tokenExchanged) { - showDialog(R.string.sign_in_error); - return; - } - - if (onGoogleExchangeCodeCompleteListener != null) { - onGoogleExchangeCodeCompleteListener.onGoogleExchangeCodeComplete(); - } - } - - private void showDialog(int messageId) { - if (context == null) { - return; - } - if (message == null) { - new AlertDialog.Builder(context).setTitle(R.string.register_error).setMessage(messageId) - .setPositiveButton(R.string.ok, null).show(); - } else { - new AlertDialog.Builder(context).setTitle(R.string.register_error).setMessage(message) - .setPositiveButton(R.string.ok, null).show(); - } - } - - public interface OnGoogleExchangeCodeCompleteListener { - void onGoogleExchangeCodeComplete(); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleLogInTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleLogInTask.java deleted file mode 100644 index 35f516ec4c6..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleLogInTask.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.app.Activity; -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import android.util.Log; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.utils.ToastUtil; -import org.catrobat.catroid.utils.Utils; -import org.catrobat.catroid.web.CatrobatWebClient; -import org.catrobat.catroid.web.ServerCalls; -import org.catrobat.catroid.web.WebConnectionException; - -import androidx.appcompat.app.AlertDialog; - -public class GoogleLogInTask extends AsyncTask { - - private static final String TAG = GoogleLogInTask.class.getSimpleName(); - - private Context context; - private ProgressDialog progressDialog; - private String mail; - private String username; - private String id; - private String locale; - private String message; - private OnGoogleServerLogInCompleteListener onGoogleServerLogInCompleteListener; - private WebConnectionException exception; - private boolean userSignedIn; - - public GoogleLogInTask(Activity activity, String mail, String username, String id, String locale) { - this.context = activity; - this.mail = mail; - this.username = username; - this.id = id; - this.locale = locale; - } - - public void setOnGoogleServerLogInCompleteListener(OnGoogleServerLogInCompleteListener listener) { - onGoogleServerLogInCompleteListener = listener; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (context == null) { - return; - } - String title = context.getString(R.string.please_wait); - String message = context.getString(R.string.loading_google_login); - progressDialog = ProgressDialog.show(context, title, message); - } - - @Override - protected Boolean doInBackground(Void... arg0) { - try { - if (!Utils.isNetworkAvailable(context)) { - exception = new WebConnectionException(WebConnectionException.ERROR_NETWORK, "Network not available!"); - return false; - } - - userSignedIn = new ServerCalls(CatrobatWebClient.INSTANCE.getClient()).googleLogin(mail, username, id, locale, context); - return true; - } catch (WebConnectionException webconnectionException) { - Log.e(TAG, Log.getStackTraceString(webconnectionException)); - message = webconnectionException.getMessage(); - } - return false; - } - - @Override - protected void onPostExecute(Boolean success) { - super.onPostExecute(success); - - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - - if (Utils.checkForNetworkError(exception)) { - showDialog(R.string.error_internet_connection); - return; - } - - if ((!success && exception != null) || !userSignedIn) { - showDialog(R.string.sign_in_error); - return; - } - - if (userSignedIn) { - ToastUtil.showSuccess(context, R.string.user_logged_in); - } - - if (onGoogleServerLogInCompleteListener != null) { - onGoogleServerLogInCompleteListener.onGoogleServerLogInComplete(); - } - } - - private void showDialog(int messageId) { - if (context == null) { - return; - } - if (message == null) { - new AlertDialog.Builder(context).setTitle(R.string.register_error).setMessage(messageId) - .setPositiveButton(R.string.ok, null).show(); - } else { - new AlertDialog.Builder(context).setTitle(R.string.register_error).setMessage(message) - .setPositiveButton(R.string.ok, null).show(); - } - } - - public interface OnGoogleServerLogInCompleteListener { - void onGoogleServerLogInComplete(); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleLoginHandler.java b/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleLoginHandler.java index 9623761629c..090d2501a3e 100644 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleLoginHandler.java +++ b/catroid/src/main/java/org/catrobat/catroid/transfers/GoogleLoginHandler.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -24,9 +24,8 @@ package org.catrobat.catroid.transfers; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.appcompat.app.AppCompatActivity; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInClient; @@ -35,27 +34,26 @@ import org.catrobat.catroid.R; import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.ui.recyclerview.dialog.login.OAuthUsernameDialogFragment; import org.catrobat.catroid.ui.recyclerview.dialog.login.SignInCompleteListener; -import org.catrobat.catroid.utils.DeviceSettingsProvider; import org.catrobat.catroid.utils.ToastUtil; +import org.catrobat.catroid.web.LoginHelper; +import org.catrobat.catroid.web.LoginRepository; -import androidx.appcompat.app.AppCompatActivity; +import kotlin.Lazy; import static com.google.android.gms.auth.api.signin.GoogleSignIn.getClient; import static com.google.android.gms.auth.api.signin.GoogleSignIn.getSignedInAccountFromIntent; - import static org.catrobat.catroid.web.ServerAuthenticationConstants.GOOGLE_LOGIN_CATROWEB_SERVER_CLIENT_ID; +import static org.koin.java.KoinJavaComponent.inject; -public class GoogleLoginHandler implements CheckOAuthTokenTask.OnCheckOAuthTokenCompleteListener, - GoogleLogInTask.OnGoogleServerLogInCompleteListener, - CheckEmailAvailableTask.OnCheckEmailAvailableCompleteListener, - GoogleExchangeCodeTask.OnGoogleExchangeCodeCompleteListener { +public class GoogleLoginHandler { private AppCompatActivity activity; public static final int REQUEST_CODE_GOOGLE_SIGNIN = 100; private GoogleSignInClient googleSignInClient; + private final Lazy loginRepository = inject(LoginRepository.class); + @SuppressWarnings("RestrictedApi") public GoogleLoginHandler(AppCompatActivity activity) { this.activity = activity; @@ -78,100 +76,34 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (task.isSuccessful()) { onGoogleLogInComplete(task.getResult()); } else { + String errorMsg = task.getException() != null && task.getException().getLocalizedMessage() != null + ? task.getException().getLocalizedMessage().replace(":", "") + : "Unknown error"; ToastUtil.showError(activity, - String.format(activity.getString(R.string.error_google_plus_sign_in), task.getException().getLocalizedMessage().replace(":", ""))); + String.format(activity.getString(R.string.error_google_plus_sign_in), errorMsg)); } } } public void onGoogleLogInComplete(GoogleSignInAccount account) { - String id = account.getId(); - String personName = account.getDisplayName(); - String email = account.getEmail(); - String locale = DeviceSettingsProvider.getUserCountryCode(); String idToken = account.getIdToken(); - String code = account.getServerAuthCode(); - - PreferenceManager.getDefaultSharedPreferences(activity).edit() - .putString(Constants.GOOGLE_ID, id) - .putString(Constants.GOOGLE_USERNAME, personName) - .putString(Constants.GOOGLE_EMAIL, email) - .putString(Constants.GOOGLE_LOCALE, locale) - .putString(Constants.GOOGLE_ID_TOKEN, idToken) - .putString(Constants.GOOGLE_EXCHANGE_CODE, code) - .apply(); - - CheckOAuthTokenTask checkOAuthTokenTask = new CheckOAuthTokenTask(activity, id, Constants.GOOGLE_PLUS); - checkOAuthTokenTask.setOnCheckOAuthTokenCompleteListener(this); - checkOAuthTokenTask.execute(); - } - @Override - public void onCheckOAuthTokenComplete(Boolean tokenAvailable, String provider) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); - if (tokenAvailable) { - GoogleLogInTask googleLogInTask = new GoogleLogInTask(activity, - sharedPreferences.getString(Constants.GOOGLE_EMAIL, Constants.NO_GOOGLE_EMAIL), - sharedPreferences.getString(Constants.GOOGLE_USERNAME, Constants.NO_GOOGLE_USERNAME), - sharedPreferences.getString(Constants.GOOGLE_ID, Constants.NO_GOOGLE_ID), - sharedPreferences.getString(Constants.GOOGLE_LOCALE, Constants.NO_GOOGLE_LOCALE)); - googleLogInTask.setOnGoogleServerLogInCompleteListener(this); - googleLogInTask.execute(); - } else { - String email = sharedPreferences.getString(Constants.GOOGLE_EMAIL, Constants.NO_GOOGLE_EMAIL); - CheckEmailAvailableTask checkEmailAvailableTask = new CheckEmailAvailableTask(email, Constants.GOOGLE_PLUS); - checkEmailAvailableTask.setOnCheckEmailAvailableCompleteListener(this); - checkEmailAvailableTask.execute(); + if (idToken == null) { + ToastUtil.showError(activity, R.string.sign_in_error); + return; } - } - - @Override - public void onGoogleServerLogInComplete() { - Bundle bundle = new Bundle(); - bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, Constants.GOOGLE_PLUS); - ((SignInCompleteListener) activity).onLoginSuccessful(bundle); - } - - @Override - public void onCheckEmailAvailableComplete(Boolean emailAvailable, String provider) { - if (emailAvailable) { - exchangeGoogleAuthorizationCode(); - } else { - showOauthUserNameDialog(Constants.GOOGLE_PLUS); - } - } - - public void exchangeGoogleAuthorizationCode() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); - GoogleExchangeCodeTask googleExchangeCodeTask = new GoogleExchangeCodeTask(activity, - sharedPreferences.getString(Constants.GOOGLE_EXCHANGE_CODE, Constants.NO_GOOGLE_EXCHANGE_CODE), - sharedPreferences.getString(Constants.GOOGLE_EMAIL, Constants.NO_GOOGLE_EMAIL), - sharedPreferences.getString(Constants.GOOGLE_USERNAME, Constants.NO_GOOGLE_USERNAME), - sharedPreferences.getString(Constants.GOOGLE_ID, Constants.NO_GOOGLE_ID), - sharedPreferences.getString(Constants.GOOGLE_LOCALE, Constants.NO_GOOGLE_LOCALE), - sharedPreferences.getString(Constants.GOOGLE_ID_TOKEN, Constants.NO_GOOGLE_ID_TOKEN)); - googleExchangeCodeTask.setOnGoogleExchangeCodeCompleteListener(this); - googleExchangeCodeTask.execute(); - } - - private void showOauthUserNameDialog(String provider) { - OAuthUsernameDialogFragment dialog = new OAuthUsernameDialogFragment(); - Bundle bundle = new Bundle(); - bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, provider); - dialog.setArguments(bundle); - dialog.setSignInCompleteListener((SignInCompleteListener) activity); - dialog.show(activity.getSupportFragmentManager(), OAuthUsernameDialogFragment.TAG); - } - @Override - public void onGoogleExchangeCodeComplete() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); - GoogleLogInTask googleLogInTask = new GoogleLogInTask(activity, - sharedPreferences.getString(Constants.GOOGLE_EMAIL, Constants.NO_GOOGLE_EMAIL), - sharedPreferences.getString(Constants.GOOGLE_USERNAME, Constants.NO_GOOGLE_USERNAME), - sharedPreferences.getString(Constants.GOOGLE_ID, Constants.NO_GOOGLE_ID), - sharedPreferences.getString(Constants.GOOGLE_LOCALE, Constants.NO_GOOGLE_LOCALE)); - googleLogInTask.setOnGoogleServerLogInCompleteListener(this); - googleLogInTask.execute(); + LoginHelper.performGoogleLogin( + loginRepository.getValue(), + idToken, + () -> { + if (activity instanceof SignInCompleteListener && !activity.isFinishing()) { + Bundle bundle = new Bundle(); + bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, Constants.GOOGLE_PLUS); + ((SignInCompleteListener) activity).onLoginSuccessful(bundle); + } + }, + errorMsg -> ToastUtil.showError(activity, errorMsg) + ); } } diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/LoginTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/LoginTask.java deleted file mode 100644 index 95479fae497..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/LoginTask.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.app.ProgressDialog; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.preference.PreferenceManager; -import android.util.Log; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.common.FlavoredConstants; -import org.catrobat.catroid.utils.ToastUtil; -import org.catrobat.catroid.utils.Utils; -import org.catrobat.catroid.web.CatrobatWebClient; -import org.catrobat.catroid.web.ServerAuthenticator; - -import java.lang.ref.WeakReference; - -public class LoginTask extends AsyncTask { - - private static final String TAG = LoginTask.class.getSimpleName(); - - private WeakReference contextWeakReference; - private ProgressDialog progressDialog; - private String username; - private String password; - - private String message; - private boolean userLoggedIn = false; - - private OnLoginListener onLoginListener; - - public LoginTask(Context activity, String username, String password) { - this.contextWeakReference = new WeakReference<>(activity); - this.username = username; - this.password = password; - } - - public void setOnLoginListener(OnLoginListener listener) { - onLoginListener = listener; - } - - @Override - protected void onPreExecute() { - Context context = contextWeakReference.get(); - if (context == null) { - return; - } - String title = context.getString(R.string.please_wait); - String message = context.getString(R.string.loading); - progressDialog = ProgressDialog.show(context, title, message); - } - - @Override - protected Void doInBackground(Void... arg0) { - Context context = contextWeakReference.get(); - - if (context == null) { - return null; - } - - if (!Utils.checkIsNetworkAvailableAndShowErrorMessage(context)) { - userLoggedIn = false; - return null; - } - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - String token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN); - Log.d(TAG, token); - - ServerAuthenticator authenticator = new ServerAuthenticator(username, password, token, - CatrobatWebClient.INSTANCE.getClient(), - FlavoredConstants.BASE_URL_HTTPS, - sharedPreferences, - new ServerAuthenticator.TaskListener() { - @Override - public void onError(int statusCode, String errorMessage) { - Log.e(TAG, "StatusCode: " + statusCode + errorMessage); - message = errorMessage; - userLoggedIn = false; - } - - @Override - public void onSuccess() { - userLoggedIn = true; - } - }); - authenticator.performCatrobatLogin(); - return null; - } - - @Override - protected void onPostExecute(Void arg) { - Context context = contextWeakReference.get(); - if (context == null) { - return; - } - - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - - if (userLoggedIn) { - ToastUtil.showSuccess(context, R.string.user_logged_in); - if (onLoginListener != null) { - onLoginListener.onLoginComplete(); - } - return; - } - - if (message == null) { - message = context.getString(R.string.sign_in_error); - } - onLoginListener.onLoginFailed(message); - Log.e(TAG, message); - } - - public interface OnLoginListener { - - void onLoginComplete(); - - void onLoginFailed(String msg); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/MediaDownloadService.java b/catroid/src/main/java/org/catrobat/catroid/transfers/MediaDownloadService.java index 2f79bcebbaa..7b9e78e1e28 100644 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/MediaDownloadService.java +++ b/catroid/src/main/java/org/catrobat/catroid/transfers/MediaDownloadService.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -31,12 +31,15 @@ import org.catrobat.catroid.R; import org.catrobat.catroid.common.Constants; import org.catrobat.catroid.utils.ToastUtil; -import org.catrobat.catroid.web.CatrobatWebClient; -import org.catrobat.catroid.web.ServerCalls; +import org.catrobat.catroid.web.DownloadClient; import org.catrobat.catroid.web.WebConnectionException; import java.io.IOException; +import kotlin.Lazy; + +import static org.koin.java.KoinJavaComponent.inject; + public class MediaDownloadService extends IntentService { public static final String TAG = MediaDownloadService.class.getSimpleName(); @@ -48,6 +51,8 @@ public class MediaDownloadService extends IntentService { public ResultReceiver receiver; private Handler handler; + private final Lazy downloadClient = inject(DownloadClient.class); + public MediaDownloadService() { super(MediaDownloadService.class.getSimpleName()); } @@ -67,7 +72,7 @@ protected void onHandleIntent(Intent intent) { receiver = intent.getParcelableExtra(RECEIVER_TAG); try { - new ServerCalls(CatrobatWebClient.INSTANCE.getClient()).downloadMedia(url, fileString, receiver); + downloadClient.getValue().downloadMedia(url, fileString, receiver); } catch (IOException ioException) { Log.e(TAG, Log.getStackTraceString(ioException)); result = false; diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/RegistrationTask.java b/catroid/src/main/java/org/catrobat/catroid/transfers/RegistrationTask.java deleted file mode 100644 index 81d03781aa5..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/RegistrationTask.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.app.ProgressDialog; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.preference.PreferenceManager; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.common.FlavoredConstants; -import org.catrobat.catroid.utils.DeviceSettingsProvider; -import org.catrobat.catroid.utils.ToastUtil; -import org.catrobat.catroid.utils.Utils; -import org.catrobat.catroid.web.CatrobatWebClient; -import org.catrobat.catroid.web.ServerAuthenticator; - -import java.lang.ref.WeakReference; -import java.util.Locale; - -public class RegistrationTask extends AsyncTask { - private final WeakReference contextWeakReference; - private ProgressDialog progressDialog; - private String username; - private String password; - private String email; - - private String message; - private boolean userRegistered = false; - - private OnRegistrationListener onRegistrationListener; - - public RegistrationTask(Context context, String username, String password, String email) { - this.contextWeakReference = new WeakReference<>(context); - this.username = username; - this.password = password; - this.email = email; - } - - public void setOnRegistrationListener(OnRegistrationListener listener) { - onRegistrationListener = listener; - } - - @Override - protected void onPreExecute() { - Context context = contextWeakReference.get(); - if (context == null) { - return; - } - String title = context.getString(R.string.please_wait); - String message = context.getString(R.string.loading); - progressDialog = ProgressDialog.show(context, title, message); - } - - @Override - protected Void doInBackground(Void... arg0) { - Context context = contextWeakReference.get(); - - if (context == null) { - return null; - } - - if (!Utils.isNetworkAvailable(context)) { - message = context.getString(R.string.error_internet_connection); - userRegistered = false; - return null; - } - - String language = Locale.getDefault().getLanguage(); - String country = DeviceSettingsProvider.getUserCountryCode(); - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - String token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN); - - ServerAuthenticator authenticator = - new ServerAuthenticator(username, password, token, - CatrobatWebClient.INSTANCE.getClient(), - FlavoredConstants.BASE_URL_HTTPS, - sharedPreferences, - new ServerAuthenticator.TaskListener() { - @Override - public void onError(int statusCode, String errorMessage) { - message = context.getString(R.string.error_internet_connection); - userRegistered = false; - } - - @Override - public void onSuccess() { - userRegistered = true; - } - }); - - authenticator.performCatrobatRegister(email, language, country); - return null; - } - - @Override - protected void onPostExecute(Void any) { - Context context = contextWeakReference.get(); - - if (context == null) { - return; - } - - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - - if (userRegistered) { - ToastUtil.showSuccess(context, R.string.new_user_registered); - if (onRegistrationListener != null) { - onRegistrationListener.onRegistrationComplete(); - } - return; - } - - if (message == null) { - message = context.getString(R.string.register_error); - } - - ToastUtil.showError(context, message); - if (onRegistrationListener != null) { - onRegistrationListener.onRegistrationFailed(message); - } - } - - public interface OnRegistrationListener { - - void onRegistrationComplete(); - - void onRegistrationFailed(String msg); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectDownloadService.kt b/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectDownloadService.kt index 45ad453d4fa..cde3f0827c3 100644 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectDownloadService.kt +++ b/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectDownloadService.kt @@ -43,20 +43,20 @@ import org.catrobat.catroid.io.XstreamSerializer import org.catrobat.catroid.io.ZipArchiver import org.catrobat.catroid.ui.MainMenuActivity import org.catrobat.catroid.utils.FileMetaDataExtractor +import org.catrobat.catroid.utils.ProjectIdUtils import org.catrobat.catroid.utils.ToastUtil import org.catrobat.catroid.utils.notifications.NotificationData import org.catrobat.catroid.utils.notifications.StatusBarNotificationManager import org.catrobat.catroid.utils.notifications.StatusBarNotificationManager.CHANNEL_ID -import org.catrobat.catroid.web.CatrobatServerCalls -import org.catrobat.catroid.web.CatrobatServerCalls.DownloadErrorCallback -import org.catrobat.catroid.web.CatrobatServerCalls.DownloadProgressCallback -import org.catrobat.catroid.web.CatrobatServerCalls.DownloadSuccessCallback -import org.catrobat.catroid.web.CatrobatWebClient +import org.catrobat.catroid.web.DownloadClient +import org.koin.android.ext.android.inject import java.io.File import java.io.IOException class ProjectDownloadService : IntentService("ProjectDownloadService") { + private val downloadClient: DownloadClient by inject() + companion object { val TAG: String = ProjectDownloadService::class.java.simpleName const val EXTRA_DOWNLOAD_NAME = "downloadName" @@ -74,13 +74,13 @@ class ProjectDownloadService : IntentService("ProjectDownloadService") { override fun onHandleIntent(intent: Intent?) { val downloadIntent = intent - ?: return logWarning("Called ProjectDownloadService with null intent - aborting") + ?: return Unit.also { Log.w(TAG, "Called ProjectDownloadService with null intent - aborting") } val projectName = downloadIntent.getStringExtra(EXTRA_DOWNLOAD_NAME) - ?: return logWarning("Called ProjectDownloadService with null projectName - aborting") + ?: return Unit.also { Log.w(TAG, "Called ProjectDownloadService with null projectName - aborting") } val url = downloadIntent.getStringExtra(EXTRA_URL) - ?: return logWarning("Called ProjectDownloadService without url - aborting") + ?: return Unit.also { Log.w(TAG, "Called ProjectDownloadService without url - aborting") } val resultReceiver = downloadIntent.getParcelableExtra(EXTRA_RESULT_RECEIVER) - ?: return logWarning("Called ProjectDownloadService without url - aborting") + ?: return Unit.also { Log.w(TAG, "Called ProjectDownloadService without receiver - aborting") } val zipFileString = File(File(CACHE_DIRECTORY, TMP_DIRECTORY_NAME), DOWNLOAD_FILE_NAME).absolutePath val destinationFile = File(zipFileString) @@ -97,31 +97,25 @@ class ProjectDownloadService : IntentService("ProjectDownloadService") { startForeground(id, notification) - CatrobatServerCalls(CatrobatWebClient.client) - .downloadProject( - url, - destinationFile, - object : DownloadSuccessCallback { - override fun onSuccess() { - downloadSuccessCallback( - this@ProjectDownloadService, - projectName, - destinationFile, - resultReceiver - ) - } - }, - object : DownloadErrorCallback { - override fun onError(code: Int, message: String) { - downloadErrorCallback(this@ProjectDownloadService, resultReceiver, projectName) - } - }, - object : DownloadProgressCallback { - override fun onProgress(progress: Long) { - downloadProgressCallback(this@ProjectDownloadService, resultReceiver, notificationData, progress) - } - } - ) + downloadClient.downloadProject( + url, + destinationFile, + successCallback = { + downloadSuccessCallback( + this@ProjectDownloadService, + projectName, + url, + destinationFile, + resultReceiver + ) + }, + errorCallback = { _, _ -> + downloadErrorCallback(this@ProjectDownloadService, resultReceiver, projectName) + }, + progressCallback = { progress -> + downloadProgressCallback(this@ProjectDownloadService, resultReceiver, notificationData, progress) + } + ) stopForeground(true) } @@ -129,10 +123,10 @@ class ProjectDownloadService : IntentService("ProjectDownloadService") { private fun downloadSuccessCallback( context: Context, projectName: String, + downloadUrl: String, destinationFile: File, resultReceiver: ResultReceiver ) { - val statusBarNotificationManager = StatusBarNotificationManager(context) val notificationData = statusBarNotificationManager.createProjectDownloadNotification(this, projectName) try { @@ -141,7 +135,14 @@ class ProjectDownloadService : IntentService("ProjectDownloadService") { ZipArchiver().unzip(destinationFile, projectDir) XstreamSerializer.renameProject(File(projectDir, Constants.CODE_XML_FILE_NAME), projectName) - ProjectManager.getInstance().addNewDownloadedProject(projectName) + + val serverProjectId = extractProjectIdFromUrl(downloadUrl) + val projectManager = ProjectManager.getInstance() + projectManager.addNewDownloadedProject(projectName) + if (serverProjectId != null) { + setRemixUrlInProject(projectDir, serverProjectId) + File(projectDir, ".server_project_id").writeText(serverProjectId) + } val downloadIntent = Intent(context, MainMenuActivity::class.java) downloadIntent.setAction(Intent.ACTION_MAIN) @@ -177,27 +178,45 @@ class ProjectDownloadService : IntentService("ProjectDownloadService") { resultReceiver: ResultReceiver, projectName: String ) { - val statusBarNotificationManager = StatusBarNotificationManager(context) val notificationData = statusBarNotificationManager.createProjectDownloadNotification(this, projectName) statusBarNotificationManager.abortProgressNotificationWithMessage(context, notificationData, R.string.error_project_download) resultReceiver.send(ERROR_CODE, Bundle()) } + private val statusBarNotificationManager by lazy { StatusBarNotificationManager(this) } + private fun downloadProgressCallback( context: Context, resultReceiver: ResultReceiver, notificationData: NotificationData, progress: Long ) { - StatusBarNotificationManager(context) + statusBarNotificationManager .showOrUpdateNotification(context, notificationData, progress.toInt(), null) val bundle = Bundle() bundle.putInt(UPDATE_PROGRESS_EXTRA, progress.toInt()) resultReceiver.send(UPDATE_PROGRESS_CODE, bundle) } - private fun logWarning(warningMessage: String) { - Log.w(TAG, warningMessage) + private fun extractProjectIdFromUrl(url: String): String? = ProjectIdUtils.extractUuidFromString(url) + + private fun setRemixUrlInProject(projectDir: File, serverProjectId: String) { + try { + val shareUrl = Constants.SHARE_PROJECT_URL + serverProjectId + val codeXml = File(projectDir, Constants.CODE_XML_FILE_NAME) + val content = codeXml.readText() + val replacement = "$shareUrl" + val updatedContent = when { + ProjectIdUtils.URL_TAG_REGEX.containsMatchIn(content) -> + content.replace(ProjectIdUtils.URL_TAG_REGEX, replacement) + content.contains("") -> content.replace("", replacement) + content.contains("") -> content.replace("", replacement) + else -> return + } + codeXml.writeText(updatedContent) + } catch (e: IOException) { + Log.w(TAG, "Failed to set remix URL in project: ${e.message}") + } } } 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 cf55e3f7d48..489d5b10782 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,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -23,20 +23,25 @@ package org.catrobat.catroid.transfers.project -import android.content.SharedPreferences import android.util.Log +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody 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.common.FlavoredConstants import org.catrobat.catroid.io.ProjectAndSceneScreenshotLoader import org.catrobat.catroid.io.ZipArchiver +import org.catrobat.catroid.retrofit.WebService import org.catrobat.catroid.utils.ImageEditing -import org.catrobat.catroid.web.ServerCalls +import org.catrobat.catroid.utils.ProjectIdUtils +import org.catrobat.catroid.utils.Utils +import org.catrobat.catroid.web.ProgressRequestBody 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 @@ -45,18 +50,17 @@ 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 + private val webService: WebService ) { fun start( successCallback: UploadProjectSuccessCallback, - errorCallback: UploadProjectErrorCallback + errorCallback: UploadProjectErrorCallback, + progressCallback: ((percent: Int) -> Unit)? = null ) { val projectArchive = zipProjectToArchive(projectDirectory, archiveDirectory) if (projectArchive == null) { @@ -64,43 +68,42 @@ class ProjectUpload( 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 - ) + try { + val checksum = Utils.md5Checksum(projectArchive) + val textPlain = MediaType.parse(MEDIA_TYPE_TEXT_PLAIN) + var fileBody: RequestBody = RequestBody.create(MediaType.parse("application/zip"), projectArchive) + if (progressCallback != null) { + fileBody = ProgressRequestBody(fileBody, progressCallback) + } + val filePart = MultipartBody.Part.createFormData("file", UPLOAD_FILE_NAME, fileBody) + val checksumPart = RequestBody.create(textPlain, checksum) + val flavorPart = RequestBody.create(textPlain, FlavoredConstants.FLAVOR_NAME) + val resolvedProjectId = readProjectId() + val projectIdPart = resolvedProjectId?.let { + RequestBody.create(textPlain, it) } - ) - } - 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 - ) + val response = webService.uploadProject(filePart, checksumPart, flavorPart, projectId = projectIdPart).execute() + + if (response.isSuccessful) { + val returnedProjectId = response.body()?.id ?: "" + if (returnedProjectId.isNotEmpty()) { + saveProjectId(returnedProjectId) + } + successCallback(returnedProjectId) + if (!projectArchive.delete()) { + Log.w(TAG, "Failed to delete project archive: ${projectArchive.absolutePath}") + } + } else { + val errorBody = response.errorBody()?.string() ?: UPLOAD_FAILED_MESSAGE + errorCallback(response.code(), errorBody) + } + } catch (e: IOException) { + Log.e(TAG, UPLOAD_FAILED_MESSAGE, e) + errorCallback(UPLOAD_NETWORK_ERROR, e.message ?: UPLOAD_FAILED_MESSAGE) + } } private fun scaleSceneScreenshots(projectName: String, sceneNames: Array?) { @@ -117,8 +120,10 @@ class ProjectUpload( 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 } + val fileList = projectDirectory.listFiles() ?: emptyArray() + val filteredFileList = fileList.filter { file -> + file.name != DEVICE_VARIABLE_JSON_FILE_NAME && file.name != SERVER_PROJECT_ID_FILE + } zipArchiver.zip(archiveDirectory, filteredFileList.toTypedArray()) archiveDirectory @@ -129,9 +134,55 @@ class ProjectUpload( } } + private fun readProjectId(): String? { + return try { + val idFile = File(projectDirectory, SERVER_PROJECT_ID_FILE) + val id = idFile.readText().trim() + if (id.isNotBlank() && ProjectIdUtils.UUID_REGEX.matches(id)) { + return id + } + + val codeXml = File(projectDirectory, Constants.CODE_XML_FILE_NAME) + val content = codeXml.readText() + val urlMatch = ProjectIdUtils.URL_TAG_REGEX.find(content) ?: return null + val urlContent = urlMatch.groupValues[1] + if (urlContent.isBlank()) return null + ProjectIdUtils.extractUuidFromString(urlContent) + } catch (e: IOException) { + Log.w(TAG, "Failed to read project ID: ${e.message}") + null + } + } + + private fun saveProjectId(projectId: String) { + try { + val idFile = File(projectDirectory, SERVER_PROJECT_ID_FILE) + idFile.writeText(projectId) + + val codeXml = File(projectDirectory, Constants.CODE_XML_FILE_NAME) + val shareUrl = Constants.SHARE_PROJECT_URL + projectId + val content = codeXml.readText() + val replacement = "$shareUrl" + val updatedContent = when { + ProjectIdUtils.URL_TAG_REGEX.containsMatchIn(content) -> + content.replace(ProjectIdUtils.URL_TAG_REGEX, replacement) + content.contains("") -> content.replace("", replacement) + content.contains("") -> content.replace("", replacement) + else -> return + } + codeXml.writeText(updatedContent) + } catch (e: IOException) { + Log.w(TAG, "Failed to update project URL in code.xml: ${e.message}") + } + } + companion object { private val TAG = ProjectUpload::class.java.simpleName + private const val SERVER_PROJECT_ID_FILE = ".server_project_id" + private const val MEDIA_TYPE_TEXT_PLAIN = "text/plain" const val UPLOAD_ZIP_ERROR = 32_202 const val UPLOAD_ZIP_ERROR_MESSAGE = "Failed to zip directory for upload" + const val UPLOAD_FAILED_MESSAGE = "Upload failed" + const val UPLOAD_NETWORK_ERROR = 32_203 } } 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 index 3d972ce56fa..1ff65d51e8e 100644 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadService.kt +++ b/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadService.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -29,64 +29,53 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build 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_PLUS 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.retrofit.WebService 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 org.koin.android.ext.android.inject import java.io.File -import java.util.Locale const val UPLOAD_FILE_NAME = "upload$CATROBAT_EXTENSION" class ProjectUploadService : IntentService("ProjectUploadService") { + private val webService: WebService by inject() + override fun onHandleIntent(projectUploadIntent: Intent?) { val intent = projectUploadIntent - ?: return logWarning("Called ProjectUploadService with null intent!") + ?: return Unit.also { Log.w(TAG, "Called ProjectUploadService with null intent!") } val projectPath = intent.getStringExtra(EXTRA_PROJECT_PATH) - ?: return logWarning("Called ProjectUploadService without project path!") + ?: return Unit.also { Log.w(TAG, "Called ProjectUploadService without project path!") } val projectDirectory = File(projectPath) - if (projectDirectory.listFiles().isEmpty()) { - return logWarning("Called ProjectUploadService with empty project directory!") + if (projectDirectory.listFiles().isNullOrEmpty()) { + return Unit.also { Log.w(TAG, "Called ProjectUploadService with empty project directory!") } } val resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER) as? ResultReceiver - ?: return logWarning("Called ProjectUploadService without resultReceiver!") + ?: return Unit.also { Log.w(TAG, "Called ProjectUploadService without resultReceiver!") } val projectName = intent.getStringExtra(EXTRA_UPLOAD_NAME) - ?: return logWarning("Called ProjectUploadService with empty project name!") + ?: return Unit.also { Log.w(TAG, "Called ProjectUploadService with empty project name!") } val notificationID = StatusBarNotificationManager.getNextNotificationID() startForeground( @@ -99,15 +88,12 @@ class ProjectUploadService : IntentService("ProjectUploadService") { 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(), @@ -115,9 +101,14 @@ class ProjectUploadService : IntentService("ProjectUploadService") { applicationContext.resources.getDimensionPixelSize(R.dimen.project_thumbnail_width), applicationContext.resources.getDimensionPixelSize(R.dimen.project_thumbnail_height) ), - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this), - serverCalls = ServerCalls(CatrobatWebClient.client) + webService = webService ).start( + progressCallback = { percent -> + val progressBundle = Bundle().apply { + putInt(Constants.EXTRA_UPLOAD_PROGRESS, percent) + } + resultReceiver.send(Constants.UPLOAD_PROGRESS_RESULT_CODE, progressBundle) + }, successCallback = { projectId -> Log.v(TAG, "Upload successful") stopForeground(true) @@ -134,16 +125,12 @@ class ProjectUploadService : IntentService("ProjectUploadService") { ToastUtil.showError(this, resources.getString(R.string.error_project_upload) + " " + errorMessage) StatusBarNotificationManager(applicationContext) .createUploadRejectedNotification(applicationContext, errorCode, errorMessage, reUploadBundle) - resultReceiver.send(0, null) + val errorResult = Bundle().apply { putInt(EXTRA_ERROR_CODE, errorCode) } + resultReceiver.send(0, errorResult) } ) } - override fun onDestroy() { - Utils.invalidateLoginTokenIfUserRestricted(applicationContext) - super.onDestroy() - } - private fun createUploadNotification(programName: String): Notification { StatusBarNotificationManager(applicationContext).createNotificationChannel(applicationContext) @@ -151,19 +138,11 @@ class ProjectUploadService : IntentService("ProjectUploadService") { uploadIntent.action = Intent.ACTION_MAIN uploadIntent = uploadIntent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) - val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.getActivity( - applicationContext, - StatusBarNotificationManager.UPLOAD_PENDING_INTENT_REQUEST_CODE, - uploadIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } else { - PendingIntent.getActivity( - applicationContext, - StatusBarNotificationManager.UPLOAD_PENDING_INTENT_REQUEST_CODE, - uploadIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } + val pendingIntent = PendingIntent.getActivity( + applicationContext, + StatusBarNotificationManager.UPLOAD_PENDING_INTENT_REQUEST_CODE, + uploadIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) return NotificationCompat.Builder(applicationContext, StatusBarNotificationManager.CHANNEL_ID) .setContentIntent(pendingIntent) @@ -181,20 +160,11 @@ class ProjectUploadService : IntentService("ProjectUploadService") { var uploadIntent = Intent(applicationContext, MainMenuActivity::class.java) uploadIntent.action = Intent.ACTION_MAIN uploadIntent = uploadIntent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) - val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.getActivity( - applicationContext, - StatusBarNotificationManager.UPLOAD_PENDING_INTENT_REQUEST_CODE, - uploadIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } else { - PendingIntent.getActivity( - applicationContext, - StatusBarNotificationManager.UPLOAD_PENDING_INTENT_REQUEST_CODE, - uploadIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } - + val pendingIntent = PendingIntent.getActivity( + applicationContext, + StatusBarNotificationManager.UPLOAD_PENDING_INTENT_REQUEST_CODE, + uploadIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) return NotificationCompat.Builder(applicationContext, StatusBarNotificationManager.CHANNEL_ID) .setContentIntent(pendingIntent) @@ -209,28 +179,8 @@ class ProjectUploadService : IntentService("ProjectUploadService") { .build() } - private fun getUserEmail(provider: String?): String { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) - - val email = when (provider) { - GOOGLE_PLUS -> 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 + const val EXTRA_ERROR_CODE = "errorCode" } } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt index fabdf5eef7a..c2267c1ca7f 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt @@ -232,14 +232,12 @@ class ProjectActivity : BaseCastActivity() { addSpriteFromUri(uri) } - SPRITE_LIBRARY -> { - uri = Uri.fromFile(File(data!!.getStringExtra(WebViewActivity.MEDIA_FILE_PATH))) - addSpriteFromUri(uri) + SPRITE_LIBRARY -> getMediaFilePaths(data!!).forEach { path -> + addSpriteFromUri(Uri.fromFile(File(path))) } - SPRITE_OBJECT -> { - uri = Uri.fromFile(File(data!!.getStringExtra(WebViewActivity.MEDIA_FILE_PATH))) - addObjectFromUri(uri) + SPRITE_OBJECT -> getMediaFilePaths(data!!).forEach { path -> + addObjectFromUri(Uri.fromFile(File(path))) } SPRITE_FILE -> { @@ -274,6 +272,13 @@ class ProjectActivity : BaseCastActivity() { } } + private fun getMediaFilePaths(data: Intent): List { + val paths = data.getStringArrayListExtra(WebViewActivity.MEDIA_FILE_PATHS) + if (!paths.isNullOrEmpty()) return paths + val single = data.getStringExtra(WebViewActivity.MEDIA_FILE_PATH) + return if (single != null) listOf(single) else emptyList() + } + private fun addSpriteFromUri(uri: Uri?, imageExtension: String = DEFAULT_IMAGE_EXTENSION) { addSpriteObjectFromUri(uri, imageExtension, false) } 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 4c8007c789f..0f058a56519 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/ProjectUploadActivity.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/ProjectUploadActivity.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -22,6 +22,11 @@ */ package org.catrobat.catroid.ui +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import android.content.ActivityNotFoundException import android.content.DialogInterface import android.content.Intent @@ -38,6 +43,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ImageView +import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.google.common.base.Charsets import com.google.common.io.Files @@ -54,10 +60,9 @@ import org.catrobat.catroid.io.ProjectAndSceneScreenshotLoader import org.catrobat.catroid.io.asynctask.ProjectLoader.ProjectLoadListener import org.catrobat.catroid.io.asynctask.loadProject import org.catrobat.catroid.io.asynctask.renameProject -import org.catrobat.catroid.transfers.CheckTokenTask -import org.catrobat.catroid.transfers.CheckTokenTask.TokenCheckListener -import org.catrobat.catroid.transfers.GetTagsTask -import org.catrobat.catroid.transfers.GetTagsTask.TagResponseListener +import org.catrobat.catroid.retrofit.WebService +import org.catrobat.catroid.web.LoginRepository +import org.catrobat.catroid.transfers.project.ProjectUploadService import org.catrobat.catroid.transfers.project.ResultReceiverWrapper import org.catrobat.catroid.transfers.project.ResultReceiverWrapperInterface import org.catrobat.catroid.ui.controller.ProjectUploadController @@ -65,7 +70,6 @@ import org.catrobat.catroid.ui.controller.ProjectUploadController.ProjectUploadI import org.catrobat.catroid.utils.FileMetaDataExtractor import org.catrobat.catroid.utils.ToastUtil import org.catrobat.catroid.utils.Utils -import org.catrobat.catroid.web.ServerAuthenticationConstants import org.koin.android.ext.android.inject import java.io.File import java.io.FileOutputStream @@ -87,6 +91,9 @@ private const val LICENSE_TO_PLAY_URL = private const val PROGRAM_NAME_START_TAG = "" private const val PROGRAM_NAME_END_TAG = "" private const val THUMBNAIL_SIZE = 100 +private const val COMPLETE_PERCENT = 100 +private const val BYTES_PER_MB = 1024 * 1024 +private const val UPLOAD_RATING_THRESHOLD = 2 private val TAG = ProjectUploadActivity::class.java.simpleName const val PROJECT_DIR = "projectDir" @@ -95,11 +102,12 @@ const val NUMBER_OF_UPLOADED_PROJECTS = "number_of_uploaded_projects" open class ProjectUploadActivity : BaseActivity(), ProjectLoadListener, - TokenCheckListener, - TagResponseListener, ResultReceiverWrapperInterface, ProjectUploadInterface { + private val loginRepository: LoginRepository by inject() + private val webService: WebService by inject() + private lateinit var project: Project private lateinit var xmlFile: File private lateinit var xml: String @@ -108,6 +116,7 @@ open class ProjectUploadActivity : BaseActivity(), private lateinit var apiMatcher: Matcher private var uploadProgressDialog: AlertDialog? = null + private var verifyTokenJob: Job? = null private val uploadResultReceiver = ResultReceiverWrapper(this, Handler()) private val nameInputTextWatcher = NameInputTextWatcher() @@ -159,7 +168,7 @@ open class ProjectUploadActivity : BaseActivity(), verifyUserIdentity() } - private fun onCreateView() { + protected open fun onCreateView() { val thumbnailSize = THUMBNAIL_SIZE val screenshotLoader = ProjectAndSceneScreenshotLoader( thumbnailSize, @@ -173,9 +182,23 @@ open class ProjectUploadActivity : BaseActivity(), findViewById(R.id.project_image_view) ) + val projectSizeBytes = FileMetaDataExtractor.getSizeOfFileOrDirectoryInByte(project.directory) binding.projectSizeView.text = FileMetaDataExtractor.getSizeAsString(project.directory, this) + if (projectSizeBytes > Constants.UPLOAD_MAX_SIZE_BYTES) { + val maxSizeMb = Constants.UPLOAD_MAX_SIZE_BYTES / BYTES_PER_MB + binding.projectSizeView.text = getString( + R.string.error_project_too_large_for_upload, + FileMetaDataExtractor.getSizeAsString(project.directory, this), + maxSizeMb.toString() + ) + binding.projectSizeView.setTextColor(getColor(android.R.color.holo_red_dark)) + setShowProgressBar(false) + setNextButtonEnabled(false) + return + } + if (!projectManager.isChangedProject(project)) { showUploadIsUnchangedDialog() } @@ -215,6 +238,7 @@ open class ProjectUploadActivity : BaseActivity(), } override fun onDestroy() { + verifyTokenJob?.cancel() if (uploadProgressDialog?.isShowing == true) { uploadProgressDialog?.dismiss() } @@ -478,11 +502,9 @@ open class ProjectUploadActivity : BaseActivity(), loadBackup() projectManager.resetChangedFlag(project) } - .setNegativeButton(R.string.done) { _, _ -> + .setNegativeButton(R.string.cancel) { _, _ -> loadBackup() projectManager.resetChangedFlag(project) - MainMenuActivity.surveyCampaign?.showSurvey(this) - finish() } .setCancelable(false) @@ -500,20 +522,48 @@ open class ProjectUploadActivity : BaseActivity(), startService(intent) } + private fun handleUploadProgress(progress: Int) { + val dialog = uploadProgressDialog ?: return + val progressBar = dialog.findViewById(R.id.dialog_upload_progress_progressbar) + val percentView = dialog.findViewById(R.id.dialog_upload_progress_percent) + progressBar?.isIndeterminate = false + progressBar?.progress = progress + percentView?.text = getString(R.string.upload_progress_percent, progress) + if (progress >= COMPLETE_PERCENT) { + progressBar?.visibility = View.GONE + percentView?.visibility = View.GONE + dialog.findViewById(R.id.dialog_upload_processing_container)?.visibility = View.VISIBLE + } + } + + private fun handleUploadFailure(errorCode: Int) { + val dialog = uploadProgressDialog ?: return + dialog.findViewById(R.id.dialog_upload_progress_progressbar)?.visibility = View.GONE + dialog.findViewById(R.id.dialog_upload_progress_percent)?.visibility = View.GONE + dialog.findViewById(R.id.dialog_upload_processing_container)?.visibility = View.GONE + val failedMessage = dialog.findViewById(R.id.dialog_upload_message_failed) + failedMessage?.visibility = View.VISIBLE + if (errorCode == Constants.ERROR_TOO_MANY_REQUESTS) { + failedMessage?.setText(R.string.error_project_upload_rate_limit) + } + val image = dialog.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_PROGRESS_RESULT_CODE && resultData != null) { + handleUploadProgress(resultData.getInt(Constants.EXTRA_UPLOAD_PROGRESS, 0)) + return + } 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 + handleUploadFailure(resultData?.getInt(ProjectUploadService.EXTRA_ERROR_CODE, 0) ?: 0) return } + uploadProgressDialog?.findViewById(R.id.dialog_upload_processing_container)?.visibility = View.GONE + val projectId = resultData.getString(Constants.EXTRA_PROJECT_ID) val positiveButton = uploadProgressDialog?.getButton(DialogInterface.BUTTON_POSITIVE) @@ -528,6 +578,16 @@ open class ProjectUploadActivity : BaseActivity(), } positiveButton?.isEnabled = true + + val negativeButton = uploadProgressDialog?.getButton(DialogInterface.BUTTON_NEGATIVE) + negativeButton?.setText(R.string.done) + negativeButton?.setOnClickListener { + loadBackup() + projectManager.resetChangedFlag(project) + MainMenuActivity.surveyCampaign?.showSurvey(this) + finish() + } + uploadProgressDialog?.findViewById(R.id.dialog_upload_progress_progressbar)?.visibility = View.GONE @@ -541,7 +601,7 @@ open class ProjectUploadActivity : BaseActivity(), .putInt(NUMBER_OF_UPLOADED_PROJECTS, numberOfUploadedProjects) .apply() - if (numberOfUploadedProjects != 2) { + if (numberOfUploadedProjects != UPLOAD_RATING_THRESHOLD) { return } @@ -578,34 +638,21 @@ open class ProjectUploadActivity : BaseActivity(), } protected open fun verifyUserIdentity() { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) - val token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN) - val username = sharedPreferences.getString(Constants.USERNAME, Constants.NO_USERNAME) - val isTokenSetInPreferences = - token != Constants.NO_TOKEN && token?.length == ServerAuthenticationConstants.TOKEN_LENGTH && token != ServerAuthenticationConstants.TOKEN_CODE_INVALID - if (isTokenSetInPreferences) { - CheckTokenTask(this) - .execute(token, username) - } else { + if (!loginRepository.isLoggedIn()) { startSignInWorkflow() + return } - } - - override fun onTokenCheckComplete(tokenValid: Boolean, connectionFailed: Boolean) { - if (connectionFailed) { - if (!tokenValid) { - ToastUtil.showError(this, R.string.error_session_expired) - Utils.logoutUser(this) - startSignInWorkflow() - } else { - ToastUtil.showError(this, R.string.error_internet_connection) - return + verifyTokenJob = CoroutineScope(Dispatchers.IO).launch { + val isValid = loginRepository.validateToken() + withContext(Dispatchers.Main) { + if (isValid) { + onCreateView() + } else { + loginRepository.clearLocalSession() + startSignInWorkflow() + } } - } else if (!tokenValid) { - startSignInWorkflow() - return } - onCreateView() } fun startSignInWorkflow() { @@ -625,13 +672,17 @@ open class ProjectUploadActivity : BaseActivity(), } private fun getTags() { - val getTagsTask = GetTagsTask() - getTagsTask.setOnTagsResponseListener(this) - getTagsTask.execute() - } - - override fun onTagsReceived(tags: List) { - this.tags = tags + CoroutineScope(Dispatchers.IO).launch { + try { + val response = webService.getProjectTags().execute() + if (response.isSuccessful) { + val tagNames = response.body()?.data?.map { it.text } ?: emptyList() + withContext(Dispatchers.Main) { this@ProjectUploadActivity.tags = tagNames } + } + } catch (_: Exception) { + // Tags are optional + } + } } inner class NameInputTextWatcher : TextWatcher { diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SignInActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SignInActivity.java index 50acd8207b4..dec175f3706 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SignInActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SignInActivity.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -34,7 +34,6 @@ import org.catrobat.catroid.common.Constants; import org.catrobat.catroid.transfers.GoogleLoginHandler; import org.catrobat.catroid.ui.recyclerview.dialog.login.LoginDialogFragment; -import org.catrobat.catroid.ui.recyclerview.dialog.login.RegistrationDialogFragment; import org.catrobat.catroid.ui.recyclerview.dialog.login.SignInCompleteListener; import org.catrobat.catroid.utils.Utils; @@ -42,6 +41,7 @@ public class SignInActivity extends BaseActivity implements SignInCompleteListener { public static final String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL"; + public static final String REGISTRATION_PATH = "register"; private GoogleLoginHandler googleLoginHandler; @@ -71,25 +71,25 @@ public void onButtonClick(final View view) { } private void onButtonClickForRealThisTime(View view) { - switch (view.getId()) { - case R.id.sign_in_login: - LoginDialogFragment logInDialog = new LoginDialogFragment(); - logInDialog.setSignInCompleteListener(this); - logInDialog.show(getSupportFragmentManager(), LoginDialogFragment.TAG); - break; - case R.id.sign_in_register: - RegistrationDialogFragment registrationDialog = new RegistrationDialogFragment(); - registrationDialog.setSignInCompleteListener(this); - registrationDialog.show(getSupportFragmentManager(), RegistrationDialogFragment.TAG); - break; - case R.id.sign_in_google_login_button: - startActivityForResult(googleLoginHandler.getGoogleSignInClient().getSignInIntent(), REQUEST_CODE_GOOGLE_SIGNIN); - break; - default: - break; + int id = view.getId(); + if (id == R.id.sign_in_login) { + LoginDialogFragment logInDialog = new LoginDialogFragment(); + logInDialog.setSignInCompleteListener(this); + logInDialog.show(getSupportFragmentManager(), LoginDialogFragment.TAG); + } else if (id == R.id.sign_in_register) { + openRegistrationInWebView(); + } else if (id == R.id.sign_in_google_login_button) { + startActivityForResult(googleLoginHandler.getGoogleSignInClient().getSignInIntent(), REQUEST_CODE_GOOGLE_SIGNIN); } } + private void openRegistrationInWebView() { + String url = Constants.BASE_APP_URL_HTTPS + REGISTRATION_PATH; + Intent intent = new Intent(this, WebViewActivity.class); + intent.putExtra(WebViewActivity.INTENT_PARAMETER_URL, url); + startActivity(intent); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { googleLoginHandler.onActivityResult(requestCode, resultCode, data); diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java index f499330634c..5ae038aa1a3 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -103,6 +103,7 @@ import static org.catrobat.catroid.ui.SpriteActivityOnTabSelectedListenerKt.loadFragment; import static org.catrobat.catroid.ui.SpriteActivityOnTabSelectedListenerKt.removeTabLayout; import static org.catrobat.catroid.ui.WebViewActivity.MEDIA_FILE_PATH; +import static org.catrobat.catroid.ui.WebViewActivity.MEDIA_FILE_PATHS; import static org.catrobat.catroid.visualplacement.VisualPlacementActivity.CHANGED_COORDINATES; import static org.catrobat.catroid.visualplacement.VisualPlacementActivity.X_COORDINATE_BUNDLE_ARGUMENT; import static org.catrobat.catroid.visualplacement.VisualPlacementActivity.Y_COORDINATE_BUNDLE_ARGUMENT; @@ -366,8 +367,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { addSpriteFromUri(uri); break; case SPRITE_LIBRARY: - uri = Uri.fromFile(new File(data.getStringExtra(MEDIA_FILE_PATH))); - addSpriteFromUri(uri); + for (String path : getMediaFilePaths(data)) { + addSpriteFromUri(Uri.fromFile(new File(path))); + } break; case SPRITE_FILE: uri = data.getData(); @@ -382,8 +384,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { addBackgroundFromUri(uri); break; case BACKGROUND_LIBRARY: - uri = Uri.fromFile(new File(data.getStringExtra(MEDIA_FILE_PATH))); - addBackgroundFromUri(uri); + for (String path : getMediaFilePaths(data)) { + addBackgroundFromUri(Uri.fromFile(new File(path))); + } break; case BACKGROUND_FILE: uri = data.getData(); @@ -399,8 +402,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { setUndoMenuItemVisibility(true); break; case LOOK_LIBRARY: - uri = Uri.fromFile(new File(data.getStringExtra(MEDIA_FILE_PATH))); - addLookFromUri(uri); + for (String path : getMediaFilePaths(data)) { + addLookFromUri(Uri.fromFile(new File(path))); + } setUndoMenuItemVisibility(true); break; case LOOK_FILE: @@ -419,8 +423,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { addSoundFromUri(uri); break; case SOUND_LIBRARY: - uri = Uri.fromFile(new File(data.getStringExtra(MEDIA_FILE_PATH))); - addSoundFromUri(uri); + for (String path : getMediaFilePaths(data)) { + addSoundFromUri(Uri.fromFile(new File(path))); + } break; case REQUEST_CODE_VISUAL_PLACEMENT: Bundle extras = data.getExtras(); @@ -466,6 +471,15 @@ public void registerOnNewSoundListener(NewItemInterface listener) { onNewSoundListener = listener; } + private java.util.List getMediaFilePaths(Intent data) { + java.util.ArrayList paths = data.getStringArrayListExtra(MEDIA_FILE_PATHS); + if (paths != null && !paths.isEmpty()) { + return paths; + } + String singlePath = data.getStringExtra(MEDIA_FILE_PATH); + return singlePath != null ? java.util.Collections.singletonList(singlePath) : java.util.Collections.emptyList(); + } + private void addSpriteFromUri(final Uri uri) { addSpriteFromUri(uri, DEFAULT_IMAGE_EXTENSION); } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/WebViewActivity.java b/catroid/src/main/java/org/catrobat/catroid/ui/WebViewActivity.java index 71c02643b9a..758e79c8884 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/WebViewActivity.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/WebViewActivity.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -27,46 +27,57 @@ import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Environment; -import android.preference.PreferenceManager; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.KeyEvent; import android.webkit.CookieManager; import android.webkit.URLUtil; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.res.ResourcesCompat; import com.google.common.annotations.VisibleForTesting; import org.catrobat.catroid.BuildConfig; +import org.catrobat.catroid.ProjectManager; import org.catrobat.catroid.R; import org.catrobat.catroid.common.Constants; import org.catrobat.catroid.common.FlavoredConstants; +import org.catrobat.catroid.io.ZipArchiver; +import org.catrobat.catroid.utils.FileMetaDataExtractor; import org.catrobat.catroid.utils.MediaDownloader; import org.catrobat.catroid.utils.ProjectDownloadUtil; import org.catrobat.catroid.utils.ToastUtil; import org.catrobat.catroid.utils.Utils; +import org.catrobat.catroid.web.CatrobatWebClient; import org.catrobat.catroid.web.Cookie; import org.catrobat.catroid.web.GlobalProjectDownloadQueue; import org.catrobat.catroid.web.ProjectDownloader; import java.io.File; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.res.ResourcesCompat; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; import static org.catrobat.catroid.common.Constants.MEDIA_LIBRARY_CACHE_DIRECTORY; +import static org.catrobat.catroid.common.FlavoredConstants.CATROBAT_CONTENT_DOWNLOAD_URL; import static org.catrobat.catroid.common.FlavoredConstants.CATROBAT_HELP_URL; import static org.catrobat.catroid.common.FlavoredConstants.LIBRARY_BASE_URL; -import static org.catrobat.catroid.common.FlavoredConstants.CATROBAT_CONTENT_DOWNLOAD_URL; import static org.catrobat.catroid.ui.MainMenuActivity.surveyCampaign; @SuppressLint("SetJavaScriptEnabled") @@ -78,14 +89,34 @@ public class WebViewActivity extends AppCompatActivity { public static final String INTENT_FORCE_OPEN_IN_APP = "openInApp"; public static final String ANDROID_APPLICATION_EXTENSION = ".apk"; public static final String MEDIA_FILE_PATH = "media_file_path"; + public static final String MEDIA_FILE_PATHS = "media_file_paths"; private static final String PACKAGE_NAME_WHATSAPP = "com.whatsapp"; + private static final Pattern PROJECT_DOWNLOAD_PATTERN = + Pattern.compile("/api/projects/([a-zA-Z0-9-]+)/catrobat"); private WebView webView; + private org.catrobat.catroid.web.JwtTokenStore tokenStore; private boolean allowGoBack = false; private boolean forceOpenInApp = false; - private ProgressDialog progressDialog; private ProgressDialog webViewLoadingDialog; private Intent resultIntent = new Intent(); + private final java.util.ArrayList downloadedMediaPaths = new java.util.ArrayList<>(); + private ValueCallback fileUploadCallback; + private final ActivityResultLauncher fileChooserLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (fileUploadCallback == null) { + return; + } + Uri[] uris = null; + if (result.getResultCode() == RESULT_OK && result.getData() != null) { + String dataString = result.getData().getDataString(); + if (dataString != null) { + uris = new Uri[]{Uri.parse(dataString)}; + } + } + fileUploadCallback.onReceiveValue(uris); + fileUploadCallback = null; + }); @Override protected void onCreate(Bundle savedInstanceState) { @@ -102,7 +133,21 @@ protected void onCreate(Bundle savedInstanceState) { webView = findViewById(R.id.webView); webView.setBackgroundColor(ResourcesCompat.getColor(getResources(), R.color.app_background, null)); webView.setWebViewClient(new MyWebViewClient()); + webView.setWebChromeClient(new WebChromeClient() { + @Override + public boolean onShowFileChooser(WebView webView, ValueCallback callback, + FileChooserParams fileChooserParams) { + if (fileUploadCallback != null) { + fileUploadCallback.onReceiveValue(null); + } + fileUploadCallback = callback; + Intent intent = fileChooserParams.createIntent(); + fileChooserLauncher.launch(intent); + return true; + } + }); webView.getSettings().setJavaScriptEnabled(true); + webView.getSettings().setDomStorageEnabled(true); String language = String.valueOf(Constants.CURRENT_CATROBAT_LANGUAGE_VERSION); String flavor = Constants.FLAVOR_DEFAULT; String version = Utils.getVersionName(getApplicationContext()); @@ -111,16 +156,22 @@ protected void onCreate(Bundle savedInstanceState) { webView.getSettings().setUserAgentString("Catrobat/" + language + " " + flavor + "/" + version + " Platform/" + platform + " BuildType/" + buildType); - setLoginCookies(url, PreferenceManager.getDefaultSharedPreferences(getApplicationContext()), CookieManager.getInstance()); + tokenStore = org.koin.java.KoinJavaComponent.inject(org.catrobat.catroid.web.JwtTokenStore.class).getValue(); + setLoginCookies(url, CookieManager.getInstance(), tokenStore.getAccessToken()); webView.loadUrl(url); webView.setDownloadListener((downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> { - // TODO: Delete only this if case, when the Catrobat share server completely closes - if (getExtensionFromContentDisposition(contentDisposition).contains(Constants.CATROBAT_EXTENSION) && !downloadUrl.contains(LIBRARY_BASE_URL)) { + if (downloadUrl != null && downloadUrl.startsWith("blob:")) { + return; + } + if (contentDisposition != null && getExtensionFromContentDisposition(contentDisposition).contains(Constants.CATROBAT_EXTENSION) && !downloadUrl.contains(LIBRARY_BASE_URL)) { + String projectName = extractProjectNameFromContentDisposition(contentDisposition); new ProjectDownloader(GlobalProjectDownloadQueue.INSTANCE.getQueue(), downloadUrl, - ProjectDownloadUtil.INSTANCE).download(this); - } else if (downloadUrl.contains(CATROBAT_CONTENT_DOWNLOAD_URL)) { - String fileName = URLUtil.guessFileName(downloadUrl, contentDisposition, mimetype); + ProjectDownloadUtil.INSTANCE, projectName).download(this); + } else if (downloadUrl.contains(CATROBAT_CONTENT_DOWNLOAD_URL) + || downloadUrl.contains("/resources/media/") + || downloadUrl.contains("/api/media/assets/")) { + String fileName = getFilenameFromContentDisposition(contentDisposition, downloadUrl, mimetype); MEDIA_LIBRARY_CACHE_DIRECTORY.mkdirs(); if (!MEDIA_LIBRARY_CACHE_DIRECTORY.isDirectory()) { @@ -129,9 +180,11 @@ protected void onCreate(Bundle savedInstanceState) { } File file = new File(MEDIA_LIBRARY_CACHE_DIRECTORY, fileName); + downloadedMediaPaths.add(file.getAbsolutePath()); resultIntent.putExtra(MEDIA_FILE_PATH, file.getAbsolutePath()); - new MediaDownloader(this) - .startDownload(this, downloadUrl, fileName, file.getAbsolutePath()); + resultIntent.putStringArrayListExtra(MEDIA_FILE_PATHS, downloadedMediaPaths); + new MediaDownloader(WebViewActivity.this) + .startDownload(WebViewActivity.this, downloadUrl, fileName, file.getAbsolutePath()); } else { DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl)); String projectName = ProjectDownloader.Companion.getProjectNameFromUrl(downloadUrl); @@ -158,7 +211,75 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { return super.onKeyDown(keyCode, event); } - private class MyWebViewClient extends WebViewClient { + private final class MyWebViewClient extends WebViewClient { + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + String url = request.getUrl().toString(); + + Matcher projectMatcher = PROJECT_DOWNLOAD_PATTERN.matcher(url); + if (projectMatcher.find()) { + return interceptProjectDownload(url); + } + + return super.shouldInterceptRequest(view, request); + } + + private WebResourceResponse interceptProjectDownload(String url) { + try { + okhttp3.Request httpRequest = new okhttp3.Request.Builder().url(url).build(); + okhttp3.Response httpResponse = CatrobatWebClient.INSTANCE.getClient() + .newCall(httpRequest).execute(); + if (httpResponse.isSuccessful() && httpResponse.body() != null) { + String projectName = extractProjectNameFromContentDisposition( + httpResponse.header("Content-Disposition")); + if (projectName == null) { + Matcher m = PROJECT_DOWNLOAD_PATTERN.matcher(url); + projectName = m.find() ? m.group(1) : "Project"; + } + + File tempFile = new File(Constants.CACHE_DIRECTORY, + Constants.TMP_DIRECTORY_NAME + "/down.catrobat"); + if (tempFile.getParentFile() != null) { + tempFile.getParentFile().mkdirs(); + } + + okio.BufferedSink sink = okio.Okio.buffer(okio.Okio.sink(tempFile)); + sink.writeAll(httpResponse.body().source()); + sink.close(); + httpResponse.close(); + + String safeName = FileMetaDataExtractor + .encodeSpecialCharsForFileSystem(projectName); + File projectDir = new File( + FlavoredConstants.DEFAULT_ROOT_DIRECTORY, safeName); + if (projectDir.exists()) { + org.catrobat.catroid.io.StorageOperations.deleteDir(projectDir); + } + new ZipArchiver().unzip(tempFile, projectDir); + + if (!tempFile.delete()) { + Log.w(TAG, "Could not delete temp file: " + tempFile.getAbsolutePath()); + } + + final String finalName = projectName; + new Handler(Looper.getMainLooper()).post(() -> { + ProjectManager.getInstance() + .addNewDownloadedProject(finalName); + Intent resultIntent = new Intent(WebViewActivity.this, MainMenuActivity.class); + resultIntent.setAction(Intent.ACTION_MAIN); + resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + resultIntent.putExtra(Constants.EXTRA_PROJECT_NAME, finalName); + startActivity(resultIntent); + finish(); + }); + return null; + } + } catch (Exception e) { + Log.e(TAG, "Project download interception failed", e); + } + return null; + } + @Override public void onPageStarted(WebView view, String urlClient, Bitmap favicon) { if (webViewLoadingDialog == null && !allowGoBack) { @@ -167,7 +288,8 @@ public void onPageStarted(WebView view, String urlClient, Bitmap favicon) { webViewLoadingDialog.setCanceledOnTouchOutside(false); webViewLoadingDialog.setProgressStyle(android.R.style.Widget_ProgressBar_Small); webViewLoadingDialog.show(); - } else if (allowGoBack && (urlClient.equals(FlavoredConstants.BASE_URL_HTTPS) + } else if (allowGoBack && (urlClient.contains("/exit") + || urlClient.equals(FlavoredConstants.BASE_URL_HTTPS) || urlClient.equals(Constants.BASE_APP_URL_HTTPS))) { allowGoBack = false; onBackPressed(); @@ -181,6 +303,27 @@ public void onPageFinished(WebView view, String url) { webViewLoadingDialog.dismiss(); webViewLoadingDialog = null; } + syncLoginStateFromCookies(url); + } + + private void syncLoginStateFromCookies(String url) { + if (url == null || !url.contains(MAIN_URL_HTTPS)) { + return; + } + + String cookies = CookieManager.getInstance().getCookie(url); + String bearerToken = extractBearerFromCookies(cookies); + + if (bearerToken != null && !bearerToken.isEmpty() + && org.catrobat.catroid.web.JwtTokenStore.Companion.isValidJwtFormat(bearerToken)) { + if (!tokenStore.isLoggedIn()) { + tokenStore.setAccessTokenOnly(bearerToken); + } + } else if (bearerToken == null || bearerToken.isEmpty()) { + if (tokenStore.isLoggedIn()) { + tokenStore.clearTokens(); + } + } } @Override @@ -232,43 +375,44 @@ private boolean checkIfWebViewVisitExternalWebsite(String url) { } } - public void createProgressDialog(String mediaName) { - progressDialog = new ProgressDialog(this); - - progressDialog.setTitle(getString(R.string.notification_download_title_pending) + mediaName); - progressDialog.setMessage(getString(R.string.notification_download_pending)); - progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - progressDialog.setProgress(0); - progressDialog.setMax(100); - progressDialog.setProgressNumberFormat(null); - if (!isFinishing()) { - progressDialog.show(); - } + public Intent getResultIntent() { + return resultIntent; + } + + public void setResultIntent(Intent intent) { + resultIntent = intent; } - public void updateProgressDialog(long progress) { - if (progress == 100) { - if (progressDialog.isShowing() && !isFinishing()) { - progressDialog.setProgress(progressDialog.getMax()); - setResult(RESULT_OK, resultIntent); - progressDialog.dismiss(); + @VisibleForTesting + static String getFilenameFromContentDisposition(String contentDisposition, String url, String mimetype) { + if (contentDisposition != null) { + Matcher starMatcher = CONTENT_DISPOSITION_FILENAME_STAR.matcher(contentDisposition); + if (starMatcher.find()) { + try { + return URLDecoder.decode(starMatcher.group(1), StandardCharsets.UTF_8.name()); + } catch (Exception ignored) { + Log.w(TAG, "Failed to decode filename from Content-Disposition", ignored); + } + } + Matcher quotedMatcher = CONTENT_DISPOSITION_FILENAME_QUOTED.matcher(contentDisposition); + if (quotedMatcher.find()) { + return quotedMatcher.group(1); } - finish(); - } else { - progressDialog.setProgress((int) progress); } + return URLUtil.guessFileName(url, null, mimetype); } - public void dismissProgressDialog() { - progressDialog.dismiss(); - } + private static final Pattern CONTENT_DISPOSITION_FILENAME_STAR = + Pattern.compile("filename\\*\\s*=\\s*[Uu][Tt][Ff]-8''(.+?)(?:;|$)", Pattern.CASE_INSENSITIVE); + private static final Pattern CONTENT_DISPOSITION_FILENAME_QUOTED = + Pattern.compile("filename\\s*=\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); - public Intent getResultIntent() { - return resultIntent; - } - - public void setResultIntent(Intent intent) { - resultIntent = intent; + private static String extractProjectNameFromContentDisposition(String contentDisposition) { + String filename = getFilenameFromContentDisposition(contentDisposition, "", null); + if (filename != null && filename.endsWith(Constants.CATROBAT_EXTENSION)) { + return filename.substring(0, filename.length() - Constants.CATROBAT_EXTENSION.length()); + } + return filename; } // TODO: Delete this function, when the Catrobat share server completely closes @@ -283,26 +427,14 @@ private String getExtensionFromContentDisposition(String contentDisposition) { } @VisibleForTesting - public static void setLoginCookies(String url, SharedPreferences sharedPreferences, CookieManager cookieManager) { - String username = sharedPreferences.getString(Constants.USERNAME, Constants.NO_USERNAME); - String token = sharedPreferences.getString(Constants.TOKEN, Constants.NO_TOKEN); - - if (username.equals(Constants.NO_USERNAME) || token.equals(Constants.NO_TOKEN)) { + public static void setLoginCookies(String url, CookieManager cookieManager, String jwtToken) { + if (jwtToken == null || jwtToken.isEmpty()) { return; } - String encodedUsername; - try { - encodedUsername = URLEncoder.encode(username, "UTF-8"); - } catch (UnsupportedEncodingException e) { - Log.e(TAG, Log.getStackTraceString(e)); - return; - } - - Cookie usernameCookie = new Cookie(Constants.USERNAME_COOKIE_NAME, encodedUsername); - Cookie tokenCookie = new Cookie(Constants.TOKEN_COOKIE_NAME, token); - cookieManager.setCookie(url, usernameCookie.generateCookieString()); - cookieManager.setCookie(url, tokenCookie.generateCookieString()); + boolean secure = url != null && url.regionMatches(true, 0, "https://", 0, "https://".length()); + Cookie bearerCookie = new Cookie("BEARER", jwtToken, secure); + cookieManager.setCookie(url, bearerCookie.generateCookieString()); } public static void clearCookies() { @@ -310,6 +442,20 @@ public static void clearCookies() { CookieManager.getInstance().flush(); } + @VisibleForTesting + public static String extractBearerFromCookies(String cookies) { + if (cookies == null) { + return null; + } + for (String cookie : cookies.split(";")) { + String trimmed = cookie.trim(); + if (trimmed.startsWith("BEARER=")) { + return trimmed.substring("BEARER=".length()); + } + } + return null; + } + private boolean isWhatsappInstalled() { PackageManager packageManager = getPackageManager(); try { diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt index f8e62a799ee..817ec241dd1 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/fragment/ProjectOptionsFragment.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -59,6 +59,8 @@ import org.catrobat.catroid.io.asynctask.renameProject import org.catrobat.catroid.io.asynctask.saveProjectSerial import org.catrobat.catroid.merge.NewProjectNameTextWatcher import org.catrobat.catroid.ui.BottomBar.hideBottomBar +import org.catrobat.catroid.ui.ProjectUploadActivity +import org.catrobat.catroid.ui.PROJECT_DIR import org.catrobat.catroid.ui.runtimepermissions.RequiresPermissionTask import org.catrobat.catroid.utils.ToastUtil import org.catrobat.catroid.utils.notifications.StatusBarNotificationManager @@ -97,6 +99,7 @@ class ProjectOptionsFragment : Fragment() { addTags() setupProjectAspectRatio() setupProjectSaveExternal() + setupProjectUpload() setupProjectMoreDetails() setupProjectOptionDelete() @@ -159,6 +162,17 @@ class ProjectOptionsFragment : Fragment() { } } + private fun setupProjectUpload() { + binding.projectOptionsUpload.setOnClickListener { + saveProject() + project?.let { + val intent = Intent(requireContext(), ProjectUploadActivity::class.java) + intent.putExtra(PROJECT_DIR, it.directory.absolutePath) + startActivity(intent) + } + } + } + private fun setupProjectMoreDetails() { binding.projectOptionsMoreDetails.setOnClickListener { moreDetails() diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/adapter/FeaturedProjectsAdapter.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/adapter/FeaturedProjectsAdapter.kt index 062085c675e..1c7af0b4071 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/adapter/FeaturedProjectsAdapter.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/adapter/FeaturedProjectsAdapter.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -60,8 +60,8 @@ class FeaturedProjectsAdapter : RecyclerView.Adapter) * * This program is free software: you can redistribute it and/or modify @@ -33,21 +33,23 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; import com.google.android.material.textfield.TextInputLayout; import org.catrobat.catroid.R; import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.common.FlavoredConstants; -import org.catrobat.catroid.transfers.LoginTask; import org.catrobat.catroid.ui.ViewUtils; import org.catrobat.catroid.ui.WebViewActivity; -import org.catrobat.catroid.web.ServerCalls; +import org.catrobat.catroid.web.LoginHelper; +import org.catrobat.catroid.web.LoginRepository; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; +import kotlin.Lazy; + +import static org.koin.java.KoinJavaComponent.inject; -public class LoginDialogFragment extends DialogFragment implements LoginTask.OnLoginListener { +public class LoginDialogFragment extends DialogFragment { public static final String TAG = LoginDialogFragment.class.getSimpleName(); public static final String PASSWORD_FORGOTTEN_PATH = "reset-password"; @@ -60,6 +62,8 @@ public class LoginDialogFragment extends DialogFragment implements LoginTask.OnL private SignInCompleteListener signInCompleteListener; + private final Lazy loginRepository = inject(LoginRepository.class); + public void setSignInCompleteListener(SignInCompleteListener signInCompleteListener) { this.signInCompleteListener = signInCompleteListener; } @@ -150,35 +154,42 @@ public void afterTextChanged(Editable s) { } @Override - public void onLoginComplete() { - Bundle bundle = new Bundle(); - bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, Constants.NO_OAUTH_PROVIDER); - signInCompleteListener.onLoginSuccessful(bundle); - dismiss(); - } - - @Override - public void onLoginFailed(String msg) { - passwordEditText.setError(msg); - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + public void onCancel(DialogInterface dialog) { + LoginHelper.cancel(); + signInCompleteListener.onLoginCancel(); } @Override - public void onCancel(DialogInterface dialog) { - signInCompleteListener.onLoginCancel(); + public void onDestroyView() { + LoginHelper.cancel(); + super.onDestroyView(); } private void onLoginButtonClick() { String username = usernameEditText.getText().toString().replaceAll("\\s", ""); String password = passwordEditText.getText().toString(); - LoginTask loginTask = new LoginTask(getActivity(), username, password); - loginTask.setOnLoginListener(this); - loginTask.execute(); + + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + + LoginHelper.performLogin( + loginRepository.getValue(), + username, + password, + () -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, Constants.NO_OAUTH_PROVIDER); + signInCompleteListener.onLoginSuccessful(bundle); + dismiss(); + }, + errorMsg -> { + passwordEditText.setError(errorMsg); + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); + } + ); } private void onPasswordForgottenButtonClick() { - String baseUrl = ServerCalls.useTestUrl ? ServerCalls.BASE_URL_TEST_HTTPS : FlavoredConstants.BASE_URL_HTTPS; - String url = baseUrl + PASSWORD_FORGOTTEN_PATH; + String url = Constants.BASE_APP_URL_HTTPS + PASSWORD_FORGOTTEN_PATH; Intent intent = new Intent(getActivity(), WebViewActivity.class); intent.putExtra(WebViewActivity.INTENT_PARAMETER_URL, url); diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/dialog/login/OAuthUsernameDialogFragment.java b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/dialog/login/OAuthUsernameDialogFragment.java deleted file mode 100644 index 1fc5c180ef9..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/dialog/login/OAuthUsernameDialogFragment.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.recyclerview.dialog.login; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.Button; - -import com.google.android.material.textfield.TextInputLayout; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.transfers.CheckUserNameAvailableTask; -import org.catrobat.catroid.transfers.GoogleExchangeCodeTask; -import org.catrobat.catroid.transfers.GoogleLogInTask; -import org.catrobat.catroid.ui.recyclerview.dialog.textwatcher.DialogInputWatcher; - -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; - -public class OAuthUsernameDialogFragment extends DialogFragment implements - CheckUserNameAvailableTask.OnCheckUserNameAvailableCompleteListener, - GoogleExchangeCodeTask.OnGoogleExchangeCodeCompleteListener, - GoogleLogInTask.OnGoogleServerLogInCompleteListener { - - public static final String TAG = OAuthUsernameDialogFragment.class.getSimpleName(); - - private TextInputLayout inputLayout; - private String openAuthProvider; - - private SignInCompleteListener signInCompleteListener; - - public void setSignInCompleteListener(SignInCompleteListener signInCompleteListener) { - this.signInCompleteListener = signInCompleteListener; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - View view = View.inflate(getActivity(), R.layout.dialog_sign_in_oauth_username, null); - - inputLayout = view.findViewById(R.id.dialog_signin_oauth_username); - - Bundle bundle = getArguments(); - if (bundle != null) { - openAuthProvider = bundle.getString(Constants.CURRENT_OAUTH_PROVIDER); - } - - final AlertDialog alertDialog = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.sign_in_dialog_title) - .setView(view) - .setPositiveButton(R.string.ok, null) - .create(); - - alertDialog.setOnShowListener(dialog -> { - Button buttonPositive = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - buttonPositive.setOnClickListener(view1 -> onPositiveButtonClick()); - buttonPositive.setEnabled(!inputLayout.getEditText().getText().toString().isEmpty()); - DialogInputWatcher inputWatcher = new DialogInputWatcher(inputLayout, buttonPositive, false); - inputLayout.getEditText().addTextChangedListener(inputWatcher); - }); - - return alertDialog; - } - - private void onPositiveButtonClick() { - String username = inputLayout.getEditText().getText().toString().trim(); - - if (username.isEmpty()) { - inputLayout.setError(getString(R.string.signin_choose_username_empty)); - } else { - CheckUserNameAvailableTask checkUserNameAvailableTask = new CheckUserNameAvailableTask(username); - checkUserNameAvailableTask.setOnCheckUserNameAvailableCompleteListener(this); - checkUserNameAvailableTask.execute(); - } - } - - @Override - public void onCancel(DialogInterface dialog) { - signInCompleteListener.onLoginCancel(); - } - - @Override - public void onCheckUserNameAvailableComplete(Boolean userNameAvailable, String username) { - if (userNameAvailable) { - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.error) - .setMessage(R.string.oauth_username_taken) - .setPositiveButton(R.string.ok, null) - .show(); - } else { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - if (openAuthProvider.equals(Constants.GOOGLE_PLUS)) { - sharedPreferences.edit() - .putString(Constants.GOOGLE_USERNAME, username) - .apply(); - - GoogleExchangeCodeTask googleExchangeCodeTask = new GoogleExchangeCodeTask(getActivity(), - sharedPreferences.getString(Constants.GOOGLE_EXCHANGE_CODE, Constants.NO_GOOGLE_EXCHANGE_CODE), - sharedPreferences.getString(Constants.GOOGLE_EMAIL, Constants.NO_GOOGLE_EMAIL), - sharedPreferences.getString(Constants.GOOGLE_USERNAME, Constants.NO_GOOGLE_USERNAME), - sharedPreferences.getString(Constants.GOOGLE_ID, Constants.NO_GOOGLE_ID), - sharedPreferences.getString(Constants.GOOGLE_LOCALE, Constants.NO_GOOGLE_LOCALE), - sharedPreferences.getString(Constants.GOOGLE_ID_TOKEN, Constants.NO_GOOGLE_ID_TOKEN)); - googleExchangeCodeTask.setOnGoogleExchangeCodeCompleteListener(this); - googleExchangeCodeTask.execute(); - } - } - } - - @Override - public void onGoogleExchangeCodeComplete() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - GoogleLogInTask googleLogInTask = new GoogleLogInTask(getActivity(), - sharedPreferences.getString(Constants.GOOGLE_EMAIL, Constants.NO_GOOGLE_EMAIL), - sharedPreferences.getString(Constants.GOOGLE_USERNAME, Constants.NO_GOOGLE_USERNAME), - sharedPreferences.getString(Constants.GOOGLE_ID, Constants.NO_GOOGLE_ID), - sharedPreferences.getString(Constants.GOOGLE_LOCALE, Constants.NO_GOOGLE_LOCALE) - ); - googleLogInTask.setOnGoogleServerLogInCompleteListener(this); - googleLogInTask.execute(); - } - - @Override - public void onGoogleServerLogInComplete() { - signInCompleteListener.onLoginSuccessful(null); - dismiss(); - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/dialog/login/RegistrationDialogFragment.java b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/dialog/login/RegistrationDialogFragment.java deleted file mode 100644 index 25e79f68247..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/dialog/login/RegistrationDialogFragment.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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.recyclerview.dialog.login; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.util.Patterns; -import android.view.View; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.EditText; - -import com.google.android.material.textfield.TextInputLayout; - -import org.catrobat.catroid.R; -import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.transfers.CheckEmailAvailableTask; -import org.catrobat.catroid.transfers.CheckUserNameAvailableTask; -import org.catrobat.catroid.transfers.RegistrationTask; -import org.catrobat.catroid.transfers.RegistrationTask.OnRegistrationListener; -import org.catrobat.catroid.ui.ViewUtils; -import org.catrobat.catroid.utils.DeviceSettingsProvider; -import org.catrobat.catroid.utils.ToastUtil; -import org.catrobat.catroid.utils.Utils; - -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; - -public class RegistrationDialogFragment extends DialogFragment implements OnRegistrationListener { - - public static final String TAG = RegistrationDialogFragment.class.getSimpleName(); - - private TextInputLayout usernameInputLayout; - private TextInputLayout emailInputLayout; - private TextInputLayout passwordInputLayout; - private TextInputLayout confirmPasswordInputLayout; - private EditText userNameEditText; - private EditText emailAddressEditText; - private EditText passwordEditText; - private EditText confirmPasswordEditText; - private AlertDialog alertDialog; - - private SignInCompleteListener signInCompleteListener; - - public void setSignInCompleteListener(SignInCompleteListener signInCompleteListener) { - this.signInCompleteListener = signInCompleteListener; - } - - @Override - public Dialog onCreateDialog(Bundle bundle) { - View view = View.inflate(getActivity(), R.layout.dialog_register, null); - - usernameInputLayout = view.findViewById(R.id.dialog_register_username); - emailInputLayout = view.findViewById(R.id.dialog_register_email); - passwordInputLayout = view.findViewById(R.id.dialog_register_password); - confirmPasswordInputLayout = view.findViewById(R.id.dialog_register_password_confirm); - - alertDialog = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.register) - .setView(view) - .setPositiveButton(R.string.register, null) - .create(); - - userNameEditText = usernameInputLayout.getEditText(); - emailAddressEditText = emailInputLayout.getEditText(); - passwordEditText = passwordInputLayout.getEditText(); - confirmPasswordEditText = confirmPasswordInputLayout.getEditText(); - - userNameEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(final Editable s) { - if (s.toString().trim().isEmpty()) { - usernameInputLayout.setError(getString(R.string.error_register_empty_username)); - } else if (s.toString().trim().contains("@")) { - usernameInputLayout.setError(getString(R.string.error_register_username_as_email)); - } else if (!s.toString().trim().matches("^[a-zA-Z0-9-_.]*$")) { - usernameInputLayout.setError(getString(R.string.error_register_invalid_username)); - } else if (s.toString().trim().startsWith("-") || s.toString().startsWith("_") || s.toString().startsWith(".")) { - usernameInputLayout.setError(getString(R.string.error_register_username_start_with)); - } else { - usernameInputLayout.setErrorEnabled(false); - } - - if (!usernameInputLayout.isErrorEnabled()) { - CheckUserNameAvailableTask checkUserNameAvailableTask = new CheckUserNameAvailableTask(s.toString()); - checkUserNameAvailableTask.setOnCheckUserNameAvailableCompleteListener(new CheckUserNameAvailableTask.OnCheckUserNameAvailableCompleteListener() { - @Override - public void onCheckUserNameAvailableComplete(Boolean userNameAvailable, String username) { - if (userNameAvailable == null) { - ToastUtil.showError(getActivity(), R.string.error_internet_connection); - } else if (userNameAvailable) { - usernameInputLayout.setError(getString(R.string.error_register_username_already_exists)); - } - } - }); - checkUserNameAvailableTask.execute(); - } - handleRegisterBtnStatus(); - } - }); - - emailAddressEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(final Editable s) { - if (!Patterns.EMAIL_ADDRESS.matcher(s.toString()).matches() || s.toString().isEmpty()) { - emailInputLayout.setError(getString(R.string.error_register_invalid_email_format)); - } else { - emailInputLayout.setErrorEnabled(false); - } - if (!emailInputLayout.isErrorEnabled()) { - CheckEmailAvailableTask checkEmailAvailableTask = new - CheckEmailAvailableTask(s.toString(), Constants - .NO_OAUTH_PROVIDER); - checkEmailAvailableTask.setOnCheckEmailAvailableCompleteListener(new CheckEmailAvailableTask.OnCheckEmailAvailableCompleteListener() { - @Override - public void onCheckEmailAvailableComplete(Boolean emailAvailable, String provider) { - if (emailAvailable == null) { - ToastUtil.showError(getActivity(), R.string.error_internet_connection); - } else if (emailAvailable) { - emailInputLayout.setError(getString(R.string.error_register_email_exists)); - } - } - }); - checkEmailAvailableTask.execute(); - } - handleRegisterBtnStatus(); - } - }); - - passwordEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - if (s.toString().isEmpty()) { - passwordInputLayout.setError(getString(R.string.error_register_empty_password)); - } else if (s.toString().length() < 6) { - passwordInputLayout.setError(getString(R.string.error_register_password_at_least_6_characters)); - } else if (!s.toString().equals(confirmPasswordEditText.getText().toString())) { - confirmPasswordInputLayout.setError(getString(R.string.error_register_passwords_mismatch)); - passwordInputLayout.setErrorEnabled(false); - } else { - passwordInputLayout.setErrorEnabled(false); - confirmPasswordInputLayout.setErrorEnabled(false); - } - handleRegisterBtnStatus(); - } - }); - - confirmPasswordEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - if (s.toString().isEmpty()) { - confirmPasswordInputLayout.setError(getString(R.string.error_register_empty_confirm_password)); - } else if (s.toString().length() < 6) { - confirmPasswordInputLayout.setError(getString(R.string.error_register_password_at_least_6_characters)); - } else if (!s.toString().equals(passwordEditText.getText().toString())) { - confirmPasswordInputLayout.setError(getString(R.string.error_register_passwords_mismatch)); - } else { - confirmPasswordInputLayout.setErrorEnabled(false); - passwordInputLayout.setErrorEnabled(false); - } - handleRegisterBtnStatus(); - } - }); - - CheckBox showPasswordCheckBox = view.findViewById(R.id.show_password); - showPasswordCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT); - confirmPasswordEditText.setInputType(InputType.TYPE_CLASS_TEXT); - } else { - passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - confirmPasswordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } - } - }); - - String eMail = DeviceSettingsProvider.getUserEmail(getActivity()); - if (eMail != null) { - emailAddressEditText.setText(eMail); - emailInputLayout.setErrorEnabled(false); - } - - alertDialog.setOnShowListener(dialog -> { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(buttonView -> onRegisterButtonClick()); - ViewUtils.showKeyboard(userNameEditText); - }); - return alertDialog; - } - - @Override - public void onCancel(DialogInterface dialog) { - signInCompleteListener.onLoginCancel(); - } - - @Override - public void onRegistrationComplete() { - Bundle bundle = new Bundle(); - bundle.putString(Constants.CURRENT_OAUTH_PROVIDER, Constants.NO_OAUTH_PROVIDER); - signInCompleteListener.onLoginSuccessful(bundle); - dismiss(); - } - - @Override - public void onRegistrationFailed(String msg) { - confirmPasswordEditText.setError(msg); - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - } - - private void onRegisterButtonClick() { - String username = userNameEditText.getText().toString().trim(); - String password = passwordEditText.getText().toString(); - String email = emailAddressEditText.getText().toString(); - - RegistrationTask registrationTask = new RegistrationTask(getActivity(), username, password, email); - registrationTask.setOnRegistrationListener(this); - registrationTask.execute(); - } - - private void handleRegisterBtnStatus() { - if (alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) != null) { - if (!usernameInputLayout.isErrorEnabled() && !emailInputLayout.isErrorEnabled() && !passwordInputLayout - .isErrorEnabled() && !confirmPasswordInputLayout.isErrorEnabled() && Utils.isNetworkAvailable(getActivity())) { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); - } else { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - } - } - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/MainMenuFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/MainMenuFragment.kt index 350e7b5f88e..ff39ffd33e7 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/MainMenuFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/MainMenuFragment.kt @@ -120,7 +120,7 @@ class MainMenuFragment : Fragment(), binding.myProjectsTextView.setOnClickListener(this) binding.projectImageView.setOnClickListener(this) binding.playProject.setOnClickListener(this) - binding.featuredProjectsTextView.setOnClickListener(this) + binding.exploreShareTextView.setOnClickListener(this) setFragment(this) @@ -155,6 +155,7 @@ class MainMenuFragment : Fragment(), binding.categoriesRecyclerView.setVisibleOrGone(!showNoInternetLayout) featuredProjectsAdapter.setItems(featuredProjectsList) + binding.featuredProjectsRecyclerView.setVisibleOrGone(featuredProjectsList.isNotEmpty()) binding.featuredProjectsRecyclerView.itemsCount = featuredProjectsList.size categoriesAdapter.setItems(projectsCategoriesList) @@ -318,9 +319,9 @@ class MainMenuFragment : Fragment(), startActivity(Intent(activity, ProjectListActivity::class.java)) } - R.id.featuredProjectsTextView -> { + R.id.exploreShareTextView -> { viewModel.setIsLoading(true) - startActivity(Intent(activity, WebViewActivity::class.java)) + openWebView(Constants.MAIN_URL_HTTPS) } } } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/SpriteListFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/SpriteListFragment.kt index 6e1835c4633..39ab989cd86 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/SpriteListFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/fragment/SpriteListFragment.kt @@ -255,37 +255,50 @@ class SpriteListFragment : RecyclerViewFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == IMPORT_OBJECT_REQUEST_CODE && resultCode == Activity.RESULT_OK) { - val uri = if (data?.hasExtra(IMPORT_LOCAL_INTENT) == true) { - Uri.fromFile(File(data.getStringExtra(IMPORT_LOCAL_INTENT))) - } else { - Uri.fromFile(File(data?.getStringExtra(WebViewActivity.MEDIA_FILE_PATH))) - } + if (requestCode != IMPORT_OBJECT_REQUEST_CODE || resultCode != Activity.RESULT_OK) { + return + } + importSpritesFromIntent(data) + } - val currentScene = projectManager.currentlyEditedScene - val resolvedName: String - val resolvedFileName = - StorageOperations.resolveFileName(requireActivity().contentResolver, uri) - val lookFileName: String - val useDefaultSpriteName = - resolvedFileName == null || StorageOperations.getSanitizedFileName(resolvedFileName) == TMP_IMAGE_FILE_NAME - if (useDefaultSpriteName) { - resolvedName = getString(R.string.default_sprite_name) - lookFileName = resolvedName + Constants.CATROBAT_EXTENSION - } else { - lookFileName = resolvedFileName - } - val importProjectHelper = ImportProjectHelper( - lookFileName, currentScene, requireActivity() - ) - if (!importProjectHelper.checkForConflicts()) { - return - } - if (currentSprite != null) { - importProjectHelper.addObjectDataToNewSprite(currentSprite) - } else { - importProjectHelper.rejectImportDialog(null) - } + private fun importSpritesFromIntent(data: Intent?) { + if (data?.hasExtra(IMPORT_LOCAL_INTENT) == true) { + importSpriteFromUri(Uri.fromFile(File(data.getStringExtra(IMPORT_LOCAL_INTENT)))) + return + } + val paths = data?.getStringArrayListExtra(WebViewActivity.MEDIA_FILE_PATHS) + if (!paths.isNullOrEmpty()) { + paths.forEach { importSpriteFromUri(Uri.fromFile(File(it))) } + return + } + val single = data?.getStringExtra(WebViewActivity.MEDIA_FILE_PATH) + if (single != null) { + importSpriteFromUri(Uri.fromFile(File(single))) + } + } + + private fun importSpriteFromUri(uri: Uri) { + val currentScene = projectManager.currentlyEditedScene + val resolvedFileName = + StorageOperations.resolveFileName(requireActivity().contentResolver, uri) + val lookFileName: String + val useDefaultSpriteName = + resolvedFileName == null || StorageOperations.getSanitizedFileName(resolvedFileName) == TMP_IMAGE_FILE_NAME + if (useDefaultSpriteName) { + lookFileName = getString(R.string.default_sprite_name) + Constants.CATROBAT_EXTENSION + } else { + lookFileName = resolvedFileName + } + val importProjectHelper = ImportProjectHelper( + lookFileName, currentScene, requireActivity() + ) + if (!importProjectHelper.checkForConflicts()) { + return + } + if (currentSprite != null) { + importProjectHelper.addObjectDataToNewSprite(currentSprite) + } else { + importProjectHelper.rejectImportDialog(null) } } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/viewmodel/MainFragmentViewModel.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/viewmodel/MainFragmentViewModel.kt index 472835a4785..4bf83166ba9 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/viewmodel/MainFragmentViewModel.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/viewmodel/MainFragmentViewModel.kt @@ -60,7 +60,6 @@ class MainFragmentViewModel( private val connectionMonitor: NetworkConnectionMonitor ) : ViewModel() { private val projectList = MutableLiveData>() - private val coroutineScope = CoroutineScope(Dispatchers.IO) fun getProjects(): LiveData> = projectList 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 d61b5f65bec..04622cb13b8 100644 --- a/catroid/src/main/java/org/catrobat/catroid/utils/Extensions.kt +++ b/catroid/src/main/java/org/catrobat/catroid/utils/Extensions.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -36,6 +36,12 @@ import org.catrobat.catroid.retrofit.models.ProjectResponse import org.catrobat.catroid.retrofit.models.ProjectResponseApi import org.catrobat.catroid.retrofit.models.ProjectsCategory import org.catrobat.catroid.retrofit.models.ProjectsCategoryApi +import org.catrobat.catroid.retrofit.models.getDetailUrl +import org.catrobat.catroid.retrofit.models.getScreenshotUrl +import org.catrobat.catroid.retrofit.models.getThumbUrl +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone fun ImageView.loadImageFromUrl(url: String) { Glide.with(context) @@ -62,8 +68,20 @@ fun View.setVisibleOrGone(show: Boolean) { } } +private fun parseIso8601ToMillis(dateString: String?): Long { + if (dateString.isNullOrEmpty()) return 0L + return try { + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US) + format.timeZone = TimeZone.getTimeZone("UTC") + format.parse(dateString)?.time ?: 0L + } catch (_: Exception) { + 0L + } +} + fun List.toProjectResponsesList(projectType: String): List { return this.map { src -> + val screenshotUrl = src.getScreenshotUrl() ?: "" ProjectResponse( id = src.id, name = src.name, @@ -71,20 +89,20 @@ fun List.toProjectResponsesList(projectType: String): List

.toProjectCategoryWithResponsesList() = - this.map { it.toProjectCategoryWithResponses() }.toMutableList() + this.map { it.toProjectCategoryWithResponses() } diff --git a/catroid/src/main/java/org/catrobat/catroid/utils/MediaDownloader.java b/catroid/src/main/java/org/catrobat/catroid/utils/MediaDownloader.java index 65deb88afeb..c932279b216 100644 --- a/catroid/src/main/java/org/catrobat/catroid/utils/MediaDownloader.java +++ b/catroid/src/main/java/org/catrobat/catroid/utils/MediaDownloader.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -28,6 +28,7 @@ import android.os.Handler; import android.os.ResultReceiver; +import org.catrobat.catroid.R; import org.catrobat.catroid.common.Constants; import org.catrobat.catroid.transfers.MediaDownloadService; import org.catrobat.catroid.ui.WebViewActivity; @@ -52,7 +53,6 @@ public void startDownload(WebViewActivity activity, String url, String mediaName downloadIntent.putExtra(MediaDownloadService.RECEIVER_TAG, new DownloadMediaReceiver(new Handler())); downloadIntent.putExtra(MediaDownloadService.URL_TAG, url); downloadIntent.putExtra(MediaDownloadService.MEDIA_FILE_PATH, filePath); - webViewActivity.createProgressDialog(mediaName); webViewActivity.setResultIntent(webViewActivity.getResultIntent() .putExtra(WebViewActivity.MEDIA_FILE_PATH, filePath)); activity.startService(downloadIntent); @@ -75,13 +75,12 @@ protected void onReceiveResult(int resultCode, Bundle resultData) { if (resultCode == Constants.UPDATE_DOWNLOAD_PROGRESS) { long progress = resultData.getLong(ProgressResponseBody.TAG_PROGRESS); boolean endOfFileReached = resultData.getBoolean(ProgressResponseBody.TAG_ENDOFFILE); - if (endOfFileReached) { - progress = 100; + if (endOfFileReached || progress == 100) { + webViewActivity.setResult(WebViewActivity.RESULT_OK, webViewActivity.getResultIntent()); + ToastUtil.showSuccess(webViewActivity, R.string.notification_download_finished); } - - webViewActivity.updateProgressDialog(progress); } else if (resultCode == Constants.UPDATE_DOWNLOAD_ERROR) { - webViewActivity.dismissProgressDialog(); + ToastUtil.showError(webViewActivity, R.string.error_internet_connection); } } } diff --git a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadData.kt b/catroid/src/main/java/org/catrobat/catroid/utils/ProjectIdUtils.kt similarity index 73% rename from catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadData.kt rename to catroid/src/main/java/org/catrobat/catroid/utils/ProjectIdUtils.kt index cdee0d55844..62ce9a0d8bd 100644 --- a/catroid/src/main/java/org/catrobat/catroid/transfers/project/ProjectUploadData.kt +++ b/catroid/src/main/java/org/catrobat/catroid/utils/ProjectIdUtils.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -21,16 +21,11 @@ * along with this program. If not, see . */ -package org.catrobat.catroid.transfers.project +package org.catrobat.catroid.utils -import java.io.File +object ProjectIdUtils { + val UUID_REGEX = Regex("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}") + val URL_TAG_REGEX = Regex("(.*?)") -data class ProjectUploadData( - val projectName: String, - val projectDescription: String, - val projectArchive: File, - val userEmail: String, - val language: String, - val token: String, - val username: String -) + fun extractUuidFromString(input: String): String? = UUID_REGEX.find(input)?.value +} diff --git a/catroid/src/main/java/org/catrobat/catroid/utils/Utils.java b/catroid/src/main/java/org/catrobat/catroid/utils/Utils.java index 16a89ebe328..ba2ba69f82e 100644 --- a/catroid/src/main/java/org/catrobat/catroid/utils/Utils.java +++ b/catroid/src/main/java/org/catrobat/catroid/utils/Utils.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -39,6 +39,8 @@ import android.view.View; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; +import androidx.appcompat.app.AppCompatActivity; +import androidx.exifinterface.media.ExifInterface; import com.google.common.base.Splitter; import com.huawei.hms.mlsdk.asr.MLAsrConstants; @@ -74,8 +76,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import androidx.appcompat.app.AppCompatActivity; -import androidx.exifinterface.media.ExifInterface; +import kotlin.Lazy; import okhttp3.Response; import static android.speech.RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS; @@ -87,8 +88,6 @@ import static org.catrobat.catroid.common.Constants.PREF_PROJECTNAME_KEY; import static org.catrobat.catroid.common.FlavoredConstants.DEFAULT_ROOT_DIRECTORY; import static org.catrobat.catroid.io.asynctask.ProjectSaverKt.saveProjectSerial; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.TOKEN_CODE_INVALID; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.TOKEN_LENGTH; import static org.koin.java.KoinJavaComponent.get; public final class Utils { @@ -478,29 +477,31 @@ public static void invalidateLoginTokenIfUserRestricted(Context context) { } public static void logoutUser(Context context) { + // Clear JWT tokens + Lazy tokenStoreLazy = + org.koin.java.KoinJavaComponent.inject(org.catrobat.catroid.web.JwtTokenStore.class); + tokenStoreLazy.getValue().clearTokens(); + + // Sign out of Google + if (context instanceof AppCompatActivity) { + GoogleLoginHandler googleLoginHandler = new GoogleLoginHandler((AppCompatActivity) context); + googleLoginHandler.getGoogleSignInClient().signOut(); + } + + // Clear old tokens from SharedPreferences (migration cleanup) SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - GoogleLoginHandler googleLoginHandler = new GoogleLoginHandler((AppCompatActivity) context); - googleLoginHandler.getGoogleSignInClient().signOut(); sharedPreferences.edit() - .putString(Constants.TOKEN, Constants.NO_TOKEN) - .putString(Constants.USERNAME, Constants.NO_USERNAME) - .putString(Constants.GOOGLE_EXCHANGE_CODE, Constants.NO_GOOGLE_EXCHANGE_CODE) - .putString(Constants.GOOGLE_EMAIL, Constants.NO_GOOGLE_EMAIL) - .putString(Constants.GOOGLE_USERNAME, Constants.NO_GOOGLE_USERNAME) - .putString(Constants.GOOGLE_ID, Constants.NO_GOOGLE_ID) - .putString(Constants.GOOGLE_LOCALE, Constants.NO_GOOGLE_LOCALE) - .putString(Constants.GOOGLE_ID_TOKEN, Constants.NO_GOOGLE_ID_TOKEN) + .remove(Constants.TOKEN) + .remove(Constants.USERNAME) + .remove(Constants.GOOGLE_EXCHANGE_CODE) + .remove(Constants.GOOGLE_EMAIL) + .remove(Constants.GOOGLE_USERNAME) + .remove(Constants.GOOGLE_ID) + .remove(Constants.GOOGLE_LOCALE) + .remove(Constants.GOOGLE_ID_TOKEN) .apply(); - WebViewActivity.clearCookies(); - } - public static boolean isUserLoggedIn(Context context) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - String token = preferences.getString(Constants.TOKEN, Constants.NO_TOKEN); - - boolean tokenValid = !(token.equals(Constants.NO_TOKEN) || token.length() != TOKEN_LENGTH - || token.equals(TOKEN_CODE_INVALID)); - return tokenValid; + WebViewActivity.clearCookies(); } public static int setBit(int number, int index, int value) { diff --git a/catroid/src/main/java/org/catrobat/catroid/web/CatrobatServerCalls.kt b/catroid/src/main/java/org/catrobat/catroid/web/CatrobatServerCalls.kt deleted file mode 100644 index 39f0f9580f6..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/web/CatrobatServerCalls.kt +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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 - -import android.content.Context -import android.preference.PreferenceManager -import android.util.Log -import okhttp3.ConnectionSpec -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Request -import okio.Okio -import org.catrobat.catroid.common.Constants -import org.catrobat.catroid.web.ServerAuthenticationConstants.CHECK_EMAIL_AVAILABLE_URL -import org.catrobat.catroid.web.ServerAuthenticationConstants.CHECK_GOOGLE_TOKEN_URL -import org.catrobat.catroid.web.ServerAuthenticationConstants.CHECK_TOKEN_URL -import org.catrobat.catroid.web.ServerAuthenticationConstants.CHECK_USERNAME_AVAILABLE_URL -import org.catrobat.catroid.web.ServerAuthenticationConstants.EMAIL_AVAILABLE -import org.catrobat.catroid.web.ServerAuthenticationConstants.FILE_SURVEY_URL_HTTP -import org.catrobat.catroid.web.ServerAuthenticationConstants.FILE_TAG_URL_HTTP -import org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_ANSWER -import org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_STATUS_CODE -import org.catrobat.catroid.web.ServerAuthenticationConstants.OAUTH_TOKEN_AVAILABLE -import org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_TOKEN_OK -import org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_EMAIL_KEY -import org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_OAUTH_ID_KEY -import org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_USERNAME_KEY -import org.catrobat.catroid.web.ServerAuthenticationConstants.USERNAME_AVAILABLE -import org.json.JSONException -import org.json.JSONObject -import java.io.File -import java.io.IOException -import java.util.HashMap - -class CatrobatServerCalls(private val okHttpClient: OkHttpClient = CatrobatWebClient.client) { - private val tag = CatrobatServerCalls::class.java.simpleName - - @Throws(WebConnectionException::class) - fun checkToken(token: String, username: String, baseUrl: String): Boolean { - try { - val postValues = HashMap() - postValues[Constants.TOKEN] = token - postValues[SIGNIN_USERNAME_KEY] = username - - val serverUrl = baseUrl + CHECK_TOKEN_URL - - val request = postValues.createFormEncodedRequest(serverUrl) - val resultString = okHttpClient.performCallWith(request) - - val jsonObject = JSONObject(resultString) - val statusCode = jsonObject.getInt(JSON_STATUS_CODE) - val serverAnswer = jsonObject.optString(JSON_ANSWER) - - return if (statusCode == SERVER_RESPONSE_TOKEN_OK) { - true - } else { - throw WebConnectionException(statusCode, "server response token ok, but error: $serverAnswer") - } - } catch (e: JSONException) { - throw WebConnectionException(WebConnectionException.ERROR_JSON, Log.getStackTraceString(e)) - } - } - - @Throws(WebConnectionException::class) - private fun getRequest(url: String): String { - val request = Request.Builder() - .url(url) - .build() - return okHttpClient.performCallWith(request) - } - - fun getTags(language: String?): String { - return try { - var serverUrl = FILE_TAG_URL_HTTP - if (language != null) { - serverUrl += "?language=$language" - } - getRequest(serverUrl) - } catch (e: WebConnectionException) { - Log.e(tag, Log.getStackTraceString(e)) - "" - } - } - - fun getSurvey(language: String?): String { - return try { - var serverUrl = FILE_SURVEY_URL_HTTP - if (language != null) { - serverUrl += language - } - getRequest(serverUrl) - } catch (e: WebConnectionException) { - Log.e(tag, Log.getStackTraceString(e)) - "" - } - } - - @Throws(WebConnectionException::class) - fun checkOAuthToken(id: String, oauthProvider: String, context: Context?): Boolean? { - var statusCode: Int - var message: String - try { - val postValues = HashMap() - postValues[SIGNIN_OAUTH_ID_KEY] = id - - val serverUrl = when (oauthProvider) { - Constants.GOOGLE_PLUS -> CHECK_GOOGLE_TOKEN_URL - else -> throw WebConnectionException(-1, "OAuth provider not supported!") - } - - val request = postValues.createFormEncodedRequest(serverUrl) - val resultString = okHttpClient.performCallWith(request) - - val jsonObject = JSONObject(resultString) - statusCode = jsonObject.getInt(JSON_STATUS_CODE) - if (statusCode == SERVER_RESPONSE_TOKEN_OK) { - val serverEmail = jsonObject.optString(SIGNIN_EMAIL_KEY) - val serverUsername = jsonObject.optString(SIGNIN_USERNAME_KEY) - val tokenAvailable = jsonObject.getBoolean(OAUTH_TOKEN_AVAILABLE) - - if (tokenAvailable && oauthProvider == Constants.GOOGLE_PLUS) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - sharedPreferences.edit() - .putString(Constants.GOOGLE_USERNAME, serverUsername) - .putString(Constants.GOOGLE_EMAIL, serverEmail) - .apply() - } - - return tokenAvailable - } - message = resultString - } catch (e: JSONException) { - statusCode = WebConnectionException.ERROR_JSON - message = Log.getStackTraceString(e) - } - throw WebConnectionException(statusCode, message) - } - - @Throws(WebConnectionException::class) - fun isEMailAvailable(email: String): Boolean { - try { - val postValues = HashMap() - postValues[SIGNIN_EMAIL_KEY] = email - - val serverUrl = CHECK_EMAIL_AVAILABLE_URL - val request = postValues.createFormEncodedRequest(serverUrl) - val resultString = okHttpClient.performCallWith(request) - - val jsonObject = JSONObject(resultString) - val statusCode = jsonObject.getInt(JSON_STATUS_CODE) - if (statusCode != SERVER_RESPONSE_TOKEN_OK) { - throw WebConnectionException(statusCode, resultString) - } - - return jsonObject.getBoolean(EMAIL_AVAILABLE) - } catch (e: JSONException) { - throw WebConnectionException(WebConnectionException.ERROR_JSON, Log.getStackTraceString(e)) - } - } - - @Throws(WebConnectionException::class) - fun isUserNameAvailable(username: String): Boolean { - var resultString = "" - try { - val postValues = HashMap() - postValues[SIGNIN_USERNAME_KEY] = username - - val serverUrl = CHECK_USERNAME_AVAILABLE_URL - val request = postValues.createFormEncodedRequest(serverUrl) - resultString = okHttpClient.performCallWith(request) - - val jsonObject = JSONObject(resultString) - val statusCode = jsonObject.getInt(JSON_STATUS_CODE) - if (statusCode != SERVER_RESPONSE_TOKEN_OK) { - throw WebConnectionException(statusCode, resultString) - } - - return jsonObject.getBoolean(USERNAME_AVAILABLE) - } catch (jsonException: JSONException) { - Log.e(tag, Log.getStackTraceString(jsonException)) - throw WebConnectionException(WebConnectionException.ERROR_JSON, resultString) - } - } - - @Throws(WebConnectionException::class) - fun deleteTestUserAccountsOnServer(): Boolean { - try { - val resultString = getRequest("") - val jsonObject = JSONObject(resultString) - val statusCode = jsonObject.getInt(JSON_STATUS_CODE) - if (statusCode != SERVER_RESPONSE_TOKEN_OK) { - throw WebConnectionException(statusCode, resultString) - } - return true - } catch (e: JSONException) { - throw WebConnectionException(WebConnectionException.ERROR_JSON, Log.getStackTraceString(e)) - } - } - - fun downloadProject( - url: String, - destination: File, - successCallback: DownloadSuccessCallback, - errorCallback: DownloadErrorCallback, - progressCallback: DownloadProgressCallback - ) { - val request = Request.Builder().url(url).build() - val httpClientBuilder = okHttpClient.newBuilder() - httpClientBuilder.networkInterceptors() - .add(Interceptor { chain -> - val originalResponse = - chain.proceed(chain.request()) - val body = ProgressResponseBody( - originalResponse.body(), - progressCallback - ) - originalResponse.newBuilder().body(body).build() - }) - val httpClient = if (url.startsWith("http://")) { - httpClientBuilder - .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT)) - .build() - } else { - httpClientBuilder.build() - } - - try { - val response = httpClient.newCall(request).execute() - if (response.isSuccessful) { - val bufferedSink = Okio.buffer(Okio.sink(destination)) - response.body()?.let { bufferedSink.writeAll(it.source()) } - bufferedSink.close() - successCallback.onSuccess() - } else { - Log.v(tag, "Download not successful") - errorCallback.onError(response.code(), "Download failed! HTTP Status code was " + response.code()) - } - } catch (ioException: IOException) { - Log.e(tag, Log.getStackTraceString(ioException)) - errorCallback.onError(WebConnectionException.ERROR_NETWORK, "I/O Exception") - } - } - - interface DownloadSuccessCallback { - fun onSuccess() - } - - interface DownloadErrorCallback { - fun onError(code: Int, message: String) - } - - interface DownloadProgressCallback { - fun onProgress(progress: Long) - } -} diff --git a/catroid/src/main/java/org/catrobat/catroid/web/Cookie.kt b/catroid/src/main/java/org/catrobat/catroid/web/Cookie.kt index 988c8fe1760..e1a646dcc35 100644 --- a/catroid/src/main/java/org/catrobat/catroid/web/Cookie.kt +++ b/catroid/src/main/java/org/catrobat/catroid/web/Cookie.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -22,6 +22,13 @@ */ package org.catrobat.catroid.web -class Cookie(private val name: String, private val value: String) { - fun generateCookieString(): String = "$name=$value" +class Cookie( + private val name: String, + private val value: String, + private val secure: Boolean = true +) { + fun generateCookieString(): String { + val secureFlag = if (secure) "Secure; " else "" + return "$name=$value; HttpOnly; ${secureFlag}Path=/; SameSite=Strict" + } } diff --git a/catroid/src/main/java/org/catrobat/catroid/web/DownloadClient.kt b/catroid/src/main/java/org/catrobat/catroid/web/DownloadClient.kt new file mode 100644 index 00000000000..f4b04fe8693 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/web/DownloadClient.kt @@ -0,0 +1,123 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.web + +import android.os.Bundle +import android.os.ResultReceiver +import android.util.Log +import okhttp3.ConnectionSpec +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okio.buffer +import okio.sink +import org.catrobat.catroid.BuildConfig +import org.catrobat.catroid.common.Constants +import java.io.File +import java.io.IOException + +class DownloadClient(private val okHttpClient: OkHttpClient) { + + fun downloadProject( + url: String, + destination: File, + successCallback: () -> Unit, + errorCallback: (code: Int, message: String) -> Unit, + progressCallback: (progress: Long) -> Unit + ) { + try { + executeDownload(url, destination) { progress -> progressCallback(progress) } + successCallback() + } catch (e: WebConnectionException) { + Log.v(TAG, "Download not successful") + errorCallback(e.statusCode, e.message ?: "Download failed") + } catch (e: IOException) { + Log.e(TAG, Log.getStackTraceString(e)) + errorCallback(WebConnectionException.ERROR_NETWORK, "I/O Exception") + } + } + + @Throws(IOException::class, WebConnectionException::class) + fun downloadMedia(url: String, filePath: String, receiver: ResultReceiver) { + val file = File(filePath) + val parentDir = file.parentFile ?: throw IOException("No parent directory") + if (!(parentDir.mkdirs() || parentDir.isDirectory)) { + throw IOException("Directory not created") + } + + executeDownload(url, file) { progress -> + val bundle = Bundle() + bundle.putLong(ProgressResponseBody.TAG_PROGRESS, progress) + receiver.send(Constants.UPDATE_DOWNLOAD_PROGRESS, bundle) + } + } + + private fun executeDownload(url: String, destination: File, progressCallback: (Long) -> Unit) { + val request = Request.Builder().url(url).build() + val httpClient = buildClientWithProgress(url, progressCallback) + httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw WebConnectionException(response.code(), "Download failed! HTTP ${response.code()}") + } + writeBodyToFile(response, destination) + } + } + + @Throws(WebConnectionException::class) + private fun writeBodyToFile(response: Response, destination: File) { + val body = response.body() ?: throw WebConnectionException( + WebConnectionException.ERROR_NETWORK, "Empty response body" + ) + try { + destination.sink().buffer().use { it.writeAll(body.source()) } + } catch (e: IOException) { + throw WebConnectionException(WebConnectionException.ERROR_NETWORK, Log.getStackTraceString(e)) + } + } + + private fun buildClientWithProgress(url: String, progressCallback: (Long) -> Unit): OkHttpClient { + val builder = okHttpClient.newBuilder() + builder.networkInterceptors().add { chain -> + val originalResponse = chain.proceed(chain.request()) + val originalBody = originalResponse.body() + if (originalBody == null) { + originalResponse + } else { + val body = ProgressResponseBody(originalBody) { progress -> + progressCallback(progress) + } + originalResponse.newBuilder().body(body).build() + } + } + return if (url.startsWith("http://") && BuildConfig.DEBUG) { + builder.connectionSpecs(listOf(ConnectionSpec.CLEARTEXT)).build() + } else { + builder.build() + } + } + + companion object { + private const val TAG = "DownloadClient" + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/web/JwtTokenStore.kt b/catroid/src/main/java/org/catrobat/catroid/web/JwtTokenStore.kt new file mode 100644 index 00000000000..5199cf87340 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/web/JwtTokenStore.kt @@ -0,0 +1,93 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.web + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys + +class JwtTokenStore(context: Context) { + + private val prefs: SharedPreferences by lazy { + EncryptedSharedPreferences.create( + PREFS_NAME, + MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + fun getAccessToken(): String? = prefs.getString(KEY_ACCESS_TOKEN, null) + + fun getRefreshToken(): String? = prefs.getString(KEY_REFRESH_TOKEN, null) + + fun getUsername(): String? = prefs.getString(KEY_USERNAME, null) + + fun setTokens(accessToken: String, refreshToken: String) { + prefs.edit { + putString(KEY_ACCESS_TOKEN, accessToken) + putString(KEY_REFRESH_TOKEN, refreshToken) + } + } + + fun setAccessTokenOnly(accessToken: String) { + if (!isValidJwtFormat(accessToken)) return + prefs.edit { + putString(KEY_ACCESS_TOKEN, accessToken) + } + } + + fun setUsername(username: String) { + prefs.edit { + putString(KEY_USERNAME, username) + } + } + + fun clearTokens() { + prefs.edit { + remove(KEY_ACCESS_TOKEN) + remove(KEY_REFRESH_TOKEN) + remove(KEY_USERNAME) + } + } + + fun isLoggedIn(): Boolean = getAccessToken() != null + + companion object { + private const val PREFS_NAME = "jwt_token_store" + private const val KEY_ACCESS_TOKEN = "jwt_access_token" + private const val KEY_REFRESH_TOKEN = "jwt_refresh_token" + private const val KEY_USERNAME = "jwt_username" + private const val JWT_PARTS = 3 + + fun isValidJwtFormat(token: String?): Boolean { + if (token.isNullOrEmpty()) return false + val parts = token.split(".") + return parts.size == JWT_PARTS && parts.all { it.isNotEmpty() } + } + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/web/LoginHelper.kt b/catroid/src/main/java/org/catrobat/catroid/web/LoginHelper.kt new file mode 100644 index 00000000000..dbfbfd50664 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/web/LoginHelper.kt @@ -0,0 +1,99 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.web + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +object LoginHelper { + + private var activeJob: Job? = null + private var ioDispatcher: CoroutineDispatcher = Dispatchers.IO + private var mainDispatcher: CoroutineDispatcher = Dispatchers.Main + + @JvmStatic + fun performLogin( + loginRepository: LoginRepository, + username: String, + password: String, + onSuccess: Runnable, + onError: java.util.function.Consumer + ) { + cancel() + activeJob = CoroutineScope(SupervisorJob() + ioDispatcher).launch { + val result = loginRepository.login(username, password) + withContext(mainDispatcher) { + coroutineContext.ensureActive() + result.fold( + onSuccess = { onSuccess.run() }, + onFailure = { e -> onError.accept(e.message ?: "Login failed") } + ) + } + } + } + + @JvmStatic + fun performGoogleLogin( + loginRepository: LoginRepository, + idToken: String, + onSuccess: Runnable, + onError: java.util.function.Consumer + ) { + cancel() + activeJob = CoroutineScope(SupervisorJob() + ioDispatcher).launch { + val result = loginRepository.loginWithGoogle(idToken) + withContext(mainDispatcher) { + coroutineContext.ensureActive() + result.fold( + onSuccess = { onSuccess.run() }, + onFailure = { e -> onError.accept(e.message ?: "Google login failed") } + ) + } + } + } + + @JvmStatic + fun performLogout(loginRepository: LoginRepository, onComplete: Runnable) { + cancel() + activeJob = CoroutineScope(SupervisorJob() + ioDispatcher).launch { + loginRepository.logout() + withContext(mainDispatcher) { + coroutineContext.ensureActive() + onComplete.run() + } + } + } + + @JvmStatic + fun cancel() { + activeJob?.cancel() + activeJob = null + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/web/LoginRepository.kt b/catroid/src/main/java/org/catrobat/catroid/web/LoginRepository.kt new file mode 100644 index 00000000000..0bb016d9b3c --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/web/LoginRepository.kt @@ -0,0 +1,132 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.web + +import com.squareup.moshi.JsonDataException +import org.catrobat.catroid.retrofit.AuthService +import org.catrobat.catroid.retrofit.CatroidWebServer +import org.catrobat.catroid.retrofit.models.ApiErrorResponse +import org.catrobat.catroid.retrofit.models.AuthResponse +import org.catrobat.catroid.retrofit.models.LoginRequest +import org.catrobat.catroid.retrofit.models.OAuthLoginRequest +import org.catrobat.catroid.retrofit.models.RefreshRequest +import retrofit2.HttpException +import java.io.IOException + +class LoginRepository( + private val authService: AuthService, + private val tokenStore: JwtTokenStore +) { + + suspend fun login(username: String, password: String): Result { + return safeAuthCall { + val response = authService.login(LoginRequest(username, password)) + tokenStore.setTokens(response.token, response.refreshToken) + tokenStore.setUsername(username) + response + } + } + + suspend fun loginWithGoogle(idToken: String): Result { + return safeAuthCall { + val response = authService.oauthLogin( + OAuthLoginRequest(idToken = idToken, resourceOwner = "google") + ) + tokenStore.setTokens(response.token, response.refreshToken) + response.username?.let { tokenStore.setUsername(it) } + response + } + } + + private inline fun safeAuthCall(block: () -> T): Result { + return try { + Result.success(block()) + } catch (e: HttpException) { + Result.failure(Exception(extractErrorMessage(e) ?: e.message())) + } catch (e: IOException) { + Result.failure(e) + } + } + + suspend fun validateToken(): Boolean { + val token = tokenStore.getAccessToken() ?: return false + return try { + authService.checkToken("Bearer $token").isSuccessful + } catch (_: IOException) { + false + } catch (_: HttpException) { + false + } + } + + fun clearLocalSession() { + tokenStore.clearTokens() + } + + private fun extractErrorMessage(e: HttpException): String? { + return try { + val errorBody = e.response()?.errorBody()?.string() ?: return null + val adapter = CatroidWebServer.moshi.adapter(ApiErrorResponse::class.java) + adapter.fromJson(errorBody)?.error?.message + } catch (_: IOException) { + null + } catch (_: JsonDataException) { + null + } + } + + suspend fun refreshToken(): Result { + val refreshToken = tokenStore.getRefreshToken() + ?: return Result.failure(IllegalStateException("No refresh token available")) + return try { + val response = authService.refreshToken(RefreshRequest(refreshToken)) + tokenStore.setTokens(response.token, response.refreshToken) + Result.success(response) + } catch (e: IOException) { + tokenStore.clearTokens() + Result.failure(e) + } catch (e: HttpException) { + tokenStore.clearTokens() + Result.failure(e) + } + } + + suspend fun logout() { + val token = tokenStore.getAccessToken() + if (token != null) { + try { + authService.logout("Bearer $token") + } catch (_: IOException) { + // Best effort — clear local tokens regardless + } catch (_: HttpException) { + // Best effort — clear local tokens regardless + } + } + tokenStore.clearTokens() + } + + fun isLoggedIn(): Boolean = tokenStore.isLoggedIn() + + fun getUsername(): String? = tokenStore.getUsername() +} diff --git a/catroid/src/main/java/org/catrobat/catroid/web/ProgressRequestBody.kt b/catroid/src/main/java/org/catrobat/catroid/web/ProgressRequestBody.kt new file mode 100644 index 00000000000..e6e0a3ac7a7 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/web/ProgressRequestBody.kt @@ -0,0 +1,71 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.web + +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.Buffer +import okio.BufferedSink +import okio.ForwardingSink +import okio.buffer +import java.io.IOException + +class ProgressRequestBody( + private val delegate: RequestBody, + private val onProgress: (percent: Int) -> Unit +) : RequestBody() { + + override fun contentType(): MediaType? = delegate.contentType() + + override fun contentLength(): Long = delegate.contentLength() + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + val totalBytes = contentLength() + var bytesWritten = 0L + var lastReportedPercent = -1 + + val countingSink = object : ForwardingSink(sink) { + override fun write(source: Buffer, byteCount: Long) { + super.write(source, byteCount) + bytesWritten += byteCount + if (totalBytes > 0) { + val percent = ((bytesWritten * PERCENT) / totalBytes).toInt() + if (percent != lastReportedPercent) { + lastReportedPercent = percent + onProgress(percent) + } + } + } + } + + val bufferedSink = countingSink.buffer() + delegate.writeTo(bufferedSink) + bufferedSink.flush() + } + + companion object { + private const val PERCENT = 100L + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/web/ProgressResponseBody.java b/catroid/src/main/java/org/catrobat/catroid/web/ProgressResponseBody.java index d779c6dceb4..e02fa27a1fb 100644 --- a/catroid/src/main/java/org/catrobat/catroid/web/ProgressResponseBody.java +++ b/catroid/src/main/java/org/catrobat/catroid/web/ProgressResponseBody.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -39,12 +39,16 @@ public class ProgressResponseBody extends ResponseBody { public static final String TAG_PROGRESS = "currentDownloadProgress"; public static final String TAG_ENDOFFILE = "endOfFileReached"; + public interface DownloadProgressCallback { + void onProgress(long progress); + } + private final ResponseBody responseBody; private BufferedSource bufferedSource; - private CatrobatServerCalls.DownloadProgressCallback progressCallback; + private DownloadProgressCallback progressCallback; ProgressResponseBody(ResponseBody responseBody, - CatrobatServerCalls.DownloadProgressCallback progressCallback) { + DownloadProgressCallback progressCallback) { this.responseBody = responseBody; this.progressCallback = progressCallback; } @@ -76,8 +80,15 @@ private Source source(Source source) { public long read(@NotNull Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); totalBytesRead += bytesRead != -1 ? bytesRead : 0; - long progress = (100 * totalBytesRead) / contentLength(); boolean endOfFile = bytesRead == -1; + long length = contentLength(); + if (length <= 0) { + if (endOfFile) { + sendUpdateIntent(100); + } + return bytesRead; + } + long progress = (100 * totalBytesRead) / length; if (progress > lastProgress || endOfFile) { sendUpdateIntent(progress); lastProgress = progress; diff --git a/catroid/src/main/java/org/catrobat/catroid/web/ProjectDownloader.kt b/catroid/src/main/java/org/catrobat/catroid/web/ProjectDownloader.kt index 9556a7dd46c..5454a288c5a 100644 --- a/catroid/src/main/java/org/catrobat/catroid/web/ProjectDownloader.kt +++ b/catroid/src/main/java/org/catrobat/catroid/web/ProjectDownloader.kt @@ -33,7 +33,6 @@ import android.os.ResultReceiver import android.util.Log import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity -import org.catrobat.catroid.ProjectManager import org.catrobat.catroid.R import org.catrobat.catroid.scratchconverter.Client.ProjectDownloadCallback import org.catrobat.catroid.transfers.project.ProjectDownloadService @@ -53,10 +52,11 @@ import java.util.Collections import java.util.HashSet import java.util.Locale -class ProjectDownloader( +class ProjectDownloader @JvmOverloads constructor( private val queue: ProjectDownloadQueue, private val url: String, - callback: ProjectDownloadCallback? + callback: ProjectDownloadCallback?, + private val resolvedProjectName: String? = null ) : Serializable { private val callbackWeakReference = WeakReference(callback) @@ -66,19 +66,27 @@ class ProjectDownloader( private val TAG = ProjectDownloader::class.java.simpleName fun getProjectNameFromUrl(url: String): String? { - val projectNameIndex = url.lastIndexOf(FILENAME_QUERY_PARAM) + FILENAME_QUERY_PARAM.length - val projectNameUTF8 = url.substring(projectNameIndex) - return try { - URLDecoder.decode(projectNameUTF8, "UTF-8") - } catch (e: UnsupportedEncodingException) { - Log.e(TAG, "Could not decode project name: $projectNameUTF8", e) - null + if (url.contains(FILENAME_QUERY_PARAM)) { + val projectNameIndex = url.lastIndexOf(FILENAME_QUERY_PARAM) + FILENAME_QUERY_PARAM.length + val projectNameUTF8 = url.substring(projectNameIndex) + return try { + URLDecoder.decode(projectNameUTF8, "UTF-8") + } catch (e: UnsupportedEncodingException) { + Log.e(TAG, "Could not decode project name: $projectNameUTF8", e) + null + } + } + val apiPattern = Regex("/api/projects/([a-zA-Z0-9-]+)/catrobat") + val match = apiPattern.find(url) + if (match != null) { + return match.groupValues[1] } + return null } } fun download(activity: AppCompatActivity) { - val projectName = getProjectNameFromUrl(url) + val projectName = resolvedProjectName ?: getProjectNameFromUrl(url) if (projectName == null) { ToastUtil.showError(activity, R.string.error_could_not_decode_project_name_from_url) @@ -139,11 +147,10 @@ class ProjectDownloader( when (resultCode) { UPDATE_PROGRESS_CODE -> { val progress = resultData.getInt(UPDATE_PROGRESS_EXTRA) - callbackWeakReference.get()?.onDownloadProgress(progress.toInt(), url) + callbackWeakReference.get()?.onDownloadProgress(progress, url) } SUCCESS_CODE -> { callbackWeakReference.get()?.onDownloadFinished(projectName, url) - ProjectManager.getInstance().addNewDownloadedProject(projectName) queue.finished(projectName) } ERROR_CODE -> queue.finished(projectName) 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 82ad92aeedf..72350d45210 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-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -23,48 +23,10 @@ package org.catrobat.catroid.web; -import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.common.FlavoredConstants; - public final class ServerAuthenticationConstants { - public static final String SIGNIN_USERNAME_KEY = "username"; - public static final String SIGNIN_OAUTH_ID_KEY = "id"; - public static final String SIGNIN_EMAIL_KEY = "email"; - public static final String SIGNIN_LOCALE_KEY = "locale"; - public static final String CATROBAT_USERNAME_KEY = "registrationUsername"; - public static final String CATROBAT_PASSWORD_KEY = "registrationPassword"; - public static final String CATROBAT_COUNTRY_KEY = "registrationCountry"; - public static final String CATROBAT_EMAIL_KEY = "registrationEmail"; - public static final String GOOGLE_LOGIN_URL_APPENDING = "api/loginWithGoogle/loginWithGoogle.json"; - public static final String LOGIN_URL_APPENDING = "api/login/Login.json"; - public static final String REGISTRATION_URL_APPENDING = "api/register/Register.json"; - public static final Integer SERVER_RESPONSE_REGISTER_OK = 201; - public static final Integer SERVER_RESPONSE_TOKEN_OK = 200; - public static final String JSON_STATUS_CODE = "statusCode"; - public static final String JSON_ANSWER = "answer"; - public static final String JSON_TOKEN = "token"; - public static final int TOKEN_LENGTH = 32; - public static final String TOKEN_CODE_INVALID = "-1"; public static final String GOOGLE_LOGIN_CATROWEB_SERVER_CLIENT_ID = "427226922034" + "-r016ige5kb30q9vflqbt1h0i3arng8u1.apps.googleusercontent.com"; - public static final String FILE_TAG_URL_HTTP = FlavoredConstants.BASE_URL_HTTPS + "api/tags/getTags.json"; - public static final String FILE_SURVEY_URL_HTTP = Constants.MAIN_URL_HTTPS + "/api/survey/"; - public static final String SIGNIN_GOOGLE_CODE_KEY = "code"; - public static final String SIGNIN_ID_TOKEN = "id_token"; - public static final String OAUTH_TOKEN_AVAILABLE = "token_available"; - public static final String EMAIL_AVAILABLE = "email_available"; - public static final String USERNAME_AVAILABLE = "username_available"; - public static final String CHECK_TOKEN_URL = "api/checkToken/check.json"; - public static final String CHECK_GOOGLE_TOKEN_URL = - FlavoredConstants.BASE_URL_HTTPS + "api/GoogleServerTokenAvailable/GoogleServerTokenAvailable.json"; - public static final String CHECK_EMAIL_AVAILABLE_URL = - FlavoredConstants.BASE_URL_HTTPS + "api/EMailAvailable/EMailAvailable.json"; - public static final String CHECK_USERNAME_AVAILABLE_URL = - FlavoredConstants.BASE_URL_HTTPS + "api/UsernameAvailable/UsernameAvailable.json"; - public static final String EXCHANGE_GOOGLE_CODE_URL = - FlavoredConstants.BASE_URL_HTTPS + "api/exchangeGoogleCode/exchangeGoogleCode.json"; - private ServerAuthenticationConstants() { throw new AssertionError("No."); } diff --git a/catroid/src/main/java/org/catrobat/catroid/web/ServerAuthenticator.kt b/catroid/src/main/java/org/catrobat/catroid/web/ServerAuthenticator.kt deleted file mode 100644 index b469bff92ca..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/web/ServerAuthenticator.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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 - -import android.content.SharedPreferences -import android.util.Log -import androidx.annotation.VisibleForTesting -import okhttp3.OkHttpClient -import org.catrobat.catroid.common.Constants -import org.catrobat.catroid.common.SharedPreferenceKeys.DEVICE_LANGUAGE -import org.catrobat.catroid.web.ServerAuthenticationConstants.CATROBAT_COUNTRY_KEY -import org.catrobat.catroid.web.ServerAuthenticationConstants.CATROBAT_EMAIL_KEY -import org.catrobat.catroid.web.ServerAuthenticationConstants.CATROBAT_PASSWORD_KEY -import org.catrobat.catroid.web.ServerAuthenticationConstants.CATROBAT_USERNAME_KEY -import org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_ANSWER -import org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_STATUS_CODE -import org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_TOKEN -import org.catrobat.catroid.web.ServerAuthenticationConstants.LOGIN_URL_APPENDING -import org.catrobat.catroid.web.ServerAuthenticationConstants.REGISTRATION_URL_APPENDING -import org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_REGISTER_OK -import org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_TOKEN_OK -import org.catrobat.catroid.web.ServerAuthenticationConstants.TOKEN_LENGTH -import org.json.JSONObject -import java.util.HashMap - -class ServerAuthenticator( - var username: String, - var password: String, - private val token: String, - private val okHttpClient: OkHttpClient, - private val baseUrl: String, - val sharedPreferences: SharedPreferences, - private val taskListener: TaskListener -) { - - @VisibleForTesting - val postValues = HashMap() - private val tag = ServerAuthenticator::class.java.simpleName - - fun performCatrobatRegister( - userEmail: String, - language: String, - country: String - ) { - postValues[CATROBAT_USERNAME_KEY] = username - postValues[CATROBAT_PASSWORD_KEY] = password - postValues[CATROBAT_EMAIL_KEY] = userEmail - if (token != Constants.NO_TOKEN) { - postValues[Constants.TOKEN] = token - } - postValues[CATROBAT_COUNTRY_KEY] = country - postValues[DEVICE_LANGUAGE] = language - - val url = baseUrl + REGISTRATION_URL_APPENDING - performTask(url, SERVER_RESPONSE_REGISTER_OK) - } - - fun performCatrobatLogin() { - postValues[CATROBAT_USERNAME_KEY] = username - postValues[CATROBAT_PASSWORD_KEY] = password - if (token != Constants.NO_TOKEN) { - postValues[Constants.TOKEN] = token - } - val url = baseUrl + LOGIN_URL_APPENDING - performTask(url, SERVER_RESPONSE_TOKEN_OK) - } - - @VisibleForTesting - fun performTask( - serverUrl: String, - acceptedStatusCode: Int - ) { - val resultString = try { - val request = postValues.createFormEncodedRequest(serverUrl) - okHttpClient.performCallWith(request) - } catch (exception: WebConnectionException) { - exception.message?.let { - Log.e(tag, it) - } - taskListener.onError(exception.statusCode, null) - return - } - - val resultJsonObject = JSONObject(resultString) - if (isInvalidResponse(acceptedStatusCode, resultJsonObject)) { - val statusCode = resultJsonObject.optInt(JSON_STATUS_CODE) - val serverAnswer = resultJsonObject.optString(JSON_ANSWER) - taskListener.onError(statusCode, serverAnswer) - return - } - - val tokenReceived = resultJsonObject.optString(JSON_TOKEN) - val sharedPreferencesEditor = sharedPreferences.edit() - sharedPreferencesEditor.putString(Constants.TOKEN, tokenReceived) - sharedPreferencesEditor.putString(Constants.USERNAME, username) - - val eMail = resultJsonObject.optString(Constants.EMAIL) - if (eMail.isNotEmpty()) { - sharedPreferencesEditor.putString(Constants.EMAIL, eMail) - } - sharedPreferencesEditor.apply() - taskListener.onSuccess() - } - - @VisibleForTesting - fun isInvalidResponse(acceptedStatusCode: Int, resultJsonObject: JSONObject): Boolean { - val statusCode = resultJsonObject.optInt(JSON_STATUS_CODE) - val serverAnswer = resultJsonObject.optString(JSON_ANSWER) - val tokenReceived = resultJsonObject.optString(JSON_TOKEN) - - if (acceptedStatusCode != statusCode) { - Log.i(tag, "Not accepted StatusCode: $statusCode; Server Answer: $serverAnswer") - return true - } - if (tokenReceived.length != TOKEN_LENGTH) { - Log.e(tag, "Invlaid TokenError: $tokenReceived; StatusCode: $statusCode Server Answer: $serverAnswer") - return true - } - return false - } - - interface TaskListener { - fun onError(statusCode: Int, errorMessage: String?) - fun onSuccess() - } -} 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 3289eb247d1..f834c3a5a4d 100644 --- a/catroid/src/main/java/org/catrobat/catroid/web/ServerCalls.java +++ b/catroid/src/main/java/org/catrobat/catroid/web/ServerCalls.java @@ -23,30 +23,19 @@ package org.catrobat.catroid.web; -import android.content.Context; -import android.content.SharedPreferences; import android.net.Uri; -import android.os.Bundle; -import android.os.ResultReceiver; -import android.preference.PreferenceManager; 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.FlavoredConstants; 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; -import java.io.File; import java.io.IOException; import java.io.InterruptedIOException; import java.io.UnsupportedEncodingException; @@ -66,32 +55,11 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import okio.BufferedSink; -import okio.Okio; - -import static org.catrobat.catroid.web.CatrobatWebClientKt.createFormEncodedRequest; -import static org.catrobat.catroid.web.CatrobatWebClientKt.performCallWith; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.EXCHANGE_GOOGLE_CODE_URL; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.GOOGLE_LOGIN_URL_APPENDING; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_STATUS_CODE; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_REGISTER_OK; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_TOKEN_OK; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_EMAIL_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_GOOGLE_CODE_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_ID_TOKEN; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_LOCALE_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_OAUTH_ID_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SIGNIN_USERNAME_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.TOKEN_CODE_INVALID; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.TOKEN_LENGTH; 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; @@ -230,7 +198,7 @@ public ScratchSearchResult scratchSearch(final String query, final int numberOfI urlStringBuilder.append(entry.getValue()); urlStringBuilder.append('&'); } - urlStringBuilder.setLength(urlStringBuilder.length() - 1); // removes trailing "&" or "?" character + urlStringBuilder.setLength(urlStringBuilder.length() - 1); final String url = urlStringBuilder.toString(); resultString = getRequestInterruptable(url); @@ -294,86 +262,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.equals(TOKEN_CODE_INVALID) || newToken.length() != 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 { - - File file = new File(filePath); - if (!(file.getParentFile().mkdirs() || file.getParentFile().isDirectory())) { - throw new IOException("Directory not created"); - } - - Request request = new Request.Builder() - .url(url) - .build(); - - OkHttpClient.Builder httpClientBuilder = okHttpClient.newBuilder(); - httpClientBuilder.networkInterceptors().add(chain -> { - Response originalResponse = chain.proceed(chain.request()); - ProgressResponseBody body = new ProgressResponseBody(originalResponse.body(), - progress -> { - Bundle bundle = new Bundle(); - bundle.putLong(ProgressResponseBody.TAG_PROGRESS, progress); - receiver.send(Constants.UPDATE_DOWNLOAD_PROGRESS, bundle); - }); - return originalResponse.newBuilder() - .body(body) - .build(); - }); - OkHttpClient httpClient = httpClientBuilder.build(); - Response response = httpClient.newCall(request).execute(); - - try (BufferedSink bufferedSink = Okio.buffer(Okio.sink(file))) { - if (response.body() != null) { - bufferedSink.writeAll(response.body().source()); - } else { - throw new WebConnectionException(WebConnectionException.ERROR_NETWORK, "FAIL"); - } - } catch (IOException e) { - throw new WebConnectionException(WebConnectionException.ERROR_NETWORK, Log.getStackTraceString(e)); - } - } private String getRequestInterruptable(String url) throws WebConnectionException { Request request = new Request.Builder() .url(url) @@ -394,93 +282,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 boolean googleLogin(String mail, String username, String id, String locale, Context context) throws - WebConnectionException { - - if (context == null) { - throw new WebConnectionException(WebConnectionException.ERROR_JSON, "Context is null."); - } - - try { - HashMap postValues = new HashMap<>(); - postValues.put(SIGNIN_EMAIL_KEY, mail); - postValues.put(SIGNIN_USERNAME_KEY, username); - postValues.put(SIGNIN_OAUTH_ID_KEY, id); - postValues.put(SIGNIN_LOCALE_KEY, locale); - - String serverUrl = FlavoredConstants.BASE_URL_HTTPS + GOOGLE_LOGIN_URL_APPENDING; - Request request = createFormEncodedRequest(postValues, serverUrl); - resultString = performCallWith(okHttpClient, request); - - JSONObject jsonObject = new JSONObject(resultString); - checkStatusCode200(jsonObject.getInt(JSON_STATUS_CODE)); - refreshUploadTokenAndUsername(jsonObject.getString(Constants.TOKEN), username, context); - - return true; - } catch (JSONException e) { - throw new WebConnectionException(WebConnectionException.ERROR_JSON, Log.getStackTraceString(e)); - } - } - - public boolean googleExchangeCode(String code, String id, String username, - String mail, String locale, String idToken) throws WebConnectionException { - - try { - HashMap postValues = new HashMap<>(); - postValues.put(SIGNIN_GOOGLE_CODE_KEY, code); - postValues.put(SIGNIN_OAUTH_ID_KEY, id); - postValues.put(SIGNIN_USERNAME_KEY, username); - postValues.put(SIGNIN_EMAIL_KEY, mail); - postValues.put(SIGNIN_LOCALE_KEY, locale); - postValues.put(SIGNIN_ID_TOKEN, idToken); - postValues.put(Constants.REQUEST_MOBILE, "Android"); - - Request request = createFormEncodedRequest(postValues, EXCHANGE_GOOGLE_CODE_URL); - resultString = performCallWith(okHttpClient, request); - - JSONObject jsonObject = new JSONObject(resultString); - int statusCode = jsonObject.getInt(JSON_STATUS_CODE); - if (!(statusCode == SERVER_RESPONSE_TOKEN_OK || statusCode == SERVER_RESPONSE_REGISTER_OK)) { - throw new WebConnectionException(statusCode, resultString); - } - - return true; - } catch (JSONException e) { - throw new WebConnectionException(WebConnectionException.ERROR_JSON, Log.getStackTraceString(e)); - } - } - - private void checkStatusCode200(int statusCode) throws WebConnectionException { - if (statusCode != SERVER_RESPONSE_TOKEN_OK) { - throw new WebConnectionException(statusCode, resultString); - } - } - - private void refreshUploadTokenAndUsername(String newToken, String username, Context context) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - sharedPreferences.edit() - .putString(Constants.TOKEN, newToken) - .putString(Constants.USERNAME, username) - .apply(); - } - - 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 deleted file mode 100644 index 8b5b48c29f0..00000000000 --- a/catroid/src/main/java/org/catrobat/catroid/web/requests/HttpRequests.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 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/main/res/layout/dialog_register.xml b/catroid/src/main/res/layout/dialog_register.xml deleted file mode 100644 index 639382347cf..00000000000 --- a/catroid/src/main/res/layout/dialog_register.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/catroid/src/main/res/layout/dialog_sign_in_oauth_username.xml b/catroid/src/main/res/layout/dialog_sign_in_oauth_username.xml deleted file mode 100644 index 19c7f9cf2ea..00000000000 --- a/catroid/src/main/res/layout/dialog_sign_in_oauth_username.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/catroid/src/main/res/layout/dialog_upload_project_progress.xml b/catroid/src/main/res/layout/dialog_upload_project_progress.xml index 628f38d1a50..3ccc120c329 100644 --- a/catroid/src/main/res/layout/dialog_upload_project_progress.xml +++ b/catroid/src/main/res/layout/dialog_upload_project_progress.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ar/strings.xml b/catroid/src/main/res/values-ar/strings.xml index 078c732f187..32eb86392af 100644 --- a/catroid/src/main/res/values-ar/strings.xml +++ b/catroid/src/main/res/values-ar/strings.xml @@ -1,7 +1,7 @@ المشروع الذي قمت بتنزيله، يستمع إلى صوتك في الخلفية وكذلك يرسل معلومات إلى الإنترنت. هذا قد يكون غير ضار، وضروري diff --git a/catroid/src/main/res/values-az/strings.xml b/catroid/src/main/res/values-az/strings.xml index 9b34602076f..fa3d11f37e3 100644 --- a/catroid/src/main/res/values-az/strings.xml +++ b/catroid/src/main/res/values-az/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-bg/strings.xml b/catroid/src/main/res/values-bg/strings.xml index a0fbdd1ca3a..8a13fd9525f 100644 --- a/catroid/src/main/res/values-bg/strings.xml +++ b/catroid/src/main/res/values-bg/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-bn/strings.xml b/catroid/src/main/res/values-bn/strings.xml index 3f3e291e547..7314aca4346 100644 --- a/catroid/src/main/res/values-bn/strings.xml +++ b/catroid/src/main/res/values-bn/strings.xml @@ -1,7 +1,7 @@ আপনি যে প্রকল্পটি ডাউনলোড করেছেন তা আপনার ভয়েস শুনে ব্যাকগ্রাউন্ডে এবং ইন্টারনেটে ডেটা প্রেরণ করে। এটি নিরীহ এবং প্রয়োজনীয় হতে পারে diff --git a/catroid/src/main/res/values-bs/strings.xml b/catroid/src/main/res/values-bs/strings.xml index b6aa4f7660e..424179779b2 100644 --- a/catroid/src/main/res/values-bs/strings.xml +++ b/catroid/src/main/res/values-bs/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ca/strings.xml b/catroid/src/main/res/values-ca/strings.xml index edb3755f660..ac96c5899d7 100644 --- a/catroid/src/main/res/values-ca/strings.xml +++ b/catroid/src/main/res/values-ca/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-chr/strings.xml b/catroid/src/main/res/values-chr/strings.xml index fd8349bbc75..851f2836ec6 100644 --- a/catroid/src/main/res/values-chr/strings.xml +++ b/catroid/src/main/res/values-chr/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-cs/strings.xml b/catroid/src/main/res/values-cs/strings.xml index 1bca6c3ec8a..45176fca883 100644 --- a/catroid/src/main/res/values-cs/strings.xml +++ b/catroid/src/main/res/values-cs/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-da/strings.xml b/catroid/src/main/res/values-da/strings.xml index 69531179620..125fc4eafd8 100644 --- a/catroid/src/main/res/values-da/strings.xml +++ b/catroid/src/main/res/values-da/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-de/strings.xml b/catroid/src/main/res/values-de/strings.xml index 5c6ae586aea..4eb43cdb3ca 100644 --- a/catroid/src/main/res/values-de/strings.xml +++ b/catroid/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Das Projekt, das du heruntergeladen hast, hört deine Stimme im Hintergrund und sendet auch Daten an das Internet. Dies kann harmlos sein und ist notwendig diff --git a/catroid/src/main/res/values-el/strings.xml b/catroid/src/main/res/values-el/strings.xml index 33faea635ae..e377b790fc1 100644 --- a/catroid/src/main/res/values-el/strings.xml +++ b/catroid/src/main/res/values-el/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-en-rAU/strings.xml b/catroid/src/main/res/values-en-rAU/strings.xml index 6a84378cbd9..6db9c2d91f7 100644 --- a/catroid/src/main/res/values-en-rAU/strings.xml +++ b/catroid/src/main/res/values-en-rAU/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-en-rCA/strings.xml b/catroid/src/main/res/values-en-rCA/strings.xml index 7c3c00c4623..1b68ac8568f 100644 --- a/catroid/src/main/res/values-en-rCA/strings.xml +++ b/catroid/src/main/res/values-en-rCA/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-en-rGB/strings.xml b/catroid/src/main/res/values-en-rGB/strings.xml index f80595720b5..33abe2f2ae1 100644 --- a/catroid/src/main/res/values-en-rGB/strings.xml +++ b/catroid/src/main/res/values-en-rGB/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-es/strings.xml b/catroid/src/main/res/values-es/strings.xml index 144cc09f006..5fb28ce064f 100644 --- a/catroid/src/main/res/values-es/strings.xml +++ b/catroid/src/main/res/values-es/strings.xml @@ -1,7 +1,7 @@ El proyecto que ha descargado escucha su voz. en segundo plano y también envía datos a Internet. Esto puede ser inofensivo y necesario diff --git a/catroid/src/main/res/values-eu-rES/strings.xml b/catroid/src/main/res/values-eu-rES/strings.xml index 5bd32c206ba..18a9a15a20b 100644 --- a/catroid/src/main/res/values-eu-rES/strings.xml +++ b/catroid/src/main/res/values-eu-rES/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-fa-rIR/strings.xml b/catroid/src/main/res/values-fa-rIR/strings.xml index ab30efd2814..6005acf20aa 100644 --- a/catroid/src/main/res/values-fa-rIR/strings.xml +++ b/catroid/src/main/res/values-fa-rIR/strings.xml @@ -1,7 +1,7 @@ پروژه ای که دانلود کرده اید، به صدای شما در پس زمینه گوش می دهد و همچنین داده ها را به اینترنت می فرستد. این ممکن است بی ضرر باشد، و برای رفتار مورد نظر آن ضروری است. با این حال، لطفا مطمئن شوید که آنچه شما می گویید در حالی که پروژه در حال اجرا است امن است. در صورت شک، لطفا پروژه دانلود شده را حذف کنید. شما همچنین ممکن است پروژه را در صفحه خود در پلت فرم انجمن Catrobat گزارش دهید در صورتی که مطمئن هستید که مضر است. diff --git a/catroid/src/main/res/values-fa/strings.xml b/catroid/src/main/res/values-fa/strings.xml index 0b7dc5c21e1..8fd5f40b80c 100644 --- a/catroid/src/main/res/values-fa/strings.xml +++ b/catroid/src/main/res/values-fa/strings.xml @@ -1,7 +1,7 @@ پروژه ای که دانلود کرده اید به صدای شما گوش می دهد در پس زمینه و همچنین اطلاعات را به اینترنت ارسال می کند. این ممکن است بی ضرر و ضروری باشد diff --git a/catroid/src/main/res/values-fi/strings.xml b/catroid/src/main/res/values-fi/strings.xml index 5a6fa3e7405..beb7e2c45be 100644 --- a/catroid/src/main/res/values-fi/strings.xml +++ b/catroid/src/main/res/values-fi/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-fr/strings.xml b/catroid/src/main/res/values-fr/strings.xml index 7e468320cdf..fc2b9d7d9db 100644 --- a/catroid/src/main/res/values-fr/strings.xml +++ b/catroid/src/main/res/values-fr/strings.xml @@ -1,7 +1,7 @@ Le projet que vous avez téléchargé écoute votre voix en arrière-plan et envoie également des données sur Internet. Cela peut être inoffensif, et nécessaire diff --git a/catroid/src/main/res/values-gl/strings.xml b/catroid/src/main/res/values-gl/strings.xml index 47f9e21243f..5fd5f5c0a0e 100644 --- a/catroid/src/main/res/values-gl/strings.xml +++ b/catroid/src/main/res/values-gl/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-gu/strings.xml b/catroid/src/main/res/values-gu/strings.xml index 8740352a3c1..02bbc85c9b6 100644 --- a/catroid/src/main/res/values-gu/strings.xml +++ b/catroid/src/main/res/values-gu/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ha/strings.xml b/catroid/src/main/res/values-ha/strings.xml index d3b87e3399a..033c252267d 100644 --- a/catroid/src/main/res/values-ha/strings.xml +++ b/catroid/src/main/res/values-ha/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-hi/strings.xml b/catroid/src/main/res/values-hi/strings.xml index ae890d70cf0..008734c2331 100644 --- a/catroid/src/main/res/values-hi/strings.xml +++ b/catroid/src/main/res/values-hi/strings.xml @@ -1,7 +1,7 @@ आपने जिस प्रोजेक्ट को डाउनलोड किया है, वह आपकी आवाज को सुनता है पृष्ठभूमि में और इंटरनेट पर डेटा भी भेजता है। यह हानिरहित और आवश्यक हो सकता है diff --git a/catroid/src/main/res/values-hr/strings.xml b/catroid/src/main/res/values-hr/strings.xml index bf6368ab1c2..6e4ba9e7a31 100644 --- a/catroid/src/main/res/values-hr/strings.xml +++ b/catroid/src/main/res/values-hr/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ig/strings.xml b/catroid/src/main/res/values-ig/strings.xml index 751765a5e0e..dd91e0a3751 100644 --- a/catroid/src/main/res/values-ig/strings.xml +++ b/catroid/src/main/res/values-ig/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-in/strings.xml b/catroid/src/main/res/values-in/strings.xml index 7e16bd20e71..98d50243e48 100644 --- a/catroid/src/main/res/values-in/strings.xml +++ b/catroid/src/main/res/values-in/strings.xml @@ -1,7 +1,7 @@ Proyek yang telah Anda unduh mendengarkan suara Anda di latar belakang dan juga mengirimkan data ke internet. Ini mungkin tidak berbahaya, dan perlu diff --git a/catroid/src/main/res/values-it/strings.xml b/catroid/src/main/res/values-it/strings.xml index be067243f08..08f9f15f9f6 100644 --- a/catroid/src/main/res/values-it/strings.xml +++ b/catroid/src/main/res/values-it/strings.xml @@ -1,7 +1,7 @@ Il progetto che hai scaricato ascolta la tua voce in background e invia i dati attraverso Internet. Questo può essere innocuo e necessario diff --git a/catroid/src/main/res/values-iw/strings.xml b/catroid/src/main/res/values-iw/strings.xml index be0aa503e98..0a15b24b6fb 100644 --- a/catroid/src/main/res/values-iw/strings.xml +++ b/catroid/src/main/res/values-iw/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ja/strings.xml b/catroid/src/main/res/values-ja/strings.xml index e960a7d5752..967ced6c7ab 100644 --- a/catroid/src/main/res/values-ja/strings.xml +++ b/catroid/src/main/res/values-ja/strings.xml @@ -1,7 +1,7 @@ あなたがダウンロードしたプロジェクトは、バックグラウンドであなたの声を聴き バックグラウンドであなたの声を聞き、またデータをインターネットに送信します。これは無害かもしれませんし diff --git a/catroid/src/main/res/values-ka/strings.xml b/catroid/src/main/res/values-ka/strings.xml index 5f229fc2dfc..a9cf5a44a4f 100644 --- a/catroid/src/main/res/values-ka/strings.xml +++ b/catroid/src/main/res/values-ka/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-kab/strings.xml b/catroid/src/main/res/values-kab/strings.xml index f47884b114f..ba4accdea1a 100644 --- a/catroid/src/main/res/values-kab/strings.xml +++ b/catroid/src/main/res/values-kab/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-kk/strings.xml b/catroid/src/main/res/values-kk/strings.xml index 3bdd3e7bbcb..5ed4faded5f 100644 --- a/catroid/src/main/res/values-kk/strings.xml +++ b/catroid/src/main/res/values-kk/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-kn/strings.xml b/catroid/src/main/res/values-kn/strings.xml index ed852c25884..6f7e3f7d807 100644 --- a/catroid/src/main/res/values-kn/strings.xml +++ b/catroid/src/main/res/values-kn/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ko/strings.xml b/catroid/src/main/res/values-ko/strings.xml index d61d608332d..459005dc9c4 100644 --- a/catroid/src/main/res/values-ko/strings.xml +++ b/catroid/src/main/res/values-ko/strings.xml @@ -1,7 +1,7 @@ 당신이 다운로드한 프로젝트는 뒤에서 당신의 목소리를 들을 수 있을 뿐만 아니라 인터넷으로 데이터를 보냅니다. 이는 위험하지 않을 수 있으며 목적을 달성하는 데에 필수적일 수 있습니다. 그러나 프로젝트가 안전하게 진행되고 있을 때 당신이 무엇을 말하는지 확실히 해주세요. 확실하지 않을 경우, 다운로드된 프로젝트를 삭제해 주세요. 또한 만약 그 프로젝트가 유해하다는 것이 확실할 경우, 당신은 카로봇 커뮤니티 플랫폼의 해당 페이지에 그 프로젝트를 신고할 수 있습니다. diff --git a/catroid/src/main/res/values-lt/strings.xml b/catroid/src/main/res/values-lt/strings.xml index 3ebef4d75b5..40a51c73660 100644 --- a/catroid/src/main/res/values-lt/strings.xml +++ b/catroid/src/main/res/values-lt/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-mk/strings.xml b/catroid/src/main/res/values-mk/strings.xml index 2a61487d22b..7cb5d8aa039 100644 --- a/catroid/src/main/res/values-mk/strings.xml +++ b/catroid/src/main/res/values-mk/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ml-rIN/strings.xml b/catroid/src/main/res/values-ml-rIN/strings.xml index bcfd5b8cc2c..2e81262295d 100644 --- a/catroid/src/main/res/values-ml-rIN/strings.xml +++ b/catroid/src/main/res/values-ml-rIN/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ml/strings.xml b/catroid/src/main/res/values-ml/strings.xml index a77809510dc..11154dd2d7c 100644 --- a/catroid/src/main/res/values-ml/strings.xml +++ b/catroid/src/main/res/values-ml/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ms/strings.xml b/catroid/src/main/res/values-ms/strings.xml index 6feba9ad915..5696868818e 100644 --- a/catroid/src/main/res/values-ms/strings.xml +++ b/catroid/src/main/res/values-ms/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-nl/strings.xml b/catroid/src/main/res/values-nl/strings.xml index 3e89fa40bd0..0281cb6d473 100644 --- a/catroid/src/main/res/values-nl/strings.xml +++ b/catroid/src/main/res/values-nl/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-no/strings.xml b/catroid/src/main/res/values-no/strings.xml index 5e65e760da2..d5287ce0142 100644 --- a/catroid/src/main/res/values-no/strings.xml +++ b/catroid/src/main/res/values-no/strings.xml @@ -1,7 +1,7 @@ Prosjektet du har lastet ned lytter til din stemme i bakgrunnen og sender også data til Internett. Dette kan være ufarlig og nødvendig diff --git a/catroid/src/main/res/values-pa-rIN/strings.xml b/catroid/src/main/res/values-pa-rIN/strings.xml index 908c199db66..2ced75fd63d 100644 --- a/catroid/src/main/res/values-pa-rIN/strings.xml +++ b/catroid/src/main/res/values-pa-rIN/strings.xml @@ -1,7 +1,7 @@ ਤੁਹਾਡੇ ਦੁਆਰਾ ਡਾedਨਲੋਡ ਕੀਤਾ ਪ੍ਰੋਜੈਕਟ ਤੁਹਾਡੀ ਆਵਾਜ਼ ਸੁਣਦਾ ਹੈ ਦੀ ਪਿੱਠਭੂਮੀ ਵਿਚ ਹੈ ਅਤੇ ਇਹ ਵੀ ਇੰਟਰਨੈੱਟ \'ਤੇ ਡਾਟਾ ਭੇਜਦਾ ਹੈ. ਇਹ ਨੁਕਸਾਨਦੇਹ ਅਤੇ ਜ਼ਰੂਰੀ ਹੋ ਸਕਦਾ ਹੈ diff --git a/catroid/src/main/res/values-pl/strings.xml b/catroid/src/main/res/values-pl/strings.xml index 75c5dfdcf03..5bbdf19ff20 100644 --- a/catroid/src/main/res/values-pl/strings.xml +++ b/catroid/src/main/res/values-pl/strings.xml @@ -1,7 +1,7 @@ Projekt, który pobrałeś nasłuchuje twój głos w tle, a także wysyła dane do Internetu. Może to być nieszkodliwe i konieczne diff --git a/catroid/src/main/res/values-ps/strings.xml b/catroid/src/main/res/values-ps/strings.xml index 5be70868ce2..420700f887d 100644 --- a/catroid/src/main/res/values-ps/strings.xml +++ b/catroid/src/main/res/values-ps/strings.xml @@ -1,7 +1,7 @@ هغه پروژه چې تاسو ډاونلوډ کړې ستاسو غږ په شالید کې اوري او انټرنیټ ته ډیټا هم لیږي. دا کیدای شي بې ضرر وي، او د diff --git a/catroid/src/main/res/values-pt-rBR/strings.xml b/catroid/src/main/res/values-pt-rBR/strings.xml index 48b66ce767f..fd7d726b49f 100644 --- a/catroid/src/main/res/values-pt-rBR/strings.xml +++ b/catroid/src/main/res/values-pt-rBR/strings.xml @@ -1,7 +1,7 @@ O projeto que você baixou ouve a sua voz em segundo plano e também envia dados para a internet. Isso pode ser inofensivo e necessário diff --git a/catroid/src/main/res/values-pt/strings.xml b/catroid/src/main/res/values-pt/strings.xml index 9b80dbbd632..d43b1fe0944 100644 --- a/catroid/src/main/res/values-pt/strings.xml +++ b/catroid/src/main/res/values-pt/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ro/strings.xml b/catroid/src/main/res/values-ro/strings.xml index b0383575a03..bbd71ad6da1 100644 --- a/catroid/src/main/res/values-ro/strings.xml +++ b/catroid/src/main/res/values-ro/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ru/strings.xml b/catroid/src/main/res/values-ru/strings.xml index ec068e1f878..bf2d8701949 100644 --- a/catroid/src/main/res/values-ru/strings.xml +++ b/catroid/src/main/res/values-ru/strings.xml @@ -1,7 +1,7 @@ Проект который вы скачали, прослушивает голос в фоновом режиме, а также отправляет данные в интернет. Это может быть безвредным и необходимым diff --git a/catroid/src/main/res/values-sd/strings.xml b/catroid/src/main/res/values-sd/strings.xml index 2ee93ef3806..8db05eca172 100644 --- a/catroid/src/main/res/values-sd/strings.xml +++ b/catroid/src/main/res/values-sd/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-si/strings.xml b/catroid/src/main/res/values-si/strings.xml index dfba76c5a91..64bf909bb69 100644 --- a/catroid/src/main/res/values-si/strings.xml +++ b/catroid/src/main/res/values-si/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-sk/strings.xml b/catroid/src/main/res/values-sk/strings.xml index 8fe0d995b25..8fc312b92de 100644 --- a/catroid/src/main/res/values-sk/strings.xml +++ b/catroid/src/main/res/values-sk/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-sq/strings.xml b/catroid/src/main/res/values-sq/strings.xml index df58ea1c28c..ecaa3ffacbf 100644 --- a/catroid/src/main/res/values-sq/strings.xml +++ b/catroid/src/main/res/values-sq/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-sr-rCS/strings.xml b/catroid/src/main/res/values-sr-rCS/strings.xml index 232b75e8c57..5e5b293f156 100644 --- a/catroid/src/main/res/values-sr-rCS/strings.xml +++ b/catroid/src/main/res/values-sr-rCS/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-sr-rSP/strings.xml b/catroid/src/main/res/values-sr-rSP/strings.xml index 675cfa7e27c..12e60ce432d 100644 --- a/catroid/src/main/res/values-sr-rSP/strings.xml +++ b/catroid/src/main/res/values-sr-rSP/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-sr/strings.xml b/catroid/src/main/res/values-sr/strings.xml index 58efc896265..762e998e337 100644 --- a/catroid/src/main/res/values-sr/strings.xml +++ b/catroid/src/main/res/values-sr/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary @@ -643,7 +643,7 @@ site. This will get displayed on your public profile and only be used for statistical and research purposes by us. It can be removed at any time in your profile settings.\n\n • You can withdraw the usage of the personal data belonging to your account at any time by deleting your - account through your profile page on https://share.catrob.at. After deleting your account you will still + account through your profile page on https://share.catrobat.org. After deleting your account you will still be able to use the provided services, but not to collaborate, e.g., through uploading programs, commenting, or liking. Also, all provided content linked to your account, e.g., uploaded programs, comments, etc., will be deleted.\n\n @@ -682,7 +682,7 @@ It is necessary to agree to the Privacy Policy to register or to continue using your account. If you do not want us to collect this data, you can delete your - account via your profile page on our https://share.catrob.at/ website. + account via your profile page on our https://share.catrobat.org/ website. diff --git a/catroid/src/main/res/values-sv/strings.xml b/catroid/src/main/res/values-sv/strings.xml index b09c38e5294..b6e66f40e88 100644 --- a/catroid/src/main/res/values-sv/strings.xml +++ b/catroid/src/main/res/values-sv/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-sw/strings.xml b/catroid/src/main/res/values-sw/strings.xml index 0edfb7cf39e..3308eeaf123 100644 --- a/catroid/src/main/res/values-sw/strings.xml +++ b/catroid/src/main/res/values-sw/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-ta/strings.xml b/catroid/src/main/res/values-ta/strings.xml index 99c7141c24d..1c2585dde5e 100644 --- a/catroid/src/main/res/values-ta/strings.xml +++ b/catroid/src/main/res/values-ta/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-te/strings.xml b/catroid/src/main/res/values-te/strings.xml index 388772c2168..1e321e06e09 100644 --- a/catroid/src/main/res/values-te/strings.xml +++ b/catroid/src/main/res/values-te/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-th/strings.xml b/catroid/src/main/res/values-th/strings.xml index 4c9cbbcbe70..211cf200f27 100644 --- a/catroid/src/main/res/values-th/strings.xml +++ b/catroid/src/main/res/values-th/strings.xml @@ -1,7 +1,7 @@ โครงการที่คุณดาวน์โหลดไว้ฟังเสียงของคุณ อยู่เบื้องหลังและส่งข้อมูลไปยังอินเทอร์เน็ต สิ่งนี้อาจไม่เป็นอันตรายและจำเป็น diff --git a/catroid/src/main/res/values-tl/strings.xml b/catroid/src/main/res/values-tl/strings.xml index 50f322ef8f6..80d1a7b27a1 100644 --- a/catroid/src/main/res/values-tl/strings.xml +++ b/catroid/src/main/res/values-tl/strings.xml @@ -1,7 +1,7 @@ Ang na-download mong project ay nakikinig sa boses mo sa background at nagpapadala ng data sa internet. Maaaring hindi naman ito nakakapanakit,at kailangan diff --git a/catroid/src/main/res/values-tr/strings.xml b/catroid/src/main/res/values-tr/strings.xml index 65158e587bf..4e67b6b86f6 100644 --- a/catroid/src/main/res/values-tr/strings.xml +++ b/catroid/src/main/res/values-tr/strings.xml @@ -1,7 +1,7 @@ İndirdiğiniz proje sesinizi dinler arka planda ve ayrıca internete veri gönderir. Bu zararsız ve gerekli olabilir diff --git a/catroid/src/main/res/values-tw/strings.xml b/catroid/src/main/res/values-tw/strings.xml index 5a397d37103..84d05c32dc2 100644 --- a/catroid/src/main/res/values-tw/strings.xml +++ b/catroid/src/main/res/values-tw/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-uk/strings.xml b/catroid/src/main/res/values-uk/strings.xml index b52f0235736..d2dfcc0b3bd 100644 --- a/catroid/src/main/res/values-uk/strings.xml +++ b/catroid/src/main/res/values-uk/strings.xml @@ -1,7 +1,7 @@ Проєкт, який Ви завантажили слухає ваш голос у фоновому режимі, а також відправляє дані в інтернет. Це може бути корисним й обов\'язковим diff --git a/catroid/src/main/res/values-ur/strings.xml b/catroid/src/main/res/values-ur/strings.xml index a9da6d776d4..ea882fc2bda 100644 --- a/catroid/src/main/res/values-ur/strings.xml +++ b/catroid/src/main/res/values-ur/strings.xml @@ -1,7 +1,7 @@ اس منصوبے نے آپ کو پس منظر میں اپنی آواز پر سنبھال لیا ہے اور انٹرنیٹ کو ڈیٹا بھی بھیجتا ہے. یہ اس کے ارادہ کے لئے نقصان دہ، اور ضروری ہو سکتا ہے. تاہم، براہ کرم اس بات کو یقینی بنائیں کہ آپ کیا کہہ رہے ہیں جبکہ منصوبے چل رہا ہے جبکہ محفوظ ہے. شک کی صورت میں، ڈاؤن لوڈ کردہ پروجیکٹ کو خارج کر دیں. diff --git a/catroid/src/main/res/values-uz/strings.xml b/catroid/src/main/res/values-uz/strings.xml index fd8349bbc75..851f2836ec6 100644 --- a/catroid/src/main/res/values-uz/strings.xml +++ b/catroid/src/main/res/values-uz/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-vi/strings.xml b/catroid/src/main/res/values-vi/strings.xml index a3863b59744..2ab9f14912e 100644 --- a/catroid/src/main/res/values-vi/strings.xml +++ b/catroid/src/main/res/values-vi/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-yo-rNG/strings.xml b/catroid/src/main/res/values-yo-rNG/strings.xml index 5425a653a29..f708a5d35e1 100644 --- a/catroid/src/main/res/values-yo-rNG/strings.xml +++ b/catroid/src/main/res/values-yo-rNG/strings.xml @@ -1,7 +1,7 @@ The project you have downloaded listens to your voice in the background and also sends data to the internet. This may be harmless, and necessary diff --git a/catroid/src/main/res/values-zh-rCN/strings.xml b/catroid/src/main/res/values-zh-rCN/strings.xml index 6420e975cb9..47254f6b24b 100644 --- a/catroid/src/main/res/values-zh-rCN/strings.xml +++ b/catroid/src/main/res/values-zh-rCN/strings.xml @@ -1,7 +1,7 @@ 您所下载的项目能听到您的声音。 在后台,也会向互联网发送数据。这可能是无害的,也是必要的 diff --git a/catroid/src/main/res/values-zh-rTW/strings.xml b/catroid/src/main/res/values-zh-rTW/strings.xml index 306f2c7c95f..afb166d39b5 100644 --- a/catroid/src/main/res/values-zh-rTW/strings.xml +++ b/catroid/src/main/res/values-zh-rTW/strings.xml @@ -1,7 +1,7 @@ 你下載的專案有在背景監聽並傳送資料到網際網路. 這可能是專案原本預期的行為且無害的. 無論如何,請確認你執行專案時的談話內容是安全的. diff --git a/catroid/src/main/res/values/strings.xml b/catroid/src/main/res/values/strings.xml index 84feaca90da..b3778fbb3a4 100644 --- a/catroid/src/main/res/values/strings.xml +++ b/catroid/src/main/res/values/strings.xml @@ -73,7 +73,7 @@ Add to backpack - share.catrob.at + share.catrobat.org The project you have downloaded listens to your voice @@ -114,6 +114,8 @@ Projects Help Catrobat + Explore our community + Upload to community Upload project Play project @@ -1455,6 +1457,8 @@ needs read and write access to it. You can always change permissions through you Something went wrong while uploading the project. + Too many uploads. Please wait a few minutes and try again. + Project size %1$s exceeds the %2$s MB upload limit. Please reduce the project size before sharing. Something went wrong while downloading the project. "Project %s already in queue." "Your download could not be @@ -2240,8 +2244,11 @@ needs read and write access to it. You can always change permissions through you Your project gets uploaded to the app\’s sharing site where others can use and download it. You can also download your project as a standalone app. + Processing on server… Upload failed. The upload feature is currently unavailable. Please try again later. Show project + %1$d%% + 0%% diff --git a/catroid/src/mindstorms/java/org/catrobat/catroid/common/FlavoredConstants.java b/catroid/src/mindstorms/java/org/catrobat/catroid/common/FlavoredConstants.java index fee09815f7d..d1957c10172 100644 --- a/catroid/src/mindstorms/java/org/catrobat/catroid/common/FlavoredConstants.java +++ b/catroid/src/mindstorms/java/org/catrobat/catroid/common/FlavoredConstants.java @@ -30,34 +30,29 @@ import java.io.File; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; -import static org.catrobat.catroid.common.Constants.UPLOAD_URL; public final class FlavoredConstants { // Web: public static final String BASE_URL_HTTPS = "https://catrobat.org/docs/"; - - public static final String BASE_UPLOAD_URL = UPLOAD_URL + "/mindstorms/"; - public static final String CATROBAT_HELP_URL = "https://catrobat.org/docs/"; - public static final String CATEGORY_URL = BASE_URL_HTTPS + "#home-projects__"; + public static final String FLAVOR_NAME = "mindstorms"; + + public static final String CATEGORY_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/#home-projects__"; public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "Mindstorms Code EV3 NXT"; - public static final String FLAVOR_NAME = "mindstorms"; - public static final File DEFAULT_ROOT_DIRECTORY = CatroidApplication.getAppContext().getFilesDir(); public static final File EXTERNAL_STORAGE_ROOT_DIRECTORY = new File( Environment.getExternalStorageDirectory().getAbsolutePath(), POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME); - // Media Library: - public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/app/download-media/"; - public static final String CATROBAT_BASE_URL = "https://catrobat.org/"; - public static final String CATROBAT_CONTENT_DOWNLOAD_URL = CATROBAT_BASE_URL + "wp-content/"; - public static final String CATROBAT_CONTENT_LOOKS_URL = CATROBAT_BASE_URL + "figures-download/"; - public static final String CATROBAT_CONTENT_SOUNDS_URL = CATROBAT_BASE_URL + "sounds-download/"; - public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = CATROBAT_BASE_URL + "backgrounds-download/"; + // Media Library (via share.catrobat.org): + public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/media-library"; + public static final String CATROBAT_CONTENT_DOWNLOAD_URL = MAIN_URL_HTTPS + "/resources/media/"; + public static final String CATROBAT_CONTENT_LOOKS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE"; + public static final String CATROBAT_CONTENT_SOUNDS_URL = LIBRARY_BASE_URL + "?file_type=SOUND"; + public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE&category=backgrounds"; public static final String PRIVACY_POLICY_URL = "https://developer.catrobat.org/pages/legal/policies/privacy/"; private FlavoredConstants() { diff --git a/catroid/src/phiro/java/org/catrobat/catroid/common/FlavoredConstants.java b/catroid/src/phiro/java/org/catrobat/catroid/common/FlavoredConstants.java index 4170704b1f4..5f90a84e856 100644 --- a/catroid/src/phiro/java/org/catrobat/catroid/common/FlavoredConstants.java +++ b/catroid/src/phiro/java/org/catrobat/catroid/common/FlavoredConstants.java @@ -30,34 +30,29 @@ import java.io.File; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; -import static org.catrobat.catroid.common.Constants.UPLOAD_URL; public final class FlavoredConstants { // Web: public static final String BASE_URL_HTTPS = "https://catrobat.org/docs/"; - - public static final String BASE_UPLOAD_URL = UPLOAD_URL + "/pocketcode/"; - public static final String CATROBAT_HELP_URL = "https://catrobat.org/docs/"; - public static final String CATEGORY_URL = BASE_URL_HTTPS + "#home-projects__"; + public static final String FLAVOR_NAME = "pocketcode"; + + public static final String CATEGORY_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/#home-projects__"; public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "Pocket Code"; - public static final String FLAVOR_NAME = "pocketcode"; - public static final File DEFAULT_ROOT_DIRECTORY = CatroidApplication.getAppContext().getFilesDir(); public static final File EXTERNAL_STORAGE_ROOT_DIRECTORY = new File( Environment.getExternalStorageDirectory().getAbsolutePath(), POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME); - // Media Library: - public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/app/download-media/"; - public static final String CATROBAT_BASE_URL = "https://catrobat.org/"; - public static final String CATROBAT_CONTENT_DOWNLOAD_URL = CATROBAT_BASE_URL + "wp-content/"; - public static final String CATROBAT_CONTENT_LOOKS_URL = CATROBAT_BASE_URL + "figures-download/"; - public static final String CATROBAT_CONTENT_SOUNDS_URL = CATROBAT_BASE_URL + "sounds-download/"; - public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = CATROBAT_BASE_URL + "backgrounds-download/"; + // Media Library (via share.catrobat.org): + public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/media-library"; + public static final String CATROBAT_CONTENT_DOWNLOAD_URL = MAIN_URL_HTTPS + "/resources/media/"; + public static final String CATROBAT_CONTENT_LOOKS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE"; + public static final String CATROBAT_CONTENT_SOUNDS_URL = LIBRARY_BASE_URL + "?file_type=SOUND"; + public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE&category=backgrounds"; public static final String PRIVACY_POLICY_URL = "https://developer.catrobat.org/pages/legal/policies/privacy/"; private FlavoredConstants() { diff --git a/catroid/src/pocketCodeBeta/java/org/catrobat/catroid/common/FlavoredConstants.java b/catroid/src/pocketCodeBeta/java/org/catrobat/catroid/common/FlavoredConstants.java index 4ad57054665..37a2933a263 100644 --- a/catroid/src/pocketCodeBeta/java/org/catrobat/catroid/common/FlavoredConstants.java +++ b/catroid/src/pocketCodeBeta/java/org/catrobat/catroid/common/FlavoredConstants.java @@ -30,34 +30,29 @@ import java.io.File; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; -import static org.catrobat.catroid.common.Constants.UPLOAD_URL; public final class FlavoredConstants { // Web: public static final String BASE_URL_HTTPS = "https://catrobat.org/docs/"; - - public static final String BASE_UPLOAD_URL = UPLOAD_URL + "/pocketcode/"; - public static final String CATROBAT_HELP_URL = "https://catrobat.org/docs/"; - public static final String CATEGORY_URL = BASE_URL_HTTPS + "#home-projects__"; + public static final String FLAVOR_NAME = "pocketcode"; + + public static final String CATEGORY_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/#home-projects__"; public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "Pocket Code Beta"; - public static final String FLAVOR_NAME = "pocketcode"; - public static final File DEFAULT_ROOT_DIRECTORY = CatroidApplication.getAppContext().getFilesDir(); public static final File EXTERNAL_STORAGE_ROOT_DIRECTORY = new File( Environment.getExternalStorageDirectory(), POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME); - // Media Library: - public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/app/download-media/"; - public static final String CATROBAT_BASE_URL = "https://catrobat.org/"; - public static final String CATROBAT_CONTENT_DOWNLOAD_URL = CATROBAT_BASE_URL + "wp-content/"; - public static final String CATROBAT_CONTENT_LOOKS_URL = CATROBAT_BASE_URL + "figures-download/"; - public static final String CATROBAT_CONTENT_SOUNDS_URL = CATROBAT_BASE_URL + "sounds-download/"; - public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = CATROBAT_BASE_URL + "backgrounds-download/"; + // Media Library (via share.catrobat.org): + public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/media-library"; + public static final String CATROBAT_CONTENT_DOWNLOAD_URL = MAIN_URL_HTTPS + "/resources/media/"; + public static final String CATROBAT_CONTENT_LOOKS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE"; + public static final String CATROBAT_CONTENT_SOUNDS_URL = LIBRARY_BASE_URL + "?file_type=SOUND"; + public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE&category=backgrounds"; public static final String PRIVACY_POLICY_URL = "https://developer.catrobat.org/pages/legal/policies/privacy/"; private FlavoredConstants() { diff --git a/catroid/src/standalone/java/org/catrobat/catroid/common/FlavoredConstants.java b/catroid/src/standalone/java/org/catrobat/catroid/common/FlavoredConstants.java index 4170704b1f4..5f90a84e856 100644 --- a/catroid/src/standalone/java/org/catrobat/catroid/common/FlavoredConstants.java +++ b/catroid/src/standalone/java/org/catrobat/catroid/common/FlavoredConstants.java @@ -30,34 +30,29 @@ import java.io.File; import static org.catrobat.catroid.common.Constants.MAIN_URL_HTTPS; -import static org.catrobat.catroid.common.Constants.UPLOAD_URL; public final class FlavoredConstants { // Web: public static final String BASE_URL_HTTPS = "https://catrobat.org/docs/"; - - public static final String BASE_UPLOAD_URL = UPLOAD_URL + "/pocketcode/"; - public static final String CATROBAT_HELP_URL = "https://catrobat.org/docs/"; - public static final String CATEGORY_URL = BASE_URL_HTTPS + "#home-projects__"; + public static final String FLAVOR_NAME = "pocketcode"; + + public static final String CATEGORY_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/#home-projects__"; public static final String POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME = "Pocket Code"; - public static final String FLAVOR_NAME = "pocketcode"; - public static final File DEFAULT_ROOT_DIRECTORY = CatroidApplication.getAppContext().getFilesDir(); public static final File EXTERNAL_STORAGE_ROOT_DIRECTORY = new File( Environment.getExternalStorageDirectory().getAbsolutePath(), POCKET_CODE_EXTERNAL_STORAGE_FOLDER_NAME); - // Media Library: - public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/app/download-media/"; - public static final String CATROBAT_BASE_URL = "https://catrobat.org/"; - public static final String CATROBAT_CONTENT_DOWNLOAD_URL = CATROBAT_BASE_URL + "wp-content/"; - public static final String CATROBAT_CONTENT_LOOKS_URL = CATROBAT_BASE_URL + "figures-download/"; - public static final String CATROBAT_CONTENT_SOUNDS_URL = CATROBAT_BASE_URL + "sounds-download/"; - public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = CATROBAT_BASE_URL + "backgrounds-download/"; + // Media Library (via share.catrobat.org): + public static final String LIBRARY_BASE_URL = MAIN_URL_HTTPS + "/" + FLAVOR_NAME + "/media-library"; + public static final String CATROBAT_CONTENT_DOWNLOAD_URL = MAIN_URL_HTTPS + "/resources/media/"; + public static final String CATROBAT_CONTENT_LOOKS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE"; + public static final String CATROBAT_CONTENT_SOUNDS_URL = LIBRARY_BASE_URL + "?file_type=SOUND"; + public static final String CATROBAT_CONTENT_BACKGROUNDS_URL = LIBRARY_BASE_URL + "?file_type=IMAGE&category=backgrounds"; public static final String PRIVACY_POLICY_URL = "https://developer.catrobat.org/pages/legal/policies/privacy/"; private FlavoredConstants() { diff --git a/catroid/src/test/java/org/catrobat/catroid/test/transfers/ProjectDownloaderTest.java b/catroid/src/test/java/org/catrobat/catroid/test/transfers/ProjectDownloaderTest.java index 70729f405ea..8cdd7337637 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/transfers/ProjectDownloaderTest.java +++ b/catroid/src/test/java/org/catrobat/catroid/test/transfers/ProjectDownloaderTest.java @@ -56,7 +56,7 @@ @PrepareForTest({ProjectDownloader.class, ReplaceExistingProjectDialogFragment.class, ToastUtil.class, URLDecoder.class}) public class ProjectDownloaderTest { - private static final String URL = "https://share.catrob.at/pocketcode/download/71489.catrobat?fname=Pet%20Simulator"; + private static final String URL = "https://share.catrobat.org/pocketcode/download/71489.catrobat?fname=Pet%20Simulator"; private static final String PROJECT_NAME = "Pet Simulator"; private ProjectDownloader downloaderSpy = null; 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 ec56a729d58..00000000000 --- a/catroid/src/test/java/org/catrobat/catroid/test/transfers/ProjectUploadTest.kt +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2026 The Catrobat Team - * () - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * An additional term exception under section 7 of the GNU Affero - * General Public License, version 3, is available at - * http://developer.catrobat.org/license_additional_term - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* -package org.catrobat.catroid.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) - ) - } -} -*/ \ No newline at end of file diff --git a/catroid/src/test/java/org/catrobat/catroid/test/transfers/WebViewActivityLoginCookieTest.java b/catroid/src/test/java/org/catrobat/catroid/test/transfers/WebViewActivityLoginCookieTest.java index f4f888ab36c..943cd3901d9 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/transfers/WebViewActivityLoginCookieTest.java +++ b/catroid/src/test/java/org/catrobat/catroid/test/transfers/WebViewActivityLoginCookieTest.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -23,7 +23,6 @@ package org.catrobat.catroid.test.transfers; -import android.content.SharedPreferences; import android.webkit.CookieManager; import org.catrobat.catroid.ui.WebViewActivity; @@ -33,71 +32,57 @@ import org.junit.runners.JUnit4; import org.mockito.Mockito; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; - -import static org.catrobat.catroid.common.Constants.NO_TOKEN; -import static org.catrobat.catroid.common.Constants.NO_USERNAME; -import static org.catrobat.catroid.common.Constants.TOKEN; -import static org.catrobat.catroid.common.Constants.TOKEN_COOKIE_NAME; -import static org.catrobat.catroid.common.Constants.USERNAME; -import static org.catrobat.catroid.common.Constants.USERNAME_COOKIE_NAME; -import static org.mockito.ArgumentMatchers.eq; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; @RunWith(JUnit4.class) public class WebViewActivityLoginCookieTest { private CookieManager cookieManagerMock; - private SharedPreferences sharedPreferencesMock; - private String urlStub = "url"; - private String username = "username #1"; - private String token = "token"; + private String urlStub = "https://share.catrobat.org"; + private String jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.signature"; @Before - public void setUp() throws Exception { + public void setUp() { cookieManagerMock = Mockito.mock(CookieManager.class); - sharedPreferencesMock = Mockito.mock(SharedPreferences.class); } @Test - public void testNoUserNameAvailable() { - when(sharedPreferencesMock.getString(eq(USERNAME), eq(NO_USERNAME))).thenReturn(NO_USERNAME); - when(sharedPreferencesMock.getString(eq(TOKEN), eq(NO_TOKEN))).thenReturn(token); - - WebViewActivity.setLoginCookies(urlStub, sharedPreferencesMock, cookieManagerMock); + public void testSetLoginCookiesWithJwtToken() { + WebViewActivity.setLoginCookies(urlStub, cookieManagerMock, jwtToken); + String expectedCookie = "BEARER=" + jwtToken + "; HttpOnly; Secure; Path=/; SameSite=Strict"; + verify(cookieManagerMock, times(1)).setCookie(urlStub, expectedCookie); + } + @Test + public void testSetLoginCookiesWithNullToken() { + WebViewActivity.setLoginCookies(urlStub, cookieManagerMock, null); verifyNoMoreInteractions(cookieManagerMock); } @Test - public void testNoTokenAvailable() { - when(sharedPreferencesMock.getString(eq(USERNAME), eq(NO_USERNAME))).thenReturn(username); - when(sharedPreferencesMock.getString(eq(TOKEN), eq(NO_TOKEN))).thenReturn(NO_TOKEN); - - WebViewActivity.setLoginCookies(urlStub, sharedPreferencesMock, cookieManagerMock); - + public void testSetLoginCookiesWithEmptyToken() { + WebViewActivity.setLoginCookies(urlStub, cookieManagerMock, ""); verifyNoMoreInteractions(cookieManagerMock); } @Test - public void testSuccess() throws UnsupportedEncodingException { - when(sharedPreferencesMock.getString(eq(USERNAME), eq(NO_USERNAME))).thenReturn(username); - when(sharedPreferencesMock.getString(eq(TOKEN), eq(NO_TOKEN))).thenReturn(token); - - WebViewActivity.setLoginCookies(urlStub, sharedPreferencesMock, cookieManagerMock); + public void testExtractBearerFromCookies() { + String cookies = "session=abc; BEARER=my-jwt-token; other=value"; + assertEquals("my-jwt-token", WebViewActivity.extractBearerFromCookies(cookies)); + } - verify(cookieManagerMock, times(1)) - .setCookie(eq(urlStub), eq(generateCookie(USERNAME_COOKIE_NAME, URLEncoder.encode(username, "UTF-8")))); - verify(cookieManagerMock, times(1)) - .setCookie(eq(urlStub), eq(generateCookie(TOKEN_COOKIE_NAME, token))); - verifyNoMoreInteractions(cookieManagerMock); + @Test + public void testExtractBearerFromCookiesNoBearerPresent() { + String cookies = "session=abc; other=value"; + assertNull(WebViewActivity.extractBearerFromCookies(cookies)); } - private String generateCookie(String name, String value) { - return name + "=" + value; + @Test + public void testExtractBearerFromNullCookies() { + assertNull(WebViewActivity.extractBearerFromCookies(null)); } } diff --git a/catroid/src/test/java/org/catrobat/catroid/test/utiltests/UtilsTest.java b/catroid/src/test/java/org/catrobat/catroid/test/utiltests/UtilsTest.java index a8536323404..b1c652b8b5c 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/utiltests/UtilsTest.java +++ b/catroid/src/test/java/org/catrobat/catroid/test/utiltests/UtilsTest.java @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -40,7 +40,7 @@ public class UtilsTest { @Test public void testExtractRemixUrlsOfProgramHeaderUrlFieldContainingSingleAbsoluteUrl() { - final String expectedFirstProgramRemixUrl = "https://share.catrob.at/pocketcode/program/16267"; + final String expectedFirstProgramRemixUrl = "https://share.catrobat.org/pocketcode/program/16267"; List result = Utils.extractRemixUrlsFromString(expectedFirstProgramRemixUrl); assertEquals(1, result.size()); @@ -58,7 +58,7 @@ public void testExtractRemixUrlsOfProgramHeaderUrlFieldContainingSingleRelativeU @Test public void testExtractRemixUrlsOfMergedProgramHeaderUrlFieldContainingTwoAbsoluteUrls() { - final String expectedFirstProgramRemixUrl = "https://share.catrob.at/pocketcode/program/16267"; + final String expectedFirstProgramRemixUrl = "https://share.catrobat.org/pocketcode/program/16267"; final String expectedSecondProgramRemixUrl = "https://scratch.mit.edu/projects/110380057/"; final XmlHeader headerOfFirstProgram = new XmlHeader(); @@ -141,7 +141,7 @@ public void testExtractRemixUrlsOfRemergedProgramHeaderUrlFieldContainingMixedUr final String expectedFirstProgramRemixUrl = "https://scratch.mit.edu/projects/117697631/"; final String expectedSecondProgramRemixUrl = "/pocketcode/program/3570"; final String expectedThirdProgramRemixUrl = "https://scratch.mit.edu/projects/121648946/"; - final String expectedFourthProgramRemixUrl = "https://share.catrob.at/pocketcode/program/16267"; + final String expectedFourthProgramRemixUrl = "https://share.catrobat.org/pocketcode/program/16267"; final XmlHeader headerOfFirstProgram = new XmlHeader(); headerOfFirstProgram.setProjectName("My first Scratch program"); @@ -187,7 +187,7 @@ public void testExtractRemixUrlsOfRemergedProgramHeaderUrlFieldContainingMixedUr @Test public void testExtractRemixUrlsOfRemergedProgramHeaderUrlFieldContainingMissingUrls() { final String expectedSecondProgramRemixUrl = "/pocketcode/program/3570"; - final String expectedFourthProgramRemixUrl = "https://share.catrob.at/pocketcode/program/16267"; + final String expectedFourthProgramRemixUrl = "https://share.catrobat.org/pocketcode/program/16267"; final XmlHeader headerOfFirstProgram = new XmlHeader(); headerOfFirstProgram.setProjectName("Program A"); diff --git a/catroid/src/test/java/org/catrobat/catroid/test/web/AuthInterceptorTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/web/AuthInterceptorTest.kt new file mode 100644 index 00000000000..1ed9531b77e --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/web/AuthInterceptorTest.kt @@ -0,0 +1,187 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.web + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import okhttp3.Interceptor +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import org.catrobat.catroid.retrofit.AuthInterceptor +import org.catrobat.catroid.web.JwtTokenStore +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class AuthInterceptorTest { + + private lateinit var tokenStore: JwtTokenStore + private lateinit var interceptor: AuthInterceptor + private lateinit var chain: Interceptor.Chain + + @Before + fun setUp() { + tokenStore = mockk(relaxed = true) + interceptor = AuthInterceptor(tokenStore, "https://share.catrobat.org/api/") + chain = mockk(relaxed = true) + } + + @Test + fun `attaches Bearer header when token exists`() { + every { tokenStore.getAccessToken() } returns "test-jwt-token" + + val request = Request.Builder().url("https://share.catrobat.org/api/projects").build() + every { chain.request() } returns request + + val response = Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(ResponseBody.create(null, "")) + .build() + every { chain.proceed(any()) } returns response + + interceptor.intercept(chain) + + verify { + chain.proceed(match { req -> + req.header("Authorization") == "Bearer test-jwt-token" + }) + } + } + + @Test + fun `does not attach header when no token`() { + every { tokenStore.getAccessToken() } returns null + + val request = Request.Builder().url("https://share.catrobat.org/api/projects").build() + every { chain.request() } returns request + + val response = Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(ResponseBody.create(null, "")) + .build() + every { chain.proceed(any()) } returns response + + interceptor.intercept(chain) + + verify { + chain.proceed(match { req -> + req.header("Authorization") == null + }) + } + } + + @Test + fun `retries with new token when another thread already refreshed`() { + every { tokenStore.getAccessToken() } returns "old-token" andThen "new-token-from-other-thread" + every { tokenStore.getRefreshToken() } returns "refresh-token" + + val request = Request.Builder().url("https://share.catrobat.org/api/projects").build() + every { chain.request() } returns request + + val unauthorizedResponse = Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(401) + .message("Unauthorized") + .body(ResponseBody.create(null, "")) + .build() + + val successResponse = Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(ResponseBody.create(null, "")) + .build() + + every { chain.proceed(any()) } returns unauthorizedResponse andThen successResponse + + val result = interceptor.intercept(chain) + + assertEquals(200, result.code()) + verify { + chain.proceed(match { req -> + req.header("Authorization") == "Bearer new-token-from-other-thread" + }) + } + } + + @Test + fun `clears tokens when another thread cleared token during 401 handling`() { + every { tokenStore.getAccessToken() } returns "old-token" andThen null + every { tokenStore.getRefreshToken() } returns null + + val request = Request.Builder().url("https://share.catrobat.org/api/projects").build() + every { chain.request() } returns request + + val response = Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(401) + .message("Unauthorized") + .body(ResponseBody.create(null, "")) + .build() + every { chain.proceed(any()) } returns response + + val result = interceptor.intercept(chain) + + assertEquals(401, result.code()) + verify { tokenStore.clearTokens() } + } + + @Test + fun `clears tokens on 401 when no refresh token`() { + every { tokenStore.getAccessToken() } returns "expired-token" + every { tokenStore.getRefreshToken() } returns null + + val request = Request.Builder().url("https://share.catrobat.org/api/projects").build() + every { chain.request() } returns request + + val response = Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(401) + .message("Unauthorized") + .body(ResponseBody.create(null, "")) + .build() + every { chain.proceed(any()) } returns response + + val result = interceptor.intercept(chain) + + assertEquals(401, result.code()) + verify { tokenStore.clearTokens() } + } +} diff --git a/catroid/src/test/java/org/catrobat/catroid/test/web/CookieTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/web/CookieTest.kt index 30ca0b3b7c1..f3484751439 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/web/CookieTest.kt +++ b/catroid/src/test/java/org/catrobat/catroid/test/web/CookieTest.kt @@ -1,6 +1,6 @@ /* * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2025 The Catrobat Team + * Copyright (C) 2010-2026 The Catrobat Team * () * * This program is free software: you can redistribute it and/or modify @@ -25,12 +25,38 @@ package org.catrobat.catroid.test.web import org.catrobat.catroid.web.Cookie import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class CookieTest { @Test fun `test generateCookieString`() { val cookie = Cookie("sessionID", "12345") - assertEquals("sessionID=12345", cookie.generateCookieString()) + assertEquals("sessionID=12345; HttpOnly; Secure; Path=/; SameSite=Strict", cookie.generateCookieString()) + } + + @Test + fun `cookie string contains HttpOnly flag`() { + val cookie = Cookie("BEARER", "jwt-token-value") + val cookieString = cookie.generateCookieString() + assertTrue(cookieString.contains("HttpOnly")) + assertTrue(cookieString.contains("Secure")) + assertTrue(cookieString.contains("SameSite=Strict")) + } + + @Test + fun `cookie string starts with name value pair`() { + val cookie = Cookie("TOKEN", "abc123") + assertTrue(cookie.generateCookieString().startsWith("TOKEN=abc123")) + } + + @Test + fun `cookie string omits Secure flag when secure is false`() { + val cookie = Cookie("BEARER", "jwt-token-value", secure = false) + val cookieString = cookie.generateCookieString() + assertFalse(cookieString.contains("Secure")) + assertTrue(cookieString.contains("HttpOnly")) + assertTrue(cookieString.contains("SameSite=Strict")) } } diff --git a/catroid/src/test/java/org/catrobat/catroid/test/web/DownloadClientTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/web/DownloadClientTest.kt new file mode 100644 index 00000000000..a67081cf848 --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/web/DownloadClientTest.kt @@ -0,0 +1,120 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.web + +import io.mockk.every +import io.mockk.mockk +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import org.catrobat.catroid.web.DownloadClient +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class DownloadClientTest { + + @get:Rule + val tempFolder = TemporaryFolder() + + private lateinit var okHttpClient: OkHttpClient + private lateinit var client: DownloadClient + + @Before + fun setUp() { + okHttpClient = mockk(relaxed = true) + client = DownloadClient(okHttpClient) + } + + private fun buildResponse(code: Int, body: String): Response { + val request = Request.Builder().url("https://share.catrobat.org/test").build() + return Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(code) + .message(if (code == 200) "OK" else "Error") + .body(ResponseBody.create(null, body)) + .build() + } + + @Test + fun `downloadProject writes file and calls success on 200`() { + val content = "fake-project-zip" + val call = mockk() + every { call.execute() } returns buildResponse(200, content) + + val realClient = OkHttpClient.Builder() + .addInterceptor { chain -> + buildResponse(200, content) + } + .build() + + val downloadClient = DownloadClient(realClient) + val destination = tempFolder.newFile("test.catrobat") + var successCalled = false + + downloadClient.downloadProject( + "https://share.catrobat.org/api/projects/123/catrobat", + destination, + successCallback = { successCalled = true }, + errorCallback = { _, _ -> throw AssertionError("Should not fail") }, + progressCallback = { } + ) + + assertTrue(successCalled) + assertEquals(content, destination.readText()) + } + + @Test + fun `downloadProject calls error on non-200`() { + val realClient = OkHttpClient.Builder() + .addInterceptor { chain -> + buildResponse(404, "Not Found") + } + .build() + + val downloadClient = DownloadClient(realClient) + val destination = tempFolder.newFile("test.catrobat") + var errorCode = -1 + + downloadClient.downloadProject( + "https://share.catrobat.org/api/projects/999/catrobat", + destination, + successCallback = { throw AssertionError("Should not succeed") }, + errorCallback = { code, _ -> errorCode = code }, + progressCallback = { } + ) + + assertEquals(404, errorCode) + } +} diff --git a/catroid/src/test/java/org/catrobat/catroid/test/web/LoginRepositoryTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/web/LoginRepositoryTest.kt new file mode 100644 index 00000000000..04aeca08532 --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/web/LoginRepositoryTest.kt @@ -0,0 +1,125 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.web + +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.catrobat.catroid.retrofit.AuthService +import org.catrobat.catroid.retrofit.models.AuthResponse +import org.catrobat.catroid.web.JwtTokenStore +import org.catrobat.catroid.web.LoginRepository +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import retrofit2.Response + +@RunWith(JUnit4::class) +class LoginRepositoryTest { + + private lateinit var authService: AuthService + private lateinit var tokenStore: JwtTokenStore + private lateinit var loginRepository: LoginRepository + + @Before + fun setUp() { + authService = mockk(relaxed = true) + tokenStore = mockk(relaxed = true) + loginRepository = LoginRepository(authService, tokenStore) + } + + @Test + fun `login stores tokens on success`() = runBlocking { + val authResponse = AuthResponse("access-token-123", "refresh-token-456") + coEvery { authService.login(any()) } returns authResponse + + val result = loginRepository.login("testuser", "password123") + + assertTrue(result.isSuccess) + verify { tokenStore.setTokens("access-token-123", "refresh-token-456") } + verify { tokenStore.setUsername("testuser") } + } + + @Test + fun `login returns failure on exception`() = runBlocking { + coEvery { authService.login(any()) } throws RuntimeException("Network error") + + val result = loginRepository.login("testuser", "password123") + + assertTrue(result.isFailure) + assertEquals("Network error", result.exceptionOrNull()?.message) + } + + @Test + fun `loginWithGoogle stores tokens on success`() = runBlocking { + val authResponse = AuthResponse("google-access", "google-refresh") + coEvery { authService.oauthLogin(any()) } returns authResponse + + val result = loginRepository.loginWithGoogle("google-id-token") + + assertTrue(result.isSuccess) + verify { tokenStore.setTokens("google-access", "google-refresh") } + } + + @Test + fun `logout clears tokens`() = runBlocking { + every { tokenStore.getAccessToken() } returns "some-token" + coEvery { authService.logout(any()) } returns Response.success(Unit) + + loginRepository.logout() + + verify { tokenStore.clearTokens() } + } + + @Test + fun `logout clears tokens even if server call fails`() = runBlocking { + every { tokenStore.getAccessToken() } returns "some-token" + coEvery { authService.logout(any()) } throws RuntimeException("Server down") + + loginRepository.logout() + + verify { tokenStore.clearTokens() } + } + + @Test + fun `isLoggedIn delegates to tokenStore`() { + every { tokenStore.isLoggedIn() } returns true + assertTrue(loginRepository.isLoggedIn()) + + every { tokenStore.isLoggedIn() } returns false + assertFalse(loginRepository.isLoggedIn()) + } + + @Test + fun `getUsername delegates to tokenStore`() { + every { tokenStore.getUsername() } returns "testuser" + assertEquals("testuser", loginRepository.getUsername()) + } +} diff --git a/catroid/src/test/java/org/catrobat/catroid/test/web/ServerAuthenticatorTest.java b/catroid/src/test/java/org/catrobat/catroid/test/web/ServerAuthenticatorTest.java deleted file mode 100644 index d753fdfb0c1..00000000000 --- a/catroid/src/test/java/org/catrobat/catroid/test/web/ServerAuthenticatorTest.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Catroid: An on-device visual programming system for Android devices - * Copyright (C) 2010-2026 The Catrobat Team - * () - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * An additional term exception under section 7 of the GNU Affero - * General Public License, version 3, is available at - * http://developer.catrobat.org/license_additional_term - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* -package org.catrobat.catroid.test.web; - -import android.content.SharedPreferences; - -import org.catrobat.catroid.common.Constants; -import org.catrobat.catroid.web.CatrobatWebClientKt; -import org.catrobat.catroid.web.ServerAuthenticator; -import org.catrobat.catroid.web.WebConnectionException; -import org.apache.commons.lang3.StringUtils; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -import java.util.HashMap; - -import okhttp3.OkHttpClient; -import okhttp3.Request; - -import static org.catrobat.catroid.common.SharedPreferenceKeys.DEVICE_LANGUAGE; -import static org.catrobat.catroid.web.CatrobatWebClientKt.createFormEncodedRequest; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.CATROBAT_COUNTRY_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.CATROBAT_EMAIL_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.CATROBAT_PASSWORD_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.CATROBAT_USERNAME_KEY; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_ANSWER; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_STATUS_CODE; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.JSON_TOKEN; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.LOGIN_URL_APPENDING; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.REGISTRATION_URL_APPENDING; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_REGISTER_OK; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.SERVER_RESPONSE_TOKEN_OK; -import static org.catrobat.catroid.web.ServerAuthenticationConstants.TOKEN_LENGTH; -import static org.catrobat.catroid.web.ServerAuthenticator.TaskListener; -import static org.catrobat.catroid.web.ServerCalls.BASE_URL_TEST_HTTPS; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({CatrobatWebClientKt.class, JSONObject.class, SharedPreferences.class, ServerAuthenticator.class, Request.class}) -public class ServerAuthenticatorTest { - - private static final String USERNAME = "USERNAME"; - private static final String PASSWORD = "PASSWORD"; - private static final String TOKEN = "TOKEN"; - private static final String EMAIL = "EMAIL"; - private static final String LANGUAGE = "LANGUAGE"; - private static final String COUNTRY = "COUNTRY"; - - private TaskListener taskListenerMock; - private OkHttpClient okHttpClientMock; - private ServerAuthenticator authenticatorSpy; - private SharedPreferences.Editor sharedPreferencesEditorMock; - - @Before - public void setUp() { - PowerMockito.mockStatic(CatrobatWebClientKt.class); - taskListenerMock = mock(TaskListener.class); - okHttpClientMock = mock(OkHttpClient.class); - SharedPreferences sharedPreferencesMock = mock(SharedPreferences.class); - sharedPreferencesEditorMock = mock(SharedPreferences.Editor.class); - when(sharedPreferencesMock.edit()).thenReturn(sharedPreferencesEditorMock); - when(sharedPreferencesEditorMock.putString(anyString(), anyString())).thenReturn(sharedPreferencesEditorMock); - authenticatorSpy = PowerMockito.spy(new ServerAuthenticator(USERNAME, PASSWORD, TOKEN, okHttpClientMock, - BASE_URL_TEST_HTTPS, sharedPreferencesMock, taskListenerMock)); - } - - @Test - public void testPerformCatrobatRegisterInit() { - PowerMockito.doNothing().when(authenticatorSpy).performTask(anyString(), anyInt()); - authenticatorSpy.performCatrobatRegister(EMAIL, LANGUAGE, COUNTRY); - verify(authenticatorSpy, times(1)) - .performTask(eq(BASE_URL_TEST_HTTPS + REGISTRATION_URL_APPENDING), eq(SERVER_RESPONSE_REGISTER_OK)); - - verify(taskListenerMock, never()).onError(anyInt(), anyString()); - HashMap expectedMap = new HashMap<>(); - expectedMap.put(CATROBAT_USERNAME_KEY, USERNAME); - expectedMap.put(CATROBAT_PASSWORD_KEY, PASSWORD); - expectedMap.put(CATROBAT_EMAIL_KEY, EMAIL); - expectedMap.put(Constants.TOKEN, TOKEN); - expectedMap.put(CATROBAT_COUNTRY_KEY, COUNTRY); - expectedMap.put(DEVICE_LANGUAGE, LANGUAGE); - assertEquals(expectedMap, authenticatorSpy.getPostValues()); - } - - @Test - public void testPerformCatrobatLoginInit() { - PowerMockito.doNothing().when(authenticatorSpy).performTask(anyString(), anyInt()); - authenticatorSpy.performCatrobatLogin(); - verify(authenticatorSpy, times(1)) - .performTask(eq(BASE_URL_TEST_HTTPS + LOGIN_URL_APPENDING), eq(SERVER_RESPONSE_TOKEN_OK)); - - verify(taskListenerMock, never()).onError(anyInt(), anyString()); - HashMap expectedMap = new HashMap<>(); - expectedMap.put(CATROBAT_USERNAME_KEY, USERNAME); - expectedMap.put(CATROBAT_PASSWORD_KEY, PASSWORD); - expectedMap.put(Constants.TOKEN, TOKEN); - assertEquals(expectedMap, authenticatorSpy.getPostValues()); - } - - @Test - public void testWhenServerConnectionFailsOnTaskFailedCalled() throws Exception { - Request requestMock = PowerMockito.mock(Request.class); - PowerMockito.when(createFormEncodedRequest(anyMap(), anyString())).thenReturn(requestMock); - - int expectedStatusCode = 0; - PowerMockito.when(CatrobatWebClientKt.performCallWith(eq(okHttpClientMock), any(Request.class))) - .thenThrow(new WebConnectionException(expectedStatusCode, "any string")); - - authenticatorSpy.performTask(BASE_URL_TEST_HTTPS, 0); - verify(taskListenerMock, times(1)).onError(eq(expectedStatusCode), eq(null)); - verifyNoMoreInteractions(taskListenerMock); - } - - @Test - public void testInvalidResponseOnTaskFailedCalled() throws Exception { - String responseString = "response"; - - Request requestMock = PowerMockito.mock(Request.class); - PowerMockito.when(createFormEncodedRequest(anyMap(), anyString())).thenReturn(requestMock); - PowerMockito.when(CatrobatWebClientKt.performCallWith(eq(okHttpClientMock), eq(requestMock))).thenReturn(responseString); - JSONObject responseJsonObjectMock = PowerMockito.mock(JSONObject.class); - PowerMockito.whenNew(JSONObject.class).withAnyArguments().thenReturn(responseJsonObjectMock); - when(responseJsonObjectMock.optString(anyString())).thenReturn("message"); - - doReturn(true).when(authenticatorSpy).isInvalidResponse(eq(0), eq(responseJsonObjectMock)); - authenticatorSpy.performTask(BASE_URL_TEST_HTTPS, 0); - - verify(taskListenerMock, times(1)).onError(eq(0), eq("message")); - verifyNoMoreInteractions(taskListenerMock); - verify(responseJsonObjectMock, times(1)).optString(JSON_ANSWER); - verify(responseJsonObjectMock, times(1)).optInt(eq(JSON_STATUS_CODE)); - verifyNoMoreInteractions(responseJsonObjectMock); - } - - @Test - public void testValidResponseUpdateSharedPreferences() throws Exception { - Request requestMock = PowerMockito.mock(Request.class); - PowerMockito.when(createFormEncodedRequest(anyMap(), anyString())).thenReturn(requestMock); - PowerMockito.when(CatrobatWebClientKt.performCallWith(eq(okHttpClientMock), eq(requestMock))).thenReturn(""); - - JSONObject responseJsonObjectMock = PowerMockito.mock(JSONObject.class); - PowerMockito.whenNew(JSONObject.class).withAnyArguments().thenReturn(responseJsonObjectMock); - String expectedToken = "any TOKEN"; - String expectedEmail = "random EMAIL"; - when(responseJsonObjectMock.optString(eq(Constants.TOKEN))).thenReturn(expectedToken); - when(responseJsonObjectMock.optString(eq(Constants.EMAIL))).thenReturn(expectedEmail); - - doReturn(false).when(authenticatorSpy).isInvalidResponse(eq(0), eq(responseJsonObjectMock)); - authenticatorSpy.performTask(BASE_URL_TEST_HTTPS, 0); - - verify(taskListenerMock, atLeastOnce()).onSuccess(); - verifyNoMoreInteractions(taskListenerMock); - - verify(sharedPreferencesEditorMock, times(1)).putString(eq(Constants.TOKEN), eq(expectedToken)); - verify(sharedPreferencesEditorMock, times(1)).putString(Constants.USERNAME, USERNAME); - verify(sharedPreferencesEditorMock, times(1)).putString(eq(Constants.EMAIL), eq(expectedEmail)); - verify(sharedPreferencesEditorMock, times(1)).apply(); - verifyNoMoreInteractions(sharedPreferencesEditorMock); - } - - @Test - public void testWrongStateIsInvalidResponseMethod() { - JSONObject responseJsonObjectMock = PowerMockito.mock(JSONObject.class); - when(responseJsonObjectMock.optInt(JSON_STATUS_CODE)).thenReturn(-1); - assertTrue(authenticatorSpy.isInvalidResponse(0, responseJsonObjectMock)); - } - - @Test - public void testWrongTokenIsInvalidResponseMethod() { - JSONObject responseJsonObjectMock = PowerMockito.mock(JSONObject.class); - when(responseJsonObjectMock.optInt(JSON_STATUS_CODE)).thenReturn(0); - when(responseJsonObjectMock.optString(JSON_TOKEN)).thenReturn("invalid"); - assertTrue(authenticatorSpy.isInvalidResponse(0, responseJsonObjectMock)); - } - @Test - public void testValidResponse() { - JSONObject responseJsonObjectMock = PowerMockito.mock(JSONObject.class); - when(responseJsonObjectMock.optInt(JSON_STATUS_CODE)).thenReturn(0); - String validToken = StringUtils.repeat("1", TOKEN_LENGTH); - when(responseJsonObjectMock.optString(JSON_TOKEN)).thenReturn(validToken); - assertFalse(authenticatorSpy.isInvalidResponse(0, responseJsonObjectMock)); - } -} -*/ \ No newline at end of file