mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Take direct internet access into account
This commit is contained in:
committed by
Arjan Schrijver
parent
d2cd47231a
commit
528bb8d362
@@ -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;
|
||||
}
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
+150
-60
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -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);
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user