diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..915dae6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,3 @@ +Licensed under the Zscaler End User Subscription Agreement available at https://www.zscaler.com/legal or as otherwise indicated on a written order form, purchase order, or similar ordering document for Zscaler SaaS, Software, Hardware, Deployment Services, and Support Services, including all Upgrades. + +Copyright 2024 Zscaler Inc. All rights reserved. diff --git a/README.md b/README.md index e69de29..ec17c2e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,26 @@ + +# Zscaler Android SDK + +Zscaler Development Kit (Zscaler SDK), part of the Zscaler Zero Trust Exchangeâ„¢ platform, combines a set of robust capabilities to protect the integrity of your intellectual property, secure network communications from your mobile application, and prevent breaches against your APIs and core backend services. + + +## Integration +1. Add the Github repository to the dependencyResolutionManagement repositories in settings.gradle. +``` +dependencyResolutionManagement { + repositories { + maven { + name = "ZscalerSDKAndroid" + url = uri("https://maven.pkg.github.com/zscaler/zscaler-sdk-android") + } + } +} +``` + +2. Install Zscaler SDK in the dependencies section of build.gradle. +``` +dependencies { + implementation("com.zscaler.sdk:zscalersdk-android:latest.release") +} +``` + diff --git a/sample-app/app/build.gradle.kts b/sample-app/app/build.gradle.kts new file mode 100644 index 0000000..4414a70 --- /dev/null +++ b/sample-app/app/build.gradle.kts @@ -0,0 +1,181 @@ +import com.android.build.gradle.internal.tasks.MergeNativeLibsTask +import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension +import com.github.triplet.gradle.androidpublisher.ReleaseStatus +import com.github.triplet.gradle.androidpublisher.ResolutionStrategy +import com.google.firebase.crashlytics.buildtools.gradle.tasks.GenerateSymbolFileTask + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("maven-publish") + id("com.github.triplet.play") version "3.9.1" + id("com.google.gms.google-services") + id("com.google.firebase.crashlytics") +} + +var versionNameVal = "x.x-dev" +var versionCodeVal = 1 + +if (project.hasProperty("buildVersionName")) { + versionNameVal = project.ext.get("buildVersionName").toString() + rootProject.version = versionNameVal + // calculate version code from version name + val parts = versionNameVal.split("-")[0].split(".") // handles SNAPSHOT also + val versionCodeInt = parts[0].toInt() * 10000 + parts[1].toInt() * 100 + (parts.getOrNull(2)?.toInt() ?: 0) // handles 0...99 for each major,minor,patch/build + if (versionCodeInt > versionCodeVal) { + versionCodeVal = versionCodeInt + } + println("versionNameVal " + versionNameVal) + println("versionCodeVal " + versionCodeVal) +} + +android { + namespace = "com.zscaler.sdk.demoapp" + compileSdk = 34 + + defaultConfig { + applicationId = "com.zscaler.sdk.android.testapp" + minSdk = 26 + targetSdk = 34 + versionCode = versionCodeVal + versionName = versionNameVal + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + signingConfigs { + register("release") { + if (System.getenv("KEYSTORE_FILENAME") != null && file(System.getenv("KEYSTORE_FILENAME")).exists()) { + storeFile = file(System.getenv("KEYSTORE_FILENAME")) + storePassword = System.getenv("KEYSTORE_PASSWORD") + keyAlias = "key0" //System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEYSTORE_PASSWORD") // System.getenv("KEY_PASSWORD") + // Enable Signing Versions + enableV1Signing = true + enableV2Signing = true + enableV3Signing = true + enableV4Signing = true + } + } + } + + buildTypes { + getByName("release") { + isDebuggable = false + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("release") + // Add this extension + configure { + nativeSymbolUploadEnabled = true + unstrippedNativeLibsDir = file("../../../zdklibrary/app/build/intermediates/merged_native_libs/") + } + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + buildConfig = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + ndkVersion = "25.2.9519653" + buildToolsVersion = "34.0.0" + + publishing { + singleVariant("release") { + publishApk() + } + } + + if (System.getenv("API_PRIVATE_KEY_FILENAME") != null) { + play { + val keyfile: String = + System.getenv("API_PRIVATE_KEY_FILENAME") ?: "Missing API_PRIVATE_KEY_FILENAME" + serviceAccountCredentials.set(file(keyfile)) + track.set("internal") + releaseStatus.set(ReleaseStatus.DRAFT) + resolutionStrategy.set(ResolutionStrategy.AUTO) + } + } +} + +afterEvaluate { + // This provides a workaround for https://github.com/firebase/firebase-android-sdk/issues/5629 + tasks.withType().configureEach { + mustRunAfter(tasks.withType()) + } +} + +publishing { + publications { + register("release") { + groupId = (System.getenv("ARTIFACT_GROUP_ID") ?: "com.zscaler.sdk") + ".zscalersdk-android" + artifactId = "testapp" + version = versionNameVal + pom { + packaging = "apk" + } + afterEvaluate { + from(components["release"]) + } + } + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.3.1") + implementation("com.google.android.material:material:1.3.0") + implementation("androidx.activity:activity:1.2.4") + implementation("androidx.constraintlayout:constraintlayout:2.0.4") + implementation("androidx.core:core-ktx:1.6.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") + implementation("androidx.lifecycle:lifecycle-process:2.3.1") + implementation("com.google.code.gson:gson:2.8.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0") + implementation ("androidx.compose.runtime:runtime-livedata:1.1.1") + + // ViewModel + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") + implementation("androidx.compose.runtime:runtime-livedata:1.0.0") + // ViewModel utilities for Compose + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0") + implementation("androidx.webkit:webkit:1.4.0") + + //retrofit + implementation ("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + // zdk dependency + implementation("com.zscaler.sdk:zscalersdk-android:latest.release") + + implementation("androidx.security:security-crypto:1.1.0-alpha06") + + implementation(platform("com.google.firebase:firebase-bom:31.1.0")) + implementation("com.google.firebase:firebase-analytics") + implementation("com.google.firebase:firebase-crashlytics") + implementation("com.google.firebase:firebase-crashlytics-ktx") + implementation("com.google.firebase:firebase-crashlytics-ndk") + + // test dependencies + testImplementation("junit:junit:4.13.2") + testImplementation("org.mockito:mockito-core:5.1.0") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + +} diff --git a/sample-app/app/google-services.json b/sample-app/app/google-services.json new file mode 100644 index 0000000..9d0423d --- /dev/null +++ b/sample-app/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "", + "project_id": "", + "storage_bucket": "" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "", + "android_client_info": { + "package_name": "" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "" +} \ No newline at end of file diff --git a/sample-app/app/proguard-rules.pro b/sample-app/app/proguard-rules.pro new file mode 100644 index 0000000..99db228 --- /dev/null +++ b/sample-app/app/proguard-rules.pro @@ -0,0 +1,33 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +-renamesourcefileattribute SourceFile + +-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue +-dontwarn com.google.errorprone.annotations.CheckReturnValue +-dontwarn com.google.errorprone.annotations.Immutable +-dontwarn com.google.errorprone.annotations.RestrictedApi + +-keep public class com.zscaler.sdk.android.ZscalerSDK { + @kotlin.jvm.JvmStatic ; +} +-keep public class com.zscaler.sdk.android.ZscalerSDK$* { + public ; +} \ No newline at end of file diff --git a/sample-app/app/src/main/AndroidManifest.xml b/sample-app/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c0ab1bf --- /dev/null +++ b/sample-app/app/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample-app/app/src/main/ic_launcher-playstore.png b/sample-app/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..2cc704a Binary files /dev/null and b/sample-app/app/src/main/ic_launcher-playstore.png differ diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/AppLifecycleObserver.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/AppLifecycleObserver.kt new file mode 100644 index 0000000..f31814e --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/AppLifecycleObserver.kt @@ -0,0 +1,21 @@ +package com.zscaler.sdk.demoapp + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import com.zscaler.sdk.android.ZscalerSDK + +class AppLifecycleObserver : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun onAppForeground() { + // Call this as soon as app comes to foreground or network requests are going to be resumed, whichever is earlier. + ZscalerSDK.resume() + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onAppBackground() { + // Call this when app goes to background or when ongoing network requests are finished, whichever is later. + ZscalerSDK.suspend() + } + +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainActivity.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainActivity.kt new file mode 100644 index 0000000..9f824ad --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainActivity.kt @@ -0,0 +1,610 @@ +package com.zscaler.sdk.demoapp + +import android.Manifest +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.os.StatFs +import android.provider.MediaStore +import android.util.Base64 +import android.util.Log +import android.view.View +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.AdapterView +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.NotificationCompat +import androidx.core.content.FileProvider +import androidx.core.text.HtmlCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.ktx.Firebase +import com.google.firebase.ktx.initialize +import com.zscaler.sdk.android.ZscalerSDK +import com.zscaler.sdk.android.exception.ZscalerSDKException +import com.zscaler.sdk.android.networking.ZscalerSDKRetrofit +import com.zscaler.sdk.android.notification.ZscalerSDKNotificationEnum +import com.zscaler.sdk.demoapp.databinding.ActivityMainBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.text.SimpleDateFormat +import java.util.Date + +class MainActivity : AppCompatActivity() { + + private val TAG = "MainActivity" + private lateinit var binding: ActivityMainBinding + private lateinit var logFileName: String + private lateinit var mainViewModel: MainViewModel + private var isRetrofitClientNeeded: Boolean = false + private var httpMethod: HttpMethod = HttpMethod.WEB + private lateinit var broadcastReceiver: BroadcastReceiver + private lateinit var notificationManager: NotificationManager + private val requestNotificationCode = 101 + private val WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 1001 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initCrashlytics() + binding = ActivityMainBinding.inflate(layoutInflater) + try { + ZscalerSDK.init(application, ZscalerSDKSetting.defaultZscalerSDKConfiguration()) + } catch (e: ZscalerSDKException) { + Log.e(TAG, "Got exception while initializing ZscalerSDK = $e") + // App won't work after this + return + } + val viewModelProvider = ViewModelProvider(this) + mainViewModel = viewModelProvider[MainViewModel::class.java] + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + binding.preLoginToggle.contentDescription = "OFF" + binding.zeroTrustToggle.contentDescription = "OFF" + addLoggerFunction() + configureTunnelToggleButtons() + enableBrowsing() + spinnerListener() + createNotificationChannel() + initZdkConfiguration() + } + + private fun initZdkConfiguration() { + binding.llZscalerConfig.setOnClickListener { startActivity(Intent(this@MainActivity, SettingActivity::class.java)) } + } + + /** + * Enabling Firebase crashlytics only build variant other than debug. + */ + private fun initCrashlytics() { + Firebase.initialize(this) + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG) + } + + private fun registerZDKReceiver() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + requestNotificationPermission() + } + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val notificationCode = intent?.getIntExtra(ZscalerSDK.NOTIFICATION_CODE, -1) + val notificationMessage = intent?.getStringExtra(ZscalerSDK.NOTIFICATION_MESSAGE) + Log.d(TAG, "onReceive() called with: notificationCode = $notificationCode, notificationMessage = $notificationMessage") + notificationCode?.takeIf() {it > -1 + createNotification(ZscalerSDKNotificationEnum.values()[notificationCode].message, ZscalerSDKNotificationEnum.values()[notificationCode].message) + } + } + } + val filter = IntentFilter(ZscalerSDK.ZSCALER_RECEIVER_ID) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(broadcastReceiver, filter, RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(broadcastReceiver, filter) + } + } + + private fun spinnerListener() { + binding.httpMethodButton.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parentView: AdapterView<*>?, + selectedItemView: View?, + position: Int, + id: Long + ) { + isRetrofitClientNeeded = when (position) { + 1, 2 -> { + httpMethod = HttpMethod.entries.toTypedArray()[position.minus(1)] + true + } + + else -> { + httpMethod = HttpMethod.WEB + false + } + } + } + + override fun onNothingSelected(parentView: AdapterView<*>?) { + // Do nothing here + } + } + } + + override fun onDestroy() { + Log.d(TAG, "onDestroy() called") + if (mainViewModel.getSelectedTunnel() == ZDKTunnel.PRE_LOGIN) { + mainViewModel.stopTunnel(resetStatusText = { getString(R.string.status_off) }) + } else if (mainViewModel.getSelectedTunnel() == ZDKTunnel.ZERO_TRUST) { + mainViewModel.stopTunnel(resetStatusText = { getString(R.string.status_off) }) + } + try { + unregisterReceiver(broadcastReceiver) + } catch (e: RuntimeException) { + Log.e(TAG, "onDestroy() :: exception raised while unregistering broadcastReceiver : ", e) + } + super.onDestroy() + } + + @SuppressLint("SetJavaScriptEnabled") + private fun enableBrowsing() { + binding.webview.settings.javaScriptEnabled = true + addMessageOnWebView(zpaNotConnectedHtml) + binding.goButton.setOnClickListener { + var url = binding.browserUrlTextField.text.toString() + val urlRegex = + "^((http|https)://)?(www\\.)?([a-zA-Z0-9\\-\\.]+\\.)+[a-zA-Z]{2,}(/?.*)?\$" + if (url.isBlank() || !url.matches(urlRegex.toRegex())) { + ZdkDialog.showMessageDialog(this, getString(R.string.enter_valid_url)) + } else { + url = addHttpsIfNeeded(url) + when (httpMethod) { + HttpMethod.GET -> + if (ZscalerSDKSetting.getZscalerSDKConfiguration().automaticallyConfigureRequests) { + mainViewModel.getData(url = url.ensureEndsWithSlash()) + } else { + mainViewModel.loadManuallyWithProxyInfo(url.ensureEndsWithSlash(), true) + } + HttpMethod.POST -> + if (ZscalerSDKSetting.getZscalerSDKConfiguration().automaticallyConfigureRequests) { + mainViewModel.postData(url = url.ensureEndsWithSlash(), params = emptyMap()) + } else { + mainViewModel.loadManuallyWithProxyInfo(url.ensureEndsWithSlash(), false) + } + HttpMethod.WEB -> loadUrlInWebView(url = url) + } + } + } + mainViewModel.responseData.observe(this) { responseData -> + binding.webview.loadUrl("about:blank") + binding.webview.clearCache(true) + if (httpMethod != HttpMethod.WEB) { + binding.webview.visibility = View.GONE + binding.tvResponse.visibility = View.VISIBLE + if (responseData != null) { + if (responseData.toString() + .startsWith("") || responseData.toString() + .startsWith("") + ) { + binding.tvResponse.text = HtmlCompat.fromHtml(responseData, 0) + } else { + binding.tvResponse.text = responseData + } + Log.d(TAG, "API data: responseData = $responseData") + } else { + binding.tvResponse.text = getString(R.string.error_loading_data) + Log.d(TAG, "API response error") + } + } + } + } + + private fun loadUrlInWebView(url: String) { + binding.tvResponse.visibility = View.GONE + binding.webview.visibility = View.VISIBLE + var formattedUrl = url.trim() + if (!formattedUrl.startsWith("http://") + && !formattedUrl.startsWith("https://") + && !formattedUrl.startsWith("www.") + ) { + formattedUrl = "https://$formattedUrl" + } + binding.webview.clearCache(true) + binding.webview.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + binding.webview.contentDescription = "Page is loading..." + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + binding.webview.contentDescription = "Page loaded successfully" + } + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + super.onReceivedError(view, request, error) + addMessageOnWebView(zpaErrorHtml) + binding.webview.contentDescription = "Failed to load the page" + } + } + binding.webview.loadUrl(formattedUrl) + } + + private fun addMessageOnWebView(htmlText: String) { + if (mainViewModel.getSelectedTunnel() == ZDKTunnel.NO_SELECTION) { + val encodedHtml = Base64.encodeToString(htmlText.toByteArray(), Base64.NO_PADDING) + binding.webview.loadData(encodedHtml, "text/html", "base64") + } + } + + @SuppressLint("SetTextI18n") + private fun configureTunnelToggleButtons() { + binding.preLoginToggle.setOnCheckedChangeListener { buttonView, isChecked -> + reloadWebView() + if (isChecked) { + // everytime clear any previously manually created retrofit when tunnel status changes. + ManualRetrofitApiClient.clearRetroFitInstance() + ZscalerSDKRetrofit.clearInstance() + registerZDKReceiver() + binding.tvPreStatus.text = getString(R.string.status_format, "CONNECTING") + + binding.tvZeroStatus.visibility = View.INVISIBLE + + val appKey = binding.zdkIdTextField.text.toString() + if (appKey.isBlank()) { + binding.zdkIdTextField.error = getString(R.string.zdk_id_is_empty) + binding.preLoginToggle.isChecked = false + binding.preLoginToggle.contentDescription = "OFF" + return@setOnCheckedChangeListener + + } else { + binding.tvPreStatus.visibility = View.VISIBLE + binding.zdkIdTextField.error = null + mainViewModel.startPreLoginTunnel(appKey = appKey, + udid = mainViewModel.getUdid(getString(R.string.random_udid)), + onErrorOccurred = { + binding.preLoginToggle.isChecked = false + binding.preLoginToggle.contentDescription = "OFF" + binding.tvPreStatus.visibility = View.VISIBLE + binding.tvPreStatus.text = + getString(R.string.error_status_format, it.toString()) + binding.zeroTrustToggle.isEnabled = true + }) + binding.zeroTrustToggle.isEnabled = false + binding.preLoginToggle.contentDescription = "ON" + binding.zeroTrustToggle.contentDescription = "OFF" + mainViewModel.startTunnelStatusUpdates() + binding.tvPreStatus.text = + getString(R.string.status_format, mainViewModel.getStatus()) + binding.tvZeroStatus.text = getString(R.string.status_format, "OFF") + mainViewModel.zdkStatus.observe(this) { + binding.tvPreStatus.text = getString(R.string.status_format, it) + } + addMessageOnWebView(zpaEmptyHtml) + } + } else { + unregisterReceiver(broadcastReceiver) + stopService(Intent(this, NotificationCancellationService::class.java)) + notificationManager.cancel(NOTIFICATION_ID) + Log.d( + TAG, + mainViewModel.stopTunnel(resetStatusText = { getString(R.string.status_off) }) + .toString() + ) + mainViewModel.stopTunnelStatusUpdates() + + binding.zeroTrustToggle.isEnabled = true + binding.zeroTrustToggle.contentDescription = "OFF" + binding.preLoginToggle.contentDescription = "OFF" + binding.tvPreStatus.text = getString(R.string.status_format, "OFF") + binding.tvPreStatus.visibility = View.INVISIBLE + addMessageOnWebView(zpaNotConnectedHtml) + } + } + + binding.zeroTrustToggle.setOnCheckedChangeListener { buttonView, isChecked -> + reloadWebView() + if (isChecked) { + // everytime clear any previously created retrofit when tunnel status changes. + ManualRetrofitApiClient.clearRetroFitInstance() + ZscalerSDKRetrofit.clearInstance() + registerZDKReceiver() + binding.tvZeroStatus.text = getString(R.string.status_format, "CONNECTING") + binding.tvPreStatus.visibility = View.INVISIBLE + + val appKey = binding.zdkIdTextField.text.toString() + val accessToke = binding.accessTokenTextField.text.toString() + if (appKey.isBlank()) { + binding.zdkIdTextField.error = getString(R.string.zdk_id_is_empty) + binding.zeroTrustToggle.isChecked = false + binding.zeroTrustToggle.contentDescription = "OFF" + return@setOnCheckedChangeListener + } else if (accessToke.isBlank()) { + binding.accessTokenTextField.error = getString(R.string.access_token_is_empty) + binding.zeroTrustToggle.isChecked = false + binding.zeroTrustToggle.contentDescription = "OFF" + return@setOnCheckedChangeListener + } else { + binding.tvZeroStatus.visibility = View.VISIBLE + binding.zdkIdTextField.error = null + binding.accessTokenTextField.error = null + mainViewModel.startZeroTrustTunnel( + appKey = appKey, + udid = mainViewModel.getUdid(getString(R.string.random_udid)), + accessToken = accessToke, + onErrorOccurred = { + binding.zeroTrustToggle.isChecked = false + binding.zeroTrustToggle.contentDescription = "OFF" + binding.tvZeroStatus.visibility = View.VISIBLE + binding.tvZeroStatus.text = + getString(R.string.error_status_format, it.toString()) + }) + binding.preLoginToggle.isEnabled = false + binding.zeroTrustToggle.contentDescription = "ON" + binding.preLoginToggle.contentDescription = "OFF" + mainViewModel.startTunnelStatusUpdates() + binding.tvZeroStatus.text = + getString(R.string.status_format, mainViewModel.getStatus()) + binding.tvPreStatus.text = getString(R.string.status_format, "OFF") + mainViewModel.zdkStatus.observe(this) { + binding.tvZeroStatus.text = getString(R.string.status_format, it) + } + addMessageOnWebView(zpaEmptyHtml) + } + } else { + unregisterReceiver(broadcastReceiver) + stopService(Intent(this, NotificationCancellationService::class.java)) + notificationManager.cancel(NOTIFICATION_ID) + mainViewModel.stopTunnel(resetStatusText = { getString(R.string.status_off) }) + addMessageOnWebView(zpaNotConnectedHtml) + binding.zeroTrustToggle.contentDescription = "OFF" + binding.preLoginToggle.contentDescription = "OFF" + binding.preLoginToggle.isEnabled = true + mainViewModel.stopTunnelStatusUpdates() + binding.tvZeroStatus.text = getString(R.string.status_format, "OFF") + binding.tvZeroStatus.visibility = View.INVISIBLE + } + } + } + + private fun reloadWebView() { + var url1: String = binding.webview.url.toString() + if (!(url1.isNullOrEmpty() || url1.equals("null"))) { + Toast.makeText(this, getString(R.string.reloading_web_page), Toast.LENGTH_SHORT).show() + binding.webview.loadUrl("about:blank") + binding.webview.clearCache(true) //required + binding.webview.loadUrl(url1) + } + } + + private fun addLoggerFunction() { + binding.exportLogsButton.setOnClickListener { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + createZipFileAndShare() + } else { + requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), WRITE_EXTERNAL_STORAGE_REQUEST_CODE) + } + } else { + createZipFileAndShare() + } + } + + binding.clearLogsButton.setOnClickListener { + mainViewModel.clearLogs(onSuccess = { + Toast.makeText(this, getString(R.string.zdk_log_cleared), Toast.LENGTH_LONG).show() + }) + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST_CODE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + createZipFileAndShare() + } else { + Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show() + } + } else if (requestCode == requestNotificationCode && grantResults.isNotEmpty() && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // TODO: add notification if permission granted + } + } + + private fun createZipFileAndShare() { + val currentDateTime = SimpleDateFormat("yyyy-MM-dd-hh-mm-ss.SS").format(Date()) + logFileName = "ZscalerSDK-$currentDateTime.zip" + ZipUtility.createEmptyZipFile(this, logFileName) + ?.let { + launchShareIntentLogZip(it) + } + } + + private fun launchShareIntentLogZip(it: String) { + Toast.makeText(this, getString(R.string.saved_to_download), Toast.LENGTH_LONG).show() + lifecycleScope.launch(Dispatchers.IO) { + mainViewModel.exportLog(it) + val logZipFile = File(this@MainActivity.filesDir, logFileName) + val downloadFolder = this@MainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + if (downloadFolder != null) { + val stat = StatFs(downloadFolder.path) + val bytesAvailable = stat.blockSizeLong * stat.availableBlocksLong + if (bytesAvailable > logZipFile.length()) { + val fileUri = FileProvider.getUriForFile( + this@MainActivity, + this@MainActivity.packageName + ".file-provider", + logZipFile + ) + saveFileUsingMediaStore(this@MainActivity, logZipFile) + withContext(Dispatchers.Main) { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.type = "application/zip" + shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri) + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivity(Intent.createChooser(shareIntent, getString(R.string.share_zdk_log_zip_file))) + } + } else { + withContext(Dispatchers.Main) { + Toast.makeText(this@MainActivity, getString(R.string.not_enough_space), Toast.LENGTH_LONG).show() + } + } + } else { + withContext(Dispatchers.Main) { + Toast.makeText(this@MainActivity, getString(R.string.download_folder_not_found), Toast.LENGTH_LONG).show() + } + } + } + + + } + + private fun saveFileUsingMediaStore(context: Context, logZipFile: File) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + saveLogToDownloadAboveAndEqualQ(context, logZipFile) + } else { + saveLogToDownloadBelowQ(context, logFileName) + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun saveLogToDownloadAboveAndEqualQ(context: Context, logZipFile: File) { + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, logZipFile.name) + put(MediaStore.MediaColumns.MIME_TYPE, "application/zip") + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + + val resolver = context.contentResolver + var outputStream: OutputStream? = null + + try { + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + if (uri != null) { + outputStream = resolver.openOutputStream(uri) + logZipFile.inputStream().use { inputStream -> + inputStream.copyTo(outputStream!!) + } + Log.d(TAG, "saveLogToDownloadAboveAndEqualQ :: saveLogToDownloadAboveAndEqualQ: File saved successfully") + } else { + Log.d(TAG, "saveLogToDownloadAboveAndEqualQ :: Failed to save file") + } + } catch (e: IOException) { + e.printStackTrace() + Log.e(TAG, "saveLogToDownloadAboveAndEqualQ :: exception raised", e) + } finally { + outputStream?.close() + } + } + + private fun saveLogToDownloadBelowQ(context: Context, zipFileName: String): Boolean { + val sourceFile = File(context.filesDir, zipFileName) + + if (!sourceFile.exists()) { + Log.e(TAG, "saveLogToDownloadBelowQ :: Source file does not exist: ${sourceFile.absolutePath}") + return false + } + val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + + if (!downloadsDir.mkdirs() && !downloadsDir.canWrite()) { + Log.e(TAG, "saveLogToDownloadBelowQ :: Could not create or write to Downloads directory: ${downloadsDir.absolutePath}") + return false + } + val destinationFile = File(downloadsDir, zipFileName) + + try { + val inStream = FileInputStream(sourceFile) + val outStream = FileOutputStream(destinationFile) + + val buffer = ByteArray(1024) + var readBytes: Int + while (inStream.read(buffer).also { readBytes = it } != -1) { + outStream.write(buffer, 0, readBytes) + } + + inStream.close() + outStream.close() + Log.i(TAG, "saveLogToDownloadBelowQ :: File copied successfully.") + return true + } catch (e: IOException) { + Log.e(TAG, "saveLogToDownloadBelowQ :: Error copying file: ${e.message}") + return false + } + } + + fun addHttpsIfNeeded(url: String): String { + return if (url.startsWith("https://")) { + url + } else { + "https://$url" + } + } + + private fun createNotificationChannel() { + val name = "ZDK Notification" + val descriptionText = "Tunnel status from the ZDK lib" + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance).apply { + description = descriptionText + } + val notificationManager: NotificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + private fun createNotification(title: String, message: String): Boolean { + + val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setContentTitle(title) + .setContentText(message) + .setSmallIcon(R.drawable.ic_notification) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setContentIntent(pendingIntentForActivity()) + + startForegroundService(Intent(this, NotificationCancellationService::class.java)) + notificationManager.notify(NOTIFICATION_ID, builder.build()) + return true + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun requestNotificationPermission() { + requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), requestNotificationCode) + } + + private fun pendingIntentForActivity(): PendingIntent { + val intent = Intent(this, MainActivity::class.java) + return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) + } +} diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainApplication.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainApplication.kt new file mode 100644 index 0000000..c72fa70 --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainApplication.kt @@ -0,0 +1,13 @@ +package com.zscaler.sdk.demoapp + +import android.app.Application +import androidx.lifecycle.ProcessLifecycleOwner + +class MainApplication : Application() { + + override fun onCreate() { + super.onCreate() + + ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver()) + } +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainViewModel.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainViewModel.kt new file mode 100644 index 0000000..3bff3de --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/MainViewModel.kt @@ -0,0 +1,281 @@ +package com.zscaler.sdk.demoapp + +import android.app.Application +import android.util.Log +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.zscaler.sdk.android.ZscalerSDK +import com.zscaler.sdk.android.exception.ZscalerSDKException +import com.zscaler.sdk.android.networking.ZscalerSDKRetrofit +import com.zscaler.sdk.demoapp.repository.SharedPrefsUserRepository +import com.zscaler.sdk.demoapp.repository.UserRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.ResponseBody +import retrofit2.Response +import java.io.BufferedInputStream +import java.io.IOException +import java.util.Base64 +import java.util.UUID + +class MainViewModel(application: Application) : AndroidViewModel(application) { + private val TAG = "MainViewModel" + var tunnelOption = mutableStateOf(ZDKTunnel.NO_SELECTION) + var tunnelStatus = mutableStateOf("") + var zdkStatus: MutableLiveData = MutableLiveData() + private lateinit var zdkStatusLaunch: Job + private val userRepository: UserRepository = SharedPrefsUserRepository(application) + private val _responseData = MutableLiveData() + val responseData: LiveData + get() = _responseData + + fun exportLog(destination: String): String { + val exportLogDestination = + ZscalerSDK.exportLogs(destinationFolder = destination).toString() + Log.d(TAG, "exportLog() called with: destination = $exportLogDestination") + return exportLogDestination + } + + fun clearLogs(onSuccess: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + ZscalerSDK.clearLogs() + withContext(Dispatchers.Main) { + onSuccess() + } + } + } + + fun saveUdid(username: String) { + userRepository.saveUdid(username) + } + + fun getUdid(defaultUdid: String): String { + var udid = userRepository.getUdid() + if (udid.isNullOrEmpty()) { + udid = UUID.randomUUID().toString() + if (udid.isNullOrEmpty()) { + udid = defaultUdid + } + saveUdid(udid) + } + return udid + } + + fun startPreLoginTunnel(appKey: String, + udid : String, + onErrorOccurred: (errorCode: Int) -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + try { + ZscalerSDK.startPreLoginTunnel(appKey = appKey, deviceUdid = udid) + tunnelStatus.value = ZscalerSDK.status() + setSelectedTunnel(ZDKTunnel.PRE_LOGIN) + Log.d(TAG, "startPreLoginTunnel completed") + } catch (e: Exception) { + Log.e(TAG, "startPreLoginTunnel() failed with exception :: ${e.message}") + viewModelScope.launch(Dispatchers.Main) { + setSelectedTunnel(ZDKTunnel.NO_SELECTION) + stopTunnelStatusUpdates() + onErrorOccurred( + when (e) { + is ZscalerSDKException -> e.errorCode + else -> -1 + } + ) + } + } + } + } + + fun startZeroTrustTunnel( + appKey: String, + accessToken: String, + udid : String, + onErrorOccurred: (errorCode: Int) -> Unit + ) { + viewModelScope.launch(Dispatchers.IO) { + try { + ZscalerSDK.startZeroTrustTunnel(appKey = appKey, deviceUdid = udid, accessToken = accessToken) + tunnelStatus.value = ZscalerSDK.status() + setSelectedTunnel(ZDKTunnel.ZERO_TRUST) + Log.d(TAG, "startZeroTrustTunnel completed") + } catch (e: Exception) { + Log.e(TAG, "startZeroTrustTunnel() failed with exception ${e.message}") + viewModelScope.launch(Dispatchers.Main) { + setSelectedTunnel(ZDKTunnel.NO_SELECTION) + stopTunnelStatusUpdates() + onErrorOccurred( + when (e) { + is ZscalerSDKException -> e.errorCode + else -> -1 + } + ) + } + } + } + } + + fun stopTunnel(resetStatusText:()-> String): Unit { + Log.d(TAG, "stopTunnel() called") + try { + val retVal = ZscalerSDK.stopTunnel() + if (retVal == 0) zdkStatus.value = resetStatusText() + } catch (e: Exception) { + Log.e(TAG, "stopTunnel() failed with exception ${e.message}") + } + } + + fun getStatus(): String { + Log.d(TAG, "getStatus() called ${ZscalerSDK.status()}") + tunnelStatus.value = ZscalerSDK.status() + return tunnelStatus.value + } + + fun setSelectedTunnel(tunnelOption: ZDKTunnel) { + this.tunnelOption.value = tunnelOption + } + + fun getSelectedTunnel(): ZDKTunnel { + return this.tunnelOption.value + } + + fun startTunnelStatusUpdates(): String { + zdkStatusLaunch = viewModelScope.launch(Dispatchers.IO) { + while (true) { + tunnelStatus.value = ZscalerSDK.status() + delay(2000) + Log.d(TAG, "startPeriodicStatusUpdate() called status ${tunnelStatus.value}") + withContext(Dispatchers.Main) { + zdkStatus.value = ZscalerSDK.status() + } + } + } + return tunnelStatus.value + } + + fun stopTunnelStatusUpdates() { + if (::zdkStatusLaunch.isInitialized) { + zdkStatusLaunch.cancel() + } + } + + fun getData(url: String) { + Log.d(TAG, "getData() called with: url = $url") + val apiService = ZscalerSDKRetrofit.getInstance(url).create(ApiService::class.java) + viewModelScope.launch(Dispatchers.IO) { + try { + val response = apiService.getData(url).execute() + parseRetrofitResponse(response) + } catch (e: IOException) { + _responseData.postValue("Network error") + e.printStackTrace() + } + } + } + + fun loadManuallyWithProxyInfo(url: String, method: Boolean) { + val proxyInfo = ZscalerSDK.proxyInfo() + val apiService = proxyInfo?.let { + ManualRetrofitApiClient.getRetrofitWithProxyInfo(baseUrl = url, it)?.create(ApiService::class.java) + } + viewModelScope.launch(Dispatchers.IO) { + try { + val response = if(method) { + apiService?.getData(url)?.execute() + } else { + apiService?.postData(url)?.execute() + } + response?.let { parseRetrofitResponse(it) } + } catch (e: IOException) { + _responseData.postValue("Network error") + e.printStackTrace() + } + } + } + + fun postData(url: String, params: Map) { + Log.d(TAG, "postData() called with: url = $url, params = $params") + val apiService = ZscalerSDKRetrofit.getInstance(url).create(ApiService::class.java) + viewModelScope.launch(Dispatchers.IO) { + try { + val response = apiService.postData(url, params).execute() + parseRetrofitResponse(response) + } catch (e: IOException) { + _responseData.postValue("Network error") + e.printStackTrace() + } + } + } + + private fun parseRetrofitResponse(response: Response) { + if (response.isSuccessful && response.body() != null) { + val responseBody = response.body()!! + val contentType = responseBody.contentType().toString() + + // Check if content type is application/octet-stream + if (contentType.contains("application/octet-stream")) { + // Handle application/octet-stream content type + handleOctetStreamResponse(responseBody) + } else { + // For other content types, handle as usual + handleRegularResponse(responseBody) + } + } else { + // If response is not successful, handle error + handleErrorResponse(response) + } + } + + private fun handleOctetStreamResponse(responseBody: ResponseBody) { + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var bytesRead: Int + var totalBytesRead: Long = 0 + + val inputStream = responseBody.byteStream() + val bufferedInputStream = BufferedInputStream(inputStream) + + try { + while (bufferedInputStream.read(buffer).also { bytesRead = it } != -1) { + totalBytesRead += bytesRead + _responseData.postValue("Total Bytes Read: $totalBytesRead") + } + } catch (e: IOException) { + e.printStackTrace() + _responseData.postValue("Error reading octet-stream response: ${e.message}") + } finally { + bufferedInputStream.close() + inputStream.close() + } + } + private fun handleRegularResponse(responseBody: ResponseBody) { + try { + val responseData = responseBody.string() + _responseData.postValue(responseData) + } catch (e: IOException) { + e.printStackTrace() + _responseData.postValue("Error reading response: ${e.message}") + } + } + + private fun handleErrorResponse(response: Response) { + _responseData.postValue(response.errorBody()?.string() ?: "Unknown error") + } +} + +fun String.toBase64(): String { + val bytes = this.toByteArray(Charsets.UTF_8) + return Base64.getEncoder().encodeToString(bytes) +} + +fun String.ensureEndsWithSlash(): String { + return if (this.endsWith('/')) { + this + } else { + "$this/" + } +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/NotificationCancellationService.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/NotificationCancellationService.kt new file mode 100644 index 0000000..ee095fa --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/NotificationCancellationService.kt @@ -0,0 +1,67 @@ +package com.zscaler.sdk.demoapp + +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder + + +/** + * A Service that handles notification cancellation-related tasks. + */ +class NotificationCancellationService : Service() { + + private fun showNotification() { + val notificationManager = (getSystemService(NOTIFICATION_SERVICE) as NotificationManager) + val existingNotification = notificationManager.activeNotifications.find { + it.id == NOTIFICATION_ID + }?.notification + + if (existingNotification != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground( + NOTIFICATION_ID, + existingNotification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE + ) + } else { + startForeground(NOTIFICATION_ID, existingNotification) + } + } + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + /** + * Called when the service's task is removed from the recent apps list. + * Cancels all notifications and stops the service. + * + * @param rootIntent The intent that was used to start the task that is being removed. + */ + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancelAll() + stopSelf() + } + + /** + * Called when the service is started with startService(). + * This service will not restart automatically if it is killed by the system. + * + * @param intent The Intent that was used to start the service. + * @param flags Additional data about this start request. + * @param startId A unique integer representing this specific request to start. + * @return The start mode for this service, which is START_NOT_STICKY. + */ + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + showNotification() + return START_NOT_STICKY + } +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/RetrofitApiClient.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/RetrofitApiClient.kt new file mode 100644 index 0000000..8767bcc --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/RetrofitApiClient.kt @@ -0,0 +1,99 @@ +package com.zscaler.sdk.demoapp + +import android.util.Log +import com.zscaler.sdk.android.ZscalerSDK + +import com.zscaler.sdk.android.networking.ZscalerSDKProxyInfo +import okhttp3.Authenticator +import okhttp3.Credentials +import okhttp3.OkHttpClient +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.Route +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.FieldMap +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Url +import java.io.IOException +import java.net.InetSocketAddress +import java.net.Proxy +import java.net.ProxySelector +import java.net.SocketAddress +import java.net.URI + +interface ApiService { + + @GET + fun getData(@Url url: String): Call + + @FormUrlEncoded + @POST + fun postData( + @Url url: String, + @FieldMap params: Map = emptyMap() + ): Call +} + +object ManualRetrofitApiClient { + private var TAG = "ManualRetrofitApiClient" + private var retrofit: Retrofit? = null + private var baseUrl: String = "" + + fun getRetrofitWithProxyInfo(baseUrl: String, proxyInfo: ZscalerSDKProxyInfo): Retrofit? { + if (retrofit == null || ManualRetrofitApiClient.baseUrl != baseUrl) { + val client = getOkHttpclient(proxyInfo) + ManualRetrofitApiClient.baseUrl = baseUrl + retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + return retrofit + } + + fun clearRetroFitInstance() { + retrofit = null + } + + private fun getOkHttpclient(zscalerSDKProxyInfo: ZscalerSDKProxyInfo): OkHttpClient { + val proxyHost = zscalerSDKProxyInfo.proxyHost + val proxyPort = zscalerSDKProxyInfo.proxyPort + val username = zscalerSDKProxyInfo.username + val password = zscalerSDKProxyInfo.password + val builder = OkHttpClient.Builder() + setProxyForHttpRequest(proxyHost, proxyPort) + if (username?.isNotEmpty() == true && password?.isNotEmpty() == true) { + val authenticator = Authenticator { _: Route?, response: Response -> + val credential = Credentials.basic(username, password) + response.request.newBuilder() + .header("Proxy-Authorization", credential) + .build() + } + builder.proxyAuthenticator(authenticator) + } + return builder.build() + } + + private fun setProxyForHttpRequest(proxyHost: String, proxyPort: Int) { + ProxySelector.setDefault(object : ProxySelector() { + override fun select(uri: URI?): MutableList { + val proxyList = mutableListOf() + proxyList.add(Proxy(Proxy.Type.HTTP, InetSocketAddress(proxyHost, proxyPort))) + return proxyList + } + + override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) { + Log.d(TAG, "connectFailed : proxy connectFailed() with: uri = $uri, SocketAddress = $sa, IOException = ${ioe?.message}") + } + }) + } +} + +enum class HttpMethod { + GET, POST, WEB +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/SettingActivity.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/SettingActivity.kt new file mode 100644 index 0000000..e6c8770 --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/SettingActivity.kt @@ -0,0 +1,87 @@ +package com.zscaler.sdk.demoapp + +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import com.zscaler.sdk.android.ZscalerSDK +import com.zscaler.sdk.android.exception.ZscalerSDKException +import com.zscaler.sdk.demoapp.databinding.ActivitySettingBinding + +class SettingActivity : AppCompatActivity() { + private val TAG = "SettingActivity" + private lateinit var binding: ActivitySettingBinding + private lateinit var settingsList: MutableList + private var zscalerSDKConfigurationMap = mutableMapOf() + private lateinit var settingsAdapter: SettingsAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySettingBinding.inflate(layoutInflater) + setContentView(binding.root) + initZscalerConfigOption() + } + + private fun initZscalerConfigOption() { + settingsList = mutableListOf( + SettingItem( + SettingType.URL_SESSIONS, + getString(R.string.setting_title_urlsessions), + getString(R.string.setting_desc_urlsessions), + zscalerSDKConfigurationMap.getOrDefault(SettingType.URL_SESSIONS, false), true + ), + SettingItem( + SettingType.WEB_VIEWS, + getString(R.string.setting_title_webviews), + getString(R.string.setting_desc_webviews), + zscalerSDKConfigurationMap.getOrDefault(SettingType.WEB_VIEWS, false), true + ), + SettingItem( + SettingType.PROXY_AUTHENTICATION, + getString(R.string.setting_title_proxy_authentication), + getString(R.string.setting_desc_proxy_authentication), + zscalerSDKConfigurationMap.getOrDefault(SettingType.PROXY_AUTHENTICATION, false), + true + ), + SettingItem( + SettingType.BLOCK_JB_TRAFFIC, + getString(R.string.setting_title_block_root_traffic), + getString(R.string.setting_desc_block_root_traffic), + zscalerSDKConfigurationMap.getOrDefault(SettingType.BLOCK_JB_TRAFFIC, false), true + ), + SettingItem( + SettingType.BLOCK_ZPA_CONNECTION, + getString(R.string.setting_title_block_zpa_connection), + getString(R.string.setting_desc_block_zpa_connection), + zscalerSDKConfigurationMap.getOrDefault(SettingType.BLOCK_ZPA_CONNECTION, false), + true + ), + SettingItem( + SettingType.ENABLE_DEBUG_LOGS, + getString(R.string.setting_title_enable_log), + getString(R.string.setting_desc_enable_log), + zscalerSDKConfigurationMap.getOrDefault(SettingType.ENABLE_DEBUG_LOGS, false), + true + ), + SettingItem( + SettingType.LOG_LEVEL, + getString(R.string.setting_title_enable_log), + getString(R.string.setting_desc_enable_log), + zscalerSDKConfigurationMap.getOrDefault(SettingType.LOG_LEVEL, false), + false + ) + ) + settingsAdapter = SettingsAdapter(this, settingsList) + binding.settingsRecycleView.layoutManager = LinearLayoutManager(this) + binding.settingsRecycleView.adapter = settingsAdapter + binding.tvSettingDone.setOnClickListener { + ManualRetrofitApiClient.clearRetroFitInstance() + try { + ZscalerSDK.setConfiguration(ZscalerSDKSetting.getZscalerSDKConfiguration()) + } catch (exception: ZscalerSDKException) { + Log.e(TAG, "Got exception while setting configuration = $exception") + } + onBackPressedDispatcher.onBackPressed() + } + } +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/SettingsAdapter.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/SettingsAdapter.kt new file mode 100644 index 0000000..7751eb6 --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/SettingsAdapter.kt @@ -0,0 +1,92 @@ +package com.zscaler.sdk.demoapp + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.zscaler.sdk.android.configuration.ZscalerSDKConfiguration +import com.zscaler.sdk.android.configuration.ZscalerSDKConfiguration.* +import com.zscaler.sdk.demoapp.databinding.ItemSettingBinding +import com.zscaler.sdk.demoapp.databinding.ItemSettingRadioBinding + +class SettingsAdapter( + private val context: Context, + private val settingsList: MutableList, +) : RecyclerView.Adapter() { + + companion object { + private const val VIEW_TYPE_TOGGLE = 1 + private const val VIEW_TYPE_RADIO_BUTTONS = 2 + } + + class SettingsViewHolder(val binding: ViewBinding) : + RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingsViewHolder { + return when (viewType) { + VIEW_TYPE_TOGGLE -> { + val binding = + ItemSettingBinding.inflate(LayoutInflater.from(context), parent, false) + SettingsViewHolder(binding) + } + + VIEW_TYPE_RADIO_BUTTONS -> { + val binding = + ItemSettingRadioBinding.inflate(LayoutInflater.from(context), parent, false) + SettingsViewHolder(binding) + } + + else -> throw IllegalArgumentException("Invalid view type") + } + } + + override fun getItemViewType(position: Int): Int { + val settingItem = settingsList[position] + return if (settingItem.hasToggle) { + VIEW_TYPE_TOGGLE + } else { + VIEW_TYPE_RADIO_BUTTONS + } + } + + override fun onBindViewHolder(holder: SettingsViewHolder, position: Int) { + val settingItem = settingsList[position] + + when (getItemViewType(position)) { + VIEW_TYPE_TOGGLE -> { + val binding = holder.binding as ItemSettingBinding + binding.title.text = settingItem.title + binding.description.text = settingItem.description + binding.toggle.isChecked = + ZscalerSDKSetting.zscalerSDKConfigurationMap.getOrDefault( + settingItem.type, + false + ) + binding.toggle.setOnClickListener { + ZscalerSDKSetting.zscalerSDKConfigurationMap[settingItem.type] = + binding.toggle.isChecked + } + } + + VIEW_TYPE_RADIO_BUTTONS -> { + val binding = holder.binding as ItemSettingRadioBinding + when (ZscalerSDKSetting.logLevel) { + LogLevel.error -> binding.rbLogLevelError.isChecked = true + LogLevel.info -> binding.rbLogLevelInfo.isChecked = true + LogLevel.debug -> binding.rbLogLevelDebug.isChecked = true + } + binding.rbLogLevelInfo.setOnClickListener { ZscalerSDKSetting.logLevel = LogLevel.info } + binding.rbLogLevelDebug.setOnClickListener { ZscalerSDKSetting.logLevel = LogLevel.debug } + binding.rbLogLevelError.setOnClickListener { ZscalerSDKSetting.logLevel = LogLevel.error } + } + } + } + + override fun getItemCount(): Int { + return settingsList.size + } +} + diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZDKTunnel.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZDKTunnel.kt new file mode 100644 index 0000000..e5600f7 --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZDKTunnel.kt @@ -0,0 +1,7 @@ +package com.zscaler.sdk.demoapp + +enum class ZDKTunnel { + PRE_LOGIN, + ZERO_TRUST, + NO_SELECTION +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZdkConstants.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZdkConstants.kt new file mode 100644 index 0000000..6dbeec0 --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZdkConstants.kt @@ -0,0 +1,102 @@ +package com.zscaler.sdk.demoapp + +const val NOTIFICATION_CHANNEL_ID = "ZSCALERSDK_NOTIFICATION_ID" +const val NOTIFICATION_ID = 1 +const val zpaNotConnectedHtml = """ + + + + + +
+ ZPA Not Connected +
+ + +""" + +const val zpaEmptyHtml = """ + + + + + +
+ +
+ + +""" + +const val zpaErrorHtml = """ + + + + + +
+ An SSL error has occurred and a secure connection to the server cannot be made +
+ + +""" +const val apiResponseErrorHtml = """ + + + + + +
+ Failed to fetch the data using retrofit client +
+ + +""" \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZdkDialog.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZdkDialog.kt new file mode 100644 index 0000000..7a8597b --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZdkDialog.kt @@ -0,0 +1,24 @@ +package com.zscaler.sdk.demoapp + +import android.content.Context +import androidx.appcompat.app.AlertDialog + +object ZdkDialog { + fun showMessageDialog(context: Context, message: String) { + // Create a new AlertDialog builder + val builder = AlertDialog.Builder(context) + + // Set the message for the dialog + builder.setMessage(message) + + // Add a button to the dialog + builder.setPositiveButton("OK") { dialog, which -> + // Dismiss the dialog when the button is clicked + dialog.dismiss() + } + + // Create and show the dialog + val dialog = builder.create() + dialog.show() + } +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZipUtility.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZipUtility.kt new file mode 100644 index 0000000..f2ef43a --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZipUtility.kt @@ -0,0 +1,45 @@ +package com.zscaler.sdk.demoapp + +import android.content.Context +import android.os.Build +import android.util.Log +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +object ZipUtility { + + private const val TAG = "ZipUtility" + + fun createEmptyZipFile(context: Context, zipFileName: String): String? { + return try { + // Get the internal storage directory of the app + val internalStorageDir = context.filesDir + val zipFile = File(internalStorageDir, zipFileName) + + // Create a new empty zip file + val fos = FileOutputStream(zipFile) + val zos = ZipOutputStream(fos) + + // Add an empty entry to the zip file to avoid crash on Android 28 and below as empty zip cannot be created + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + val entry = ZipEntry("empty_file.txt") + zos.putNextEntry(entry) + zos.closeEntry() + } + + zos.close() + + // Log the URI of the created zip file + Log.d(TAG, "Created ZIP file: ${zipFile.absolutePath}") + + // Return the URI of the created zip file + zipFile.absolutePath + } catch (e: Exception) { + Log.e(TAG, "Error creating ZIP file: ${e.message}") + e.printStackTrace() + null + } + } +} diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZscalerSDKConfig.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZscalerSDKConfig.kt new file mode 100644 index 0000000..2520b53 --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/ZscalerSDKConfig.kt @@ -0,0 +1,65 @@ +package com.zscaler.sdk.demoapp + +import com.zscaler.sdk.android.configuration.ZscalerSDKConfiguration + +enum class SettingType { + URL_SESSIONS, + WEB_VIEWS, + PROXY_AUTHENTICATION, + BLOCK_JB_TRAFFIC, + BLOCK_ZPA_CONNECTION, + ENABLE_DEBUG_LOGS, + LOG_LEVEL, +} + +data class SettingItem( + val type: SettingType, + val title: String, + val description: String, + var isChecked: Boolean, + var hasToggle: Boolean +) + +object ZscalerSDKSetting { + val zscalerSDKConfigurationMap: MutableMap = mutableMapOf() + var logLevel: ZscalerSDKConfiguration.LogLevel = ZscalerSDKConfiguration.LogLevel.debug + + fun getZscalerSDKConfiguration(): ZscalerSDKConfiguration { + return ZscalerSDKConfiguration( + automaticallyConfigureRequests = zscalerSDKConfigurationMap.getOrDefault( + SettingType.URL_SESSIONS, + false + ), + automaticallyConfigureWebviews = zscalerSDKConfigurationMap.getOrDefault( + SettingType.WEB_VIEWS, + false + ), + useProxyAuthentication = zscalerSDKConfigurationMap.getOrDefault( + SettingType.PROXY_AUTHENTICATION, + false + ), + blockZPAConnectionsOnTunnelFailure = zscalerSDKConfigurationMap.getOrDefault( + SettingType.BLOCK_ZPA_CONNECTION, + false + ), + enableDebugLogsInConsole = zscalerSDKConfigurationMap.getOrDefault( + SettingType.ENABLE_DEBUG_LOGS, + false + ), + logLevel = logLevel + ) + } + + fun defaultZscalerSDKConfiguration(): ZscalerSDKConfiguration { + zscalerSDKConfigurationMap[SettingType.URL_SESSIONS] = true + zscalerSDKConfigurationMap[SettingType.WEB_VIEWS] = true + zscalerSDKConfigurationMap[SettingType.ENABLE_DEBUG_LOGS] = true + logLevel = ZscalerSDKConfiguration.LogLevel.debug + return ZscalerSDKConfiguration( + automaticallyConfigureRequests = true, + automaticallyConfigureWebviews = true, + enableDebugLogsInConsole = true, + logLevel = ZscalerSDKConfiguration.LogLevel.debug + ) + } +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/repository/SharedPrefsUserRepository.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/repository/SharedPrefsUserRepository.kt new file mode 100644 index 0000000..75b3cb3 --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/repository/SharedPrefsUserRepository.kt @@ -0,0 +1,21 @@ +package com.zscaler.sdk.demoapp.repository + +import android.content.Context + +/** + * Created by Anurag Goel on 04/12/24. + * + */ +class SharedPrefsUserRepository(private val context: Context) : UserRepository { + + private val sharedPreferences = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + private val UDID_KEY = "udid" + + override fun saveUdid(udid: String) { + sharedPreferences.edit().putString(UDID_KEY, udid).apply() + } + + override fun getUdid(): String { + return sharedPreferences.getString(UDID_KEY, "") ?: "" + } +} \ No newline at end of file diff --git a/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/repository/UserRepository.kt b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/repository/UserRepository.kt new file mode 100644 index 0000000..67c8391 --- /dev/null +++ b/sample-app/app/src/main/java/com/zscaler/sdk/demoapp/repository/UserRepository.kt @@ -0,0 +1,10 @@ +package com.zscaler.sdk.demoapp.repository + +/** + * Created by Anurag Goel on 04/12/24. + * + */ +interface UserRepository { + fun saveUdid(udid: String) + fun getUdid(): String +} \ No newline at end of file diff --git a/sample-app/app/src/main/res/drawable/border.xml b/sample-app/app/src/main/res/drawable/border.xml new file mode 100644 index 0000000..a3e3db5 --- /dev/null +++ b/sample-app/app/src/main/res/drawable/border.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/sample-app/app/src/main/res/drawable/ic_clear_log.png b/sample-app/app/src/main/res/drawable/ic_clear_log.png new file mode 100644 index 0000000..acd17ea Binary files /dev/null and b/sample-app/app/src/main/res/drawable/ic_clear_log.png differ diff --git a/sample-app/app/src/main/res/drawable/ic_export_log.png b/sample-app/app/src/main/res/drawable/ic_export_log.png new file mode 100644 index 0000000..f588aca Binary files /dev/null and b/sample-app/app/src/main/res/drawable/ic_export_log.png differ diff --git a/sample-app/app/src/main/res/drawable/ic_notification.xml b/sample-app/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000..4db0bf4 --- /dev/null +++ b/sample-app/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sample-app/app/src/main/res/drawable/ic_setting.xml b/sample-app/app/src/main/res/drawable/ic_setting.xml new file mode 100644 index 0000000..875e221 --- /dev/null +++ b/sample-app/app/src/main/res/drawable/ic_setting.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sample-app/app/src/main/res/layout/activity_main.xml b/sample-app/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..bd4fa54 --- /dev/null +++ b/sample-app/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample-app/app/src/main/res/layout/activity_setting.xml b/sample-app/app/src/main/res/layout/activity_setting.xml new file mode 100644 index 0000000..50cd882 --- /dev/null +++ b/sample-app/app/src/main/res/layout/activity_setting.xml @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/sample-app/app/src/main/res/layout/item_setting.xml b/sample-app/app/src/main/res/layout/item_setting.xml new file mode 100644 index 0000000..c202ba4 --- /dev/null +++ b/sample-app/app/src/main/res/layout/item_setting.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/sample-app/app/src/main/res/layout/item_setting_radio.xml b/sample-app/app/src/main/res/layout/item_setting_radio.xml new file mode 100644 index 0000000..7b973e1 --- /dev/null +++ b/sample-app/app/src/main/res/layout/item_setting_radio.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/sample-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/sample-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..daa3c21 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..4078d57 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9e48532 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..546d51d Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..f1782e5 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..bc5ba1d Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..ee25fc8 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..d986587 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..f78b483 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..434bf27 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b4d2ca2 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c7699b2 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..14397db Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..50676d3 Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..340002e Binary files /dev/null and b/sample-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/sample-app/app/src/main/res/values/arrays.xml b/sample-app/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..e2cd4c8 --- /dev/null +++ b/sample-app/app/src/main/res/values/arrays.xml @@ -0,0 +1,8 @@ + + + + WEB + GET + POST + + \ No newline at end of file diff --git a/sample-app/app/src/main/res/values/colors.xml b/sample-app/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/sample-app/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/sample-app/app/src/main/res/values/default.xml b/sample-app/app/src/main/res/values/default.xml new file mode 100644 index 0000000..b07b85d --- /dev/null +++ b/sample-app/app/src/main/res/values/default.xml @@ -0,0 +1,4 @@ + + + 0f76c3dcf837024a0ea6826228d802cc3de21d6ca81857ab0b578903596d951b + \ No newline at end of file diff --git a/sample-app/app/src/main/res/values/dimens.xml b/sample-app/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..59a027b --- /dev/null +++ b/sample-app/app/src/main/res/values/dimens.xml @@ -0,0 +1,10 @@ + + + 16sp + 16dp + 8dp + 10dp + 10dp + 16dp + 60dp + \ No newline at end of file diff --git a/sample-app/app/src/main/res/values/ic_launcher_background.xml b/sample-app/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..7b97cda --- /dev/null +++ b/sample-app/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #3D52DC + \ No newline at end of file diff --git a/sample-app/app/src/main/res/values/secret.xml b/sample-app/app/src/main/res/values/secret.xml new file mode 100644 index 0000000..8e4f220 --- /dev/null +++ b/sample-app/app/src/main/res/values/secret.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample-app/app/src/main/res/values/strings.xml b/sample-app/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d67d3fc --- /dev/null +++ b/sample-app/app/src/main/res/values/strings.xml @@ -0,0 +1,44 @@ + + ZscalerSDK Demo + App Key Base64 + Export Logs + Access Token + Clear Logs + Enter URL + Go + Zscaler Config + PreLogin Tunnel + ZeroTrust Tunnel + Status: %1$s + Status: Error %1$s + Enter valid App Key + App key is empty + Access token is empty + OFF + Enter valid URL + Reloading Web Page + Cleared ZscalerSDK logs + Share ZscalerSDK log ZIP file + Error loading data + ZscalerSDK Log saved to download folder + There is not enough disk space to export log files. + Download folder not found + Configuration Options + Save + URLSessions + WebViews + Proxy Authentication + Block Root Traffic + Block ZPA Connection + Should automatically configure URLSessions + Should automatically configure WebViews + Use proxy authentication + Block traffic if device is rooted + Block ZPA connection on tunnel failure + Enable log + Enable Zscaler sdk log in console + Debug + Info + Error + Log Level + \ No newline at end of file diff --git a/sample-app/app/src/main/res/values/themes.xml b/sample-app/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..e48770a --- /dev/null +++ b/sample-app/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +