aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuutaW <17158086+trumeet@users.noreply.github.com>2019-04-06 09:32:14 -0700
committerYuutaW <17158086+Trumeet@users.noreply.github.com>2019-04-06 09:32:14 -0700
commit3b747bb655aacd0c1d66d6628379c3789f22a7c9 (patch)
treeab76430b95cbaea162a15369aa01156188dd5765
parentf2b6da67d7f564de171d837cfaaa7c5d5562425b (diff)
downloadSysUIController-3b747bb655aacd0c1d66d6628379c3789f22a7c9.tar
SysUIController-3b747bb655aacd0c1d66d6628379c3789f22a7c9.tar.gz
SysUIController-3b747bb655aacd0c1d66d6628379c3789f22a7c9.tar.bz2
SysUIController-3b747bb655aacd0c1d66d6628379c3789f22a7c9.zip
feat(app): support Shizuku mode21
Signed-off-by: YuutaW <17158086+Trumeet@users.noreply.github.com>
-rw-r--r--app/build.gradle7
-rw-r--r--app/src/main/AndroidManifest.xml12
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/Main.java44
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/MainFragment.java287
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/auto_start/AutoStartWorker.java2
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/core/Controller.java82
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/core/ControllerFactory.java66
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/core/CoreUtils.java55
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/core/IController.java44
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/core/root/ControllerService.java (renamed from app/src/main/java/moe/yuuta/sysuicontroller/core/ControllerService.java)10
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/core/root/RootController.java192
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/core/shizuku/IStatusBarServiceShizuku.java272
-rw-r--r--app/src/main/java/moe/yuuta/sysuicontroller/core/shizuku/ShizukuController.java244
-rw-r--r--app/src/main/res/menu/menu_main.xml24
-rw-r--r--app/src/main/res/values/strings.xml11
15 files changed, 1190 insertions, 162 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 529328d..7d44c1b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -51,6 +51,12 @@ android {
}
}
+repositories {
+ maven {
+ url 'https://dl.bintray.com/rikkaw/Shizuku'
+ }
+}
+
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
@@ -72,4 +78,5 @@ dependencies {
implementation "android.arch.work:work-runtime:1.0.0"
implementation 'com.google.android.gms:play-services-oss-licenses:16.0.2'
implementation 'com.google.firebase:firebase-perf:16.2.4'
+ implementation 'moe.shizuku.privilege:api:3.0.0-alpha6'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 08bcb9b..51c560b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,11 @@
<permission android:name="${applicationId}.SERVICE"
android:protectionLevel="signature|privileged" />
<uses-permission android:name="${applicationId}.SERVICE" />
+ <uses-permission
+ android:name="moe.shizuku.manager.permission.API"
+ android:maxSdkVersion="23" />
+ <uses-permission-sdk-23 android:name="moe.shizuku.manager.permission.API_V23" />
+ <uses-permission-sdk-23 android:name="moe.shizuku.manager.permission.EXEC_COMMAND" />
<application
android:name=".Main"
android:allowBackup="true"
@@ -30,6 +35,13 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
+ <provider
+ android:name="moe.shizuku.api.BinderReceiveProvider"
+ android:authorities="${applicationId}.shizuku"
+ android:multiprocess="false"
+ android:enabled="true"
+ android:exported="true"
+ android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
</application>
</manifest> \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/Main.java b/app/src/main/java/moe/yuuta/sysuicontroller/Main.java
index 5da172a..628b280 100644
--- a/app/src/main/java/moe/yuuta/sysuicontroller/Main.java
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/Main.java
@@ -1,17 +1,61 @@
package moe.yuuta.sysuicontroller;
import android.app.Application;
+import android.content.Context;
+import android.os.Build;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.perf.FirebasePerformance;
+import moe.shizuku.api.ShizukuClientHelper;
+import moe.shizuku.api.ShizukuService;
+
public class Main extends Application {
public static final String GLOBAL_TAG = "UIC";
+ private static boolean v3Failed;
+ private static boolean v3TokenValid;
+
+ // From the sample of Shizuku
+ public static boolean isShizukuV3Failed() {
+ return v3Failed;
+ }
+
+ public static boolean isShizukuV3TokenValid() {
+ return v3TokenValid;
+ }
+
+ public static void setV3TokenValid(boolean v3TokenValid) {
+ Main.v3TokenValid = v3TokenValid;
+ }
+
@Override
public void onCreate() {
super.onCreate();
FirebasePerformance.getInstance().setPerformanceCollectionEnabled(!BuildConfig.DEBUG);
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(!BuildConfig.DEBUG);
}
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ ShizukuClientHelper.setBinderReceivedListener(() -> {
+ if (ShizukuService.getBinder() == null) {
+ v3Failed = true;
+ return;
+ } else {
+ try {
+ // test the binder first
+ ShizukuService.pingBinder();
+
+ if (Build.VERSION.SDK_INT < 23) {
+ String token = ShizukuClientHelper.loadPre23Token(base);
+ v3TokenValid = ShizukuService.setCurrentProcessTokenPre23(token);
+ }
+ } catch (Throwable tr) {
+ return;
+ }
+ }
+ });
+ }
}
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/MainFragment.java b/app/src/main/java/moe/yuuta/sysuicontroller/MainFragment.java
index ff43d34..a42809a 100644
--- a/app/src/main/java/moe/yuuta/sysuicontroller/MainFragment.java
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/MainFragment.java
@@ -1,5 +1,7 @@
package moe.yuuta.sysuicontroller;
+import android.annotation.SuppressLint;
+import android.app.StatusBarManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@@ -14,7 +16,10 @@ import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity;
import com.google.firebase.perf.FirebasePerformance;
@@ -26,64 +31,44 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import eu.chainfire.librootjava.RootIPCReceiver;
-import eu.chainfire.librootjavadaemon.RootDaemon;
-import eu.chainfire.libsuperuser.Shell;
import moe.shizuku.preference.Preference;
import moe.shizuku.preference.PreferenceCategory;
import moe.shizuku.preference.PreferenceFragment;
import moe.shizuku.preference.PreferenceGroup;
import moe.shizuku.preference.SwitchPreference;
import moe.yuuta.sysuicontroller.about.VersionDialogFragment;
-import moe.yuuta.sysuicontroller.core.ControllerService;
+import moe.yuuta.sysuicontroller.core.Controller;
+import moe.yuuta.sysuicontroller.core.ControllerFactory;
+import moe.yuuta.sysuicontroller.core.CoreUtils;
import moe.yuuta.sysuicontroller.core.DisableItem;
+import moe.yuuta.sysuicontroller.core.IController;
+import moe.yuuta.sysuicontroller.core.shizuku.ShizukuController;
import static moe.yuuta.sysuicontroller.Main.GLOBAL_TAG;
-public class MainFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Shell.OnCommandResultListener {
+public class MainFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, IController.Callback {
private static final String TAG = GLOBAL_TAG + ".MainFragment";
- private static final String ARG_SERVICE = MainFragment.class.getName() + ".ARG_SERVICE";
- private static final int CODE_START = 1;
-
- private Shell.Interactive mShell;
- private volatile IStatusController mService;
-
private Preference mStatusPreference;
private SparseArray<SwitchPreference> mDisableMap;
private SparseArray<SwitchPreference> mDisable2Map;
private List<PreferenceGroup> mGroupsShouldBeDisabledBeforeServer = new ArrayList<>(3);
-
- private RootIPCReceiver<IStatusController> mReceiver = new RootIPCReceiver<IStatusController>(null, ControllerService.CODE_SERVICE) {
- @Override
- public void onConnect(IStatusController ipc) {
- Log.d(TAG, "Connected to remote service");
- mService = ipc;
- postStatusUpdate();
- }
-
- @Override
- public void onDisconnect(IStatusController ipc) {
- Log.d(TAG, "Disconnected to remote service");
- mService = ipc;
- postStatusUpdate();
- }
- };
+ private Controller mController;
+ // When it's true, the activity will be automatically recreated after the next successful status update.
+ private volatile boolean mIsSwitchModeScheduled = false;
private void serializeDisableFlags () {
- if (obtainService() == null) return;
+ if (!mController.isServiceReady()) return;
SharedPreferences.Editor editor = requireContext().getSharedPreferences("flags", Context.MODE_PRIVATE)
.edit();
try {
- int disableFlags = mService.getDisableFlags();
+ int disableFlags = mController.getDisableFlags();
editor.putInt("disable_flags", disableFlags);
} catch (RemoteException e) {
Log.e(TAG, "Receive disable flags", e);
}
try {
- int disable2Flags = mService.getDisable2Flags();
+ int disable2Flags = mController.getDisable2Flags();
editor.putInt("disable2_flags", disable2Flags);
} catch (RemoteException e) {
Log.e(TAG, "Receive disable2 flags", e);
@@ -109,8 +94,10 @@ public class MainFragment extends PreferenceFragment implements Preference.OnPre
private void postStatusUpdate () {
new Handler(Looper.getMainLooper()).post(() -> {
Log.d(TAG, "Start update status");
- obtainService();
- boolean enable = mService != null;
+ boolean enable = mController.isServiceReady();
+ if (enable && !mController.canStop()) {
+ mStatusPreference.setVisible(false);
+ }
for (PreferenceGroup group : mGroupsShouldBeDisabledBeforeServer) {
group.setEnabled(enable);
}
@@ -124,54 +111,50 @@ public class MainFragment extends PreferenceFragment implements Preference.OnPre
mDisableMap.clear();
traceClear.stop();
if (enable) {
- try {
- List<DisableItem> available = mService.getAvailableDisableItems();
- Log.d(TAG, "Available disable items: " + available.toString());
- Trace traceDisplayAvailableItems = FirebasePerformance.getInstance().newTrace("traceDisplayAvailableItems");
- traceDisplayAvailableItems.start();
- traceDisplayAvailableItems.putMetric("available_item_size", available.size());
- Set<String /* Name (key) */> showedKeys = new HashSet<>(available.size());
- for (DisableItem item : available) {
- showedKeys.add(item.getKey().toLowerCase());
- SwitchPreference preference = (SwitchPreference) findPreference(item.getKey().toLowerCase());
- if (preference == null) {
- // Create a new one for special keys
- preference = new SwitchPreference(requireContext(), null, moe.shizuku.preference.R.attr.switchPreferenceStyle,
- R.style.Preference_SwitchPreference);
- preference.setKey(item.getKey().toLowerCase());
- preference.setTitle(item.getKey());
- preference.setSummary(R.string.additional_key);
- ((PreferenceCategory) findPreference("key_other")).addPreference(preference);
- }
- preference.setOnPreferenceChangeListener((p, newValue) -> runDisable(item.getFlag(), (Boolean) newValue, item.getKey(), item.isDisable2()));
- if (item.isDisable2()) mDisable2Map.put(item.getFlag(), preference);
- mDisableMap.put(item.getFlag(), preference);
+ List<DisableItem> available = CoreUtils.getAvailableDisableItems();
+ Log.d(TAG, "Available disable items: " + available.toString());
+ Trace traceDisplayAvailableItems = FirebasePerformance.getInstance().newTrace("traceDisplayAvailableItems");
+ traceDisplayAvailableItems.start();
+ traceDisplayAvailableItems.putMetric("available_item_size", available.size());
+ Set<String /* Name (key) */> showedKeys = new HashSet<>(available.size());
+ for (DisableItem item : available) {
+ showedKeys.add(item.getKey().toLowerCase());
+ SwitchPreference preference = (SwitchPreference) findPreference(item.getKey().toLowerCase());
+ if (preference == null) {
+ // Create a new one for special keys
+ preference = new SwitchPreference(requireContext(), null, moe.shizuku.preference.R.attr.switchPreferenceStyle,
+ R.style.Preference_SwitchPreference);
+ preference.setKey(item.getKey().toLowerCase());
+ preference.setTitle(item.getKey());
+ preference.setSummary(R.string.additional_key);
+ ((PreferenceCategory) findPreference("key_other")).addPreference(preference);
}
- traceDisplayAvailableItems.stop();
-
- // Disable not supported prebuilt preferences
- Trace traceSetNotAvailableBatch = FirebasePerformance.getInstance().newTrace("traceSetNotAvailableBatch");
- traceSetNotAvailableBatch.start();
- traceSetNotAvailableBatch.putMetric("showed_keys", showedKeys.size());
- Log.d(TAG, "Showed items: " + showedKeys.toString());
- setNotAvailableBatch(showedKeys, getPreferenceScreen());
- traceSetNotAvailableBatch.stop();
- } catch (Exception e) {
- Log.e(TAG, "Read available disable items", e);
+ preference.setOnPreferenceChangeListener((p, newValue) -> runDisable(item.getFlag(), (Boolean) newValue, item.getKey(), item.isDisable2()));
+ if (item.isDisable2()) mDisable2Map.put(item.getFlag(), preference);
+ mDisableMap.put(item.getFlag(), preference);
}
+ traceDisplayAvailableItems.stop();
+
+ // Disable not supported prebuilt preferences
+ Trace traceSetNotAvailableBatch = FirebasePerformance.getInstance().newTrace("traceSetNotAvailableBatch");
+ traceSetNotAvailableBatch.start();
+ traceSetNotAvailableBatch.putMetric("showed_keys", showedKeys.size());
+ Log.d(TAG, "Showed items: " + showedKeys.toString());
+ setNotAvailableBatch(showedKeys, getPreferenceScreen());
+ traceSetNotAvailableBatch.stop();
SharedPreferences preferences = requireContext().getSharedPreferences("flags", Context.MODE_PRIVATE);
Trace traceUpdateUIDisabled = FirebasePerformance.getInstance().newTrace("traceUpdateUIDisabled");
traceUpdateUIDisabled.start();
try {
- mService.disable(preferences.getInt("disable_flags", mService.getDisableNoneFlag(false)));
- updateUIDisabled(false, mService.getDisableFlags());
+ mController.disable(preferences.getInt("disable_flags", StatusBarManager.DISABLE_NONE));
+ updateUIDisabled(false, mController.getDisableFlags());
} catch (RemoteException e) {
Log.e(TAG, "Receive disable flags", e);
}
try {
- mService.disable2(preferences.getInt("disable2_flags", mService.getDisableNoneFlag(true)));
- updateUIDisabled(true, mService.getDisableFlags());
+ mController.disable2(preferences.getInt("disable2_flags", StatusBarManager.DISABLE2_NONE));
+ updateUIDisabled(true, mController.getDisableFlags());
} catch (RemoteException e) {
Log.e(TAG, "Receive disable2 flags", e);
}
@@ -202,65 +185,66 @@ public class MainFragment extends PreferenceFragment implements Preference.OnPre
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
- mReceiver.setContext(requireContext());
- mService = mReceiver.getIPC();
+ mController = ControllerFactory.create(this, this);
+ postStatusUpdate();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- runRestoreBinder(savedInstanceState);
- }
-
- private void runRestoreBinder (@Nullable Bundle savedInstanceState) {
- if (BuildConfig.DEBUG) {
- if (savedInstanceState == null) {
- Log.d(TAG, "savedInstanceState is null");
- } else {
- if (savedInstanceState.getBinder(ARG_SERVICE) == null)
- Log.d(TAG, "savedInstanceState doesn't contain binder");
- else
- Log.d(TAG, "Restoring");
- }
- }
- if (savedInstanceState != null && savedInstanceState.getBinder(ARG_SERVICE) != null) {
- // FIXME: 11/25/18 The restoried interface's binder not alive
- mService = IStatusController.Stub.asInterface(savedInstanceState.getBinder(ARG_SERVICE));
- }
+ mController.restoreStatus(savedInstanceState);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- menu.add(0, 0, 0, com.google.android.gms.oss.licenses.R.string.oss_license_title);
- menu.add(0, 1, 0, R.string.about_view_in_github);
- menu.add(0, 2, 0, R.string.about_title);
+ inflater.inflate(R.menu.menu_main, menu);
+ List<String> availableModes = ControllerFactory.getSupportedControllers(requireContext());
+ if (!availableModes.contains(ControllerFactory.ID_ROOT)) menu.findItem(R.id.action_mode_root).setEnabled(false);
+ if (!availableModes.contains(ControllerFactory.ID_SHIZUKU)) menu.findItem(R.id.action_mode_shizuku).setEnabled(false);
+ switch (ControllerFactory.getUserChoice(this)) {
+ case ControllerFactory.ID_ROOT:
+ menu.findItem(R.id.action_mode_root).setChecked(true);
+ break;
+ case ControllerFactory.ID_SHIZUKU:
+ menu.findItem(R.id.action_mode_shizuku).setChecked(true);
+ break;
+ }
}
+ @SuppressLint("RestrictedApi")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
- case 0:
+ case R.id.action_oss:
startActivity(new Intent(getActivity(), OssLicensesMenuActivity.class));
return true;
- case 1:
+ case R.id.action_view:
try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Trumeet/SysUIController_Releases"))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException ignored) {}
return true;
- case 2:
+ case R.id.action_version:
new VersionDialogFragment()
.show(getChildFragmentManager(), "Version");
return true;
+ case R.id.action_mode_shizuku:
+ ControllerFactory.set(requireContext(), ControllerFactory.ID_SHIZUKU);
+ mIsSwitchModeScheduled = true;
+ mStatusPreference.performClick();
+ return true;
+ case R.id.action_mode_root:
+ ControllerFactory.set(requireContext(), ControllerFactory.ID_ROOT);
+ mIsSwitchModeScheduled = true;
+ mStatusPreference.performClick();
+ return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onDestroy() {
- mReceiver.release();
- // Shell will be closed immediately after executing.
- if (mShell != null) mShell.close();
+ mController.destroy();
super.onDestroy();
}
@@ -280,19 +264,19 @@ public class MainFragment extends PreferenceFragment implements Preference.OnPre
}
private boolean runDisable (int flag, boolean enable, String name, boolean disable2) {
- if (obtainService() == null) return false;
+ if (!mController.isServiceReady()) return false;
Log.i(TAG, "runDisable: " + flag + " " + enable + " (" + name + ") " + disable2);
try {
- int flags = disable2 ? mService.getDisable2Flags() : mService.getDisableFlags();
+ int flags = disable2 ? mController.getDisable2Flags() : mController.getDisableFlags();
if (enable) {
flags |= flag;
} else {
flags ^= flag;
}
if (disable2) {
- mService.disable2(flags);
+ mController.disable2(flags);
} else {
- mService.disable(flags);
+ mController.disable(flags);
}
serializeDisableFlags();
return true;
@@ -311,18 +295,20 @@ public class MainFragment extends PreferenceFragment implements Preference.OnPre
public boolean onPreferenceClick(Preference preference) {
switch (preference.getKey()) {
case "key_service_status": {
- boolean shouldStart = obtainService() == null;
- mStatusPreference.setEnabled(false);
- mStatusPreference.setSummary(shouldStart ? R.string.status_starting : R.string.status_stopping);
- if (shouldStart) {
- mShell = new Shell.Builder()
- .useSU()
- .open(this); // Will start after shell opening
- } else {
- try {
- mService.exit();
- } catch (Throwable ignored) {
+ if (mController.canStop()) {
+ boolean shouldStart = !mController.isServiceReady();
+ Log.d(TAG, "sS: " + shouldStart);
+ mStatusPreference.setEnabled(false);
+ mStatusPreference.setSummary(shouldStart ? R.string.status_starting : R.string.status_stopping);
+ if (shouldStart) {
+ mController.startAsync();
+ } else {
+ mController.stopSync();
}
+ } else if (mIsSwitchModeScheduled) {
+ // Revert only if the user is switching mode and the current mode doesn't support stopping.
+ mController.revert();
+ update(null);
}
return true;
}
@@ -331,48 +317,45 @@ public class MainFragment extends PreferenceFragment implements Preference.OnPre
}
@Override
- public void onCommandResult(int commandCode, int exitCode, List<String> output) {
- Log.i(TAG, "Result " + commandCode + ": " + exitCode);
- if (output != null) {
- Log.w(TAG, output.toString());
- }
- switch (commandCode) {
- case SHELL_RUNNING:
- if (exitCode != 0) {
- Toast.makeText(getContext(), R.string.error_can_not_open_shell, Toast.LENGTH_LONG).show();
- postStatusUpdate(); // Return status to not started
- break;
- }
- mShell.addCommand(RootDaemon.getLaunchScript(requireContext(),
- ControllerService.class,
- null,
- null,
- null, BuildConfig.APPLICATION_ID + ":daemon"),
- CODE_START, this);
- break;
- case CODE_START:
- // Kill process immediately
- mShell.kill();
- mShell.close();
- mShell = null;
- if (exitCode != 0) Toast.makeText(getContext(), R.string.error_can_not_start, Toast.LENGTH_LONG).show();
- break;
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mController.saveStatus(outState);
+ }
+
+ @Override
+ public void update(@Nullable Throwable e) {
+ postStatusUpdate();
+ if (e != null) {
+ new AlertDialog.Builder(requireContext())
+ .setMessage(e.getClass().getName() + ": " + e.getMessage())
+ .setPositiveButton(android.R.string.ok, null)
+ .setCancelable(false)
+ .show();
+ } else {
+ if (mIsSwitchModeScheduled) {
+ requireActivity().recreate();
+ mIsSwitchModeScheduled = false;
+ }
}
}
@Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- if (obtainService() != null)
- outState.putBinder(ARG_SERVICE, mService.asBinder());
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.d(TAG, "onActivityResult");
+ if (mController != null && mController.controller() instanceof ShizukuController) {
+ if (((ShizukuController) mController.controller()).onActivityResult(requestCode, resultCode, data))
+ return;
+ }
+ super.onActivityResult(requestCode, resultCode, data);
}
- private IStatusController obtainService () {
- if (!mReceiver.isConnected()) { // TODO: Add instance state support
- mService = null;
- return null;
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ Log.d(TAG, "onRequestPermissionsResult");
+ if (mController != null && mController.controller() instanceof ShizukuController) {
+ if (((ShizukuController) mController.controller()).onRequestPermissionsResult(requestCode, permissions, grantResults))
+ return;
}
- mService = mReceiver.getIPC();
- return mService;
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/auto_start/AutoStartWorker.java b/app/src/main/java/moe/yuuta/sysuicontroller/auto_start/AutoStartWorker.java
index 2ae465b..1ef0e81 100644
--- a/app/src/main/java/moe/yuuta/sysuicontroller/auto_start/AutoStartWorker.java
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/auto_start/AutoStartWorker.java
@@ -18,7 +18,7 @@ import eu.chainfire.librootjavadaemon.RootDaemon;
import eu.chainfire.libsuperuser.Shell;
import moe.yuuta.sysuicontroller.BuildConfig;
import moe.yuuta.sysuicontroller.IStatusController;
-import moe.yuuta.sysuicontroller.core.ControllerService;
+import moe.yuuta.sysuicontroller.core.root.ControllerService;
public class AutoStartWorker extends Worker {
private static final String TAG = AutoStartWorker.class.getSimpleName();
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/core/Controller.java b/app/src/main/java/moe/yuuta/sysuicontroller/core/Controller.java
new file mode 100644
index 0000000..0c0362a
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/core/Controller.java
@@ -0,0 +1,82 @@
+package moe.yuuta.sysuicontroller.core;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * User-friendly controller
+ */
+public final class Controller<T extends IController> implements IController {
+ private final T mTargetController;
+
+ public Controller(T mTargetController) {
+ this.mTargetController = mTargetController;
+ }
+
+ @Override
+ public void startAsync() {
+ mTargetController.startAsync();
+ }
+
+ @Override
+ public boolean isServiceReady() {
+ return mTargetController.isServiceReady();
+ }
+
+ @Override
+ public void disable(int flags) throws RemoteException {
+ mTargetController.disable(flags);
+ }
+
+ @Override
+ public void disable2(int flags) throws RemoteException {
+ mTargetController.disable2(flags);
+ }
+
+ @Override
+ public int getDisableFlags() throws RemoteException {
+ return mTargetController.getDisableFlags();
+ }
+
+ @Override
+ public int getDisable2Flags() throws RemoteException {
+ return mTargetController.getDisable2Flags();
+ }
+
+ @Override
+ public void restoreStatus(@Nullable Bundle savedInstanceState) {
+ if (isServiceReady()) mTargetController.restoreStatus(savedInstanceState);
+ }
+
+ @Override
+ public void saveStatus(@NonNull Bundle savedInstanceState) {
+ if (isServiceReady()) mTargetController.saveStatus(savedInstanceState);
+ }
+
+ @Override
+ public void destroy() {
+ if (isServiceReady()) mTargetController.destroy();
+ }
+
+ @Override
+ public void stopSync() {
+ if (isServiceReady()) mTargetController.stopSync();
+ }
+
+ @Override
+ public boolean canStop() {
+ return mTargetController.canStop();
+ }
+
+ @Override
+ public void revert() {
+ mTargetController.revert();
+ }
+
+ public T controller() {
+ return mTargetController;
+ }
+}
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/core/ControllerFactory.java b/app/src/main/java/moe/yuuta/sysuicontroller/core/ControllerFactory.java
new file mode 100644
index 0000000..9d4a976
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/core/ControllerFactory.java
@@ -0,0 +1,66 @@
+package moe.yuuta.sysuicontroller.core;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import moe.shizuku.api.ShizukuClientHelper;
+import moe.yuuta.sysuicontroller.core.root.RootController;
+import moe.yuuta.sysuicontroller.core.shizuku.ShizukuController;
+
+public final class ControllerFactory {
+ public static final String ID_SHIZUKU = "shizuku";
+ public static final String ID_ROOT = "root";
+
+ public static final String[] ALL_CONTROLLERS = new String[] {
+ ID_SHIZUKU,
+ ID_ROOT
+ };
+
+ private static final String FALLBACK = ID_ROOT;
+
+ public static void set(@NonNull Context context, @NonNull String id) {
+ context.getSharedPreferences("mode", Context.MODE_PRIVATE)
+ .edit()
+ .putString("mode", id)
+ .apply();
+ }
+
+ @NonNull
+ public static String getUserChoice(@NonNull Fragment context) {
+ List<String> available = getSupportedControllers(context.requireContext());
+ String userChoice = context.requireContext().getSharedPreferences("mode", Context.MODE_PRIVATE)
+ .getString("mode", FALLBACK);
+ if (!available.contains(userChoice)) {
+ userChoice = FALLBACK;
+ }
+ return userChoice;
+ }
+
+ @SuppressWarnings("unchecked")
+ @NonNull
+ public static Controller create(@NonNull Fragment context, @NonNull IController.Callback callback) {
+ switch (getUserChoice(context)) {
+ case ID_SHIZUKU:
+ return new Controller(new ShizukuController(callback, context));
+ case ID_ROOT:
+ return new Controller(new RootController(callback, context));
+ default:
+ return new Controller(new RootController(callback, context));
+ }
+ }
+
+ @NonNull
+ public static List<String> getSupportedControllers(@NonNull Context context) {
+ List<String> available = new ArrayList<>(2);
+ if (ShizukuClientHelper.isManagerV3Installed(context)) {
+ available.add(ID_SHIZUKU);
+ }
+ available.add(ID_ROOT);
+ return available;
+ }
+}
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/core/CoreUtils.java b/app/src/main/java/moe/yuuta/sysuicontroller/core/CoreUtils.java
new file mode 100644
index 0000000..6155672
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/core/CoreUtils.java
@@ -0,0 +1,55 @@
+package moe.yuuta.sysuicontroller.core;
+
+import android.app.StatusBarManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import moe.yuuta.sysuicontroller.BuildConfig;
+import moe.yuuta.sysuicontroller.dump.StatusBarServiceDumpDeserializer;
+
+public class CoreUtils {
+ private static final String TAG = "CU";
+ public static List<DisableItem> getAvailableDisableItems() {
+ Field[] fields = StatusBarManager.class.getDeclaredFields();
+ if (fields.length <= 0) return Collections.emptyList();
+ List<DisableItem> items = new ArrayList<>(fields.length);
+ for (Field field : fields) {
+ if (!Modifier.isStatic(field.getModifiers())) continue;
+ if (!Modifier.isFinal(field.getModifiers())) continue;
+ if (!Modifier.isPublic(field.getModifiers())) continue;
+ if (field.getName().equals("DISABLE_MASK") || field.getName().equals("DISABLE2_MASK")
+ || field.getName().equals("DISABLE_NONE") || field.getName().equals("DISABLE2_NONE")) continue;
+ try {
+ if (field.getName().startsWith("DISABLE_")) {
+ items.add(new DisableItem(field.getInt(null), field.getName(), false));
+ continue;
+ }
+ if (field.getName().startsWith("DISABLE2_")) {
+ items.add(new DisableItem(field.getInt(null), field.getName(), true));
+ }
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Unable to access value: " + field.getName(), e);
+ }
+ }
+ return items;
+ }
+
+ @NonNull
+ public static StatusBarServiceDumpDeserializer deserialize (@NonNull final String result) {
+ StatusBarServiceDumpDeserializer deserializer = new StatusBarServiceDumpDeserializer();
+ try {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Result: " + result);
+ deserializer.deserialize(result);
+ } catch (Exception e) {
+ Log.e(TAG, "Error when deserialize status bar service", e);
+ }
+ return deserializer;
+ }
+}
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/core/IController.java b/app/src/main/java/moe/yuuta/sysuicontroller/core/IController.java
new file mode 100644
index 0000000..0e983d3
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/core/IController.java
@@ -0,0 +1,44 @@
+package moe.yuuta.sysuicontroller.core;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public interface IController {
+ /**
+ * Start & init & request permissions. This won't be called if {@link #isServiceReady()} returns true
+ */
+ void startAsync();
+
+ /**
+ * Revert the settings only and not destroy the service
+ */
+ void stopSync();
+
+ /**
+ * Revert the settings only, it will be called only if #canStop() returns false.
+ */
+ void revert();
+
+ boolean canStop();
+
+ boolean isServiceReady();
+ void disable(int flags) throws RemoteException;
+ void disable2(int flags) throws RemoteException;
+ int getDisableFlags() throws RemoteException;
+ int getDisable2Flags() throws RemoteException;
+
+ void restoreStatus(@Nullable Bundle savedInstanceState);
+ void saveStatus(@NonNull Bundle savedInstanceState);
+
+ /**
+ * This will be called either is ready or not. Destroy the service only and not revert settings
+ */
+ void destroy();
+
+ interface Callback {
+ void update(@Nullable Throwable e);
+ }
+}
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/core/ControllerService.java b/app/src/main/java/moe/yuuta/sysuicontroller/core/root/ControllerService.java
index 45026ff..63f6f08 100644
--- a/app/src/main/java/moe/yuuta/sysuicontroller/core/ControllerService.java
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/core/root/ControllerService.java
@@ -1,4 +1,4 @@
-package moe.yuuta.sysuicontroller.core;
+package moe.yuuta.sysuicontroller.core.root;
import android.annotation.SuppressLint;
import android.app.ActivityThread;
@@ -27,6 +27,8 @@ import eu.chainfire.librootjavadaemon.RootDaemon;
import eu.chainfire.libsuperuser.Shell;
import moe.yuuta.sysuicontroller.BuildConfig;
import moe.yuuta.sysuicontroller.IStatusController;
+import moe.yuuta.sysuicontroller.core.CoreUtils;
+import moe.yuuta.sysuicontroller.core.DisableItem;
import moe.yuuta.sysuicontroller.dump.StatusBarServiceDumpDeserializer;
import static moe.yuuta.sysuicontroller.Main.GLOBAL_TAG;
@@ -165,7 +167,6 @@ public class ControllerService extends IStatusController.Stub {
}
private void deserialize () {
- StatusBarServiceDumpDeserializer deserializer = new StatusBarServiceDumpDeserializer();
try {
StringBuilder builder = new StringBuilder();
for (String res : Shell.SH.run(Collections.singletonList("dumpsys statusbar"))) {
@@ -173,12 +174,11 @@ public class ControllerService extends IStatusController.Stub {
builder.append("\n");
}
String result = builder.toString();
- if (BuildConfig.DEBUG) Log.d(TAG, "Result: " + result);
- deserializer.deserialize(result);
+ StatusBarServiceDumpDeserializer deserializer = CoreUtils.deserialize(result);
if (deserializer.getDisable1() != -1) disableFlags = deserializer.getDisable1();
if (deserializer.getDisable2() != -1) disable2Flags = deserializer.getDisable2();
} catch (Exception e) {
- Log.e(TAG, "Error when deserialize status bar service", e);
+ Log.e(TAG, "Error when dumping status bar service", e);
}
}
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/core/root/RootController.java b/app/src/main/java/moe/yuuta/sysuicontroller/core/root/RootController.java
new file mode 100644
index 0000000..ea6a203
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/core/root/RootController.java
@@ -0,0 +1,192 @@
+package moe.yuuta.sysuicontroller.core.root;
+
+import android.app.StatusBarManager;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import java.util.List;
+
+import eu.chainfire.librootjava.RootIPCReceiver;
+import eu.chainfire.librootjavadaemon.RootDaemon;
+import eu.chainfire.libsuperuser.Shell;
+import moe.yuuta.sysuicontroller.BuildConfig;
+import moe.yuuta.sysuicontroller.IStatusController;
+import moe.yuuta.sysuicontroller.core.IController;
+
+public class RootController implements IController, Shell.OnCommandResultListener {
+ private static final String TAG = "Controller_R";
+
+ private static final int CODE_START = 1;
+
+ private Shell.Interactive mShell;
+ private volatile IStatusController mService;
+
+ private static final String ARG_SERVICE = RootController.class.getName() + ".ARG_SERVICE";
+
+ private final Callback mStartCallback;
+ private final Fragment mContext;
+
+ public RootController(Callback mStartCallback, Fragment mContext) {
+ this.mStartCallback = mStartCallback;
+ this.mContext = mContext;
+ mReceiver.setContext(mContext.requireContext());
+ mService = mReceiver.getIPC();
+ }
+
+ @Override
+ public void startAsync() {
+ mShell = new Shell.Builder()
+ .useSU()
+ .open(this); // Will start after shell opening
+ }
+
+ @Override
+ public boolean isServiceReady() {
+ return mService != null && mReceiver.isConnected() && !mReceiver.isDisconnectScheduled();
+ }
+
+ @Override
+ public void disable(int flags) throws RemoteException {
+ mService.disable(flags);
+ }
+
+ @Override
+ public void disable2(int flags) throws RemoteException {
+ mService.disable2(flags);
+ }
+
+ @Override
+ public int getDisableFlags() throws RemoteException {
+ return mService.getDisableFlags();
+ }
+
+ @Override
+ public int getDisable2Flags() throws RemoteException {
+ return mService.getDisable2Flags();
+ }
+
+
+ private RootIPCReceiver<IStatusController> mReceiver = new RootIPCReceiver<IStatusController>(null, ControllerService.CODE_SERVICE) {
+ @Override
+ public void onConnect(IStatusController ipc) {
+ Log.d(TAG, "Connected to remote service");
+ mService = ipc;
+ mStartCallback.update(null);
+ }
+
+ @Override
+ public void onDisconnect(IStatusController ipc) {
+ Log.d(TAG, "Disconnected to remote service");
+ mService = ipc;
+ mStartCallback.update(null);
+ }
+ };
+
+ @Override
+ public void onCommandResult(int commandCode, int exitCode, List<String> output) {
+ Log.i(TAG, "Result " + commandCode + ": " + exitCode);
+ if (output != null) {
+ Log.w(TAG, output.toString());
+ }
+ switch (commandCode) {
+ case SHELL_RUNNING:
+ if (exitCode != 0) {
+ mStartCallback.update(new UnexpectedExitCodeException(exitCode));
+ break;
+ }
+ mShell.addCommand(RootDaemon.getLaunchScript(mContext.requireContext(),
+ ControllerService.class,
+ null,
+ null,
+ null, BuildConfig.APPLICATION_ID + ":daemon"),
+ CODE_START, this);
+ break;
+ case CODE_START:
+ // Kill process immediately
+ mShell.kill();
+ mShell.close();
+ mShell = null;
+ if (exitCode != 0) {
+ mStartCallback.update(new UnexpectedExitCodeException(exitCode));
+ }
+ break;
+ }
+ }
+
+ private void runRestoreBinder (@Nullable Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ if (savedInstanceState == null) {
+ Log.d(TAG, "savedInstanceState is null");
+ } else {
+ if (savedInstanceState.getBinder(ARG_SERVICE) == null)
+ Log.d(TAG, "savedInstanceState doesn't contain binder");
+ else
+ Log.d(TAG, "Restoring");
+ }
+ }
+ if (savedInstanceState != null && savedInstanceState.getBinder(ARG_SERVICE) != null) {
+ // FIXME: 11/25/18 The restoried interface's binder not alive
+ mService = IStatusController.Stub.asInterface(savedInstanceState.getBinder(ARG_SERVICE));
+ }
+ }
+
+ @Override
+ public void restoreStatus(@Nullable Bundle savedInstanceState) {
+ runRestoreBinder(savedInstanceState);
+ }
+
+ @Override
+ public void saveStatus(@NonNull Bundle savedInstanceState) {
+ savedInstanceState.putBinder(ARG_SERVICE, mService.asBinder());
+ }
+
+ @Override
+ public void destroy() {
+ mReceiver.release();
+ // Shell will be closed immediately after executing.
+ if (mShell != null) mShell.close();
+ }
+
+ @Override
+ public void stopSync() {
+ try {
+ // When the service stops, SystemUI auto revert settings.
+ mReceiver.disconnect();
+ mService.exit();
+ } catch (Throwable e) {
+ Log.e(TAG, "exit()", e);
+ }
+ mStartCallback.update(null);
+ }
+
+ @Override
+ public boolean canStop() {
+ return true;
+ }
+
+ @Override
+ public void revert() {
+ try {
+ mService.disable(StatusBarManager.DISABLE_NONE);
+ mService.disable2(StatusBarManager.DISABLE2_NONE);
+ } catch (RemoteException ignored) {}
+ }
+
+ public static class UnexpectedExitCodeException extends Exception {
+ private final int abnormalExitCode;
+
+ public UnexpectedExitCodeException(int abnormalExitCode) {
+ super("Abnormal exit code " + abnormalExitCode);
+ this.abnormalExitCode = abnormalExitCode;
+ }
+
+ public int getAbnormalExitCode() {
+ return abnormalExitCode;
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/core/shizuku/IStatusBarServiceShizuku.java b/app/src/main/java/moe/yuuta/sysuicontroller/core/shizuku/IStatusBarServiceShizuku.java
new file mode 100644
index 0000000..1fdfc10
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/core/shizuku/IStatusBarServiceShizuku.java
@@ -0,0 +1,272 @@
+package moe.yuuta.sysuicontroller.core.shizuku;
+
+import android.content.ComponentName;
+import android.graphics.Rect;
+import android.hardware.biometrics.IBiometricPromptReceiver;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
+
+import com.android.internal.statusbar.IStatusBar;
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.internal.statusbar.StatusBarIcon;
+
+import java.util.List;
+
+import moe.shizuku.api.ShizukuApiConstants;
+import moe.shizuku.api.ShizukuService;
+import moe.shizuku.api.SystemServiceHelper;
+
+class IStatusBarServiceShizuku implements IStatusBarService {
+ private Parcel obtainData(String method) {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(ShizukuApiConstants.BINDER_DESCRIPTOR);
+ data.writeStrongBinder(SystemServiceHelper.getSystemService("statusbar"));
+ data.writeInt(SystemServiceHelper.getTransactionCode(IStatusBarService.Stub.class.getName(),
+ method));
+ data.writeInterfaceToken(IStatusBarService.class.getName());
+ return data;
+ }
+
+ private Parcel transact(Parcel data) throws RemoteException {
+ Parcel reply = Parcel.obtain();
+
+ try {
+ ShizukuService.transactRemote(data, reply, 0);
+ reply.readException();
+ return reply;
+ } finally {
+ data.recycle();
+ }
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+
+ @Override
+ public void expandNotificationsPanel() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void collapsePanels() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void togglePanel() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void disable(int what, IBinder token, String pkg) throws RemoteException {
+ Parcel data = obtainData("disable");
+ data.writeInt(what);
+ data.writeStrongBinder(token);
+ data.writeString(pkg);
+ transact(data).recycle();
+ }
+
+ @Override
+ public void disableForUser(int what, IBinder token, String pkg, int userId) throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void disable2(int what, IBinder token, String pkg) throws RemoteException {
+ Parcel data = obtainData("disable2");
+ data.writeInt(what);
+ data.writeStrongBinder(token);
+ data.writeString(pkg);
+ transact(data).recycle();
+ }
+
+ @Override
+ public void disable2ForUser(int what, IBinder token, String pkg, int userId) throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void setIcon(String slot, String iconPackage, int iconId, int iconLevel, String contentDescription) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setIconVisibility(String slot, boolean visible) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeIcon(String slot) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setImeWindowStatus(IBinder token, int vis, int backDisposition, boolean showImeSwitcher) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void expandSettingsPanel(String subPanel) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void registerStatusBar(IStatusBar callbacks, List<String> iconSlots, List<StatusBarIcon> iconList, int[] switches, List<IBinder> binders, Rect fullscreenStackBounds, Rect dockedStackBounds) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onPanelRevealed(boolean clearNotificationEffects, int numItems) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onPanelHidden() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clearNotificationEffects() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationClick(String key, NotificationVisibility nv) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationActionClick(String key, int actionIndex, NotificationVisibility nv) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationError(String pkg, String tag, int id, int uid, int initialPid, String message, int userId) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onClearAllNotifications(int userId) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationClear(String pkg, String tag, int id, int userId, String key, int dismissalSurface, NotificationVisibility nv) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationVisibilityChanged(NotificationVisibility[] newlyVisibleKeys, NotificationVisibility[] noLongerVisibleKeys) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationExpansionChanged(String key, boolean userAction, boolean expanded) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationDirectReplied(String key) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationSmartRepliesAdded(String key, int replyCount) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationSmartReplySent(String key, int replyIndex) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onNotificationSettingsViewed(String key) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setSystemUiVisibility(int vis, int mask, String cause) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onGlobalActionsShown() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onGlobalActionsHidden() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void shutdown() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void reboot(boolean safeMode) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addTile(ComponentName tile) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void remTile(ComponentName tile) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clickTile(ComponentName tile) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void handleSystemKey(int key) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void showPinningEnterExitToast(boolean entering) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void showPinningEscapeToast() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onFingerprintAuthenticated() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onFingerprintHelp(String message) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void onFingerprintError(String error) throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void hideFingerprintDialog() throws RemoteException {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/app/src/main/java/moe/yuuta/sysuicontroller/core/shizuku/ShizukuController.java b/app/src/main/java/moe/yuuta/sysuicontroller/core/shizuku/ShizukuController.java
new file mode 100644
index 0000000..ff1a9f9
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/sysuicontroller/core/shizuku/ShizukuController.java
@@ -0,0 +1,244 @@
+package moe.yuuta.sysuicontroller.core.shizuku;
+
+import android.app.Activity;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+import androidx.fragment.app.Fragment;
+
+import com.android.internal.statusbar.IStatusBarService;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import moe.shizuku.api.RemoteProcess;
+import moe.shizuku.api.ShizukuApiConstants;
+import moe.shizuku.api.ShizukuClientHelper;
+import moe.shizuku.api.ShizukuService;
+import moe.yuuta.sysuicontroller.Main;
+import moe.yuuta.sysuicontroller.R;
+import moe.yuuta.sysuicontroller.core.CoreUtils;
+import moe.yuuta.sysuicontroller.core.IController;
+
+public class ShizukuController implements IController {
+ private static final String TAG = "Controller_S";
+
+ private static final int REQUEST_CODE_PERMISSION_V3 = 0x100;
+ private static final int REQUEST_CODE_AUTHORIZATION_V3 = 0x300;
+
+ private final Callback mStartCallback;
+ private final Fragment mContext;
+
+ private final IStatusBarService mService = new IStatusBarServiceShizuku();
+
+ public ShizukuController(Callback mStartCallback, Fragment mContext) {
+ this.mStartCallback = mStartCallback;
+ this.mContext = mContext;
+ }
+
+ @Override
+ public void startAsync() {
+ if (!hasShizukuPermission()) {
+ // if (mContext instanceof Activity && mContext.canStartActivityForResult()) {
+ if (ShizukuClientHelper.isPreM()) {
+ Intent intent = ShizukuClientHelper.createPre23AuthorizationIntent(mContext.requireContext());
+ try {
+ mContext.startActivityForResult(intent, REQUEST_CODE_AUTHORIZATION_V3);
+ } catch (Throwable ignored) {
+ }
+ } else {
+ mContext.requestPermissions(new String[]{ShizukuApiConstants.PERMISSION}, REQUEST_CODE_PERMISSION_V3);
+ }
+ // }
+ return;
+ }
+ if (!enforceRoot()) {
+ mStartCallback.update(null);
+ }
+ }
+
+ private boolean enforceRoot() {
+ if (!isUnderRoot()) {
+ mStartCallback.update(new NonRootShizukuException(mContext.requireContext()));
+ return true;
+ }
+ return false;
+ }
+
+ private boolean hasShizukuPermission() {
+ if (ShizukuClientHelper.isPreM()) {
+ return Main.isShizukuV3TokenValid();
+ } else {
+ return ActivityCompat.checkSelfPermission(mContext.requireContext(), ShizukuApiConstants.PERMISSION) == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+
+ private boolean isUnderRoot() {
+ try {
+ // Check permission function doesn't function correctly
+ return ShizukuService.getUid() == Process.ROOT_UID;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to check whatever is under root", e);
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isServiceReady() {
+ return hasShizukuPermission() && ShizukuService.pingBinder() && isUnderRoot();
+ }
+
+ @Override
+ public void stopSync() {
+ try {
+ disable(StatusBarManager.DISABLE_NONE);
+ disable2(StatusBarManager.DISABLE2_NONE);
+ } catch (RemoteException ignored) {}
+ mStartCallback.update(null);
+ }
+
+ @Override
+ public void disable(int flags) throws RemoteException {
+ mService.disable(flags, ShizukuService.getBinder(), "android");
+ }
+
+ @Override
+ public void disable2(int flags) throws RemoteException {
+ mService.disable2(flags, ShizukuService.getBinder(), "android");
+ }
+
+ @Override
+ public int getDisableFlags() throws RemoteException {
+ final String dumpRaw = dump();
+ if (dumpRaw == null) return StatusBarManager.DISABLE_NONE;
+ return CoreUtils.deserialize(dumpRaw).getDisable1();
+ }
+
+ @Nullable
+ private String dump() throws RemoteException {
+ RemoteProcess dumpProc = ShizukuService.newProcess(new String[]{
+ "/system/bin/dumpsys",
+ "statusbar"
+ }, null, "/system/bin");
+ final InputStream outStream = dumpProc.getInputStream();
+ BufferedReader outReader = new BufferedReader(new InputStreamReader(outStream));
+ try {
+ String line;
+ StringBuilder builder = new StringBuilder();
+ while ((line = outReader.readLine()) != null) {
+ builder.append(line);
+ builder.append("\n");
+ }
+ final String output = builder.toString();
+ int result;
+ try {
+ result = dumpProc.waitFor();
+ } catch (InterruptedException ignored) {
+ result = -100;
+ }
+ outReader.close();
+ outStream.close();
+ if (result != 0 || output.trim().equals("")) {
+ Log.e(TAG, "Dump process exit with result " + result + ", has output: " + !output.trim().equals(""));
+ return null;
+ }
+ return output;
+ } catch (IOException e) {
+ Log.e(TAG, "IOException during dump", e);
+ return null;
+ } finally {
+ dumpProc.destroy();
+ }
+ }
+
+ @Override
+ public int getDisable2Flags() throws RemoteException {
+ final String dumpRaw = dump();
+ if (dumpRaw == null) return StatusBarManager.DISABLE2_NONE;
+ return CoreUtils.deserialize(dumpRaw).getDisable2();
+ }
+
+ @Override
+ public void restoreStatus(@Nullable Bundle savedInstanceState) {
+ }
+
+ @Override
+ public void saveStatus(@NonNull Bundle savedInstanceState) {
+ }
+
+ @Override
+ public void destroy() {}
+
+ @Override
+ public boolean canStop() {
+ return false;
+ }
+
+ public static final class NonRootShizukuException extends Exception {
+ private NonRootShizukuException(@NonNull Context context) {
+ super(context.getString(R.string.shizuku_non_root));
+ }
+ }
+
+ public boolean onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_CODE_PERMISSION_V3: {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (!enforceRoot()) {
+ mStartCallback.update(null);
+ }
+ }
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ // only called in API pre-23
+ case REQUEST_CODE_AUTHORIZATION_V3: {
+ if (resultCode == Activity.RESULT_OK) {
+ String token = ShizukuClientHelper.setPre23Token(data, mContext.requireContext());
+ if (ShizukuService.pingBinder()) {
+ try {
+ // each of your process need to call this
+ boolean valid = ShizukuService.setCurrentProcessTokenPre23(token);
+ Main.setV3TokenValid(valid);
+ if (!enforceRoot()) {
+ mStartCallback.update(null);
+ }
+ } catch (RemoteException e) {
+ mStartCallback.update(e);
+ }
+ }
+ }
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void revert() {
+ try {
+ disable(StatusBarManager.DISABLE_NONE);
+ disable2(StatusBarManager.DISABLE2_NONE);
+ } catch (RemoteException ignored) {}
+ }
+}
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..73c0d00
--- /dev/null
+++ b/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,24 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:title="@string/mode">
+ <menu>
+ <group android:checkableBehavior="single">
+ <item android:id="@+id/action_mode_shizuku"
+ android:title="@string/mode_shizuku" />
+ <item android:id="@+id/action_mode_root"
+ android:title="@string/mode_root" />
+ </group>
+ </menu>
+ </item>
+ <item android:title="@string/about">
+ <menu>
+ <group>
+ <item android:id="@+id/action_version"
+ android:title="@string/about_title" />
+ <item android:id="@+id/action_view"
+ android:title="@string/about_view_in_github" />
+ <item android:id="@+id/action_oss"
+ android:title="@string/oss_license_title" />
+ </group>
+ </menu>
+ </item>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4132d51..693d79f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,19 +2,17 @@
<string name="app_name">SysUIController</string>
<string name="app_title">System UI Control α</string>
<string name="main_service_status">Service Status</string>
- <string name="main_service_status_summary">Service must be started to apply your settings, tap to check or start (ROOT)</string>
+ <string name="main_service_status_summary">Service must be started to apply your settings, tap to check or start</string>
<string name="status_starting">Starting</string>
<string name="status_stopping">Stopping</string>
<string name="status_started">Running, tap to stop</string>
+ <string name="about">About</string>
<string name="about_title">Version info</string>
<string name="about_view_in_github">View in GitHub</string>
<string name="about_version">Version %1$s</string>
<string name="about_copyright">Author yuuta.moe</string>
- <string name="error_can_not_open_shell">Can not have ROOT access</string>
- <string name="error_can_not_start">Can not start service, please read system logs (tag "UIC") and feedback</string>
-
<string name="additional_key">Additional support item by your device</string>
<string name="not_available">Your device doesn\'t support this feature</string>
@@ -39,4 +37,9 @@
<string name="main_other">Others</string>
<string name="disable_global_actions">Disable global actions (e.g. Power menu)</string>
<string name="disable_rotate_suggestions">Hide rotate icon</string>
+
+ <string name="mode">Mode</string>
+ <string name="mode_shizuku">Shizuku (ROOT)</string>
+ <string name="mode_root">ROOT</string>
+ <string name="shizuku_non_root">Shizuku must be ran with ROOT</string>
</resources>