Take direct internet access into account

This commit is contained in:
Arjan Schrijver
2025-12-25 14:45:08 +01:00
committed by Arjan Schrijver
parent d2cd47231a
commit 528bb8d362
9 changed files with 183 additions and 69 deletions
+1
View File
@@ -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'
@@ -20,6 +20,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
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;
}
@@ -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<Preference>("pref_key_internethelper_unused")
unusedWarning?.isVisible = BuildConfig.INTERNET_ACCESS
val installWarning = findPreference<Preference>("pref_key_internethelper_not_installed")
if (AndroidUtils.isPackageInstalled(PACKAGE_INTERNET_HELPER)) {
installWarning?.isVisible = false
@@ -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() {
}
}
}
}
}
@@ -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<? extends Activity> 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);
@@ -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;
}
@@ -99,6 +99,12 @@ public class GBWebClient extends WebViewClient {
boolean matchFound = false;
List<URLFilterEntry> 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);
+2
View File
@@ -4485,4 +4485,6 @@
<string name="rebble_appstore_fetching_download_file_failed">Fetching download file failed: %1$s</string>
<string name="internet_helper_permission_title">Internet helper add-on</string>
<string name="internet_helper_permission_summary">Permit access to the Gadgetbridge-internethelper add-on app</string>
<string name="internet_helper_pref_unused_settings_title">Unused settings</string>
<string name="internet_helper_pref_unused_settings_summary">The settings below are not used because direct internet access is available.</string>
</resources>
@@ -2,6 +2,11 @@
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:icon="@drawable/ic_warning"
android:key="pref_key_internethelper_unused"
android:title="@string/internet_helper_pref_unused_settings_title"
android:summary="@string/internet_helper_pref_unused_settings_summary" />
<Preference
android:icon="@drawable/ic_warning"
android:key="pref_key_internethelper_not_installed"