From 528bb8d362b1c9c056e4808b8fc385e4cdfa3587 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Tue, 14 Oct 2025 11:13:22 +0200 Subject: [PATCH] Take direct internet access into account --- app/build.gradle | 1 + .../gadgetbridge/GBApplication.java | 6 + .../InternetHelperPreferencesActivity.kt | 3 + .../appmanager/RebbleAppStoreActivity.kt | 210 +++++++++++++----- .../devices/banglejs/BangleJSCoordinator.java | 9 +- .../banglejs/BangleJSDeviceSupport.java | 8 +- .../gadgetbridge/webview/GBWebClient.java | 8 +- app/src/main/res/values/strings.xml | 2 + .../res/xml/internethelper_preferences.xml | 5 + 9 files changed, 183 insertions(+), 69 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ee6e01089e..91290837ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -269,6 +269,7 @@ dependencies { implementation 'com.github.wax911.android-emojify:gson:1.9.7' implementation 'com.google.protobuf:protobuf-javalite:4.33.2' implementation 'com.android.volley:volley:1.2.1' + implementation 'com.squareup.okhttp3:okhttp:5.2.1' implementation 'org.msgpack:msgpack-core:0.9.10' implementation 'com.github.ByteHamster:SearchPreference:2.7.3' diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index c7369cbad4..4e07072c77 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -20,6 +20,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge; +import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlarmManager; @@ -101,6 +102,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; +import nodomain.freeyourgadget.gadgetbridge.util.PermissionsUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.backup.PeriodicZipExporter; import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs; @@ -2273,6 +2275,10 @@ public class GBApplication extends Application { return typedValue.data; } + public static boolean hasDirectInternetAccess() { + return BuildConfig.INTERNET_ACCESS && PermissionsUtils.checkPermission(getContext(), Manifest.permission.INTERNET); + } + public static GBPrefs getPrefs() { return prefs; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/InternetHelperPreferencesActivity.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/InternetHelperPreferencesActivity.kt index c875fa27e3..d6ff329dbc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/InternetHelperPreferencesActivity.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/InternetHelperPreferencesActivity.kt @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout +import nodomain.freeyourgadget.gadgetbridge.BuildConfig import nodomain.freeyourgadget.gadgetbridge.R import nodomain.freeyourgadget.gadgetbridge.database.DBHelper import nodomain.freeyourgadget.gadgetbridge.entities.URLFilterEntry @@ -116,6 +117,8 @@ class InternetHelperPreferencesActivity : AbstractGBActivity() { rootKey: String? ) { setPreferencesFromResource(R.xml.internethelper_preferences, rootKey) + val unusedWarning = findPreference("pref_key_internethelper_unused") + unusedWarning?.isVisible = BuildConfig.INTERNET_ACCESS val installWarning = findPreference("pref_key_internethelper_not_installed") if (AndroidUtils.isPackageInstalled(PACKAGE_INTERNET_HELPER)) { installWarning?.isVisible = false diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/RebbleAppStoreActivity.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/RebbleAppStoreActivity.kt index 691782f237..43f6f4accd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/RebbleAppStoreActivity.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/RebbleAppStoreActivity.kt @@ -30,6 +30,13 @@ import android.webkit.WebView import android.webkit.WebViewClient import android.widget.Toast import androidx.core.net.toUri +import com.android.volley.Request +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import nodomain.freeyourgadget.gadgetbridge.GBApplication import nodomain.freeyourgadget.gadgetbridge.R import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler @@ -43,6 +50,7 @@ import nodomain.freeyourgadget.internethelper.aidl.http.HttpGetRequest import nodomain.freeyourgadget.internethelper.aidl.http.HttpHeaders import nodomain.freeyourgadget.internethelper.aidl.http.HttpResponse import nodomain.freeyourgadget.internethelper.aidl.http.IHttpCallback +import okhttp3.OkHttpClient import org.json.JSONObject import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -92,75 +100,157 @@ class RebbleAppStoreActivity : AbstractGBActivity() { } private fun downloadInstallWatchapp(url: Uri) { - val httpHeaders = HttpHeaders() - val httpGetRequest = HttpGetRequest(url.toString(), httpHeaders) - InternetHelperSingleton.getHttpService()?.get(httpGetRequest, object : IHttpCallback.Stub() { - override fun onResponse(response: HttpResponse) { - val contentType = response.headers["content-type"]?.split(";")?.get(0) - if (!contentType.equals("application/octet-stream") && !contentType.equals("application/zip")) { - GB.toast( - getString(R.string.rebble_appstore_download_failed_wrong_content_type, contentType), - Toast.LENGTH_LONG, GB.ERROR - ) - return - } - val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body) - val filename = url.lastPathSegment ?: "downloaded_file.pbw" - val cacheDir = applicationContext.externalCacheDir ?: return - val cacheFile = File(cacheDir, filename) - val outputStream: OutputStream = FileOutputStream(cacheFile) - val buffer = ByteArray(1024) - var len: Int - while (inputStream.read(buffer).also { len = it } > 0) { - outputStream.write(buffer, 0, len) - } - outputStream.flush() - outputStream.close() - val installHandler: InstallHandler? = mGBDevice?.deviceCoordinator?.findInstallHandler(cacheFile.toUri(), applicationContext) - if (installHandler == null) { - GB.toast(getString(R.string.fwinstaller_file_not_compatible_to_device), Toast.LENGTH_LONG, GB.ERROR) - return - } - val startIntent = Intent(applicationContext, installHandler.getInstallActivity()) - startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice) - startIntent.action = Intent.ACTION_VIEW - startIntent.setDataAndType(cacheFile.toUri(), null) - startActivity(startIntent) + if (GBApplication.hasDirectInternetAccess()) { + downloadBinaryFile(url) { file -> + installFile(file) } + } else { + val httpHeaders = HttpHeaders() + val httpGetRequest = HttpGetRequest(url.toString(), httpHeaders) + InternetHelperSingleton.getHttpService() + ?.get(httpGetRequest, object : IHttpCallback.Stub() { + override fun onResponse(response: HttpResponse) { + val contentType = response.headers["content-type"]?.split(";")?.get(0) + if (!contentType.equals("application/octet-stream") && !contentType.equals("application/zip")) { + GB.toast( + getString( + R.string.rebble_appstore_download_failed_wrong_content_type, + contentType + ), + Toast.LENGTH_LONG, GB.ERROR + ) + return + } + val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body) + val filename = url.lastPathSegment ?: "downloaded_file.pbw" + val cacheDir = applicationContext.externalCacheDir ?: return + val cacheFile = File(cacheDir, filename) + val outputStream: OutputStream = FileOutputStream(cacheFile) + val buffer = ByteArray(1024) + var len: Int + while (inputStream.read(buffer).also { len = it } > 0) { + outputStream.write(buffer, 0, len) + } + outputStream.flush() + outputStream.close() + installFile(cacheFile) + } - override fun onException(message: String?) { - GB.toast(getString(R.string.rebble_appstore_download_failed, message), Toast.LENGTH_LONG, GB.ERROR) - } - }) + override fun onException(message: String?) { + GB.toast( + getString(R.string.rebble_appstore_download_failed, message), + Toast.LENGTH_LONG, + GB.ERROR + ) + } + }) + } } private fun downloadInstallWatchappById(storeId: String) { val appUrl = "https://appstore-api.rebble.io/api/v1/apps/id/$storeId" - val httpHeaders = HttpHeaders() - val httpGetRequest = HttpGetRequest(appUrl, httpHeaders) - InternetHelperSingleton.getHttpService()?.get(httpGetRequest, object : IHttpCallback.Stub() { - override fun onResponse(response: HttpResponse) { - val contentType = response.headers["content-type"]?.split(";")?.get(0) - if (!contentType.equals("application/json")) { + if (GBApplication.hasDirectInternetAccess()) { + val requestQueue = Volley.newRequestQueue(this) + val jsonObjectRequest = JsonObjectRequest( + Request.Method.GET, appUrl, null, + { response -> + val dataArray = response.getJSONArray("data") + val firstAppObject = dataArray.getJSONObject(0) + val latestRelease = firstAppObject.getJSONObject("latest_release") + val pbwFile = latestRelease.getString("pbw_file") + downloadInstallWatchapp(pbwFile.toUri()) + }, + { error -> GB.toast( - getString(R.string.rebble_appstore_fetch_app_info_failed_content_type, contentType), - Toast.LENGTH_LONG, GB.ERROR + getString( + R.string.rebble_appstore_fetching_download_file_failed, + error + ), Toast.LENGTH_LONG, GB.ERROR ) - return } - val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body) - val responseBody = InputStreamReader(inputStream, StandardCharsets.UTF_8).readText() - val jsonObject = JSONObject(responseBody) - val dataArray = jsonObject.getJSONArray("data") - val firstAppObject = dataArray.getJSONObject(0) - val latestRelease = firstAppObject.getJSONObject("latest_release") - val pbwFile = latestRelease.getString("pbw_file") - downloadInstallWatchapp(pbwFile.toUri()) + ) + requestQueue.add(jsonObjectRequest) + } else { + val httpHeaders = HttpHeaders() + val httpGetRequest = HttpGetRequest(appUrl, httpHeaders) + InternetHelperSingleton.getHttpService() + ?.get(httpGetRequest, object : IHttpCallback.Stub() { + override fun onResponse(response: HttpResponse) { + val contentType = response.headers["content-type"]?.split(";")?.get(0) + if (!contentType.equals("application/json")) { + GB.toast( + getString( + R.string.rebble_appstore_fetch_app_info_failed_content_type, + contentType + ), + Toast.LENGTH_LONG, GB.ERROR + ) + return + } + val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body) + val responseBody = + InputStreamReader(inputStream, StandardCharsets.UTF_8).readText() + val jsonObject = JSONObject(responseBody) + val dataArray = jsonObject.getJSONArray("data") + val firstAppObject = dataArray.getJSONObject(0) + val latestRelease = firstAppObject.getJSONObject("latest_release") + val pbwFile = latestRelease.getString("pbw_file") + downloadInstallWatchapp(pbwFile.toUri()) + } + + override fun onException(message: String?) { + GB.toast( + getString( + R.string.rebble_appstore_fetching_download_file_failed, + message + ), Toast.LENGTH_LONG, GB.ERROR + ) + } + }) + } + } + + fun downloadBinaryFile(url: Uri, onComplete: (File) -> Unit) { + CoroutineScope(Dispatchers.IO).launch { + try { + val client = OkHttpClient() + val request = okhttp3.Request.Builder().url(url.toString()).build() + val response = client.newCall(request).execute() + + if (!response.isSuccessful) { + LOG.error("Downloading $url failed: ${response.code}") + return@launch + } + + val filename = url.lastPathSegment ?: "downloaded_file.pbw" + val cacheDir = applicationContext.externalCacheDir + val cacheFile = File(cacheDir, filename) + val outputStream = FileOutputStream(cacheFile) + + val inputStream = response.body.byteStream() + inputStream.copyTo(outputStream) + outputStream.flush() + outputStream.close() + inputStream.close() + + onComplete(cacheFile) + } catch (e: Exception) { + LOG.error("Downloading $url failed: ", e) } - override fun onException(message: String?) { - GB.toast(getString(R.string.rebble_appstore_fetching_download_file_failed, message), Toast.LENGTH_LONG, GB.ERROR) - } - }) + } + } + + fun installFile(file: File) { + val installHandler: InstallHandler? = mGBDevice?.deviceCoordinator?.findInstallHandler(file.toUri(), applicationContext) + if (installHandler == null) { + GB.toast(getString(R.string.fwinstaller_file_not_compatible_to_device), Toast.LENGTH_LONG, GB.ERROR) + return + } + val startIntent = Intent(applicationContext, installHandler.getInstallActivity()) + startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice) + startIntent.action = Intent.ACTION_VIEW + startIntent.setDataAndType(file.toUri(), null) + startActivity(startIntent) } @SuppressLint("SetJavaScriptEnabled") @@ -223,4 +313,4 @@ class RebbleAppStoreActivity : AbstractGBActivity() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java index 5bdba5307e..4ad057d0ef 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java @@ -40,7 +40,7 @@ import java.util.regex.Pattern; import de.greenrobot.dao.AbstractDao; import de.greenrobot.dao.Property; -import nodomain.freeyourgadget.gadgetbridge.BuildConfig; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; @@ -55,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton; public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator { @@ -168,12 +169,12 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator { @Override public boolean supportsAppsManagement(final GBDevice device) { - return true; + return GBApplication.hasDirectInternetAccess() || InternetHelperSingleton.INSTANCE.ensureInternetHelperBound(); } @Override public Class getAppsManagementActivity(final GBDevice device) { - return AppsManagementActivity.class; + return supportsAppsManagement(device) ? AppsManagementActivity.class : null; } @Override @@ -232,7 +233,7 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator { settings.add(R.xml.devicesettings_header_connection); settings.add(R.xml.devicesettings_high_mtu); - if (BuildConfig.INTERNET_ACCESS) + if (GBApplication.hasDirectInternetAccess() || InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) settings.add(R.xml.devicesettings_device_internet_access); settings.add(R.xml.devicesettings_banglejs_activity); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java index 2e86f72b68..40f1e9d38b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java @@ -99,7 +99,6 @@ import java.util.SimpleTimeZone; import de.greenrobot.dao.query.QueryBuilder; import io.wax911.emojify.EmojiManager; import io.wax911.emojify.parser.EmojiParserKt; -import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity; @@ -141,8 +140,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.NavigationInfoSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; -import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather; import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; @@ -152,6 +151,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; +import nodomain.freeyourgadget.gadgetbridge.util.InternetHelperSingleton; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; import nodomain.freeyourgadget.gadgetbridge.util.MediaManager; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -918,14 +918,14 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport { } final String id = _id; - if (!BuildConfig.INTERNET_ACCESS) { + if (!GBApplication.hasDirectInternetAccess() && !InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) { uartTxJSONError("http", "Internet access not enabled, check Gadgetbridge Device Settings", id); return; } Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())); if (! devicePrefs.getBoolean(PREF_DEVICE_INTERNET_ACCESS, false)) { - uartTxJSONError("http", "Internet access not enabled in this Gadgetbridge build", id); + uartTxJSONError("http", "Internet access not enabled for this device", id); return; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBWebClient.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBWebClient.java index 220fe74978..d5a7470acd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBWebClient.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/webview/GBWebClient.java @@ -99,6 +99,12 @@ public class GBWebClient extends WebViewClient { boolean matchFound = false; List urlFilterEntries = DBHelper.getURLFilterEntries(); + // Allow full access to internet when available + boolean directInternetAccess = GBApplication.hasDirectInternetAccess(); + if (directInternetAccess && !locallySupported) { + return null; + } + // Handle local schemes locally if (requestedUri.toString().startsWith("file://") || requestedUri.toString().startsWith("gadgetbridge://")) { return null; @@ -151,7 +157,7 @@ public class GBWebClient extends WebViewClient { // Handle request if (requestedUri.getHost() != null && urlIsAllowed) { - if (!forceLocal && InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) { + if (!forceLocal && !directInternetAccess && InternetHelperSingleton.INSTANCE.ensureInternetHelperBound()) { LOG.debug("WEBVIEW forwarding request to the internet helper"); try { return InternetHelperSingleton.INSTANCE.send(requestedUri); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98c0a3cab5..f70a3cf4e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4485,4 +4485,6 @@ Fetching download file failed: %1$s Internet helper add-on Permit access to the Gadgetbridge-internethelper add-on app + Unused settings + The settings below are not used because direct internet access is available. diff --git a/app/src/main/res/xml/internethelper_preferences.xml b/app/src/main/res/xml/internethelper_preferences.xml index d0eb93a74f..b7709ea02e 100644 --- a/app/src/main/res/xml/internethelper_preferences.xml +++ b/app/src/main/res/xml/internethelper_preferences.xml @@ -2,6 +2,11 @@ +