Skip to content
Snippets Groups Projects
Commit 77732d15 authored by Dominik Hebeler's avatar Dominik Hebeler
Browse files

Merge branch '2-support-for-android-15-api-35' into 'master'

Resolve "Support for Android 15 (API 35)"

Closes #2

See merge request metagermaps/android!3
parents 9f1a075e 311b7a53
Branches master
No related tags found
No related merge requests found
Showing
with 190 additions and 59 deletions
......@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-06-06T12:36:47.407555689Z">
<DropdownSelection timestamp="2024-07-02T10:38:41.057675738Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/dominik/.android/avd/7_inch_tablet_API_34.avd" />
<DeviceId pluginId="LocalEmulator" identifier="path=/home/dominik/.android/avd/Pixel_8_API_35.avd" />
</handle>
</Target>
</DropdownSelection>
......
......@@ -5,7 +5,7 @@ android {
defaultConfig {
applicationId "de.metager.maps"
minSdkVersion 23
targetSdkVersion 34
targetSdkVersion 35
versionCode 100004
versionName "1.0.4"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
......@@ -100,10 +100,12 @@ dependencies {
implementation 'androidx.navigation:navigation-fragment:2.7.7'
implementation 'androidx.navigation:navigation-ui:2.7.7'
implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.activity:activity:1.9.0'
implementation 'androidx.compose.ui:ui:1.6.8'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation group: 'commons-io', name: 'commons-io', version: '2.16.1'
implementation group: 'commons-codec', name: 'commons-codec', version: '1.17.0'
implementation "org.mozilla.geckoview:geckoview:127.0.20240618110440"
implementation "org.mozilla.geckoview:geckoview:127.0.20240624183754"
}
......@@ -7,10 +7,15 @@ import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
......@@ -29,6 +34,7 @@ import org.mozilla.geckoview.GeckoSession;
import org.mozilla.geckoview.GeckoView;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -40,6 +46,7 @@ import de.metager.maps.notifications.NotificationManager;
import de.metager.maps.permissions.PermissionManager;
import de.metager.maps.updater.UpdateChecker;
import de.metager.maps.util.androidUtil.PropertyLoader;
import de.metager.maps.webview.InsetChangeController;
import de.metager.maps.webview.MapsMessageDelegate;
import de.metager.maps.webview.MapsNavigationDelegate;
import de.metager.maps.webview.MapsPermissionDelegate;
......@@ -55,18 +62,21 @@ public class MainActivity extends AppCompatActivity implements ActivityCompat.On
protected MapsPortDelegate portDelegate;
protected MapsNavigationDelegate navigationDelegate;
protected MapsPromptDelegate promptDelegate;
protected InsetChangeController insetChangeCOntroller;
public static int LOCATION_PERMISSION_REQUEST_CODE = 0;
private ArrayList<String> allowedHosts;
private ArrayList<URL> allowedHosts;
private URL loadURL;
@SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"})
@Override
protected void onCreate(Bundle savedInstanceState) {
EdgeToEdge.enable(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webview = findViewById(R.id.geckoview);
this.session = new GeckoSession();
GeckoRuntimeSettings settings = new GeckoRuntimeSettings.Builder().consoleOutput(true).build();
......@@ -76,12 +86,22 @@ public class MainActivity extends AppCompatActivity implements ActivityCompat.On
settings.setAboutConfigEnabled(true);
}
if(this.runtime == null) {
if (this.runtime == null) {
this.runtime = GeckoRuntime.create(this, settings);
}
this.loadURL = this.getWebserverURL();
this.allowedHosts = new ArrayList<>(Arrays.asList(loadURL.getHost()));
this.allowedHosts = new ArrayList<>(Arrays.asList(loadURL));
if(BuildConfig.DEBUG) {
// Allow Github Devtunnels in Debug ModeL(
try {
this.allowedHosts.add(new URL("https://visualstudio.com"));
this.allowedHosts.add(new URL("https://github.com"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
session.open(runtime);
webview.setSession(session);
......@@ -103,7 +123,7 @@ public class MainActivity extends AppCompatActivity implements ActivityCompat.On
new Runnable() {
@Override
public void run() {
if(extension == null) return;
if (extension == null) return;
session.getWebExtensionController()
.setMessageDelegate(extension, messageDelegate, "browser");
......@@ -121,21 +141,21 @@ public class MainActivity extends AppCompatActivity implements ActivityCompat.On
}
}
private void loadView(){
private void loadView() {
String intentUriString = getIntent().getStringExtra("url");
if(intentUriString != null && allowedHosts.contains(Uri.parse(intentUriString).getHost())){
if (intentUriString != null && allowedHosts.contains(Uri.parse(intentUriString).getHost())) {
session.loadUri(intentUriString);
}else{
} else {
session.loadUri(loadURL.toString());
}
// For manual Installation only: Check for updates
if(de.metager.maps.BuildConfig.FLAVOR.equals("manual")){
if (de.metager.maps.BuildConfig.FLAVOR.equals("manual")) {
this.createUpdateChecker();
}
}
private URL getWebserverURL(){
private URL getWebserverURL() {
String url = null;
try {
url = PropertyLoader.getProperty("maps_webserver_url", this);
......@@ -145,83 +165,73 @@ public class MainActivity extends AppCompatActivity implements ActivityCompat.On
}
}
private void createUpdateChecker(){
if(PermissionManager.checkNotificationPermission(this, true)) {
private void createUpdateChecker() {
if (PermissionManager.checkNotificationPermission(this, true)) {
NotificationManager.createUpdateNotificationChannel(this.getApplicationContext());
// Periodically run Updatecheck and notify preferred in unmetered Networks
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED).build();
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(UpdateChecker.class, 1, TimeUnit.DAYS)
.addTag(UpdateChecker.PERIODIC_TAG)
.setConstraints(constraints).build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(UpdateChecker.NAME, ExistingPeriodicWorkPolicy.KEEP, workRequest);
// Check when Updatecheck last ran
ListenableFuture<List<WorkInfo>> workInfosFuture = WorkManager.getInstance(this).getWorkInfosByTag(UpdateChecker.PERIODIC_TAG);
try {
long daysSinceLastExecution = 0;
List<WorkInfo> workInfos = workInfosFuture.get();
for(WorkInfo workInfo : workInfos){
Date nextExecution = new Date(workInfo.getNextScheduleTimeMillis());
daysSinceLastExecution = TimeUnit.DAYS.convert(Math.abs(System.currentTimeMillis() - nextExecution.getTime()), TimeUnit.MILLISECONDS);
}
if(daysSinceLastExecution >= 7){
// Last execution was more than 7 days ago. We will check now
OneTimeWorkRequest singleworkRequest = new OneTimeWorkRequest.Builder(UpdateChecker.class).addTag(UpdateChecker.PERIODIC_TAG).build();
WorkManager.getInstance(this).enqueue(singleworkRequest);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
WorkManager.getInstance(this).enqueueUniquePeriodicWork(UpdateChecker.NAME, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, workRequest);
}
}
/**
* Called when the Android App is finally connected
*/
public void androidAppConnected() {
// Setup Watch for Inset changes
insetChangeCOntroller = new InsetChangeController(findViewById(android.R.id.content), getResources().getDisplayMetrics().density, messageDelegate);
}
@Override
public void onRequestPermissionsResult(int i, @NonNull String[] strings, @NonNull int[] ints) {
super.onRequestPermissionsResult(i, strings, ints);
if (i == MainActivity.LOCATION_PERMISSION_REQUEST_CODE) {
this.permissionHandler.onRequestPermissionsResult(i, strings, ints);
}else if(i == PermissionManager.SINGLE_LOCATION_REQUEST_JAVASCRIPT){
} else if (i == PermissionManager.SINGLE_LOCATION_REQUEST_JAVASCRIPT) {
boolean granted = false;
for(int permissionResult : ints){
for (int permissionResult : ints) {
if (permissionResult == PackageManager.PERMISSION_GRANTED) {
granted = true;
break;
}
}
if(granted){
if (granted) {
portDelegate.getCurrentPositions();
}else{
} else {
portDelegate.getCurrentPositionsError();
}
}else if(i == PermissionManager.WATCH_LOCATION_REQUEST_JAVASCRIPT){
} else if (i == PermissionManager.WATCH_LOCATION_REQUEST_JAVASCRIPT) {
boolean granted = false;
for(int permissionResult : ints){
for (int permissionResult : ints) {
if (permissionResult == PackageManager.PERMISSION_GRANTED) {
granted = true;
break;
}
}
if(granted){
if (granted) {
portDelegate.watchPositions();
}else{
} else {
portDelegate.watchPositionsError();
}
}else if(i == PermissionManager.PERMISSION_POST_NOTIFICATIONS){
} else if (i == PermissionManager.PERMISSION_POST_NOTIFICATIONS) {
boolean granted = false;
for(int permissionResult : ints){
for (int permissionResult : ints) {
if (permissionResult == PackageManager.PERMISSION_GRANTED) {
granted = true;
break;
}
}
if(granted && de.metager.maps.BuildConfig.FLAVOR.equals("manual")){
if (granted && de.metager.maps.BuildConfig.FLAVOR.equals("manual")) {
this.createUpdateChecker();
}
}
}
@Override
public void onNewIntent(Intent intent){
public void onNewIntent(Intent intent) {
setIntent(intent);
this.loadView();
super.onNewIntent(intent);
......@@ -229,15 +239,15 @@ public class MainActivity extends AppCompatActivity implements ActivityCompat.On
@Override
public void onBackPressed() {
if(navigationDelegate.canGoBack){
if (navigationDelegate.canGoBack) {
session.goBack();
}else{
} else {
super.onBackPressed();
}
}
@Override
public void onConfigurationChanged(Configuration newConfig){
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
......@@ -248,7 +258,7 @@ public class MainActivity extends AppCompatActivity implements ActivityCompat.On
super.onDestroy();
}
boolean setIntentExtras(Intent intent, Uri data){
boolean setIntentExtras(Intent intent, Uri data) {
return false;
}
}
......@@ -90,6 +90,11 @@ public class UpdateActivity extends AppCompatActivity {
}
private void startInstallation() throws IOException, ExecutionException, InterruptedException {
// Check if we are allowed to install apps from unknown source
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !getPackageManager().canRequestPackageInstalls()) {
startActivity(new Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getPackageName())));
return;
}
// Download APK
downloadProgressBar.setVisibility(View.VISIBLE);
downloadProgressText.setVisibility(View.VISIBLE);
......
package de.metager.maps.webview;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class InsetChangeController {
private final View mView;
private final MapsMessageDelegate mMessageDelegate;
private final float displayDensity;
public InsetChangeController(View view, float displayDensity, MapsMessageDelegate messageDelegate) {
this.mView = view;
this.mMessageDelegate = messageDelegate;
this.displayDensity = displayDensity;
WindowInsetsCompat current_insets = ViewCompat.getRootWindowInsets(mView);
if(current_insets != null){
Insets new_insets = current_insets.getInsets(WindowInsetsCompat.Type.systemGestures());
this.insetsChanged(new_insets);
}
ViewCompat.setOnApplyWindowInsetsListener(mView, (v, windowInsets) -> {
Insets gestures = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures());
insetsChanged(gestures);
return WindowInsetsCompat.CONSUMED;
});
}
private void insetsChanged(@NonNull Insets insets){
int top = (int)Math.round(insets.top / displayDensity);
int right = (int)Math.round(insets.right / displayDensity);
int bottom = (int)Math.round(insets.bottom / displayDensity);
int left = (int)Math.round(insets.left / displayDensity);
mMessageDelegate.updateInsets(top, right, bottom, left);
}
}
......@@ -29,6 +29,26 @@ public class MapsMessageDelegate implements WebExtension.MessageDelegate {
} catch (JSONException e) {
throw new RuntimeException(e);
}
mPortDelegate.androidAppConnected();
}
public void updateInsets(int top, int right, int bottom, int left){
// Post message about connected Android app when port connects
JSONObject message = new JSONObject();
try {
message.put("from", "android");
message.put("type", "insets");
JSONObject data = new JSONObject();
data.put("top", top);
data.put("right", right);
data.put("bottom", bottom);
data.put("left", left);
message.put("data", data);
mPort.postMessage(message);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public GeckoResult<Object> onMessage(final @NonNull String nativeApp, final @NonNull Object message, final @NonNull WebExtension.MessageSender sender){
......
......@@ -15,17 +15,18 @@ import org.mozilla.geckoview.AllowOrDeny;
import org.mozilla.geckoview.GeckoResult;
import org.mozilla.geckoview.GeckoSession;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import de.metager.maps.R;
public class MapsNavigationDelegate implements GeckoSession.NavigationDelegate {
private final ArrayList<String> allowedHosts;
private final ArrayList<URL> allowedHosts;
private final Context mContext;
public boolean canGoBack = false;
public MapsNavigationDelegate(ArrayList<String> allowedHosts, Context context) {
public MapsNavigationDelegate(ArrayList<URL> allowedHosts, Context context) {
super();
this.allowedHosts = allowedHosts;
this.mContext = context;
......@@ -43,12 +44,13 @@ public class MapsNavigationDelegate implements GeckoSession.NavigationDelegate {
// Other URLs we'll open in a system browser
// Determine this apps URL
Uri uri = Uri.parse(request.uri);
if(uri.getScheme().startsWith("http") && allowedHosts.contains(uri.getHost())){
return GeckoResult.allow();
}else{
this.requestURLOpen(uri);
return GeckoResult.deny();
for (URL allowed_url: allowedHosts) {
if(uri.getScheme().startsWith("http") && uri.getHost().contains(allowed_url.getHost())){
return GeckoResult.allow();
}
}
this.requestURLOpen(uri);
return GeckoResult.deny();
}
private void requestURLOpen(Uri uri) {
......
......@@ -36,6 +36,11 @@ public class MapsPortDelegate implements WebExtension.PortDelegate {
this.activity = mainActivity;
this.locationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
}
public void androidAppConnected(){
activity.androidAppConnected();
}
public void onPortMessage(final @NonNull Object message, final @NonNull WebExtension.Port port){
JSONObject json_message = (JSONObject)message;
try {
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">Der er kommet en opdatering til MetaGer Maps-appen.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">Ein Update ist für die MetaGer Maps App verfügbar.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">Hay una actualización disponible para la aplicación MetaGer Maps.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">MetaGer Maps -sovellukseen on saatavilla päivitys.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">Une mise à jour est disponible pour l\'application MetaGer Maps.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">È disponibile un aggiornamento per l\'applicazione MetaGer Maps.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">Er is een update beschikbaar voor de MetaGer Maps app.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">Dostępna jest aktualizacja aplikacji MetaGer Maps.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">En uppdatering är tillgänglig för MetaGer Maps-appen.</string>
</resources>
\ No newline at end of file
<resources>
<string name="app_name">Metager Maps</string>
<string name="start_update">Ein Update ist für die MetaGer Maps App verfügbar.</string>
<string name="update_now">Jetzt Installieren</string>
<string name="update_later">Später</string>
<string name="permissions_notification_message">Dürfen wir dich benachrichtigen, wenn ein Update für diese App zur Verfügung steht? Wenn nicht, wird diese App keine automatischen Updates erhalten.</string>
......@@ -11,4 +10,5 @@
<string name="notification_update_title">Update verfügbar</string>
<string name="notification_update_description">Ein Update für MetaGer Maps ist verfügbar</string>
<string name="updater_download_title">MetaGer Maps Update wird heruntergeladen...</string>
<string name="start_update">An update is available for the MetaGer Maps app.</string>
</resources>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment