Skip to content

Commit 4cb03ff

Browse files
authored
fix(#308): Add prominent disclosure on startup requesting user to link app and domain (#354)
1 parent bfbb902 commit 4cb03ff

File tree

14 files changed

+393
-21
lines changed

14 files changed

+393
-21
lines changed

src/main/AndroidManifest.xml

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
<activity android:name="RequestSendSmsPermissionActivity"
6363
android:screenOrientation="portrait"
6464
tools:ignore="DiscouragedApi"/>
65+
<activity android:name="DomainVerificationActivity"
66+
android:screenOrientation="portrait"
67+
tools:ignore="DiscouragedApi" />
6568
<activity android:name="AppUrlIntentActivity"
6669
android:launchMode="singleInstance"
6770
android:exported="true">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.medicmobile.webapp.mobile;
2+
3+
import static org.medicmobile.webapp.mobile.MedicLog.trace;
4+
5+
import android.annotation.SuppressLint;
6+
import android.app.Activity;
7+
import android.content.Intent;
8+
import android.net.Uri;
9+
import android.os.Bundle;
10+
import android.provider.Settings;
11+
import android.view.View;
12+
import android.widget.TextView;
13+
14+
15+
public class DomainVerificationActivity extends Activity {
16+
@Override
17+
public void onCreate(Bundle savedInstanceState) {
18+
super.onCreate(savedInstanceState);
19+
trace(this, "onCreate()");
20+
21+
setContentView(R.layout.request_app_domain_association);
22+
23+
String appName = getResources().getString(R.string.app_name);
24+
String title = getResources().getString(R.string.domainAppAssociationTitle);
25+
TextView field = findViewById(R.id.domainAppAssociationTitleText);
26+
field.setText(String.format(title, appName));
27+
}
28+
29+
@SuppressLint("unused")
30+
public void onClickOk(View view) {
31+
trace(this, "DomainVerificationActivity :: User agreed with prominent disclosure message.");
32+
@SuppressLint("InlinedApi") Intent intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, Uri.parse("package:" + this.getPackageName()));
33+
this.startActivity(intent);
34+
finish();
35+
}
36+
37+
@SuppressLint("unused")
38+
public void onClickNegative(View view) {
39+
trace(this, "DomainVerificationActivity :: User disagreed with prominent disclosure message.");
40+
finish();
41+
}
42+
}

src/main/java/org/medicmobile/webapp/mobile/StartupActivity.java

+13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.medicmobile.webapp.mobile;
22

33
import android.app.Activity;
4+
import android.content.Context;
45
import android.content.Intent;
6+
import android.os.Build;
57
import android.os.Bundle;
68

79
import static org.medicmobile.webapp.mobile.MedicLog.trace;
@@ -14,6 +16,7 @@ public class StartupActivity extends Activity {
1416
super.onCreate(savedInstanceState);
1517
trace(this, "onCreate()");
1618
configureAndStartNextActivity();
19+
startDomainVerificationActivity();
1720
}
1821

1922
private void configureAndStartNextActivity() {
@@ -29,6 +32,16 @@ private void configureAndStartNextActivity() {
2932
finish();
3033
}
3134

35+
private void startDomainVerificationActivity() {
36+
Context context = getApplicationContext();
37+
38+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !Utils.checkIfDomainsAreVerified(context)) {
39+
Intent intent = new Intent(this, DomainVerificationActivity.class);
40+
startActivity(intent);
41+
finish();
42+
}
43+
}
44+
3245
private boolean hasEnoughFreeSpace() {
3346
long freeSpace = getFilesDir().getFreeSpace();
3447

src/main/java/org/medicmobile/webapp/mobile/Utils.java

+35-1
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33
import static org.medicmobile.webapp.mobile.BuildConfig.APPLICATION_ID;
44
import static org.medicmobile.webapp.mobile.BuildConfig.DEBUG;
55
import static org.medicmobile.webapp.mobile.BuildConfig.VERSION_NAME;
6+
import static org.medicmobile.webapp.mobile.MedicLog.warn;
67

78
import android.app.Activity;
89
import android.content.Context;
910
import android.content.Intent;
11+
import android.content.pm.PackageManager;
12+
import android.content.pm.verify.domain.DomainVerificationManager;
13+
import android.content.pm.verify.domain.DomainVerificationUserState;
1014
import android.net.Uri;
15+
import android.os.Build;
1116

1217
import org.json.JSONException;
1318
import org.json.JSONObject;
1419

1520
import java.io.File;
21+
import java.util.Map;
1622
import java.util.Optional;
1723

1824
final class Utils {
@@ -81,7 +87,7 @@ static String createUseragentFrom(String current) {
8187
if(current.contains(APPLICATION_ID)) return current;
8288

8389
return String.format("%s %s/%s",
84-
current, APPLICATION_ID, VERSION_NAME);
90+
current, APPLICATION_ID, VERSION_NAME);
8591
}
8692

8793
static void restartApp(Context context) {
@@ -117,4 +123,32 @@ static Optional<Uri> getUriFromFilePath(String path) {
117123
static boolean isDebug() {
118124
return DEBUG;
119125
}
126+
127+
static boolean checkIfDomainsAreVerified(Context context) {
128+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
129+
return true;
130+
}
131+
132+
DomainVerificationManager manager =
133+
context.getSystemService(DomainVerificationManager.class);
134+
try {
135+
DomainVerificationUserState userState =
136+
manager.getDomainVerificationUserState(context.getPackageName());
137+
138+
return areAllDomainsVerifiedOrSelected(userState.getHostToStateMap());
139+
} catch (PackageManager.NameNotFoundException e) {
140+
warn(e, "Error while getting package name");
141+
}
142+
return true;
143+
}
144+
145+
private static boolean areAllDomainsVerifiedOrSelected(Map<String, Integer> hostToStateMap) {
146+
for (int stateValue : hostToStateMap.values()) {
147+
if (stateValue != DomainVerificationUserState.DOMAIN_STATE_VERIFIED &&
148+
stateValue != DomainVerificationUserState.DOMAIN_STATE_SELECTED) {
149+
return false;
150+
}
151+
}
152+
return true;
153+
}
120154
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
4+
android:layout_width="fill_parent"
5+
android:layout_height="wrap_content"
6+
android:orientation="vertical"
7+
android:paddingLeft="16dp"
8+
android:paddingRight="16dp">
9+
10+
<TextView
11+
android:id="@+id/domainAppAssociationTitleText"
12+
style="@android:style/Widget.DeviceDefault.Light.TextView"
13+
android:layout_width="match_parent"
14+
android:layout_height="wrap_content"
15+
android:layout_marginTop="100dp"
16+
android:gravity="center"
17+
android:padding="10dp"
18+
android:text="@string/domainAppAssociationTitle"
19+
android:textSize="25sp"
20+
android:textStyle="bold" />
21+
22+
<TextView
23+
android:id="@+id/domainAppAssociationMessageText"
24+
android:layout_width="wrap_content"
25+
android:layout_height="wrap_content"
26+
android:layout_below="@+id/domainAppAssociationTitleText"
27+
android:layout_marginTop="10dp"
28+
android:gravity="center"
29+
android:padding="20dp"
30+
android:text="@string/domainAppAssociationMessage"
31+
android:textSize="18sp" />
32+
33+
<LinearLayout
34+
android:id="@+id/domainAppAssociationButtons"
35+
android:layout_width="match_parent"
36+
android:layout_height="wrap_content"
37+
android:layout_alignParentEnd="true"
38+
android:layout_alignParentBottom="true"
39+
android:paddingLeft="20dp"
40+
android:paddingRight="20dp"
41+
android:paddingBottom="20dp">
42+
43+
<Button
44+
android:id="@+id/domainAppAssociationNegativeButton"
45+
style="@style/borderlessButton"
46+
android:onClick="onClickNegative"
47+
android:text="@string/domainAppAssociationRequestDenyButton"
48+
tools:ignore="OnClick"
49+
android:layout_marginTop="10dp"/>
50+
51+
<View
52+
android:layout_width="0dp"
53+
android:layout_height="0dp"
54+
android:layout_weight="1" />
55+
56+
<Button
57+
android:id="@+id/domainAppAssociationOkButton"
58+
style="@style/standardButton"
59+
android:layout_width="137dp"
60+
android:background="@android:color/holo_blue_dark"
61+
android:onClick="onClickOk"
62+
android:text="@string/domainAppAssociationRequestOkButton"
63+
android:textColor="#ffffff"
64+
tools:ignore="OnClick" />
65+
66+
</LinearLayout>
67+
68+
</RelativeLayout>

src/main/res/values-es/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,10 @@
2828
<string name="connErrorMessage">Parece que no estás conectado a Internet. Por favor comprueba tu conexión y vuelve a intentar.</string>
2929

3030
<string name="waitMigration">Por favor espere a que el proceso de migración haya finalizado</string>
31+
32+
<string name="domainAppAssociationTitle">¿Vincular dominio con %s?</string>
33+
<string name="domainAppAssociationMessage">Esto garantiza que todos los enlaces se abrirán en su aplicación para mejor experiencia</string>
34+
<string name="domainAppAssociationIconDescription">Icono del dominio</string>
35+
<string name="domainAppAssociationRequestOkButton">Sí, vincule dominio</string>
36+
<string name="domainAppAssociationRequestDenyButton">No vincular</string>
3137
</resources>

src/main/res/values-fr/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,10 @@
2929

3030
<string name="waitMigration">Veuillez attendre que la migration de données termine</string>
3131
<string name="usingTrainingApp">Utiliser uniquement pendant la formation</string>
32+
33+
<string name="domainAppAssociationTitle">Associer le domaine à %s ?</string>
34+
<string name="domainAppAssociationMessage">Cela garantit que tous les liens s\'ouvriront dans votre application pour une meilleure expérience.</string>
35+
<string name="domainAppAssociationIconDescription">Icône de domaine</string>
36+
<string name="domainAppAssociationRequestOkButton">Oui, lier le domaine</string>
37+
<string name="domainAppAssociationRequestDenyButton">Non, ne pas lier</string>
3238
</resources>

src/main/res/values-hi/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@
55
<string name="locRequestOkButton">चालू करो</string>
66
<string name="locRequestDenyButton">जी नहीं, धन्यवाद</string>
77
<string name="locRequestIconDescription">मेरा स्थान आइकन</string>
8+
9+
<string name="domainAppAssociationTitle">डोमेन को %s से लिंक करें?</string>
10+
<string name="domainAppAssociationMessage">यह सुनिश्चित करता है कि आपके ऐप में सभी लिंक खुलेंगे एक बेहतर अनुभव के लिए</string>
11+
<string name="domainAppAssociationIconDescription">डोमेन का चिह्न</string>
12+
<string name="domainAppAssociationRequestOkButton">हां, डोमेन लिंक करें</string>
13+
<string name="domainAppAssociationRequestDenyButton">लिंक न करें</string>
814
</resources>

src/main/res/values-in/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@
55
<string name="locRequestOkButton">Nyalakan</string>
66
<string name="locRequestDenyButton">Tidak, terima kasih</string>
77
<string name="locRequestIconDescription">Ikon lokasiku</string>
8+
9+
<string name="domainAppAssociationTitle">Tautkan domain dengan %s?</string>
10+
<string name="domainAppAssociationMessage">Ini memastikan semua tautan akan terbuka di aplikasi Anda untuk pengalaman yang lebih baik.</string>
11+
<string name="domainAppAssociationIconDescription">Ikon Domain</string>
12+
<string name="domainAppAssociationRequestOkButton">Ya, tautkan domain</string>
13+
<string name="domainAppAssociationRequestDenyButton">Jangan tautkan</string>
814
</resources>

src/main/res/values-ne/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@
55
<string name="locRequestOkButton">खोल्नुहोस्</string>
66
<string name="locRequestDenyButton">हुँदैन, धन्यबाद</string>
77
<string name="locRequestIconDescription">मेरो स्थान आइकन</string>
8+
9+
<string name="domainAppAssociationTitle">%s डोमेन लिंक गर्नुहुन्छ?</string>
10+
<string name="domainAppAssociationMessage">यसले राम्रो अनुभवको लागि तपाइँको एपमा सबै लिङ्कहरू खुल्ने सुनिश्चित गर्दछ।</string>
11+
<string name="domainAppAssociationIconDescription">डोमेन चिन्ह</string>
12+
<string name="domainAppAssociationRequestOkButton">हुन्छ, डोमेन लिङ्क गर्नुहोस्</string>
13+
<string name="domainAppAssociationRequestDenyButton">हुदैन</string>
814
</resources>

src/main/res/values-tl/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@
55
<string name="locRequestOkButton">Buksan</string>
66
<string name="locRequestDenyButton">Hindi na, Salamat na lang</string>
77
<string name="locRequestIconDescription">Ang icon ng lokasyon ko</string>
8+
9+
<string name="domainAppAssociationTitle">I-link ang domain sa %s?</string>
10+
<string name="domainAppAssociationMessage">Tinitiyak nito na magbubukas ang lahat ng link sa iyong app para sa mas magandang karanasan.</string>
11+
<string name="domainAppAssociationIconDescription">Icon ng Domain</string>
12+
<string name="domainAppAssociationRequestOkButton">Oo, i-link ang domain</string>
13+
<string name="domainAppAssociationRequestDenyButton">Huwag i-link</string>
814
</resources>

src/main/res/values/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@
5757

5858
<string name="waitMigration">Please wait until the migration process is finished</string>
5959
<string name="usingTrainingApp">Use only during training</string>
60+
61+
<string name="domainAppAssociationTitle">Link domain with %s?</string>
62+
<string name="domainAppAssociationMessage">This ensures all links will open in your app for a better experience.</string>
63+
<string name="domainAppAssociationIconDescription">Domain Icon</string>
64+
<string name="domainAppAssociationRequestOkButton">Yes, link domain</string>
65+
<string name="domainAppAssociationRequestDenyButton">Don\'t link</string>
6066
</resources>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.medicmobile.webapp.mobile;
2+
3+
import static android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS;
4+
import static org.junit.Assert.assertEquals;
5+
import static org.junit.Assert.assertNotNull;
6+
import static org.mockito.ArgumentMatchers.any;
7+
import static org.mockito.ArgumentMatchers.eq;
8+
import static org.mockito.Mockito.mockStatic;
9+
import static org.robolectric.Shadows.shadowOf;
10+
11+
import android.app.Activity;
12+
import android.content.Intent;
13+
14+
import androidx.lifecycle.Lifecycle;
15+
import androidx.test.core.app.ActivityScenario;
16+
17+
import org.junit.Test;
18+
import org.junit.runner.RunWith;
19+
import org.mockito.MockedStatic;
20+
import org.robolectric.RobolectricTestRunner;
21+
import org.robolectric.shadows.ShadowActivity;
22+
23+
@RunWith(RobolectricTestRunner.class)
24+
public class DomainVerificationActivityTest {
25+
@Test
26+
public void onClickOk_withSdkVersionGreaterThanS_startAppOpenByDefaultSettings() {
27+
try (
28+
MockedStatic<MedicLog> medicLogMock = mockStatic(MedicLog.class);
29+
ActivityScenario<DomainVerificationActivity> scenario = ActivityScenario.launch(DomainVerificationActivity.class)
30+
) {
31+
scenario.onActivity(domainVerificationActivity -> {
32+
ShadowActivity shadowActivity = shadowOf(domainVerificationActivity);
33+
34+
domainVerificationActivity.onClickOk(null);
35+
36+
Intent startedIntent = shadowActivity.getNextStartedActivity();
37+
assertNotNull(startedIntent);
38+
assertEquals(ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, startedIntent.getAction());
39+
assertEquals("package:" + domainVerificationActivity.getPackageName(), startedIntent.getData().toString());
40+
41+
medicLogMock.verify(() -> MedicLog.trace(
42+
any(DomainVerificationActivity.class),
43+
eq("DomainVerificationActivity :: User agreed with prominent disclosure message.")
44+
));
45+
});
46+
47+
scenario.moveToState(Lifecycle.State.DESTROYED);
48+
}
49+
}
50+
51+
@Test
52+
public void onClickNegative_finishActivity() {
53+
try (
54+
MockedStatic<MedicLog> medicLogMock = mockStatic(MedicLog.class);
55+
ActivityScenario<DomainVerificationActivity> scenario = ActivityScenario.launchActivityForResult(DomainVerificationActivity.class)
56+
) {
57+
scenario.onActivity(domainVerificationActivity -> {
58+
domainVerificationActivity.onClickNegative(null);
59+
60+
assertEquals(Activity.RESULT_CANCELED, scenario.getResult().getResultCode());
61+
62+
medicLogMock.verify(() -> MedicLog.trace(
63+
any(DomainVerificationActivity.class),
64+
eq("DomainVerificationActivity :: User disagreed with prominent disclosure message.")
65+
));
66+
});
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)