Implement service helper

This commit is contained in:
Nullptr 2023-01-11 01:14:07 +08:00
parent 542f5dd67d
commit d56659ffc8
No known key found for this signature in database
8 changed files with 732 additions and 1 deletions

1
gradle.properties Normal file
View File

@ -0,0 +1 @@
android.useAndroidX=true

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

@ -0,0 +1,37 @@
plugins {
id("com.android.library")
}
android {
namespace = "io.github.libxposed.service"
compileSdk = 33
buildToolsVersion = "33.0.1"
defaultConfig {
minSdk = 21
targetSdk = 33
}
buildFeatures {
androidResources = false
buildConfig = false
}
buildTypes {
release {
isMinifyEnabled = true
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(project(":interface"))
compileOnly("androidx.annotation:annotation:1.5.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -0,0 +1,241 @@
package io.github.libxposed.service;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@SuppressWarnings("unchecked")
public final class RemotePreferences implements SharedPreferences {
private static final String TAG = "RemotePreferences";
private static final Object CONTENT = new Object();
private static final Lock LOCK = new ReentrantLock();
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
private final XposedService mService;
private final String mGroup;
private final Map<String, Object> mMap = new ConcurrentHashMap<>();
private final Map<OnSharedPreferenceChangeListener, Object> mListeners = Collections.synchronizedMap(new WeakHashMap<>());
private volatile boolean isDeleted = false;
private RemotePreferences(XposedService service, String group) {
this.mService = service;
this.mGroup = group;
}
@Nullable
static RemotePreferences newInstance(XposedService service, String group) throws RemoteException {
Bundle output = service.getRaw().requestRemotePreferences(group);
if (output == null) return null;
var prefs = new RemotePreferences(service, group);
if (output.containsKey("map")) {
prefs.mMap.putAll((Map<String, Object>) output.getSerializable("map"));
}
return prefs;
}
void setDeleted() {
this.isDeleted = true;
}
@Override
public Map<String, ?> getAll() {
return new TreeMap<>(mMap);
}
@Nullable
@Override
public String getString(String key, @Nullable String defValue) {
var v = (String) mMap.getOrDefault(key, defValue);
if (v != null) return v;
return defValue;
}
@Nullable
@Override
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
var v = (Set<String>) mMap.getOrDefault(key, defValues);
if (v != null) return v;
return defValues;
}
@Override
public int getInt(String key, int defValue) {
var v = (Integer) mMap.getOrDefault(key, defValue);
if (v != null) return v;
return defValue;
}
@Override
public long getLong(String key, long defValue) {
var v = (Long) mMap.getOrDefault(key, defValue);
if (v != null) return v;
return defValue;
}
@Override
public float getFloat(String key, float defValue) {
var v = (Float) mMap.getOrDefault(key, defValue);
if (v != null) return v;
return defValue;
}
@Override
public boolean getBoolean(String key, boolean defValue) {
var v = (Boolean) mMap.getOrDefault(key, defValue);
if (v != null) return v;
return defValue;
}
@Override
public boolean contains(String key) {
return mMap.containsKey(key);
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
mListeners.put(listener, CONTENT);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
mListeners.remove(listener);
}
@Override
public Editor edit() {
return new Editor();
}
public class Editor implements SharedPreferences.Editor {
private final HashSet<String> mDelete = new HashSet<>();
private final HashMap<String, Object> mPut = new HashMap<>();
private void put(String key, @NonNull Object value) {
mDelete.remove(key);
mPut.put(key, value);
}
@Override
public SharedPreferences.Editor putString(String key, @Nullable String value) {
if (value == null) remove(key);
else put(key, value);
return this;
}
@Override
public SharedPreferences.Editor putStringSet(String key, @Nullable Set<String> values) {
if (values != null) values.forEach(v -> putString(key, v));
return this;
}
@Override
public SharedPreferences.Editor putInt(String key, int value) {
put(key, value);
return this;
}
@Override
public SharedPreferences.Editor putLong(String key, long value) {
put(key, value);
return this;
}
@Override
public SharedPreferences.Editor putFloat(String key, float value) {
put(key, value);
return this;
}
@Override
public SharedPreferences.Editor putBoolean(String key, boolean value) {
put(key, value);
return this;
}
@Override
public SharedPreferences.Editor remove(String key) {
mDelete.add(key);
mPut.remove(key);
return this;
}
@Override
public SharedPreferences.Editor clear() {
mDelete.clear();
mPut.clear();
return this;
}
private void doUpdate(boolean throwing) {
mService.deletionLock.readLock().lock();
try {
if (isDeleted) {
throw new IllegalStateException("This preferences group has been deleted");
}
mDelete.forEach(mMap::remove);
mMap.putAll(mPut);
List<String> changes = new ArrayList<>(mDelete.size() + mMap.size());
changes.addAll(mDelete);
changes.addAll(mMap.keySet());
synchronized (mListeners) {
for (var key : changes) {
mListeners.keySet().forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key));
}
}
var bundle = new Bundle();
bundle.putSerializable("delete", mDelete);
bundle.putSerializable("put", mPut);
try {
mService.getRaw().updateRemotePreferences(mGroup, bundle);
} catch (RemoteException e) {
if (throwing) {
throw new RuntimeException(e);
} else {
Log.e(TAG, "Failed to update remote preferences", e);
}
}
} finally {
mService.deletionLock.readLock().unlock();
}
}
@Override
public boolean commit() {
if (!LOCK.tryLock()) return false;
try {
doUpdate(true);
return true;
} finally {
LOCK.unlock();
}
}
@Override
public void apply() {
HANDLER.post(() -> doUpdate(false));
}
}
}

View File

@ -0,0 +1,64 @@
package io.github.libxposed.service;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class XposedProvider extends ContentProvider {
private static final String TAG = "XposedProvider";
@Override
public boolean onCreate() {
return false;
}
@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;
}
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
if (method.equals(IXposedService.SEND_BINDER) && extras != null) {
IBinder binder = extras.getBinder("binder");
if (binder != null) {
Log.d(TAG, "binder received: " + binder);
XposedServiceHelper.onBinderReceived(binder);
}
return new Bundle();
}
return null;
}
}

View File

@ -0,0 +1,314 @@
package io.github.libxposed.service;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@SuppressWarnings("unused")
public final class XposedService {
public final static class ServiceException extends RuntimeException {
private ServiceException(RemoteException e) {
super("Xposed service error", e);
}
}
public enum Privilege {
/**
* Unknown privilege value
*/
FRAMEWORK_PRIVILEGE_UNKNOWN,
/**
* The framework is running as root
*/
FRAMEWORK_PRIVILEGE_ROOT,
/**
* The framework is running in a container with a fake system_server
*/
FRAMEWORK_PRIVILEGE_CONTAINER,
/**
* The framework is running as a different app, which may have at most shell permission
*/
FRAMEWORK_PRIVILEGE_APP,
/**
* The framework is embedded in the hooked app, which means {@link #getRemotePreferences} and remote file streams will be null
*/
FRAMEWORK_PRIVILEGE_EMBEDDED
}
private final IXposedService mService;
private final Map<String, RemotePreferences> mRemotePrefs = new HashMap<>();
final ReentrantReadWriteLock deletionLock = new ReentrantReadWriteLock();
XposedService(IXposedService service) {
mService = service;
}
IXposedService getRaw() {
return mService;
}
/**
* Get the Xposed API version of current implementation
*
* @return API version
* @throws ServiceException If the service is dead or error occurred
*/
public int getAPIVersion() {
try {
return mService.getAPIVersion();
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Get the Xposed framework name of current implementation
*
* @return Framework name
* @throws ServiceException If the service is dead or error occurred
*/
@NonNull
public String getFrameworkName() {
try {
return mService.getFrameworkName();
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Get the Xposed framework version of current implementation
*
* @return Framework version
* @throws ServiceException If the service is dead or error occurred
*/
@NonNull
public String getFrameworkVersion() {
try {
return mService.getFrameworkVersion();
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Get the Xposed framework version code of current implementation
*
* @return Framework version code
* @throws ServiceException If the service is dead or error occurred
*/
public long getFrameworkVersionCode() {
try {
return mService.getFrameworkVersionCode();
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Get the Xposed framework privilege of current implementation
*
* @return Framework privilege
* @throws ServiceException If the service is dead or error occurred
*/
@NonNull
public Privilege getFrameworkPrivilege() {
try {
int value = mService.getFrameworkPrivilege();
return (value >= 0 && value <= 3) ? Privilege.values()[value + 1] : Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN;
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Additional methods provided by specific Xposed framework
*
* @param name Featured method name
* @param args Featured method arguments
* @return Featured method result
* @throws UnsupportedOperationException If the framework does not provide a method with given name
* @throws ServiceException If the service is dead or error occurred
* @deprecated Normally, modules should never rely on implementation details about the Xposed framework,
* but if really necessary, this method can be used to acquire such information
*/
@Deprecated
@Nullable
public Bundle featuredMethod(@NonNull String name, @Nullable Bundle args) throws UnsupportedOperationException {
try {
return mService.featuredMethod(name, args);
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Get the application scope of current module
*
* @return Module scope
* @throws ServiceException If the service is dead or error occurred
*/
@NonNull
public List<String> getScope() {
try {
return mService.getScope();
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Request to add a new app to the module scope
*
* @param packageName Package name of the app to be added
* @param callback Callback to be invoked when the request is completed or error occurred
* @throws ServiceException If the service is dead or error occurred
*/
public void requestScope(@NonNull String packageName, @NonNull IXposedScopeCallback callback) {
try {
mService.requestScope(packageName, callback);
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Remove an app from the module scope
*
* @param packageName Package name of the app to be added
* @return null if successful, or non-null with error message
* @throws ServiceException If the service is dead or error occurred
*/
@Nullable
public String removeScope(@NonNull String packageName) {
try {
return mService.removeScope(packageName);
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Get remote preferences from Xposed framework
*
* @param group Group name
* @return The preferences, null if the framework is embedded
* @throws ServiceException If the service is dead or error occurred
*/
@Nullable
public SharedPreferences getRemotePreferences(@NonNull String group) {
return mRemotePrefs.computeIfAbsent(group, k -> {
try {
return RemotePreferences.newInstance(this, k);
} catch (RemoteException e) {
throw new ServiceException(e);
}
});
}
/**
* Delete a group of remote preferences
*
* @param group Group name
* @throws ServiceException If the service is dead or error occurred
*/
public void deleteRemotePreferences(@NonNull String group) {
deletionLock.writeLock().lock();
try {
mService.deleteRemotePreferences(group);
mRemotePrefs.computeIfPresent(group, (k, v) -> {
v.setDeleted();
return null;
});
} catch (RemoteException e) {
throw new ServiceException(e);
} finally {
deletionLock.writeLock().unlock();
}
}
/**
* Open an InputStream to read a file from the module's shared data directory
*
* @param name File name
* @return The InputStream, null if the framework is embedded
* @throws ServiceException If the service is dead or error occurred
*/
@Nullable
public FileInputStream openRemoteFileInput(@NonNull String name) {
try {
var file = mService.openRemoteFile(name, MODE_READ_ONLY);
if (file == null) return null;
return new FileInputStream(file.getFileDescriptor());
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Open an OutputStream to write a file to the module's shared data directory
*
* @param name File name
* @param mode Operating mode
* @return The OutputStream, null if the framework is embedded
* @throws ServiceException If the service is dead or error occurred
*/
@Nullable
public FileOutputStream openRemoteFileOutput(@NonNull String name, int mode) {
try {
var file = mService.openRemoteFile(name, mode);
if (file == null) return null;
return new FileOutputStream(file.getFileDescriptor());
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* Delete a file in the module's shared data directory
*
* @param name File name
* @return true if successful, false if failed or the framework is embedded
* @throws ServiceException If the service is dead or error occurred
*/
public boolean deleteRemoteFile(@NonNull String name) {
try {
return mService.deleteRemoteFile(name);
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
/**
* List all files in the module's shared data directory
*
* @return The file list, null if the framework is embedded
* @throws ServiceException If the service is dead or error occurred
*/
@Nullable
public String[] listRemoteFiles() {
try {
return mService.listRemoteFiles();
} catch (RemoteException e) {
throw new ServiceException(e);
}
}
}

View File

@ -0,0 +1,72 @@
package io.github.libxposed.service;
import android.os.IBinder;
import android.util.Log;
import java.util.HashSet;
import java.util.Set;
@SuppressWarnings("unused")
public final class XposedServiceHelper {
public interface ServiceListener {
/**
* Callback when the service is connected<br/>
* This method could be called multiple times if multiple Xposed frameworks exist
*
* @param service Service instance
*/
void onServiceBind(XposedService service);
/**
* Callback when the service is dead
*/
void onServiceDied(XposedService service);
}
private static final String TAG = "XposedServiceHelper";
private static final Set<XposedService> mCache = new HashSet<>();
private static ServiceListener mListener = null;
static void onBinderReceived(IBinder binder) {
if (binder == null) return;
synchronized (mCache) {
try {
var service = new XposedService(IXposedService.Stub.asInterface(binder));
if (mListener == null) {
mCache.add(service);
} else {
binder.linkToDeath(() -> mListener.onServiceDied(service), 0);
mListener.onServiceBind(service);
}
} catch (Throwable t) {
Log.e(TAG, "onBinderReceived", t);
}
}
}
/**
* Register a ServiceListener to receive service binders from Xposed frameworks<br/>
* This method should only be called once
*
* @param listener Listener to register
*/
public static void registerListener(ServiceListener listener) {
synchronized (mCache) {
mListener = listener;
if (!mCache.isEmpty()) {
for (var it = mCache.iterator(); it.hasNext(); ) {
try {
var service = it.next();
service.getRaw().asBinder().linkToDeath(() -> mListener.onServiceDied(service), 0);
mListener.onServiceBind(service);
} catch (Throwable t) {
Log.e(TAG, "registerListener", t);
it.remove();
}
}
mCache.clear();
}
}
}
}

View File

@ -19,4 +19,4 @@ dependencyResolutionManagement {
rootProject.name = "libxposed"
include(":interface")
include(":interface", ":service")