diff options
author | YuutaW <17158086+trumeet@users.noreply.github.com> | 2019-04-06 09:32:14 -0700 |
---|---|---|
committer | YuutaW <17158086+Trumeet@users.noreply.github.com> | 2019-04-06 09:32:14 -0700 |
commit | 3b747bb655aacd0c1d66d6628379c3789f22a7c9 (patch) | |
tree | ab76430b95cbaea162a15369aa01156188dd5765 | |
parent | f2b6da67d7f564de171d837cfaaa7c5d5562425b (diff) | |
download | SysUIController-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>
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> |