Java tutorial
/* * Copyright (C) 2013 Koushik Dutta (@koush) * * 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 com.koushikdutta.superuser; import java.io.DataInputStream; import java.io.File; import java.util.HashMap; import junit.framework.Assert; import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.LocalSocket; import android.net.LocalSocketAddress; import android.net.LocalSocketAddress.Namespace; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.FragmentActivity; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageView; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import com.koushikdutta.superuser.db.SuDatabaseHelper; import com.koushikdutta.superuser.db.UidPolicy; import com.koushikdutta.superuser.util.Settings; import com.koushikdutta.superuser.util.SuHelper; @SuppressLint("ValidFragment") public class MultitaskSuRequestActivity extends FragmentActivity { private static final String LOGTAG = "Superuser"; int mCallerUid; int mDesiredUid; String mDesiredCmd; int mPid; Spinner mSpinner; Handler mHandler = new Handler(); int mTimeLeft = 3; Button mAllow; Button mDeny; boolean mHandled; public int getGracePeriod() { return 10; } int getUntil() { int until = -1; if (mSpinner.isShown()) { int pos = mSpinner.getSelectedItemPosition(); int id = mSpinnerIds[pos]; if (id == R.string.remember_for) { until = (int) (System.currentTimeMillis() / 1000) + getGracePeriod() * 60; } else if (id == R.string.remember_forever) { until = 0; } } else if (mRemember.isShown()) { if (mRemember.getCheckedRadioButtonId() == R.id.remember_for) { until = (int) (System.currentTimeMillis() / 1000) + getGracePeriod() * 60; } else if (mRemember.getCheckedRadioButtonId() == R.id.remember_forever) { until = 0; } } return until; } void handleAction(boolean action, Integer until) { Assert.assertTrue(!mHandled); mHandled = true; try { mSocket.getOutputStream().write((action ? "socket:ALLOW" : "socket:DENY").getBytes()); } catch (Exception ex) { } try { if (until == null) { until = getUntil(); } // got a policy? let's set it. if (until != -1) { UidPolicy policy = new UidPolicy(); policy.policy = action ? UidPolicy.ALLOW : UidPolicy.DENY; policy.uid = mCallerUid; // for now just approve all commands, since per command approval is stupid // policy.command = mDesiredCmd; policy.command = null; policy.until = until; policy.desiredUid = mDesiredUid; SuDatabaseHelper.setPolicy(this, policy); } // TODO: logging? or should su binary handle that via broadcast? // Probably the latter, so it is consolidated and from the system of record. } catch (Exception ex) { } finish(); } @Override protected void onDestroy() { super.onDestroy(); if (!mHandled) handleAction(false, -1); try { if (mSocket != null) mSocket.close(); } catch (Exception ex) { } new File(mSocketPath).delete(); } public static final String PERMISSION = "android.permission.ACCESS_SUPERUSER"; boolean mRequestReady; void requestReady() { findViewById(R.id.incoming).setVisibility(View.GONE); findViewById(R.id.ready).setVisibility(View.VISIBLE); final View packageInfo = findViewById(R.id.packageinfo); final PackageManager pm = getPackageManager(); String[] pkgs = pm.getPackagesForUid(mCallerUid); TextView unknown = (TextView) findViewById(R.id.unknown); unknown.setText(getString(R.string.unknown_uid, mCallerUid)); final View appInfo = findViewById(R.id.app_info); appInfo.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (packageInfo.getVisibility() == View.GONE) { appInfo.setVisibility(View.GONE); packageInfo.setVisibility(View.VISIBLE); } } }); packageInfo.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (appInfo.getVisibility() == View.GONE) { appInfo.setVisibility(View.VISIBLE); packageInfo.setVisibility(View.GONE); } } }); ((TextView) findViewById(R.id.uid_header)).setText(Integer.toString(mDesiredUid)); ((TextView) findViewById(R.id.command_header)).setText(mDesiredCmd); boolean superuserDeclared = false; boolean granted = false; if (pkgs != null && pkgs.length > 0) { for (String pkg : pkgs) { try { PackageInfo pi = pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS); ((TextView) findViewById(R.id.request)) .setText(getString(R.string.application_request, pi.applicationInfo.loadLabel(pm))); ImageView icon = (ImageView) packageInfo.findViewById(R.id.image); icon.setImageDrawable(pi.applicationInfo.loadIcon(pm)); ((TextView) packageInfo.findViewById(R.id.title)).setText(pi.applicationInfo.loadLabel(pm)); ((TextView) findViewById(R.id.app_header)).setText(pi.applicationInfo.loadLabel(pm)); ((TextView) findViewById(R.id.package_header)).setText(pi.packageName); if (pi.requestedPermissions != null) { for (String perm : pi.requestedPermissions) { if (PERMISSION.equals(perm)) { superuserDeclared = true; break; } } } granted |= checkPermission(PERMISSION, mPid, mCallerUid) == PackageManager.PERMISSION_GRANTED; // could display them all, but screw it... // maybe a better ux for this later break; } catch (Exception ex) { } } findViewById(R.id.unknown).setVisibility(View.GONE); } if (!superuserDeclared) { findViewById(R.id.developer_warning).setVisibility(View.VISIBLE); } // handle automatic responses // these will be considered permanent user policies // even though they are automatic. // this is so future su requests dont invoke ui // handle declared permission if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !superuserDeclared) { Log.i(LOGTAG, "Automatically denying due to missing permission"); mHandler.post(new Runnable() { @Override public void run() { if (!mHandled) handleAction(false, 0); } }); return; } // automatic response switch (Settings.getAutomaticResponse(MultitaskSuRequestActivity.this)) { case Settings.AUTOMATIC_RESPONSE_ALLOW: // // automatic response and pin can not be used together // if (Settings.isPinProtected(MultitaskSuRequestActivity.this)) // break; // check if the permission must be granted if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !granted) break; Log.i(LOGTAG, "Automatically allowing due to user preference"); mHandler.post(new Runnable() { @Override public void run() { if (!mHandled) handleAction(true, 0); } }); return; case Settings.AUTOMATIC_RESPONSE_DENY: Log.i(LOGTAG, "Automatically denying due to user preference"); mHandler.post(new Runnable() { @Override public void run() { if (!mHandled) handleAction(false, 0); } }); return; } new Runnable() { public void run() { mAllow.setText(getString(R.string.allow) + " (" + mTimeLeft + ")"); if (mTimeLeft-- <= 0) { mAllow.setText(getString(R.string.allow)); if (!mHandled) mAllow.setEnabled(true); return; } mHandler.postDelayed(this, 1000); }; }.run(); } private final static int SU_PROTOCOL_PARAM_MAX = 20; private final static int SU_PROTOCOL_NAME_MAX = 20; private final static int SU_PROTOCOL_VALUE_MAX_DEFAULT = 256; private final static HashMap<String, Integer> SU_PROTOCOL_VALUE_MAX = new HashMap<String, Integer>() { { put("command", 2048); } }; private static int getValueMax(String name) { Integer max = SU_PROTOCOL_VALUE_MAX.get(name); if (max == null) return SU_PROTOCOL_VALUE_MAX_DEFAULT; return max; } void manageSocket() { new Thread() { @Override public void run() { try { mSocket = new LocalSocket(); mSocket.connect(new LocalSocketAddress(mSocketPath, Namespace.FILESYSTEM)); DataInputStream is = new DataInputStream(mSocket.getInputStream()); ContentValues payload = new ContentValues(); for (int i = 0; i < SU_PROTOCOL_PARAM_MAX; i++) { int nameLen = is.readInt(); if (nameLen > SU_PROTOCOL_NAME_MAX) throw new IllegalArgumentException("name length too long: " + nameLen); byte[] nameBytes = new byte[nameLen]; is.readFully(nameBytes); String name = new String(nameBytes); int dataLen = is.readInt(); if (dataLen > getValueMax(name)) throw new IllegalArgumentException(name + " data length too long: " + dataLen); byte[] dataBytes = new byte[dataLen]; is.readFully(dataBytes); String data = new String(dataBytes); payload.put(name, data); // Log.i(LOGTAG, name); // Log.i(LOGTAG, data); if ("eof".equals(name)) break; } int protocolVersion = payload.getAsInteger("version"); mCallerUid = payload.getAsInteger("from.uid"); mDesiredUid = payload.getAsByte("to.uid"); mDesiredCmd = payload.getAsString("command"); String calledBin = payload.getAsString("from.bin"); mPid = payload.getAsInteger("pid"); runOnUiThread(new Runnable() { @Override public void run() { mRequestReady = true; requestReady(); } }); if ("com.koushikdutta.superuser".equals(getPackageName())) { if (!SuHelper.CURRENT_VERSION.equals(payload.getAsString("binary.version"))) SuCheckerReceiver.doNotification(MultitaskSuRequestActivity.this); } } catch (Exception ex) { Log.i(LOGTAG, ex.getMessage(), ex); try { mSocket.close(); } catch (Exception e) { } runOnUiThread(new Runnable() { @Override public void run() { finish(); } }); } } }.start(); } RadioGroup mRemember; LocalSocket mSocket; @Override protected void onCreate(Bundle savedInstanceState) { Settings.applyDarkThemeSetting(this, R.style.RequestThemeDark); super.onCreate(savedInstanceState); Intent intent = getIntent(); if (intent == null) { finish(); return; } mSocketPath = intent.getStringExtra("socket"); if (mSocketPath == null) { finish(); return; } setContentView(); manageSocket(); // watch for the socket disappearing. that means su died. new Runnable() { public void run() { if (isFinishing()) return; if (!new File(mSocketPath).exists()) { finish(); return; } mHandler.postDelayed(this, 1000); }; }.run(); mHandler.postDelayed(new Runnable() { @Override public void run() { if (isFinishing()) return; if (!mHandled) handleAction(false, -1); } }, Settings.getRequestTimeout(this) * 1000); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setContentView(); } final int[] mSpinnerIds = new int[] { R.string.this_time_only, R.string.remember_for, R.string.remember_forever }; void approve() { mAllow.setEnabled(false); mDeny.setEnabled(false); handleAction(true, null); } void deny() { mAllow.setEnabled(false); mDeny.setEnabled(false); handleAction(false, null); } String mSocketPath; ArrayAdapter<String> mSpinnerAdapter; void setContentView() { setContentView(R.layout.request); mSpinner = (Spinner) findViewById(R.id.remember_choices); mSpinner.setAdapter(mSpinnerAdapter = new ArrayAdapter<String>(this, R.layout.request_spinner_choice, R.id.request_spinner_choice)); for (int id : mSpinnerIds) { mSpinnerAdapter.add(getString(id, getGracePeriod())); } mRemember = (RadioGroup) findViewById(R.id.remember); RadioButton rememberFor = (RadioButton) findViewById(R.id.remember_for); rememberFor.setText(getString(R.string.remember_for, getGracePeriod())); mAllow = (Button) findViewById(R.id.allow); mDeny = (Button) findViewById(R.id.deny); mAllow.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (!Settings.isPinProtected(MultitaskSuRequestActivity.this)) { approve(); return; } ViewGroup ready = (ViewGroup) findViewById(R.id.root); final int until = getUntil(); ready.removeAllViews(); PinViewHelper pin = new PinViewHelper(getLayoutInflater(), (ViewGroup) findViewById(android.R.id.content), null) { @Override public void onEnter(String password) { super.onEnter(password); if (Settings.checkPin(MultitaskSuRequestActivity.this, password)) { mAllow.setEnabled(false); mDeny.setEnabled(false); handleAction(true, until); } else { Toast.makeText(MultitaskSuRequestActivity.this, getString(R.string.incorrect_pin), Toast.LENGTH_SHORT).show(); } } @Override public void onCancel() { super.onCancel(); deny(); } }; ready.addView(pin.getView()); } }); mDeny.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { deny(); } }); if (mRequestReady) requestReady(); } }