Introduce RootService

Remote root services using Binder IPC
This commit is contained in:
topjohnwu 2020-07-26 02:06:08 -07:00
parent 71e36cdc81
commit 6d9f30e81b
18 changed files with 669 additions and 72 deletions

View File

@ -112,16 +112,20 @@ subprojects {
buildConfig = false
}
}
}
if (plugins.hasPlugin("com.github.dcendents.android-maven")) {
val sources = android.sourceSets.getByName("main").java.sourceFiles
(rootProject.tasks["javadoc"] as Javadoc).apply {
source = source.plus(android.sourceSets.getByName("main").java.sourceFiles)
classpath = classpath.plus(project.files(android.bootClasspath))
classpath = classpath.plus(configurations.getByName("javadocDeps"))
source += sources
classpath += project.files(android.bootClasspath)
classpath += configurations.getByName("javadocDeps")
}
val sourcesJar = tasks.register("sourcesJar", Jar::class) {
archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.sourceFiles)
from(sources)
}
artifacts {

View File

@ -16,6 +16,8 @@
package com.topjohnwu.superuser.internal;
import androidx.annotation.RestrictTo;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.List;
@ -29,7 +31,8 @@ import java.util.concurrent.TimeoutException;
import static com.topjohnwu.superuser.Shell.EXECUTOR;
class SerialExecutorService extends AbstractExecutorService implements Callable<Void> {
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class SerialExecutorService extends AbstractExecutorService implements Callable<Void> {
private boolean isShutdown = false;
private ArrayDeque<Runnable> mTasks = new ArrayDeque<>();

View File

@ -5,11 +5,15 @@ plugins {
android {
defaultConfig {
applicationId = "com.topjohnwu.libsuexample"
minSdkVersion(16)
minSdkVersion(18)
versionCode = 1
versionName ="1.0"
}
buildFeatures {
viewBinding = true
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
@ -25,4 +29,5 @@ dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation(project(":core"))
implementation(project(":busybox"))
implementation(project(":service"))
}

View File

@ -0,0 +1,9 @@
// ITestService.aidl
package com.topjohnwu.libsuexample;
// Declare any non-default types here with import statements
interface ITestService {
int getPid();
int getUid();
}

View File

@ -17,20 +17,25 @@
package com.topjohnwu.libsuexample;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.topjohnwu.libsuexample.databinding.ActivityMainBinding;
import com.topjohnwu.superuser.BusyBoxInstaller;
import com.topjohnwu.superuser.CallbackList;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ipc.RootService;
import java.io.IOException;
import java.util.List;
@ -39,19 +44,20 @@ public class MainActivity extends Activity {
public static final String TAG = "EXAMPLE";
private TextView console;
private EditText input;
private ScrollView sv;
private List<String> consoleList;
static {
// Configuration
Shell.Config.setFlags(Shell.FLAG_REDIRECT_STDERR);
Shell.Config.verboseLogging(BuildConfig.DEBUG);
// Use internal busybox
Shell.Config.setInitializers(BusyBoxInstaller.class, ExampleInitializer.class);
}
private List<String> consoleList;
private ActivityMainBinding binding;
private ITestService testIPC;
private RootConnection conn = new RootConnection();
private boolean queuedTest = false;
// Demonstrate Shell.Initializer
static class ExampleInitializer extends Shell.Initializer {
@ -62,38 +68,72 @@ public class MainActivity extends Activity {
}
}
// Demonstrate RootService
static class ExampleService extends RootService {
@Override
public IBinder onBind(@NonNull Intent intent) {
return new ITestService.Stub() {
@Override
public int getPid() {
return Process.myPid();
}
@Override
public int getUid() {
return Process.myUid();
}
};
}
}
class RootConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "onServiceConnected");
testIPC = ITestService.Stub.asInterface(service);
if (queuedTest) {
queuedTest = false;
testService();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected");
testIPC = null;
}
}
private void testService() {
try {
consoleList.add("Remote PID: " + testIPC.getPid());
consoleList.add("Remote UID: " + testIPC.getUid());
} catch (RemoteException e) {
Log.e(TAG, "Remote error", e);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
console = findViewById(R.id.console);
input = findViewById(R.id.cmd_input);
sv = findViewById(R.id.sv);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Button sync_cmd = findViewById(R.id.sync_cmd);
Button async_cmd = findViewById(R.id.async_cmd);
Button close_shell = findViewById(R.id.close_shell);
Button sync_script = findViewById(R.id.sync_script);
Button async_script = findViewById(R.id.async_script);
Button clear = findViewById(R.id.clear);
// Run the shell command in the input box synchronously
sync_cmd.setOnClickListener(v -> {
Shell.sh(input.getText().toString()).to(consoleList).exec();
input.setText("");
binding.testSvc.setOnClickListener(v -> {
if (testIPC == null) {
queuedTest = true;
Intent intent = new Intent(this, ExampleService.class);
RootService.bind(intent, conn);
return;
}
testService();
});
// Run the shell command in the input box asynchronously.
// Also demonstrates that Async.Callback works
async_cmd.setOnClickListener(v -> {
Shell.sh(input.getText().toString())
.to(consoleList)
.submit(out -> Log.d(TAG, "async_cmd_result: " + out.getCode()));
input.setText("");
});
binding.closeSvc.setOnClickListener(v -> RootService.unbind(conn));
// Closing a shell is always synchronous
close_shell.setOnClickListener(v -> {
binding.closeShell.setOnClickListener(v -> {
try {
Shell shell = Shell.getCachedShell();
if (shell != null)
@ -104,14 +144,14 @@ public class MainActivity extends Activity {
});
// Load a script from raw resources synchronously
sync_script.setOnClickListener(v ->
binding.syncScript.setOnClickListener(v ->
Shell.sh(getResources().openRawResource(R.raw.info)).to(consoleList).exec());
// Load a script from raw resources asynchronously
async_script.setOnClickListener(v ->
binding.asyncScript.setOnClickListener(v ->
Shell.sh(getResources().openRawResource(R.raw.count)).to(consoleList).submit());
clear.setOnClickListener(v -> consoleList.clear());
binding.clear.setOnClickListener(v -> consoleList.clear());
/* Create a CallbackList to update the UI with Shell output
* Here I demonstrate 2 ways to implement a CallbackList
@ -131,14 +171,14 @@ public class MainActivity extends Activity {
@Override
public void onAddElement(String s) {
console.append(s);
console.append("\n");
sv.postDelayed(() -> sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
binding.console.append(s);
binding.console.append("\n");
binding.sv.postDelayed(() -> binding.sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
}
@Override
public synchronized void clear() {
runOnUiThread(() -> console.setText(""));
runOnUiThread(() -> binding.console.setText(""));
}
}
@ -161,14 +201,14 @@ public class MainActivity extends Activity {
@Override
public void onAddElement(String s) {
console.setText(TextUtils.join("\n", this));
sv.postDelayed(() -> sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
binding.console.setText(TextUtils.join("\n", this));
binding.sv.postDelayed(() -> binding.sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
}
@Override
public synchronized void clear() {
super.clear();
runOnUiThread(() -> console.setText(""));
runOnUiThread(() -> binding.console.setText(""));
}
}
}

View File

@ -31,14 +31,6 @@
</ScrollView>
<EditText
android:id="@+id/cmd_input"
android:inputType="textNoSuggestions|textMultiLine"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/background_light"
android:padding="5dp" />
<LinearLayout
style="?android:buttonStyle"
android:layout_width="match_parent"
@ -47,20 +39,20 @@
android:orientation="horizontal">
<Button
android:id="@+id/sync_cmd"
android:id="@+id/test_svc"
style="?android:borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sync CMD" />
android:text="Test SVC" />
<Button
android:id="@+id/async_cmd"
android:id="@+id/close_svc"
style="?android:borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Async CMD" />
android:text="Close SVC" />
<Button
android:id="@+id/close_shell"
@ -85,7 +77,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sync Script" />
android:text="Test sync" />
<Button
android:id="@+id/async_script"
@ -93,7 +85,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Async Script" />
android:text="Test Async" />
<Button
android:id="@+id/clear"

View File

@ -22,15 +22,7 @@ api_level_arch_detect() {
if [ "$ABILONG" = "x86_64" ]; then ARCH=x64; IS64BIT=true; fi;
}
TOOLPATH=`command -v toolbox`
TOYPATH=`command -v toybox`
BUSYPATH=`command -v busybox`
api_level_arch_detect
echo "Device API: $API"
echo "Device ABI: $ARCH"
[ ! -z $TOOLPATH ] && echo "toolbox at: $TOOLPATH"
[ ! -z $TOYPATH ] && echo "toybox at: $TOYPATH"
[ ! -z $BUSYPATH ] && echo "busybox at: $BUSYPATH"

1
service/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

14
service/build.gradle.kts Normal file
View File

@ -0,0 +1,14 @@
plugins {
id("com.android.library")
}
android {
defaultConfig {
minSdkVersion(18)
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
api(project(":core"))
}

21
service/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.topjohnwu.superuser.ipc" />

View File

@ -0,0 +1,9 @@
// IRootIPC.aidl
package com.topjohnwu.superuser.internal;
// Declare any non-default types here with import statements
interface IRootIPC {
IBinder bind(in Intent intent, IBinder client);
void unbind();
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 2020 John "topjohnwu" Wu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.topjohnwu.superuser.internal;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Looper;
import android.util.Log;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
/**
* Trampoline to start a root service.
* <p>
* This is the only class included in main.jar as raw resources.
* The client code will execute this main method in a root shell.
* <p>
* This class will get the system context by calling into Android private APIs with reflection, and
* uses that to create our client package context. The client context will have the full APK loaded,
* just like it was launched in a non-root environment.
* <p>
* Expected command-line args:
* args[0]: client package name
* args[1]: class name of IPCServer (reason: name could be obfuscated in client APK)
*/
public class IPCMain {
/**
* These private APIs are very unlikely to change, should be relatively
* stable across different Android versions and OEMs.
*/
@SuppressLint("PrivateApi")
public static Context getSystemContext() {
try {
synchronized (Looper.class) {
if (Looper.getMainLooper() == null)
Looper.prepareMainLooper();
}
Class<?> atClazz = Class.forName("android.app.ActivityThread");
Method systemMain = atClazz.getMethod("systemMain");
Object activityThread = systemMain.invoke(null);
Method getSystemContext = atClazz.getMethod("getSystemContext");
return (Context) getSystemContext.invoke(activityThread);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
// Close STDOUT/STDERR since it belongs to the parent shell
System.out.close();
System.err.close();
try {
String packageName = args[0];
String ipcServerClassName = args[1];
Context systemContext = getSystemContext();
Context context = systemContext.createPackageContext(packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
// Use classloader from the package context to run everything
ClassLoader cl = context.getClassLoader();
Class<?> clz = cl.loadClass(ipcServerClassName);
Constructor<?> con = clz.getDeclaredConstructor(Context.class);
con.setAccessible(true);
con.newInstance(context);
// Shall never return
System.exit(0);
} catch (Exception e) {
Log.e("IPC", "Error in IPCMain", e);
System.exit(1);
}
}
}

View File

@ -0,0 +1,158 @@
/*
* Copyright 2020 John "topjohnwu" Wu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.topjohnwu.superuser.ipc;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.IRootIPC;
import com.topjohnwu.superuser.internal.InternalUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import static com.topjohnwu.superuser.ipc.RootService.serialExecutor;
class IPCClient implements IBinder.DeathRecipient {
private static final String BROADCAST_ACTION = "com.topjohnwu.superuser.BROADCAST_IPC";
private static final String INTENT_EXTRA_KEY = "binder_bundle";
private static final String BUNDLE_BINDER_KEY = "binder";
private ComponentName name;
private IRootIPC server = null;
private IBinder binder = null;
private Map<ServiceConnection, Executor> connections = new HashMap<>();
IPCClient(Intent intent) throws InterruptedException, RemoteException, IOException {
name = intent.getComponent();
startRootServer(InternalUtils.getContext(), intent);
}
private void startRootServer(Context context, Intent intent)
throws IOException, InterruptedException, RemoteException {
// Dump main.jar as trampoline
Context de = Build.VERSION.SDK_INT >= 24 ? context.createDeviceProtectedStorageContext() : context;
File mainJar = new File(de.getCacheDir(), "main.jar");
try (InputStream in = context.getResources().openRawResource(R.raw.main);
OutputStream out = new FileOutputStream(mainJar)) {
InternalUtils.pump(in, out);
}
// Register BinderReceiver to receive binder from root process
context.registerReceiver(new BinderReceiver(), new IntentFilter(BROADCAST_ACTION));
// Execute main.jar through root shell
String app_process = new File("/proc/self/exe").getCanonicalPath();
String cmd = String.format(
"(CLASSPATH=%1$s %2$s /system/bin --nice-name=%4$s:root %3$s %4$s %5$s)&",
mainJar, app_process, "com.topjohnwu.superuser.internal.IPCMain",
context.getPackageName(), IPCServer.class.getName());
synchronized (this) {
Shell.su(cmd).exec();
// Wait for broadcast receiver
wait();
}
server.asBinder().linkToDeath(this, 0);
binder = server.bind(intent, new Binder());
}
boolean isSameService(Intent intent) {
return name.equals(intent.getComponent());
}
void newConnection(ServiceConnection conn, Executor executor) {
connections.put(conn, executor);
if (binder != null)
executor.execute(() -> conn.onServiceConnected(name, binder));
else if (Build.VERSION.SDK_INT >= 28)
executor.execute(() -> conn.onNullBinding(name));
}
boolean unbind(ServiceConnection conn) {
Executor executor = connections.remove(conn);
if (executor != null) {
executor.execute(() -> conn.onServiceDisconnected(name));
if (connections.isEmpty()) {
server.asBinder().unlinkToDeath(this, 0);
try {
server.unbind();
} catch (RemoteException ignored) {}
server = null;
binder = null;
return true;
}
}
return false;
}
@Override
public void binderDied() {
server.asBinder().unlinkToDeath(this, 0);
server = null;
binder = null;
for (Map.Entry<ServiceConnection, Executor> entry : connections.entrySet()) {
ServiceConnection conn = entry.getKey();
entry.getValue().execute(() -> {
if (Build.VERSION.SDK_INT >= 26) {
conn.onBindingDied(name);
}
conn.onServiceDisconnected(name);
});
}
connections.clear();
serialExecutor.execute(() -> RootService.active.remove(this));
}
static Intent getBroadcastIntent(String packageName, IRootIPC.Stub ipc) {
Bundle bundle = new Bundle();
bundle.putBinder(BUNDLE_BINDER_KEY, ipc);
return new Intent()
.setPackage(packageName)
.setAction(BROADCAST_ACTION)
.putExtra(INTENT_EXTRA_KEY, bundle);
}
class BinderReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
context.unregisterReceiver(this);
Bundle bundle = intent.getBundleExtra(INTENT_EXTRA_KEY);
IBinder binder = bundle.getBinder(BUNDLE_BINDER_KEY);
synchronized (IPCClient.this) {
server = IRootIPC.Stub.asInterface(binder);
IPCClient.this.notifyAll();
}
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright 2020 John "topjohnwu" Wu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.topjohnwu.superuser.ipc;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.Looper;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.IRootIPC;
import com.topjohnwu.superuser.internal.InternalUtils;
import java.lang.reflect.Constructor;
import static com.topjohnwu.superuser.ipc.RootService.INTENT_VERBOSE_KEY;
class IPCServer extends IRootIPC.Stub implements IBinder.DeathRecipient {
private RootService service;
private IBinder client;
private Intent intent;
private Context context;
IPCServer(Context context) {
this.context = context;
String packageName = context.getPackageName();
Intent broadcast = IPCClient.getBroadcastIntent(packageName, this);
context.sendBroadcast(broadcast);
Looper.loop();
}
@Override
public synchronized IBinder bind(Intent intent, IBinder client) {
this.intent = intent.cloneFilter();
Shell.Config.verboseLogging(intent.getBooleanExtra(INTENT_VERBOSE_KEY, false));
try {
if (service == null) {
String name = intent.getComponent().getClassName();
Class<? extends RootService> clz = (Class<? extends RootService>) Class.forName(name);
Constructor<? extends RootService> constructor = clz.getDeclaredConstructor();
constructor.setAccessible(true);
service = constructor.newInstance();
service.attach(context);
service.onCreate();
} else {
service.onRebind(intent);
}
this.client = client;
client.linkToDeath(this, 0);
return service.onBind(intent);
} catch (Exception e) {
InternalUtils.stackTrace(e);
return null;
}
}
@Override
public synchronized void unbind() {
boolean rebind = service.onUnbind(intent);
client.unlinkToDeath(this, 0);
client = null;
if (!rebind) {
service.onDestroy();
System.exit(0);
}
}
@Override
public void binderDied() {
unbind();
}
}

View File

@ -0,0 +1,168 @@
/*
* Copyright 2020 John "topjohnwu" Wu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.topjohnwu.superuser.ipc;
import android.app.Service;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import androidx.annotation.NonNull;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.InternalUtils;
import com.topjohnwu.superuser.internal.SerialExecutorService;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import static com.topjohnwu.superuser.Shell.FLAG_VERBOSE_LOGGING;
import static com.topjohnwu.superuser.internal.InternalUtils.hasFlag;
/**
* A remote root service using native Android Binder IPC.
* <p>
* This class is almost a complete recreation of a bound service running in a root process.
* Instead of using the original {@code Context.bindService(...)} methods to start and bind
* to a service, use the provided static methods {@code RootService.bind(...)}.
* Please note, unlike normal services, RootServices do not have an API similar to
* {@link Context#startService(Intent)} because RootServices are strictly bound-only.
* <p>
* Because the service will not run in the same process as your application, you have to use AIDL
* to define the IPC interface for communication. Please read the official documentations for more
* details.
* @see Service
* @see <a href="Bound services">https://developer.android.com/guide/components/bound-services</a>
* @see <a href="Android Interface Definition Language (AIDL)">https://developer.android.com/guide/components/aidl</a>
*/
public abstract class RootService extends ContextWrapper {
static final String INTENT_VERBOSE_KEY = "verbose_logging";
static List<IPCClient> active = new ArrayList<>();
static ExecutorService serialExecutor = new SerialExecutorService();
/**
* Connect to a root service, creating if needed.
* @param intent identifies the service to connect to.
* @param executor callbacks on ServiceConnection will be called on this executor.
* @param conn receives information as the service is started and stopped.
* @see Context#bindService(Intent, int, Executor, ServiceConnection)
*/
public static void bind(
@NonNull Intent intent,
@NonNull Executor executor,
@NonNull ServiceConnection conn) {
serialExecutor.execute(() -> {
// If no root access, don't even bother
if (!Shell.rootAccess())
return;
Intent intentCopy = new Intent(intent);
intentCopy.putExtra(INTENT_VERBOSE_KEY, hasFlag(FLAG_VERBOSE_LOGGING));
for (IPCClient client : active) {
if (client.isSameService(intentCopy)) {
client.newConnection(conn, executor);
return;
}
}
try {
IPCClient client = new IPCClient(intentCopy);
client.newConnection(conn, executor);
active.add(client);
} catch (Exception e) {
InternalUtils.stackTrace(e);
}
});
}
/**
* Connect to a root service, creating if needed.
* @param intent identifies the service to connect to.
* @param conn receives information as the service is started and stopped.
* @see Context#bindService(Intent, ServiceConnection, int)
*/
public static void bind(@NonNull Intent intent, @NonNull ServiceConnection conn) {
bind(intent, UiThreadHandler.executor, conn);
}
/**
* Disconnect from a root service.
* @param conn the connection interface previously supplied to {@link #bind(Intent, ServiceConnection)}
* @see Context#unbindService(ServiceConnection)
*/
public static void unbind(@NonNull ServiceConnection conn) {
serialExecutor.execute(() -> {
Iterator<IPCClient> it = active.iterator();
while (it.hasNext()) {
IPCClient client = it.next();
if (client.unbind(conn)) {
it.remove();
break;
}
}
});
}
public RootService() {
super(null);
}
void attach(Context base) {
attachBaseContext(base);
}
@Override
public final Context getApplicationContext() {
// Always return ourselves
return this;
}
/**
* @see Service#onBind(Intent)
*/
abstract public IBinder onBind(@NonNull Intent intent);
/**
* @see Service#onCreate()
*/
public void onCreate() {}
/**
* @see Service#onUnbind(Intent)
*/
public boolean onUnbind(@NonNull Intent intent) {
return false;
}
/**
* @see Service#onRebind(Intent)
*/
public void onRebind(@NonNull Intent intent) {}
/**
* @see Service#onDestroy()
*/
public void onDestroy() {}
}

Binary file not shown.

View File

@ -1 +1 @@
include(":core", ":example", ":busybox", ":io")
include(":core", ":example", ":busybox", ":io", ":service")