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.util.Log;
37 import android.view.View;
38 import android.view.View.OnClickListener;
39 import android.view.ViewGroup;
40 import android.widget.ArrayAdapter;
41 import android.widget.Button;
42 import android.widget.ImageView;
43 import android.widget.RadioButton;
44 import android.widget.RadioGroup;
45 import android.widget.Spinner;
46 import android.widget.TextView;
47 import android.widget.Toast;
49 import com.koushikdutta.superuser.db.SuDatabaseHelper;
50 import com.koushikdutta.superuser.db.UidPolicy;
51 import com.koushikdutta.superuser.util.Settings;
52 import com.koushikdutta.superuser.util.SuHelper;
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() {
79 if (mSpinner.isShown()) {
80 int pos = mSpinner.getSelectedItemPosition();
81 int id = mSpinnerIds[pos];
82 if (id == R.string.remember_for) {
83 until = (int)(System.currentTimeMillis() / 1000) + getGracePeriod() * 60;
85 else if (id == R.string.remember_forever) {
89 else if (mRemember.isShown()) {
90 if (mRemember.getCheckedRadioButtonId() == R.id.remember_for) {
91 until = (int)(System.currentTimeMillis() / 1000) + getGracePeriod() * 60;
93 else if (mRemember.getCheckedRadioButtonId() == R.id.remember_forever) {
100 void handleAction(boolean action, Integer until) {
101 Assert.assertTrue(!mHandled);
104 mSocket.getOutputStream().write((action ? "socket:ALLOW" : "socket:DENY").getBytes());
106 catch (Exception ex) {
112 // got a policy? let's set it.
114 UidPolicy policy = new UidPolicy();
115 policy.policy = action ? UidPolicy.ALLOW : UidPolicy.DENY;
116 policy.uid = mCallerUid;
117 // for now just approve all commands, since per command approval is stupid
118 // policy.command = mDesiredCmd;
119 policy.command = null;
120 policy.until = until;
121 policy.desiredUid = mDesiredUid;
122 SuDatabaseHelper.setPolicy(this, policy);
124 // TODO: logging? or should su binary handle that via broadcast?
125 // Probably the latter, so it is consolidated and from the system of record.
127 catch (Exception ex) {
133 protected void onDestroy() {
136 handleAction(false, -1);
141 catch (Exception ex) {
143 new File(mSocketPath).delete();
146 public static final String PERMISSION = "android.permission.ACCESS_SUPERUSER";
148 boolean mRequestReady;
149 void requestReady() {
150 findViewById(R.id.incoming).setVisibility(View.GONE);
151 findViewById(R.id.ready).setVisibility(View.VISIBLE);
153 final View packageInfo = findViewById(R.id.packageinfo);
154 final PackageManager pm = getPackageManager();
155 String[] pkgs = pm.getPackagesForUid(mCallerUid);
156 TextView unknown = (TextView)findViewById(R.id.unknown);
157 unknown.setText(getString(R.string.unknown_uid, mCallerUid));
159 final View appInfo = findViewById(R.id.app_info);
160 appInfo.setOnClickListener(new OnClickListener() {
162 public void onClick(View v) {
163 if (packageInfo.getVisibility() == View.GONE) {
164 appInfo.setVisibility(View.GONE);
165 packageInfo.setVisibility(View.VISIBLE);
170 packageInfo.setOnClickListener(new OnClickListener() {
172 public void onClick(View v) {
173 if (appInfo.getVisibility() == View.GONE) {
174 appInfo.setVisibility(View.VISIBLE);
175 packageInfo.setVisibility(View.GONE);
180 ((TextView)findViewById(R.id.uid_header)).setText(Integer.toString(mDesiredUid));
181 ((TextView)findViewById(R.id.command_header)).setText(mDesiredCmd);
183 boolean superuserDeclared = false;
184 boolean granted = false;
185 if (pkgs != null && pkgs.length > 0) {
186 for (String pkg: pkgs) {
188 PackageInfo pi = pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS);
189 ((TextView)findViewById(R.id.request)).setText(getString(R.string.application_request, pi.applicationInfo.loadLabel(pm)));
190 ImageView icon = (ImageView)packageInfo.findViewById(R.id.image);
191 icon.setImageDrawable(pi.applicationInfo.loadIcon(pm));
192 ((TextView)packageInfo.findViewById(R.id.title)).setText(pi.applicationInfo.loadLabel(pm));
194 ((TextView)findViewById(R.id.app_header)).setText(pi.applicationInfo.loadLabel(pm));
195 ((TextView)findViewById(R.id.package_header)).setText(pi.packageName);
197 if (pi.requestedPermissions != null) {
198 for (String perm: pi.requestedPermissions) {
199 if (PERMISSION.equals(perm)) {
200 superuserDeclared = true;
206 granted |= checkPermission(PERMISSION, mPid, mCallerUid) == PackageManager.PERMISSION_GRANTED;
208 // could display them all, but screw it...
209 // maybe a better ux for this later
212 catch (Exception ex) {
215 findViewById(R.id.unknown).setVisibility(View.GONE);
218 if (!superuserDeclared) {
219 findViewById(R.id.developer_warning).setVisibility(View.VISIBLE);
222 // handle automatic responses
223 // these will be considered permanent user policies
224 // even though they are automatic.
225 // this is so future su requests dont invoke ui
227 // handle declared permission
228 if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !superuserDeclared) {
229 Log.i(LOGTAG, "Automatically denying due to missing permission");
230 mHandler.post(new Runnable() {
234 handleAction(false, 0);
240 // automatic response
241 switch (Settings.getAutomaticResponse(MultitaskSuRequestActivity.this)) {
242 case Settings.AUTOMATIC_RESPONSE_ALLOW:
243 // // automatic response and pin can not be used together
244 // if (Settings.isPinProtected(MultitaskSuRequestActivity.this))
246 // check if the permission must be granted
247 if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !granted)
249 Log.i(LOGTAG, "Automatically allowing due to user preference");
250 mHandler.post(new Runnable() {
254 handleAction(true, 0);
258 case Settings.AUTOMATIC_RESPONSE_DENY:
259 Log.i(LOGTAG, "Automatically denying due to user preference");
260 mHandler.post(new Runnable() {
264 handleAction(false, 0);
272 String allow = getString(R.string.allow);
273 mAllow.setText(getString(R.string.allow_countdown, allow, mTimeLeft));
274 if (mTimeLeft-- <= 0) {
275 mAllow.setText(allow);
277 mAllow.setEnabled(true);
280 mHandler.postDelayed(this, 1000);
285 private final static int SU_PROTOCOL_PARAM_MAX = 20;
286 private final static int SU_PROTOCOL_NAME_MAX = 20;
287 private final static int SU_PROTOCOL_VALUE_MAX_DEFAULT = 256;
288 private final static HashMap<String, Integer> SU_PROTOCOL_VALUE_MAX = new HashMap<String, Integer>() {
292 private static final long serialVersionUID = 5649873127008413475L;
295 put("command", 2048);
299 private static int getValueMax(String name) {
300 Integer max = SU_PROTOCOL_VALUE_MAX.get(name);
302 return SU_PROTOCOL_VALUE_MAX_DEFAULT;
306 void manageSocket() {
311 mSocket = new LocalSocket();
312 mSocket.connect(new LocalSocketAddress(mSocketPath, Namespace.FILESYSTEM));
314 DataInputStream is = new DataInputStream(mSocket.getInputStream());
316 ContentValues payload = new ContentValues();
319 for (int i = 0; i < SU_PROTOCOL_PARAM_MAX; i++) {
320 int nameLen = is.readInt();
321 if (nameLen > SU_PROTOCOL_NAME_MAX)
322 throw new IllegalArgumentException("name length too long: " + nameLen);
323 byte[] nameBytes = new byte[nameLen];
324 is.readFully(nameBytes);
325 String name = new String(nameBytes);
326 int dataLen = is.readInt();
327 if (dataLen > getValueMax(name))
328 throw new IllegalArgumentException(name + " data length too long: " + dataLen);
329 byte[] dataBytes = new byte[dataLen];
330 is.readFully(dataBytes);
331 String data = new String(dataBytes);
332 payload.put(name, data);
333 // Log.i(LOGTAG, name);
334 // Log.i(LOGTAG, data);
335 if ("eof".equals(name))
339 //int protocolVersion = payload.getAsInteger("version");
340 mCallerUid = payload.getAsInteger("from.uid");
341 mDesiredUid = payload.getAsByte("to.uid");
342 mDesiredCmd = payload.getAsString("command");
343 //String calledBin = payload.getAsString("from.bin");
344 mPid = payload.getAsInteger("pid");
345 runOnUiThread(new Runnable() {
348 mRequestReady = true;
353 if ("com.koushikdutta.superuser".equals(getPackageName())) {
354 if (!SuHelper.CURRENT_VERSION.equals(payload.getAsString("binary.version")))
355 SuCheckerReceiver.doNotification(MultitaskSuRequestActivity.this);
358 catch (Exception ex) {
359 Log.i(LOGTAG, ex.getMessage(), ex);
363 catch (Exception e) {
365 runOnUiThread(new Runnable() {
377 RadioGroup mRemember;
381 protected void onCreate(Bundle savedInstanceState) {
382 Settings.applyDarkThemeSetting(this, R.style.RequestThemeDark);
383 super.onCreate(savedInstanceState);
385 Intent intent = getIntent();
386 if (intent == null) {
391 mSocketPath = intent.getStringExtra("socket");
392 if (mSocketPath == null) {
402 // watch for the socket disappearing. that means su died.
407 if (!new File(mSocketPath).exists()) {
412 mHandler.postDelayed(this, 1000);
416 mHandler.postDelayed(new Runnable() {
422 handleAction(false, -1);
424 }, Settings.getRequestTimeout(this) * 1000);
428 public void onConfigurationChanged(Configuration newConfig) {
429 super.onConfigurationChanged(newConfig);
434 final int[] mSpinnerIds = new int[] {
435 R.string.this_time_only,
436 R.string.remember_for,
437 R.string.remember_forever
441 mAllow.setEnabled(false);
442 mDeny.setEnabled(false);
443 handleAction(true, null);
447 mAllow.setEnabled(false);
448 mDeny.setEnabled(false);
449 handleAction(false, null);
453 ArrayAdapter<String> mSpinnerAdapter;
454 void setContentView() {
455 setContentView(R.layout.request);
457 mSpinner = (Spinner)findViewById(R.id.remember_choices);
458 mSpinner.setAdapter(mSpinnerAdapter = new ArrayAdapter<String>(this, R.layout.request_spinner_choice, R.id.request_spinner_choice));
459 for (int id: mSpinnerIds) {
460 mSpinnerAdapter.add(getString(id, getGracePeriod()));
463 mRemember = (RadioGroup)findViewById(R.id.remember);
464 RadioButton rememberFor = (RadioButton)findViewById(R.id.remember_for);
465 rememberFor.setText(getString(R.string.remember_for, getGracePeriod()));
467 mAllow = (Button)findViewById(R.id.allow);
468 mDeny = (Button)findViewById(R.id.deny);
470 mAllow.setOnClickListener(new OnClickListener() {
472 public void onClick(View v) {
473 if (!Settings.isPinProtected(MultitaskSuRequestActivity.this)) {
478 ViewGroup ready = (ViewGroup)findViewById(R.id.root);
479 final int until = getUntil();
480 ready.removeAllViews();
482 PinViewHelper pin = new PinViewHelper(getLayoutInflater(), (ViewGroup)findViewById(android.R.id.content), null) {
484 public void onEnter(String password) {
485 super.onEnter(password);
486 if (Settings.checkPin(MultitaskSuRequestActivity.this, password)) {
487 mAllow.setEnabled(false);
488 mDeny.setEnabled(false);
489 handleAction(true, until);
492 Toast.makeText(MultitaskSuRequestActivity.this, getString(R.string.incorrect_pin), Toast.LENGTH_SHORT).show();
496 public void onCancel() {
502 ready.addView(pin.getView());
505 mDeny.setOnClickListener(new OnClickListener() {
507 public void onClick(View v) {