Merge pull request #26 from threethan/main
Add Background Audio, Update UI
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
<bytecodeTargetLevel target="15" />
|
||||
</component>
|
||||
</project>
|
17
.idea/deploymentTargetDropDown.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<runningDeviceTargetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="RUNNING_DEVICE_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="SERIAL_NUMBER" />
|
||||
<value value="1WMHH834Y71103" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</runningDeviceTargetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2022-02-05T10:56:11.381491200Z" />
|
||||
</component>
|
||||
</project>
|
@ -1,6 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/drawable-v24/buttonbg.xml" value="0.1925" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.2445" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/drawable/buttonbg.xml" value="0.492" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/drawable/buttonbgfocus.xml" value="0.1925" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/drawable/buttonbgx.xml" value="0.2855" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/drawable/hover.xml" value="0.282" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/drawable/ic_launcher_background.xml" value="0.2445" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/drawable/windowbg.xml" value="0.2995" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/layout/activity_main.xml" value="0.22" />
|
||||
<entry key="..\:/Portable Apps/Programming/Oculess/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" value="0.216" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_15_PREVIEW" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
12
README.md
@ -5,6 +5,7 @@
|
||||
- (Fully) Log Out OF Oculus / FaceBook account (aka. Disable Oculus Companion)
|
||||
- Disable telemetry
|
||||
- Disable Updates
|
||||
- (New) Enable Audio in Background
|
||||
|
||||
## Tutorial
|
||||
|
||||
@ -52,5 +53,14 @@ Only use for to [disable telemetry](https://github.com/basti564/Oculess#disable-
|
||||
5. Click “REMOVE ACCOUNT”
|
||||
(Your accounts should return after like 5 minutes or a restart)
|
||||
|
||||
### Enable Background Audio (temporary)
|
||||
1. Follow steps 1-3 of "Disable Telemetry" to set device owner. This only needs to be done once.
|
||||
2. Click on "ENABLE BACKGROUND AUDIO"
|
||||
|
||||
Apps will now be allowed to play audio in the background, and record audio as if they have been given microphone access.
|
||||
Music will be paused when a game is quit. If this happens, you will have to reopen the music app and click play.
|
||||
Note that apps might still be killed. You may need to reopen apps or click the button again if oculess is killed.
|
||||
Setting applies to all apps until the next reboot.
|
||||
|
||||
## Screenshot
|
||||
![Screenshot](https://user-images.githubusercontent.com/34898868/135829932-f043a990-3fdc-433e-8eb4-e6f34d997e52.png)
|
||||
![Screenshot](https://user-images.githubusercontent.com/12588584/152667664-40db8b5b-1e93-4518-836f-e1de3782a07a.jpg)
|
||||
|
@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.bos.oculess"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 3
|
||||
versionName "1.2"
|
||||
versionCode 5
|
||||
versionName "1.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@ -33,7 +33,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
|
21
app/oculess.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1"
|
||||
id="Horizontal" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 351.5 351.6"
|
||||
style="enable-background:new 0 0 351.5 351.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#1C1E20;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M312.2,93.7c-0.7-0.5-1.4-1.1-2.1-1.6l-38.6,60.3c5.8,6.3,9.1,14.6,9.1,23.4c0.1,11.5-5.5,22.1-15,28.6
|
||||
c-4.7,3.3-9.9,5.2-15.4,6.1c-5.4,0.8-11,0.7-16.5,0.7l-45.4,70.8h35.1c6.7,0,13.4,0,20.1-0.1c9.1-0.3,17.8-1.3,26.7-3.4
|
||||
c15.4-3.7,29.6-10.6,41.9-20.5c24.9-20,39.3-50.1,39.3-82.1C351.5,143.8,337.1,113.7,312.2,93.7z"/>
|
||||
<path class="st0" d="M108.9,281.8l45.3-70.6h-36c-5.7,0-11.2,0.1-16.7-0.7c-5.7-0.8-10.8-2.8-15.4-6.1c-9.3-6.5-15-17.1-15-28.6
|
||||
c0-11.5,5.7-22.1,15-28.6c4.7-3.3,9.9-5.2,15.4-6.1c5.5-0.8,11.2-0.7,16.7-0.7h81.4l45.3-70.6c-0.5,0-0.9,0-1.4-0.1
|
||||
c-6.7-0.3-13.4-0.1-20.1-0.1h-95.2c-6.7,0-13.4,0-20.1,0.1c-9.1,0.3-17.8,1.3-26.7,3.4c-15.4,3.7-29.6,10.6-41.9,20.5
|
||||
C14.4,113.7,0,143.8,0,175.8c0,32,14.4,62.1,39.3,82.1c12.3,9.9,26.6,16.8,42,20.5c8.8,2.1,17.7,3.1,26.7,3.4
|
||||
C108.4,281.8,108.6,281.8,108.9,281.8z"/>
|
||||
</g>
|
||||
<rect x="120.2" y="-0.2" transform="matrix(0.8418 0.5398 -0.5398 0.8418 119.2303 -55.1826)" class="st0" width="67" height="352"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -9,6 +9,7 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Oculess">
|
||||
<receiver
|
||||
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 16 KiB |
@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
|
||||
class DevAdminReceiver : DeviceAdminReceiver() {
|
||||
private val TAG = "DeviceAdminReceiver"
|
||||
|
@ -1,21 +1,32 @@
|
||||
package com.bos.oculess
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AppOpsManager
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bos.oculess.util.AppOpsUtil
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
import android.os.Handler as Handler
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
var audioApps: Array<String>? = null
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
@ -36,6 +47,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val viewAccountsBtn = findViewById<Button>(R.id.viewAccountsBtn)
|
||||
val viewOtaBtn = findViewById<Button>(R.id.viewOtaBtn)
|
||||
val viewTelemetryBtn = findViewById<Button>(R.id.viewTelemetryBtn)
|
||||
val viewPermissionsBtn = findViewById<Button>(R.id.viewPermissionsBtn)
|
||||
|
||||
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
|
||||
|
||||
@ -76,6 +88,38 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
viewPermissionsBtn.setOnClickListener {
|
||||
if (dpm.isDeviceOwnerApp(packageName)) {
|
||||
updateAudioPackages()
|
||||
|
||||
// Set recurring task (every 2s)
|
||||
val handler = Handler()
|
||||
val run = object : Runnable {
|
||||
override fun run() {
|
||||
handler.postDelayed(this, 2000)
|
||||
enableBackgroundAudio()
|
||||
}
|
||||
}
|
||||
handler.post(run)
|
||||
|
||||
viewPermissionsBtn.isEnabled = false
|
||||
|
||||
} else {
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
|
||||
builder.setTitle(getString(R.string.title))
|
||||
builder.setMessage(getString(R.string.message2))
|
||||
builder.setPositiveButton(
|
||||
getString(R.string.ok)
|
||||
) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
val alertDialog: AlertDialog = builder.create()
|
||||
alertDialog.show()
|
||||
alertDialog.window!!.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
viewAdminsBtn.setOnClickListener {
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
|
||||
builder.setTitle(getString(R.string.title))
|
||||
@ -213,4 +257,27 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
fun enableBackgroundAudio() {
|
||||
for (app in audioApps!!) {
|
||||
val info: ApplicationInfo = applicationContext.packageManager.getApplicationInfo(app, 0)
|
||||
// Allow background audio playback
|
||||
AppOpsUtil.setMode(applicationContext, 28, info.uid, app, AppOpsManager.MODE_ALLOWED)
|
||||
// Allow background audio recording (app must have recording permission for this to do anything)
|
||||
AppOpsUtil.setMode(applicationContext, 27, info.uid, app, AppOpsManager.MODE_ALLOWED)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAudioPackages() {
|
||||
/* Get All Installed Packages for Audio */
|
||||
//getInstalledPackages longer works properly in android 11, but quest is on android 10 so it's fine
|
||||
val packageinfos = applicationContext.packageManager.getInstalledPackages(0)
|
||||
val packageNames = arrayListOf<String>()
|
||||
for (packageinfo in packageinfos) {
|
||||
packageNames.add(packageinfo.packageName)
|
||||
}
|
||||
audioApps = packageNames.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
|
89
app/src/main/java/com/bos/oculess/util/AppOpsUtil.java
Normal file
@ -0,0 +1,89 @@
|
||||
package com.bos.oculess.util;
|
||||
|
||||
import android.app.AppOpsManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.bos.oculess.util.Hack;
|
||||
|
||||
/**
|
||||
* @author heruoxin @ CatchingNow Inc.
|
||||
* @since 2019-03-31
|
||||
*/
|
||||
public class AppOpsUtil {
|
||||
private static AppOpsManager sManager;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.P)
|
||||
public static void setMode(Context context, int opCode, int uid, String packageName, int mode) {
|
||||
if (sManager == null) {
|
||||
sManager = context.getSystemService(AppOpsManager.class);
|
||||
}
|
||||
Hack.into(AppOpsManager.class)
|
||||
.method("setMode")
|
||||
.returning(void.class)
|
||||
.withParams(int.class, int.class, String.class, int.class)
|
||||
.invoke(opCode, uid, packageName, mode)
|
||||
.on(sManager);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.P)
|
||||
public static void startWatchingMode(Context context, int opCode, String packageName, int flags, final AppOpsManager.OnOpChangedListener callback) {
|
||||
if (sManager == null) {
|
||||
sManager = context.getSystemService(AppOpsManager.class);
|
||||
}
|
||||
Hack.into(AppOpsManager.class)
|
||||
.method("startWatchingMode")
|
||||
.returning(void.class)
|
||||
.withParams(int.class, String.class, int.class, AppOpsManager.OnOpChangedListener.class)
|
||||
.invoke(opCode, packageName, flags, callback)
|
||||
.on(sManager);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.P)
|
||||
public static void setUidMode(Context context, int opCode, int uid, int mode) {
|
||||
if (sManager == null) {
|
||||
sManager = context.getSystemService(AppOpsManager.class);
|
||||
}
|
||||
Hack.into(AppOpsManager.class)
|
||||
.method("setUidMode")
|
||||
.returning(void.class)
|
||||
.withParams(int.class, int.class, int.class)
|
||||
.invoke(opCode, uid, mode)
|
||||
.on(sManager);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.P)
|
||||
public static void resetAllModes(int userId, String packageName) throws RemoteException {
|
||||
IBinder binder = Hack.into("android.os.ServiceManager")
|
||||
.staticMethod("getService")
|
||||
.returning(IBinder.class)
|
||||
.withParams(String.class)
|
||||
.invoke(Context.APP_OPS_SERVICE)
|
||||
.statically();
|
||||
|
||||
Object appopsService;
|
||||
try {
|
||||
appopsService = Hack.into("com.android.internal.app.IAppOpsService$Stub")
|
||||
.staticMethod("asInterface")
|
||||
.returning(Class.forName("com.android.internal.app.IAppOpsService"))
|
||||
.withParams(IBinder.class)
|
||||
.invoke(binder)
|
||||
.statically();
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Hack.into("com.android.internal.app.IAppOpsService")
|
||||
.method("resetAllModes")
|
||||
.returning(void.class)
|
||||
.throwing(RemoteException.class)
|
||||
.withParams(int.class, String.class)
|
||||
.invoke(userId, packageName)
|
||||
.on(appopsService);
|
||||
}
|
||||
|
||||
}
|
861
app/src/main/java/com/bos/oculess/util/Hack.java
Normal file
@ -0,0 +1,861 @@
|
||||
package com.bos.oculess.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.AccessibleObject;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Java reflection helper optimized for hacking non-public APIs.
|
||||
* The core design philosophy behind is compile-time consistency enforcement.
|
||||
*
|
||||
* It's suggested to declare all hacks in a centralized point, typically as static fields in a class.
|
||||
* Then call it during application initialization, thus they are verified all together in an early stage.
|
||||
* If any assertion failed, a fall-back strategy is suggested.
|
||||
*
|
||||
* <p>https://gist.github.com/oasisfeng/75d3774ca5441372f049818de4d52605
|
||||
*
|
||||
* @see Demo
|
||||
*
|
||||
* @author Oasis
|
||||
*/
|
||||
@SuppressWarnings({"Convert2Lambda", "WeakerAccess", "unused"})
|
||||
public class Hack {
|
||||
|
||||
public static Class<?> ANY_TYPE = $.class; private static class $ {}
|
||||
|
||||
public static class AssertionException extends Throwable {
|
||||
|
||||
private Class<?> mClass;
|
||||
private Field mHackedField;
|
||||
private Method mHackedMethod;
|
||||
private String mHackedFieldName;
|
||||
private String mHackedMethodName;
|
||||
private Class<?>[] mParamTypes;
|
||||
|
||||
AssertionException(final String e) { super(e); }
|
||||
AssertionException(final Exception e) { super(e); }
|
||||
|
||||
@Override public String toString() {
|
||||
return getCause() != null ? getClass().getName() + ": " + getCause() : super.toString();
|
||||
}
|
||||
|
||||
public String getDebugInfo() {
|
||||
final StringBuilder info = new StringBuilder(getCause() != null ? getCause().toString() : super.toString());
|
||||
final Throwable cause = getCause();
|
||||
if (cause instanceof NoSuchMethodException) {
|
||||
info.append(" Potential candidates:");
|
||||
final int initial_length = info.length();
|
||||
final String name = getHackedMethodName();
|
||||
if (name != null) {
|
||||
for (final Method method : getHackedClass().getDeclaredMethods())
|
||||
if (method.getName().equals(name)) // Exact name match
|
||||
info.append(' ').append(method);
|
||||
if (info.length() == initial_length)
|
||||
for (final Method method : getHackedClass().getDeclaredMethods())
|
||||
if (method.getName().startsWith(name)) // Name prefix match
|
||||
info.append(' ').append(method);
|
||||
if (info.length() == initial_length)
|
||||
for (final Method method : getHackedClass().getDeclaredMethods())
|
||||
if (! method.getName().startsWith("-")) // Dump all but generated methods
|
||||
info.append(' ').append(method);
|
||||
} else for (final Constructor<?> constructor : getHackedClass().getDeclaredConstructors())
|
||||
info.append(' ').append(constructor);
|
||||
} else if (cause instanceof NoSuchFieldException) {
|
||||
info.append(" Potential candidates:");
|
||||
final int initial_length = info.length();
|
||||
final String name = getHackedFieldName();
|
||||
final Field[] fields = getHackedClass().getDeclaredFields();
|
||||
for (final Field field : fields)
|
||||
if (field.getName().equals(name)) // Exact name match
|
||||
info.append(' ').append(field);
|
||||
if (info.length() == initial_length) for (final Field field : fields)
|
||||
if (field.getName().startsWith(name)) // Name prefix match
|
||||
info.append(' ').append(field);
|
||||
if (info.length() == initial_length) for (final Field field : fields)
|
||||
if (! field.getName().startsWith("$")) // Dump all but generated fields
|
||||
info.append(' ').append(field);
|
||||
}
|
||||
return info.toString();
|
||||
}
|
||||
|
||||
public Class<?> getHackedClass() {
|
||||
return mClass;
|
||||
}
|
||||
|
||||
AssertionException setHackedClass(final Class<?> hacked_class) {
|
||||
mClass = hacked_class; return this;
|
||||
}
|
||||
|
||||
public Method getHackedMethod() {
|
||||
return mHackedMethod;
|
||||
}
|
||||
|
||||
AssertionException setHackedMethod(final Method method) {
|
||||
mHackedMethod = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getHackedMethodName() {
|
||||
return mHackedMethodName;
|
||||
}
|
||||
|
||||
AssertionException setHackedMethodName(final String method) {
|
||||
mHackedMethodName = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Class<?>[] getParamTypes() { return mParamTypes; }
|
||||
|
||||
AssertionException setParamTypes(final Class<?>[] param_types) {
|
||||
mParamTypes = param_types;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Field getHackedField() {
|
||||
return mHackedField;
|
||||
}
|
||||
|
||||
AssertionException setHackedField(final Field field) {
|
||||
mHackedField = field;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getHackedFieldName() {
|
||||
return mHackedFieldName;
|
||||
}
|
||||
|
||||
AssertionException setHackedFieldName(final String field) {
|
||||
mHackedFieldName = field;
|
||||
return this;
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
|
||||
/** Placeholder for unchecked exception */
|
||||
public class Unchecked extends RuntimeException {}
|
||||
|
||||
/** Use {@link Hack#setAssertionFailureHandler(AssertionFailureHandler) } to set the global handler */
|
||||
public interface AssertionFailureHandler {
|
||||
void onAssertionFailure(AssertionException failure);
|
||||
}
|
||||
|
||||
public static class FieldToHack<C> {
|
||||
|
||||
protected @Nullable
|
||||
<T> Field findField(final @Nullable Class<T> type) {
|
||||
if (mClass == ANY_TYPE) return null; // AnyType as a internal indicator for class not found.
|
||||
Field field = null;
|
||||
try {
|
||||
field = mClass.getDeclaredField(mName);
|
||||
if (Modifier.isStatic(mModifiers) != Modifier.isStatic(field.getModifiers())) {
|
||||
fail(new AssertionException(field + (Modifier.isStatic(mModifiers) ? " is not static" : " is static")).setHackedFieldName(mName));
|
||||
field = null;
|
||||
} else if (mModifiers > 0 && (field.getModifiers() & mModifiers) != mModifiers) {
|
||||
fail(new AssertionException(field + " does not match modifiers: " + mModifiers).setHackedFieldName(mName));
|
||||
field = null;
|
||||
} else if (! field.isAccessible()) field.setAccessible(true);
|
||||
} catch (final NoSuchFieldException e) {
|
||||
final AssertionException hae = new AssertionException(e);
|
||||
hae.setHackedClass(mClass);
|
||||
hae.setHackedFieldName(mName);
|
||||
fail(hae);
|
||||
}
|
||||
|
||||
if (type != null && field != null && ! type.isAssignableFrom(field.getType()))
|
||||
fail(new AssertionException(new ClassCastException(field + " is not of type " + type)).setHackedField(field));
|
||||
return field;
|
||||
}
|
||||
|
||||
/** @param modifiers the modifiers this field must have */
|
||||
protected FieldToHack(final Class<C> clazz, final String name, final int modifiers) {
|
||||
mClass = clazz;
|
||||
mName = name;
|
||||
mModifiers = modifiers;
|
||||
}
|
||||
|
||||
protected final Class<C> mClass;
|
||||
protected final String mName;
|
||||
protected final int mModifiers;
|
||||
}
|
||||
|
||||
public static class MemberFieldToHack<C> extends FieldToHack<C> {
|
||||
|
||||
/** Assert the field type. */
|
||||
public @Nullable <T> HackedField<C, T> ofType(final Class<T> type) {
|
||||
return ofType(type, false, null);
|
||||
}
|
||||
|
||||
public @Nullable <T> HackedField<C, T> ofType(final String type_name) {
|
||||
try { //noinspection unchecked
|
||||
return ofType((Class<T>) Class.forName(type_name, false, mClass.getClassLoader()));
|
||||
} catch (final ClassNotFoundException e) {
|
||||
fail(new AssertionException(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull
|
||||
HackedField<C, Byte> fallbackTo(final byte value) { //noinspection ConstantConditions
|
||||
return ofType(byte.class, true, value);
|
||||
}
|
||||
public @NonNull HackedField<C, Character> fallbackTo(final char value) { //noinspection ConstantConditions
|
||||
return ofType(char.class, true, value);
|
||||
}
|
||||
public @NonNull HackedField<C, Short> fallbackTo(final short value) { //noinspection ConstantConditions
|
||||
return ofType(short.class, true, value);
|
||||
}
|
||||
public @NonNull HackedField<C, Integer> fallbackTo(final int value) { //noinspection ConstantConditions
|
||||
return ofType(int.class, true, value);
|
||||
}
|
||||
public @NonNull HackedField<C, Long> fallbackTo(final long value) { //noinspection ConstantConditions
|
||||
return ofType(long.class, true, value);
|
||||
}
|
||||
public @NonNull HackedField<C, Boolean> fallbackTo(final boolean value) { //noinspection ConstantConditions
|
||||
return ofType(boolean.class, true, value);
|
||||
}
|
||||
public @NonNull HackedField<C, Float> fallbackTo(final float value) { //noinspection ConstantConditions
|
||||
return ofType(float.class, true, value);
|
||||
}
|
||||
public @NonNull HackedField<C, Double> fallbackTo(final double value) { //noinspection ConstantConditions
|
||||
return ofType(double.class, true, value);
|
||||
}
|
||||
|
||||
/** Fallback to the given value if this field is unavailable at runtime */
|
||||
public @NonNull <T> HackedField<C, T> fallbackTo(final T value) {
|
||||
@SuppressWarnings("unchecked") final Class<T> type = value == null ? null : (Class<T>) value.getClass();
|
||||
//noinspection ConstantConditions
|
||||
return ofType(type, true, value);
|
||||
}
|
||||
|
||||
private <T> HackedField<C, T> ofType(final Class<T> type, final boolean fallback, final T fallback_value) {
|
||||
final Field field = findField(type);
|
||||
return field != null ? new HackedFieldImpl<C, T>(field) : fallback ? new FallbackField<C, T>(type, fallback_value) : null;
|
||||
}
|
||||
|
||||
/** @param modifiers the modifiers this field must have */
|
||||
private MemberFieldToHack(final Class<C> clazz, final String name, final int modifiers) {
|
||||
super(clazz, name, modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StaticFieldToHack<C> extends FieldToHack<C> {
|
||||
|
||||
/** Assert the field type. */
|
||||
public @Nullable <T> HackedTargetField<T> ofType(final Class<T> type) {
|
||||
return ofType(type, false, null);
|
||||
}
|
||||
|
||||
public @Nullable <T> HackedTargetField<T> ofType(final String type_name) {
|
||||
try { //noinspection unchecked
|
||||
return ofType((Class<T>) Class.forName(type_name, false, mClass.getClassLoader()));
|
||||
} catch (final ClassNotFoundException e) {
|
||||
fail(new AssertionException(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fallback to the given value if this field is unavailable at runtime */
|
||||
public @NonNull <T> HackedTargetField<T> fallbackTo(final T value) {
|
||||
@SuppressWarnings("unchecked") final Class<T> type = value == null ? null : (Class<T>) value.getClass();
|
||||
//noinspection ConstantConditions
|
||||
return ofType(type, true, value);
|
||||
}
|
||||
|
||||
private <T> HackedTargetField<T> ofType(final Class<T> type, final boolean fallback, final T fallback_value) {
|
||||
final Field field = findField(type);
|
||||
return field != null ? new HackedFieldImpl<C, T>(field).onTarget(null) : fallback ? new FallbackField<C, T>(type, fallback_value) : null;
|
||||
}
|
||||
|
||||
/** @param modifiers the modifiers this field must have */
|
||||
private StaticFieldToHack(final Class<C> clazz, final String name, final int modifiers) {
|
||||
super(clazz, name, modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
public interface HackedField<C, T> {
|
||||
T get(C instance);
|
||||
void set(C instance, T value);
|
||||
HackedTargetField<T> on(C target);
|
||||
Class<T> getType();
|
||||
boolean isAbsent();
|
||||
}
|
||||
|
||||
public interface HackedTargetField<T> {
|
||||
T get();
|
||||
void set(T value);
|
||||
Class<T> getType();
|
||||
boolean isAbsent();
|
||||
}
|
||||
|
||||
private static class HackedFieldImpl<C, T> implements HackedField<C, T> {
|
||||
|
||||
@Override public HackedTargetFieldImpl<T> on(final C target) {
|
||||
if (target == null) throw new IllegalArgumentException("target is null");
|
||||
return onTarget(target);
|
||||
}
|
||||
|
||||
private HackedTargetFieldImpl<T> onTarget(final @Nullable C target) { return new HackedTargetFieldImpl<>(mField, target); }
|
||||
|
||||
/** Get current value of this field */
|
||||
@Override public T get(final C instance) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked") final T value = (T) mField.get(instance);
|
||||
return value;
|
||||
} catch (final IllegalAccessException e) { return null; } // Should never happen
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value of this field
|
||||
*
|
||||
* <p>No type enforced here since most type mismatch can be easily tested and exposed early.</p>
|
||||
*/
|
||||
@Override public void set(final C instance, final T value) {
|
||||
try {
|
||||
mField.set(instance, value);
|
||||
} catch (final IllegalAccessException ignored) {} // Should never happen
|
||||
}
|
||||
|
||||
@Override @SuppressWarnings("unchecked") public @Nullable Class<T> getType() {
|
||||
return (Class<T>) mField.getType();
|
||||
}
|
||||
@Override public boolean isAbsent() { return false; }
|
||||
|
||||
HackedFieldImpl(final @NonNull Field field) { mField = field; }
|
||||
|
||||
public @Nullable Field getField() { return mField; }
|
||||
|
||||
private final @NonNull Field mField;
|
||||
}
|
||||
|
||||
private static class FallbackField<C, T> implements HackedField<C, T>, HackedTargetField<T> {
|
||||
|
||||
@Override public T get(final C instance) { return mValue; }
|
||||
@Override public void set(final C instance, final T value) {}
|
||||
@Override public T get() { return mValue; }
|
||||
@Override public void set(final T value) {}
|
||||
@Override public HackedTargetField<T> on(final C target) { return this; }
|
||||
@Override public Class<T> getType() { return mType; }
|
||||
@Override public boolean isAbsent() { return true; }
|
||||
|
||||
private FallbackField(final Class<T> type, final T value) { mType = type; mValue = value; }
|
||||
|
||||
private final Class<T> mType;
|
||||
private final T mValue;
|
||||
}
|
||||
|
||||
public static class HackedTargetFieldImpl<T> implements HackedTargetField<T> {
|
||||
|
||||
@Override public T get() {
|
||||
if (mField == null) return mFallbackValue;
|
||||
try {
|
||||
@SuppressWarnings("unchecked") final T value = (T) mField.get(mInstance);
|
||||
return value;
|
||||
} catch (final IllegalAccessException e) { return null; } // Should never happen
|
||||
}
|
||||
|
||||
@Override public void set(final T value) {
|
||||
if (mField != null) try {
|
||||
mField.set(mInstance, value);
|
||||
} catch (final IllegalAccessException ignored) {} // Should never happen
|
||||
}
|
||||
|
||||
@Override @SuppressWarnings("unchecked") public @Nullable Class<T> getType() {
|
||||
return (Class<T>) mField.getType();
|
||||
}
|
||||
@Override public boolean isAbsent() { return mField == null; }
|
||||
|
||||
HackedTargetFieldImpl(final Field field, final @Nullable Object instance) {
|
||||
mField = field;
|
||||
mInstance = instance;
|
||||
}
|
||||
|
||||
private final Field mField;
|
||||
private final Object mInstance; // Instance type is already checked
|
||||
private @Nullable T mFallbackValue;
|
||||
}
|
||||
|
||||
public interface HackedInvokable<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> {
|
||||
@CheckResult
|
||||
<TT1 extends Throwable> HackedInvokable<R, C, TT1, T2, T3> throwing(Class<TT1> type);
|
||||
@CheckResult <TT1 extends Throwable, TT2 extends Throwable> HackedInvokable<R, C, TT1, TT2, T3> throwing(Class<TT1> type1, Class<TT2> type2);
|
||||
@CheckResult <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> HackedInvokable<R, C, TT1, TT2, TT3> throwing(Class<TT1> type1, Class<TT2> type2, Class<TT3> type3);
|
||||
|
||||
@Nullable HackedMethod0<R, C, T1, T2, T3> withoutParams();
|
||||
@Nullable <A1> HackedMethod1<R, C, T1, T2, T3, A1> withParam(Class<A1> type);
|
||||
@Nullable <A1, A2> HackedMethod2<R, C, T1, T2, T3, A1, A2> withParams(Class<A1> type1, Class<A2> type2);
|
||||
@Nullable <A1, A2, A3> HackedMethod3<R, C, T1, T2, T3, A1, A2, A3> withParams(Class<A1> type1, Class<A2> type2, Class<A3> type3);
|
||||
@Nullable <A1, A2, A3, A4> HackedMethod4<R, C, T1, T2, T3, A1, A2, A3, A4> withParams(Class<A1> type1, Class<A2> type2, Class<A3> type3, Class<A4> type4);
|
||||
@Nullable <A1, A2, A3, A4, A5> HackedMethod5<R, C, T1, T2, T3, A1, A2, A3, A4, A5> withParams(Class<A1> type1, final Class<A2> type2, final Class<A3> type3, final Class<A4> type4, final Class<A5> type5);
|
||||
@Nullable HackedMethodN<R, C, T1, T2, T3> withParams(Class<?>... types);
|
||||
}
|
||||
|
||||
public interface NonNullHackedInvokable<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends HackedInvokable<R, C, T1,T2,T3> {
|
||||
@CheckResult <TT1 extends Throwable> NonNullHackedInvokable<R, C, TT1, T2, T3> throwing(Class<TT1> type);
|
||||
@CheckResult <TT1 extends Throwable, TT2 extends Throwable> NonNullHackedInvokable<R, C, TT1, TT2, T3> throwing(Class<TT1> type1, Class<TT2> type2);
|
||||
@CheckResult <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> NonNullHackedInvokable<R, C, TT1, TT2, TT3> throwing(Class<TT1> type1, Class<TT2> type2, Class<TT3> type3);
|
||||
|
||||
@NonNull HackedMethod0<R, C, T1, T2, T3> withoutParams();
|
||||
@NonNull <A1> HackedMethod1<R, C, T1, T2, T3, A1> withParam(Class<A1> type);
|
||||
@NonNull <A1, A2> HackedMethod2<R, C, T1, T2, T3, A1, A2> withParams(Class<A1> type1, Class<A2> type2);
|
||||
@NonNull <A1, A2, A3> HackedMethod3<R, C, T1, T2, T3, A1, A2, A3> withParams(Class<A1> type1, Class<A2> type2, Class<A3> type3);
|
||||
@NonNull <A1, A2, A3, A4> HackedMethod4<R, C, T1, T2, T3, A1, A2, A3, A4> withParams(Class<A1> type1, Class<A2> type2, Class<A3> type3, Class<A4> type4);
|
||||
@NonNull <A1, A2, A3, A4, A5> HackedMethod5<R, C, T1, T2, T3, A1, A2, A3, A4, A5> withParams(Class<A1> type1, final Class<A2> type2, final Class<A3> type3, final Class<A4> type4, final Class<A5> type5);
|
||||
@NonNull HackedMethodN<R, C, T1, T2, T3> withParams(Class<?>... types);
|
||||
}
|
||||
|
||||
public interface HackedMethod<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends HackedInvokable<R, C, T1,T2,T3> {
|
||||
/** Optional */
|
||||
@CheckResult <RR> HackedMethod<RR, C, T1, T2, T3> returning(Class<RR> type);
|
||||
/** Fallback to the given value if this field is unavailable at runtime. (Optional) */
|
||||
@CheckResult NonNullHackedMethod<R, C, T1, T2, T3> fallbackReturning(R return_value);
|
||||
|
||||
@CheckResult <TT1 extends Throwable> HackedMethod<R, C, TT1, T2, T3> throwing(Class<TT1> type);
|
||||
@CheckResult <TT1 extends Throwable, TT2 extends Throwable> HackedMethod<R, C, TT1, TT2, T3> throwing(Class<TT1> type1, Class<TT2> type2);
|
||||
@CheckResult <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> HackedMethod<R, C, TT1, TT2, TT3> throwing(Class<TT1> type1, Class<TT2> type2, Class<TT3> type3);
|
||||
@CheckResult HackedMethod<R, C, Exception, T2, T3> throwing(Class<?>... types);
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullableProblems") // Force to NonNull
|
||||
public interface NonNullHackedMethod<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends HackedMethod<R, C, T1,T2,T3>, NonNullHackedInvokable<R, C, T1,T2,T3> {
|
||||
/** Optional */
|
||||
@CheckResult <RR> HackedMethod<RR, C, T1, T2, T3> returning(Class<RR> type);
|
||||
|
||||
@CheckResult <TT1 extends Throwable> NonNullHackedMethod<R, C, TT1, T2, T3> throwing(Class<TT1> type);
|
||||
@CheckResult <TT1 extends Throwable, TT2 extends Throwable> NonNullHackedMethod<R, C, TT1, TT2, T3> throwing(Class<TT1> type1, Class<TT2> type2);
|
||||
@CheckResult <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> NonNullHackedMethod<R, C, TT1, TT2, TT3> throwing(Class<TT1> type1, Class<TT2> type2, Class<TT3> type3);
|
||||
}
|
||||
|
||||
public static class CheckedHackedMethod<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> {
|
||||
|
||||
CheckedHackedMethod(final Invokable invokable) { mInvokable = invokable; }
|
||||
@SuppressWarnings("unchecked") public Class<R> getReturnType() { return (Class<R>) mInvokable.getReturnType(); }
|
||||
protected HackInvocation<R, C, T1, T2, T3> invoke(final Object... args) { return new HackInvocation<>(mInvokable, args); }
|
||||
/** Whether this hack is absent, thus will be fallen-back when invoked */
|
||||
public boolean isAbsent() { return mInvokable instanceof FallbackInvokable; }
|
||||
|
||||
private final Invokable mInvokable;
|
||||
}
|
||||
|
||||
public static class HackedMethod0<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends CheckedHackedMethod<R, C, T1,T2,T3> {
|
||||
HackedMethod0(final Invokable invokable) { super(invokable); }
|
||||
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke() { return super.invoke(); }
|
||||
}
|
||||
public static class HackedMethod1<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1> extends CheckedHackedMethod<R, C, T1,T2,T3> {
|
||||
HackedMethod1(final Invokable invokable) { super(invokable); }
|
||||
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg) { return super.invoke(arg); }
|
||||
}
|
||||
public static class HackedMethod2<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1, A2> extends CheckedHackedMethod<R, C, T1,T2,T3> {
|
||||
HackedMethod2(final Invokable invokable) { super(invokable); }
|
||||
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg1, final A2 arg2) { return super.invoke(arg1, arg2); }
|
||||
}
|
||||
public static class HackedMethod3<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1, A2, A3> extends CheckedHackedMethod<R, C, T1,T2,T3> {
|
||||
HackedMethod3(final Invokable invokable) { super(invokable); }
|
||||
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg1, final A2 arg2, final A3 arg3) { return super.invoke(arg1, arg2, arg3); }
|
||||
}
|
||||
public static class HackedMethod4<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1, A2, A3, A4> extends CheckedHackedMethod<R, C, T1,T2,T3> {
|
||||
HackedMethod4(final Invokable invokable) { super(invokable); }
|
||||
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg1, final A2 arg2, final A3 arg3, final A4 arg4) { return super.invoke(arg1, arg2, arg3, arg4); }
|
||||
}
|
||||
public static class HackedMethod5<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1, A2, A3, A4, A5> extends CheckedHackedMethod<R, C, T1,T2,T3> {
|
||||
HackedMethod5(final Invokable invokable) { super(invokable); }
|
||||
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg1, final A2 arg2, final A3 arg3, final A4 arg4, final A5 arg5) { return super.invoke(arg1, arg2, arg3, arg4, arg5); }
|
||||
}
|
||||
public static class HackedMethodN<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends CheckedHackedMethod<R, C, T1,T2,T3> {
|
||||
HackedMethodN(final Invokable invokable) { super(invokable); }
|
||||
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final Object... args) { return super.invoke(args); }
|
||||
}
|
||||
|
||||
public static class HackInvocation<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> {
|
||||
|
||||
HackInvocation(final Invokable invokable, final Object... args) {
|
||||
this.invokable = invokable;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
public R on(final @NonNull C target) throws T1, T2, T3 { return onTarget(target); }
|
||||
public R statically() throws T1, T2, T3 { return onTarget(null); }
|
||||
|
||||
private R onTarget(final C target) throws T1 { //noinspection TryWithIdenticalCatches
|
||||
try {
|
||||
@SuppressWarnings("unchecked") final R result = (R) invokable.invoke(target, args);
|
||||
return result;
|
||||
} catch (final IllegalAccessException e) { throw new RuntimeException(e); // Should never happen
|
||||
} catch (final InstantiationException e) { throw new RuntimeException(e);
|
||||
} catch (final InvocationTargetException e) {
|
||||
final Throwable ex = e.getTargetException();
|
||||
//noinspection unchecked
|
||||
throw (T1) ex;
|
||||
}
|
||||
}
|
||||
|
||||
private final Invokable invokable;
|
||||
private final Object[] args;
|
||||
}
|
||||
|
||||
interface Invokable<C> {
|
||||
Object invoke(C target, Object[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException;
|
||||
Class<?> getReturnType();
|
||||
}
|
||||
|
||||
private static class HackedMethodImpl<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> implements NonNullHackedMethod<R, C, T1, T2, T3> {
|
||||
|
||||
HackedMethodImpl(final Class<?> clazz, @Nullable final String name, final int modifiers) {
|
||||
//noinspection unchecked, to be compatible with HackedClass.staticMethod()
|
||||
mClass = (Class<C>) clazz;
|
||||
mName = name;
|
||||
mModifiers = modifiers;
|
||||
}
|
||||
|
||||
@Override public <RR> HackedMethod<RR, C, T1, T2, T3> returning(final Class<RR> type) {
|
||||
mReturnType = type;
|
||||
@SuppressWarnings("unchecked") final HackedMethod<RR, C, T1, T2, T3> casted = (HackedMethod<RR, C, T1, T2, T3>) this;
|
||||
return casted;
|
||||
}
|
||||
|
||||
@Override public NonNullHackedMethod<R, C, T1, T2, T3> fallbackReturning(final R value) {
|
||||
mFallbackReturnValue = value; mHasFallback = true; return this;
|
||||
}
|
||||
|
||||
@Override public <TT extends Throwable> NonNullHackedMethod<R, C, TT, T2, T3> throwing(final Class<TT> type) {
|
||||
mThrowTypes = new Class[] { type };
|
||||
@SuppressWarnings("unchecked") final NonNullHackedMethod<R, C, TT, T2, T3> casted = (NonNullHackedMethod<R, C, TT, T2, T3>) this;
|
||||
return casted;
|
||||
}
|
||||
|
||||
@Override public <TT1 extends Throwable, TT2 extends Throwable> NonNullHackedMethod<R, C, TT1, TT2, T3>
|
||||
throwing(final Class<TT1> type1, final Class<TT2> type2) {
|
||||
mThrowTypes = new Class<?>[] { type1, type2 };
|
||||
Arrays.sort(mThrowTypes, CLASS_COMPARATOR);
|
||||
@SuppressWarnings("unchecked") final NonNullHackedMethod<R, C, TT1, TT2, T3> cast = (NonNullHackedMethod<R, C, TT1, TT2, T3>) this;
|
||||
return cast;
|
||||
}
|
||||
|
||||
@Override public <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> NonNullHackedMethod<R, C, TT1, TT2, TT3>
|
||||
throwing(final Class<TT1> type1, final Class<TT2> type2, final Class<TT3> type3) {
|
||||
mThrowTypes = new Class<?>[] { type1, type2, type3 };
|
||||
Arrays.sort(mThrowTypes, CLASS_COMPARATOR);
|
||||
@SuppressWarnings("unchecked") final NonNullHackedMethod<R, C, TT1, TT2, TT3> cast = (NonNullHackedMethod<R, C, TT1, TT2, TT3>) this;
|
||||
return cast;
|
||||
}
|
||||
|
||||
@Override public HackedMethod<R, C, Exception, T2, T3> throwing(final Class<?>... types) {
|
||||
mThrowTypes = types;
|
||||
Arrays.sort(mThrowTypes, CLASS_COMPARATOR);
|
||||
@SuppressWarnings("unchecked") final HackedMethod<R, C, Exception, T2, T3> cast = (HackedMethod<R, C, Exception, T2, T3>) this;
|
||||
return cast;
|
||||
}
|
||||
|
||||
@NonNull @SuppressWarnings("ConstantConditions")
|
||||
@Override public HackedMethod0<R, C, T1, T2, T3> withoutParams() {
|
||||
final Invokable<C> invokable = findInvokable();
|
||||
return invokable == null ? null : new HackedMethod0<R, C, T1, T2, T3>(invokable);
|
||||
}
|
||||
|
||||
@NonNull @SuppressWarnings("ConstantConditions")
|
||||
@Override public <A1> HackedMethod1<R, C, T1, T2, T3, A1> withParam(final Class<A1> type) {
|
||||
final Invokable invokable = findInvokable(type);
|
||||
return invokable == null ? null : new HackedMethod1<R, C, T1, T2, T3, A1>(invokable);
|
||||
}
|
||||
|
||||
@NonNull @SuppressWarnings("ConstantConditions")
|
||||
@Override public <A1, A2> HackedMethod2<R, C, T1, T2, T3, A1, A2> withParams(final Class<A1> type1, final Class<A2> type2) {
|
||||
final Invokable invokable = findInvokable(type1, type2);
|
||||
return invokable == null ? null : new HackedMethod2<R, C, T1, T2, T3, A1, A2>(invokable);
|
||||
}
|
||||
|
||||
@NonNull @SuppressWarnings("ConstantConditions")
|
||||
@Override public <A1, A2, A3> HackedMethod3<R, C, T1, T2, T3, A1, A2, A3> withParams(final Class<A1> type1, final Class<A2> type2, final Class<A3> type3) {
|
||||
final Invokable invokable = findInvokable(type1, type2, type3);
|
||||
return invokable == null ? null : new HackedMethod3<R, C, T1, T2, T3, A1, A2, A3>(invokable);
|
||||
}
|
||||
|
||||
@NonNull @SuppressWarnings("ConstantConditions")
|
||||
@Override public <A1, A2, A3, A4> HackedMethod4<R, C, T1, T2, T3, A1, A2, A3, A4> withParams(final Class<A1> type1, final Class<A2> type2, final Class<A3> type3, final Class<A4> type4) {
|
||||
final Invokable invokable = findInvokable(type1, type2, type3, type4);
|
||||
return invokable == null ? null : new HackedMethod4<R, C, T1, T2, T3, A1, A2, A3, A4>(invokable);
|
||||
}
|
||||
|
||||
@NonNull @SuppressWarnings("ConstantConditions")
|
||||
@Override public <A1, A2, A3, A4, A5> HackedMethod5<R, C, T1, T2, T3, A1, A2, A3, A4, A5> withParams(final Class<A1> type1, final Class<A2> type2, final Class<A3> type3, final Class<A4> type4, final Class<A5> type5) {
|
||||
final Invokable invokable = findInvokable(type1, type2, type3, type4, type5);
|
||||
return invokable == null ? null : new HackedMethod5<R, C, T1, T2, T3, A1, A2, A3, A4, A5>(invokable);
|
||||
}
|
||||
|
||||
@NonNull @SuppressWarnings("ConstantConditions")
|
||||
@Override public HackedMethodN<R, C, T1, T2, T3> withParams(final Class<?>... types) {
|
||||
final Invokable invokable = findInvokable(types);
|
||||
return invokable == null ? null : new HackedMethodN<R, C, T1, T2, T3>(invokable);
|
||||
}
|
||||
|
||||
private @Nullable Invokable<C> findInvokable(final Class<?>... param_types) {
|
||||
if (mClass == ANY_TYPE) // AnyType as a internal indicator for class not found.
|
||||
return mHasFallback ? new FallbackInvokable<C>(mFallbackReturnValue) : null;
|
||||
|
||||
final int modifiers; Invokable<C> invokable; final AccessibleObject accessible; final Class<?>[] ex_types;
|
||||
try {
|
||||
if (mName != null) {
|
||||
final Method candidate = mClass.getDeclaredMethod(mName, param_types); Method method = candidate;
|
||||
ex_types = candidate.getExceptionTypes();
|
||||
modifiers = method.getModifiers();
|
||||
if (Modifier.isStatic(mModifiers) != Modifier.isStatic(candidate.getModifiers())) {
|
||||
fail(new AssertionException(candidate + (Modifier.isStatic(mModifiers) ? " is not static" : "is static")).setHackedMethod(method));
|
||||
method = null;
|
||||
}
|
||||
if (mReturnType != null && mReturnType != ANY_TYPE && ! candidate.getReturnType().equals(mReturnType)) {
|
||||
fail(new AssertionException("Return type mismatch: " + candidate));
|
||||
method = null;
|
||||
}
|
||||
if (method != null) {
|
||||
invokable = new InvokableMethod<>(method);
|
||||
accessible = method;
|
||||
} else { invokable = null; accessible = null; }
|
||||
} else {
|
||||
final Constructor<C> ctor = mClass.getDeclaredConstructor(param_types);
|
||||
modifiers = ctor.getModifiers(); invokable = new InvokableConstructor<>(ctor); accessible = ctor;
|
||||
ex_types = ctor.getExceptionTypes();
|
||||
}
|
||||
} catch (final NoSuchMethodException e) {
|
||||
fail(new AssertionException(e).setHackedClass(mClass).setHackedMethodName(mName).setParamTypes(param_types));
|
||||
return mHasFallback ? new FallbackInvokable<C>(mFallbackReturnValue) : null;
|
||||
}
|
||||
|
||||
if (mModifiers > 0 && (modifiers & mModifiers) != mModifiers)
|
||||
fail(new AssertionException(invokable + " does not match modifiers: " + mModifiers).setHackedMethodName(mName));
|
||||
|
||||
if (mThrowTypes == null && ex_types.length > 0 || mThrowTypes != null && ex_types.length == 0) {
|
||||
fail(new AssertionException("Checked exception(s) not match: " + invokable));
|
||||
if (ex_types.length > 0) invokable = null; // No need to fall-back if expected checked exceptions are absent.
|
||||
} else if (mThrowTypes != null) {
|
||||
Arrays.sort(ex_types, CLASS_COMPARATOR);
|
||||
if (! Arrays.equals(ex_types, mThrowTypes)) { // TODO: Check derived relation of exceptions
|
||||
fail(new AssertionException("Checked exception(s) not match: " + invokable));
|
||||
invokable = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (invokable == null) {
|
||||
if (! mHasFallback) return null;
|
||||
return new FallbackInvokable<>(mFallbackReturnValue);
|
||||
}
|
||||
|
||||
if (! accessible.isAccessible()) accessible.setAccessible(true);
|
||||
return invokable;
|
||||
}
|
||||
|
||||
private final Class<C> mClass;
|
||||
private final @Nullable String mName; // Null for constructor
|
||||
private final int mModifiers;
|
||||
private Class<?> mReturnType;
|
||||
private Class<?>[] mThrowTypes;
|
||||
private R mFallbackReturnValue;
|
||||
private boolean mHasFallback;
|
||||
private static final Comparator<Class> CLASS_COMPARATOR = new Comparator<Class>() {
|
||||
@Override public int compare(final Class lhs, final Class rhs) {
|
||||
return lhs.toString().compareTo(rhs.toString());
|
||||
}
|
||||
|
||||
@Override public boolean equals(final Object object) {
|
||||
return this == object;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static class InvokableMethod<C> implements Invokable<C> {
|
||||
|
||||
InvokableMethod(final Method method) { this.method = method; }
|
||||
|
||||
public Object invoke(final C target, final Object[] args) throws IllegalAccessException,
|
||||
IllegalArgumentException, InvocationTargetException {
|
||||
return method.invoke(target, args);
|
||||
}
|
||||
|
||||
@Override public Class<?> getReturnType() { return method.getReturnType(); }
|
||||
|
||||
@Override public String toString() { return method.toString(); }
|
||||
|
||||
private final Method method;
|
||||
}
|
||||
|
||||
private static class InvokableConstructor<C> implements Invokable<C> {
|
||||
|
||||
InvokableConstructor(final Constructor<C> method) { this.constructor = method; }
|
||||
|
||||
public Object invoke(final C target, final Object[] args) throws InstantiationException,
|
||||
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
|
||||
@Override public Class<?> getReturnType() {
|
||||
return constructor.getDeclaringClass();
|
||||
}
|
||||
|
||||
@Override public String toString() { return constructor.toString(); }
|
||||
|
||||
private final Constructor<C> constructor;
|
||||
}
|
||||
|
||||
private static class FallbackInvokable<C> implements Invokable<C> {
|
||||
|
||||
FallbackInvokable(final @Nullable Object value) { mValue = value; }
|
||||
|
||||
@Override public Object invoke(final C target, final Object[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
@Override public Class<?> getReturnType() {
|
||||
return mValue == null ? Object.class : mValue.getClass();
|
||||
}
|
||||
|
||||
private final @Nullable Object mValue;
|
||||
}
|
||||
|
||||
public static class HackedClass<C> {
|
||||
|
||||
public @CheckResult <T> MemberFieldToHack<C> field(final @NonNull String name) {
|
||||
return new MemberFieldToHack<>(mClass, name, 0);
|
||||
}
|
||||
|
||||
public @CheckResult <T> StaticFieldToHack<C> staticField(final @NonNull String name) {
|
||||
return new StaticFieldToHack<>(mClass, name, Modifier.STATIC);
|
||||
}
|
||||
|
||||
public @CheckResult NonNullHackedMethod<Void, C, Unchecked, Unchecked, Unchecked> method(final String name) {
|
||||
return new HackedMethodImpl<>(mClass, name, 0);
|
||||
}
|
||||
|
||||
public @CheckResult NonNullHackedMethod<Void, Void, Unchecked, Unchecked, Unchecked> staticMethod(final String name) {
|
||||
return new HackedMethodImpl<>(mClass, name, Modifier.STATIC);
|
||||
}
|
||||
|
||||
public @CheckResult NonNullHackedInvokable<C, Void, Unchecked, Unchecked, Unchecked> constructor() {
|
||||
final HackedMethodImpl<C, Void, Unchecked, Unchecked, Unchecked> constructor = new HackedMethodImpl<>(mClass, null, 0);
|
||||
constructor.fallbackReturning(null); // Always fallback to null.
|
||||
return constructor;
|
||||
}
|
||||
|
||||
HackedClass(final Class<C> clazz) { mClass = clazz; }
|
||||
|
||||
private final Class<C> mClass;
|
||||
}
|
||||
|
||||
public static <T> HackedClass<T> into(final @NonNull Class<T> clazz) {
|
||||
return new HackedClass<>(clazz);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public static <T> HackedClass<T> into(final String class_name) {
|
||||
try {
|
||||
return new HackedClass(Class.forName(class_name));
|
||||
} catch (final ClassNotFoundException e) {
|
||||
fail(new AssertionException(e));
|
||||
return new HackedClass(ANY_TYPE); // Use AnyType as a lazy trick to make fallback working and avoid null.
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") public static <C> Hack.HackedClass<C> onlyIf(final boolean condition, final Hacking<Hack.HackedClass<C>> hacking) {
|
||||
if (condition) return hacking.hack();
|
||||
return (Hack.HackedClass<C>) FALLBACK;
|
||||
}
|
||||
public interface Hacking<T> { T hack(); }
|
||||
private static final Hack.HackedClass<?> FALLBACK = new HackedClass<>(ANY_TYPE);
|
||||
|
||||
public static ConditionalHack onlyIf(final boolean condition) {
|
||||
return condition ? new ConditionalHack() {
|
||||
@Override public <T> HackedClass<T> into(@NonNull final Class<T> clazz) {
|
||||
return Hack.into(clazz);
|
||||
}
|
||||
@Override public <T> HackedClass<T> into(final String class_name) {
|
||||
return Hack.into(class_name);
|
||||
}
|
||||
} : new ConditionalHack() {
|
||||
@SuppressWarnings("unchecked") @Override public <T> HackedClass<T> into(@NonNull final Class<T> clazz) {
|
||||
return (HackedClass<T>) FALLBACK;
|
||||
}
|
||||
@SuppressWarnings("unchecked") @Override public <T> HackedClass<T> into(final String class_name) {
|
||||
return (HackedClass<T>) FALLBACK;
|
||||
}
|
||||
};
|
||||
}
|
||||
public interface ConditionalHack {
|
||||
/** WARNING: Never use this method if the target class may not exist when the condition is not met, use {@link #onlyIf(boolean, Hacking)} instead. */
|
||||
<T> HackedClass<T> into(final @NonNull Class<T> clazz);
|
||||
<T> HackedClass<T> into(final String class_name);
|
||||
}
|
||||
|
||||
private static void fail(final AssertionException e) {
|
||||
if (sFailureHandler != null) sFailureHandler.onAssertionFailure(e);
|
||||
}
|
||||
|
||||
/** Specify a handler to deal with assertion failure, and decide whether the failure should be thrown. */
|
||||
public static AssertionFailureHandler setAssertionFailureHandler(final AssertionFailureHandler handler) {
|
||||
final AssertionFailureHandler old = sFailureHandler;
|
||||
sFailureHandler = handler;
|
||||
return old;
|
||||
}
|
||||
|
||||
private Hack() {}
|
||||
|
||||
private static AssertionFailureHandler sFailureHandler;
|
||||
|
||||
/** This is a simple demo for the common usage of {@link Hack} */
|
||||
@SuppressWarnings("unused")
|
||||
private static class Demo {
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "UnnecessarilyQualifiedStaticUsage"})
|
||||
static class Hacks {
|
||||
|
||||
static {
|
||||
Hack.setAssertionFailureHandler(new AssertionFailureHandler() { @Override public void onAssertionFailure(final AssertionException failure) {
|
||||
Log.w("Demo", "Partially incompatible: " + failure.getDebugInfo());
|
||||
// Report the incompatibility silently.
|
||||
//...
|
||||
}});
|
||||
Demo_ctor = Hack.into(Demo.class).constructor().withParam(int.class);
|
||||
// Method without fallback (will be null if absent)
|
||||
Demo_methodThrows = Hack.into(Demo.class).method("methodThrows").returning(Void.class).fallbackReturning(null)
|
||||
.throwing(InterruptedException.class, IOException.class).withoutParams();
|
||||
// Method with fallback (will never be null)
|
||||
Demo_staticMethod = Hack.into(Demo.class).staticMethod("methodWith2Params").returning(boolean.class)
|
||||
.fallbackReturning(false).withParams(int.class, String.class);
|
||||
Demo_mField = Hack.into(Demo.class).field("mField").fallbackTo(false);
|
||||
Demo_sField = Hack.into(Demo.class).staticField("sField").ofType(String.class);
|
||||
}
|
||||
|
||||
static HackedMethod1<Demo, Void, Unchecked, Unchecked, Unchecked, Integer> Demo_ctor;
|
||||
static Hack.HackedMethod0<Void, Demo, InterruptedException, IOException, Unchecked> Demo_methodThrows;
|
||||
static Hack.HackedMethod2<Boolean, Void, Unchecked, Unchecked, Unchecked, Integer, String> Demo_staticMethod;
|
||||
static @Nullable HackedField<Demo, Boolean> Demo_mField; // Optional hack may be null if assertion failed
|
||||
static @Nullable HackedTargetField<String> Demo_sField;
|
||||
}
|
||||
|
||||
static void demo() {
|
||||
final Demo demo = Hacks.Demo_ctor.invoke(0).statically();
|
||||
try {
|
||||
Hacks.Demo_methodThrows.invoke().on(demo);
|
||||
} catch (final InterruptedException | IOException e) { // The checked exceptions declared by throwing() in hack definition.
|
||||
e.printStackTrace();
|
||||
}
|
||||
Hacks.Demo_staticMethod.invoke(1, "xx").statically();
|
||||
}
|
||||
|
||||
Demo(final int flags) {}
|
||||
|
||||
private void methodThrows() throws InterruptedException, IOException {}
|
||||
static boolean staticMethod(final int a, final String c) { return false; }
|
||||
boolean mField;
|
||||
static String sField;
|
||||
}
|
||||
}
|
9
app/src/main/res/drawable-v24/buttonbg.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="4dp" />
|
||||
<color android:color="#034abe" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
9
app/src/main/res/drawable/buttonbgfocus.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="4dp" />
|
||||
<color android:color="#034ABE" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
9
app/src/main/res/drawable/buttonbgunfocus.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="4dp" />
|
||||
<color android:color="#034abe" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
47
app/src/main/res/drawable/ic_banner_foreground.xml
Normal file
@ -0,0 +1,47 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="320dp"
|
||||
android:height="180dp"
|
||||
android:viewportWidth="320"
|
||||
android:viewportHeight="180">
|
||||
<group android:scaleX="0.6666667"
|
||||
android:scaleY="0.6666667"
|
||||
android:translateX="53.333332"
|
||||
android:translateY="30">
|
||||
<group android:scaleX="0.21501707"
|
||||
android:scaleY="0.21501707"
|
||||
android:translateX="35.81075"
|
||||
android:translateY="52.2">
|
||||
<path
|
||||
android:pathData="M312.2,93.7c-0.7,-0.5 -1.4,-1.1 -2.1,-1.6l-38.6,60.3c5.8,6.3 9.1,14.6 9.1,23.4c0.1,11.5 -5.5,22.1 -15,28.6c-4.7,3.3 -9.9,5.2 -15.4,6.1c-5.4,0.8 -11,0.7 -16.5,0.7l-45.4,70.8h35.1c6.7,0 13.4,0 20.1,-0.1c9.1,-0.3 17.8,-1.3 26.7,-3.4c15.4,-3.7 29.6,-10.6 41.9,-20.5c24.9,-20 39.3,-50.1 39.3,-82.1C351.5,143.8 337.1,113.7 312.2,93.7z"
|
||||
android:fillColor="#1C1E20"/>
|
||||
<path
|
||||
android:pathData="M108.9,281.8l45.3,-70.6h-36c-5.7,0 -11.2,0.1 -16.7,-0.7c-5.7,-0.8 -10.8,-2.8 -15.4,-6.1c-9.3,-6.5 -15,-17.1 -15,-28.6c0,-11.5 5.7,-22.1 15,-28.6c4.7,-3.3 9.9,-5.2 15.4,-6.1c5.5,-0.8 11.2,-0.7 16.7,-0.7h81.4l45.3,-70.6c-0.5,0 -0.9,0 -1.4,-0.1c-6.7,-0.3 -13.4,-0.1 -20.1,-0.1h-95.2c-6.7,0 -13.4,0 -20.1,0.1c-9.1,0.3 -17.8,1.3 -26.7,3.4c-15.4,3.7 -29.6,10.6 -41.9,20.5C14.4,113.7 0,143.8 0,175.8c0,32 14.4,62.1 39.3,82.1c12.3,9.9 26.6,16.8 42,20.5c8.8,2.1 17.7,3.1 26.7,3.4C108.4,281.8 108.6,281.8 108.9,281.8z"
|
||||
android:fillColor="#1C1E20"/>
|
||||
<path
|
||||
android:pathData="M220.523,9.533l56.401,36.167l-190.01,296.314l-56.401,-36.167z"
|
||||
android:fillColor="#1C1E20"/>
|
||||
</group>
|
||||
|
||||
<group android:scaleX="0.27198938"
|
||||
android:scaleY="0.27198938"
|
||||
android:translateX="128"
|
||||
android:translateY="67.20389">
|
||||
<group android:translateY="133.66406">
|
||||
<path android:pathData="M43.03125,1Q6,1,6,-51.671875Q6,-104,43.171875,-104Q61,-104,70.5,-90.359375Q80,-76.71875,80,-51.53125Q80,-26.34375,70.390625,-12.671875Q60.78125,1,43.03125,1ZM43.03125,-10.53125Q55,-10.53125,60.5625,-20.609375Q66.140625,-30.703125,66.140625,-51.5625Q66.140625,-72.640625,60.5,-82.546875Q54.859375,-92.46875,43.171875,-92.46875Q19.859375,-92.46875,19.859375,-51.5625Q19.859375,-10.53125,43.03125,-10.53125Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path android:pathData="M160,-74.859375L155.6875,-63.75Q145.25,-67.8125,137.03125,-67.8125Q111.21875,-67.8125,111.21875,-38.75Q111.21875,-10.1875,136.51562,-10.1875Q147.51562,-10.1875,159,-14.546875L159,-3.296875Q149.60938,1,135.9375,1Q117.90625,1,107.953125,-9.21875Q98,-19.453125,98,-38.578125Q98,-58.328125,108.109375,-68.65625Q118.21875,-79,136.5625,-79Q148.98438,-79,160,-74.859375Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path android:pathData="M236.67188,0L234.76562,-10.75L234.0625,-10.75Q226.67188,1,210.53125,1Q183,1,183,-26.984375L183,-77L195.79688,-77L195.79688,-27.734375Q195.79688,-9.625,212.42188,-9.625Q223.78125,-9.625,228.98438,-16.0625Q234.20312,-22.515625,234.20312,-37.28125L234.20312,-77L247,-77L247,0L236.67188,0Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path android:pathData="M295,-98.9375L276.15625,-100.421875L276.15625,-109L307.79688,-109L307.79688,-10.0625L332.54688,-8.65625L332.54688,0L270.8125,0L270.8125,-8.65625L295,-10.0625L295,-98.9375Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path android:pathData="M421,-36.375L366.21875,-36.375Q366.78125,-10.1875,390.57812,-10.1875Q404.39062,-10.1875,416.78125,-15.53125L416.78125,-4.28125Q405.0625,1,391.07812,1Q373.76562,1,363.375,-9.5Q353,-20.015625,353,-38.4375Q353,-57.140625,362.5625,-68.0625Q372.14062,-79,388.17188,-79Q403.14062,-79,412.0625,-69.546875Q421,-60.09375,421,-44.34375L421,-36.375ZM366.5,-47L407.78125,-47Q407.78125,-68.375,388.1875,-68.375Q368.375,-68.375,366.5,-47Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path android:pathData="M444,-3.859375L444,-15.53125Q457.60938,-9.625,470.09375,-9.625Q489.48438,-9.625,489.48438,-20.859375Q489.48438,-24.859375,486.01562,-27.8125Q482.54688,-30.765625,470.03125,-35.265625Q453.5625,-41.390625,449.28125,-46.484375Q445,-51.578125,445,-58.546875Q445,-68.109375,452.90625,-73.546875Q460.82812,-79,474.8125,-79Q488.85938,-79,501,-73.796875L496.75,-63.328125Q483.89062,-68.375,474.20312,-68.375Q457.51562,-68.375,457.51562,-59.03125Q457.51562,-54.75,461.0625,-52.171875Q464.60938,-49.609375,477.40625,-45.046875Q492.23438,-39.625,497.10938,-34.390625Q502,-29.15625,502,-21.5625Q502,-11.015625,493.75,-5Q485.51562,1,470.42188,1Q452.95312,1,444,-3.859375Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path android:pathData="M530,-3.859375L530,-15.53125Q543.6094,-9.625,556.09375,-9.625Q575.4844,-9.625,575.4844,-20.859375Q575.4844,-24.859375,572.0156,-27.8125Q568.5469,-30.765625,556.03125,-35.265625Q539.5625,-41.390625,535.28125,-46.484375Q531,-51.578125,531,-58.546875Q531,-68.109375,538.90625,-73.546875Q546.8281,-79,560.8125,-79Q574.8594,-79,587,-73.796875L582.75,-63.328125Q569.8906,-68.375,560.2031,-68.375Q543.5156,-68.375,543.5156,-59.03125Q543.5156,-54.75,547.0625,-52.171875Q550.6094,-49.609375,563.40625,-45.046875Q578.2344,-39.625,583.1094,-34.390625Q588,-29.15625,588,-21.5625Q588,-11.015625,579.75,-5Q571.5156,1,556.4219,1Q538.9531,1,530,-3.859375Z"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
20
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="0.12576792"
|
||||
android:scaleY="0.12576792"
|
||||
android:translateX="31.896288"
|
||||
android:translateY="31.89">
|
||||
<path
|
||||
android:pathData="M312.2,93.7c-0.7,-0.5 -1.4,-1.1 -2.1,-1.6l-38.6,60.3c5.8,6.3 9.1,14.6 9.1,23.4c0.1,11.5 -5.5,22.1 -15,28.6c-4.7,3.3 -9.9,5.2 -15.4,6.1c-5.4,0.8 -11,0.7 -16.5,0.7l-45.4,70.8h35.1c6.7,0 13.4,0 20.1,-0.1c9.1,-0.3 17.8,-1.3 26.7,-3.4c15.4,-3.7 29.6,-10.6 41.9,-20.5c24.9,-20 39.3,-50.1 39.3,-82.1C351.5,143.8 337.1,113.7 312.2,93.7z"
|
||||
android:fillColor="#1C1E20"/>
|
||||
<path
|
||||
android:pathData="M108.9,281.8l45.3,-70.6h-36c-5.7,0 -11.2,0.1 -16.7,-0.7c-5.7,-0.8 -10.8,-2.8 -15.4,-6.1c-9.3,-6.5 -15,-17.1 -15,-28.6c0,-11.5 5.7,-22.1 15,-28.6c4.7,-3.3 9.9,-5.2 15.4,-6.1c5.5,-0.8 11.2,-0.7 16.7,-0.7h81.4l45.3,-70.6c-0.5,0 -0.9,0 -1.4,-0.1c-6.7,-0.3 -13.4,-0.1 -20.1,-0.1h-95.2c-6.7,0 -13.4,0 -20.1,0.1c-9.1,0.3 -17.8,1.3 -26.7,3.4c-15.4,3.7 -29.6,10.6 -41.9,20.5C14.4,113.7 0,143.8 0,175.8c0,32 14.4,62.1 39.3,82.1c12.3,9.9 26.6,16.8 42,20.5c8.8,2.1 17.7,3.1 26.7,3.4C108.4,281.8 108.6,281.8 108.9,281.8z"
|
||||
android:fillColor="#1C1E20"/>
|
||||
<path
|
||||
android:pathData="M220.523,9.533l56.401,36.167l-190.01,296.314l-56.401,-36.167z"
|
||||
android:fillColor="#1C1E20"/>
|
||||
</group>
|
||||
</vector>
|
8
app/src/main/res/drawable/windowbg.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:topLeftRadius="15dp" android:topRightRadius="15dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
@ -4,65 +4,141 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<View
|
||||
android:id="@+id/view2"
|
||||
android:layout_width="99999dp"
|
||||
android:layout_height="99999dp"
|
||||
android:background="@drawable/windowbg"
|
||||
android:backgroundTint="#D8202020"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/viewOtaBtn"
|
||||
android:layout_width="217dp"
|
||||
android:layout_height="87dp"
|
||||
style="@style/OculusButtonStyle"
|
||||
android:layout_width="450dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/buttonbg"
|
||||
android:backgroundTint="#024abf"
|
||||
android:text="@string/disable_ota"
|
||||
app:icon="@android:drawable/ic_popup_sync"
|
||||
app:iconPadding="-25dp"
|
||||
app:iconSize="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.494"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/viewAccountsBtn" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/viewTelemetryBtn"
|
||||
android:layout_width="217dp"
|
||||
android:layout_height="87dp"
|
||||
style="@style/OculusButtonStyle"
|
||||
android:layout_width="450dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/buttonbg"
|
||||
android:text="@string/telemetry"
|
||||
app:icon="?android:attr/actionModeFindDrawable"
|
||||
app:iconPadding="-27dp"
|
||||
app:iconSize="26dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/viewOtaBtn" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/isEnabledText"
|
||||
android:id="@+id/Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginBottom="52dp"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="20sp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="34sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/viewAdminsBtn"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.498"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/isEnabledText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintBottom_toTopOf="@+id/viewAdminsBtn"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/isEnabledText2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/app_subtitle"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintBottom_toTopOf="@+id/viewAdminsBtn"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.501"
|
||||
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/viewAdminsBtn"
|
||||
android:layout_width="217dp"
|
||||
android:layout_height="87dp"
|
||||
style="@style/OculusButtonStyle"
|
||||
android:layout_width="450dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/buttonbg"
|
||||
android:backgroundTint="@color/ic_banner_background"
|
||||
android:backgroundTintMode="multiply"
|
||||
android:text="@string/disable_companion"
|
||||
|
||||
android:typeface="normal"
|
||||
app:icon="?android:attr/alertDialogIcon"
|
||||
app:iconPadding="-25dp"
|
||||
app:iconSize="24dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/viewAccountsBtn"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.494"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/viewAccountsBtn"
|
||||
android:layout_width="217dp"
|
||||
android:layout_height="87dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
style="@style/OculusButtonStyle"
|
||||
android:layout_width="450dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/buttonbg"
|
||||
android:text="@string/remove_accounts"
|
||||
app:icon="?attr/actionModeShareDrawable"
|
||||
app:iconPadding="-25dp"
|
||||
app:iconSize="24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.494"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.362" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/viewPermissionsBtn"
|
||||
style="@style/OculusButtonStyle"
|
||||
android:layout_width="450dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/buttonbg"
|
||||
android:text="@string/audio"
|
||||
app:icon="@android:drawable/ic_lock_silent_mode_off"
|
||||
app:iconPadding="-27dp"
|
||||
app:iconSize="29dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/viewTelemetryBtn" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_banner.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_banner_background"/>
|
||||
<foreground android:drawable="@drawable/ic_banner_foreground"/>
|
||||
</adaptive-icon>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_banner.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
@ -1,16 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Oculess</string>
|
||||
<string name="app_subtitle">Modifikationstool für Oculus Quest-Geräte</string>
|
||||
|
||||
<string name="is_disabled">Oculus Begleit-Server ist deaktiviert!</string>
|
||||
<string name="is_enabled">Oculus Begleit-Server ist aktiviert!</string>
|
||||
|
||||
<string name="disable_companion">BEGLEITER DEAKTIVIEREN</string>
|
||||
<string name="enable_companion">BEGLEITER AKTIVIEREN</string>
|
||||
<string name="remove_accounts">ACCOUNTS ENTFERNEN</string>
|
||||
<string name="disable_ota">UPDATES DEAKTIVIEREN</string>
|
||||
<string name="enable_ota">UPDATES AKTIVIEREN</string>
|
||||
<string name="telemetry">TELEMETRIE</string>
|
||||
<string name="disable_companion">Begleiter Aktivieren</string>
|
||||
<string name="enable_companion">Accounts Entfernen</string>
|
||||
<string name="remove_accounts">Updates Deaktivieren</string>
|
||||
<string name="disable_ota">Updates Deaktivieren</string>
|
||||
<string name="enable_ota">Updates Aktivieren</string>
|
||||
<string name="telemetry">Aktivieren/Deaktivieren Telemetrie</string>
|
||||
|
||||
<string name="title">Wichtige Information</string>
|
||||
<string name="message0">Bitte starten Sie nach dem ersten Mal neu!\nWiederholen Sie den Schritt nach jedem Neustart!</string>
|
||||
@ -21,4 +22,5 @@
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="disable">Telemetrie Deaktivieren</string>
|
||||
<string name="enable">Telemetrie Aktivieren</string>
|
||||
<string name="audio">Hintergrundaudio für alle Apps Aktivieren\n(bleibt nach Neustart nicht)</string>
|
||||
</resources>
|
8
app/src/main/res/values/Styles.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="OculusButtonStyle" parent="@style/Widget.AppCompat.Button.Borderless">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:fontFamily">"sans-serif-light"</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
</resources>
|
4
app/src/main/res/values/ic_banner_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_banner_background">#FFFFFF</color>
|
||||
</resources>
|
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
@ -1,18 +1,19 @@
|
||||
<resources>
|
||||
<string name="app_name">Oculess</string>
|
||||
<string name="app_subtitle">Toolbox for Oculus Quest devices</string>
|
||||
|
||||
<string name="is_disabled">Oculus Companion Server is disabled!</string>
|
||||
<string name="is_enabled">Oculus Companion Server is enabled!</string>
|
||||
|
||||
<string name="disable_companion">DISABLE COMPANION</string>
|
||||
<string name="enable_companion">ENABLE COMPANION</string>
|
||||
<string name="remove_accounts">REMOVE ACCOUNTS</string>
|
||||
<string name="disable_ota">DISABLE UPDATES</string>
|
||||
<string name="enable_ota">ENABLE UPDATES</string>
|
||||
<string name="telemetry">TELEMETRY</string>
|
||||
<string name="disable_companion">Disable Companion Server</string>
|
||||
<string name="enable_companion">Enable Companion Server</string>
|
||||
<string name="remove_accounts">Remove Accounts</string>
|
||||
<string name="disable_ota">Disable Updates</string>
|
||||
<string name="enable_ota">Enable Updates</string>
|
||||
<string name="telemetry">Enable/Disable Telemetry</string>
|
||||
|
||||
<string name="title">Important Info</string>
|
||||
<string name="message0">Please restart after the first time!\nRepeat this step after every restart!</string>
|
||||
<string name="message0">NOTE: Set light theme, or else text will be white on white\nPlease restart after the first time!\nRepeat this step after every restart!</string>
|
||||
<string name="message1">Please remove *all* the accounts on your Device!\nRepeat this step after every restart!</string>
|
||||
<string name="message2">Device Owner has not been set!</string>
|
||||
<string name="message3">What would you like to do?</string>
|
||||
@ -20,4 +21,6 @@
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="disable">Disable Telemetry</string>
|
||||
<string name="enable">Enable Telemetry</string>
|
||||
<string name="audio">Enable Background Audio for Installed Apps\n(Must reapply after reboot)</string>
|
||||
|
||||
</resources>
|
@ -2,7 +2,7 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Oculess" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimary">#024abf</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
|