OSDN Git Service

LICENSE
[android-x86/external-koush-Superuser.git] / Superuser / src / com / koushikdutta / superuser / MultitaskSuRequestActivity.java
1 /*
2  * Copyright (C) 2013 Koushik Dutta (@koush)
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.koushikdutta.superuser;
18
19 import java.io.DataInputStream;
20 import java.io.File;
21 import java.util.HashMap;
22
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;
49
50 import com.koushikdutta.superuser.db.SuDatabaseHelper;
51 import com.koushikdutta.superuser.db.UidPolicy;
52 import com.koushikdutta.superuser.util.Settings;
53
54 @SuppressLint("ValidFragment")
55 public class MultitaskSuRequestActivity extends FragmentActivity {
56     private static final String LOGTAG = "Superuser";
57     int mCallerUid;
58     int mDesiredUid;
59     String mDesiredCmd;
60     int mPid;
61     
62     Spinner mSpinner;
63     
64     Handler mHandler = new Handler();
65     
66     int mTimeLeft = 3;
67     
68     Button mAllow;
69     Button mDeny;
70
71     boolean mHandled;
72     
73     public int getGracePeriod() {
74         return 10;
75     }
76     
77     void handleAction(boolean action, Integer until) {
78         Assert.assertTrue(!mHandled);
79         mHandled = true;
80         try {
81             mSocket.getOutputStream().write((action ? "socket:ALLOW" : "socket:DENY").getBytes());
82         }
83         catch (Exception ex) {
84         }
85         try {
86             if (until == null) {
87                 until = -1;
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;
93                     }
94                     else if (id == R.string.remember_forever) {
95                         until = 0;
96                     }
97                 }
98                 else if (mRemember.isShown()) {
99                     if (mRemember.getCheckedRadioButtonId() == R.id.remember_for) {
100                         until = (int)(System.currentTimeMillis() / 1000) + getGracePeriod() * 60;
101                     }
102                     else if (mRemember.getCheckedRadioButtonId() == R.id.remember_forever) {
103                         until = 0;
104                     }
105                 }
106             }
107             // got a policy? let's set it.
108             if (until != -1) {
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);
118             }
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.
121         }
122         catch (Exception ex) {
123         }
124         finish();
125     }
126     
127     @Override
128     protected void onDestroy() {
129         super.onDestroy();
130         if (!mHandled)
131             handleAction(false, -1);
132         try {
133             if (mSocket != null)
134                 mSocket.close();
135         }
136         catch (Exception ex) {
137         }
138         new File(mSocketPath).delete();
139     }
140
141     public static final String PERMISSION = "android.permission.ACCESS_SUPERUSER";
142     
143     boolean mRequestReady;
144     void requestReady() {
145         findViewById(R.id.incoming).setVisibility(View.GONE);
146         findViewById(R.id.ready).setVisibility(View.VISIBLE);
147         
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));
153
154         final View appInfo = findViewById(R.id.app_info);
155         appInfo.setOnClickListener(new OnClickListener() {
156             @Override
157             public void onClick(View v) {
158                 if (packageInfo.getVisibility() == View.GONE) {
159                     appInfo.setVisibility(View.GONE);
160                     packageInfo.setVisibility(View.VISIBLE);
161                 }
162             }
163         });
164         
165         packageInfo.setOnClickListener(new OnClickListener() {
166             @Override
167             public void onClick(View v) {
168                 if (appInfo.getVisibility() == View.GONE) {
169                     appInfo.setVisibility(View.VISIBLE);
170                     packageInfo.setVisibility(View.GONE);
171                 }
172             }
173         });
174         
175         ((TextView)findViewById(R.id.uid_header)).setText(Integer.toString(mDesiredUid));
176         ((TextView)findViewById(R.id.command_header)).setText(mDesiredCmd);
177
178         boolean superuserDeclared = false;
179         boolean granted = false;
180         if (pkgs != null && pkgs.length > 0) {
181             for (String pkg: pkgs) {
182                 try {
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));
188                     
189                     ((TextView)findViewById(R.id.app_header)).setText(pi.applicationInfo.loadLabel(pm));
190                     ((TextView)findViewById(R.id.package_header)).setText(pi.packageName);
191                     
192                     if (pi.requestedPermissions != null) {
193                         for (String perm: pi.requestedPermissions) {
194                             if (PERMISSION.equals(perm)) {
195                                 superuserDeclared = true;
196                                 break;
197                             }
198                         }
199                     }
200                     
201                     granted |= checkPermission(PERMISSION, mPid, mCallerUid) == PackageManager.PERMISSION_GRANTED;
202                     
203                     // could display them all, but screw it...
204                     // maybe a better ux for this later
205                     break;
206                 }
207                 catch (Exception ex) {
208                 }
209             }
210             findViewById(R.id.unknown).setVisibility(View.GONE);
211         }
212         
213         if (!superuserDeclared) {
214             findViewById(R.id.developer_warning).setVisibility(View.VISIBLE);
215         }
216         
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
221
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() {
226                 @Override
227                 public void run() {
228                     if (!mHandled)
229                         handleAction(false, 0);
230                 }
231             });
232             return;
233         }
234
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))
240 //                break;
241             // check if the permission must be granted 
242             if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !granted)
243                 break;
244             Log.i(LOGTAG, "Automatically allowing due to user preference");
245             mHandler.post(new Runnable() {
246                 @Override
247                 public void run() {
248                     if (!mHandled)
249                         handleAction(true, 0);
250                 }
251             });
252             return;
253         case Settings.AUTOMATIC_RESPONSE_DENY:
254             Log.i(LOGTAG, "Automatically denying due to user preference");
255             mHandler.post(new Runnable() {
256                 @Override
257                 public void run() {
258                     if (!mHandled)
259                         handleAction(false, 0);
260                 }
261             });
262             return;
263         }
264         
265         new Runnable() {
266             public void run() {
267                 mAllow.setText(getString(R.string.allow) + " (" + mTimeLeft + ")");
268                 if (mTimeLeft-- <= 0) {
269                     mAllow.setText(getString(R.string.allow));
270                     if (!mHandled)
271                         mAllow.setEnabled(true);
272                     return;
273                 }
274                 mHandler.postDelayed(this, 1000);
275             };
276         }.run();
277     }
278
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>() {
283         {
284             put("command", 2048);
285         }
286     };
287     
288     private static int getValueMax(String name) {
289         Integer max = SU_PROTOCOL_VALUE_MAX.get(name);
290         if (max == null)
291             return SU_PROTOCOL_VALUE_MAX_DEFAULT;
292         return max;
293     }
294     
295     void manageSocket() {
296         new Thread() {
297             @Override
298             public void run() {
299                 try {
300                     mSocket = new LocalSocket();
301                     mSocket.connect(new LocalSocketAddress(mSocketPath, Namespace.FILESYSTEM));
302
303                     DataInputStream is = new DataInputStream(mSocket.getInputStream());
304                     
305                     ContentValues payload = new ContentValues();
306
307
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))
325                             break;
326                     }
327                     
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() {
335                         @Override
336                         public void run() {
337                             mRequestReady = true;
338                             requestReady();
339                         }
340                     });                    
341                     
342                 }
343                 catch (Exception ex) {
344                     Log.i(LOGTAG, ex.getMessage(), ex);
345                     try {
346                         mSocket.close();
347                     }
348                     catch (Exception e) {
349                     }
350                     runOnUiThread(new Runnable() {
351                         @Override
352                         public void run() {
353                             finish();
354                         }
355                     });
356                 }
357             }
358         }.start();
359     }
360     
361
362     RadioGroup mRemember;
363     
364     LocalSocket mSocket;
365     @Override
366     protected void onCreate(Bundle savedInstanceState) {
367         super.onCreate(savedInstanceState);
368
369         Intent intent = getIntent();
370         if (intent == null) {
371             finish();
372             return;
373         }
374
375         mSocketPath = intent.getStringExtra("socket");
376         if (mSocketPath == null) {
377             finish();
378             return;
379         }
380
381         setContentView();
382
383         manageSocket();
384         
385         
386         // watch for the socket disappearing. that means su died.
387         new Runnable() {
388             public void run() {
389                 if (isFinishing())
390                     return;
391                 if (!new File(mSocketPath).exists()) {
392                     finish();
393                     return;
394                 }
395                 
396                 mHandler.postDelayed(this, 1000);
397             };
398         }.run();
399         
400         mHandler.postDelayed(new Runnable() {
401             @Override
402             public void run() {
403                 if (isFinishing())
404                     return;
405                 if (!mHandled)
406                     handleAction(false, -1);
407             }
408         }, Settings.getRequestTimeout(this) * 1000);
409     }
410     
411     @Override
412     public void onConfigurationChanged(Configuration newConfig) {
413         super.onConfigurationChanged(newConfig);
414         
415         setContentView();
416     }
417     
418     final int[] mSpinnerIds = new int[] {
419             R.string.this_time_only,
420             R.string.remember_for,
421             R.string.remember_forever
422     };
423     
424     void approve() {
425         mAllow.setEnabled(false);
426         mDeny.setEnabled(false);
427         handleAction(true, null);
428     }
429
430     void deny() {
431         mAllow.setEnabled(false);
432         mDeny.setEnabled(false);
433         handleAction(false, null);
434     }
435
436     String mSocketPath;
437     ArrayAdapter<String> mSpinnerAdapter;
438     void setContentView() {
439         setContentView(R.layout.request);
440
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()));
445         }
446         
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()));
450
451         mAllow = (Button)findViewById(R.id.allow);
452         mDeny = (Button)findViewById(R.id.deny);
453         
454         mAllow.setOnClickListener(new OnClickListener() {
455             @Override
456             public void onClick(View v) {
457                 if (!Settings.isPinProtected(MultitaskSuRequestActivity.this)) {
458                     approve();
459                     return;
460                 }
461                 
462                 ViewGroup ready = (ViewGroup)findViewById(R.id.root);
463                 ready.removeAllViews();
464                 
465                 PinViewHelper pin = new PinViewHelper(getLayoutInflater(), ready, null) {
466                     @Override
467                     public void onEnter(String password) {
468                         super.onEnter(password);
469                         if (Settings.checkPin(MultitaskSuRequestActivity.this, password)) {
470                             approve();
471                         }
472                         else {
473                             Toast.makeText(MultitaskSuRequestActivity.this, getString(R.string.incorrect_pin), Toast.LENGTH_SHORT).show();
474                         }
475                     }
476                     @Override
477                     public void onCancel() {
478                         super.onCancel();
479                         deny();
480                     }
481                 };
482                 
483                 ready.addView(pin.getView());
484             }
485         });
486         mDeny.setOnClickListener(new OnClickListener() {
487             @Override
488             public void onClick(View v) {
489                 deny();
490             }
491         });
492         
493         if (mRequestReady)
494             requestReady();
495     }
496 }