Magisk Manager is now a SU client

1. Add request popup
2. Add su request notifications
3. Add su database helpers
This commit is contained in:
topjohnwu 2017-01-24 14:19:28 +08:00
parent cf16fd0104
commit 0bd0eb9e59
12 changed files with 505 additions and 3 deletions

View File

@ -8,9 +8,6 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<application
android:allowBackup="true"
@ -41,6 +38,20 @@
<activity
android:name=".SettingsActivity"
android:theme="@style/AppTheme.Transparent" />
<activity
android:name=".superuser.RequestActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity="internal.superuser"
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".superuser.SuRequestActivity"
android:excludeFromRecents="true"
android:taskAffinity="internal.superuser"
android:theme="@style/SuRequest" />
<receiver
android:name=".superuser.SuReceiver" />
<provider
android:name="android.support.v4.content.FileProvider"

View File

@ -0,0 +1,36 @@
package com.topjohnwu.magisk.superuser;
import android.content.ContentValues;
import android.database.Cursor;
public class Policy {
public int uid, policy;
public long until;
public boolean logging, notification;
public String packageName, appName;
public Policy() {}
public Policy(Cursor c) {
uid = c.getInt(c.getColumnIndex("uid"));
packageName = c.getString(c.getColumnIndex("package_name"));
appName = c.getString(c.getColumnIndex("app_name"));
policy = c.getInt(c.getColumnIndex("policy"));
until = c.getLong(c.getColumnIndex("until"));
logging = c.getInt(c.getColumnIndex("logging")) != 0;
notification = c.getInt(c.getColumnIndex("notification")) != 0;
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("uid", uid);
values.put("package_name", packageName);
values.put("app_name", appName);
values.put("policy",policy);
values.put("until", until);
values.put("logging", logging ? 1 : 0);
values.put("notification", notification ? 1 : 0);
return values;
}
}

View File

@ -0,0 +1,23 @@
package com.topjohnwu.magisk.superuser;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
public class RequestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent == null) {
finish();
return;
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClass(this, SuRequestActivity.class);
startActivity(intent);
finish();
}
}

View File

@ -0,0 +1,78 @@
package com.topjohnwu.magisk.superuser;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
import java.util.List;
public class SuDatabaseHelper extends SQLiteOpenHelper {
private static final int DATABASE_VER = 1;
private static final String TABLE_NAME = "policies";
public SuDatabaseHelper(Context context) {
super(context, "su.db", null, DATABASE_VER);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
"(uid INT, package_name TEXT, app_name TEXT, policy INT, " +
"until INT, logging INT, notification INT, " +
"PRIMARY KEY(uid))");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Currently new database, no upgrading
}
public boolean deletePolicy(int uid) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, "uid=?", new String[] { String.valueOf(uid) });
db.close();
return getPolicy(uid) == null;
}
public Policy getPolicy(int uid) {
Policy policy = null;
SQLiteDatabase db = getReadableDatabase();
try (Cursor c = db.query(TABLE_NAME, null, "uid=?", new String[] { String.valueOf(uid) }, null, null, null)) {
if (c.moveToNext())
policy = new Policy(c);
}
db.close();
return policy;
}
public void addPolicy(Policy policy) {
SQLiteDatabase db = getWritableDatabase();
db.replace(TABLE_NAME, null, policy.getContentValues());
db.close();
}
public List<Policy> getPolicyList(PackageManager pm) {
SQLiteDatabase db = getWritableDatabase();
ArrayList<Policy> 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;
}
}

View File

@ -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
}
}
}

View File

@ -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<CharSequence> 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<Void, Void, Boolean> {
@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);
}
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFd32f2f"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.topjohnwu.playground.superuser.RequestActivity"
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_width="375dp"
android:layout_gravity="center">
<TextView
android:text="@string/su_request_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:id="@+id/request_title"
android:gravity="center"
android:layout_marginTop="20dp" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:paddingStart="30dp">
<ImageView
app:srcCompat="@mipmap/ic_launcher"
android:id="@+id/app_icon"
android:layout_weight="1"
android:layout_marginEnd="15dp"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_marginStart="5dp" />
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_weight="5">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorPrimary"
android:textAppearance="?android:attr/textAppearanceMedium"
android:id="@+id/app_name" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorSecondary"
android:id="@+id/package_name" />
</LinearLayout>
</LinearLayout>
<Spinner
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/timeout"
android:layout_margin="15dp"
android:layout_gravity="center_horizontal" />
<TextView
android:id="@+id/warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_su_warning"
android:textColor="?android:textColorSecondary"
android:text="@string/su_warning"
android:layout_gravity="center_horizontal"
android:layout_margin="5dp"
android:drawablePadding="10dp" />
<LinearLayout
style="?android:buttonBarStyle"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
android:paddingLeft="30dp"
android:paddingRight="30dp">
<Button
style="?android:buttonBarButtonStyle"
android:text="@string/deny"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/deny_btn"
android:layout_weight="1" />
<Button
style="?android:buttonBarButtonStyle"
android:text="@string/grant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/grant_btn"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>

View File

@ -8,4 +8,12 @@
<item>@string/theme_default_value</item>
<item>@string/theme_dark_value</item>
</string-array>
<string-array name="timeout">
<item>@string/forever</item>
<item>@string/once</item>
<item>@string/tenmin</item>
<item>@string/twentymin</item>
<item>@string/thirtymin</item>
<item>@string/sixtymin</item>
</string-array>
</resources>

View File

@ -20,4 +20,6 @@
<color name="dh_icons">#dedede</color>
<color name="dh_divider">#313131</color>
<color name="su_request_background">#e0e0e0</color>
</resources>

View File

@ -143,5 +143,19 @@
<string name="theme_default_value" translatable="false">default</string>
<string name="theme_dark_value" translatable="false">dark</string>
<!--Superuser-->
<string name="su_request_title">Superuser Request</string>
<string name="deny">Deny</string>
<string name="grant">Grant</string>
<string name="su_warning">Grants full access to your device.\nDeny if you\'re not sure!</string>
<string name="forever">Forever</string>
<string name="once">Once</string>
<string name="tenmin">10 min</string>
<string name="twentymin">20 min</string>
<string name="thirtymin">30 min</string>
<string name="sixtymin">60 min</string>
<string name="su_allow_toast">%1$s is granted Superuser permissions</string>
<string name="su_deny_toast">%1$s is denied Superuser permissions</string>
</resources>

View File

@ -121,4 +121,9 @@
<item name="android:windowTranslucentNavigation">true</item>
</style>
<style name="SuRequest" parent="Theme.AppCompat.Light.Dialog">
<item name="android:windowCloseOnTouchOutside">false</item>
<item name="android:colorBackground">@color/su_request_background</item>
</style>
</resources>