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"
|
rootProject.name = "libxposed"
|
||||||
|
|
||||||
include(":interface")
|
include(":interface", ":service")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue