Securing Android: Behind a few seconds of payment transaction … 💳📱🔐

Sofien Rahmouni on 2025-04-14

Securing Android: Behind a few seconds of payment transaction … 💳📱🔐

Secure Payment Transaction TapToPay Android

Over the past few months, I’ve been deeply involved in the development of an Android payment application, where security has been a fundamental pillar of the entire process.

Working on this project has given me invaluable insights into the intricate world of securing Android applications.

The protection levels on this Android Payment App, can be devided in Compilation Layer, Runtime Layer.

For free complete read access, please continue with this link : https://medium.com/@sofienrahmouni/securing-android-behind-a-few-seconds-of-payment-transaction-bf6817119d51?source=friends_link&sk=0117880616ac48dde2590f0b8138d766

Published in AndroidWeekly Blog also.

1. Protecting Your App from Reverse Engineering & Debugging

// Check debbugable flag
val isDebuggable =(context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
// Check debugger is connected at runtime
import android.os.Debug.isDebuggerConnected
val isDebuggerConnected= isDebuggerConnected()

The debug check also can be done at the system level using native methodptrace() :

bool = ptrace(PTRACE_TRACEME, 0, 0, 0) == -1;
{
  "requestDetails": {
    "requestPackageName": "com.payment.app",
    "nonce": "b64_encoded_nonce",
    "timestampMillis": 1710000000000
  },
  "appIntegrity": {
    "appRecognitionVerdict": "PLAY_RECOGNIZED",
    "packageName": "com.payment.app",
    "certificateSha256Digest": ["b64_encoded_cert_hash"]
  },
  "deviceIntegrity": {
    "deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"]
  },
  "accountDetails": {
    "appLicensingVerdict": "LICENSED"
  }
}

Continuing with Anti-tampering protection, we should also implement APK signature verification. It is recommended to perform this check on a remote server by sending the current certificate signature and comparing it on the server side.

Here is below an example showing the mechanism to get signature application certificate :

object SignatureUtil {
    private val TAG = SignatureUtil::class.java.simpleName

    /**
      ** Get application signature to be used later in the check anti-tampering
      ** on Server side, in case have is different than setted in the BE means 
      ** the application was changed/tampered. 
      **/
    fun getApkSigners(context: Context): List<Signature>? {
        return try {
            if (context
                    .packageManager
                    .getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
                    .signingInfo?.hasMultipleSigners() == true
            ) {
                context
                    .packageManager
                    .getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
                    .signingInfo?.apkContentsSigners
                    ?.toList()
            } else {
                context
                    .packageManager
                    .getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
                    .signingInfo?.signingCertificateHistory
                    ?.toList()
            }
        } catch (e: PackageManager.NameNotFoundException) {
            Log.e(TAG, "Cannot read APK content signers", e)
            null
        }
    }
}

And finally, here is new anti-tampering protection addition was added in Android 10, the useEmbeddedDex flag which is really simple to be used.

Here is an example how to use :

2. Securing Execution Environment

import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.util.Log
import java.io.BufferedReader

object SecureEnvironmentCheckerInfo {
    private const val TAG = "SecureEnvironmentCheckerInfo"
// The verified paths are : 
//    "/data/local/",
//    "/data/local/bin/",
//    "/data/local/xbin/",
//    "/sbin/",
//    "/system/bin/",
//    "/system/bin/.ext/",
//    "/system/bin/failsafe/",
//    "/system/sd/xbin/",
//    "/system/usr/we-need-root/",
//    "/system/xbin/",
//    "/system/etc/",
//    "/data/",
//    "/dev/"
    fun getFileAccesses(paths: Array<String>): HashMap<String, Boolean> {
        Log.d(TAG, "Checking file accesses using system calls")
        val fileAccesses = HashMap<String, Boolean>()
        for (path in paths) {
            fileAccesses[path] = try {
                Os.access(path, OsConstants.F_OK)
            } catch (e: ErrnoException) {
                false
            }
        }
        Log.d(TAG, "Checked ${fileAccesses.size} file accesses")
        return fileAccesses
    }
    fun getSystemProperties(): HashMap<String, String>? {
        Log.d(TAG, "Check system properties using property service")
        val systemCommand = "getprop"
        val content = getOutput(systemCommand, event) ?: return null
        val systemProperties = SystemPropertyParser.parse(content)
        if (systemProperties != null) {
            Log.d(TAG, "Retrieve${systemProperties.size} system properties")
        } else {
            Log.e(TAG, "Retrieve system properties from '$systemCommand' output")
        }
        return systemProperties
    }
    fun getInstalledPackages(): Array<String>? {
        Log.d(TAG, "Retrieve installed packages using package manager with user 0")
        val systemCommand = "pm list packages --user 0"
        val content = getOutput(systemCommand) ?: return null
        val installedPackages = PackageParser.parse(content)
        Log.d(TAG, "Retrieved ${installedPackages.size} installed packages")
        return installedPackages
    }
    private fun getOutput(systemCommand: String): String? {
        var process: Process? = null
        return try {
            process = Runtime.getRuntime().exec(systemCommand)
            val output = process!!.inputStream.bufferedReader().use(BufferedReader::readText)
            val exitValue = process.waitFor()
            if (exitValue == 0) {
                output
            } else {
                Log.e(TAG, "'$systemCommand' command terminated with exit value $exitValue")
                null
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error occurred while executing '$systemCommand' command: $e")
            null
        } finally {
            process?.destroy()
            if (process?.isAlive == true) {
                process.destroyForcibly()
            }
        }
    }
}

3. Preventing Data Leakage & Unauthorized Access

In payment applications, data leakage and unauthorized access present significant security risks, potentially exposing sensitive user information such as Payment Card details (PAN, CVV, etc.). To mitigate these security vulnerabilities, it is crucial to implement the following protective measures:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <permission
        android:name="com.payment.app.permission.BIND_SERVICE"
        android:protectionLevel="signature" />
    <application
        android:name="com.payment.app.Application"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name">
        <activity
            android:name="com.payment.app.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name="com.payment.app.service.CoreService"
            android:exported="true"
            android:enabled="true"
            android:permission="com.payment.app.BIND_SERVICE" >
            <intent-filter>
                <action android:name="com.payment.app.action.BIND_SERVICE" />
            </intent-filter>
        </service>
    </application>
</manifest>
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Instrumentation
import android.graphics.Point
import android.os.Build
import android.os.SystemClock
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import kotlinx.coroutines.*
import kotlin.math.absoluteValue

class OverlayDetection(
    screenSize: Point,
    private var onOverlayDetected: (() -> Unit)? = null
) {
    companion object {
        private val TAG = OverlayDetection::class.java.simpleName
        const val INJECTED_STATE = -35
        private const val OFFSET_PERCENT = 10
        private const val RETRY_DELAY_INJECT_EVENT = 1500L //1.5 seconds
        private fun getFlagsWindow() =
            MotionEvent.FLAG_WINDOW_IS_OBSCURED or MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED
    }
    private var lastCallNotCaught = false
    private var overlayDetectionRunning = true
    private val scope = CoroutineScope(Dispatchers.IO)
    private val instrumentation = Instrumentation()
    private var view: SecureViewContainer? = null
    private var activity: Activity? = null
    private val offset: Point = Point(
        (screenSize.x * OFFSET_PERCENT) / 100,
        (screenSize.y * OFFSET_PERCENT) / 100
    )
    private val range = Point(
        screenSize.x - (2 * offset.x),
        screenSize.y - (2 * offset.y),
    )
    private var job: Job? = null
    fun setViewToProtect(view: SecureViewContainer) {
        Log.d(TAG, "OverlayDetection: protect view $view")
        this.view = view
        activity = view.context as Activity
            this.view?.setOnTouchListener { _: View?, event: MotionEvent ->
                onOverlayEventDetected(event)
                false
            }
            this.view?.setOnOverlayDetectionListener {
                onOverlayEventDetected(event)
           }
    }
    private fun hasSecureFlag(): Boolean =
        activity?.window?.attributes?.flags?.let {
            (it and WindowManager.LayoutParams.FLAG_SECURE) != 0
        } ?: false
    private fun injectEvent() {
        if (lastCallNotCaught) {
            onOverlayDetected.invoke()
        } else {
            injectTouchEvent()
        }
    }
    private fun injectTouchEvent() =
        randomPoint().also { point ->
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
                if (point.value != null) {
                    Log.d(TAG, "Inject event at ${point.value}")
                    lastCallNotCaught = true
                    try {
                        touch(point.value, MotionEvent.ACTION_DOWN)
                        touch(point.value, MotionEvent.ACTION_UP)
                    } catch (e: Exception) {
                        Log.e(TAG, e.stackTraceToString())
                        onOverlayDetected.invoke()
                    }
                } else {
                    Log.e(TAG, "Random point can't be generated for inject event")
                    onOverlayDetected.invoke()
                }
            } else {
                Log.w(TAG, "ANDROID 13: Touch injection deactivated at $point")
            }
        }
    private fun touch(point: Point, event: Int) {
        instrumentation.sendPointerSync(
            MotionEvent.obtain(
                SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
                event,
                point.x.toFloat(), point.y.toFloat(),
                INJECTED_STATE
            )
        )
    }
    private fun randomPoint(): Rand.Result<Point> {
        val xRand = Rand.nextInt()
        val x = xRand.value ?: return Rand.Result(xRand.nativeResult!!)
        val yRand = Rand.nextInt()
        val y = yRand.value ?: return Rand.Result(yRand.nativeResult!!)
        val point =
            Point((x.absoluteValue % range.x) + offset.x, (y.absoluteValue % range.y) + offset.y)
        return Rand.Result(point)
    }
    fun start() {
            stop()
            overlayDetectionRunning = true
            lastCallNotCaught = false
            Log.d(TAG, "Start overlay protection")
            job = scope.launch {
                delay(1000L)
                view?.requestFocus()
                while (overlayDetectionRunning) {
                    injectEvent()
                    delay(RETRY_DELAY_INJECT_EVENT)
                }
            }
    }
    fun stop() {
        overlayDetectionRunning = false
        lastCallNotCaught = false
        Log.d(TAG, "Stop overlay protection")
        job?.cancel()
        job = null
    }
    fun forbidMultiWindowMode(activity: Activity): Boolean =
        activity.isInMultiWindowMode
    fun onOverlayEventDetected(event: MotionEvent) {
        Log.d(TAG, "Received motion event: state(${event.metaState}) flags(${event.flags})")
        when {
            !hasSecureFlag() -> onOverlayEventDetected(event)
            event.metaState != INJECTED_STATE -> Unit
            event.flags == MotionEvent.ACTION_MOVE -> Unit // Workaround  to avoid false/positive toast overlay detection
            (event.flags and getFlagsWindow()) != 0 -> onOverlayDetected.invoke()
        }
        lastCallNotCaught = false
    }
}
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import androidx.constraintlayout.widget.ConstraintLayout

class SecureViewContainer @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyle: Int = 0,
) : ConstraintLayout(
    context,
    attributeSet,
    defStyle
) {
    companion object {
        private val TAG = SecureViewContainer::class.java.simpleName
    }
    private var onOverlayDetectionListener: (() -> Unit)? = null
    fun setOnOverlayDetectionListener(onOverlayDetectionListener: () -> Unit) {
        this.onOverlayDetectionListener = onOverlayDetectionListener
    }
    override fun onFilterTouchEventForSecurity(ev: MotionEvent): Boolean {
        val checkIsObscured =
            ev.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
        val checkIsPartiallyObscured =
            ev.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
        Log.d(TAG, "onFilterTouchEventForSecurity: " +
                "isObscured: $checkIsObscured, isPartiallyObscured: $checkIsPartiallyObscured")
        if (checkIsObscured || checkIsPartiallyObscured) {
            onOverlayDetectionListener?.invoke()
        }
        return super.onFilterTouchEventForSecurity(ev)
    }
}
<com.payment.app.SecureViewContainer xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/icon"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:src="@drawable/icon" />
</com.payment.app.SecureViewContainer>
import android.graphics.Rect
import android.view.View
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.view.ViewCompat

class SecuredAccessibilityDelegateHelper(val view: View) : View.AccessibilityDelegate() {
    override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
        super.onInitializeAccessibilityNodeInfo(host, info)
            view.importantForAccessibility =
                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
            info.isEnabled = false
            info.setBoundsInScreen(Rect(0, 0, 0, 0))
            info.text = ""
    }
}
class SecuredText(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) {
    init {
        accessibilityDelegate = SecuredAccessibilityDelegateHelper(this)
    }
}
<com.payement.app.SecuredText
                android:id="@+id/text"
                android:layout_width="wrap_content"xxx
                android:layout_height="30dp"
                android:gravity="center"
                android:textColor="@color/white"
                android:textSize="15sp"
                android:textStyle="bold" />

4. Strengthening Authentication & Secure Communication

At network security config file :

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
   <domain-config>
       <domain includeSubdomains="true">payment-domain.com</domain>
       <pin-set>
           <pin digest="SHA-256">ZEDJEEKKEKLEE+fibTqbIsWNR/X7CWNVW+CEEA=</pin>
           <pin digest="SHA-256">BFDBDFDFVDccvVFVVcfdesdsdvsdsdvsd=</pin>
       </pin-set>
   </domain-config>
</network-security-config>
Recommended cryptography algorithm by Android

From NIST :

Recommended cryptography algorithm by NIST

From ECRYPT

Recommended cryptography algorithm by ECRYPT

From CRYPTREC

Recommended cryptography algorithm by CRYPTREC

and finally, to store the encrypted locally, it’s required to generate it by the Android Keystore API using Hardware-backed Keystore

7. Advanced Security Techniques

import android.content.Context
import android.opengl.EGL14.*
import android.opengl.GLSurfaceView
import android.util.Log
import com.visa.BuildConfig
import javax.microedition.khronos.egl.*

/**
 * A simple GLSurfaceView sub-class that demonstrate how to perform
 * OpenGL ES 2.0 rendering into a GL Surface. Note the following important
 * details:
 *
 * - The class must use a custom context factory to enable 2.0 rendering.
 * See ContextFactory class definition below.
 *
 * - The class must use a custom EGLConfigChooser to be able to select
 * an EGLConfig that supports 2.0. This is done by providing a config
 * specification to eglChooseConfig() that has the attribute
 * EGL10.ELG_RENDERABLE_TYPE containing the EGL_OPENGL_ES2_BIT flag
 * set. See ConfigChooser class definition below.
 *
 * - The class must select the surface's format, then choose an EGLConfig
 * that matches it exactly (with regards to red/green/blue/alpha channels
 * bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
 *
 * - The class use some OpenGL protection extension if it's exist on the Android Device otherwise
 *  the used context is standard.
 *  About this mechanism of this protected extension EGL_PROTECTED_CONTENT_EXT = 0x32C0 :
 *
 *  The attribute EGL_PROTECTED_CONTENT_EXT can be applied to EGL contexts,
 *  EGL surfaces and EGLImages. If the attribute EGL_PROTECTED_CONTENT_EXT
 *  is set to EGL_TRUE by the application, then the newly created EGL object
 *  is said to be protected. A protected context is required to allow the
 *  GPU to operate on protected resources, including protected surfaces and
 *  protected EGLImages.
 *
 *  GPU operations are grouped into pipeline stages. Pipeline stages can be
 *  defined to be protected or not protected. Each stage defines
 *  restrictions on whether it can read or write protected and unprotected
 *  resources, as follows:
 *  When a GPU stage is protected, it:
 * - Can read from protected resources
 * - Can read from unprotected resources
 * - Can write to protected resources
 * - Can NOT write to unprotected resources
 *
 * When a GPU stage is not protected, it:
 * - Can NOT read from protected resources
 * - Can read from unprotected resources
 * - Can NOT write to protected resources
 * - Can write to unprotected resources
 *
 */
internal class GL2JNIView(
    context: Context?,
) : GLSurfaceView(context) {
    companion object {
        private val TAG = GL2JNIView::class.java.simpleName
        private const val EGL_CONTEXT_CLIENT_ATTR_VERSION = 0x3098
        private const val EGL_CONTEXT_CLIENT_VALUE_VERSION = 2
        private const val EGL_EXTENSION_PROTECTED_CONTENT_NAME = "EGL_EXT_protected_content"
        private const val EGL_EXT_PROTECTED_CONTENT = 0x32C0
    }
    init {
        val isSecureExtensionSupported = isSecureExtensionSupported()
        Log.d(TAG, "OpenGl Secure extension supported : $isSecureExtensionSupported")
        if (BuildConfig.DEBUG) {
            debugFlags = DEBUG_CHECK_GL_ERROR or DEBUG_LOG_GL_CALLS
        }
        setEGLContextFactory(ContextFactory(isSecureExtensionSupported))
        setEGLWindowSurfaceFactory(WindowSurfaceFactory(isSecureExtensionSupported))
    }
    internal inner class ContextFactory(private val secureContext: Boolean) : EGLContextFactory {
        override fun createContext(egl: EGL10, display: EGLDisplay, config: EGLConfig): EGLContext {
            val attrList = if (secureContext) {
                intArrayOf(EGL_CONTEXT_CLIENT_ATTR_VERSION,
                    EGL_CONTEXT_CLIENT_VALUE_VERSION,
                    EGL_EXT_PROTECTED_CONTENT,
                    EGL_TRUE,
                    EGL10.EGL_NONE)
            } else {
                intArrayOf(EGL_CONTEXT_CLIENT_VERSION,
                    EGL_CONTEXT_CLIENT_VALUE_VERSION,
                    EGL10.EGL_NONE)
            }
            val context = egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, attrList)
            if (context == EGL10.EGL_NO_CONTEXT) {
                Log.e(TAG, "Error creating EGL context.")
            }
            checkEglError("eglCreateContext")
            return context
        }
        override fun destroyContext(egl: EGL10, display: EGLDisplay, context: EGLContext) {
            if (!egl.eglDestroyContext(display, context)) {
                Log.e("DefaultContextFactory", "display: $display context: $context")
            }
        }
    }
    internal inner class WindowSurfaceFactory(private val secureWindowSurface: Boolean) :
        EGLWindowSurfaceFactory {
        override fun createWindowSurface(
            egl: EGL10,
            display: EGLDisplay,
            config: EGLConfig,
            nativeWindow: Any,
        ): EGLSurface? {
            var result: EGLSurface? = null
            try {
                val attrList = if (secureWindowSurface) {
                    intArrayOf(EGL_EXT_PROTECTED_CONTENT, EGL_TRUE, EGL10.EGL_NONE)
                } else {
                    intArrayOf(EGL10.EGL_NONE, EGL10.EGL_NONE, EGL10.EGL_NONE)
                }
                result = egl.eglCreateWindowSurface(display, config, nativeWindow, attrList)
                checkEglError("eglCreateWindowSurface")
            } catch (e: IllegalArgumentException) {
                Log.e(TAG, "eglCreateWindowSurface", e)
            }
            return result
        }
        override fun destroySurface(egl: EGL10, display: EGLDisplay, surface: EGLSurface) {
            egl.eglDestroySurface(display, surface)
        }
    }
    private fun isSecureExtensionSupported(): Boolean {
        val display: android.opengl.EGLDisplay? = eglGetDisplay(EGL_DEFAULT_DISPLAY)
        val extensions = eglQueryString(display, EGL10.EGL_EXTENSIONS)
        return extensions != null && extensions.contains(EGL_EXTENSION_PROTECTED_CONTENT_NAME)
    }
    private fun checkEglError(methodName: String) {
        val eglResult = eglGetError()
        if (eglResult == EGL_SUCCESS) {
            val message = "$methodName succeeded without error (EGL_SUCCESS)"
            Log.d(TAG, message)
        } else {
            val errorHexString = "0x${Integer.toHexString(eglResult)}"
            val errorDesc = eglResultDesc(eglResult)
            val message = "$methodName: EGL error $errorHexString encountered. $errorDesc"
            Log.e(TAG, message)
        }
    }
    // https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetError.xhtml
    private fun eglResultDesc(eglResult: Int) =
        when (eglResult) {
            EGL_SUCCESS -> "The last function succeeded without error."
            EGL_NOT_INITIALIZED -> "EGL is not initialized, or could not be initialized, for the specified EGL display connection."
            EGL_BAD_ACCESS -> "EGL cannot access a requested resource (for example a context is bound in another thread)."
            EGL_BAD_ALLOC -> "EGL failed to allocate resources for the requested operation."
            EGL_BAD_ATTRIBUTE -> "An unrecognized attribute or attribute value was passed in the attribute list."
            EGL_BAD_CONTEXT -> "An EGLContext argument does not name a valid EGL rendering context."
            EGL_BAD_CONFIG -> "An EGLConfig argument does not name a valid EGL frame buffer configuration."
            EGL_BAD_CURRENT_SURFACE -> "The current surface of the calling thread is a window, pixel buffer or pixmap that is no longer valid."
            EGL_BAD_DISPLAY -> "An EGLDisplay argument does not name a valid EGL display connection."
            EGL_BAD_SURFACE -> "An EGLSurface argument does not name a valid surface (window, pixel buffer or pixmap) configured for GL rendering."
            EGL_BAD_MATCH -> "Arguments are inconsistent (for example, a valid context requires buffers not supplied by a valid surface)."
            EGL_BAD_PARAMETER -> "One or more argument values are invalid."
            EGL_BAD_NATIVE_PIXMAP -> "A NativePixmapType argument does not refer to a valid native pixmap."
            EGL_BAD_NATIVE_WINDOW -> "A NativeWindowType argument does not refer to a valid native window."
            EGL_CONTEXT_LOST -> "A power management event has occurred. The application must destroy all contexts and reinitialise OpenGL ES state and objects to continue rendering."
            else -> ""
        }
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.payement.app"
    xmlns:tools="http://schemas.android.com/tools">
    <application>
        <service
            android:name="com.payement.app.service.IsoltedProcessService"
            android:process=":isoltedProcessProcess" />
    </application>
</manifest>

By reading this article, you can get an idea of how a simple payment transaction, which takes only a few seconds, goes through a comprehensive security workflow. Also, keep in mind that there is no final version of security — it’s crucial to continuously stay aligned and updated with the evolving security landscape of Android.

And finally remember, in every update of your security layer, don’t forget this quote:

“Don’t let security kill your business, try to find the right balance.”

For any question & suggestion, Let’s discuss !