diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 43c189f..ded05dc 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -4,3 +4,7 @@
private static *** newProcess(...);
}
-dontwarn rikka.shizuku.**
+
+# Native Shevery Privilege API
+-keep class com.hamondev.shevery.api.** { *; }
+-dontwarn com.hamondev.shevery.api.**
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 655ddfa..7db92bc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,12 @@
android:maxSdkVersion="28" />
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/hamondev/shevery/api/SheveryApiConstants.java b/app/src/main/java/com/hamondev/shevery/api/SheveryApiConstants.java
new file mode 100644
index 0000000..7c69978
--- /dev/null
+++ b/app/src/main/java/com/hamondev/shevery/api/SheveryApiConstants.java
@@ -0,0 +1,53 @@
+package com.hamondev.shevery.api;
+
+/**
+ * Constants for the native Shevery Privilege API.
+ */
+public final class SheveryApiConstants {
+
+ private SheveryApiConstants() {
+ }
+
+ public static final int SERVER_VERSION = 14;
+ public static final int SERVER_PATCH_VERSION = 5;
+
+ // Binder interface
+ public static final String BINDER_DESCRIPTOR = "moe.shizuku.server.IShizukuService";
+ public static final int BINDER_TRANSACTION_transact = 1;
+
+ // Package identifiers
+ public static final String MANAGER_APPLICATION_ID = "com.hamondev.shevery";
+ public static final String SHIZUKU_STUB_APPLICATION_ID = "moe.shizuku.privileged.api";
+
+ // Native Shevery permissions
+ public static final String PERMISSION_GROUP_API = "com.hamondev.shevery.permission-group.API";
+ public static final String PERMISSION_PRIVILEGED_API = "com.hamondev.shevery.permission.PRIVILEGED_API";
+ public static final String PERMISSION_MANAGER = "com.hamondev.shevery.permission.MANAGER";
+
+ // Client metadata markers
+ public static final String META_DATA_CLIENT_SUPPORT = "com.hamondev.shevery.client.PRIVILEGED_API_SUPPORT";
+
+ // ContentProvider authorities & methods
+ public static final String PROVIDER_SUFFIX = ".shevery";
+ public static final String LEGACY_PROVIDER_SUFFIX = ".shizuku";
+ public static final String MANAGER_PROVIDER_AUTHORITY = MANAGER_APPLICATION_ID + PROVIDER_SUFFIX;
+ public static final String MANAGER_LEGACY_PROVIDER_AUTHORITY = MANAGER_APPLICATION_ID + LEGACY_PROVIDER_SUFFIX;
+
+ public static final String METHOD_SEND_BINDER = "sendBinder";
+ public static final String METHOD_GET_BINDER = "getBinder";
+ public static final String METHOD_REQUEST_BINDER = "requestBinder";
+
+ // Extras & broadcast actions
+ public static final String EXTRA_BINDER = "com.hamondev.shevery.intent.extra.BINDER";
+ public static final String LEGACY_EXTRA_BINDER = "moe.shizuku.privileged.api.intent.extra.BINDER";
+ public static final String EXTRA_PERMISSION_CHANNEL = "com.hamondev.shevery.intent.extra.PERMISSION_CHANNEL";
+ public static final String EXTRA_REQUESTED_PERMISSION = "com.hamondev.shevery.intent.extra.REQUESTED_PERMISSION";
+
+ public static final String ACTION_BINDER_RECEIVED = "com.hamondev.shevery.api.action.BINDER_RECEIVED";
+ public static final String ACTION_REQUEST_PERMISSION = "com.hamondev.shevery.intent.action.REQUEST_PERMISSION";
+ public static final String ACTION_REQUEST_BINDER = "com.hamondev.shevery.intent.action.REQUEST_BINDER";
+
+ // Attach application channel negotiation
+ public static final String ATTACH_APPLICATION_CLIENT_CHANNEL = "shevery:attach-client-channel";
+ public static final String CHANNEL_SHEVERY_NATIVE = "shevery_native";
+}
diff --git a/app/src/main/java/com/hamondev/shevery/api/SheveryPrivilege.java b/app/src/main/java/com/hamondev/shevery/api/SheveryPrivilege.java
new file mode 100644
index 0000000..92c270e
--- /dev/null
+++ b/app/src/main/java/com/hamondev/shevery/api/SheveryPrivilege.java
@@ -0,0 +1,362 @@
+package com.hamondev.shevery.api;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import moe.shizuku.api.BinderContainer;
+import moe.shizuku.server.IShizukuApplication;
+import moe.shizuku.server.IShizukuService;
+
+/**
+ * Native, low-latency Privilege API for Shevery ({@code com.hamondev.shevery.permission.PRIVILEGED_API}).
+ */
+public final class SheveryPrivilege {
+
+ private static final String TAG = "SheveryPrivilege";
+ private static final int TRANSACTION_attachApplication_v13 = 18;
+
+ private static final String ATTACH_APPLICATION_API_VERSION = "shizuku:attach-api-version";
+ private static final String ATTACH_APPLICATION_PACKAGE_NAME = "shizuku:attach-package-name";
+ private static final String BIND_APPLICATION_SERVER_UID = "shizuku:attach-reply-uid";
+ private static final String BIND_APPLICATION_SERVER_VERSION = "shizuku:attach-reply-version";
+ private static final String BIND_APPLICATION_SERVER_PATCH_VERSION = "shizuku:attach-reply-patch-version";
+ private static final String BIND_APPLICATION_SERVER_SECONTEXT = "shizuku:attach-reply-secontext";
+ private static final String BIND_APPLICATION_PERMISSION_GRANTED = "shizuku:attach-reply-permission-granted";
+ private static final String BIND_APPLICATION_SHOULD_SHOW_REQUEST_PERMISSION_RATIONALE = "shizuku:attach-reply-should-show-request-permission-rationale";
+ private static final String REQUEST_PERMISSION_REPLY_ALLOWED = "shizuku:request-permission-reply-allowed";
+
+ private static volatile IBinder binder;
+ private static volatile IShizukuService service;
+
+ private static volatile int serverUid = -1;
+ private static volatile int serverApiVersion = -1;
+ private static volatile int serverPatchVersion = -1;
+ private static volatile String serverContext = null;
+ private static volatile boolean permissionGranted = false;
+ private static volatile boolean shouldShowRequestPermissionRationale = false;
+ private static volatile boolean binderReady = false;
+
+ public interface OnBinderReceivedListener {
+ void onBinderReceived();
+ }
+
+ public interface OnBinderDeadListener {
+ void onBinderDead();
+ }
+
+ public interface OnPermissionResultListener {
+ void onPermissionResult(int requestCode, int grantResult);
+ }
+
+ private static final class ListenerEntry {
+ final T listener;
+ final Handler handler;
+
+ ListenerEntry(@NonNull T listener, @Nullable Handler handler) {
+ this.listener = listener;
+ this.handler = handler;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ListenerEntry)) return false;
+ ListenerEntry> that = (ListenerEntry>) o;
+ return Objects.equals(listener, that.listener);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(listener);
+ }
+ }
+
+ private static final CopyOnWriteArraySet> RECEIVED_LISTENERS = new CopyOnWriteArraySet<>();
+ private static final CopyOnWriteArraySet> DEAD_LISTENERS = new CopyOnWriteArraySet<>();
+ private static final CopyOnWriteArraySet> PERMISSION_LISTENERS = new CopyOnWriteArraySet<>();
+ private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
+
+ private static final IShizukuApplication SHEVERY_APPLICATION = new IShizukuApplication.Stub() {
+
+ @Override
+ public void bindApplication(Bundle data) {
+ serverUid = data.getInt(BIND_APPLICATION_SERVER_UID, -1);
+ serverApiVersion = data.getInt(BIND_APPLICATION_SERVER_VERSION, -1);
+ serverPatchVersion = data.getInt(BIND_APPLICATION_SERVER_PATCH_VERSION, -1);
+ serverContext = data.getString(BIND_APPLICATION_SERVER_SECONTEXT);
+ permissionGranted = data.getBoolean(BIND_APPLICATION_PERMISSION_GRANTED, false);
+ shouldShowRequestPermissionRationale = data.getBoolean(BIND_APPLICATION_SHOULD_SHOW_REQUEST_PERMISSION_RATIONALE, false);
+
+ binderReady = true;
+ dispatchBinderReceived();
+ }
+
+ @Override
+ public void dispatchRequestPermissionResult(int requestCode, Bundle data) {
+ boolean allowed = data != null && data.getBoolean(REQUEST_PERMISSION_REPLY_ALLOWED, false);
+ permissionGranted = allowed;
+ if (allowed) {
+ shouldShowRequestPermissionRationale = false;
+ }
+ int result = allowed ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
+ dispatchPermissionResult(requestCode, result);
+ }
+
+ @Override
+ public void showPermissionConfirmation(int requestUid, int requestPid, String requestPackageName, int requestCode) {
+ }
+ };
+
+ private static final IBinder.DeathRecipient DEATH_RECIPIENT = () -> {
+ binderReady = false;
+ onBinderReceived(null, null);
+ };
+
+ private SheveryPrivilege() {
+ }
+
+ /**
+ * Proactive fast pull acquisition: connects directly to Shevery Manager's provider.
+ */
+ public static boolean connectFast(@NonNull Context context) {
+ if (pingBinder()) {
+ return true;
+ }
+ final String pkg = context.getPackageName();
+ final String[] authorities = new String[]{
+ SheveryApiConstants.MANAGER_PROVIDER_AUTHORITY,
+ SheveryApiConstants.MANAGER_LEGACY_PROVIDER_AUTHORITY,
+ pkg + SheveryApiConstants.PROVIDER_SUFFIX
+ };
+
+ for (String authority : authorities) {
+ try {
+ Bundle extras = new Bundle();
+ extras.putString(ATTACH_APPLICATION_PACKAGE_NAME, pkg);
+ extras.putString(SheveryApiConstants.ATTACH_APPLICATION_CLIENT_CHANNEL, SheveryApiConstants.CHANNEL_SHEVERY_NATIVE);
+ Bundle reply = context.getContentResolver().call(
+ Uri.parse("content://" + authority),
+ SheveryApiConstants.METHOD_REQUEST_BINDER,
+ pkg,
+ extras
+ );
+ if (reply != null) {
+ reply.setClassLoader(BinderContainer.class.getClassLoader());
+ BinderContainer container = reply.getParcelable(SheveryApiConstants.EXTRA_BINDER);
+ if (container == null) {
+ container = reply.getParcelable(SheveryApiConstants.LEGACY_EXTRA_BINDER);
+ }
+ if (container != null && container.binder != null && container.binder.pingBinder()) {
+ onBinderReceived(container.binder, pkg);
+ return true;
+ }
+ }
+ } catch (Throwable ignored) {
+ }
+ }
+ return pingBinder();
+ }
+
+ private static boolean attachApplicationFast(@NonNull IBinder targetBinder, @NonNull String packageName) throws RemoteException {
+ Bundle args = new Bundle();
+ args.putInt(ATTACH_APPLICATION_API_VERSION, SheveryApiConstants.SERVER_VERSION);
+ args.putString(ATTACH_APPLICATION_PACKAGE_NAME, packageName);
+ args.putString(SheveryApiConstants.ATTACH_APPLICATION_CLIENT_CHANNEL, SheveryApiConstants.CHANNEL_SHEVERY_NATIVE);
+ args.putString(SheveryApiConstants.EXTRA_REQUESTED_PERMISSION, SheveryApiConstants.PERMISSION_PRIVILEGED_API);
+
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ data.writeInterfaceToken(SheveryApiConstants.BINDER_DESCRIPTOR);
+ data.writeStrongBinder(SHEVERY_APPLICATION.asBinder());
+ data.writeInt(1);
+ args.writeToParcel(data, 0);
+ boolean result = targetBinder.transact(TRANSACTION_attachApplication_v13, data, reply, 0);
+ reply.readException();
+ return result;
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+
+ public static synchronized void onBinderReceived(@Nullable IBinder newBinder, @Nullable String packageName) {
+ if (binder == newBinder && (newBinder == null || newBinder.pingBinder())) {
+ return;
+ }
+
+ if (newBinder == null) {
+ if (binder != null) {
+ try {
+ binder.unlinkToDeath(DEATH_RECIPIENT, 0);
+ } catch (Throwable ignored) {
+ }
+ }
+ binder = null;
+ service = null;
+ serverUid = -1;
+ serverApiVersion = -1;
+ serverPatchVersion = -1;
+ serverContext = null;
+ permissionGranted = false;
+ binderReady = false;
+ dispatchBinderDead();
+ return;
+ }
+
+ try {
+ newBinder.linkToDeath(DEATH_RECIPIENT, 0);
+ } catch (RemoteException e) {
+ return;
+ }
+
+ binder = newBinder;
+ service = IShizukuService.Stub.asInterface(newBinder);
+
+ if (packageName != null) {
+ try {
+ attachApplicationFast(newBinder, packageName);
+ } catch (Throwable t) {
+ Log.w(TAG, "attachApplicationFast failed", t);
+ }
+ }
+ }
+
+ public static boolean pingBinder() {
+ return binder != null && binder.pingBinder();
+ }
+
+ @NonNull
+ public static IShizukuService requireService() {
+ IShizukuService s = service;
+ if (s == null) {
+ throw new IllegalStateException("Shevery service is not attached");
+ }
+ return s;
+ }
+
+ public static boolean isGranted() {
+ return checkSelfPermission() == PackageManager.PERMISSION_GRANTED;
+ }
+
+ public static int checkSelfPermission() {
+ if (permissionGranted) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ try {
+ permissionGranted = requireService().checkSelfPermission();
+ return permissionGranted ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void requestPermission(int requestCode) {
+ try {
+ requireService().requestPermission(requestCode);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static int getVersion() {
+ if (serverApiVersion != -1) return serverApiVersion;
+ try {
+ serverApiVersion = requireService().getVersion();
+ return serverApiVersion;
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ } catch (SecurityException e) {
+ return -1;
+ }
+ }
+
+ public static SheveryRemoteProcess newProcess(@NonNull String[] cmd, @Nullable String[] env, @Nullable String dir) {
+ try {
+ return new SheveryRemoteProcess(requireService().newProcess(cmd, env, dir));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void addBinderReceivedListener(@NonNull OnBinderReceivedListener listener) {
+ addBinderReceivedListener(listener, null);
+ }
+
+ public static void addBinderReceivedListener(@NonNull OnBinderReceivedListener listener, @Nullable Handler handler) {
+ RECEIVED_LISTENERS.add(new ListenerEntry<>(listener, handler));
+ if (pingBinder() && binderReady) {
+ postDispatch(handler, listener::onBinderReceived);
+ }
+ }
+
+ public static void removeBinderReceivedListener(@NonNull OnBinderReceivedListener listener) {
+ RECEIVED_LISTENERS.remove(new ListenerEntry<>(listener, null));
+ }
+
+ public static void addBinderDeadListener(@NonNull OnBinderDeadListener listener) {
+ addBinderDeadListener(listener, null);
+ }
+
+ public static void addBinderDeadListener(@NonNull OnBinderDeadListener listener, @Nullable Handler handler) {
+ DEAD_LISTENERS.add(new ListenerEntry<>(listener, handler));
+ }
+
+ public static void removeBinderDeadListener(@NonNull OnBinderDeadListener listener) {
+ DEAD_LISTENERS.remove(new ListenerEntry<>(listener, null));
+ }
+
+ public static void addRequestPermissionResultListener(@NonNull OnPermissionResultListener listener) {
+ addRequestPermissionResultListener(listener, null);
+ }
+
+ public static void addRequestPermissionResultListener(@NonNull OnPermissionResultListener listener, @Nullable Handler handler) {
+ PERMISSION_LISTENERS.add(new ListenerEntry<>(listener, handler));
+ }
+
+ public static void removeRequestPermissionResultListener(@NonNull OnPermissionResultListener listener) {
+ PERMISSION_LISTENERS.remove(new ListenerEntry<>(listener, null));
+ }
+
+ private static void dispatchBinderReceived() {
+ for (ListenerEntry entry : RECEIVED_LISTENERS) {
+ postDispatch(entry.handler, entry.listener::onBinderReceived);
+ }
+ }
+
+ private static void dispatchBinderDead() {
+ for (ListenerEntry entry : DEAD_LISTENERS) {
+ postDispatch(entry.handler, entry.listener::onBinderDead);
+ }
+ }
+
+ private static void dispatchPermissionResult(int requestCode, int result) {
+ for (ListenerEntry entry : PERMISSION_LISTENERS) {
+ postDispatch(entry.handler, () -> entry.listener.onPermissionResult(requestCode, result));
+ }
+ }
+
+ private static void postDispatch(@Nullable Handler handler, @NonNull Runnable action) {
+ if (handler != null) {
+ handler.post(action);
+ } else if (Looper.myLooper() == Looper.getMainLooper()) {
+ action.run();
+ } else {
+ MAIN_HANDLER.post(action);
+ }
+ }
+}
diff --git a/app/src/main/java/com/hamondev/shevery/api/SheveryProvider.java b/app/src/main/java/com/hamondev/shevery/api/SheveryProvider.java
new file mode 100644
index 0000000..4b8d751
--- /dev/null
+++ b/app/src/main/java/com/hamondev/shevery/api/SheveryProvider.java
@@ -0,0 +1,83 @@
+package com.hamondev.shevery.api;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import moe.shizuku.api.BinderContainer;
+
+/**
+ * Native ContentProvider endpoint for Shevery Privilege API ({@code ${applicationId}.shevery}).
+ */
+public class SheveryProvider extends ContentProvider {
+
+ @Override
+ public boolean onCreate() {
+ Context context = getContext();
+ if (context != null) {
+ SheveryPrivilege.connectFast(context);
+ }
+ return true;
+ }
+
+ @Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ super.attachInfo(context, info);
+ }
+
+ @Nullable
+ @Override
+ public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
+ if (extras == null) {
+ return null;
+ }
+ if (SheveryApiConstants.METHOD_SEND_BINDER.equals(method)) {
+ extras.setClassLoader(BinderContainer.class.getClassLoader());
+ BinderContainer container = extras.getParcelable(SheveryApiConstants.EXTRA_BINDER);
+ if (container == null) {
+ container = extras.getParcelable(SheveryApiConstants.LEGACY_EXTRA_BINDER);
+ }
+ if (container != null && container.binder != null) {
+ Context context = getContext();
+ SheveryPrivilege.onBinderReceived(container.binder, context != null ? context.getPackageName() : null);
+ }
+ return new Bundle();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/com/hamondev/shevery/api/SheveryRemoteProcess.java b/app/src/main/java/com/hamondev/shevery/api/SheveryRemoteProcess.java
new file mode 100644
index 0000000..6fd4b45
--- /dev/null
+++ b/app/src/main/java/com/hamondev/shevery/api/SheveryRemoteProcess.java
@@ -0,0 +1,154 @@
+package com.hamondev.shevery.api;
+
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import moe.shizuku.server.IRemoteProcess;
+
+/**
+ * Remote process wrapper for native Shevery commands.
+ */
+public final class SheveryRemoteProcess extends Process implements Parcelable {
+
+ private static final Set CACHE = Collections.synchronizedSet(new ArraySet<>());
+ private static final String TAG = "SheveryRemoteProcess";
+
+ private IRemoteProcess remote;
+ private OutputStream os;
+ private InputStream is;
+ private InputStream err;
+
+ SheveryRemoteProcess(IRemoteProcess remote) {
+ this.remote = remote;
+ try {
+ this.remote.asBinder().linkToDeath(() -> {
+ this.remote = null;
+ CACHE.remove(SheveryRemoteProcess.this);
+ }, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "linkToDeath", e);
+ }
+ CACHE.add(this);
+ }
+
+ private SheveryRemoteProcess(Parcel in) {
+ this.remote = IRemoteProcess.Stub.asInterface(in.readStrongBinder());
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ if (os == null) {
+ try {
+ os = new ParcelFileDescriptor.AutoCloseOutputStream(remote.getOutputStream());
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return os;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ if (is == null) {
+ try {
+ is = new ParcelFileDescriptor.AutoCloseInputStream(remote.getInputStream());
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return is;
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ if (err == null) {
+ try {
+ err = new ParcelFileDescriptor.AutoCloseInputStream(remote.getErrorStream());
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return err;
+ }
+
+ @Override
+ public int waitFor() throws InterruptedException {
+ try {
+ return remote.waitFor();
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
+ long startTime = System.nanoTime();
+ long rem = unit.toNanos(timeout);
+
+ do {
+ try {
+ exitValue();
+ return true;
+ } catch (IllegalThreadStateException ex) {
+ if (rem > 0) {
+ Thread.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100));
+ }
+ }
+ rem = unit.toNanos(timeout) - (System.nanoTime() - startTime);
+ } while (rem > 0);
+ return false;
+ }
+
+ @Override
+ public int exitValue() {
+ try {
+ return remote.exitValue();
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ if (remote != null) {
+ remote.destroy();
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeStrongBinder(remote.asBinder());
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public SheveryRemoteProcess createFromParcel(Parcel in) {
+ return new SheveryRemoteProcess(in);
+ }
+
+ @Override
+ public SheveryRemoteProcess[] newArray(int size) {
+ return new SheveryRemoteProcess[size];
+ }
+ };
+}
diff --git a/app/src/main/java/com/hmndev/chronika/ChronikaApp.kt b/app/src/main/java/com/hmndev/chronika/ChronikaApp.kt
index 7f5c688..e4b4fdf 100644
--- a/app/src/main/java/com/hmndev/chronika/ChronikaApp.kt
+++ b/app/src/main/java/com/hmndev/chronika/ChronikaApp.kt
@@ -13,7 +13,7 @@ class ChronikaApp : Application() {
SettingsStore.init(this)
S.setLanguage(AppLanguage.resolve(SettingsStore.current().language))
JournalDb.get(this)
- ShizukuManager.init()
+ ShizukuManager.init(this)
ShizukuManager.refresh()
}
}
diff --git a/app/src/main/java/com/hmndev/chronika/shizuku/ShizukuManager.kt b/app/src/main/java/com/hmndev/chronika/shizuku/ShizukuManager.kt
index e7f0a1e..9c02985 100644
--- a/app/src/main/java/com/hmndev/chronika/shizuku/ShizukuManager.kt
+++ b/app/src/main/java/com/hmndev/chronika/shizuku/ShizukuManager.kt
@@ -3,6 +3,7 @@ package com.hmndev.chronika.shizuku
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
+import com.hamondev.shevery.api.SheveryPrivilege
import com.hmndev.chronika.i18n.S
import com.hmndev.chronika.service.CaptureEngine
import com.hmndev.chronika.service.JournalAccessibilityService
@@ -18,11 +19,18 @@ import rikka.shizuku.Shizuku
import java.io.BufferedReader
import java.io.InputStreamReader
+enum class PrivilegedBackend {
+ SHEVERY,
+ SHIZUKU,
+ NONE
+}
+
data class ShizukuState(
val available: Boolean = false,
val permissionGranted: Boolean = false,
val version: Int = 0,
val message: String = S["shizuku.not_running"],
+ val backend: PrivilegedBackend = PrivilegedBackend.NONE,
)
data class ShellResult(
@@ -46,6 +54,16 @@ object ShizukuManager {
@Volatile
private var initialized = false
+ private val sheveryBinderReceived = SheveryPrivilege.OnBinderReceivedListener {
+ refresh()
+ }
+ private val sheveryBinderDead = SheveryPrivilege.OnBinderDeadListener {
+ refresh()
+ }
+ private val sheveryPermissionResult = SheveryPrivilege.OnPermissionResultListener { _, grantResult ->
+ refresh(grantResult == PackageManager.PERMISSION_GRANTED)
+ }
+
private val binderReceived = Shizuku.OnBinderReceivedListener {
refresh()
}
@@ -56,38 +74,71 @@ object ShizukuManager {
refresh(grantResult == PackageManager.PERMISSION_GRANTED)
}
- fun init() {
- if (initialized) return
- initialized = true
- runCatching {
- Shizuku.addBinderReceivedListenerSticky(binderReceived)
- Shizuku.addBinderDeadListener(binderDead)
- Shizuku.addRequestPermissionResultListener(permissionResult)
+ fun init(context: Context? = null) {
+ if (!initialized) {
+ initialized = true
+ runCatching {
+ SheveryPrivilege.addBinderReceivedListener(sheveryBinderReceived)
+ SheveryPrivilege.addBinderDeadListener(sheveryBinderDead)
+ SheveryPrivilege.addRequestPermissionResultListener(sheveryPermissionResult)
+ }
+ runCatching {
+ Shizuku.addBinderReceivedListenerSticky(binderReceived)
+ Shizuku.addBinderDeadListener(binderDead)
+ Shizuku.addRequestPermissionResultListener(permissionResult)
+ }
+ }
+ if (context != null) {
+ runCatching { SheveryPrivilege.connectFast(context) }
}
refresh()
}
fun refresh(forcedPermission: Boolean? = null) {
- val available = runCatching { Shizuku.pingBinder() }.getOrDefault(false)
- val version = if (available) {
+ val sheveryAvailable = runCatching { SheveryPrivilege.pingBinder() }.getOrDefault(false)
+ if (sheveryAvailable) {
+ val version = runCatching { SheveryPrivilege.getVersion() }.getOrDefault(0)
+ val permission = when {
+ forcedPermission != null -> forcedPermission
+ else -> runCatching {
+ SheveryPrivilege.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
+ }.getOrDefault(false)
+ }
+ val message = when {
+ !permission -> S["shizuku.permission_needed"]
+ else -> S.f("shizuku.active", version).replace("Shizuku", "Shevery")
+ }
+ _state.value = ShizukuState(
+ available = true,
+ permissionGranted = permission,
+ version = version,
+ message = message,
+ backend = PrivilegedBackend.SHEVERY
+ )
+ return
+ }
+
+ val shizukuAvailable = runCatching { Shizuku.pingBinder() }.getOrDefault(false)
+ val version = if (shizukuAvailable) {
runCatching { Shizuku.getVersion() }.getOrDefault(0)
} else {
0
}
val permission = when {
- !available -> false
+ !shizukuAvailable -> false
forcedPermission != null -> forcedPermission
runCatching { Shizuku.isPreV11() }.getOrDefault(true) -> false
else -> runCatching {
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
}.getOrDefault(false)
}
+ val backend = if (shizukuAvailable) PrivilegedBackend.SHIZUKU else PrivilegedBackend.NONE
val message = when {
- !available -> S["shizuku.unavailable"]
+ !shizukuAvailable -> S["shizuku.unavailable"]
!permission -> S["shizuku.permission_needed"]
else -> S.f("shizuku.active", version)
}
- _state.value = ShizukuState(available, permission, version, message)
+ _state.value = ShizukuState(shizukuAvailable, permission, version, message, backend)
}
fun requestPermission() {
@@ -96,6 +147,14 @@ object ShizukuManager {
_state.value = current.copy(message = S["shizuku.start_first"])
return
}
+ if (current.backend == PrivilegedBackend.SHEVERY || SheveryPrivilege.pingBinder()) {
+ runCatching {
+ SheveryPrivilege.requestPermission(REQUEST_CODE)
+ }.onFailure {
+ _state.value = current.copy(message = S.f("shizuku.request_failed", it.message))
+ }
+ return
+ }
runCatching {
Shizuku.requestPermission(REQUEST_CODE)
}.onFailure {
@@ -104,27 +163,32 @@ object ShizukuManager {
}
suspend fun runCommand(command: String): ShellResult = withContext(Dispatchers.IO) {
- if (!_state.value.available) {
+ val current = _state.value
+ if (!current.available) {
return@withContext ShellResult(false, message = S["shizuku.not_running"])
}
- if (!_state.value.permissionGranted) {
+ if (!current.permissionGranted) {
return@withContext ShellResult(false, message = S["shizuku.no_permission"])
}
var process: Process? = null
try {
- val method = Shizuku::class.java.getDeclaredMethod(
- "newProcess",
- emptyArray()::class.java,
- emptyArray()::class.java,
- String::class.java
- )
- method.isAccessible = true
- val spawned = method.invoke(
- null,
- arrayOf("sh", "-c", command),
- null,
- null
- ) as Process
+ val spawned: Process = if (current.backend == PrivilegedBackend.SHEVERY || SheveryPrivilege.pingBinder()) {
+ SheveryPrivilege.newProcess(arrayOf("sh", "-c", command), null, null)
+ } else {
+ val method = Shizuku::class.java.getDeclaredMethod(
+ "newProcess",
+ emptyArray()::class.java,
+ emptyArray()::class.java,
+ String::class.java
+ )
+ method.isAccessible = true
+ method.invoke(
+ null,
+ arrayOf("sh", "-c", command),
+ null,
+ null
+ ) as Process
+ }
process = spawned
val (stdout, stderr) = coroutineScope {
val out = async(Dispatchers.IO) {
diff --git a/app/src/main/java/com/hmndev/chronika/util/PermissionTools.kt b/app/src/main/java/com/hmndev/chronika/util/PermissionTools.kt
index 14ca1f1..7757fe0 100644
--- a/app/src/main/java/com/hmndev/chronika/util/PermissionTools.kt
+++ b/app/src/main/java/com/hmndev/chronika/util/PermissionTools.kt
@@ -81,12 +81,13 @@ object PermissionTools {
}
fun openShizuku(context: Context) {
- val launch = context.packageManager.getLaunchIntentForPackage("moe.shizuku.privileged.api")
+ val launch = context.packageManager.getLaunchIntentForPackage("com.hamondev.shevery")
+ ?: context.packageManager.getLaunchIntentForPackage("moe.shizuku.privileged.api")
if (launch != null) {
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(launch)
} else {
- openUrl(context, "https://shizuku.rikka.app/")
+ openUrl(context, "https://github.com/HmnDev-Tech/shevery")
}
}