Skip to content

Commit 58557bd

Browse files
karimkkanjim5rmrjones-plipbinokaryg
authored
feat(#362): android 15 support (#407)
* uplift `targetSdkVersion` to 35 (Android 15) * android 15 security flag "Block apps that don't match the top UID on the stack from launching activities" * compile app using android 15 preview sdk * try handling overlaps with window insets * fix `java.lang.RuntimeException: Unable to start activity ComponentInfo{org.medicmobile.webapp.mobile/org.medicmobile.webapp.mobile.EmbeddedBrowserActivity}: android.app.Fragment$InstantiationException: Unable to instantiate fragment org.medicmobile.webapp.mobile.OpenSettingsDialogFragment: could not find Fragment constructor` * apply insets margins to layout instead of individual elements * android:fitsSystemWindows="true" * simplify * UPD: add echis.go.ke domains to app * UPD: add echis urls to prod app * UPD: add aab files in artifacts * UPD: add version number * UPD: add version number * UPD: add version number * UPD: add keystore data * UPD: add keystore data * UPD: add build pipelines * chore(#362): bump gradle API to support Android 15 and 16 * bump actions/cache to v4 per CI failing * UPD: target sdk 36 * chore(#362): target API 35 (Android 15) instead of 36 We use Robolectric which supports up to API level 35 * chore(#362): Bump robolectric and junit versions to latest * UPD: add training branch * UPD: add chis kenya for training * UPD: restore working gradle versions, AGP to be updated later * UPD: update tests * UPD: add support for android 15 to production apps * Update .gitlab-ci.yml file for prod * UPD: remove gitlab ci * UPD: removed commented out code lines * UPD: only add file in master * UPD: only add file in master * UPD: only add file in master * UPD: only add file in master * UPD: remove chis_kenya specific files * UPD: remove chis_kenya specific files * CHORE: remove commented out code * CHORE: removed unused imports --------- Co-authored-by: m5r <mokht@rmi.al> Co-authored-by: mrjones-plip <mrjones-plip@plip.com> Co-authored-by: Binod Adhikary <adhikary@medicmobile.org>
1 parent 4e10c44 commit 58557bd

File tree

9 files changed

+94
-65
lines changed

9 files changed

+94
-65
lines changed

build.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def getVersionName = {
101101
}
102102

103103
android {
104-
compileSdk 34
104+
compileSdk 35
105105
packagingOptions {
106106
resources {
107107
excludes += ['META-INF/LICENSE', 'META-INF/NOTICE']
@@ -114,7 +114,7 @@ android {
114114

115115
defaultConfig {
116116
//noinspection OldTargetApi
117-
targetSdkVersion 34
117+
targetSdkVersion 35
118118
minSdkVersion 21 // Android 5.0
119119
versionCode getVersionCode()
120120
versionName getVersionName()
@@ -483,10 +483,10 @@ dependencies {
483483
testImplementation 'junit:junit:4.13.2'
484484
testImplementation 'org.mockito:mockito-inline:5.2.0'
485485
testImplementation 'com.google.android:android-test:4.1.1.4'
486-
testImplementation 'org.robolectric:robolectric:4.12.1'
486+
testImplementation 'org.robolectric:robolectric:4.15.1'
487487
testImplementation 'androidx.test.espresso:espresso-core:3.5.1'
488488
testImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
489-
testImplementation 'androidx.test.ext:junit:1.1.5'
489+
testImplementation 'androidx.test.ext:junit:1.2.1'
490490
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
491491
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
492492
androidTestImplementation 'androidx.test:runner:1.5.2'

src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<application android:label="@string/app_name"
2929
android:icon="@mipmap/ic_launcher"
3030
android:allowBackup="false"
31+
android:allowCrossUidActivitySwitchFromBelow="false"
3132
android:fullBackupContent="@xml/backup_rules_sdk_30_and_lower"
3233
android:dataExtractionRules="@xml/backup_rules"
3334
android:largeHeap="true"

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import android.content.IntentFilter;
2222
import android.net.ConnectivityManager;
2323
import android.net.Uri;
24+
import android.os.Build;
2425
import android.os.Bundle;
2526
import android.view.View;
2627
import android.view.Window;
@@ -33,6 +34,7 @@
3334
import android.widget.Toast;
3435

3536
import androidx.core.content.ContextCompat;
37+
import androidx.core.view.ViewCompat;
3638

3739
import java.util.Arrays;
3840
import java.util.Optional;
@@ -61,7 +63,7 @@ public void onReceiveValue(String result) {
6163
};
6264

6365

64-
//> ACTIVITY LIFECYCLE METHODS
66+
//> ACTIVITY LIFECYCLE METHODS
6567
@SuppressLint("ClickableViewAccessibility")
6668
@Override public void onCreate(Bundle savedInstanceState) {
6769
super.onCreate(savedInstanceState);
@@ -83,23 +85,26 @@ public void onReceiveValue(String result) {
8385

8486
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
8587
setContentView(R.layout.main);
88+
View webviewContainer = findViewById(R.id.lytWebView);
89+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
90+
ViewCompat.requestApplyInsets(webviewContainer.getRootView());
91+
}
8692

8793
// Add an alarming red border if using configurable (i.e. dev)
8894
// app with a medic production server.
89-
if (settings.allowCustomHosts() && appUrl != null && appUrl.contains("app.medicmobile.org")) {
90-
View webviewContainer = findViewById(R.id.lytWebView);
95+
if (settings.allowsConfiguration() && appUrl != null && appUrl.contains("app.medicmobile.org")) {
9196
webviewContainer.setPadding(10, 10, 10, 10);
9297
webviewContainer.setBackgroundResource(R.drawable.warning_background);
9398
}
9499

95100
// Add a noticeable border to easily identify a training app
96101
if (BuildConfig.IS_TRAINING_APP) {
97-
View webviewContainer = findViewById(R.id.lytWebView);
98102
webviewContainer.setPadding(10, 10, 10, 10);
99103
webviewContainer.setBackgroundResource(R.drawable.training_background);
100104
}
101105

102106
container = findViewById(R.id.wbvMain);
107+
103108
getFragmentManager()
104109
.beginTransaction()
105110
.add(new OpenSettingsDialogFragment(), OpenSettingsDialogFragment.class.getName())
@@ -171,8 +176,8 @@ protected void onStop() {
171176
@Override public void onBackPressed() {
172177
trace(this, "onBackPressed()");
173178
container.evaluateJavascript(
174-
"angular.element(document.body).injector().get('AndroidApi').v1.back()",
175-
backButtonHandler);
179+
"angular.element(document.body).injector().get('AndroidApi').v1.back()",
180+
backButtonHandler);
176181
}
177182

178183
@Override
@@ -218,7 +223,7 @@ protected void onActivityResult(int requestCd, int resultCode, Intent intent) {
218223
}
219224
}
220225

221-
//> ACCESSORS
226+
//> ACCESSORS
222227
MrdtSupport getMrdtSupport() {
223228
return this.mrdt;
224229
}
@@ -231,7 +236,7 @@ ChtExternalAppHandler getChtExternalAppHandler() {
231236
return this.chtExternalAppHandler;
232237
}
233238

234-
//> PUBLIC API
239+
//> PUBLIC API
235240
public void evaluateJavascript(final String js) {
236241
evaluateJavascript(js, true);
237242
}
@@ -278,7 +283,7 @@ public boolean getLocationPermissions() {
278283
return false;
279284
}
280285

281-
//> PRIVATE HELPERS
286+
//> PRIVATE HELPERS
282287
private void locationRequestResolved() {
283288
evaluateJavascript("window.CHTCore.AndroidApi.v1.locationPermissionRequestResolved();");
284289
}
@@ -403,7 +408,7 @@ private void registerRetryConnectionBroadcastReceiver() {
403408
);
404409
}
405410

406-
//> ENUMS
411+
//> ENUMS
407412
public enum RequestCode {
408413
ACCESS_LOCATION_PERMISSION(100),
409414
ACCESS_STORAGE_PERMISSION(101),

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
import java.time.Clock;
1515

16+
@SuppressLint("ValidFragment")
1617
public class OpenSettingsDialogFragment extends Fragment {
1718

19+
private View view;
1820
private int fingerTapCount = 0;
1921
private long lastTimeTap = 0;
2022
private GestureHandler swipeGesture;
@@ -31,11 +33,10 @@ public boolean onTouch(View view, MotionEvent event) {
3133
};
3234

3335
@Override
34-
public void onCreate(@Nullable Bundle savedInstanceState) {
35-
super.onCreate(savedInstanceState);
36-
setRetainInstance(true);
37-
View view = getActivity().findViewById(R.id.wbvMain);
38-
view.setOnTouchListener(onTouchListener);
36+
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
37+
super.onViewCreated(view, savedInstanceState);
38+
this.view = view.findViewById(R.id.wbvMain);
39+
this.view.setOnTouchListener(onTouchListener);
3940
}
4041

4142
private void countTaps(MotionEvent event) {

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import android.content.SharedPreferences;
1111
import android.content.res.Resources;
1212
import android.content.res.XmlResourceParser;
13+
import android.os.Build;
1314
import android.os.Bundle;
1415
import android.view.View;
1516
import android.widget.AdapterView;
@@ -19,6 +20,7 @@
1920
import android.widget.ListView;
2021
import android.widget.TextView;
2122

23+
import androidx.core.view.ViewCompat;
2224
import androidx.fragment.app.FragmentActivity;
2325

2426
import org.medicmobile.webapp.mobile.adapters.FilterableListAdapter;
@@ -59,11 +61,14 @@ private void displayServerSelectList() {
5961
state = STATE_LIST;
6062

6163
setContentView(R.layout.server_select_list);
62-
63-
ListView list = findViewById(R.id.lstServers);
64+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
65+
View view = findViewById(R.id.serverSelectListLayout);
66+
ViewCompat.requestApplyInsets(view.getRootView());
67+
}
6468

6569
List<ServerMetadata> servers = serverRepo.getServers();
6670
ServerMetadataAdapter adapter = ServerMetadataAdapter.createInstance(this, servers);
71+
ListView list = findViewById(R.id.lstServers);
6772
list.setAdapter(adapter);
6873
list.setOnItemClickListener(new ServerClickListener(adapter));
6974

@@ -80,6 +85,10 @@ private void displayCustomServerForm() {
8085
state = STATE_FORM;
8186

8287
setContentView(R.layout.custom_server_form);
88+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
89+
View view = findViewById(R.id.customServerFormLayout);
90+
ViewCompat.requestApplyInsets(view.getRootView());
91+
}
8392

8493
if(!this.settings.hasWebappSettings()) {
8594
cancelButton().setVisibility(View.GONE);
Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,42 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<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="fill_parent"
6-
android:paddingLeft="16dp"
7-
android:paddingRight="16dp"
8-
android:orientation="vertical">
9-
<EditText android:id="@+id/txtAppUrl"
10-
android:hint="@string/txtAppUrl"
11-
android:layout_width="wrap_content"
12-
android:layout_height="wrap_content"
13-
android:inputType="textUri"
14-
android:autofillHints=""
15-
android:padding="32dp"/>
16-
<!-- OnClick is ignored because lint generates error
3+
xmlns:tools="http://schemas.android.com/tools"
4+
android:id="@+id/customServerFormLayout"
5+
android:layout_width="fill_parent"
6+
android:layout_height="fill_parent"
7+
android:fitsSystemWindows="true"
8+
android:orientation="vertical"
9+
android:paddingLeft="16dp"
10+
android:paddingRight="16dp">
11+
12+
<EditText
13+
android:id="@+id/txtAppUrl"
14+
android:layout_width="wrap_content"
15+
android:layout_height="wrap_content"
16+
android:autofillHints=""
17+
android:hint="@string/txtAppUrl"
18+
android:inputType="textUri"
19+
android:padding="32dp" />
20+
<!-- OnClick is ignored because lint generates error
1721
incorrectly. Test with future build tool versions
1822
to see if this exception can be removed -->
19-
<Button android:id="@+id/btnCancelSettings"
20-
style="@style/standardButton"
21-
android:layout_toStartOf="@+id/btnSaveSettings"
22-
android:layout_alignParentBottom="true"
23-
android:onClick="cancelSettingsEdit"
24-
android:text="@string/btnCancel"
25-
tools:ignore="OnClick" />
26-
<!-- OnClick is ignored because lint generates error
23+
<Button
24+
android:id="@+id/btnCancelSettings"
25+
style="@style/standardButton"
26+
android:layout_alignParentBottom="true"
27+
android:layout_toStartOf="@+id/btnSaveSettings"
28+
android:onClick="cancelSettingsEdit"
29+
android:text="@string/btnCancel"
30+
tools:ignore="OnClick" />
31+
<!-- OnClick is ignored because lint generates error
2732
incorrectly. Test with future build tool versions
2833
to see if this exception can be removed -->
29-
<Button android:id="@+id/btnSaveSettings"
30-
style="@style/standardButton"
31-
android:layout_alignParentEnd="true"
32-
android:layout_alignParentBottom="true"
33-
android:onClick="verifyAndSave"
34-
android:text="@string/btnSave"
35-
tools:ignore="OnClick" />
34+
<Button
35+
android:id="@+id/btnSaveSettings"
36+
style="@style/standardButton"
37+
android:layout_alignParentEnd="true"
38+
android:layout_alignParentBottom="true"
39+
android:onClick="verifyAndSave"
40+
android:text="@string/btnSave"
41+
tools:ignore="OnClick" />
3642
</RelativeLayout>

src/main/res/layout/main.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
33
android:id="@+id/lytWebView"
44
android:layout_width="fill_parent"
5-
android:layout_height="fill_parent">
5+
android:layout_height="fill_parent"
6+
android:fitsSystemWindows="true">
67
<WebView android:id="@+id/wbvMain"
78
android:layout_width="fill_parent"
89
android:layout_height="fill_parent"/>

src/main/res/layout/server_select_list.xml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:id="@+id/serverSelectListLayout"
34
android:layout_width="fill_parent"
45
android:layout_height="fill_parent"
6+
android:fitsSystemWindows="true"
57
android:orientation="vertical">
68

7-
<EditText android:id="@+id/instanceSearchBox"
8-
android:hint="@string/instancesSearch"
9+
<EditText
10+
android:id="@+id/instanceSearchBox"
911
android:layout_width="fill_parent"
1012
android:layout_height="wrap_content"
11-
android:inputType="textUri"
1213
android:autofillHints=""
13-
android:padding="32dp"/>
14+
android:hint="@string/instancesSearch"
15+
android:inputType="textUri"
16+
android:padding="32dp" />
1417

1518
<ListView
1619
android:id="@+id/lstServers"

src/test/java/org/medicmobile/webapp/mobile/OpenSettingsDialogFragmentTest.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class OpenSettingsDialogFragmentTest {
3737

3838
private OpenSettingsDialogFragment openSettingsDialogFragment;
3939
private Activity activity;
40+
private View fragmentView;
4041
private ArgumentCaptor<OnTouchListener> argsOnTouch;
4142
private ArgumentCaptor<Intent> argsStartActivity;
4243

@@ -45,21 +46,22 @@ public void setup() {
4546
activity = mock(Activity.class, RETURNS_SMART_NULLS);
4647
doNothing().when(activity).finish();
4748

48-
View view = mock(View.class);
49+
fragmentView = mock(View.class);
50+
View webView = mock(View.class);
4951
argsOnTouch = ArgumentCaptor.forClass(OnTouchListener.class);
50-
doNothing().when(view).setOnTouchListener(argsOnTouch.capture());
52+
doNothing().when(webView).setOnTouchListener(argsOnTouch.capture());
53+
when(fragmentView.findViewById(R.id.wbvMain)).thenReturn(webView);
5154

5255
MockSettings fragmentSettings = withSettings()
5356
.useConstructor()
5457
.defaultAnswer(CALLS_REAL_METHODS);
5558

5659
openSettingsDialogFragment = mock(OpenSettingsDialogFragment.class, fragmentSettings);
5760
when(openSettingsDialogFragment.getActivity()).thenReturn(activity);
58-
when(openSettingsDialogFragment.getActivity().findViewById(R.id.wbvMain)).thenReturn(view);
5961
argsStartActivity = ArgumentCaptor.forClass(Intent.class);
6062
doNothing().when(openSettingsDialogFragment).startActivity(argsStartActivity.capture());
6163

62-
openSettingsDialogFragment.onCreate(null);
64+
openSettingsDialogFragment.onViewCreated(fragmentView, null);
6365
}
6466

6567
private void tap(OnTouchListener onTouchListener, MotionEvent eventTap, int times) {
@@ -240,11 +242,11 @@ public void onCreate_setsRetainInstanceTrue_preservesStateAfterRecreation() {
240242
when(eventTap.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN);
241243

242244
// First creation
243-
openSettingsDialogFragment.onCreate(savedState);
245+
openSettingsDialogFragment.onViewCreated(fragmentView, savedState);
244246
OnTouchListener firstListener = argsOnTouch.getValue();
247+
245248
// Set up initial clock mock
246249
Clock initialTime = Clock.fixed(Instant.ofEpochMilli(1000), ZoneOffset.UTC);
247-
// Update clock time for subsequent taps
248250
Clock laterTime = Clock.fixed(Instant.ofEpochMilli(1200), ZoneOffset.UTC);
249251

250252
try (MockedStatic<Clock> mockClock = mockStatic(Clock.class)) {
@@ -255,14 +257,15 @@ public void onCreate_setsRetainInstanceTrue_preservesStateAfterRecreation() {
255257

256258
// Simulate fragment recreation (e.g., due to configuration change)
257259
Activity newActivity = mock(Activity.class, RETURNS_SMART_NULLS);
258-
View newView = mock(View.class);
260+
View newFragmentView = mock(View.class);
261+
View newWebView = mock(View.class);
259262
ArgumentCaptor<OnTouchListener> newArgsOnTouch = ArgumentCaptor.forClass(OnTouchListener.class);
260-
doNothing().when(newView).setOnTouchListener(newArgsOnTouch.capture());
263+
doNothing().when(newWebView).setOnTouchListener(newArgsOnTouch.capture());
264+
when(newFragmentView.findViewById(R.id.wbvMain)).thenReturn(newWebView);
261265
when(openSettingsDialogFragment.getActivity()).thenReturn(newActivity);
262-
when(newActivity.findViewById(R.id.wbvMain)).thenReturn(newView);
263266

264267
//> WHEN
265-
openSettingsDialogFragment.onCreate(savedState);
268+
openSettingsDialogFragment.onViewCreated(newFragmentView, savedState);
266269
OnTouchListener recreatedListener = newArgsOnTouch.getValue();
267270

268271
mockClock.when(Clock::systemUTC).thenReturn(laterTime);

0 commit comments

Comments
 (0)