diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5eade796a..95388009d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,9 +8,6 @@
-
+
+
+
+
getPolicyList(PackageManager pm) {
+ SQLiteDatabase db = getWritableDatabase();
+ ArrayList ret = new ArrayList<>();
+ Policy policy;
+ // Clear outdated policies
+ db.delete(TABLE_NAME, "until > 0 and until < ?", new String[] { String.valueOf(System.currentTimeMillis()) });
+ try (Cursor c = db.query(TABLE_NAME, null, null, null, null, null, null)) {
+ while (c.moveToNext()) {
+ policy = new Policy(c);
+ // Package is uninstalled
+ if (pm.getPackagesForUid(policy.uid) == null)
+ deletePolicy(policy.uid);
+ else
+ ret.add(policy);
+ }
+ }
+ db.close();
+ return ret;
+ }
+}
diff --git a/app/src/main/java/com/topjohnwu/magisk/superuser/SuReceiver.java b/app/src/main/java/com/topjohnwu/magisk/superuser/SuReceiver.java
new file mode 100644
index 000000000..a45a560f9
--- /dev/null
+++ b/app/src/main/java/com/topjohnwu/magisk/superuser/SuReceiver.java
@@ -0,0 +1,49 @@
+package com.topjohnwu.magisk.superuser;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import com.topjohnwu.magisk.R;
+
+public class SuReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int fromUid, toUid, pid;
+ String command, action;
+
+ if (intent == null) return;
+ fromUid = intent.getIntExtra("from.uid", -1);
+ if (fromUid < 0) return;
+ action = intent.getStringExtra("action");
+ if (action == null) return;
+ SuDatabaseHelper dbHelper = new SuDatabaseHelper(context);
+ Policy policy = dbHelper.getPolicy(fromUid);
+ if (policy == null) return;
+ if (policy.notification) {
+ String message;
+ switch (action) {
+ case "allow":
+ message = context.getString(R.string.su_allow_toast, policy.appName);
+ break;
+ case "deny":
+ message = context.getString(R.string.su_deny_toast, policy.appName);
+ break;
+ default:
+ return;
+ }
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ }
+ if (policy.logging) {
+ toUid = intent.getIntExtra("to.uid", -1);
+ if (toUid < 0) return;
+ pid = intent.getIntExtra("pid", -1);
+ if (pid < 0) return;
+ command = intent.getStringExtra("command");
+ if (command == null) return;
+ // TODO: Place info into logs
+ }
+ }
+}
diff --git a/app/src/main/java/com/topjohnwu/magisk/superuser/SuRequestActivity.java b/app/src/main/java/com/topjohnwu/magisk/superuser/SuRequestActivity.java
new file mode 100644
index 000000000..ba8809ba1
--- /dev/null
+++ b/app/src/main/java/com/topjohnwu/magisk/superuser/SuRequestActivity.java
@@ -0,0 +1,163 @@
+package com.topjohnwu.magisk.superuser;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Window;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.topjohnwu.magisk.R;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+
+public class SuRequestActivity extends AppCompatActivity {
+
+ 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 = 256;
+
+ @BindView(R.id.timeout) Spinner timeout;
+ @BindView(R.id.app_icon) ImageView appIcon;
+ @BindView(R.id.app_name) TextView appNameView;
+ @BindView(R.id.package_name) TextView packageNameView;
+ @BindView(R.id.grant_btn) Button grant_btn;
+ @BindView(R.id.deny_btn) Button deny_btn;
+
+ private String socketPath;
+ private LocalSocket socket;
+ private PackageManager pm;
+ private PackageInfo info;
+
+ private int uid;
+ private String appName, packageName;
+
+ private int[] timeoutList = {0, -1, 10, 20, 30, 60};
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ pm = getPackageManager();
+
+ Intent intent = getIntent();
+ socketPath = intent.getStringExtra("socket");
+
+ new SocketManager().execute();
+ }
+
+ void updateUI() throws Throwable {
+ setContentView(R.layout.activity_request);
+ ButterKnife.bind(this);
+ ArrayAdapter adapter = ArrayAdapter.createFromResource(this, R.array.timeout, android.R.layout.simple_spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ timeout.setAdapter(adapter);
+ appIcon.setImageDrawable(info.applicationInfo.loadIcon(pm));
+ appNameView.setText(appName);
+ packageNameView.setText(packageName);
+
+ grant_btn.setOnClickListener(v -> handleAction(true, timeoutList[timeout.getSelectedItemPosition()]));
+ deny_btn.setOnClickListener(v -> handleAction(false, 0));
+ }
+
+ void handleAction(boolean action, int timeout) {
+
+ try {
+ socket.getOutputStream().write((action ? "socket:ALLOW" : "socket:DENY").getBytes());
+ } catch (Exception ignored) {}
+
+ if (timeout >= 0) {
+ Policy policy = new Policy();
+ policy.uid = uid;
+ policy.packageName = packageName;
+ policy.appName = appName;
+ policy.until = (timeout == 0) ? 0 : System.currentTimeMillis() + timeout * 60 * 1000;
+ policy.policy = action ? 2 : 1;
+ policy.logging = true;
+ policy.notification = true;
+ new SuDatabaseHelper(this).addPolicy(policy);
+ }
+
+ finish();
+ }
+
+ private class SocketManager extends AsyncTask {
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ try{
+ socket = new LocalSocket();
+ socket.connect(new LocalSocketAddress(socketPath, LocalSocketAddress.Namespace.FILESYSTEM));
+
+ DataInputStream is = new DataInputStream(socket.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 > SU_PROTOCOL_VALUE_MAX)
+ 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);
+
+ if ("eof".equals(name))
+ break;
+ }
+
+ uid = payload.getAsInteger("uid");
+
+ }catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ try {
+ if (!result) throw new Throwable();
+ String[] pkgs = pm.getPackagesForUid(uid);
+ if (pkgs != null && pkgs.length > 0) {
+ info = pm.getPackageInfo(pkgs[0], 0);
+ packageName = pkgs[0];
+ appName = info.applicationInfo.loadLabel(pm).toString();
+ updateUI();
+ }
+ else
+ throw new Throwable();
+ } catch (Throwable e) {
+ handleAction(false, -1);
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_su_warning.xml b/app/src/main/res/drawable/ic_su_warning.xml
new file mode 100644
index 000000000..87e5f4ac4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_su_warning.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_request.xml b/app/src/main/res/layout/activity_request.xml
new file mode 100644
index 000000000..a7a6b8a84
--- /dev/null
+++ b/app/src/main/res/layout/activity_request.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index e29d5bfa6..72fde488e 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -8,4 +8,12 @@
- @string/theme_default_value
- @string/theme_dark_value
+
+ - @string/forever
+ - @string/once
+ - @string/tenmin
+ - @string/twentymin
+ - @string/thirtymin
+ - @string/sixtymin
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 21e346955..cef57fb62 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -20,4 +20,6 @@
#dedede
#313131
+ #e0e0e0
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fd85acf62..bf792072b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -143,5 +143,19 @@
default
dark
+
+ Superuser Request
+ Deny
+ Grant
+ Grants full access to your device.\nDeny if you\'re not sure!
+ Forever
+ Once
+ 10 min
+ 20 min
+ 30 min
+ 60 min
+ %1$s is granted Superuser permissions
+ %1$s is denied Superuser permissions
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index c97efd694..c7224cde4 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -121,4 +121,9 @@
- true
+
+