summaryrefslogtreecommitdiff
path: root/main/src
diff options
context:
space:
mode:
authorArne Schwabe <arne@rfc2549.org>2017-11-26 23:41:50 -0500
committerArne Schwabe <arne@rfc2549.org>2017-11-26 23:51:44 -0500
commit7b0af007a717c72d957ed413bb91ae17da9343a1 (patch)
treeac5a67f74bf75f5d4cc6fc9808e1f25fff1b7f5b /main/src
parentbfc020a66bad76966b37f1a2be7887c0d9a6dc91 (diff)
New NDK and OpenSSL Speed test
Diffstat (limited to 'main/src')
-rw-r--r--main/src/main/AndroidManifest.xml62
-rw-r--r--main/src/main/java/android/support/v4n/view/ViewPager.java12
-rw-r--r--main/src/main/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java192
-rw-r--r--main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java7
-rw-r--r--main/src/main/java/de/blinkt/openvpn/fragments/GeneralSettings.java5
-rw-r--r--main/src/main/res/layout/openssl_speed.xml40
-rw-r--r--main/src/main/res/layout/speedviewitem.xml54
-rwxr-xr-xmain/src/main/res/values/strings.xml8
-rw-r--r--main/src/main/res/values/untranslatable.xml6
-rw-r--r--main/src/main/res/xml/general_settings.xml4
10 files changed, 343 insertions, 47 deletions
diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml
index 925d7945..e57cf096 100644
--- a/main/src/main/AndroidManifest.xml
+++ b/main/src/main/AndroidManifest.xml
@@ -1,10 +1,7 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2012-2016 Arne Schwabe
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
- -->
-
+-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.blinkt.openvpn">
@@ -16,6 +13,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> -->
+
<!-- <uses-permission android:name="com.android.vending.BILLING" /> -->
<uses-feature
@@ -27,8 +25,8 @@
<application
android:name=".core.ICSOpenVPNApplication"
- android:appCategory="productivity"
android:allowBackup="true"
+ android:appCategory="productivity"
android:banner="@mipmap/banner_tv"
android:icon="@mipmap/ic_launcher"
android:label="@string/app"
@@ -39,7 +37,6 @@
android:name=".activities.VPNPreferences"
android:exported="false"
android:windowSoftInputMode="stateHidden" />
-
<activity
android:name=".activities.DisconnectVPN"
android:autoRemoveFromRecents="true"
@@ -47,7 +44,6 @@
android:noHistory="true"
android:taskAffinity=".DisconnectVPN"
android:theme="@style/blinkt.dialog" />
-
<activity
android:name=".activities.LogWindow"
android:allowTaskReparenting="true"
@@ -68,39 +64,36 @@
<service
android:name=".core.OpenVPNService"
- android:process=":openvpn"
- android:permission="android.permission.BIND_VPN_SERVICE" >
+ android:permission="android.permission.BIND_VPN_SERVICE"
+ android:process=":openvpn">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
-
</service>
-
<service
android:name=".api.ExternalOpenVPNService"
android:process=":openvpn"
tools:ignore="ExportedService">
-
<intent-filter>
<action android:name="de.blinkt.openvpn.api.IOpenVPNAPIService" />
</intent-filter>
</service>
-
- <service android:name=".core.OpenVPNStatusService"
- android:process=":openvpn"
- android:exported="false" />
-
-
+ <service
+ android:name=".core.OpenVPNStatusService"
+ android:exported="false"
+ android:process=":openvpn" />
<service
android:name=".OpenVPNTileService"
- android:value="true"
- android:label="@string/qs_title"
android:icon="@drawable/ic_quick"
- android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+ android:label="@string/qs_title"
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
+ android:value="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
- <meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
+
+ <meta-data
+ android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="false" />
</service>
@@ -119,7 +112,6 @@
<intent-filter android:priority="999">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
-
</intent-filter>
</receiver>
@@ -130,7 +122,8 @@
android:taskAffinity=".ConfigConverter"
android:uiOptions="splitActionBarWhenNarrow"
tools:ignore="ExportedActivity">
- <intent-filter android:label="@string/import_config"
+ <intent-filter
+ android:label="@string/import_config"
tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
@@ -139,7 +132,8 @@
<data android:mimeType="application/x-openvpn-profile" />
</intent-filter>
- <intent-filter android:label="@string/import_config"
+ <intent-filter
+ android:label="@string/import_config"
tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
@@ -181,8 +175,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
-
<activity
android:name=".api.Intents"
android:autoRemoveFromRecents="true"
@@ -197,8 +189,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
-
<activity
android:name=".activities.CreateShortcuts"
android:excludeFromRecents="true"
@@ -207,6 +197,7 @@
android:theme="@android:style/Theme.DeviceDefault.Light.DialogWhenLarge">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
+
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
@@ -218,7 +209,6 @@
android:grantUriPermissions="true"
tools:ignore="ExportedContentProvider" />
-
<!--
<receiver android:name="core.GetRestrictionReceiver">
<intent-filter>
@@ -226,6 +216,14 @@
</intent-filter>
</receiver>
-->
+ <activity
+ android:name=".activities.OpenSSLSpeed"
+ android:label="@string/title_activity_open_sslspeed"
+ android:parentActivityName=".activities.MainActivity">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.blinkt.openvpn.activities.MainActivity" />
+ </activity>
</application>
-</manifest>
+</manifest> \ No newline at end of file
diff --git a/main/src/main/java/android/support/v4n/view/ViewPager.java b/main/src/main/java/android/support/v4n/view/ViewPager.java
index 4e44bd99..53daa70d 100644
--- a/main/src/main/java/android/support/v4n/view/ViewPager.java
+++ b/main/src/main/java/android/support/v4n/view/ViewPager.java
@@ -33,7 +33,6 @@ import android.support.annotation.DrawableRes;
import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.KeyEventCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
@@ -2508,17 +2507,6 @@ public class ViewPager extends ViewGroup {
case KeyEvent.KEYCODE_DPAD_RIGHT:
handled = arrowScroll(FOCUS_RIGHT);
break;
- case KeyEvent.KEYCODE_TAB:
- if (Build.VERSION.SDK_INT >= 11) {
- // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
- // before Android 3.0. Ignore the tab key on those devices.
- if (KeyEventCompat.hasNoModifiers(event)) {
- handled = arrowScroll(FOCUS_FORWARD);
- } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
- handled = arrowScroll(FOCUS_BACKWARD);
- }
- }
- break;
}
}
return handled;
diff --git a/main/src/main/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java b/main/src/main/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java
new file mode 100644
index 00000000..e10778f1
--- /dev/null
+++ b/main/src/main/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2012-2017 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.activities;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.app.Activity;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.Locale;
+import java.util.Vector;
+
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.core.NativeUtils;
+import de.blinkt.openvpn.core.OpenVPNService;
+
+public class OpenSSLSpeed extends Activity {
+
+ private static SpeeedTest runTestAlgorithms;
+ private EditText mCipher;
+ private SpeedArrayAdapter mAdapter;
+ private ListView mListView;
+
+
+ static class SpeedArrayAdapter extends ArrayAdapter<SpeedResult> {
+
+ private final Context mContext;
+ private final LayoutInflater mInflater;
+
+ public SpeedArrayAdapter(@NonNull Context context) {
+ super(context, 0);
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+
+ }
+
+ class ViewHolder {
+ TextView ciphername;
+ TextView blocksize;
+ TextView blocksInTime;
+ TextView speed;
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
+ SpeedResult res = getItem(position);
+ if (view == null) {
+ view = mInflater.inflate(R.layout.speedviewitem, parent, false);
+ ViewHolder holder = new ViewHolder();
+ holder.ciphername = view.findViewById(R.id.ciphername);
+ holder.speed = view.findViewById(R.id.speed);
+ holder.blocksize = view.findViewById(R.id.blocksize);
+ holder.blocksInTime = view.findViewById(R.id.blocksintime);
+ view.setTag(holder);
+ }
+
+ ViewHolder holder = (ViewHolder) view.getTag();
+
+ double total = res.count * res.length;
+ String size = OpenVPNService.humanReadableByteCount((long) res.length, false, mContext.getResources());
+
+ holder.blocksize.setText(size);
+ holder.ciphername.setText(res.algorithm);
+
+ if (res.failed) {
+ holder.blocksInTime.setText(R.string.openssl_error);
+ holder.speed.setText("-");
+ } else if (res.running) {
+ holder.blocksInTime.setText(R.string.running_test);
+ holder.speed.setText("-");
+ } else {
+ String totalBytes = OpenVPNService.humanReadableByteCount((long) total, false, mContext.getResources());
+ // TODO: Fix localisation here
+ String blockPerSec = OpenVPNService.humanReadableByteCount((long) (total / res.time), false, mContext.getResources()) + "/s";
+ holder.speed.setText(blockPerSec);
+ holder.blocksInTime.setText(String.format(Locale.ENGLISH, "%d blocks (%s) in %2.1fs", (long) res.count, totalBytes, res.time));
+ }
+
+ return view;
+
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.openssl_speed);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ findViewById(R.id.testSpecific).setOnClickListener((view) -> {
+ runAlgorithms(mCipher.getText().toString());
+ });
+ mCipher = (EditText) findViewById(R.id.ciphername);
+
+ mListView = findViewById(R.id.results);
+
+ mAdapter = new SpeedArrayAdapter(this);
+ mListView.setAdapter(mAdapter);
+
+ }
+
+ private void runAlgorithms(String algorithms) {
+ if (runTestAlgorithms != null)
+ runTestAlgorithms.cancel(true);
+ runTestAlgorithms = new SpeeedTest();
+ runTestAlgorithms.execute(algorithms.split(" "));
+ }
+
+
+ static class SpeedResult {
+ String algorithm;
+ boolean failed = false;
+
+ double count;
+ double time;
+ int length;
+ public boolean running=true;
+
+ SpeedResult(String algorithm) {
+ this.algorithm = algorithm;
+ }
+ }
+
+
+ private class SpeeedTest extends AsyncTask<String, SpeedResult, SpeedResult[]> {
+
+
+ private boolean mCancel = false;
+
+ @Override
+ protected SpeedResult[] doInBackground(String... strings) {
+ Vector<SpeedResult> mResult = new Vector<>();
+
+ for (String algorithm : strings) {
+
+ for (int i = 0; i < NativeUtils.openSSLlengths.length && !mCancel; i++) {
+ SpeedResult result = new SpeedResult(algorithm);
+ result.length = NativeUtils.openSSLlengths[i];
+ mResult.add(result);
+ publishProgress(result);
+ double[] resi = NativeUtils.getOpenSSLSpeed(algorithm, i);
+ if (resi == null) {
+ result.failed = true;
+ } else {
+ result.count = resi[1];
+ result.time = resi[2];
+ }
+ result.running = false;
+ publishProgress(result);
+ }
+ }
+
+ return mResult.toArray(new SpeedResult[mResult.size()]);
+
+ }
+
+ @Override
+ protected void onProgressUpdate(SpeedResult... values) {
+ for (SpeedResult r : values) {
+ if (r.running)
+ mAdapter.add(r);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(SpeedResult[] speedResult) {
+
+ }
+
+ @Override
+ protected void onCancelled(SpeedResult[] speedResults) {
+ mCancel = true;
+ }
+ }
+
+
+}
diff --git a/main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java b/main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
index ea003d41..70c7455a 100644
--- a/main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
+++ b/main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
@@ -18,6 +18,13 @@ public class NativeUtils {
public static native String getNativeAPI();
+
+ public final static int[] openSSLlengths = {
+ 16, 64, 256, 1024, 8 * 1024, 16 * 1024
+ };
+
+ public static native double[] getOpenSSLSpeed(String algorithm, int testnum);
+
static {
System.loadLibrary("opvpnutil");
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN)
diff --git a/main/src/main/java/de/blinkt/openvpn/fragments/GeneralSettings.java b/main/src/main/java/de/blinkt/openvpn/fragments/GeneralSettings.java
index 700095d7..ef49c656 100644
--- a/main/src/main/java/de/blinkt/openvpn/fragments/GeneralSettings.java
+++ b/main/src/main/java/de/blinkt/openvpn/fragments/GeneralSettings.java
@@ -12,6 +12,7 @@ import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -28,6 +29,7 @@ import android.preference.PreferenceManager;
import de.blinkt.openvpn.BuildConfig;
import de.blinkt.openvpn.R;
import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.activities.OpenSSLSpeed;
import de.blinkt.openvpn.api.ExternalAppDatabase;
import de.blinkt.openvpn.core.ProfileManager;
@@ -66,6 +68,7 @@ public class GeneralSettings extends PreferenceFragment implements OnPreferenceC
Preference clearapi = findPreference("clearapi");
clearapi.setOnPreferenceClickListener(this);
+ findPreference("osslspeed").setOnPreferenceClickListener(this);
if(devHacks.getPreferenceCount()==0)
getPreferenceScreen().removePreference(devHacks);
@@ -168,6 +171,8 @@ public class GeneralSettings extends PreferenceFragment implements OnPreferenceC
builder.setNegativeButton(android.R.string.cancel, null);
builder.setMessage(getString(R.string.clearappsdialog,getExtAppList("\n")));
builder.show();
+ } else if (preference.getKey().equals("osslspeed")) {
+ startActivity(new Intent(getActivity(), OpenSSLSpeed.class));
}
return true;
diff --git a/main/src/main/res/layout/openssl_speed.xml b/main/src/main/res/layout/openssl_speed.xml
new file mode 100644
index 00000000..c23d3567
--- /dev/null
+++ b/main/src/main/res/layout/openssl_speed.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (c) 2012-2017 Arne Schwabe
+ ~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+
+
+ <EditText
+ android:id="@+id/ciphername"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ems="10"
+ android:text="@string/default_cipherlist_test"
+ android:hint="@string/openssl_cipher_name"
+ android:inputType="textPersonName" />
+
+ <Button
+ android:id="@+id/testSpecific"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/test_algoirhtms" />
+ </LinearLayout>
+
+ <ListView
+ android:id="@+id/results"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/main/src/main/res/layout/speedviewitem.xml b/main/src/main/res/layout/speedviewitem.xml
new file mode 100644
index 00000000..06a760ec
--- /dev/null
+++ b/main/src/main/res/layout/speedviewitem.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (c) 2012-2017 Arne Schwabe
+ ~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/ciphername"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ android:minWidth="100sp"
+ android:textAlignment="textEnd"
+ tools:text="aes-256-gcm" />
+
+ <TextView
+ android:id="@+id/blocksize"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/ciphername"
+ android:gravity="end"
+ android:minWidth="100sp"
+ android:textAlignment="textEnd"
+ tools:text="1024 kB" />
+
+
+ <TextView
+ android:id="@+id/blocksintime"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="20sp"
+ android:layout_marginStart="20sp"
+ android:layout_toEndOf="@id/ciphername"
+ android:layout_toRightOf="@id/ciphername"
+ tools:text="12345 blocks in 5s" />
+
+ <TextView
+ android:id="@+id/speed"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/blocksintime"
+ android:layout_marginLeft="20dp"
+ android:layout_marginStart="20dp"
+ android:layout_marginTop="2sp"
+ android:layout_toEndOf="@id/ciphername"
+ android:layout_toRightOf="@id/ciphername"
+ tools:text="772 MB/s" />
+
+
+</RelativeLayout>
diff --git a/main/src/main/res/values/strings.xml b/main/src/main/res/values/strings.xml
index 79c84223..67248dae 100755
--- a/main/src/main/res/values/strings.xml
+++ b/main/src/main/res/values/strings.xml
@@ -446,5 +446,13 @@
<string name="channel_name_status">Connection status change</string>
<string name="channel_description_status">Status changes of the OpenVPN connection (Connecting, authenticating,…)</string>
<string name="weakmd_title">Weak (MD5) hashes in certificate signature (SSL_CTX_use_certificate md too weak)</string>
+ <string name="title_activity_open_sslspeed">OpenSSL Speed Test</string>
+ <string name="testcommon">Test commonly used algorithms</string>
+ <string name="testspecific">Test common algorithm</string>
+ <string name="openssl_cipher_name">OpenSSL cipher names</string>
+ <string name="osslspeedtest">OpenSSL Crypto Speed test</string>
+ <string name="openssl_error">OpenSSL returned an error</string>
+ <string name="running_test">Running test…</string>
+ <string name="test_algoirhtms">Test selected algorithms</string>
</resources>
diff --git a/main/src/main/res/values/untranslatable.xml b/main/src/main/res/values/untranslatable.xml
index 8e6f2c75..f8f0cc96 100644
--- a/main/src/main/res/values/untranslatable.xml
+++ b/main/src/main/res/values/untranslatable.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2012-2016 Arne Schwabe
~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
-->
@@ -43,11 +42,12 @@
<item>-1</item>
</string-array>
<string name="crash_toast_text">OpenVPN for Android crashed, crash reported</string>
-
+
<!-- These strings should not be visible to the user -->
<string name="state_user_vpn_permission" translatable="false">Waiting for user permission to use VPN API</string>
<string name="state_user_vpn_password" translatable="false">Waiting for user VPN password</string>
<string name="state_user_vpn_password_cancelled" translatable="false">VPN password input dialog cancelled</string>
<string name="state_user_vpn_permission_cancelled" translatable="false">VPN API permission dialog cancelled</string>
+ <string name="default_cipherlist_test" translatable="false">aes-256-gcm bf-cbc sha1</string>
</resources>
diff --git a/main/src/main/res/xml/general_settings.xml b/main/src/main/res/xml/general_settings.xml
index f53e6d5b..3b8ba4f5 100644
--- a/main/src/main/res/xml/general_settings.xml
+++ b/main/src/main/res/xml/general_settings.xml
@@ -59,6 +59,10 @@
android:summary="@string/screenoff_summary"
android:title="@string/screenoff_title"/>
+ <Preference
+ android:key="osslspeed"
+ android:persistent="false"
+ android:title="@string/osslspeedtest" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/device_specific" android:key="device_hacks">