From d7b1da05b6b6a5a28644833f4fa5c92a912d16cc Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Wed, 18 Jun 2025 11:20:39 +0500 Subject: [PATCH 1/9] optimized the code --- README.md | 6 +++--- .../src/main/java/com/github/shortiosdk/Constants.kt | 2 +- .../com/github/shortiosdk/Model/ShortIOParameters.kt | 1 - .../java/com/github/shortiosdk/Model/ShortIOResult.kt | 6 ++---- .../src/main/java/com/github/shortiosdk/ShortIO.kt | 9 ++++----- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e43a543..6b6687f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ It will be: ```kotlin dependencies { implementation("com.github.User:Repo:Tag") // Example - implementation("com.github.Short-io:android-sdk:v1.0.4") // Use this + implementation("com.github.Short-io:android-sdk:v1.0.5") // Use this } ``` ### 3. Sync the Project @@ -104,11 +104,11 @@ thread { try { when (val result = ShortioSdk.shortenUrl(apiKey, params)) { is ShortIOResult.Success -> { - println("Shortened URL: ${result.data.shortURL}") + Log.d("ShortIOResult","Shortened URL: ${result.data.shortURL}") } is ShortIOResult.Error -> { val error = result.data - println("Error ${error.statusCode}: ${error.message} (code: ${error.code})") + Log.d("ShortIOResult","Error ${error.statusCode}: ${error.message} (code: ${error.code})") } } } catch (e: Exception) { diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Constants.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Constants.kt index 1853fea..88003dc 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Constants.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Constants.kt @@ -1,3 +1,3 @@ package com.github.shortiosdk -val shortenUrl = "https://api.short.io/links/public" \ No newline at end of file +val baseURL = "https://api.short.io/links/public" \ No newline at end of file diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOParameters.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOParameters.kt index 349e214..b9cb0f7 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOParameters.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOParameters.kt @@ -1,6 +1,5 @@ package com.github.shortiosdk - data class ShortIOParameters( val originalURL: String, val cloaking: Boolean? = null, diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt index ebe9574..4628080 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt @@ -1,13 +1,11 @@ package com.github.shortiosdk -import com.github.shortiosdk.ShortIOResponseModel - -public sealed class ShortIOResult { +sealed class ShortIOResult { data class Success(val data: ShortIOResponseModel) : ShortIOResult() data class Error(val data: ShortIOErrorModel) : ShortIOResult() } -public sealed class StringOrInt { +sealed class StringOrInt { data class Str(val value: String) : StringOrInt() data class IntVal(val value: Int) : StringOrInt() } diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt index 85f7eba..e943649 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt @@ -5,7 +5,6 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import com.google.gson.GsonBuilder import android.content.Intent -import android.net.Uri import android.util.Log import com.github.shortiosdk.Helpers.StringOrIntSerializer import com.github.shortiosdk.Helpers.HandleClick @@ -25,7 +24,7 @@ object ShortioSdk { val body = jsonBody.toRequestBody(mediaType) val request = Request.Builder() - .url(shortenUrl) + .url(baseURL) .post(body) .addHeader("accept", "application/json") .addHeader("content-type", "application/json") @@ -80,15 +79,15 @@ object ShortioSdk { var response: String? = null val thread = Thread { response = HandleClick(uri.toString()) - Log.d("Response", "Response: $response") + Log.d("HandleClickResponse", "Response: $response") } thread.start() thread.join() if (response == "200") { - Log.d("Success","Response:-${response}") + Log.d("HandleClickResponse","Short SDK click call completed successfully.") } else { - Log.d("Error","Error:- ${response}") + Log.d("HandleClickError","Error:- ${response}") } return UrlComponents( scheme = scheme, From ae86b12b566a7e57ecbe630df4e154e37ea29dd5 Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Fri, 27 Jun 2025 18:24:50 +0500 Subject: [PATCH 2/9] integrated encrypted links functionality and updated README.md file --- README.md | 27 +++++++--- .../github/shortiosdk/Model/ShortIOResult.kt | 5 ++ .../java/com/github/shortiosdk/ShortIO.kt | 49 ++++++++++++++++--- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6b6687f..9c6d986 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ It will be: ```kotlin dependencies { implementation("com.github.User:Repo:Tag") // Example - implementation("com.github.Short-io:android-sdk:v1.0.5") // Use this + implementation("com.github.Short-io:android-sdk:v1.0.6") // Use this } ``` ### 3. Sync the Project @@ -241,7 +241,7 @@ keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -sto 3. Tap on **β€œAdd link”** under the **Open by Default** section. -4. Add your URL and make sure to enable the checkbox for your link. +4. Add your URL if not added and make sure to enable the checkbox for your link. ### πŸ”— Step 4: Open the App Using a Deep Link @@ -260,8 +260,10 @@ keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -sto ```kotlin override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - val result = ShortioSdk.handleIntent(intent) - Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}") + lifecycleScope.launch { + val result = ShortioSdk.handleIntent(intent) + Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}") + } } ``` @@ -270,13 +272,26 @@ override fun onNewIntent(intent: Intent) { ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycleScope.launch { val result = ShortioSdk.handleIntent(intent) - Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}") + Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}") + } } ``` +### πŸ” Secure Short Link + +If you want to encrypt the original URL before shortening it. For privacy or security reasons β€” the SDK provides a utility function called createSecure. This function encrypts the original URL using AES-GCM and returns a secured URL with a separate decryption key. + +```kotlin +val originalURL = "your_original_URL" +val result = ShortioSdk.createSecure(originalURL) +Log.d("SecureURL", "RESULT: ${result}") +Log.d("securedOriginalURL", "URL: ${result.securedOriginalURL}") +Log.d("securedShortUrl", "URL: ${result.securedShortUrl}") +``` -### βœ… Final Checklist +### βœ… Final Checklist for Deep Linking * App is signed with the correct keystore. diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt index 4628080..51aff88 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt @@ -9,3 +9,8 @@ sealed class StringOrInt { data class Str(val value: String) : StringOrInt() data class IntVal(val value: Int) : StringOrInt() } + +data class SecureResult( + val securedOriginalURL: String, + val securedShortUrl: String +) \ No newline at end of file diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt index e943649..43ff43b 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt @@ -8,6 +8,14 @@ import android.content.Intent import android.util.Log import com.github.shortiosdk.Helpers.StringOrIntSerializer import com.github.shortiosdk.Helpers.HandleClick +import android.util.Base64 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.charset.StandardCharsets +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.GCMParameterSpec object ShortioSdk { @@ -69,20 +77,16 @@ object ShortioSdk { } } - fun handleIntent(intent: Intent): UrlComponents? { + suspend fun handleIntent(intent: Intent): UrlComponents? { val uri = intent.data ?: return null val scheme = uri.scheme?.lowercase() if (scheme != "http" && scheme != "https") return null val host = uri.host ?: return null - var response: String? = null - val thread = Thread { - response = HandleClick(uri.toString()) - Log.d("HandleClickResponse", "Response: $response") + val response = withContext(Dispatchers.IO) { + HandleClick(uri.toString()) } - thread.start() - thread.join() if (response == "200") { Log.d("HandleClickResponse","Short SDK click call completed successfully.") @@ -98,4 +102,35 @@ object ShortioSdk { fullUrl = uri.toString() ) } + + fun createSecure(originalURL: String): SecureResult { + return try { + + val keyGenerator = KeyGenerator.getInstance("AES") + keyGenerator.init(128) + val secretKey = keyGenerator.generateKey() + + val iv = ByteArray(12) + SecureRandom().nextBytes(iv) + + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec) + val urlBytes = originalURL.toByteArray(StandardCharsets.UTF_8) + val encryptedBytes = cipher.doFinal(urlBytes) + + val encryptedUrlBase64 = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP) + val encryptedIvBase64 = Base64.encodeToString(iv, Base64.NO_WRAP) + val securedOriginalURL = "shortsecure://$encryptedUrlBase64?$encryptedIvBase64" + + val rawKey = secretKey.encoded + val keyBase64 = Base64.encodeToString(rawKey, Base64.NO_WRAP) + val securedShortUrl = "#$keyBase64" + + SecureResult(securedOriginalURL, securedShortUrl) + } catch (e: Exception) { + e.printStackTrace() + throw e + } + } } From b13edc915bf4ec8410712c61425d270144883747 Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Fri, 8 Aug 2025 20:02:42 +0500 Subject: [PATCH 3/9] add link conversion tracking and return destination URL from SDK --- .../shortiosdk/Helpers/HelperMethods.kt | 83 +++++++++++++++++++ .../github/shortiosdk/Model/UrlComponents.kt | 3 +- .../java/com/github/shortiosdk/ShortIO.kt | 15 +++- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt index eda18fe..b3fa367 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt @@ -1,7 +1,17 @@ package com.github.shortiosdk.Helpers +import android.net.Uri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.Response +import java.net.HttpURLConnection +import java.net.URL +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.iterator +import kotlin.collections.joinToString fun HandleClick(uri: String): String? { @@ -29,4 +39,77 @@ fun HandleClick(uri: String): String? { } catch (e: Exception) { e.toString() } +} + +fun extractClidFromUrl(urlString: String): String? { + return try { + val uri = Uri.parse(urlString) + uri.getQueryParameter("clid") + } catch (e: Exception) { + e.printStackTrace() + null + } +} + +suspend fun fetchHeadersValue(urlString: String): String? { + return try { + val url = URL(urlString) + val connection = withContext(Dispatchers.IO) { + url.openConnection() as HttpURLConnection + } + + connection.requestMethod = "HEAD" + connection.instanceFollowRedirects = false + + connection.connect() + + println("Response Headers:") + for ((key, value) in connection.headerFields) { + if (key != null && value != null) { + println(" $key: ${value.joinToString()}") + } + } + + val redirectedUrl = connection.getHeaderField("Location") + + connection.disconnect() + + redirectedUrl ?: "Not Found" + } catch (e: Exception) { + println("Network error: ${e.localizedMessage}") + null + } +} + +suspend fun trackConversion( + originalURL: String, + clid: String? +): Boolean = withContext(Dispatchers.IO) { + val client = OkHttpClient() + + val baseURL = if (originalURL.endsWith("/")) originalURL.dropLast(1) else originalURL + + val conversionURLString = "$baseURL/.shortio/conversion?clid=$clid" + + try { + val request = Request.Builder() + .url(conversionURLString) + .get() + .build() + + val response: Response = client.newCall(request).execute() + response.use { res -> + if (res.isSuccessful) { + val bodyString = res.body?.string() + println("Success! Response body: $bodyString") + } else { + println("Failed with status code: ${res.code}") + println("Response message: ${res.message}") + } + } + response.use { it.isSuccessful } + } catch (e: Exception) { + e.printStackTrace() + false + } } \ No newline at end of file diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/UrlComponents.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/UrlComponents.kt index cc5e87a..791fae9 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/UrlComponents.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/UrlComponents.kt @@ -6,5 +6,6 @@ data class UrlComponents( val path: String?, val query: String?, val fragment: String?, - val fullUrl: String + val fullUrl: String, + val destinationUrl: String? ) \ No newline at end of file diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt index 43ff43b..4abbba5 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt @@ -9,6 +9,9 @@ import android.util.Log import com.github.shortiosdk.Helpers.StringOrIntSerializer import com.github.shortiosdk.Helpers.HandleClick import android.util.Base64 +import com.github.shortiosdk.Helpers.extractClidFromUrl +import com.github.shortiosdk.Helpers.fetchHeadersValue +import com.github.shortiosdk.Helpers.trackConversion import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.nio.charset.StandardCharsets @@ -84,6 +87,15 @@ object ShortioSdk { val host = uri.host ?: return null + val shortioClidUrl = withContext(Dispatchers.IO) { + fetchHeadersValue(uri.toString()) + } + + val clid = shortioClidUrl?.let { extractClidFromUrl(it) } + + val trackingSuccess = trackConversion("https://$host", clid = clid) + println("Conversion success: $trackingSuccess") + val response = withContext(Dispatchers.IO) { HandleClick(uri.toString()) } @@ -99,7 +111,8 @@ object ShortioSdk { path = uri.path?.removePrefix("/"), query = uri.encodedQuery, fragment = uri.fragment, - fullUrl = uri.toString() + fullUrl = uri.toString(), + destinationUrl = shortioClidUrl ) } From 311fbf842ae3dd9b358ec53c4a4bd1717f8b555f Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Mon, 11 Aug 2025 14:49:50 +0500 Subject: [PATCH 4/9] updated readme included section for conversion tracking with better copy --- README.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c6d986..c27875b 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ It will be: ```kotlin dependencies { implementation("com.github.User:Repo:Tag") // Example - implementation("com.github.Short-io:android-sdk:v1.0.6") // Use this + implementation("com.github.Short-io:android-sdk:v1.0.7") // Use this } ``` ### 3. Sync the Project @@ -262,7 +262,7 @@ override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) lifecycleScope.launch { val result = ShortioSdk.handleIntent(intent) - Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}") + Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}, DestinationURL: ${result?.destinationUrl}") } } ``` @@ -274,7 +274,7 @@ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { val result = ShortioSdk.handleIntent(intent) - Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}") + Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}, DestinationURL: ${result?.destinationUrl}") } } ``` @@ -291,6 +291,25 @@ Log.d("securedOriginalURL", "URL: ${result.securedOriginalURL}") Log.d("securedShortUrl", "URL: ${result.securedShortUrl}") ``` +### πŸ”„ Conversion Tracking + +Conversion tracking is automated in the SDK β€” no manual setup or method calls are required. +When your app is opened via a deep link, the SDK automatically records the conversion in the background through the `handleIntent` intent-handling method. Conversion tracking will only be triggered once `handleIntent` is called, so until that happens, no conversion will be recorded. + +This means: +- No need to call trackConversion manually. +- Conversions are automatically sent whenever the app is launched from a deep link. +- Conversion tracking without additional code. + +Example: +Simply handle incoming deep links with the SDK’s handleOpen method β€” conversion tracking happens automatically: +```kotlin + lifecycleScope.launch { + val result = ShortioSdk.handleIntent(intent) + Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}, DestinationURL: ${result?.destinationUrl}") + } +``` + ### βœ… Final Checklist for Deep Linking * App is signed with the correct keystore. From 233a0fb9072e493ec205bc2bbbf12ab373de1625 Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Tue, 12 Aug 2025 20:19:26 +0500 Subject: [PATCH 5/9] * Fixed multiple calls and instances issue --- README.md | 51 +++++---- .../shortiosdk/Helpers/HelperMethods.kt | 101 +++--------------- .../shortiosdk/Model/ShortIOParameters.kt | 2 +- .../java/com/github/shortiosdk/ShortIO.kt | 96 +++++++++++++---- 4 files changed, 128 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index c27875b..a059bef 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ It will be: ```kotlin dependencies { implementation("com.github.User:Repo:Tag") // Example - implementation("com.github.Short-io:android-sdk:v1.0.7") // Use this + implementation("com.github.Short-io:android-sdk:v1.0.8") // Use this } ``` ### 3. Sync the Project @@ -80,6 +80,23 @@ import com.github.shortiosdk.ShortioSdk ### πŸ”— SDK Usage +#### Initialization + +To start using ShortioSdk, you need to initialize it early in your app lifecycle, preferably in your Activity's onCreate() method or in your custom Application class. + +Example: Initialize in Activity + +```kotlin + override fun onCreate() { + super.onCreate() + ShortioSdk.initialize(apiKey, domain) + } +``` +* apiKey: Your API key string for authenticating requests. +* domain: The default domain to use for URL shortening. + +##### Create Short URL + ```kotlin import com.github.shortiosdk.ShortioSdk import com.github.shortiosdk.ShortIOParameters @@ -87,7 +104,6 @@ import com.github.shortiosdk.ShortIOResult try { val params = ShortIOParameters( - domain = "your_domain", // Replace with your Short.io domain originalURL = "your_originalURL" // Replace with your Short.io domain ) } catch (e: Exception) { @@ -98,11 +114,10 @@ try { **Note**: Both `domain` and `originalURL` are the required parameters. You can also pass optional parameters such as `path`, `title`, `utmParameters`, etc. ```kotlin -val apiKey = "your_public_apiKey" // Replace with your Short.io Public API Key thread { try { - when (val result = ShortioSdk.shortenUrl(apiKey, params)) { + when (val result = ShortioSdk.shortenUrl(params)) { is ShortIOResult.Success -> { Log.d("ShortIOResult","Shortened URL: ${result.data.shortURL}") } @@ -123,7 +138,7 @@ The `ShortIOParameters` struct is used to define the details of the short link y | Parameter | Type | Required | Description | | ------------------- | ----------- | -------- | ------------------------------------------------------------ | -| `domain` | `String` | βœ… | Your Short.io domain (e.g., `example.short.gy`) | +| `domain` | `String` | ❌ | Your Short.io domain (e.g., `example.short.gy`) | | `originalURL` | `String` | βœ… | The original URL to be shortened | | `cloaking` | `Boolean` | ❌ | If `true`, hides the destination URL from the user | | `password` | `String` | ❌ | Password to protect the short link | @@ -293,22 +308,22 @@ Log.d("securedShortUrl", "URL: ${result.securedShortUrl}") ### πŸ”„ Conversion Tracking -Conversion tracking is automated in the SDK β€” no manual setup or method calls are required. -When your app is opened via a deep link, the SDK automatically records the conversion in the background through the `handleIntent` intent-handling method. Conversion tracking will only be triggered once `handleIntent` is called, so until that happens, no conversion will be recorded. +Track conversions for your short links to measure campaign effectiveness. The SDK provides a simple method to record conversions. -This means: -- No need to call trackConversion manually. -- Conversions are automatically sent whenever the app is launched from a deep link. -- Conversion tracking without additional code. - -Example: -Simply handle incoming deep links with the SDK’s handleOpen method β€” conversion tracking happens automatically: ```kotlin - lifecycleScope.launch { - val result = ShortioSdk.handleIntent(intent) - Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}, DestinationURL: ${result?.destinationUrl}") + CoroutineScope(Dispatchers.IO).launch { + try { + val res = ShortioSdk.trackConversion( + "https://{your_domain}/", + conversionId = null + ) + // conversionId can be 'signup', 'purchase', 'download', etc. + Log.d("Handle Conversion Tracking", "Handle Conversion Tracking: $res") + } catch (e: Exception) { + Log.e("Handle Conversion Tracking", "Error calling trackConversion", e) + } } -``` + ### βœ… Final Checklist for Deep Linking diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt index b3fa367..91b85b8 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt @@ -5,42 +5,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.Response -import java.net.HttpURLConnection import java.net.URL -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.iterator -import kotlin.collections.joinToString -fun HandleClick(uri: String): String? { - val client = OkHttpClient() - - val url = when { - uri.contains("utm_medium=android", ignoreCase = true) -> uri - uri.contains("?") -> "$uri&utm_medium=android" - else -> "$uri?utm_medium=android" - } - - val request = Request.Builder() - .url(url) - .addHeader("accept", "application/json") - .build() - - return try { - client.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.code.toString() - } else { - "Link is not Valid" - } - } - } catch (e: Exception) { - e.toString() - } -} - fun extractClidFromUrl(urlString: String): String? { return try { val uri = Uri.parse(urlString) @@ -51,65 +18,29 @@ fun extractClidFromUrl(urlString: String): String? { } } -suspend fun fetchHeadersValue(urlString: String): String? { - return try { - val url = URL(urlString) - val connection = withContext(Dispatchers.IO) { - url.openConnection() as HttpURLConnection +suspend fun HandleClick(uriString: String): String? = withContext(Dispatchers.IO) { + try { + val urlString = when { + uriString.contains("utm_medium=android", ignoreCase = true) -> uriString + uriString.contains("?") -> "$uriString&utm_medium=android" + else -> "$uriString?utm_medium=android" } - connection.requestMethod = "HEAD" - connection.instanceFollowRedirects = false - - connection.connect() - - println("Response Headers:") - for ((key, value) in connection.headerFields) { - if (key != null && value != null) { - println(" $key: ${value.joinToString()}") - } - } + val client = OkHttpClient.Builder() + .followRedirects(false) + .build() - val redirectedUrl = connection.getHeaderField("Location") + val request = Request.Builder() + .url(URL(urlString)) + .head() + .build() - connection.disconnect() + client.newCall(request).execute().use { response -> - redirectedUrl ?: "Not Found" + response.header("Location") + } } catch (e: Exception) { println("Network error: ${e.localizedMessage}") null } } - -suspend fun trackConversion( - originalURL: String, - clid: String? -): Boolean = withContext(Dispatchers.IO) { - val client = OkHttpClient() - - val baseURL = if (originalURL.endsWith("/")) originalURL.dropLast(1) else originalURL - - val conversionURLString = "$baseURL/.shortio/conversion?clid=$clid" - - try { - val request = Request.Builder() - .url(conversionURLString) - .get() - .build() - - val response: Response = client.newCall(request).execute() - response.use { res -> - if (res.isSuccessful) { - val bodyString = res.body?.string() - println("Success! Response body: $bodyString") - } else { - println("Failed with status code: ${res.code}") - println("Response message: ${res.message}") - } - } - response.use { it.isSuccessful } - } catch (e: Exception) { - e.printStackTrace() - false - } -} \ No newline at end of file diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOParameters.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOParameters.kt index b9cb0f7..e34b3dd 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOParameters.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOParameters.kt @@ -29,7 +29,7 @@ data class ShortIOParameters( val integrationFB: String? = null, var integrationGA: String? = null, val integrationGTM: String? = null, - val domain: String, + var domain: String? = null, val folderId: String? = null ) { init { diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt index 4abbba5..ca19c5d 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt @@ -5,15 +5,15 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import com.google.gson.GsonBuilder import android.content.Intent -import android.util.Log import com.github.shortiosdk.Helpers.StringOrIntSerializer import com.github.shortiosdk.Helpers.HandleClick import android.util.Base64 +import android.util.Log import com.github.shortiosdk.Helpers.extractClidFromUrl -import com.github.shortiosdk.Helpers.fetchHeadersValue -import com.github.shortiosdk.Helpers.trackConversion import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.net.URI +import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.security.SecureRandom import javax.crypto.Cipher @@ -22,13 +22,32 @@ import javax.crypto.spec.GCMParameterSpec object ShortioSdk { + var savedDomain: String? = null + var storedClid: String = "" + + var apiKey: String = "" + var isInitialized: Boolean = false + + fun initialize( apiKey: String, domain: String) { + if (!isInitialized){ + this.apiKey = apiKey + this.savedDomain = domain + println("SDK initialized with API key: $apiKey") + isInitialized = true + } else{ + Log.d("SDK Initialization","SDK is already Initialzed") + } + } + fun shortenUrl( - apiKey: String, parameters: ShortIOParameters ): ShortIOResult { val gson = GsonBuilder() .registerTypeAdapter(StringOrInt::class.java, StringOrIntSerializer()) .create() + if (parameters.domain.isNullOrBlank()) { + parameters.domain = savedDomain!! + } val client = OkHttpClient() val mediaType = "application/json".toMediaType() val jsonBody = gson.toJson(parameters) @@ -88,23 +107,11 @@ object ShortioSdk { val host = uri.host ?: return null val shortioClidUrl = withContext(Dispatchers.IO) { - fetchHeadersValue(uri.toString()) - } - - val clid = shortioClidUrl?.let { extractClidFromUrl(it) } - - val trackingSuccess = trackConversion("https://$host", clid = clid) - println("Conversion success: $trackingSuccess") - - val response = withContext(Dispatchers.IO) { HandleClick(uri.toString()) } - if (response == "200") { - Log.d("HandleClickResponse","Short SDK click call completed successfully.") - } else { - Log.d("HandleClickError","Error:- ${response}") - } + storedClid = shortioClidUrl.let { it?.let { urlString -> extractClidFromUrl(urlString) } ?: "" } + return UrlComponents( scheme = scheme, host = host, @@ -116,6 +123,11 @@ object ShortioSdk { ) } + /** + * It creates a Secure URL + - parameters: originalURL: String + - Returns: SecureResult + */ fun createSecure(originalURL: String): SecureResult { return try { @@ -146,4 +158,52 @@ object ShortioSdk { throw e } } + + /** + - parameters: originalURL: String, clid: String, conversionId: String? = nil + - conversionId can be 'signup', 'purchase', 'download', etc. + - Returns: Result Boolean based on status code + */ + suspend fun trackConversion( + originalURL: String, + clid: String? = null, + conversionId: String? = null + ): Boolean = withContext(Dispatchers.IO) { + try { + val originalUri = URI(originalURL) + val scheme = originalUri.scheme ?: return@withContext false + val host = originalUri.host ?: return@withContext false + + // Build query params + val queryParams = mutableListOf>() + if (!conversionId.isNullOrEmpty()) { + queryParams.add("c" to conversionId) + } + if (!clid.isNullOrEmpty()) { + queryParams.add("clid" to clid) + } else{ + queryParams.add("clid" to storedClid) + } + + val queryString = queryParams.joinToString("&") { + "${it.first}=${URLEncoder.encode(it.second, "UTF-8")}" + } + + val finalUrl = "$scheme://$host/.shortio/conversion?$queryString" + + val client = OkHttpClient() + + val request = Request.Builder() + .url(finalUrl) + .get() + .build() + + client.newCall(request).execute().use { response -> + return@withContext response.isSuccessful + } + } catch (e: Exception) { + e.printStackTrace() + throw e + } + } } From dfcbc9f0c724459d5460b12302888ba6cd720ca3 Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Wed, 13 Aug 2025 12:12:09 +0500 Subject: [PATCH 6/9] * added comments in the sdk --- README.md | 38 ++++---- .../shortiosdk/Helpers/HelperMethods.kt | 32 ------- .../java/com/github/shortiosdk/ShortIO.kt | 93 ++++++++++++++++--- 3 files changed, 100 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index a059bef..0959543 100644 --- a/README.md +++ b/README.md @@ -87,15 +87,15 @@ To start using ShortioSdk, you need to initialize it early in your app lifecycle Example: Initialize in Activity ```kotlin - override fun onCreate() { - super.onCreate() - ShortioSdk.initialize(apiKey, domain) - } +override fun onCreate() { + super.onCreate() + ShortioSdk.initialize(apiKey, domain) +} ``` -* apiKey: Your API key string for authenticating requests. +* apiKey: Your API key string for SDK initialization. * domain: The default domain to use for URL shortening. -##### Create Short URL +#### Create Short URL ```kotlin import com.github.shortiosdk.ShortioSdk @@ -111,7 +111,7 @@ try { } ``` -**Note**: Both `domain` and `originalURL` are the required parameters. You can also pass optional parameters such as `path`, `title`, `utmParameters`, etc. +**Note**: Only the `originalURL` is the required parameter as `domain` is passed in the initialize method of SDK. You can also pass optional parameters such as `path`, `title`, `utmParameters`, etc. ```kotlin @@ -311,19 +311,19 @@ Log.d("securedShortUrl", "URL: ${result.securedShortUrl}") Track conversions for your short links to measure campaign effectiveness. The SDK provides a simple method to record conversions. ```kotlin - CoroutineScope(Dispatchers.IO).launch { - try { - val res = ShortioSdk.trackConversion( - "https://{your_domain}/", - conversionId = null - ) - // conversionId can be 'signup', 'purchase', 'download', etc. - Log.d("Handle Conversion Tracking", "Handle Conversion Tracking: $res") - } catch (e: Exception) { - Log.e("Handle Conversion Tracking", "Error calling trackConversion", e) - } +CoroutineScope(Dispatchers.IO).launch { + try { + val res = ShortioSdk.trackConversion( + "https://{your_domain}/", + conversionId = null + ) + // conversionId can be 'signup', 'purchase', 'download', etc. + Log.d("Handle Conversion Tracking", "Handle Conversion Tracking: $res") + } catch (e: Exception) { + Log.e("Handle Conversion Tracking", "Error calling trackConversion", e) } - +} +``` ### βœ… Final Checklist for Deep Linking diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt index 91b85b8..dbbd7eb 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt @@ -1,11 +1,6 @@ package com.github.shortiosdk.Helpers import android.net.Uri -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import java.net.URL fun extractClidFromUrl(urlString: String): String? { @@ -17,30 +12,3 @@ fun extractClidFromUrl(urlString: String): String? { null } } - -suspend fun HandleClick(uriString: String): String? = withContext(Dispatchers.IO) { - try { - val urlString = when { - uriString.contains("utm_medium=android", ignoreCase = true) -> uriString - uriString.contains("?") -> "$uriString&utm_medium=android" - else -> "$uriString?utm_medium=android" - } - - val client = OkHttpClient.Builder() - .followRedirects(false) - .build() - - val request = Request.Builder() - .url(URL(urlString)) - .head() - .build() - - client.newCall(request).execute().use { response -> - - response.header("Location") - } - } catch (e: Exception) { - println("Network error: ${e.localizedMessage}") - null - } -} diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt index ca19c5d..4f7bf68 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt @@ -6,13 +6,13 @@ import okhttp3.RequestBody.Companion.toRequestBody import com.google.gson.GsonBuilder import android.content.Intent import com.github.shortiosdk.Helpers.StringOrIntSerializer -import com.github.shortiosdk.Helpers.HandleClick import android.util.Base64 import android.util.Log import com.github.shortiosdk.Helpers.extractClidFromUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.net.URI +import java.net.URL import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.security.SecureRandom @@ -22,12 +22,18 @@ import javax.crypto.spec.GCMParameterSpec object ShortioSdk { + var apiKey: String = "" + var savedDestinationUrl: String = "" var savedDomain: String? = null var storedClid: String = "" - - var apiKey: String = "" var isInitialized: Boolean = false + /** + * Initialize the SDK through this method to use it. + * Parameters required: + * apiKey of type String + * domain of type String + */ fun initialize( apiKey: String, domain: String) { if (!isInitialized){ this.apiKey = apiKey @@ -39,9 +45,24 @@ object ShortioSdk { } } + /** + * Use this method to check the SDK is already initialized + */ + fun isSdkInitialized(): Boolean { + if (!isInitialized) { + throw IllegalStateException("SDK is not initialized. Please call initialize before using the SDK.") + } + return true + } + + /** + * Create ShortUrl by using shortenUrl Method + * Parameters:- It takes ShortIOParameters as parameter which includes originalUrl, domain, clocking, password, title etc. + */ fun shortenUrl( parameters: ShortIOParameters ): ShortIOResult { + isSdkInitialized() val gson = GsonBuilder() .registerTypeAdapter(StringOrInt::class.java, StringOrIntSerializer()) .create() @@ -98,7 +119,12 @@ object ShortioSdk { return ShortIOResult.Error(errorModel) } } - + + /** + * handleIntent() method is used handle the intent and it returns UrlComponents + * Parameters: intent of type Intent + * Returns: UrlComponents which includes scheme, host, path, destibnationUrl, etc. + */ suspend fun handleIntent(intent: Intent): UrlComponents? { val uri = intent.data ?: return null val scheme = uri.scheme?.lowercase() @@ -109,7 +135,9 @@ object ShortioSdk { val shortioClidUrl = withContext(Dispatchers.IO) { HandleClick(uri.toString()) } - + if (shortioClidUrl != null) { + savedDestinationUrl = shortioClidUrl + } storedClid = shortioClidUrl.let { it?.let { urlString -> extractClidFromUrl(urlString) } ?: "" } return UrlComponents( @@ -123,12 +151,46 @@ object ShortioSdk { ) } + /** + * HandleClick() is used to track the click + * Parameters: It takes uriString of type String as parameter. + * Returns String + */ + suspend private fun HandleClick(uriString: String): String? = withContext(Dispatchers.IO) { + isSdkInitialized() + try { + val urlString = when { + uriString.contains("utm_medium=android", ignoreCase = true) -> uriString + uriString.contains("?") -> "$uriString&utm_medium=android" + else -> "$uriString?utm_medium=android" + } + + val client = OkHttpClient.Builder() + .followRedirects(false) + .build() + + val request = Request.Builder() + .url(URL(urlString)) + .head() + .build() + + client.newCall(request).execute().use { response -> + + response.header("Location") + } + } catch (e: Exception) { + println("Network error: ${e.localizedMessage}") + null + } + } + /** * It creates a Secure URL - - parameters: originalURL: String - - Returns: SecureResult + * parameters: originalURL: String + * Returns: SecureResult which includes securedOriginalURL and securedShortUrl */ fun createSecure(originalURL: String): SecureResult { + isSdkInitialized() return try { val keyGenerator = KeyGenerator.getInstance("AES") @@ -160,17 +222,24 @@ object ShortioSdk { } /** - - parameters: originalURL: String, clid: String, conversionId: String? = nil - - conversionId can be 'signup', 'purchase', 'download', etc. - - Returns: Result Boolean based on status code + * trackConversion() method is used to track the conversion. + * parameters: originalURL: String, clid: String, conversionId: String? = nil + * conversionId can be 'signup', 'purchase', 'download', etc. + * Returns: Result Boolean based on status code */ suspend fun trackConversion( - originalURL: String, + originalURL: String? = null, clid: String? = null, conversionId: String? = null ): Boolean = withContext(Dispatchers.IO) { + isSdkInitialized() try { - val originalUri = URI(originalURL) + val originalURLString = if (originalURL.isNullOrEmpty()){ + savedDestinationUrl + } else { + originalURL + } + val originalUri = URI(originalURLString) val scheme = originalUri.scheme ?: return@withContext false val host = originalUri.host ?: return@withContext false From d4d974a63652014b45a815a4310958bcd400bf8b Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Wed, 13 Aug 2025 17:22:35 +0500 Subject: [PATCH 7/9] * updated code --- README.md | 14 +- .../shortiosdk/Helpers/HelperMethods.kt | 15 ++ .../java/com/github/shortiosdk/ShortIO.kt | 149 +++++++++++++++--- 3 files changed, 150 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0959543..d1405df 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,11 @@ override fun onCreate() { * apiKey: Your API key string for SDK initialization. * domain: The default domain to use for URL shortening. -#### Create Short URL +### πŸ’‘ How It Works + +The app demonstrates: + +#### βœ… Generating Short Links ```kotlin import com.github.shortiosdk.ShortioSdk @@ -131,6 +135,8 @@ thread { } } ``` +**Note**: Deprecated: `shortenUrl`(apiKey, params) is still supported for backward compatibility but is no longer recommended for use. Use shortenUrl(params) instead. + ## πŸ“„ API Parameters The `ShortIOParameters` struct is used to define the details of the short link you want to create. Below are the available parameters: @@ -138,7 +144,7 @@ The `ShortIOParameters` struct is used to define the details of the short link y | Parameter | Type | Required | Description | | ------------------- | ----------- | -------- | ------------------------------------------------------------ | -| `domain` | `String` | ❌ | Your Short.io domain (e.g., `example.short.gy`) | +| `domain` | `String` | βœ… | Your Short.io domain (e.g., `example.short.gy`) | | `originalURL` | `String` | βœ… | The original URL to be shortened | | `cloaking` | `Boolean` | ❌ | If `true`, hides the destination URL from the user | | `password` | `String` | ❌ | Password to protect the short link | @@ -315,8 +321,10 @@ CoroutineScope(Dispatchers.IO).launch { try { val res = ShortioSdk.trackConversion( "https://{your_domain}/", - conversionId = null + "your_clid" + conversionId = null ) + /// all parameters are optional // conversionId can be 'signup', 'purchase', 'download', etc. Log.d("Handle Conversion Tracking", "Handle Conversion Tracking: $res") } catch (e: Exception) { diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt index dbbd7eb..b32a4b6 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt @@ -12,3 +12,18 @@ fun extractClidFromUrl(urlString: String): String? { null } } + +fun removeUtmParams(url: String): String { + val uri = Uri.parse(url) + val builder = uri.buildUpon().clearQuery() + + uri.queryParameterNames + .filter { !it.startsWith("utm_", ignoreCase = true) } + .forEach { key -> + uri.getQueryParameters(key)?.forEach { value -> + builder.appendQueryParameter(key, value) + } + } + + return builder.build().toString() +} \ No newline at end of file diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt index 4f7bf68..9e17322 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt @@ -9,8 +9,10 @@ import com.github.shortiosdk.Helpers.StringOrIntSerializer import android.util.Base64 import android.util.Log import com.github.shortiosdk.Helpers.extractClidFromUrl +import com.github.shortiosdk.Helpers.removeUtmParams import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.net.HttpURLConnection import java.net.URI import java.net.URL import java.net.URLEncoder @@ -19,10 +21,14 @@ import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.spec.GCMParameterSpec +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.iterator +import kotlin.collections.joinToString object ShortioSdk { - var apiKey: String = "" + var storedApiKey: String = "" var savedDestinationUrl: String = "" var savedDomain: String? = null var storedClid: String = "" @@ -36,7 +42,7 @@ object ShortioSdk { */ fun initialize( apiKey: String, domain: String) { if (!isInitialized){ - this.apiKey = apiKey + this.storedApiKey = apiKey this.savedDomain = domain println("SDK initialized with API key: $apiKey") isInitialized = true @@ -50,13 +56,96 @@ object ShortioSdk { */ fun isSdkInitialized(): Boolean { if (!isInitialized) { - throw IllegalStateException("SDK is not initialized. Please call initialize before using the SDK.") + throw IllegalStateException("SDK is not initialized. Please initialize the SDk before using it.") } return true } /** - * Create ShortUrl by using shortenUrl Method + * This function will deprecates soon. "Use shortenUrl(parameters) instead" + * Create ShortUrl by using shortenUrl Method. + * Parameters:- It takes ShortIOParameters as parameter which includes originalUrl, domain, clocking, password, title etc. It also apiKey. + */ + @Deprecated( + message = "Use shortenUrl(parameters) instead", + replaceWith = ReplaceWith("shortenUrl(parameters)"), + level = DeprecationLevel.WARNING + ) + fun shortenUrl( + deprecatedApiKey: String? = null, + parameters: ShortIOParameters + ): ShortIOResult { + val gson = GsonBuilder() + .registerTypeAdapter(StringOrInt::class.java, StringOrIntSerializer()) + .create() + if (parameters.domain.isNullOrBlank()){ + val errorModel = ShortIOErrorModel( + message = "Domain Not Provided", + statusCode = 400, + code = "MALFORMED_SUCCESS", + success = false + ) + return ShortIOResult.Error(errorModel) + } + var apiKey = if (!deprecatedApiKey.isNullOrBlank()){ + deprecatedApiKey + } else{ + storedApiKey + } + val client = OkHttpClient() + val mediaType = "application/json".toMediaType() + val jsonBody = gson.toJson(parameters) + val body = jsonBody.toRequestBody(mediaType) + + val request = Request.Builder() + .url(baseURL) + .post(body) + .addHeader("accept", "application/json") + .addHeader("content-type", "application/json") + .addHeader("authorization", apiKey) + .build() + + val response = client.newCall(request).execute() + val responseBody = response.body?.string() + + return if (response.isSuccessful) { + val model = responseBody?.let { gson.fromJson(it, ShortIOResponseModel::class.java) } + if (model != null) { + ShortIOResult.Success(model) + } else { + val errorModel = ShortIOErrorModel( + message = "Empty or malformed success response", + statusCode = response.code, + code = "MALFORMED_SUCCESS", + success = false + ) + ShortIOResult.Error(errorModel) + } + } else { + val errorModel = try { + responseBody?.let { + gson.fromJson(it, ShortIOErrorModel::class.java)?.copy(statusCode = response.code) + } ?: ShortIOErrorModel( + message = "Unknown error", + statusCode = response.code, + code = "UNKNOWN", + success = false + ) + } catch (e: Exception) { + ShortIOErrorModel( + message = "Malformed error response: ${e.localizedMessage}", + statusCode = response.code, + code = "INVALID_JSON", + success = false + ) + } + return ShortIOResult.Error(errorModel) + } + } + + /** + * This is new method of creating short URL. + * Create ShortUrl by using shortenUrl(parameters: ShortIOParameters) Method * Parameters:- It takes ShortIOParameters as parameter which includes originalUrl, domain, clocking, password, title etc. */ fun shortenUrl( @@ -69,6 +158,7 @@ object ShortioSdk { if (parameters.domain.isNullOrBlank()) { parameters.domain = savedDomain!! } + val client = OkHttpClient() val mediaType = "application/json".toMediaType() val jsonBody = gson.toJson(parameters) @@ -79,7 +169,7 @@ object ShortioSdk { .post(body) .addHeader("accept", "application/json") .addHeader("content-type", "application/json") - .addHeader("authorization", apiKey) + .addHeader("authorization", storedApiKey) .build() val response = client.newCall(request).execute() @@ -133,13 +223,15 @@ object ShortioSdk { val host = uri.host ?: return null val shortioClidUrl = withContext(Dispatchers.IO) { - HandleClick(uri.toString()) - } - if (shortioClidUrl != null) { - savedDestinationUrl = shortioClidUrl + handleClick(uri.toString()) } storedClid = shortioClidUrl.let { it?.let { urlString -> extractClidFromUrl(urlString) } ?: "" } + val destinationUrl = shortioClidUrl?.let { removeUtmParams(it) } + if (destinationUrl != null) { + savedDestinationUrl = destinationUrl + } + return UrlComponents( scheme = scheme, host = host, @@ -147,37 +239,46 @@ object ShortioSdk { query = uri.encodedQuery, fragment = uri.fragment, fullUrl = uri.toString(), - destinationUrl = shortioClidUrl + destinationUrl = destinationUrl ) } /** - * HandleClick() is used to track the click + * handleClick() is used to track the click * Parameters: It takes uriString of type String as parameter. * Returns String */ - suspend private fun HandleClick(uriString: String): String? = withContext(Dispatchers.IO) { - isSdkInitialized() - try { + suspend fun handleClick(uriString: String): String? { + return try { val urlString = when { uriString.contains("utm_medium=android", ignoreCase = true) -> uriString uriString.contains("?") -> "$uriString&utm_medium=android" else -> "$uriString?utm_medium=android" } - val client = OkHttpClient.Builder() - .followRedirects(false) - .build() + val url = URL(urlString) + val connection = withContext(Dispatchers.IO) { + url.openConnection() as HttpURLConnection + } - val request = Request.Builder() - .url(URL(urlString)) - .head() - .build() + connection.requestMethod = "HEAD" + connection.instanceFollowRedirects = false - client.newCall(request).execute().use { response -> + connection.connect() - response.header("Location") + println("Response Headers:") + for ((key, value) in connection.headerFields) { + if (key != null && value != null) { + println(" $key: ${value.joinToString()}") + } } + + val redirectedUrl = connection.getHeaderField("Location") + + connection.disconnect() + + redirectedUrl ?: "Not Found" + } catch (e: Exception) { println("Network error: ${e.localizedMessage}") null @@ -190,7 +291,6 @@ object ShortioSdk { * Returns: SecureResult which includes securedOriginalURL and securedShortUrl */ fun createSecure(originalURL: String): SecureResult { - isSdkInitialized() return try { val keyGenerator = KeyGenerator.getInstance("AES") @@ -232,7 +332,6 @@ object ShortioSdk { clid: String? = null, conversionId: String? = null ): Boolean = withContext(Dispatchers.IO) { - isSdkInitialized() try { val originalURLString = if (originalURL.isNullOrEmpty()){ savedDestinationUrl From fc3a3f7bd40e9f6543d1ce1b9a6c48b950553149 Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Mon, 18 Aug 2025 11:56:19 +0500 Subject: [PATCH 8/9] * updated and optimized the code --- README.md | 19 ++- .../java/com/github/shortiosdk/ShortIO.kt | 141 +++++------------- 2 files changed, 50 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index d1405df..05be3cb 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ It will be: ```kotlin dependencies { implementation("com.github.User:Repo:Tag") // Example - implementation("com.github.Short-io:android-sdk:v1.0.8") // Use this + implementation("com.github.Short-io:android-sdk:v1.0.9") // Use this } ``` ### 3. Sync the Project @@ -80,7 +80,7 @@ import com.github.shortiosdk.ShortioSdk ### πŸ”— SDK Usage -#### Initialization +#### Initialize the SDK To start using ShortioSdk, you need to initialize it early in your app lifecycle, preferably in your Activity's onCreate() method or in your custom Application class. @@ -121,7 +121,7 @@ try { thread { try { - when (val result = ShortioSdk.shortenUrl(params)) { + when (val result = ShortioSdk.createShortLink(params)) { is ShortIOResult.Success -> { Log.d("ShortIOResult","Shortened URL: ${result.data.shortURL}") } @@ -135,7 +135,7 @@ thread { } } ``` -**Note**: Deprecated: `shortenUrl`(apiKey, params) is still supported for backward compatibility but is no longer recommended for use. Use shortenUrl(params) instead. +**Note**: Deprecated: `createShortLink`(apiKey, params) is still supported for backward compatibility but is no longer recommended for use. Use `createShortLink(params)` instead. ## πŸ“„ API Parameters @@ -144,8 +144,8 @@ The `ShortIOParameters` struct is used to define the details of the short link y | Parameter | Type | Required | Description | | ------------------- | ----------- | -------- | ------------------------------------------------------------ | -| `domain` | `String` | βœ… | Your Short.io domain (e.g., `example.short.gy`) | -| `originalURL` | `String` | βœ… | The original URL to be shortened | +| `domain` | `String` | βœ… (Deprecated) | Your Short.io domain (e.g., `example.short.gy`). ⚠️ Deprecated. No longer required β€” inferred from API key. May be removed in future versions. | +| `originalURL` | `String` | βœ… | The original URL to be shortened | | `cloaking` | `Boolean` | ❌ | If `true`, hides the destination URL from the user | | `password` | `String` | ❌ | Password to protect the short link | | `redirectType` | `Int` | ❌ | Type of redirect (e.g., 301, 302) | @@ -320,11 +320,10 @@ Track conversions for your short links to measure campaign effectiveness. The SD CoroutineScope(Dispatchers.IO).launch { try { val res = ShortioSdk.trackConversion( - "https://{your_domain}/", - "your_clid" - conversionId = null + domain: "https://{your_domain}", // ⚠️ Deprecated (optional): + clid: "your_clid", // ⚠️ Deprecated (optional): + conversionId: "your_conversionID" (optional) ) - /// all parameters are optional // conversionId can be 'signup', 'purchase', 'download', etc. Log.d("Handle Conversion Tracking", "Handle Conversion Tracking: $res") } catch (e: Exception) { diff --git a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt index 9e17322..2de6bc3 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt @@ -13,7 +13,6 @@ import com.github.shortiosdk.Helpers.removeUtmParams import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.net.HttpURLConnection -import java.net.URI import java.net.URL import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -28,10 +27,9 @@ import kotlin.collections.joinToString object ShortioSdk { - var storedApiKey: String = "" - var savedDestinationUrl: String = "" - var savedDomain: String? = null - var storedClid: String = "" + var apiKey: String = "" + var domain: String = "" + var clid: String = "" var isInitialized: Boolean = false /** @@ -42,8 +40,8 @@ object ShortioSdk { */ fun initialize( apiKey: String, domain: String) { if (!isInitialized){ - this.storedApiKey = apiKey - this.savedDomain = domain + this.apiKey = apiKey + this.domain = domain println("SDK initialized with API key: $apiKey") isInitialized = true } else{ @@ -67,80 +65,18 @@ object ShortioSdk { * Parameters:- It takes ShortIOParameters as parameter which includes originalUrl, domain, clocking, password, title etc. It also apiKey. */ @Deprecated( - message = "Use shortenUrl(parameters) instead", - replaceWith = ReplaceWith("shortenUrl(parameters)"), + message = "Use createShortLink(parameters) instead", + replaceWith = ReplaceWith("createShortLink(parameters)"), level = DeprecationLevel.WARNING ) - fun shortenUrl( - deprecatedApiKey: String? = null, - parameters: ShortIOParameters + fun createShortLink( + parameters: ShortIOParameters, + apiKey: String? = null ): ShortIOResult { - val gson = GsonBuilder() - .registerTypeAdapter(StringOrInt::class.java, StringOrIntSerializer()) - .create() - if (parameters.domain.isNullOrBlank()){ - val errorModel = ShortIOErrorModel( - message = "Domain Not Provided", - statusCode = 400, - code = "MALFORMED_SUCCESS", - success = false - ) - return ShortIOResult.Error(errorModel) - } - var apiKey = if (!deprecatedApiKey.isNullOrBlank()){ - deprecatedApiKey - } else{ - storedApiKey - } - val client = OkHttpClient() - val mediaType = "application/json".toMediaType() - val jsonBody = gson.toJson(parameters) - val body = jsonBody.toRequestBody(mediaType) - - val request = Request.Builder() - .url(baseURL) - .post(body) - .addHeader("accept", "application/json") - .addHeader("content-type", "application/json") - .addHeader("authorization", apiKey) - .build() - - val response = client.newCall(request).execute() - val responseBody = response.body?.string() - - return if (response.isSuccessful) { - val model = responseBody?.let { gson.fromJson(it, ShortIOResponseModel::class.java) } - if (model != null) { - ShortIOResult.Success(model) - } else { - val errorModel = ShortIOErrorModel( - message = "Empty or malformed success response", - statusCode = response.code, - code = "MALFORMED_SUCCESS", - success = false - ) - ShortIOResult.Error(errorModel) - } - } else { - val errorModel = try { - responseBody?.let { - gson.fromJson(it, ShortIOErrorModel::class.java)?.copy(statusCode = response.code) - } ?: ShortIOErrorModel( - message = "Unknown error", - statusCode = response.code, - code = "UNKNOWN", - success = false - ) - } catch (e: Exception) { - ShortIOErrorModel( - message = "Malformed error response: ${e.localizedMessage}", - statusCode = response.code, - code = "INVALID_JSON", - success = false - ) - } - return ShortIOResult.Error(errorModel) - } + return performCreateShortLink( + parameters = parameters, + apiKey = apiKey.toString() + ) } /** @@ -148,15 +84,21 @@ object ShortioSdk { * Create ShortUrl by using shortenUrl(parameters: ShortIOParameters) Method * Parameters:- It takes ShortIOParameters as parameter which includes originalUrl, domain, clocking, password, title etc. */ - fun shortenUrl( + fun createShortLink( parameters: ShortIOParameters ): ShortIOResult { - isSdkInitialized() + return performCreateShortLink( + parameters = parameters, + apiKey = ShortioSdk.apiKey + ) + } + + private fun performCreateShortLink(parameters: ShortIOParameters, apiKey: String): ShortIOResult { val gson = GsonBuilder() .registerTypeAdapter(StringOrInt::class.java, StringOrIntSerializer()) .create() if (parameters.domain.isNullOrBlank()) { - parameters.domain = savedDomain!! + parameters.domain = domain } val client = OkHttpClient() @@ -169,7 +111,7 @@ object ShortioSdk { .post(body) .addHeader("accept", "application/json") .addHeader("content-type", "application/json") - .addHeader("authorization", storedApiKey) + .addHeader("authorization", apiKey) .build() val response = client.newCall(request).execute() @@ -225,12 +167,9 @@ object ShortioSdk { val shortioClidUrl = withContext(Dispatchers.IO) { handleClick(uri.toString()) } - storedClid = shortioClidUrl.let { it?.let { urlString -> extractClidFromUrl(urlString) } ?: "" } + clid = shortioClidUrl.let { it?.let { urlString -> extractClidFromUrl(urlString) } ?: "" } val destinationUrl = shortioClidUrl?.let { removeUtmParams(it) } - if (destinationUrl != null) { - savedDestinationUrl = destinationUrl - } return UrlComponents( scheme = scheme, @@ -328,36 +267,38 @@ object ShortioSdk { * Returns: Result Boolean based on status code */ suspend fun trackConversion( - originalURL: String? = null, clid: String? = null, + domain: String? = null, conversionId: String? = null ): Boolean = withContext(Dispatchers.IO) { try { - val originalURLString = if (originalURL.isNullOrEmpty()){ - savedDestinationUrl - } else { - originalURL - } - val originalUri = URI(originalURLString) - val scheme = originalUri.scheme ?: return@withContext false - val host = originalUri.host ?: return@withContext false // Build query params val queryParams = mutableListOf>() if (!conversionId.isNullOrEmpty()) { queryParams.add("c" to conversionId) } - if (!clid.isNullOrEmpty()) { - queryParams.add("clid" to clid) - } else{ - queryParams.add("clid" to storedClid) + var finalDomain = if (!domain.isNullOrEmpty()) { + domain + } else { + ShortioSdk.domain + } + + var finalClid = if (!clid.isNullOrEmpty()) { + clid + } else { + ShortioSdk.clid + } + + if (!finalClid.isNullOrEmpty()) { + queryParams.add("clid" to finalClid) } val queryString = queryParams.joinToString("&") { "${it.first}=${URLEncoder.encode(it.second, "UTF-8")}" } - val finalUrl = "$scheme://$host/.shortio/conversion?$queryString" + val finalUrl = "https://$finalDomain/.shortio/conversion?$queryString" val client = OkHttpClient() From 3ddc276c21c254a849fd7674fe64c792e4beb8ec Mon Sep 17 00:00:00 2001 From: rajpootathar Date: Mon, 25 Aug 2025 16:11:33 +0500 Subject: [PATCH 9/9] * updated the docs about getting the original url --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 05be3cb..8377d77 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,8 @@ keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -sto ### 🧭 Step 5: Handle Incoming URLs with onNewIntent() Method +To retrieve the original URL from Short.io links in your Android app, you can handle incoming intents in onNewIntent(), which allows your activity to process links that are opened while it is already running. + 1. Open your main activity file (e.g., MainActivity.kt). 2. Override the `onNewIntent()` method to receive new intents when the activity is already running: @@ -283,19 +285,31 @@ override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) lifecycleScope.launch { val result = ShortioSdk.handleIntent(intent) - Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}, DestinationURL: ${result?.destinationUrl}") + // Access the original URL + val originalUrl = result?.destinationUrl + Log.d("New Intent", + "Host: ${result?.host}, + Path: ${result?.path}, + Original URL: $originalUrl" + ) } } ``` -3. In the same activity, also handle the initial intent inside the `onCreate()` method: +3. In the same activity, To handle the initial intent inside the `onCreate()` method: ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { val result = ShortioSdk.handleIntent(intent) - Log.d("New Intent", "Host: ${result?.host}, Path: ${result?.path}, DestinationURL: ${result?.destinationUrl}") + // Access the original URL + val originalUrl = result?.destinationUrl + Log.d("New Intent", + "Host: ${result?.host}, + Path: ${result?.path}, + Original URL: $originalUrl" + ) } } ```