/** * Copyright (C) 2019 LinkedIn Corp. * <p> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.linkedin.android.testbutler.shell; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.RemoteException; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.BundleCompat; import com.linkedin.android.testbutler.AccessibilityServiceEnabler; import com.linkedin.android.testbutler.ButlerApi; import com.linkedin.android.testbutler.ButlerApiStubBase; import com.linkedin.android.testbutler.NoDialogActivityController; import java.io.Closeable; import java.util.concurrent.CountDownLatch; /** * Contains the ButlerApi implementation that is run as the shell user to gain access to privileged * APIs. Acts like a typical Service in that it contains an AIDL stub implementation which is called * via Binder, but it cannot be bound or started via service APIs. Instead, once this class is run * from the shell, it sends a Broadcast containing the ButlerApi binder back to the real * ButlerService. */ public class ShellButlerService implements Closeable { private static final String TAG = ShellButlerService.class.getSimpleName(); public static final String BROADCAST_BUTLER_API_ACTION = "com.linkedin.android.testbutler.BROADCAST_BUTLER_API"; public static final String BUTLER_API_BUNDLE_KEY = "ButlerApi"; public static final int KILL_CODE = ButlerApi.Stub.LAST_CALL_TRANSACTION; static final String SHELL_PACKAGE = "com.android.shell"; private final CountDownLatch stop = new CountDownLatch(1); private final ShellSettingsAccessor settings; private GsmDataDisabler gsmDataDisabler; private PermissionGranter permissionGranter; private WifiManagerWrapper wifiManager; private AccessibilityServiceEnabler accessibilityServiceEnabler; private final ButlerApiStubBase butlerApi = new ButlerApiStubBase() { @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { if (code == KILL_CODE) { stop.countDown(); return true; } return super.onTransact(code, data, reply, flags); } @Override public boolean setWifiState(boolean enabled) throws RemoteException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) { Log.e(TAG, "setWifiState should not be called on ShellButlerService before 8.1"); return false; } return wifiManager.setWifiEnabled(enabled); } @Override public boolean setGsmState(boolean enabled) throws RemoteException { return gsmDataDisabler.setGsmState(enabled); } @Override public boolean grantPermission(String packageName, String permission) throws RemoteException { return permissionGranter.grantPermission(packageName, permission); } @Override public boolean setAccessibilityServiceState(boolean enabled) throws RemoteException { return accessibilityServiceEnabler.setAccessibilityServiceEnabled(enabled); } }; private ShellButlerService(@NonNull ShellSettingsAccessor settings) { this.settings = settings; } private void onCreate() { Log.d(TAG, "ShellButlerService starting up..."); ServiceManagerWrapper serviceManager = ServiceManagerWrapper.newInstance(); gsmDataDisabler = new GsmDataDisabler(serviceManager); permissionGranter = new PermissionGranter(serviceManager); AccessibilityManagerWrapper accessibilityWrapper = new AccessibilityManagerWrapper(serviceManager); accessibilityServiceEnabler = new AccessibilityServiceEnabler(accessibilityWrapper, settings); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { wifiManager = WifiManagerWrapper.getInstance(serviceManager); } butlerApi.onCreate(settings); // Install custom IActivityController to prevent system dialogs from appearing if apps crash or ANR NoDialogActivityController.install(); } private void onDestroy() { Log.d(TAG, "ShellButlerService shutting down..."); butlerApi.onDestroy(); // Uninstall our IActivityController to resume normal Activity behavior NoDialogActivityController.uninstall(); // Turn the accessibility service off it we enabled it try { accessibilityServiceEnabler.setAccessibilityServiceEnabled(false); } catch (RemoteException ignored) { } Log.d(TAG, "ShellButlerService shut down completed"); } @Override public void close() { onDestroy(); } private void broadcastButlerApi() throws Exception { Intent intent = new Intent(BROADCAST_BUTLER_API_ACTION); Bundle bundle = new Bundle(); BundleCompat.putBinder(bundle, BUTLER_API_BUNDLE_KEY, butlerApi); intent.putExtra(BUTLER_API_BUNDLE_KEY, bundle); ActivityManagerWrapper.newInstance().broadcastIntent(intent); } public static void main(String[] args) { try (ShellSettingsAccessor settings = ShellSettingsAccessor.newInstance(); ShellButlerService shellButlerService = new ShellButlerService(settings)) { shellButlerService.onCreate(); shellButlerService.broadcastButlerApi(); Log.d(TAG, "ButlerApi sent, waiting for stop"); shellButlerService.stop.await(); } catch (Exception e) { Log.e(TAG, "Exception in ShellButlerService, exiting...", e); } } }