Top Ad unit 728 × 90

Advanced Android Development: Elevate app permissions using Shizuku

There are a lot of reasons the permissions normally granted to your app might not be enough. Maybe you’re like me and enjoy creating hacky apps that abuse the Android API. Some of the APIs I use are locked behind special permissions. Sometimes only the shell user (ADB) or system can access them. There’s a solution though — Shizuku.

Shizuku allows you to call system APIs almost directly, and entirely in Java or Kotlin. This guide will show you how to implement and use Shizuku.

What is Shizuku?

Before we go about using Shizuku, it might be helpful to know what exactly it is. If you’re familiar with Magisk, then Shizuku is similar. But instead of managing root access, it manages shell access.

Shizuku runs its own process with shell-level permissions. How the user activates that process depends on their device, Android version, and choice. Shizuku can be activated through ADB, through on-device wireless ADB (on Android 11 and later), or through root access. Apps implementing Shizuku can then request permission to use that process to perform elevated operations.

Shizuku (Free, Google Play) →

Why Shizuku?

While shell-level access to the system doesn’t let you do as much as root, it still gives you more access than a normal app would. On top of that, the way Shizuku works lets you use the Android APIs almost like normal. You don’t have to rely on shell commands (although you can if you want to).

If your app needs special permissions that can only be granted through ADB (or with root), Shizuku and Android 11 make a great pairing. You can just use Shizuku to grant special permissions fully on-device.

Even with devices that aren’t on Android 11, Shizuku can be useful. The app provides instructions and scripts for users so you don’t have to.

Integration

Adding Shizuku to your app isn’t the simplest, but it’s not hard, either. Unfortunately, the developer documentation isn’t exactly complete, but this article has you covered. Here’s how to integrate Shizuku into your app.

Dependencies

The first step is to add the Shizuku dependencies. In your module-level build.gradle, add the following to the dependencies block.

def shizuku_version = '11.0.3'

implementation "dev.rikka.shizuku:api:$shizuku_version"
implementation "dev.rikka.shizuku:provider:$shizuku_version"

Make sure to update the version if needed. 11.0.3 is the latest at the time of writing.

Provider

In order for Shizuku to work, you need to add a provider block to your app’s manifest. Open AndroidManifest.xml and add the following inside the application block.

<provider
    android:name="rikka.shizuku.ShizukuProvider"
    android:authorities="${applicationId}.shizuku"
    android:multiprocess="false"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />

Permission

For authorization, Shizuku uses a runtime permission. We’ll get into granting that permission in a bit. For now, add it to AndroidManifest.xml inside the manifest block.

Now that all of that is added, the basic integration is done. Let Gradle do a project sync, and continue on to Usage.


Usage

Checking Availability

Before going into how to use Shizuku, let’s talk about making sure it’s actually available to use.

Before checking to see if the permission is granted, and before making API calls through Shizuku, you can make sure those checks and calls will succeed with the following method:

Shizuku.pingBinder()

If Shizuku is installed and running, this will return true. Otherwise, it will return false.

Granting Permission

Since Shizuku uses a runtime permission, it has to be granted to your app before you can do anything with shell access. There are also two API versions in circulation, with different ways of granting it. This section will show you how to handle both.

Checking

Before you request the permission, the best thing to do is to check if you already have it. If you do, you can continue with whatever you need to do. Otherwise, you’ll need to request it before continuing.

To check if you have permission to use Shizuku, you can use the following. This code is assuming you’re running it inside an Activity.

Kotlin:

val isGranted = if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
    checkSelfPermission(ShizukuProvider.PERMISSION) == PackageManager.PERMISSION_GRANTED
} else {
    Shizuku.checkSelfPermission() = PackageManager.PERMISSION_GRANTED
}

Java:

boolean isGranted;
if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
    isGranted = checkSelfPermission(ShizukuProvider.PERMISSION) == PackageManager.PERMISSION_GRANTED;
} else {
    isGranted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED;
}

Requesting

If you need to request permission to use Shizuku, here’s how.

The SHIZUKU_CODE variable used below should be an integer with a steady value (static variable).

Kotlin:

if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
    requestPermissions(arrayOf(ShizukuProvider.PERMISSION), SHIZUKU_CODE)
} else {
    Shizuku.requestPermission(SHIZUKU_CODE)
}

Java:

if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) {
    requestPermissions(new String[] { ShizukuProvider.PERMISSION }, SHIZUKU_CODE);
} else {
    Shizuku.requestPermissions(SHIZUKU_CODE);
}

To listen for the result, you’ll need to override Activity’s onRequestPermissionsResult() method. You’ll also need to implement Shizuku.OnRequestPermissionResultListener. The example I’m going to show assumes your Activity implements that interface, but you can implement it in a variable if you want.

Kotlin:

class ExampleActivity : AppCompatActivity, Shizuku.OnRequestPermissionResultListener {
    ...

    override void onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        permissions.forEachIndexed { index, permission ->
            if (permission == ShizukuProvider.PERMISSION) {
                onRequestPermissionResult(requestCode, grantResults[index])
            }
       }
    }

    override void onRequestPermissionResult(requestCode: Int, grantResult: Int) {
        val isGranted = grantResult == PackageManager.PERMISSION_GRANTED
        //Do stuff based on the result.

    }
}

Java:

public class ExampleActivity extends AppCompatActivity implements Shizuku.OnRequestPermissionResultListener {
    ...

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        for (int i = 0; i < permissions.length; i++) {
            String permission = permissions[i];
            int result = grantResults[i];

            if (permission.equals(ShizukuProvider.PERMISSION) {
                onRequestPermissionResult(requestCode, result);
            }
        }
    }

    @Override
    public void onRequestPermissionResult(int requestCode, int grantResult) {
        boolean isGranted = grantResult == PackageManager.PERMISSION_GRANTED;
        //Do stuff based on the result.
    }
}

Using APIs

Now that Shizuku is set up and permissions are granted, you can start actually calling APIs using Shizuku. The process here is a little different than you might be used to. Instead of calling getSystemService() and casting to something like WindowManager, you’ll have to use the internal APIs for these instead (e.g., IWindowManager).

Shizuku includes a bypass for Android’s hidden API blacklist, so you won’t need to worry about that when using it. If you’re curious how to bypass that yourself though, check out my previous tutorial.

Here’s a quick example (using reflection) showing you how you can get an instance of IPackageManager and use it to grant an app a runtime permission.

Kotlin:

val iPmClass = Class.forName("android.content.pm.IPackageManager")
val iPmStub = Class.forName("android.content.pm.IPackageManager\$Stub")
val asInterfaceMethod = iPmStub.getMethod("asInterface", IBinder::class.java)
val grantRuntimePermissionMethod = iPmClass.getMethod("grantRuntimePermission", String::class.java /* package name */, String::class.java /* permission name */, Int::class.java /* user ID */)

val iPmInstance = asInterfaceMethod.invoke(null, ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")))

grantRuntimePermissionMethod.invoke(iPmInstance, "com.zacharee1.systemuituner", android.Manifest.permission.WRITE_SECURE_SETTINGS, 0)

Java:

Class<?> iPmClass = Class.forName("android.content.pm.IPackageManager");
Class<?> iPmStub = Class.forName("android.content.pm.IPackageManager$Stub");
Method asInterfaceMethod = iPmStub.getMethod("asInterface", IBinder.class);
Method grantRuntimePermissionMethod = iPmClass.getMethod("grantRuntimePermission", String.class, String.class, int.class);

val iPmInstance = asInterfaceMethod.invoke(null, new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")));

grantRuntimePermissionMethod.invoke(iPmInstance, "com.zacharee1.systemuituner", android.Manifest.permission.WRITE_SECURE_SETTINGS, 0);

The process for other APIs is similar. Get the references to the class and its Stub sub-class. Get the reference to the asInterface method for the Stub class. Get the references to the methods you want from the class itself. Then, invoke the asInterface method reference you have, replacing "package" above with whatever service you need. That instance can then be passed for method invocations.

Pro-tip: you can avoid using reflection altogether if you install a modified SDK. Check out anggrayudi’s GitHub repository for the modified SDKs and installation instructions. With this installed, the above code (in Kotlin) becomes a whole lot simpler.

val iPm = IPackageManager.Stub.asInterface(ShizukuBinderWrapper(SystemServiceHelper.getService("package"))))
iPm.grantRuntimePermission("com.zacharee1.systemuituner", android.Manifest.permission.WRITE_SECURE_SETTINGS, 0)

Any AIDL-based APIs can be used with this method from anywhere in your app, as long as Shizuku is running and your app has permission.

User Service

While the Binder method covers most use-cases, there may be times where you need an API that doesn’t have a direct Binder interface. In that case, you can use the User Service feature in Shizuku.

This method works similarly to a normal Service in Android. You “start” it, communicate by binding to it with a ServiceConnection, and run your logic in the service class. The difference is you aren’t using Android’s Service, and anything inside the service runs with ADB permissions.

Now there are some limitations. The User Service runs in a completely separate process and user, so you can’t interact with the rest of your app except through your own AIDL callbacks and Binders. Since it’s also not running in a proper app process, some things like binding Android Services may not work properly.

This also requires Shizuku version 10 or later. While most sources for the app have version 11 currently, you should still include a version check, which will be included in the example.

Defining an AIDL

To get started, you’ll need to create a new AIDL file. You can do this in Android Studio by right-clicking anything in the Android file-view, hovering over the “New” option, and choosing the “AIDL” option. Enter the name of the file (e.g., “IUserService”), and Android Studio will create a template file for you.

If you aren’t familiar with how AIDLs work, be sure to check out Google’s documentation.

Remove the template methods from the AIDL and then add a destroy() method with the proper ID for Shizuku. This will be called when Shizuku is killing the User Service, and should be used to clean up any references or ongoing logic you have.

Example AIDL:

package tk.zwander.shizukudemo;

interface IUserService {
    void destroy() = 16777114;

    void grantPermission(String packageName, String permission, int userId) = 1; //You can specify your own method IDs in the AIDL. Check out the documentation for more details on this.
}

Implementing the AIDL

The next thing to do is actually implement the AIDL.

Java:

public class UserService extends IUserService.Stub {
    //Make sure to include an empty constructor.
    public UserService() {
    }

    @Override
    public void destroy() {
        //Shizuku wants the service to be killed. Clean up and then exit.
        System.exit(0);
    }

    @Override
    public void grantPermission(String packageName, String permission, int userId) {
        //No need to use ShizukuBinderWrapper.
        IPackageManager.Stub.asInterface(SystemServiceHelper.getService("package")).grantRuntimePermission(packageName, permission, userId);
    }
}

Kotlin:

class UserService : IUserService.Stub {
    //Include an empty constructor.
    constructor() {
    }

    override fun destroy() {
        //Shizuku wants the service to be killed. Clean up and exit.
        System.exit(0)
    }

    override fun grantPermission(packageName: String, permission: String, userId: Int) {
        //No need for ShizukuBinderWrapper.
        IPackageManager.Stub.asInterface(SystemServiceHelper.getService("package")).grantRuntimePermission(packageName, permission, userId)
    }
}

The above examples assume you have the Android Hidden API SDK installed.

Setting up the Service Connection

Now that the User Service is defined and implemented, it’s time to get it set up for use. The first thing you should do is define a ServiceConnection where you want to communicate with it (e.g., from the main Activity in your app).

Java:

private IUserService binder = null;

private final ServiceConnection connection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder binder) {
        if (binder != null && binder.pingBinder()) {
            binder = IUserService.Stub.asInterface(binder);
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        binder = null;
    }
}

Kotlin:

private var binder: IUserService? = null

private val connection = object : ServiceConnection {
    override fun onServiceConnected(componentName: ComponentName, binder: IBinder?) {
        if (binder != null && binder.pingBinder()) {
            binder = IUserService.Stub.asInterface(binder)
        }
    }

    override fun onServiceDisconnected(componentName: ComponentName) {
        binder = null;
    }
}

The binder variable is what you’ll be using to communicate with the User Service from your app. To check if it’s available for use, just check that it’s not null and that pingBinder() returns true, just like in the code example above.

Creating the User Service arguments

Before you can control the User Service, you’ll need to define some arguments for Shizuku to use when starting and stopping it. These include things like actually telling Shizuku the class name of the service, specifying the process suffix, whether or not it’s debuggable, and what version it is.

Java:

private final Shizuku.UserServiceArgs serviceArgs = new Shizuku.UserServiceArgs(
    new ComponentName(BuildConfig.APPLICATION_ID, UserService.class.getName()))
        .processNameSuffix("user_service")
        .debuggable(BuildConfig.DEBUG)
        .version(BuildConfig.VERSION_CODE);

Kotlin:

private val serviceArgs = Shizuku.UserServiceArgs(
    ComponentName(BuildConfig.APPLICATION_ID, UserService.class::java.getName()))
        .processNameSuffix("user_service")
        .debuggable(BuildCOnfig.DEBUG)
        .version(BuildConfig.VERSION_CODE)

Starting, Stopping, and Binding the User Service

The start and bind actions and the stop and unbind actions are unified in Shizuku. There aren’t separate start and bind methods or stop and unbind methods.

Here’s how to start and bind the User Service.

Java:

if (Shizuku.getVersion >= 10) {
    //This only works on Shizuku 10 or later.
    Shizuku.bindUserService(serviceArgs, connection);
} else {
    //Tell the user to upgrade Shizuku.
}

Kotlin:

if (Shizuku.getVersion() >= 10) {
    //This only works on Shizuku 10 or later.
    Shizuku.bindUserService(serviceArgs, connection)
} else {
    //Tell the user to upgrade Shizuku.
}

Here’s how to stop and unbind the User Service.

Java:

if  (Shizuku.getVersion >= 10) {
    Shizuku.unbindUserService(serviceArgs, connection, true);
}

Kotlin:

if (Shizuku.getVersion >= 10) {
    Shizuku.unbindUserService(serviceArgs, connection, true)
}

Invoking the User Service

Once the User Service is started, you can start using it. Simply check whether the binder variable is non-null and pingable, and then make your method call.

Java:

if (binder != null && binder.pingBinder()) {
    binder.grantPermission("com.zacharee1.systemuituner", android.Manifest.permission.WRITE_SECURE_SETTINGS, 0);
}

Kotlin:

if (binder?.pingBinder() == true) {
    binder?.grantPermission("com.zacharee1.systemuituner", android.Manifest.permission.WRITE_SECURE_SETTINGS, 0)
}

Conclusion

If you followed through all of that, you should now have a working Shizuku integration. Just remember to tell your users to install Shizuku, and to properly check that Shizuku is available before trying to use it.

The post Advanced Android Development: Elevate app permissions using Shizuku appeared first on xda-developers.



from xda-developers https://ift.tt/3uQa3eZ
via IFTTT
Advanced Android Development: Elevate app permissions using Shizuku Reviewed by site on 07:50 Rating: 5

Aucun commentaire:

All Rights Reserved by xda © 2014 - 2015
Powered By Blogger, Designed by Sweetheme

Formulaire de contact

Nom

E-mail *

Message *

Fourni par Blogger.