mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Support installing directly from Rebble appstore using internethelper
This commit is contained in:
committed by
Arjan Schrijver
parent
1914162b54
commit
174f4a87e4
@@ -267,6 +267,10 @@
|
|||||||
android:label="@string/title_activity_appmanager"
|
android:label="@string/title_activity_appmanager"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:parentActivityName=".activities.ControlCenterv2" />
|
android:parentActivityName=".activities.ControlCenterv2" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.appmanager.RebbleAppStoreActivity"
|
||||||
|
android:label="@string/activity_title_rebble_app_store"
|
||||||
|
android:parentActivityName=".activities.appmanager.AppManagerActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.musicmanager.MusicManagerActivity"
|
android:name=".activities.musicmanager.MusicManagerActivity"
|
||||||
android:label="@string/title_activity_musicmanager"
|
android:label="@string/title_activity_musicmanager"
|
||||||
@@ -682,7 +686,6 @@
|
|||||||
android:label="@string/activity_filter_filter_title"
|
android:label="@string/activity_filter_filter_title"
|
||||||
android:parentActivityName=".activities.workouts.WorkoutListActivity"
|
android:parentActivityName=".activities.workouts.WorkoutListActivity"
|
||||||
android:excludeFromRecents="true"/>
|
android:excludeFromRecents="true"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DataManagementActivity"
|
android:name=".activities.DataManagementActivity"
|
||||||
android:label="@string/title_activity_db_management"
|
android:label="@string/title_activity_db_management"
|
||||||
|
|||||||
+29
-10
@@ -67,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
|
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader;
|
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilHRInstallHandler;
|
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilHRInstallHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridConstants;
|
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridConstants;
|
||||||
@@ -80,6 +81,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.util.GridAutoFitLayoutManager;
|
import nodomain.freeyourgadget.gadgetbridge.util.GridAutoFitLayoutManager;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Version;
|
import nodomain.freeyourgadget.gadgetbridge.util.Version;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton;
|
||||||
|
|
||||||
|
|
||||||
public abstract class AbstractAppManagerFragment extends Fragment {
|
public abstract class AbstractAppManagerFragment extends Fragment {
|
||||||
@@ -95,6 +97,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
|||||||
protected GBDevice mGBDevice = null;
|
protected GBDevice mGBDevice = null;
|
||||||
protected DeviceCoordinator mCoordinator = null;
|
protected DeviceCoordinator mCoordinator = null;
|
||||||
private Class<? extends Activity> watchfaceDesignerActivity;
|
private Class<? extends Activity> watchfaceDesignerActivity;
|
||||||
|
private Class<? extends Activity> appStoreActivity;
|
||||||
|
|
||||||
protected abstract List<GBDeviceApp> getSystemAppsInCategory();
|
protected abstract List<GBDeviceApp> getSystemAppsInCategory();
|
||||||
|
|
||||||
@@ -424,12 +427,14 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
|||||||
mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice();
|
mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice();
|
||||||
mCoordinator = mGBDevice.getDeviceCoordinator();
|
mCoordinator = mGBDevice.getDeviceCoordinator();
|
||||||
|
|
||||||
final FloatingActionButton appListFab = ((FloatingActionButton) getActivity().findViewById(R.id.fab));
|
final FloatingActionButton appListFab = getActivity().findViewById(R.id.fab);
|
||||||
final FloatingActionButton appListFabNew = ((FloatingActionButton) getActivity().findViewById(R.id.fab_new));
|
final FloatingActionButton appListFabNew = getActivity().findViewById(R.id.fab_new);
|
||||||
|
final FloatingActionButton appListFabStore = getActivity().findViewById(R.id.fab_store);
|
||||||
watchfaceDesignerActivity = mCoordinator.getWatchfaceDesignerActivity(mGBDevice);
|
watchfaceDesignerActivity = mCoordinator.getWatchfaceDesignerActivity(mGBDevice);
|
||||||
|
appStoreActivity = mCoordinator.getAppStoreActivity(mGBDevice);
|
||||||
View rootView = inflater.inflate(R.layout.activity_appmanager, container, false);
|
View rootView = inflater.inflate(R.layout.activity_appmanager, container, false);
|
||||||
|
|
||||||
RecyclerView appListView = (RecyclerView) (rootView.findViewById(R.id.appListView));
|
RecyclerView appListView = rootView.findViewById(R.id.appListView);
|
||||||
|
|
||||||
appListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
appListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -437,6 +442,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
|||||||
if (dy > 0) {
|
if (dy > 0) {
|
||||||
appListFab.hide();
|
appListFab.hide();
|
||||||
appListFabNew.hide();
|
appListFabNew.hide();
|
||||||
|
appListFabStore.hide();
|
||||||
} else if (dy < 0) {
|
} else if (dy < 0) {
|
||||||
if (mCoordinator.supportsFlashing(mGBDevice)) {
|
if (mCoordinator.supportsFlashing(mGBDevice)) {
|
||||||
appListFab.show();
|
appListFab.show();
|
||||||
@@ -444,6 +450,11 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
|||||||
if (watchfaceDesignerActivity != null) {
|
if (watchfaceDesignerActivity != null) {
|
||||||
appListFabNew.show();
|
appListFabNew.show();
|
||||||
}
|
}
|
||||||
|
if (appStoreActivity != null && mGBDevice.getDeviceCoordinator() instanceof PebbleCoordinator) {
|
||||||
|
if (WebViewSingleton.getInstance().ensureInternetHelperBound()) {
|
||||||
|
appListFabStore.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -462,17 +473,25 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
|||||||
appManagementTouchHelper.attachToRecyclerView(appListView);
|
appManagementTouchHelper.attachToRecyclerView(appListView);
|
||||||
|
|
||||||
if ((watchfaceDesignerActivity != null) && (appListFabNew != null)) {
|
if ((watchfaceDesignerActivity != null) && (appListFabNew != null)) {
|
||||||
appListFabNew.setOnClickListener(new View.OnClickListener() {
|
appListFabNew.setOnClickListener(v -> {
|
||||||
@Override
|
Intent startIntent = new Intent(getContext(), watchfaceDesignerActivity);
|
||||||
public void onClick(View v) {
|
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
|
||||||
Intent startIntent = new Intent(getContext(), watchfaceDesignerActivity);
|
getContext().startActivity(startIntent);
|
||||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
|
|
||||||
getContext().startActivity(startIntent);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
appListFabNew.show();
|
appListFabNew.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appStoreActivity != null && mGBDevice.getDeviceCoordinator() instanceof PebbleCoordinator) {
|
||||||
|
if (WebViewSingleton.getInstance().ensureInternetHelperBound()) {
|
||||||
|
appListFabStore.setOnClickListener(v -> {
|
||||||
|
Intent startIntent = new Intent(getContext(), appStoreActivity);
|
||||||
|
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
|
||||||
|
getContext().startActivity(startIntent);
|
||||||
|
});
|
||||||
|
appListFabStore.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+297
@@ -0,0 +1,297 @@
|
|||||||
|
/* Copyright (C) 2022-2024 Andreas Shimokawa, Daniel Dakhno, Gordon Williams, Arjan Schrijver
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.webkit.PermissionRequest
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.Capsule
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||||
|
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 nodomain.freeyourgadget.internethelper.aidl.http.IHttpService
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import kotlin.concurrent.Volatile
|
||||||
|
|
||||||
|
class RebbleAppStoreActivity : AbstractGBActivity() {
|
||||||
|
val LOG: Logger = LoggerFactory.getLogger(RebbleAppStoreActivity::class.java)
|
||||||
|
private var mGBDevice: GBDevice? = null
|
||||||
|
private var webView: WebView? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_banglejs_apps_management)
|
||||||
|
|
||||||
|
val intent1 = Intent("nodomain.freeyourgadget.internethelper.HttpService")
|
||||||
|
intent1.setPackage("nodomain.freeyourgadget.internethelper")
|
||||||
|
val res = applicationContext.bindService(intent1, mHttpConnection, BIND_AUTO_CREATE)
|
||||||
|
if (res) {
|
||||||
|
LOG.info("Bound to HttpService")
|
||||||
|
} else {
|
||||||
|
LOG.warn("Could not bind to HttpService")
|
||||||
|
}
|
||||||
|
|
||||||
|
val extras = intent.extras
|
||||||
|
if (extras != null) {
|
||||||
|
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE)
|
||||||
|
}
|
||||||
|
requireNotNull(mGBDevice) { "Must provide a device when invoking this activity" }
|
||||||
|
initViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (webView != null) return // already set up
|
||||||
|
val commandFilter = IntentFilter()
|
||||||
|
commandFilter.addAction(GBDevice.ACTION_DEVICE_CHANGED)
|
||||||
|
initViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
webView!!.destroy()
|
||||||
|
webView = null
|
||||||
|
super.onDestroy()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var iHttpService: IHttpService? = null
|
||||||
|
|
||||||
|
private val mHttpConnection: ServiceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(className: ComponentName?, service: IBinder?) {
|
||||||
|
LOG.info("onServiceConnected: {}", className)
|
||||||
|
iHttpService = IHttpService.Stub.asInterface(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(className: ComponentName?) {
|
||||||
|
LOG.error("Service has unexpectedly disconnected: {}", className)
|
||||||
|
iHttpService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDownloadableWatchapp(url: String): Boolean {
|
||||||
|
val downloadExtensions = listOf(".pbw", ".zip")
|
||||||
|
return downloadExtensions.any { url.endsWith(it, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadInstallWatchapp(url: Uri) {
|
||||||
|
val httpHeaders = HttpHeaders()
|
||||||
|
val httpGetRequest = HttpGetRequest(url.toString(), httpHeaders)
|
||||||
|
iHttpService!!.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("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)
|
||||||
|
}
|
||||||
|
override fun onException(message: String?) {
|
||||||
|
GB.toast("Download failed: $message", Toast.LENGTH_LONG, GB.ERROR)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle links like pebble://appstore/52b231c2b70e1c159500009b#
|
||||||
|
// so we don't have to use ?dev_settings=true
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
private fun initViews() {
|
||||||
|
webView = findViewById(R.id.webview)
|
||||||
|
webView!!.webViewClient = WebViewClient()
|
||||||
|
val settings = webView!!.settings
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
settings.loadWithOverviewMode = true
|
||||||
|
settings.useWideViewPort = true
|
||||||
|
|
||||||
|
val url = "https://apps.rebble.io/en_US/watchfaces?dev_settings=true"
|
||||||
|
|
||||||
|
webView!!.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
wv: WebView,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): Boolean {
|
||||||
|
val requestUrl = request.url
|
||||||
|
// Check if the URL is a downloadable watchface/watchapp
|
||||||
|
if (isDownloadableWatchapp(requestUrl.toString())) {
|
||||||
|
downloadInstallWatchapp(requestUrl)
|
||||||
|
return true // Indicate that the URL loading is handled
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, requestUrl)
|
||||||
|
wv.context.startActivity(intent)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldInterceptRequest(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): WebResourceResponse? {
|
||||||
|
LOG.info("shouldIntercept {} {} {}", request.method, request.url, iHttpService != null)
|
||||||
|
|
||||||
|
if (!request.method.equals("get", ignoreCase = true)) {
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
if (iHttpService == null) {
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
val httpHeaders = HttpHeaders()
|
||||||
|
for (header in request.requestHeaders.entries) {
|
||||||
|
httpHeaders.addHeader(header.key, header.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
val httpGetRequest = HttpGetRequest(request.url.toString(), httpHeaders)
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val internetResponseCapsule = Capsule<WebResourceResponse?>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
iHttpService!!.get(httpGetRequest, object : IHttpCallback.Stub() {
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun onResponse(response: HttpResponse) {
|
||||||
|
// Extract headers
|
||||||
|
val contentType = response.headers["content-type"]?.split(";")?.get(0) ?: "text/html"
|
||||||
|
val contentEncoding = response.headers["content-encoding"] ?: "UTF-8"
|
||||||
|
|
||||||
|
// Retrieve original payload as InputStream
|
||||||
|
val inputStream = ParcelFileDescriptor.AutoCloseInputStream(response.body)
|
||||||
|
|
||||||
|
// Clean up malformed HTML from Rebble
|
||||||
|
if (contentType.equals("text/html", ignoreCase = true)) {
|
||||||
|
val rawHtml = inputStream.bufferedReader(Charset.forName(contentEncoding)).use { it.readText() }
|
||||||
|
val cleanedHtml = Jsoup.parse(rawHtml).html()
|
||||||
|
val internetResponse = WebResourceResponse(
|
||||||
|
contentType,
|
||||||
|
contentEncoding,
|
||||||
|
response.status,
|
||||||
|
"OK",
|
||||||
|
response.headers.toMap(),
|
||||||
|
cleanedHtml.byteInputStream()
|
||||||
|
)
|
||||||
|
internetResponseCapsule.set(internetResponse)
|
||||||
|
} else {
|
||||||
|
// If not text/html, return original response
|
||||||
|
val internetResponse = WebResourceResponse(
|
||||||
|
contentType,
|
||||||
|
contentEncoding,
|
||||||
|
response.status,
|
||||||
|
"OK",
|
||||||
|
response.headers.toMap(),
|
||||||
|
inputStream
|
||||||
|
)
|
||||||
|
internetResponseCapsule.set(internetResponse)
|
||||||
|
}
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun onException(message: String?) {
|
||||||
|
throw RuntimeException(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
latch.await()
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return internetResponseCapsule.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView,
|
||||||
|
errorCode: Int,
|
||||||
|
description: String?,
|
||||||
|
failingUrl: String?
|
||||||
|
) {
|
||||||
|
GB.toast(
|
||||||
|
"Error: $description",
|
||||||
|
Toast.LENGTH_SHORT,
|
||||||
|
GB.ERROR
|
||||||
|
)
|
||||||
|
view.loadUrl("about:blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mainLooper = Looper.getMainLooper()
|
||||||
|
Handler(mainLooper).postDelayed({
|
||||||
|
webView!!.loadUrl(url)
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
webView!!.webChromeClient = object : WebChromeClient() {
|
||||||
|
override fun onPermissionRequest(request: PermissionRequest) {
|
||||||
|
request.grant(request.resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -580,6 +580,12 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Class<? extends Activity> getAppStoreActivity(final GBDevice device) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBondingStyle() {
|
public int getBondingStyle() {
|
||||||
return BONDING_STYLE_ASK;
|
return BONDING_STYLE_ASK;
|
||||||
|
|||||||
@@ -616,6 +616,13 @@ public interface DeviceCoordinator {
|
|||||||
*/
|
*/
|
||||||
Class<? extends Activity> getWatchfaceDesignerActivity(GBDevice device);
|
Class<? extends Activity> getWatchfaceDesignerActivity(GBDevice device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Activity class that will be used to download apps/watchfaces.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Class<? extends Activity> getAppStoreActivity(GBDevice device);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the device app cache directory.
|
* Returns the device app cache directory.
|
||||||
*/
|
*/
|
||||||
|
|||||||
-5
@@ -63,7 +63,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Capsule;
|
import nodomain.freeyourgadget.gadgetbridge.util.Capsule;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
import nodomain.freeyourgadget.internethelper.aidl.http.HttpGetRequest;
|
import nodomain.freeyourgadget.internethelper.aidl.http.HttpGetRequest;
|
||||||
import nodomain.freeyourgadget.internethelper.aidl.http.HttpHeaders;
|
import nodomain.freeyourgadget.internethelper.aidl.http.HttpHeaders;
|
||||||
@@ -115,10 +114,6 @@ public class AppsManagementActivity extends AbstractGBActivity {
|
|||||||
mCoordinator = mGBDevice.getDeviceCoordinator();
|
mCoordinator = mGBDevice.getDeviceCoordinator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toast(String data) {
|
|
||||||
GB.toast(data, Toast.LENGTH_LONG, GB.INFO);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|||||||
+6
@@ -38,6 +38,7 @@ import de.greenrobot.dao.Property;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.RebbleAppStoreActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
||||||
@@ -140,6 +141,11 @@ public class PebbleCoordinator extends AbstractBLClassicDeviceCoordinator {
|
|||||||
return AppManagerActivity.class;
|
return AppManagerActivity.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends Activity> getAppStoreActivity(final GBDevice device) {
|
||||||
|
return RebbleAppStoreActivity.class;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File getAppCacheDir() throws IOException {
|
public File getAppCacheDir() throws IOException {
|
||||||
return PebbleUtils.getPbwCacheDir();
|
return PebbleUtils.getPbwCacheDir();
|
||||||
|
|||||||
+2
-2
@@ -78,8 +78,8 @@ public class GBWebClient extends WebViewClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private WebResourceResponse mimicReply(Uri requestedUri) {
|
private WebResourceResponse mimicReply(Uri requestedUri) {
|
||||||
if (requestedUri.getHost() != null && (StringUtils.indexOfAny(requestedUri.getHost(), AllowedDomains) != -1)) {
|
if (requestedUri.getHost() != null && !requestedUri.toString().startsWith("file://")) {// && (StringUtils.indexOfAny(requestedUri.getHost(), AllowedDomains) != -1)) {
|
||||||
if (WebViewSingleton.getInstance().internetHelperBound) {
|
if (WebViewSingleton.getInstance().ensureInternetHelperBound()) {
|
||||||
LOG.debug("WEBVIEW forwarding request to the internet helper");
|
LOG.debug("WEBVIEW forwarding request to the internet helper");
|
||||||
try {
|
try {
|
||||||
return WebViewSingleton.getInstance().send(requestedUri);
|
return WebViewSingleton.getInstance().send(requestedUri);
|
||||||
|
|||||||
@@ -107,6 +107,28 @@ public class WebViewSingleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean ensureInternetHelperBound() {
|
||||||
|
if (contextWrapper != null && !internetHelperBound) {
|
||||||
|
String internetHelperPkg = "nodomain.freeyourgadget.internethelper";
|
||||||
|
String internetHelperCls = internetHelperPkg + ".HttpService";
|
||||||
|
try {
|
||||||
|
contextWrapper.getPackageManager().getApplicationInfo(internetHelperPkg, 0);
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setComponent(new ComponentName(internetHelperPkg, internetHelperCls));
|
||||||
|
|
||||||
|
final Intent intent1 = new Intent("nodomain.freeyourgadget.internethelper.HttpService");
|
||||||
|
intent1.setPackage("nodomain.freeyourgadget.internethelper");
|
||||||
|
contextWrapper.getApplicationContext().bindService(intent1, internetHelperConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
LOG.info("WEBVIEW: Internet helper bound successfully.");
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
LOG.info("WEBVIEW: Internet helper not installed, only mimicked HTTP requests will work.");
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
LOG.info("WEBVIEW: Permission for internet helper not granted, only mimicked HTTP requests will work.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return internetHelperBound;
|
||||||
|
}
|
||||||
|
|
||||||
public static WebViewSingleton getInstance() {
|
public static WebViewSingleton getInstance() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@@ -188,24 +210,7 @@ public class WebViewSingleton {
|
|||||||
webView.addJavascriptInterface(jsInterface, "GBjs");
|
webView.addJavascriptInterface(jsInterface, "GBjs");
|
||||||
webView.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500);
|
webView.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500);
|
||||||
});
|
});
|
||||||
if (contextWrapper != null && !internetHelperBound) {
|
ensureInternetHelperBound();
|
||||||
String internetHelperPkg = "nodomain.freeyourgadget.internethelper";
|
|
||||||
String internetHelperCls = internetHelperPkg + ".HttpService";
|
|
||||||
try {
|
|
||||||
contextWrapper.getPackageManager().getApplicationInfo(internetHelperPkg, 0);
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.setComponent(new ComponentName(internetHelperPkg, internetHelperCls));
|
|
||||||
|
|
||||||
final Intent intent1 = new Intent("nodomain.freeyourgadget.internethelper.HttpService");
|
|
||||||
intent1.setPackage("nodomain.freeyourgadget.internethelper");
|
|
||||||
contextWrapper.getApplicationContext().bindService(intent1, internetHelperConnection, Context.BIND_AUTO_CREATE);
|
|
||||||
LOG.info("WEBVIEW: Internet helper bound successfully.");
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
LOG.info("WEBVIEW: Internet helper not installed, only mimicked HTTP requests will work.");
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LOG.info("WEBVIEW: Permission for internet helper not granted, only mimicked HTTP requests will work.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M21.9,8.89l-1.05,-4.37c-0.22,-0.9 -1,-1.52 -1.91,-1.52H5.05C4.15,3 3.36,3.63 3.15,4.52L2.1,8.89c-0.24,1.02 -0.02,2.06 0.62,2.88C2.8,11.88 2.91,11.96 3,12.06V19c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-6.94c0.09,-0.09 0.2,-0.18 0.28,-0.28C21.92,10.96 22.15,9.91 21.9,8.89zM18.91,4.99l1.05,4.37c0.1,0.42 0.01,0.84 -0.25,1.17C19.57,10.71 19.27,11 18.77,11c-0.61,0 -1.14,-0.49 -1.21,-1.14L16.98,5L18.91,4.99zM13,5h1.96l0.54,4.52c0.05,0.39 -0.07,0.78 -0.33,1.07C14.95,10.85 14.63,11 14.22,11C13.55,11 13,10.41 13,9.69V5zM8.49,9.52L9.04,5H11v4.69C11,10.41 10.45,11 9.71,11c-0.34,0 -0.65,-0.15 -0.89,-0.41C8.57,10.3 8.45,9.91 8.49,9.52zM4.04,9.36L5.05,5h1.97L6.44,9.86C6.36,10.51 5.84,11 5.23,11c-0.49,0 -0.8,-0.29 -0.93,-0.47C4.03,10.21 3.94,9.78 4.04,9.36zM5,19v-6.03C5.08,12.98 5.15,13 5.23,13c0.87,0 1.66,-0.36 2.24,-0.95c0.6,0.6 1.4,0.95 2.31,0.95c0.87,0 1.65,-0.36 2.23,-0.93c0.59,0.57 1.39,0.93 2.29,0.93c0.84,0 1.64,-0.35 2.24,-0.95c0.58,0.59 1.37,0.95 2.24,0.95c0.08,0 0.15,-0.02 0.23,-0.03V19H5z"/>
|
||||||
|
</vector>
|
||||||
@@ -51,4 +51,17 @@
|
|||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fab_store"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="90dp"
|
||||||
|
app:srcCompat="@drawable/ic_store"
|
||||||
|
android:visibility="invisible"
|
||||||
|
/>
|
||||||
|
|
||||||
</android.widget.RelativeLayout>
|
</android.widget.RelativeLayout>
|
||||||
|
|||||||
@@ -4448,4 +4448,5 @@
|
|||||||
<string name="movement_evaluation">Movement Evaluation</string>
|
<string name="movement_evaluation">Movement Evaluation</string>
|
||||||
<string name="pref_notifications_pictures_enable">Send notification pictures</string>
|
<string name="pref_notifications_pictures_enable">Send notification pictures</string>
|
||||||
<string name="pref_notifications_pictures_enable_summary">Send notification pictures for current device</string>
|
<string name="pref_notifications_pictures_enable_summary">Send notification pictures for current device</string>
|
||||||
|
<string name="activity_title_rebble_app_store">Rebble app store</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user