Repository: labexp/osmtracker-android
Pull request #582: An easy way to upload the same filename to GitHub
Author: login: miltonials
Created: 2025-05-12T01:18:26Z | Merged: 2025-05-16T02:48:39Z

[comment]: # (Thank you for your contribution! Please fill out the following details to help us review your pull request.)

### Description
[comment]: # (Provide a clear explanation of the changes in this PR)
[comment]: # (Include information about what problem it solves, how it is implemented, and if it affects UI/API)
This PR introduces the capacity to upload the trace zip file to a GitHub repository without the need to change the filename.
Example: if we upload a file with the following:

* **Trace name**: myTrace.gpx
* **Zip file name**: myTrace.zip

In the first attempt to upload the trace, the file will have `myTrace.zip` as name, but in the second attempt his name will change to `myTrace(1).zip`. Finally the target repository will have two files `myTrace.zip` and `myTrace(1).zip`.

_Note: the number inside the parenthesis is calculated according to the total files with the same name in the repository._


### Related issues
[comment]: # (Link to the original bug report or related work in list format, if applicable)
[comment]: # (* Closes: #number)
[comment]: # (* Related to: #number)
* Related to: #460
* Related to: #361
* Related to: #344
* Related to: #85

##

### Pull Request Checklist
[comment]: # (Please confirm the following before submitting your PR)
[comment]: # (To check a task please put a "x" inside the `[]`)
[comment]: # ([ ] : not done)
[comment]: # ([x] : done)
[comment]: # (Make sure how your PR looks clicking the "Preview" tab at the top of this editor)

- [X] The PR is proposed to the proper branch.
- [X] The changes have been tested on the target Android API and minimum Android API.
- [ ] Automated tests have been added (if applicable).
- [ ] The feature is well documented.
- [X] There is a reference to the original bug report and related work.



Diff:
diff --git a/app/src/main/java/net/osmtracker/activity/GitHubUpload.java b/app/src/main/java/net/osmtracker/activity/GitHubUpload.java
index 529616556..0415267ae 100644
--- a/app/src/main/java/net/osmtracker/activity/GitHubUpload.java
+++ b/app/src/main/java/net/osmtracker/activity/GitHubUpload.java
@@ -32,7 +32,9 @@
 import net.osmtracker.GitHubUser;
 import net.osmtracker.R;
 import net.osmtracker.db.DbGitHubUser;
+import net.osmtracker.util.Callback;
 import net.osmtracker.util.DialogUtils;
+import net.osmtracker.util.GitHubUtils;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -106,7 +108,22 @@ private void uploadCommit() {
                         }
                     }
 
-                    startUploadGitHub(encondedFile.toString(), file.getName(), commitMsj);
+                    String repoOwner = getRepoName().substring(0, getRepoName().indexOf("/")).replace(".base64", "");
+                    String repoName = getRepoName().substring(getRepoName().indexOf("/") + 1);
+                    String repoFilePath = file.getName().replace(".base64", "");
+                    GitHubUtils.getGHFilenameAsync(repoOwner, repoName, repoFilePath, gitHubUser.getToken(),
+                            new Callback() {
+                                @Override
+                                public String onResult(String result) {
+                                    if (result != null) {
+                                        System.out.println("uploading to GitHub: " + result);
+                                        startUploadGitHub(encondedFile.toString(), result, commitMsj);
+                                    } else {
+                                        System.out.println("Error while getting filename.");
+                                    }
+                                    return result;
+                                }
+                            });
                 } catch (IOException e) {
                     Toast.makeText(GitHubUpload.this, R.string.gpx_file_read_error, Toast.LENGTH_SHORT).show();
                     e.printStackTrace();
@@ -151,7 +168,6 @@ private void openActivityOnClick(int btnId, Class<? extends Activity> destinatio
      * Either starts uploading directly if we are authenticated against GitHub
      */
     private void startUploadGitHub(final String fileInBase64, String filename, String commitMsj){
-        filename = filename.substring(0, filename.lastIndexOf("."));
         String fullURL = getBaseURL()+"/repos/"+getRepoName()+"/contents/"+filename;
 
         ProgressDialog progressDialog = new ProgressDialog(this);
diff --git a/app/src/main/java/net/osmtracker/util/Callback.java b/app/src/main/java/net/osmtracker/util/Callback.java
new file mode 100644
index 000000000..78df72862
--- /dev/null
+++ b/app/src/main/java/net/osmtracker/util/Callback.java
@@ -0,0 +1,17 @@
+package net.osmtracker.util;
+
+/**
+ * A generic callback interface used for asynchronous operations.
+ * Implementations of this interface should define how to handle the result
+ * of an asynchronous task.
+ */
+public interface Callback {
+    /**
+     * Called when the asynchronous operation is completed.
+     * Implementations should handle the provided result accordingly.
+     *
+     * @param result The result of the operation, which may be {@code null} if an error occurs.
+     * @return A string value that may be used by the calling function, if applicable.
+     */
+    String onResult(String result);
+}
diff --git a/app/src/main/java/net/osmtracker/util/GitHubUtils.java b/app/src/main/java/net/osmtracker/util/GitHubUtils.java
new file mode 100644
index 000000000..460a31443
--- /dev/null
+++ b/app/src/main/java/net/osmtracker/util/GitHubUtils.java
@@ -0,0 +1,125 @@
+package net.osmtracker.util;
+
+import android.os.AsyncTask;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * Utility class for interacting with the GitHub API.
+ * Provides methods to retrieve file information and manage file uploads.
+ *
+ * <p>This class includes methods to:
+ * <ul>
+ *   <li>Generate a unique filename within a GitHub repository.</li>
+ *   <li>Retrieve the SHA hash of a file stored in a GitHub repository.</li>
+ * </ul>
+ * </p>
+ *
+ * <p>It requires a valid GitHub authentication token for API requests.</p>
+ */
+public class GitHubUtils {
+    /**
+     * Retrieves the SHA hash of a file in a GitHub repository.
+     * If the file does not exist, it returns {@code null}.
+     *
+     * @param repoOwner   The owner of the repository (user or organization).
+     * @param repoName    The name of the GitHub repository.
+     * @param repoFilePath The file path in the repository.
+     * @param token       The GitHub authentication token with appropriate permissions.
+     * @return The SHA hash of the file if it exists, or {@code null} if the file is not found.
+     * @throws IOException   If an I/O error occurs while making the request.
+     * @throws JSONException If an error occurs while parsing the JSON response.
+     */
+    public static void getFileSHAAsync(String repoOwner, String repoName, String repoFilePath, String token, Callback callback) {
+        new AsyncTask<Void, Void, String>() {
+            @Override
+            protected String doInBackground(Void... voids) {
+                try {
+                    String apiUrl = "https://api.github.com/repos/" + repoOwner + "/" + repoName + "/contents/" + repoFilePath;
+                    System.out.println("Fetching SHA: " + apiUrl);
+                    HttpURLConnection connection = (HttpURLConnection) new URL(apiUrl).openConnection();
+                    connection.setRequestMethod("GET");
+                    connection.setRequestProperty("Authorization", "Bearer " + token);
+                    connection.setRequestProperty("Accept", "application/vnd.github.v3+json");
+
+                    if (connection.getResponseCode() == 200) {
+                        BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+                        StringBuilder response = new StringBuilder();
+                        String line;
+                        while ((line = reader.readLine()) != null) {
+                            response.append(line);
+                        }
+                        reader.close();
+                        JSONObject jsonResponse = new JSONObject(response.toString());
+                        return jsonResponse.getString("sha");
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+                return null;
+            }
+
+            @Override
+            protected void onPostExecute(String sha) {
+                callback.onResult(sha); // Return result via callback
+            }
+        }.execute();
+    }
+
+    /**
+     * Asynchronously generates a unique filename in a GitHub repository.
+     * If the file already exists, a number is appended before the extension.
+     *
+     * @param repoOwner   The owner of the repository.
+     * @param repoName    The name of the GitHub repository.
+     * @param repoFilePath The initial file path in the repository.
+     * @param token       The GitHub authentication token.
+     * @param callback    Callback to return the generated filename.
+     */
+    public static void getGHFilenameAsync(String repoOwner, String repoName, final String repoFilePath, String token, Callback callback) {
+        String filename = repoFilePath.substring(0, repoFilePath.lastIndexOf("."));
+        String extension = repoFilePath.substring(repoFilePath.lastIndexOf("."));
+        checkFileExists(repoOwner, repoName, filename, extension, 0, token, callback);
+    }
+
+    /**
+     * Recursively checks if a file exists and generates a unique filename.
+     *
+     * @param repoOwner  The owner of the repository.
+     * @param repoName   The GitHub repository name.
+     * @param filename   The base filename (without extension).
+     * @param extension  The file extension.
+     * @param count      The current attempt number for uniqueness.
+     * @param token      The GitHub authentication token.
+     * @param callback   Callback to return the final unique filename.
+     */
+    private static void checkFileExists(String repoOwner, String repoName, String filename, String extension, int count, String token, Callback callback) {
+        String newFilename;// (count == 0) ? filename + extension : filename + "(" + count + ")" + extension;
+        if (count == 0) {
+            newFilename = filename + extension;
+        } else {
+            newFilename = filename + "(" + count + ")" + extension;
+        }
+
+        getFileSHAAsync(repoOwner, repoName, newFilename, token, new Callback() {
+            @Override
+            public String onResult(String sha) {
+                if (sha == null) {
+                    // File does not exist, return the new unique filename
+                    callback.onResult(newFilename);
+                } else {
+                    // File exists, recursively try with the next count
+                    checkFileExists(repoOwner, repoName, filename, extension, count + 1, token, callback);
+                }
+                return null;
+            }
+        });
+    }
+}
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 2986a5173..2f445b1be 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -40,7 +40,6 @@
   <string name="trackmgr_contextmenu_export">Exportar</string>
   <string name="trackmgr_contextmenu_share">Compartir</string>
   <string name="trackmgr_contextmenu_osm_upload">Subir a OpenStreetMap</string>
-  <string name="trackmgr_contextmenu_github_upload">Subir a GitHub</string>
   <string name="trackmgr_contextmenu_display">Visualizar</string>
   <string name="trackmgr_contextmenu_details">Detalles</string>
   <string name="trackmgr_contextmenu_title">Traza #{0}</string>
@@ -197,53 +196,6 @@
   <string name="github_repository_settings_valid_server">Repositorio de GitHub válido</string>
   <string name="github_repository_settings_invalid_server">Repositorio de GitHub inválido</string>
 
-  <!-- Upload to GitHub functionality-->
-  <string name="successfully_created">Creado exitosamente</string>
-  <string name="error_creating">Error al crear</string>
-  <string name="db_error">Error en base de datos</string>
-  <string name="successfully_saved">Guardado exitosamente</string>
-  <string name="saving_error">Error al guardar</string>
-  <string name="pr_status">Estado del pull request: </string>
-  <string name="repository_information_error">Error al obtener información del repositorio</string>
-  <string name="successfully_uploaded">Subido exitosamente</string>"
-  <string name="error_uploading">Error al subir</string>
-  <string name="gpx_file_read_error">Error al leer el archivo GPX</string>
-  <string name="gpx_file_not_found">No se encontró el archivo GPX</string>
-  <string name="uploading_file">Subiendo: </string>
-  <string name="item_selected">Selecciono el elemento: </string>
-  <string name="github_repository_private">Privado: </string>
-  <string name="github_no_repository_name">Indique el nombre del repositorio</string>
-  <string name="error_field_required">Campo requerido</string>
-  <!-- GitHubConfig -->
-  <string name="github_config_btn">Configurar</string>
-  <string name="github_token_placeholder">Token de GitHub:</string>
-  <string name="github_get_token_btn">Obtener token</string>
-  <string name="github_token_guide_title">Cómo obtener un token de GitHub:</string>
-  <string name="how_to_get_ghToken_guide_step_1">1. Haga clic en el botón para abrir el sitio web de GitHub y crear un token. Inicie sesión con su cuenta de GitHub si es necesario.</string>
-  <string name="how_to_get_ghToken_guide_step_2">2. Seleccione \"Tokens (Clásicos)\" y haga clic en el botón \"Generar nuevo token\".</string>
-  <string name="how_to_get_ghToken_guide_step_3">3. Asigne un nombre descriptivo a su token.</string>
-  <string name="how_to_get_ghToken_guide_step_4">4. Seleccione una fecha de expiración. Recomendamos \"Sin expiración\" para evitar repetir este paso.</string>
-  <string name="how_to_get_ghToken_guide_step_5">5. Seleccione los permisos que desee otorgar al token (OSMTracker solo necesita el permiso de repositorio).</string>
-  <string name="how_to_get_ghToken_guide_step_6">6. Haga clic en el botón \"Generar token\".</string>
-  <string name="how_to_get_ghToken_guide_step_7">7. Copie el token generado y péguelo en el campo \"Token de GitHub\".</string>
-  <string name="how_to_get_ghToken_guide_note">⚠️ Puede guardar en blanco para borrar sus credenciales.</string>
-  <!-- GitHubUpload -->
-  <string name="upload_to_github_description">Mensaje del commit:</string>
-  <string name="upload_to_github_create_fork">Crear\nfork</string>
-  <string name="upload_to_github_open_pull_request">Abrir pull request</string>
-  <string name="upload_to_github_select_repo">Seleccione un repositorio</string>
-  <string name="upload_to_github_create_repository">Crear repositorio</string>
-  <string name="upload_to_github_commit_btn">Hacer commit</string>
-  <string name="upload_to_github_configure">Configurar</string>
-  <!-- GitHubNewFork -->
-  <string name="upload_to_github_forked_repo_owner">Usuario del repositorio original</string>
-  <string name="upload_to_github_forked_repo_name">Nombre del repositorio original</string>
-  <string name="create">Crear</string>
-  <!-- GitHubPullRequest -->
-  <string name="upload_to_github_pr_title">Título del Pull Request</string>
-  <string name="upload_to_github_pr_description">Descripción del Pull Request</string>
-  <!-- END Upload to GitHub functionality-->
-
   <!--App Introduction-->
   <string name="app_intro">Introducción a OSMTracker para Android ™</string>
   <!--Intro - slide1-->
@@ -253,4 +205,4 @@
   <string name="app_intro_slide2_title">Feliz trazado 🗺 😎</string>
   <string name="app_intro_slide2_description">OSMTracker para Android usará su ubicación GPS para registrar puntos de traza y puntos, incluso cuando la aplicación se está ejecutando en segundo plano.
 Tus datos no se utilizan para respaldar anuncios.</string>
-</resources>
+</resources>
\ No newline at end of file


Reviews:
user: login: Kevin-Salazar-itcr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T04:50:40Z
user: login: miltonials
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T05:02:45Z
user: login: Andyporras
body: the bugs that I mentioned are no longer present, if it was because the dev changes were not in this branch
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T02:46:27Z
user: login: FR3DD221
body: I didn´t see any bug or wrong behaviour, so i approved it
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T02:38:48Z
user: login: Kevin-Salazar-itcr
body: ✅ Tasks corrected and verified. Approved.


state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T02:45:16Z

Review comments:
path: app/src/main/java/net/osmtracker/util/GitHubUtils.java
user: login: Kevin-Salazar-itcr
body: Hi @miltonials,

According to @Andyporras' review, I agree with the issue regarding the filenames.
The problem is likely caused by these lines:

```bash
String filename = repoFilePath.substring(0, repoFilePath.lastIndexOf("."));
String extension = repoFilePath.substring(repoFilePath.lastIndexOf("."));
```
The use of lastIndexOf in this context can generate filenames with two consecutive dots (..), especially if the original repoFilePath already contains a trailing dot before the extension...
created_at: 2025-05-14T04:50:40Z
author_association: COLLABORATOR
path: app/src/main/java/net/osmtracker/util/GitHubUtils.java
user: login: miltonials
body: * Solved in: #512 
created_at: 2025-05-14T05:02:45Z
author_association: MEMBER

PR comments:
user: login: Andyporras
created_at: 2025-05-13T04:19:21Z
body: Hi @miltonials, this pull request has two bugs:
* It uses ".." instead of "." in the ZIP filenames, and it also adds a "." before the parentheses ().
* It ignores the "filename for named tracks" setting, as shown in the image below.Always upload the zip file with name or date and tag

![image](https://github.com/user-attachments/assets/bc9b92c4-b239-4bcd-9067-2f9a97737237)

author_association: MEMBER
user: login: miltonials
created_at: 2025-05-14T05:01:35Z
body: Hello @Andyporras @Kevin-Salazar-itcr! Thanks for the feedback. The issue with double dot at the end of a zip filename is solved by the PR:
* #512

@Andyporras regarding to:

> It ignores the "filename for named tracks" setting, as shown in the image below. Always upload the zip file with name or date and tag

I will review again what is happening. It may be the fact that this PR doesn't have the latest changes from the develop branch, since it is completed as of:

* https://github.com/labexp/osmtracker-android/pull/460#issuecomment-2654930451
author_association: MEMBER
user: login: miltonials
created_at: 2025-05-15T00:11:42Z
body: I just moved the PR to the top of the develop commits to have the latest changes from `labexp:develop`. @Andyporras @Kevin-Salazar-itcr can you check it again please
author_association: MEMBER
user: login: Andyporras
created_at: 2025-05-16T02:50:07Z
body: Thanks @miltonials for the solution ✅
author_association: MEMBER