diff --git a/README.md b/README.md index 73deeba2e..d34a37a18 100644 --- a/README.md +++ b/README.md @@ -521,6 +521,10 @@ If you enable ProGuard, then add this rules in your ProGuard file. -keep class com.nimbusds.jose.** { *; } ``` +## Protecting screenshot and screen recording + +**Omise Android SDK** comes with built-in protection against screenshoot and screen recording. If you wish to disable this feature, you can pass `OmiseActivity.EXTRA_IS_SECURE` with a value of `false` when starting the following activities: `CreditCardActivity`, `PaymentCreatorActivity`, and `AuthorizingPaymentActivity`. + ## Contributing Pull requests and bug fixes are welcome. For larger scope of work, please pop on to our [forum](https://forum.omise.co) to discuss first. diff --git a/sdk/src/androidTest/java/co/omise/android/ui/AuthorizingPaymentActivityTest.kt b/sdk/src/androidTest/java/co/omise/android/ui/AuthorizingPaymentActivityTest.kt index 6e99169b7..fd4923ff1 100644 --- a/sdk/src/androidTest/java/co/omise/android/ui/AuthorizingPaymentActivityTest.kt +++ b/sdk/src/androidTest/java/co/omise/android/ui/AuthorizingPaymentActivityTest.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.app.Instrumentation import android.content.Intent import android.net.Uri +import android.view.WindowManager import android.widget.ProgressBar import androidx.arch.core.executor.testing.CountingTaskExecutorRule import androidx.lifecycle.MutableLiveData @@ -55,6 +56,7 @@ import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.instanceOf import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before @@ -331,4 +333,21 @@ class AuthorizingPaymentActivityTest { ) ) } + + @Test + fun flagSecure_whenParameterIsFalseThenAttributesMustNotContainFlagSecure() { + intent.putExtra(OmiseActivity.EXTRA_IS_SECURE, false) + val scenario = ActivityScenario.launchActivityForResult(intent) + scenario.onActivity { + assertNotEquals(WindowManager.LayoutParams.FLAG_SECURE, it.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) + } + } + + @Test + fun flagSecure_whenParameterNotSetThenAttributesMustContainFlagSecure() { + val scenario = ActivityScenario.launchActivityForResult(intent) + scenario.onActivity { + assertEquals(WindowManager.LayoutParams.FLAG_SECURE, it.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) + } + } } diff --git a/sdk/src/main/java/co/omise/android/ui/AuthorizingPaymentActivity.kt b/sdk/src/main/java/co/omise/android/ui/AuthorizingPaymentActivity.kt index 1f330a297..29ef8bf56 100644 --- a/sdk/src/main/java/co/omise/android/ui/AuthorizingPaymentActivity.kt +++ b/sdk/src/main/java/co/omise/android/ui/AuthorizingPaymentActivity.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View +import android.view.WindowManager import android.webkit.CookieManager import android.webkit.JsPromptResult import android.webkit.JsResult @@ -54,6 +55,11 @@ class AuthorizingPaymentActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + if (intent.getBooleanExtra(OmiseActivity.EXTRA_IS_SECURE, true)) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + setContentView(R.layout.activity_authorizing_payment) supportActionBar?.title = threeDSConfig.uiCustomization?.toolbarCustomization?.headerText diff --git a/sdk/src/main/java/co/omise/android/ui/CreditCardActivity.kt b/sdk/src/main/java/co/omise/android/ui/CreditCardActivity.kt index e13897987..b92f7296b 100644 --- a/sdk/src/main/java/co/omise/android/ui/CreditCardActivity.kt +++ b/sdk/src/main/java/co/omise/android/ui/CreditCardActivity.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.os.Bundle import android.view.MenuItem import android.view.View +import android.view.WindowManager import android.widget.Button import android.widget.EditText import android.widget.ImageButton @@ -125,6 +126,10 @@ class CreditCardActivity : OmiseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (intent.getBooleanExtra(EXTRA_IS_SECURE, true)) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + setContentView(R.layout.activity_credit_card) require(intent.hasExtra(EXTRA_PKEY)) { "Could not find ${::EXTRA_PKEY.name}." } diff --git a/sdk/src/main/java/co/omise/android/ui/OmiseActivity.kt b/sdk/src/main/java/co/omise/android/ui/OmiseActivity.kt index df8d8c52a..132d45474 100644 --- a/sdk/src/main/java/co/omise/android/ui/OmiseActivity.kt +++ b/sdk/src/main/java/co/omise/android/ui/OmiseActivity.kt @@ -23,6 +23,12 @@ abstract class OmiseActivity : AppCompatActivity() { const val EXTRA_TOKEN = "OmiseActivity.token" const val EXTRA_TOKEN_OBJECT = "OmiseActivity.tokenObject" const val EXTRA_CARD_OBJECT = "OmiseActivity.cardObject" + + /** + * Applies [android.view.WindowManager.LayoutParams.FLAG_SECURE] to the activity. + * This will prevent the activity from being captured by screenshots and video recordings. + */ + const val EXTRA_IS_SECURE = "OmiseActivity.isSecure" } @VisibleForTesting diff --git a/sdk/src/main/java/co/omise/android/ui/PaymentCreatorActivity.kt b/sdk/src/main/java/co/omise/android/ui/PaymentCreatorActivity.kt index 5eabd8c17..45eb421a3 100644 --- a/sdk/src/main/java/co/omise/android/ui/PaymentCreatorActivity.kt +++ b/sdk/src/main/java/co/omise/android/ui/PaymentCreatorActivity.kt @@ -3,7 +3,7 @@ package co.omise.android.ui import android.app.Activity import android.content.Intent import android.os.Bundle -import android.util.Log +import android.view.WindowManager import androidx.annotation.VisibleForTesting import androidx.fragment.app.Fragment import co.omise.android.R @@ -18,6 +18,7 @@ import co.omise.android.ui.OmiseActivity.Companion.EXTRA_CURRENCY import co.omise.android.ui.OmiseActivity.Companion.EXTRA_GOOGLEPAY_MERCHANT_ID import co.omise.android.ui.OmiseActivity.Companion.EXTRA_GOOGLEPAY_REQUEST_BILLING_ADDRESS import co.omise.android.ui.OmiseActivity.Companion.EXTRA_GOOGLEPAY_REQUEST_PHONE_NUMBER +import co.omise.android.ui.OmiseActivity.Companion.EXTRA_IS_SECURE import co.omise.android.ui.OmiseActivity.Companion.EXTRA_PKEY import co.omise.android.ui.OmiseActivity.Companion.EXTRA_SOURCE_OBJECT import com.google.android.material.snackbar.Snackbar @@ -64,6 +65,11 @@ class PaymentCreatorActivity : OmiseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + if (intent.getBooleanExtra(EXTRA_IS_SECURE, true)) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + setContentView(R.layout.activity_payment_creator) initialize() @@ -201,6 +207,7 @@ private class PaymentCreatorNavigationImpl( override fun navigateToCreditCardForm() { val intent = Intent(activity, CreditCardActivity::class.java).apply { putExtra(EXTRA_PKEY, pkey) + putExtra(EXTRA_IS_SECURE, activity.intent.getBooleanExtra(EXTRA_IS_SECURE, true)) } activity.startActivityForResult(intent, requestCode) } diff --git a/sdk/src/sharedTest/java/co/omise/android/ui/CreditCardActivityTest.kt b/sdk/src/sharedTest/java/co/omise/android/ui/CreditCardActivityTest.kt index 378c301fe..21f40a3af 100644 --- a/sdk/src/sharedTest/java/co/omise/android/ui/CreditCardActivityTest.kt +++ b/sdk/src/sharedTest/java/co/omise/android/ui/CreditCardActivityTest.kt @@ -6,6 +6,7 @@ import android.app.Application import android.content.Intent import android.os.Bundle import android.view.View +import android.view.WindowManager import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario.launchActivityForResult @@ -42,8 +43,8 @@ import org.hamcrest.CoreMatchers.not import org.hamcrest.Matcher import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -54,7 +55,6 @@ import org.mockito.kotlin.reset import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) -@Ignore class CreditCardActivityTest { private lateinit var scenario: ActivityScenario @@ -295,10 +295,7 @@ class CreditCardActivityTest { @Test fun submitForm_disableFormWhenPressSubmit() { - whenever(mockClient.send(any(), any())).doAnswer { invocation -> - val callback = invocation.getArgument>(1) - callback.onRequestSucceed(Token()) - } + whenever(mockClient.send(any(), any())).doAnswer {} onView(withId(R.id.edit_card_number)).perform(typeText("4242424242424242")) onView(withId(R.id.edit_card_name)).perform(typeText("John Doe")) onView(withId(R.id.edit_expiry_date)).perform(typeText("1234")) @@ -386,6 +383,23 @@ class CreditCardActivityTest { val result = scenario.result assertEquals(RESULT_CANCELED, result.resultCode) } + + @Test + fun flagSecure_whenParameterIsFalseThenAttributesMustNotContainFlagSecure() { + intent.putExtra(OmiseActivity.EXTRA_IS_SECURE, false) + val scenario = ActivityScenario.launchActivityForResult(intent) + scenario.onActivity { + assertNotEquals(WindowManager.LayoutParams.FLAG_SECURE, it.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) + } + } + + @Test + fun flagSecure_whenParameterNotSetThenAttributesMustContainFlagSecure() { + val scenario = ActivityScenario.launchActivityForResult(intent) + scenario.onActivity { + assertEquals(WindowManager.LayoutParams.FLAG_SECURE, it.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) + } + } } private fun typeNumberText(numberText: String): ViewAction = diff --git a/sdk/src/sharedTest/java/co/omise/android/ui/PaymentCreatorActivityTest.kt b/sdk/src/sharedTest/java/co/omise/android/ui/PaymentCreatorActivityTest.kt index 572b6717e..930dd8d7f 100644 --- a/sdk/src/sharedTest/java/co/omise/android/ui/PaymentCreatorActivityTest.kt +++ b/sdk/src/sharedTest/java/co/omise/android/ui/PaymentCreatorActivityTest.kt @@ -2,6 +2,7 @@ package co.omise.android.ui import android.app.Activity.RESULT_OK import android.content.Intent +import android.view.WindowManager import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView @@ -9,7 +10,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.ComponentNameMatchers.hasClassName import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.espresso.intent.rule.IntentsRule import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -17,7 +18,9 @@ import co.omise.android.R import co.omise.android.models.Capability import co.omise.android.models.Token import co.omise.android.ui.OmiseActivity.Companion.EXTRA_TOKEN +import org.junit.Assert import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -26,9 +29,8 @@ import org.junit.runner.RunWith class PaymentCreatorActivityTest { @get:Rule - val intentRule = IntentsTestRule(TestFragmentActivity::class.java) + val intentRule = IntentsRule() - private lateinit var scenario: ActivityScenario private val capability = Capability() private val intent = Intent( ApplicationProvider.getApplicationContext(), @@ -42,7 +44,7 @@ class PaymentCreatorActivityTest { @Test fun initialActivity_collectExtrasIntent() { - scenario = ActivityScenario.launch(intent) + ActivityScenario.launchActivityForResult(intent) onView(withId(R.id.payment_creator_container)).check(matches(isDisplayed())) } @@ -50,7 +52,7 @@ class PaymentCreatorActivityTest { @Test fun navigateToCreditCardForm_startCreditCartActivity() { var activity: PaymentCreatorActivity? = null - scenario = ActivityScenario.launch(intent).onActivity { + ActivityScenario.launchActivityForResult(intent).onActivity { activity = it } @@ -64,10 +66,27 @@ class PaymentCreatorActivityTest { val creditCardIntent = Intent().apply { putExtra(EXTRA_TOKEN, Token()) } - scenario = ActivityScenario.launchActivityForResult(intent).onActivity { + val scenario = ActivityScenario.launchActivityForResult(intent).onActivity { it.performActivityResult(100, RESULT_OK, creditCardIntent) } assertEquals(RESULT_OK, scenario.result.resultCode) } + + @Test + fun flagSecure_whenParameterIsFalseThenAttributesMustNotContainFlagSecure() { + intent.putExtra(OmiseActivity.EXTRA_IS_SECURE, false) + val scenario = ActivityScenario.launchActivityForResult(intent) + scenario.onActivity { + assertNotEquals(WindowManager.LayoutParams.FLAG_SECURE, it.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) + } + } + + @Test + fun flagSecure_whenParameterNotSetThenAttributesMustContainFlagSecure() { + val scenario = ActivityScenario.launchActivityForResult(intent) + scenario.onActivity { + assertEquals(WindowManager.LayoutParams.FLAG_SECURE, it.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) + } + } }