diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index c4501bade0985..1af203447694a 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -19,6 +19,7 @@ package android.os; import android.util.Log; import android.util.Slog; import com.android.internal.util.FastPrintWriter; +import libcore.io.IoUtils; import java.io.FileDescriptor; import java.io.FileOutputStream; @@ -342,11 +343,7 @@ public class Binder implements IBinder { try { dump(fd.getFileDescriptor(), args); } finally { - try { - fd.close(); - } catch (IOException e) { - // swallowed, not propagated back to the caller - } + IoUtils.closeQuietly(fd); } } // Write the StrictMode header. @@ -356,6 +353,31 @@ public class Binder implements IBinder { StrictMode.clearGatheredViolations(); } return true; + } else if (code == SHELL_COMMAND_TRANSACTION) { + ParcelFileDescriptor in = data.readFileDescriptor(); + ParcelFileDescriptor out = data.readFileDescriptor(); + ParcelFileDescriptor err = data.readFileDescriptor(); + String[] args = data.readStringArray(); + ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(data); + try { + if (out != null) { + shellCommand(in != null ? in.getFileDescriptor() : null, + out.getFileDescriptor(), + err != null ? err.getFileDescriptor() : out.getFileDescriptor(), + args, resultReceiver); + } + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + IoUtils.closeQuietly(err); + // Write the StrictMode header. + if (reply != null) { + reply.writeNoException(); + } else { + StrictMode.clearGatheredViolations(); + } + } + return true; } return false; } @@ -368,33 +390,37 @@ public class Binder implements IBinder { FileOutputStream fout = new FileOutputStream(fd); PrintWriter pw = new FastPrintWriter(fout); try { - final String disabled; - synchronized (Binder.class) { - disabled = sDumpDisabled; - } - if (disabled == null) { - try { - dump(fd, pw, args); - } catch (SecurityException e) { - pw.println("Security exception: " + e.getMessage()); - throw e; - } catch (Throwable e) { - // Unlike usual calls, in this case if an exception gets thrown - // back to us we want to print it back in to the dump data, since - // that is where the caller expects all interesting information to - // go. - pw.println(); - pw.println("Exception occurred while dumping:"); - e.printStackTrace(pw); - } - } else { - pw.println(sDumpDisabled); - } + doDump(fd, pw, args); } finally { pw.flush(); } } - + + void doDump(FileDescriptor fd, PrintWriter pw, String[] args) { + final String disabled; + synchronized (Binder.class) { + disabled = sDumpDisabled; + } + if (disabled == null) { + try { + dump(fd, pw, args); + } catch (SecurityException e) { + pw.println("Security exception: " + e.getMessage()); + throw e; + } catch (Throwable e) { + // Unlike usual calls, in this case if an exception gets thrown + // back to us we want to print it back in to the dump data, since + // that is where the caller expects all interesting information to + // go. + pw.println(); + pw.println("Exception occurred while dumping:"); + e.printStackTrace(pw); + } + } else { + pw.println(sDumpDisabled); + } + } + /** * Like {@link #dump(FileDescriptor, String[])}, but ensures the target * executes asynchronously. @@ -425,6 +451,34 @@ public class Binder implements IBinder { protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { } + /** + * @param in The raw file descriptor that an input data stream can be read from. + * @param out The raw file descriptor that normal command messages should be written to. + * @param err The raw file descriptor that command error messages should be written to. + * @param args Command-line arguments. + * @param resultReceiver Called when the command has finished executing, with the result code. + * @throws RemoteException + * @hide + */ + public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) throws RemoteException { + onShellCommand(in, out, err, args, resultReceiver); + } + + /** + * Handle a call to {@link #shellCommand}. The default implementation simply prints + * an error message. Override and replace with your own. + * @hide + */ + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) throws RemoteException { + FileOutputStream fout = new FileOutputStream(err != null ? err : out); + PrintWriter pw = new FastPrintWriter(fout); + pw.println("No shell command implementation."); + pw.flush(); + resultReceiver.send(0, null); + } + /** * Default implementation rewinds the parcels and calls onTransact. On * the remote side, transact calls into the binder to do the IPC. @@ -590,6 +644,24 @@ final class BinderProxy implements IBinder { } } + public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeFileDescriptor(in); + data.writeFileDescriptor(out); + data.writeFileDescriptor(err); + data.writeStringArray(args); + resultReceiver.writeToParcel(data, 0); + try { + transact(SHELL_COMMAND_TRANSACTION, data, reply, 0); + reply.readException(); + } finally { + data.recycle(); + reply.recycle(); + } + } + BinderProxy() { mSelf = new WeakReference(this); } diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index 2c21d135e2b8d..0fa87509a77a6 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -102,6 +102,12 @@ public interface IBinder { */ int DUMP_TRANSACTION = ('_'<<24)|('D'<<16)|('M'<<8)|'P'; + /** + * IBinder protocol transaction code: execute a shell command. + * @hide + */ + int SHELL_COMMAND_TRANSACTION = ('_'<<24)|('C'<<16)|('M'<<8)|'D'; + /** * IBinder protocol transaction code: interrogate the recipient side * of the transaction for its canonical interface descriptor. @@ -187,7 +193,7 @@ public interface IBinder { * the transact() method. */ public IInterface queryLocalInterface(String descriptor); - + /** * Print the object's state into the given stream. * @@ -195,7 +201,7 @@ public interface IBinder { * @param args additional arguments to the dump request. */ public void dump(FileDescriptor fd, String[] args) throws RemoteException; - + /** * Like {@link #dump(FileDescriptor, String[])} but always executes * asynchronously. If the object is local, a new thread is created @@ -206,6 +212,20 @@ public interface IBinder { */ public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException; + /** + * Execute a shell command on this object. This may be performed asynchrously from the caller; + * the implementation must always call resultReceiver when finished. + * + * @param in The raw file descriptor that an input data stream can be read from. + * @param out The raw file descriptor that normal command messages should be written to. + * @param err The raw file descriptor that command error messages should be written to. + * @param args Command-line arguments. + * @param resultReceiver Called when the command has finished executing, with the result code. + * @hide + */ + public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) throws RemoteException; + /** * Perform a generic operation with the object. * diff --git a/core/java/android/os/ShellCommand.java b/core/java/android/os/ShellCommand.java new file mode 100644 index 0000000000000..d64273a36eacc --- /dev/null +++ b/core/java/android/os/ShellCommand.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 android.os; + +import android.util.Slog; +import com.android.internal.util.FastPrintWriter; + +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.PrintWriter; + +/** + * @hide + */ +public abstract class ShellCommand { + static final String TAG = "ShellCommand"; + static final boolean DEBUG = false; + + private Binder mTarget; + private FileDescriptor mIn; + private FileDescriptor mOut; + private FileDescriptor mErr; + private String[] mArgs; + private ResultReceiver mResultReceiver; + + private String mCmd; + private int mArgPos; + private String mCurArgData; + + private FastPrintWriter mOutPrintWriter; + private FastPrintWriter mErrPrintWriter; + + public void exec(Binder target, FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) { + mTarget = target; + mIn = in; + mOut = out; + mErr = err; + mArgs = args; + mResultReceiver = resultReceiver; + mCmd = args != null && args.length > 0 ? args[0] : null; + mArgPos = 1; + mCurArgData = null; + mOutPrintWriter = null; + mErrPrintWriter = null; + + if (DEBUG) Slog.d(TAG, "Starting command " + mCmd + " on " + mTarget); + int res = -1; + try { + res = onCommand(mCmd); + if (DEBUG) Slog.d(TAG, "Executed command " + mCmd + " on " + mTarget); + } catch (SecurityException e) { + PrintWriter eout = getErrPrintWriter(); + eout.println("Security exception: " + e.getMessage()); + eout.println(); + e.printStackTrace(eout); + } catch (Throwable e) { + // Unlike usual calls, in this case if an exception gets thrown + // back to us we want to print it back in to the dump data, since + // that is where the caller expects all interesting information to + // go. + PrintWriter eout = getErrPrintWriter(); + eout.println(); + eout.println("Exception occurred while dumping:"); + e.printStackTrace(eout); + } finally { + if (DEBUG) Slog.d(TAG, "Flushing output streams on " + mTarget); + if (mOutPrintWriter != null) { + mOutPrintWriter.flush(); + } + if (mErrPrintWriter != null) { + mErrPrintWriter.flush(); + } + if (DEBUG) Slog.d(TAG, "Sending command result on " + mTarget); + mResultReceiver.send(res, null); + } + if (DEBUG) Slog.d(TAG, "Finished command " + mCmd + " on " + mTarget); + } + + public PrintWriter getOutPrintWriter() { + if (mOutPrintWriter == null) { + FileOutputStream fout = new FileOutputStream(mOut); + mOutPrintWriter = new FastPrintWriter(fout); + } + return mOutPrintWriter; + } + + public PrintWriter getErrPrintWriter() { + if (mErr == null) { + return getOutPrintWriter(); + } + if (mErrPrintWriter == null) { + FileOutputStream fout = new FileOutputStream(mErr); + mErrPrintWriter = new FastPrintWriter(fout); + } + return mErrPrintWriter; + } + + /** + * Return the next option on the command line -- that is an argument that + * starts with '-'. If the next argument is not an option, null is returned. + */ + public String getNextOption() { + if (mCurArgData != null) { + String prev = mArgs[mArgPos - 1]; + throw new IllegalArgumentException("No argument expected after \"" + prev + "\""); + } + if (mArgPos >= mArgs.length) { + return null; + } + String arg = mArgs[mArgPos]; + if (!arg.startsWith("-")) { + return null; + } + mArgPos++; + if (arg.equals("--")) { + return null; + } + if (arg.length() > 1 && arg.charAt(1) != '-') { + if (arg.length() > 2) { + mCurArgData = arg.substring(2); + return arg.substring(0, 2); + } else { + mCurArgData = null; + return arg; + } + } + mCurArgData = null; + return arg; + } + + /** + * Return the next argument on the command line, whatever it is; if there are + * no arguments left, return null. + */ + public String getNextArg() { + if (mCurArgData != null) { + String arg = mCurArgData; + mCurArgData = null; + return arg; + } else if (mArgPos < mArgs.length) { + return mArgs[mArgPos++]; + } else { + return null; + } + } + + /** + * Return the next argument on the command line, whatever it is; if there are + * no arguments left, throws an IllegalArgumentException to report this to the user. + */ + public String getNextArgRequired() { + String arg = getNextArg(); + if (arg == null) { + String prev = mArgs[mArgPos - 1]; + throw new IllegalArgumentException("Argument expected after \"" + prev + "\""); + } + return arg; + } + + public int handleDefaultCommands(String cmd) { + if ("dump".equals(cmd)) { + String[] newArgs = new String[mArgs.length-1]; + System.arraycopy(mArgs, 1, newArgs, 0, mArgs.length-1); + mTarget.doDump(mOut, getOutPrintWriter(), newArgs); + return 0; + } else if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) { + onHelp(); + } else { + getOutPrintWriter().println("Unknown command: " + cmd); + } + return -1; + } + + public abstract int onCommand(String cmd); + + public abstract void onHelp(); +} diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java index ebcdf7c319014..223519ddb3010 100644 --- a/services/core/java/com/android/server/DeviceIdleController.java +++ b/services/core/java/com/android/server/DeviceIdleController.java @@ -55,7 +55,9 @@ import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.Process; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.ServiceManager; +import android.os.ShellCommand; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -743,6 +745,8 @@ public class DeviceIdleController extends SystemService final MyHandler mHandler; + BinderService mBinderService; + private final class BinderService extends IDeviceIdleController.Stub { @Override public void addPowerSaveWhitelistApp(String name) { getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, @@ -794,37 +798,20 @@ public class DeviceIdleController extends SystemService @Override public void addPowerSaveTempWhitelistApp(String packageName, long duration, int userId, String reason) throws RemoteException { - getContext().enforceCallingPermission( - Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, - "No permission to change device idle whitelist"); - final int callingUid = Binder.getCallingUid(); - userId = ActivityManagerNative.getDefault().handleIncomingUser( - Binder.getCallingPid(), - callingUid, - userId, - /*allowAll=*/ false, - /*requireFull=*/ false, - "addPowerSaveTempWhitelistApp", null); - final long token = Binder.clearCallingIdentity(); - try { - DeviceIdleController.this.addPowerSaveTempWhitelistAppInternal(callingUid, - packageName, duration, userId, true, reason); - } finally { - Binder.restoreCallingIdentity(token); - } + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); } @Override public long addPowerSaveTempWhitelistAppForMms(String packageName, int userId, String reason) throws RemoteException { long duration = mConstants.MMS_TEMP_APP_WHITELIST_DURATION; - addPowerSaveTempWhitelistApp(packageName, duration, userId, reason); + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); return duration; } @Override public long addPowerSaveTempWhitelistAppForSms(String packageName, int userId, String reason) throws RemoteException { long duration = mConstants.SMS_TEMP_APP_WHITELIST_DURATION; - addPowerSaveTempWhitelistApp(packageName, duration, userId, reason); + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); return duration; } @@ -837,6 +824,11 @@ public class DeviceIdleController extends SystemService @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { DeviceIdleController.this.dump(fd, pw, args); } + + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, + FileDescriptor err, String[] args, ResultReceiver resultReceiver) { + (new Shell()).exec(this, in, out, err, args, resultReceiver); + } } public final class LocalService { @@ -912,7 +904,8 @@ public class DeviceIdleController extends SystemService mInactiveTimeout = mConstants.INACTIVE_TIMEOUT; } - publishBinderService(Context.DEVICE_IDLE_CONTROLLER, new BinderService()); + mBinderService = new BinderService(); + publishBinderService(Context.DEVICE_IDLE_CONTROLLER, mBinderService); publishLocalService(LocalService.class, new LocalService()); } @@ -1104,11 +1097,33 @@ public class DeviceIdleController extends SystemService } } + void addPowerSaveTempWhitelistAppChecked(String packageName, long duration, + int userId, String reason) throws RemoteException { + getContext().enforceCallingPermission( + Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, + "No permission to change device idle whitelist"); + final int callingUid = Binder.getCallingUid(); + userId = ActivityManagerNative.getDefault().handleIncomingUser( + Binder.getCallingPid(), + callingUid, + userId, + /*allowAll=*/ false, + /*requireFull=*/ false, + "addPowerSaveTempWhitelistApp", null); + final long token = Binder.clearCallingIdentity(); + try { + addPowerSaveTempWhitelistAppInternal(callingUid, + packageName, duration, userId, true, reason); + } finally { + Binder.restoreCallingIdentity(token); + } + } + /** * Adds an app to the temporary whitelist and resets the endTime for granting the * app an exemption to access network and acquire wakelocks. */ - public void addPowerSaveTempWhitelistAppInternal(int callingUid, String packageName, + void addPowerSaveTempWhitelistAppInternal(int callingUid, String packageName, long duration, int userId, boolean sync, String reason) { try { int uid = getContext().getPackageManager().getPackageUid(packageName, userId); @@ -1122,7 +1137,7 @@ public class DeviceIdleController extends SystemService * Adds an app to the temporary whitelist and resets the endTime for granting the * app an exemption to access network and acquire wakelocks. */ - public void addPowerSaveTempWhitelistAppDirectInternal(int callingUid, int appId, + void addPowerSaveTempWhitelistAppDirectInternal(int callingUid, int appId, long duration, boolean sync, String reason) { final long timeNow = SystemClock.elapsedRealtime(); Runnable networkPolicyTempWhitelistCallback = null; @@ -1698,11 +1713,10 @@ public class DeviceIdleController extends SystemService out.endDocument(); } - private void dumpHelp(PrintWriter pw) { - pw.println("Device idle controller (deviceidle) dump options:"); - pw.println(" [-h] [CMD]"); - pw.println(" -h: print this help text."); - pw.println("Commands:"); + static void dumpHelp(PrintWriter pw) { + pw.println("Device idle controller (deviceidle) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); pw.println(" step"); pw.println(" Immediately step to next state, without waiting for alarm."); pw.println(" force-idle"); @@ -1718,10 +1732,184 @@ public class DeviceIdleController extends SystemService pw.println(" Print currently whitelisted apps."); pw.println(" whitelist [package ...]"); pw.println(" Add (prefix with +) or remove (prefix with -) packages."); - pw.println(" tempwhitelist [package ..]"); + pw.println(" tempwhitelist [-u] [package ..]"); pw.println(" Temporarily place packages in whitelist for 10 seconds."); } + class Shell extends ShellCommand { + int userId = UserHandle.USER_SYSTEM; + + @Override + public int onCommand(String cmd) { + return onShellCommand(this, cmd); + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + dumpHelp(pw); + } + } + + int onShellCommand(Shell shell, String cmd) { + PrintWriter pw = shell.getOutPrintWriter(); + if ("step".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + exitForceIdleLocked(); + stepIdleStateLocked(); + pw.print("Stepped to: "); pw.println(stateToString(mState)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("force-idle".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + if (!mEnabled) { + pw.println("Unable to go idle; not enabled"); + return -1; + } + mForceIdle = true; + becomeInactiveIfAppropriateLocked(); + int curState = mState; + while (curState != STATE_IDLE) { + stepIdleStateLocked(); + if (curState == mState) { + pw.print("Unable to go idle; stopped at "); + pw.println(stateToString(mState)); + exitForceIdleLocked(); + return -1; + } + curState = mState; + } + pw.println("Now forced in to idle mode"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("disable".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + if (mEnabled) { + mEnabled = false; + becomeActiveLocked("disabled", Process.myUid()); + pw.println("Idle mode disabled"); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("enable".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + exitForceIdleLocked(); + if (!mEnabled) { + mEnabled = true; + becomeInactiveIfAppropriateLocked(); + pw.println("Idle mode enabled"); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("enabled".equals(cmd)) { + synchronized (this) { + pw.println(mEnabled ? "1" : " 0"); + } + } else if ("whitelist".equals(cmd)) { + long token = Binder.clearCallingIdentity(); + try { + String arg = shell.getNextArg(); + if (arg != null) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DEVICE_POWER, null); + do { + if (arg.length() < 1 || (arg.charAt(0) != '-' + && arg.charAt(0) != '+')) { + pw.println("Package must be prefixed with + or -: " + arg); + return -1; + } + char op = arg.charAt(0); + String pkg = arg.substring(1); + if (op == '+') { + if (addPowerSaveWhitelistAppInternal(pkg)) { + pw.println("Added: " + pkg); + } else { + pw.println("Unknown package: " + pkg); + } + } else { + if (removePowerSaveWhitelistAppInternal(pkg)) { + pw.println("Removed: " + pkg); + } + } + } while ((arg=shell.getNextArg()) != null); + } else { + synchronized (this) { + for (int j=0; j= args.length) { - pw.println("At least one package name must be specified"); - return; - } - while (i < args.length) { - arg = args[i]; - i++; - addPowerSaveTempWhitelistAppInternal(0, arg, 10000L, userId, true, - "shell"); - pw.println("Added: " + arg); - } - } finally { - Binder.restoreCallingIdentity(token); - } - return; } else if (arg.length() > 0 && arg.charAt(0) == '-'){ pw.println("Unknown option: " + arg); return; } else { - pw.println("Unknown command: " + arg); + Shell shell = new Shell(); + shell.userId = userId; + String[] newArgs = new String[args.length-i]; + System.arraycopy(args, i, newArgs, 0, args.length-i); + shell.exec(mBinderService, null, fd, null, newArgs, new ResultReceiver(null)); return; } }