From afacb215149d8c439eddf10c6ece6460be8ed7e5 Mon Sep 17 00:00:00 2001 From: HmnDev-Tech <256042304+HmnDev-Tech@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:54:54 +0500 Subject: [PATCH] feat(api): add native Shevery-API support alongside Shizuku - Integrate com.hamondev.shevery.api (SheveryPrivilege, SheveryProvider, SheveryApiConstants, SheveryRemoteProcess) - Support fast-path binder acquisition on startup via connectFast() - Add com.hamondev.shevery.permission.PRIVILEGED_API and client metadata - Declare ${applicationId}.shevery ContentProvider authority - Enable multi-backend support in ShizukuManager with automatic Shevery priority - Prioritize launching Shevery Manager in PermissionTools.openShizuku - Update proguard-rules.pro to keep com.hamondev.shevery.api.** --- app/proguard-rules.pro | 4 + app/src/main/AndroidManifest.xml | 17 + .../shevery/api/SheveryApiConstants.java | 53 +++ .../shevery/api/SheveryPrivilege.java | 362 ++++++++++++++++++ .../hamondev/shevery/api/SheveryProvider.java | 83 ++++ .../shevery/api/SheveryRemoteProcess.java | 154 ++++++++ .../java/com/hmndev/chronika/ChronikaApp.kt | 2 +- .../hmndev/chronika/shizuku/ShizukuManager.kt | 118 ++++-- .../hmndev/chronika/util/PermissionTools.kt | 5 +- 9 files changed, 768 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/hamondev/shevery/api/SheveryApiConstants.java create mode 100644 app/src/main/java/com/hamondev/shevery/api/SheveryPrivilege.java create mode 100644 app/src/main/java/com/hamondev/shevery/api/SheveryProvider.java create mode 100644 app/src/main/java/com/hamondev/shevery/api/SheveryRemoteProcess.java 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") } }