Skip to content

Commit

Permalink
fix(#308): Add prominent disclosure on startup requesting user to lin…
Browse files Browse the repository at this point in the history
…k app and domain (#354)
  • Loading branch information
sugat009 authored Apr 25, 2024
1 parent bfbb902 commit 4cb03ff
Show file tree
Hide file tree
Showing 14 changed files with 393 additions and 21 deletions.
3 changes: 3 additions & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
<activity android:name="RequestSendSmsPermissionActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi"/>
<activity android:name="DomainVerificationActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi" />
<activity android:name="AppUrlIntentActivity"

Check warning

Code scanning / SonarCloud

Exported component access should be restricted with appropriate permissions Medium

Implement permissions on this exported component. See more on SonarCloud
android:launchMode="singleInstance"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.medicmobile.webapp.mobile;

import static org.medicmobile.webapp.mobile.MedicLog.trace;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.TextView;


public class DomainVerificationActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trace(this, "onCreate()");

setContentView(R.layout.request_app_domain_association);

String appName = getResources().getString(R.string.app_name);
String title = getResources().getString(R.string.domainAppAssociationTitle);
TextView field = findViewById(R.id.domainAppAssociationTitleText);
field.setText(String.format(title, appName));
}

@SuppressLint("unused")
public void onClickOk(View view) {
trace(this, "DomainVerificationActivity :: User agreed with prominent disclosure message.");
@SuppressLint("InlinedApi") Intent intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, Uri.parse("package:" + this.getPackageName()));
this.startActivity(intent);
finish();
}

@SuppressLint("unused")
public void onClickNegative(View view) {
trace(this, "DomainVerificationActivity :: User disagreed with prominent disclosure message.");
finish();
}
}
13 changes: 13 additions & 0 deletions src/main/java/org/medicmobile/webapp/mobile/StartupActivity.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.medicmobile.webapp.mobile;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;

import static org.medicmobile.webapp.mobile.MedicLog.trace;
Expand All @@ -14,6 +16,7 @@ public class StartupActivity extends Activity {
super.onCreate(savedInstanceState);
trace(this, "onCreate()");
configureAndStartNextActivity();
startDomainVerificationActivity();
}

private void configureAndStartNextActivity() {
Expand All @@ -29,6 +32,16 @@ private void configureAndStartNextActivity() {
finish();
}

private void startDomainVerificationActivity() {
Context context = getApplicationContext();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !Utils.checkIfDomainsAreVerified(context)) {
Intent intent = new Intent(this, DomainVerificationActivity.class);
startActivity(intent);
finish();
}
}

private boolean hasEnoughFreeSpace() {
long freeSpace = getFilesDir().getFreeSpace();

Expand Down
36 changes: 35 additions & 1 deletion src/main/java/org/medicmobile/webapp/mobile/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
import static org.medicmobile.webapp.mobile.BuildConfig.APPLICATION_ID;
import static org.medicmobile.webapp.mobile.BuildConfig.DEBUG;
import static org.medicmobile.webapp.mobile.BuildConfig.VERSION_NAME;
import static org.medicmobile.webapp.mobile.MedicLog.warn;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationUserState;
import android.net.Uri;
import android.os.Build;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.util.Map;
import java.util.Optional;

final class Utils {
Expand Down Expand Up @@ -81,7 +87,7 @@ static String createUseragentFrom(String current) {
if(current.contains(APPLICATION_ID)) return current;

return String.format("%s %s/%s",
current, APPLICATION_ID, VERSION_NAME);
current, APPLICATION_ID, VERSION_NAME);
}

static void restartApp(Context context) {
Expand Down Expand Up @@ -117,4 +123,32 @@ static Optional<Uri> getUriFromFilePath(String path) {
static boolean isDebug() {
return DEBUG;
}

static boolean checkIfDomainsAreVerified(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return true;
}

DomainVerificationManager manager =
context.getSystemService(DomainVerificationManager.class);
try {
DomainVerificationUserState userState =
manager.getDomainVerificationUserState(context.getPackageName());

return areAllDomainsVerifiedOrSelected(userState.getHostToStateMap());
} catch (PackageManager.NameNotFoundException e) {
warn(e, "Error while getting package name");
}
return true;
}

private static boolean areAllDomainsVerifiedOrSelected(Map<String, Integer> hostToStateMap) {
for (int stateValue : hostToStateMap.values()) {
if (stateValue != DomainVerificationUserState.DOMAIN_STATE_VERIFIED &&
stateValue != DomainVerificationUserState.DOMAIN_STATE_SELECTED) {
return false;
}
}
return true;
}
}
68 changes: 68 additions & 0 deletions src/main/res/layout/request_app_domain_association.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp">

<TextView
android:id="@+id/domainAppAssociationTitleText"
style="@android:style/Widget.DeviceDefault.Light.TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:gravity="center"
android:padding="10dp"
android:text="@string/domainAppAssociationTitle"
android:textSize="25sp"
android:textStyle="bold" />

<TextView
android:id="@+id/domainAppAssociationMessageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/domainAppAssociationTitleText"
android:layout_marginTop="10dp"
android:gravity="center"
android:padding="20dp"
android:text="@string/domainAppAssociationMessage"
android:textSize="18sp" />

<LinearLayout
android:id="@+id/domainAppAssociationButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingBottom="20dp">

<Button
android:id="@+id/domainAppAssociationNegativeButton"
style="@style/borderlessButton"
android:onClick="onClickNegative"
android:text="@string/domainAppAssociationRequestDenyButton"
tools:ignore="OnClick"
android:layout_marginTop="10dp"/>

<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />

<Button
android:id="@+id/domainAppAssociationOkButton"
style="@style/standardButton"
android:layout_width="137dp"
android:background="@android:color/holo_blue_dark"
android:onClick="onClickOk"
android:text="@string/domainAppAssociationRequestOkButton"
android:textColor="#ffffff"
tools:ignore="OnClick" />

</LinearLayout>

</RelativeLayout>
6 changes: 6 additions & 0 deletions src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@
<string name="connErrorMessage">Parece que no estás conectado a Internet. Por favor comprueba tu conexión y vuelve a intentar.</string>

<string name="waitMigration">Por favor espere a que el proceso de migración haya finalizado</string>

<string name="domainAppAssociationTitle">¿Vincular dominio con %s?</string>
<string name="domainAppAssociationMessage">Esto garantiza que todos los enlaces se abrirán en su aplicación para mejor experiencia</string>
<string name="domainAppAssociationIconDescription">Icono del dominio</string>
<string name="domainAppAssociationRequestOkButton">Sí, vincule dominio</string>
<string name="domainAppAssociationRequestDenyButton">No vincular</string>
</resources>
6 changes: 6 additions & 0 deletions src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@

<string name="waitMigration">Veuillez attendre que la migration de données termine</string>
<string name="usingTrainingApp">Utiliser uniquement pendant la formation</string>

<string name="domainAppAssociationTitle">Associer le domaine à %s ?</string>
<string name="domainAppAssociationMessage">Cela garantit que tous les liens s\'ouvriront dans votre application pour une meilleure expérience.</string>
<string name="domainAppAssociationIconDescription">Icône de domaine</string>
<string name="domainAppAssociationRequestOkButton">Oui, lier le domaine</string>
<string name="domainAppAssociationRequestDenyButton">Non, ne pas lier</string>
</resources>
6 changes: 6 additions & 0 deletions src/main/res/values-hi/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@
<string name="locRequestOkButton">चालू करो</string>
<string name="locRequestDenyButton">जी नहीं, धन्यवाद</string>
<string name="locRequestIconDescription">मेरा स्थान आइकन</string>

<string name="domainAppAssociationTitle">डोमेन को %s से लिंक करें?</string>
<string name="domainAppAssociationMessage">यह सुनिश्चित करता है कि आपके ऐप में सभी लिंक खुलेंगे एक बेहतर अनुभव के लिए</string>
<string name="domainAppAssociationIconDescription">डोमेन का चिह्न</string>
<string name="domainAppAssociationRequestOkButton">हां, डोमेन लिंक करें</string>
<string name="domainAppAssociationRequestDenyButton">लिंक न करें</string>
</resources>
6 changes: 6 additions & 0 deletions src/main/res/values-in/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@
<string name="locRequestOkButton">Nyalakan</string>
<string name="locRequestDenyButton">Tidak, terima kasih</string>
<string name="locRequestIconDescription">Ikon lokasiku</string>

<string name="domainAppAssociationTitle">Tautkan domain dengan %s?</string>
<string name="domainAppAssociationMessage">Ini memastikan semua tautan akan terbuka di aplikasi Anda untuk pengalaman yang lebih baik.</string>
<string name="domainAppAssociationIconDescription">Ikon Domain</string>
<string name="domainAppAssociationRequestOkButton">Ya, tautkan domain</string>
<string name="domainAppAssociationRequestDenyButton">Jangan tautkan</string>
</resources>
6 changes: 6 additions & 0 deletions src/main/res/values-ne/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@
<string name="locRequestOkButton">खोल्नुहोस्</string>
<string name="locRequestDenyButton">हुँदैन, धन्यबाद</string>
<string name="locRequestIconDescription">मेरो स्थान आइकन</string>

<string name="domainAppAssociationTitle">%s डोमेन लिंक गर्नुहुन्छ?</string>
<string name="domainAppAssociationMessage">यसले राम्रो अनुभवको लागि तपाइँको एपमा सबै लिङ्कहरू खुल्ने सुनिश्चित गर्दछ।</string>
<string name="domainAppAssociationIconDescription">डोमेन चिन्ह</string>
<string name="domainAppAssociationRequestOkButton">हुन्छ, डोमेन लिङ्क गर्नुहोस्</string>
<string name="domainAppAssociationRequestDenyButton">हुदैन</string>
</resources>
6 changes: 6 additions & 0 deletions src/main/res/values-tl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@
<string name="locRequestOkButton">Buksan</string>
<string name="locRequestDenyButton">Hindi na, Salamat na lang</string>
<string name="locRequestIconDescription">Ang icon ng lokasyon ko</string>

<string name="domainAppAssociationTitle">I-link ang domain sa %s?</string>
<string name="domainAppAssociationMessage">Tinitiyak nito na magbubukas ang lahat ng link sa iyong app para sa mas magandang karanasan.</string>
<string name="domainAppAssociationIconDescription">Icon ng Domain</string>
<string name="domainAppAssociationRequestOkButton">Oo, i-link ang domain</string>
<string name="domainAppAssociationRequestDenyButton">Huwag i-link</string>
</resources>
6 changes: 6 additions & 0 deletions src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,10 @@

<string name="waitMigration">Please wait until the migration process is finished</string>
<string name="usingTrainingApp">Use only during training</string>

<string name="domainAppAssociationTitle">Link domain with %s?</string>
<string name="domainAppAssociationMessage">This ensures all links will open in your app for a better experience.</string>
<string name="domainAppAssociationIconDescription">Domain Icon</string>
<string name="domainAppAssociationRequestOkButton">Yes, link domain</string>
<string name="domainAppAssociationRequestDenyButton">Don\'t link</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.medicmobile.webapp.mobile;

import static android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.robolectric.Shadows.shadowOf;

import android.app.Activity;
import android.content.Intent;

import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ActivityScenario;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockedStatic;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowActivity;

@RunWith(RobolectricTestRunner.class)
public class DomainVerificationActivityTest {
@Test
public void onClickOk_withSdkVersionGreaterThanS_startAppOpenByDefaultSettings() {
try (
MockedStatic<MedicLog> medicLogMock = mockStatic(MedicLog.class);
ActivityScenario<DomainVerificationActivity> scenario = ActivityScenario.launch(DomainVerificationActivity.class)
) {
scenario.onActivity(domainVerificationActivity -> {
ShadowActivity shadowActivity = shadowOf(domainVerificationActivity);

domainVerificationActivity.onClickOk(null);

Intent startedIntent = shadowActivity.getNextStartedActivity();
assertNotNull(startedIntent);
assertEquals(ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, startedIntent.getAction());
assertEquals("package:" + domainVerificationActivity.getPackageName(), startedIntent.getData().toString());

medicLogMock.verify(() -> MedicLog.trace(
any(DomainVerificationActivity.class),
eq("DomainVerificationActivity :: User agreed with prominent disclosure message.")
));
});

scenario.moveToState(Lifecycle.State.DESTROYED);
}
}

@Test
public void onClickNegative_finishActivity() {
try (
MockedStatic<MedicLog> medicLogMock = mockStatic(MedicLog.class);
ActivityScenario<DomainVerificationActivity> scenario = ActivityScenario.launchActivityForResult(DomainVerificationActivity.class)
) {
scenario.onActivity(domainVerificationActivity -> {
domainVerificationActivity.onClickNegative(null);

assertEquals(Activity.RESULT_CANCELED, scenario.getResult().getResultCode());

medicLogMock.verify(() -> MedicLog.trace(
any(DomainVerificationActivity.class),
eq("DomainVerificationActivity :: User disagreed with prominent disclosure message.")
));
});
}
}
}
Loading

0 comments on commit 4cb03ff

Please sign in to comment.