Skip to content

Commit

Permalink
[AppInfo] Add option to enable/disable sensors
Browse files Browse the repository at this point in the history
Location: App Details page > App Info tab > 3-dots menu > Sensors
Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
  • Loading branch information
MuntashirAkon committed Jul 7, 2024
1 parent 544d927 commit ab73dfc
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 0 deletions.
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
<uses-permission
android:name="android.permission.MANAGE_USERS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.MANAGE_SENSORS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public static final class permission {
public static final String MANAGE_NETWORK_POLICY = "android.permission.MANAGE_NETWORK_POLICY";
@RequiresApi(Build.VERSION_CODES.S)
public static final String MANAGE_NOTIFICATION_LISTENERS = "android.permission.MANAGE_NOTIFICATION_LISTENERS";
@RequiresApi(Build.VERSION_CODES.P)
public static final String MANAGE_SENSORS = "android.permission.MANAGE_SENSORS";
public static final String MANAGE_USERS = "android.permission.MANAGE_USERS";
public static final String READ_PRIVILEGED_PHONE_STATE = "android.permission.READ_PRIVILEGED_PHONE_STATE";
public static final String REAL_GET_TASKS = "android.permission.REAL_GET_TASKS";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package io.github.muntashirakon.AppManager.compat;

import android.annotation.UserIdInt;
import android.os.Build;
import android.os.IBinder;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;

import java.io.IOException;

import io.github.muntashirakon.AppManager.ipc.ProxyBinder;
import io.github.muntashirakon.AppManager.utils.BinderShellExecutor;

@RequiresApi(Build.VERSION_CODES.P)
public final class SensorServiceCompat {
@RequiresPermission(ManifestCompat.permission.MANAGE_SENSORS)
public static boolean isSensorEnabled(@NonNull String packageName, @UserIdInt int userId) {
String[] command;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
command = new String[]{"get-uid-state", packageName, "--user", String.valueOf(userId)};
} else command = new String[]{"get-uid-state", packageName};
try {
BinderShellExecutor.ShellResult result = BinderShellExecutor.execute(getSensorService(), command);
return result.getResultCode() == 0 && "active".contains(result.getStdout());
} catch (IOException e) {
e.printStackTrace();
}
return true;
}

@RequiresPermission(ManifestCompat.permission.MANAGE_SENSORS)
public static void enableSensor(@NonNull String packageName, @UserIdInt int userId, boolean enable) throws IOException {
String state = enable ? "active" : "idle";
String[] command;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
command = new String[]{"set-uid-state", packageName, state, "--user", String.valueOf(userId)};
} else command = new String[]{"set-uid-state", packageName, state};
BinderShellExecutor.ShellResult result = BinderShellExecutor.execute(getSensorService(), command);
if (result.getResultCode() != 0) {
throw new IOException("Could not " + (enable ? "enable" : "disable") + " sensor.");
}
}

@RequiresPermission(ManifestCompat.permission.MANAGE_SENSORS)
public static void resetSensor(@NonNull String packageName, @UserIdInt int userId) throws IOException {
String[] command;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
command = new String[]{"reset-uid-state", packageName, "--user", String.valueOf(userId)};
} else command = new String[]{"reset-uid-state", packageName};
BinderShellExecutor.ShellResult result = BinderShellExecutor.execute(getSensorService(), command);
if (result.getResultCode() != 0) {
throw new IOException("Could not reset sensor.");
}
}

@NonNull
private static IBinder getSensorService() {
return ProxyBinder.getService("sensorservice");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
import io.github.muntashirakon.AppManager.compat.ManifestCompat;
import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat;
import io.github.muntashirakon.AppManager.compat.PackageManagerCompat;
import io.github.muntashirakon.AppManager.compat.SensorServiceCompat;
import io.github.muntashirakon.AppManager.debloat.BloatwareDetailsDialog;
import io.github.muntashirakon.AppManager.details.AppDetailsActivity;
import io.github.muntashirakon.AppManager.details.AppDetailsFragment;
Expand Down Expand Up @@ -373,6 +374,11 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) {
if (batteryOptMenu != null) {
batteryOptMenu.setVisible(SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.DEVICE_POWER));
}
MenuItem sensorsMenu = menu.findItem(R.id.action_sensor);
if (sensorsMenu != null) {
sensorsMenu.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_SENSORS));
}
MenuItem netPolicyMenu = menu.findItem(R.id.action_net_policy);
if (netPolicyMenu != null) {
netPolicyMenu.setVisible(SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_NETWORK_POLICY));
Expand Down Expand Up @@ -487,6 +493,43 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
} else {
Log.e(TAG, "No DUMP permission.");
}
} else if (itemId == R.id.action_sensor) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.MANAGE_SENSORS)) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.sensors)
.setMessage(R.string.choose_what_to_do)
.setPositiveButton(R.string.enable, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> {
try {
SensorServiceCompat.enableSensor(mPackageName, mUserId, true);
ThreadUtils.postOnMainThread(() -> {
UIUtils.displayShortToast(R.string.done);
refreshDetails();
});
} catch (IOException e) {
ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(
getString(R.string.failed)
+ LangUtils.getSeparatorString()
+ e.getMessage()));
}
}))
.setNegativeButton(R.string.disable, (dialog, which) -> ThreadUtils.postOnBackgroundThread(() -> {
try {
SensorServiceCompat.enableSensor(mPackageName, mUserId, false);
ThreadUtils.postOnMainThread(() -> {
UIUtils.displayShortToast(R.string.done);
refreshDetails();
});
} catch (IOException e) {
ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(
getString(R.string.failed)
+ LangUtils.getSeparatorString()
+ e.getMessage()));
}
}))
.show();
} else {
Log.e(TAG, "No sensor permission.");
}
} else if (itemId == R.id.action_net_policy) {
if (!UserHandleHidden.isApp(mApplicationInfo.uid)) {
UIUtils.displayLongToast(R.string.netpolicy_cannot_be_modified_for_core_apps);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public static void shellCommand(@NonNull IBinder binder,
@NonNull String[] args, @Nullable ShellCallback callback,
@NonNull ResultReceiver resultReceiver) throws RemoteException {
if (!(binder instanceof ProxyBinder)) {
BinderCompat.shellCommand(binder, in, out, err, args, callback, resultReceiver);
return;
}
ProxyBinder proxyBinder = (ProxyBinder) binder;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package io.github.muntashirakon.AppManager.utils;

import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ResultReceiver;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

import io.github.muntashirakon.AppManager.ipc.ProxyBinder;
import io.github.muntashirakon.io.Path;

@RequiresApi(Build.VERSION_CODES.N)
public class BinderShellExecutor {
public static class ShellResult {
private int resultCode;
private String stdout;
private String stderr;

private ShellResult() {
}

public int getResultCode() {
return resultCode;
}

public String getStdout() {
return stdout;
}

public String getStderr() {
return stderr;
}
}

public static ShellResult execute(@NonNull IBinder binder, @NonNull String[] command,
@Nullable File input) throws IOException {
if (input == null) {
return execute(binder, command);
}
try (FileInputStream out = new FileInputStream(input)) {
return execute(binder, command, out);
}
}

public static ShellResult execute(@NonNull IBinder binder, @NonNull String[] command,
@Nullable Path input) throws IOException {
if (input == null) {
return execute(binder, command);
}
try (InputStream os = input.openInputStream()) {
return execute(binder, command, os);
}
}

public static ShellResult execute(@NonNull IBinder binder, @NonNull String[] command,
@Nullable FileInputStream input) throws IOException {
if (input == null) {
return execute(binder, command);
}
return execute(binder, command, input.getFD());
}

public static ShellResult execute(@NonNull IBinder binder, @NonNull String[] command, @Nullable InputStream input) throws IOException {
if (input == null) {
return execute(binder, command);
}
ParcelFileDescriptor inputFd = ParcelFileDescriptorUtil.pipeFrom(input);
return execute(binder, command, inputFd.getFileDescriptor());
}

public static ShellResult execute(@NonNull IBinder binder, @NonNull String[] command,
@Nullable FileDescriptor input) throws IOException {
if (input == null) {
return execute(binder, command);
}
return executeInternal(binder, command, input);
}

public static ShellResult execute(@NonNull IBinder binder, @NonNull String[] command)
throws IOException {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
return executeInternal(binder, command, readSide.getFileDescriptor());
}

private static ShellResult executeInternal(@NonNull IBinder binder, @NonNull String[] command,
@NonNull FileDescriptor in) throws IOException {
try (ByteArrayOutputStream stdoutStream = new ByteArrayOutputStream();
ByteArrayOutputStream stderrStream = new ByteArrayOutputStream()) {
ParcelFileDescriptor stdoutFd = ParcelFileDescriptorUtil.pipeTo(stdoutStream);
ParcelFileDescriptor stderrFd = ParcelFileDescriptorUtil.pipeTo(stderrStream);
AtomicInteger atomicResultCode = new AtomicInteger(-1);
CountDownLatch sem = new CountDownLatch(1);
ProxyBinder.shellCommand(binder, in, stdoutFd.getFileDescriptor(), stderrFd.getFileDescriptor(), command, null, new ResultReceiver(ThreadUtils.getUiThreadHandler()) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
atomicResultCode.set(resultCode);
sem.countDown();
}
});
try {
sem.await();
} catch (InterruptedException ignore) {
}
int resultCode = atomicResultCode.get();
if (resultCode == -1) {
throw new IOException("Invalid result code " + resultCode);
}
ShellResult result = new ShellResult();
result.resultCode = resultCode;
result.stdout = stdoutStream.toString();
result.stderr = stderrStream.toString();
return result;
} catch (RemoteException e) {
return ExUtils.rethrowFromSystemServer(e);
} catch (Throwable th) {
return ExUtils.rethrowAsIOException(th);
}
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/menu/fragment_app_info_actions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
android:title="@string/battery_optimization"
app:showAsAction="never" />

<item
android:id="@+id/action_sensor"
android:icon="@drawable/ic_pulse"
android:title="@string/sensors"
app:showAsAction="never" />

<item
android:id="@+id/action_net_policy"
android:icon="@drawable/ic_security_network"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1486,4 +1486,5 @@
<string name="mode_of_op_custom_command_title">Custom Command</string>
<string name="mode_of_op_custom_command">If you are unable to use any of the modes, you can run the following command in any supported shell to run App Manager in privileged mode:</string>
<string name="mode_of_op_alternative_custom_command">If you get a “permission denied” error with the above command, run the following command instead:</string>
<string name="sensors">Sensors</string>
</resources>

0 comments on commit ab73dfc

Please sign in to comment.