diff --git a/README.md b/README.md index e43a543..8377d77 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.9") // Use this } ``` ### 3. Sync the Project @@ -80,6 +80,27 @@ import com.github.shortiosdk.ShortioSdk ### πŸ”— SDK Usage +#### 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. + +Example: Initialize in Activity + +```kotlin +override fun onCreate() { + super.onCreate() + ShortioSdk.initialize(apiKey, domain) +} +``` +* apiKey: Your API key string for SDK initialization. +* domain: The default domain to use for URL shortening. + +### πŸ’‘ How It Works + +The app demonstrates: + +#### βœ… Generating Short Links + ```kotlin import com.github.shortiosdk.ShortioSdk import com.github.shortiosdk.ShortIOParameters @@ -87,7 +108,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) { @@ -95,20 +115,19 @@ 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 -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.createShortLink(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) { @@ -116,6 +135,8 @@ thread { } } ``` +**Note**: Deprecated: `createShortLink`(apiKey, params) is still supported for backward compatibility but is no longer recommended for use. Use `createShortLink(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: @@ -123,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) | @@ -241,7 +262,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 @@ -253,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: @@ -260,23 +283,70 @@ 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) + // 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}") + // Access the original URL + val originalUrl = result?.destinationUrl + Log.d("New Intent", + "Host: ${result?.host}, + Path: ${result?.path}, + Original URL: $originalUrl" + ) + } } ``` +### πŸ” 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}") +``` + +### πŸ”„ Conversion Tracking + +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( + domain: "https://{your_domain}", // ⚠️ Deprecated (optional): + clid: "your_clid", // ⚠️ Deprecated (optional): + conversionId: "your_conversionID" (optional) + ) + // 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 +### βœ… Final Checklist for Deep Linking * App is signed with the correct keystore. 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/Helpers/HelperMethods.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt index eda18fe..b32a4b6 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/Helpers/HelperMethods.kt @@ -1,32 +1,29 @@ package com.github.shortiosdk.Helpers -import okhttp3.OkHttpClient -import okhttp3.Request +import android.net.Uri -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" +fun extractClidFromUrl(urlString: String): String? { + return try { + val uri = Uri.parse(urlString) + uri.getQueryParameter("clid") + } catch (e: Exception) { + e.printStackTrace() + null } +} - val request = Request.Builder() - .url(url) - .addHeader("accept", "application/json") - .build() +fun removeUtmParams(url: String): String { + val uri = Uri.parse(url) + val builder = uri.buildUpon().clearQuery() - return try { - client.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.code.toString() - } else { - "Link is not Valid" + uri.queryParameterNames + .filter { !it.startsWith("utm_", ignoreCase = true) } + .forEach { key -> + uri.getQueryParameters(key)?.forEach { value -> + builder.appendQueryParameter(key, value) } } - } catch (e: Exception) { - e.toString() - } + + return builder.build().toString() } \ 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..e34b3dd 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, @@ -30,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/Model/ShortIOResult.kt b/ShortIOSDK/src/main/java/com/github/shortiosdk/Model/ShortIOResult.kt index ebe9574..51aff88 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,16 @@ 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() } + +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/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 85f7eba..2de6bc3 100644 --- a/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt +++ b/ShortIOSDK/src/main/java/com/github/shortiosdk/ShortIO.kt @@ -5,27 +5,109 @@ 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 +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.URL +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +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 { - fun shortenUrl( - apiKey: String, + var apiKey: String = "" + var domain: String = "" + var clid: 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 + this.domain = domain + println("SDK initialized with API key: $apiKey") + isInitialized = true + } else{ + Log.d("SDK Initialization","SDK is already Initialzed") + } + } + + /** + * Use this method to check the SDK is already initialized + */ + fun isSdkInitialized(): Boolean { + if (!isInitialized) { + throw IllegalStateException("SDK is not initialized. Please initialize the SDk before using it.") + } + return true + } + + /** + * 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 createShortLink(parameters) instead", + replaceWith = ReplaceWith("createShortLink(parameters)"), + level = DeprecationLevel.WARNING + ) + fun createShortLink( + parameters: ShortIOParameters, + apiKey: String? = null + ): ShortIOResult { + return performCreateShortLink( + parameters = parameters, + apiKey = apiKey.toString() + ) + } + + /** + * 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 createShortLink( parameters: ShortIOParameters ): ShortIOResult { + 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 = domain + } + val client = OkHttpClient() val mediaType = "application/json".toMediaType() val jsonBody = gson.toJson(parameters) val body = jsonBody.toRequestBody(mediaType) val request = Request.Builder() - .url(shortenUrl) + .url(baseURL) .post(body) .addHeader("accept", "application/json") .addHeader("content-type", "application/json") @@ -69,34 +151,168 @@ object ShortioSdk { return ShortIOResult.Error(errorModel) } } - - fun handleIntent(intent: Intent): UrlComponents? { + + /** + * 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() 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("Response", "Response: $response") + val shortioClidUrl = withContext(Dispatchers.IO) { + handleClick(uri.toString()) } - thread.start() - thread.join() + clid = shortioClidUrl.let { it?.let { urlString -> extractClidFromUrl(urlString) } ?: "" } + + val destinationUrl = shortioClidUrl?.let { removeUtmParams(it) } - if (response == "200") { - Log.d("Success","Response:-${response}") - } else { - Log.d("Error","Error:- ${response}") - } return UrlComponents( scheme = scheme, host = host, path = uri.path?.removePrefix("/"), query = uri.encodedQuery, fragment = uri.fragment, - fullUrl = uri.toString() + fullUrl = uri.toString(), + destinationUrl = destinationUrl ) } + + /** + * handleClick() is used to track the click + * Parameters: It takes uriString of type String as parameter. + * Returns String + */ + 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 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 + } + } + + /** + * It creates a Secure URL + * parameters: originalURL: String + * Returns: SecureResult which includes securedOriginalURL and securedShortUrl + */ + 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 + } + } + + /** + * 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( + clid: String? = null, + domain: String? = null, + conversionId: String? = null + ): Boolean = withContext(Dispatchers.IO) { + try { + + // Build query params + val queryParams = mutableListOf>() + if (!conversionId.isNullOrEmpty()) { + queryParams.add("c" to conversionId) + } + 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 = "https://$finalDomain/.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 + } + } }