2 * Copyright (C) 2013 Koushik Dutta (@koush)
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.koushikdutta.superuser;
19 import java.io.DataInputStream;
21 import java.util.HashMap;
23 import junit.framework.Assert;
24 import android.annotation.SuppressLint;
25 import android.content.ContentValues;
26 import android.content.Intent;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.content.res.Configuration;
30 import android.net.LocalSocket;
31 import android.net.LocalSocketAddress;
32 import android.net.LocalSocketAddress.Namespace;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.support.v4.app.FragmentActivity;
36 import android.support.v4.app.FragmentTransaction;
37 import android.util.Log;
38 import android.view.View;
39 import android.view.View.OnClickListener;
40 import android.view.ViewGroup;
41 import android.widget.ArrayAdapter;
42 import android.widget.Button;
43 import android.widget.ImageView;
44 import android.widget.RadioButton;
45 import android.widget.RadioGroup;
46 import android.widget.Spinner;
47 import android.widget.TextView;
48 import android.widget.Toast;
50 import com.koushikdutta.superuser.db.SuDatabaseHelper;
51 import com.koushikdutta.superuser.db.UidPolicy;
52 import com.koushikdutta.superuser.util.Settings;
54 @SuppressLint("ValidFragment")
55 public class MultitaskSuRequestActivity extends FragmentActivity {
56 private static final String LOGTAG = "Superuser";
64 Handler mHandler = new Handler();
73 public int getGracePeriod() {
77 void handleAction(boolean action, Integer until) {
78 Assert.assertTrue(!mHandled);
81 mSocket.getOutputStream().write((action ? "socket:ALLOW" : "socket:DENY").getBytes());
83 catch (Exception ex) {
88 if (mSpinner.isShown()) {
89 int pos = mSpinner.getSelectedItemPosition();
90 int id = mSpinnerIds[pos];
91 if (id == R.string.remember_for) {
92 until = (int)(System.currentTimeMillis() / 1000) + getGracePeriod() * 60;
94 else if (id == R.string.remember_forever) {
98 else if (mRemember.isShown()) {
99 if (mRemember.getCheckedRadioButtonId() == R.id.remember_for) {
100 until = (int)(System.currentTimeMillis() / 1000) + getGracePeriod() * 60;
102 else if (mRemember.getCheckedRadioButtonId() == R.id.remember_forever) {
107 // got a policy? let's set it.
109 UidPolicy policy = new UidPolicy();
110 policy.policy = action ? UidPolicy.ALLOW : UidPolicy.DENY;
111 policy.uid = mCallerUid;
112 // for now just approve all commands, since per command approval is stupid
113 // policy.command = mDesiredCmd;
114 policy.command = null;
115 policy.until = until;
116 policy.desiredUid = mDesiredUid;
117 SuDatabaseHelper.setPolicy(this, policy);
119 // TODO: logging? or should su binary handle that via broadcast?
120 // Probably the latter, so it is consolidated and from the system of record.
122 catch (Exception ex) {
128 protected void onDestroy() {
131 handleAction(false, -1);
136 catch (Exception ex) {
138 new File(mSocketPath).delete();
141 public static final String PERMISSION = "android.permission.ACCESS_SUPERUSER";
143 boolean mRequestReady;
144 void requestReady() {
145 findViewById(R.id.incoming).setVisibility(View.GONE);
146 findViewById(R.id.ready).setVisibility(View.VISIBLE);
148 final View packageInfo = findViewById(R.id.packageinfo);
149 final PackageManager pm = getPackageManager();
150 String[] pkgs = pm.getPackagesForUid(mCallerUid);
151 TextView unknown = (TextView)findViewById(R.id.unknown);
152 unknown.setText(getString(R.string.unknown_uid, mCallerUid));
154 final View appInfo = findViewById(R.id.app_info);
155 appInfo.setOnClickListener(new OnClickListener() {
157 public void onClick(View v) {
158 if (packageInfo.getVisibility() == View.GONE) {
159 appInfo.setVisibility(View.GONE);
160 packageInfo.setVisibility(View.VISIBLE);
165 packageInfo.setOnClickListener(new OnClickListener() {
167 public void onClick(View v) {
168 if (appInfo.getVisibility() == View.GONE) {
169 appInfo.setVisibility(View.VISIBLE);
170 packageInfo.setVisibility(View.GONE);
175 ((TextView)findViewById(R.id.uid_header)).setText(Integer.toString(mDesiredUid));
176 ((TextView)findViewById(R.id.command_header)).setText(mDesiredCmd);
178 boolean superuserDeclared = false;
179 boolean granted = false;
180 if (pkgs != null && pkgs.length > 0) {
181 for (String pkg: pkgs) {
183 PackageInfo pi = pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS);
184 ((TextView)findViewById(R.id.request)).setText(getString(R.string.application_request, pi.applicationInfo.loadLabel(pm)));
185 ImageView icon = (ImageView)packageInfo.findViewById(R.id.image);
186 icon.setImageDrawable(pi.applicationInfo.loadIcon(pm));
187 ((TextView)packageInfo.findViewById(R.id.title)).setText(pi.applicationInfo.loadLabel(pm));
189 ((TextView)findViewById(R.id.app_header)).setText(pi.applicationInfo.loadLabel(pm));
190 ((TextView)findViewById(R.id.package_header)).setText(pi.packageName);
192 if (pi.requestedPermissions != null) {
193 for (String perm: pi.requestedPermissions) {
194 if (PERMISSION.equals(perm)) {
195 superuserDeclared = true;
201 granted |= checkPermission(PERMISSION, mPid, mCallerUid) == PackageManager.PERMISSION_GRANTED;
203 // could display them all, but screw it...
204 // maybe a better ux for this later
207 catch (Exception ex) {
210 findViewById(R.id.unknown).setVisibility(View.GONE);
213 if (!superuserDeclared) {
214 findViewById(R.id.developer_warning).setVisibility(View.VISIBLE);
217 // handle automatic responses
218 // these will be considered permanent user policies
219 // even though they are automatic.
220 // this is so future su requests dont invoke ui
222 // handle declared permission
223 if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !superuserDeclared) {
224 Log.i(LOGTAG, "Automatically denying due to missing permission");
225 mHandler.post(new Runnable() {
229 handleAction(false, 0);
235 // automatic response
236 switch (Settings.getAutomaticResponse(MultitaskSuRequestActivity.this)) {
237 case Settings.AUTOMATIC_RESPONSE_ALLOW:
238 // // automatic response and pin can not be used together
239 // if (Settings.isPinProtected(MultitaskSuRequestActivity.this))
241 // check if the permission must be granted
242 if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !granted)
244 Log.i(LOGTAG, "Automatically allowing due to user preference");
245 mHandler.post(new Runnable() {
249 handleAction(true, 0);
253 case Settings.AUTOMATIC_RESPONSE_DENY:
254 Log.i(LOGTAG, "Automatically denying due to user preference");
255 mHandler.post(new Runnable() {
259 handleAction(false, 0);
267 mAllow.setText(getString(R.string.allow) + " (" + mTimeLeft + ")");
268 if (mTimeLeft-- <= 0) {
269 mAllow.setText(getString(R.string.allow));
271 mAllow.setEnabled(true);
274 mHandler.postDelayed(this, 1000);
279 private final static int SU_PROTOCOL_PARAM_MAX = 20;
280 private final static int SU_PROTOCOL_NAME_MAX = 20;
281 private final static int SU_PROTOCOL_VALUE_MAX_DEFAULT = 256;
282 private final static HashMap<String, Integer> SU_PROTOCOL_VALUE_MAX = new HashMap<String, Integer>() {
284 put("command", 2048);
288 private static int getValueMax(String name) {
289 Integer max = SU_PROTOCOL_VALUE_MAX.get(name);
291 return SU_PROTOCOL_VALUE_MAX_DEFAULT;
295 void manageSocket() {
300 mSocket = new LocalSocket();
301 mSocket.connect(new LocalSocketAddress(mSocketPath, Namespace.FILESYSTEM));
303 DataInputStream is = new DataInputStream(mSocket.getInputStream());
305 ContentValues payload = new ContentValues();
308 for (int i = 0; i < SU_PROTOCOL_PARAM_MAX; i++) {
309 int nameLen = is.readInt();
310 if (nameLen > SU_PROTOCOL_NAME_MAX)
311 throw new IllegalArgumentException("name length too long: " + nameLen);
312 byte[] nameBytes = new byte[nameLen];
313 is.readFully(nameBytes);
314 String name = new String(nameBytes);
315 int dataLen = is.readInt();
316 if (dataLen > getValueMax(name))
317 throw new IllegalArgumentException(name + " data length too long: " + dataLen);
318 byte[] dataBytes = new byte[dataLen];
319 is.readFully(dataBytes);
320 String data = new String(dataBytes);
321 payload.put(name, data);
322 // Log.i(LOGTAG, name);
323 // Log.i(LOGTAG, data);
324 if ("eof".equals(name))
328 int protocolVersion = payload.getAsInteger("version");
329 mCallerUid = payload.getAsInteger("from.uid");
330 mDesiredUid = payload.getAsByte("to.uid");
331 mDesiredCmd = payload.getAsString("command");
332 String calledBin = payload.getAsString("from.bin");
333 mPid = payload.getAsInteger("pid");
334 runOnUiThread(new Runnable() {
337 mRequestReady = true;
343 catch (Exception ex) {
344 Log.i(LOGTAG, ex.getMessage(), ex);
348 catch (Exception e) {
350 runOnUiThread(new Runnable() {
362 RadioGroup mRemember;
366 protected void onCreate(Bundle savedInstanceState) {
367 super.onCreate(savedInstanceState);
369 Intent intent = getIntent();
370 if (intent == null) {
375 mSocketPath = intent.getStringExtra("socket");
376 if (mSocketPath == null) {
386 // watch for the socket disappearing. that means su died.
391 if (!new File(mSocketPath).exists()) {
396 mHandler.postDelayed(this, 1000);
400 mHandler.postDelayed(new Runnable() {
406 handleAction(false, -1);
408 }, Settings.getRequestTimeout(this) * 1000);
412 public void onConfigurationChanged(Configuration newConfig) {
413 super.onConfigurationChanged(newConfig);
418 final int[] mSpinnerIds = new int[] {
419 R.string.this_time_only,
420 R.string.remember_for,
421 R.string.remember_forever
425 mAllow.setEnabled(false);
426 mDeny.setEnabled(false);
427 handleAction(true, null);
431 mAllow.setEnabled(false);
432 mDeny.setEnabled(false);
433 handleAction(false, null);
437 ArrayAdapter<String> mSpinnerAdapter;
438 void setContentView() {
439 setContentView(R.layout.request);
441 mSpinner = (Spinner)findViewById(R.id.remember_choices);
442 mSpinner.setAdapter(mSpinnerAdapter = new ArrayAdapter<String>(this, R.layout.request_spinner_choice, R.id.request_spinner_choice));
443 for (int id: mSpinnerIds) {
444 mSpinnerAdapter.add(getString(id, getGracePeriod()));
447 mRemember = (RadioGroup)findViewById(R.id.remember);
448 RadioButton rememberFor = (RadioButton)findViewById(R.id.remember_for);
449 rememberFor.setText(getString(R.string.remember_for, getGracePeriod()));
451 mAllow = (Button)findViewById(R.id.allow);
452 mDeny = (Button)findViewById(R.id.deny);
454 mAllow.setOnClickListener(new OnClickListener() {
456 public void onClick(View v) {
457 if (!Settings.isPinProtected(MultitaskSuRequestActivity.this)) {
462 ViewGroup ready = (ViewGroup)findViewById(R.id.root);
463 ready.removeAllViews();
465 PinViewHelper pin = new PinViewHelper(getLayoutInflater(), ready, null) {
467 public void onEnter(String password) {
468 super.onEnter(password);
469 if (Settings.checkPin(MultitaskSuRequestActivity.this, password)) {
473 Toast.makeText(MultitaskSuRequestActivity.this, getString(R.string.incorrect_pin), Toast.LENGTH_SHORT).show();
477 public void onCancel() {
483 ready.addView(pin.getView());
486 mDeny.setOnClickListener(new OnClickListener() {
488 public void onClick(View v) {