Implement service helper
This commit is contained in:
parent
542f5dd67d
commit
d56659ffc8
|
|
@ -0,0 +1 @@
|
|||
android.useAndroidX=true
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,4 +19,4 @@ dependencyResolutionManagement {
|
|||
|
||||
rootProject.name = "libxposed"
|
||||
|
||||
include(":interface")
|
||||
include(":interface", ":service")
|
||||
|
|
|
|||
Loading…
Reference in New Issue