package biz.bokhorst.xprivacy;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.SystemClock;
import android.os.StrictMode.ThreadPolicy;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.Patterns;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;
public class PrivacyService extends IPrivacyService.Stub {
private static int mXUid = -1;
private static Object mAm;
private static Context mContext;
private static String mSecret = null;
private static Thread mWorker = null;
private static Handler mHandler = null;
private static long mOnDemandLastAnswer = 0;
private static Semaphore mOndemandSemaphore = new Semaphore(1, true);
private static List<String> mListError = new ArrayList<String>();
private static IPrivacyService mClient = null;
private static final String cTableRestriction = "restriction";
private static final String cTableUsage = "usage";
private static final String cTableSetting = "setting";
private static final int cCurrentVersion = 481;
private static final String cServiceName = "xprivacy481";
private boolean mCorrupt = false;
private boolean mNotified = false;
private SQLiteDatabase mDb = null;
private SQLiteDatabase mDbUsage = null;
private SQLiteStatement stmtGetRestriction = null;
private SQLiteStatement stmtGetSetting = null;
private SQLiteStatement stmtGetUsageRestriction = null;
private SQLiteStatement stmtGetUsageMethod = null;
private ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(true);
private ReentrantReadWriteLock mLockUsage = new ReentrantReadWriteLock(true);
private AtomicLong mCount = new AtomicLong(0);
private AtomicLong mRestricted = new AtomicLong(0);
private Map<CSetting, CSetting> mSettingCache = new HashMap<CSetting, CSetting>();
private Map<CRestriction, CRestriction> mAskedOnceCache = new HashMap<CRestriction, CRestriction>();
private Map<CRestriction, CRestriction> mRestrictionCache = new HashMap<CRestriction, CRestriction>();
private final long cMaxUsageDataHours = 12;
private final int cMaxUsageDataCount = 700;
private final int cMaxOnDemandDialog = 20; // seconds
private ExecutorService mExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(),
new PriorityThreadFactory());
final class PriorityThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setPriority(Thread.MIN_PRIORITY);
return t;
}
}
// sqlite3 /data/system/xprivacy/xprivacy.db
private PrivacyService() {
}
private static PrivacyService mPrivacyService = null;
private static String getServiceName() {
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? "user." : "") + cServiceName;
}
public static void register(List<String> listError, ClassLoader classLoader, String secret, Object am) {
// Store secret and errors
mAm = am;
mSecret = secret;
mListError.addAll(listError);
try {
// Register privacy service
mPrivacyService = new PrivacyService();
// @formatter:off
// public static void addService(String name, IBinder service)
// public static void addService(String name, IBinder service, boolean allowIsolated)
// @formatter:on
// Requires this in /service_contexts
// xprivacy453 u:object_r:system_server_service:s0
Class<?> cServiceManager = Class.forName("android.os.ServiceManager", false, classLoader);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Method mAddService = cServiceManager.getDeclaredMethod("addService", String.class, IBinder.class,
boolean.class);
mAddService.invoke(null, getServiceName(), mPrivacyService, true);
} else {
Method mAddService = cServiceManager.getDeclaredMethod("addService", String.class, IBinder.class);
mAddService.invoke(null, getServiceName(), mPrivacyService);
}
// This will and should open the database
Util.log(null, Log.WARN, "Service registered name=" + getServiceName() + " version=" + cCurrentVersion);
// Publish semaphore to activity manager service
XActivityManagerService.setSemaphore(mOndemandSemaphore);
// Get context
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Field fContext = null;
Class<?> cam = am.getClass();
while (cam != null && fContext == null)
try {
fContext = cam.getDeclaredField("mContext");
} catch (NoSuchFieldException ignored) {
cam = cam.getSuperclass();
}
if (fContext == null)
Util.log(null, Log.ERROR, am.getClass().getName() + ".mContext not found");
else {
fContext.setAccessible(true);
mContext = (Context) fContext.get(am);
}
}
// Start a worker thread
mWorker = new Thread(new Runnable() {
@Override
public void run() {
try {
Looper.prepare();
mHandler = new Handler();
Looper.loop();
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
});
mWorker.start();
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
public static boolean isRegistered() {
return (mPrivacyService != null);
}
public static boolean checkClient() {
// Runs client side
try {
IPrivacyService client = getClient();
if (client != null)
return (client.getVersion() == cCurrentVersion);
} catch (SecurityException ignored) {
} catch (Throwable ex) {
Util.bug(null, ex);
}
return false;
}
public static IPrivacyService getClient() {
// Runs client side
if (mClient == null)
try {
// public static IBinder getService(String name)
Class<?> cServiceManager = Class.forName("android.os.ServiceManager");
Method mGetService = cServiceManager.getDeclaredMethod("getService", String.class);
mClient = IPrivacyService.Stub.asInterface((IBinder) mGetService.invoke(null, getServiceName()));
} catch (Throwable ex) {
Util.bug(null, ex);
}
return mClient;
}
public static void reportErrorInternal(String message) {
synchronized (mListError) {
mListError.add(message);
}
}
public static PRestriction getRestrictionProxy(final PRestriction restriction, boolean usage, String secret)
throws RemoteException {
if (isRegistered())
return mPrivacyService.getRestriction(restriction, usage, secret);
else {
IPrivacyService client = getClient();
if (client == null) {
Log.w("XPrivacy", "No client for " + restriction);
PRestriction result = new PRestriction(restriction);
result.restricted = false;
return result;
} else
return client.getRestriction(restriction, usage, secret);
}
}
public static PSetting getSettingProxy(PSetting setting) throws RemoteException {
if (isRegistered())
return mPrivacyService.getSetting(setting);
else {
IPrivacyService client = getClient();
if (client == null) {
Log.w("XPrivacy", "No client for " + setting + " uid=" + Process.myUid() + " pid=" + Process.myPid());
Log.w("XPrivacy", Log.getStackTraceString(new Exception("StackTrace")));
return setting;
} else
return client.getSetting(setting);
}
}
// Management
@Override
public int getVersion() throws RemoteException {
enforcePermission(-1);
return cCurrentVersion;
}
@Override
public List<String> check() throws RemoteException {
enforcePermission(-1);
List<String> listError = new ArrayList<String>();
synchronized (mListError) {
int c = 0;
int i = 0;
while (i < mListError.size()) {
String msg = mListError.get(i);
c += msg.length();
if (c < 10000)
listError.add(msg);
else
break;
i++;
}
}
return listError;
}
@Override
public boolean databaseCorrupt() {
return mCorrupt;
}
@Override
public void reportError(String message) throws RemoteException {
reportErrorInternal(message);
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public Map getStatistics() throws RemoteException {
Map map = new HashMap();
map.put("restriction_count", mCount.longValue());
map.put("restriction_restricted", mRestricted.longValue());
map.put("uptime_milliseconds", SystemClock.elapsedRealtime());
return map;
};
// Restrictions
@Override
public void setRestriction(PRestriction restriction) throws RemoteException {
try {
enforcePermission(restriction.uid);
setRestrictionInternal(restriction);
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
private void setRestrictionInternal(PRestriction restriction) throws RemoteException {
// Validate
if (restriction.restrictionName == null) {
Util.log(null, Log.ERROR, "Set invalid restriction " + restriction);
Util.logStack(null, Log.ERROR);
throw new RemoteException("Invalid restriction");
}
try {
SQLiteDatabase db = getDb();
if (db == null)
return;
// 0 not restricted, ask
// 1 restricted, ask
// 2 not restricted, asked
// 3 restricted, asked
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
// Create category record
if (restriction.methodName == null) {
ContentValues cvalues = new ContentValues();
cvalues.put("uid", restriction.uid);
cvalues.put("restriction", restriction.restrictionName);
cvalues.put("method", "");
cvalues.put("restricted", (restriction.restricted ? 1 : 0) + (restriction.asked ? 2 : 0));
db.insertWithOnConflict(cTableRestriction, null, cvalues, SQLiteDatabase.CONFLICT_REPLACE);
}
// Create method exception record
if (restriction.methodName != null) {
ContentValues mvalues = new ContentValues();
mvalues.put("uid", restriction.uid);
mvalues.put("restriction", restriction.restrictionName);
mvalues.put("method", restriction.methodName);
mvalues.put("restricted", (restriction.restricted ? 0 : 1) + (restriction.asked ? 2 : 0));
db.insertWithOnConflict(cTableRestriction, null, mvalues, SQLiteDatabase.CONFLICT_REPLACE);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
// Update cache
synchronized (mRestrictionCache) {
for (CRestriction key : new ArrayList<CRestriction>(mRestrictionCache.keySet()))
if (key.isSameMethod(restriction))
mRestrictionCache.remove(key);
CRestriction key = new CRestriction(restriction, restriction.extra);
if (mRestrictionCache.containsKey(key))
mRestrictionCache.remove(key);
mRestrictionCache.put(key, key);
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
@Override
public void setRestrictionList(List<PRestriction> listRestriction) throws RemoteException {
int uid = -1;
for (PRestriction restriction : listRestriction)
if (uid < 0)
uid = restriction.uid;
else if (uid != restriction.uid)
throw new SecurityException();
enforcePermission(uid);
for (PRestriction restriction : listRestriction)
setRestrictionInternal(restriction);
}
@Override
public PRestriction getRestriction(final PRestriction restriction, boolean usage, String secret)
throws RemoteException {
long start = System.currentTimeMillis();
// Translate isolated uid
restriction.uid = getIsolatedUid(restriction.uid);
boolean ccached = false;
boolean mcached = false;
int userId = Util.getUserId(restriction.uid);
final PRestriction mresult = new PRestriction(restriction);
// Disable strict mode
ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
ThreadPolicy newPolicy = new ThreadPolicy.Builder(oldPolicy).permitDiskReads().permitDiskWrites().build();
StrictMode.setThreadPolicy(newPolicy);
try {
// No permissions enforced, but usage data requires a secret
// Sanity checks
if (restriction.restrictionName == null) {
Util.log(null, Log.ERROR, "Get invalid restriction " + restriction);
return mresult;
}
if (usage && restriction.methodName == null) {
Util.log(null, Log.ERROR, "Get invalid restriction " + restriction);
return mresult;
}
// Get meta data
Hook hook = null;
if (restriction.methodName != null) {
hook = PrivacyManager.getHook(restriction.restrictionName, restriction.methodName);
if (hook == null)
// Can happen after updating
Util.log(null, Log.WARN, "Hook not found in service: " + restriction);
else if (hook.getFrom() != null) {
String version = getSetting(new PSetting(userId, "", PrivacyManager.cSettingVersion, null)).value;
if (version != null && new Version(version).compareTo(hook.getFrom()) < 0)
if (hook.getReplacedRestriction() == null) {
Util.log(null, Log.WARN, "Disabled version=" + version + " from=" + hook.getFrom()
+ " hook=" + hook);
return mresult;
} else {
restriction.restrictionName = hook.getReplacedRestriction();
restriction.methodName = hook.getReplacedMethod();
Util.log(null, Log.WARN, "Checking " + restriction + " instead of " + hook);
}
}
}
// Process IP address
if (restriction.extra != null && Meta.cTypeIPAddress.equals(hook.whitelist())) {
int colon = restriction.extra.lastIndexOf(':');
String address = (colon >= 0 ? restriction.extra.substring(0, colon) : restriction.extra);
String port = (colon >= 0 ? restriction.extra.substring(colon) : "");
int slash = address.indexOf('/');
if (slash == 0) // IP address
restriction.extra = address.substring(slash + 1) + port;
else if (slash > 0) // Domain name
restriction.extra = address.substring(0, slash) + port;
}
// Check for system component
if (!PrivacyManager.isApplication(restriction.uid))
if (!getSettingBool(userId, PrivacyManager.cSettingSystem, false))
return mresult;
// Check if restrictions enabled
if (usage && !getSettingBool(restriction.uid, PrivacyManager.cSettingRestricted, true))
return mresult;
// Check if can be restricted
if (!PrivacyManager.canRestrict(restriction.uid, getXUid(), restriction.restrictionName,
restriction.methodName, false))
mresult.asked = true;
else {
// Check cache for method
CRestriction key = new CRestriction(restriction, restriction.extra);
synchronized (mRestrictionCache) {
if (mRestrictionCache.containsKey(key)) {
mcached = true;
CRestriction cache = mRestrictionCache.get(key);
mresult.restricted = cache.restricted;
mresult.asked = cache.asked;
}
}
if (!mcached) {
boolean methodFound = false;
PRestriction cresult = new PRestriction(restriction.uid, restriction.restrictionName, null);
// Check cache for category
CRestriction ckey = new CRestriction(cresult, null);
synchronized (mRestrictionCache) {
if (mRestrictionCache.containsKey(ckey)) {
ccached = true;
CRestriction crestriction = mRestrictionCache.get(ckey);
cresult.restricted = crestriction.restricted;
cresult.asked = crestriction.asked;
mresult.restricted = cresult.restricted;
mresult.asked = cresult.asked;
}
}
// Get database reference
SQLiteDatabase db = getDb();
if (db == null)
return mresult;
// Precompile statement when needed
if (stmtGetRestriction == null) {
String sql = "SELECT restricted FROM " + cTableRestriction
+ " WHERE uid=? AND restriction=? AND method=?";
stmtGetRestriction = db.compileStatement(sql);
}
// Execute statement
mLock.readLock().lock();
try {
db.beginTransaction();
try {
if (!ccached)
try {
synchronized (stmtGetRestriction) {
stmtGetRestriction.clearBindings();
stmtGetRestriction.bindLong(1, restriction.uid);
stmtGetRestriction.bindString(2, restriction.restrictionName);
stmtGetRestriction.bindString(3, "");
long state = stmtGetRestriction.simpleQueryForLong();
cresult.restricted = ((state & 1) != 0);
cresult.asked = ((state & 2) != 0);
mresult.restricted = cresult.restricted;
mresult.asked = cresult.asked;
}
} catch (SQLiteDoneException ignored) {
}
if (restriction.methodName != null)
try {
synchronized (stmtGetRestriction) {
stmtGetRestriction.clearBindings();
stmtGetRestriction.bindLong(1, restriction.uid);
stmtGetRestriction.bindString(2, restriction.restrictionName);
stmtGetRestriction.bindString(3, restriction.methodName);
long state = stmtGetRestriction.simpleQueryForLong();
// Method can be excepted
if (mresult.restricted)
mresult.restricted = ((state & 1) == 0);
// Category asked=true takes precedence
if (!mresult.asked)
mresult.asked = ((state & 2) != 0);
methodFound = true;
}
} catch (SQLiteDoneException ignored) {
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.readLock().unlock();
}
// Default dangerous
if (!methodFound && hook != null && hook.isDangerous())
if (!getSettingBool(userId, PrivacyManager.cSettingDangerous, false)) {
if (mresult.restricted)
mresult.restricted = false;
if (!mresult.asked)
mresult.asked = (hook.whitelist() == null);
}
// Check whitelist
if (usage && hook != null && hook.whitelist() != null && restriction.extra != null) {
String value = getSetting(new PSetting(restriction.uid, hook.whitelist(), restriction.extra,
null)).value;
if (value == null) {
for (String xextra : getXExtra(restriction, hook)) {
value = getSetting(new PSetting(restriction.uid, hook.whitelist(), xextra, null)).value;
if (value != null)
break;
}
}
if (value != null) {
// true means allow, false means block
mresult.restricted = !Boolean.parseBoolean(value);
mresult.asked = true;
}
}
// Fallback
if (!mresult.restricted && usage && PrivacyManager.isApplication(restriction.uid)
&& !getSettingBool(userId, PrivacyManager.cSettingMigrated, false)) {
if (hook != null && !hook.isDangerous()) {
mresult.restricted = PrivacyProvider.getRestrictedFallback(null, restriction.uid,
restriction.restrictionName, restriction.methodName);
Util.log(null, Log.WARN, "Fallback " + mresult);
}
}
// Update cache
CRestriction cukey = new CRestriction(cresult, null);
synchronized (mRestrictionCache) {
if (mRestrictionCache.containsKey(cukey))
mRestrictionCache.remove(cukey);
mRestrictionCache.put(cukey, cukey);
}
CRestriction ukey = new CRestriction(mresult, restriction.extra);
synchronized (mRestrictionCache) {
if (mRestrictionCache.containsKey(ukey))
mRestrictionCache.remove(ukey);
mRestrictionCache.put(ukey, ukey);
}
}
// Ask to restrict
OnDemandResult oResult = new OnDemandResult();
if (!mresult.asked && usage) {
oResult = onDemandDialog(hook, restriction, mresult);
// Update cache
if (oResult.ondemand && !oResult.once) {
CRestriction okey = new CRestriction(mresult, oResult.whitelist ? restriction.extra : null);
synchronized (mRestrictionCache) {
if (mRestrictionCache.containsKey(okey))
mRestrictionCache.remove(okey);
mRestrictionCache.put(okey, okey);
}
}
}
// Notify user
if (!oResult.ondemand && mresult.restricted && usage && hook != null && hook.shouldNotify()) {
notifyRestricted(restriction);
mresult.time = new Date().getTime();
}
}
// Store usage data
if (usage && hook != null)
storeUsageData(restriction, secret, mresult);
} catch (SQLiteException ex) {
notifyException(ex);
} catch (Throwable ex) {
Util.bug(null, ex);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
long ms = System.currentTimeMillis() - start;
Util.log(
null,
ms < PrivacyManager.cWarnServiceDelayMs ? Log.INFO : Log.WARN,
String.format("Get service %s%s %d ms", restriction, (ccached ? " (ccached)" : "")
+ (mcached ? " (mcached)" : ""), ms));
if (mresult.debug)
Util.logStack(null, Log.WARN);
if (usage) {
mCount.incrementAndGet();
if (mresult.restricted)
mRestricted.incrementAndGet();
}
return mresult;
}
private void storeUsageData(final PRestriction restriction, String secret, final PRestriction mresult)
throws RemoteException {
// Check if enabled
final int userId = Util.getUserId(restriction.uid);
if (getSettingBool(userId, PrivacyManager.cSettingUsage, true)
&& !getSettingBool(restriction.uid, PrivacyManager.cSettingNoUsageData, false)) {
// Check secret
boolean allowed = true;
if (Util.getAppId(Binder.getCallingUid()) != getXUid()) {
if (mSecret == null || !mSecret.equals(secret)) {
allowed = false;
Util.log(null, Log.WARN, "Invalid secret restriction=" + restriction);
}
}
if (allowed) {
mExecutor.execute(new Runnable() {
public void run() {
try {
if (XActivityManagerService.canWriteUsageData()) {
SQLiteDatabase dbUsage = getDbUsage();
if (dbUsage == null)
return;
// Parameter
String extra = "";
if (restriction.extra != null)
if (getSettingBool(userId, PrivacyManager.cSettingParameters, false))
extra = restriction.extra;
// Value
if (restriction.value != null)
if (!getSettingBool(userId, PrivacyManager.cSettingValues, false))
restriction.value = null;
mLockUsage.writeLock().lock();
try {
dbUsage.beginTransaction();
try {
ContentValues values = new ContentValues();
values.put("uid", restriction.uid);
values.put("restriction", restriction.restrictionName);
values.put("method", restriction.methodName);
values.put("restricted", mresult.restricted);
values.put("time", new Date().getTime());
values.put("extra", extra);
if (restriction.value == null)
values.putNull("value");
else
values.put("value", restriction.value);
dbUsage.insertWithOnConflict(cTableUsage, null, values,
SQLiteDatabase.CONFLICT_REPLACE);
dbUsage.setTransactionSuccessful();
} finally {
dbUsage.endTransaction();
}
} finally {
mLockUsage.writeLock().unlock();
}
}
} catch (SQLiteException ex) {
Util.log(null, Log.WARN, ex.toString());
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
});
}
}
}
@Override
public List<PRestriction> getRestrictionList(PRestriction selector) throws RemoteException {
List<PRestriction> result = new ArrayList<PRestriction>();
try {
enforcePermission(selector.uid);
PRestriction query;
if (selector.restrictionName == null)
for (String sRestrictionName : PrivacyManager.getRestrictions()) {
PRestriction restriction = new PRestriction(selector.uid, sRestrictionName, null, false);
query = getRestriction(restriction, false, null);
restriction.restricted = query.restricted;
restriction.asked = query.asked;
result.add(restriction);
}
else
for (Hook md : PrivacyManager.getHooks(selector.restrictionName, null)) {
PRestriction restriction = new PRestriction(selector.uid, selector.restrictionName, md.getName(),
false);
query = getRestriction(restriction, false, null);
restriction.restricted = query.restricted;
restriction.asked = query.asked;
result.add(restriction);
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
return result;
}
@Override
public boolean isRestrictionSet(PRestriction restriction) throws RemoteException {
try {
// No permissions required
boolean set = false;
SQLiteDatabase db = getDb();
if (db != null) {
// Precompile statement when needed
if (stmtGetRestriction == null) {
String sql = "SELECT restricted FROM " + cTableRestriction
+ " WHERE uid=? AND restriction=? AND method=?";
stmtGetRestriction = db.compileStatement(sql);
}
// Execute statement
mLock.readLock().lock();
try {
db.beginTransaction();
try {
try {
synchronized (stmtGetRestriction) {
stmtGetRestriction.clearBindings();
stmtGetRestriction.bindLong(1, restriction.uid);
stmtGetRestriction.bindString(2, restriction.restrictionName);
stmtGetRestriction.bindString(3, restriction.methodName);
stmtGetRestriction.simpleQueryForLong();
set = true;
}
} catch (SQLiteDoneException ignored) {
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.readLock().unlock();
}
}
return set;
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
@Override
public void deleteRestrictions(int uid, String restrictionName) throws RemoteException {
try {
enforcePermission(uid);
SQLiteDatabase db = getDb();
if (db == null)
return;
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
if ("".equals(restrictionName))
db.delete(cTableRestriction, "uid=?", new String[] { Integer.toString(uid) });
else
db.delete(cTableRestriction, "uid=? AND restriction=?", new String[] { Integer.toString(uid),
restrictionName });
Util.log(null, Log.WARN, "Restrictions deleted uid=" + uid + " category=" + restrictionName);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
// Clear caches
synchronized (mRestrictionCache) {
mRestrictionCache.clear();
}
synchronized (mAskedOnceCache) {
mAskedOnceCache.clear();
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
// Usage
@Override
public long getUsage(List<PRestriction> listRestriction) throws RemoteException {
long lastUsage = 0;
try {
int uid = -1;
for (PRestriction restriction : listRestriction)
if (uid < 0)
uid = restriction.uid;
else if (uid != restriction.uid)
throw new SecurityException();
enforcePermission(uid);
SQLiteDatabase dbUsage = getDbUsage();
// Precompile statement when needed
if (stmtGetUsageRestriction == null) {
String sql = "SELECT MAX(time) FROM " + cTableUsage + " WHERE uid=? AND restriction=?";
stmtGetUsageRestriction = dbUsage.compileStatement(sql);
}
if (stmtGetUsageMethod == null) {
String sql = "SELECT MAX(time) FROM " + cTableUsage + " WHERE uid=? AND restriction=? AND method=?";
stmtGetUsageMethod = dbUsage.compileStatement(sql);
}
mLockUsage.readLock().lock();
try {
dbUsage.beginTransaction();
try {
for (PRestriction restriction : listRestriction) {
if (restriction.methodName == null)
try {
synchronized (stmtGetUsageRestriction) {
stmtGetUsageRestriction.clearBindings();
stmtGetUsageRestriction.bindLong(1, restriction.uid);
stmtGetUsageRestriction.bindString(2, restriction.restrictionName);
lastUsage = Math.max(lastUsage, stmtGetUsageRestriction.simpleQueryForLong());
}
} catch (SQLiteDoneException ignored) {
}
else
try {
synchronized (stmtGetUsageMethod) {
stmtGetUsageMethod.clearBindings();
stmtGetUsageMethod.bindLong(1, restriction.uid);
stmtGetUsageMethod.bindString(2, restriction.restrictionName);
stmtGetUsageMethod.bindString(3, restriction.methodName);
lastUsage = Math.max(lastUsage, stmtGetUsageMethod.simpleQueryForLong());
}
} catch (SQLiteDoneException ignored) {
}
}
dbUsage.setTransactionSuccessful();
} finally {
dbUsage.endTransaction();
}
} finally {
mLockUsage.readLock().unlock();
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
return lastUsage;
}
@Override
public List<PRestriction> getUsageList(int uid, String restrictionName) throws RemoteException {
List<PRestriction> result = new ArrayList<PRestriction>();
try {
enforcePermission(-1);
SQLiteDatabase dbUsage = getDbUsage();
int userId = Util.getUserId(Binder.getCallingUid());
mLockUsage.readLock().lock();
try {
dbUsage.beginTransaction();
try {
String sFrom = Long.toString(new Date().getTime() - cMaxUsageDataHours * 60L * 60L * 1000L);
Cursor cursor;
if (uid == 0) {
if ("".equals(restrictionName))
cursor = dbUsage.query(cTableUsage, new String[] { "uid", "restriction", "method",
"restricted", "time", "extra", "value" }, "time>?", new String[] { sFrom }, null,
null, "time DESC");
else
cursor = dbUsage.query(cTableUsage, new String[] { "uid", "restriction", "method",
"restricted", "time", "extra", "value" }, "restriction=? AND time>?", new String[] {
restrictionName, sFrom }, null, null, "time DESC");
} else {
if ("".equals(restrictionName))
cursor = dbUsage.query(cTableUsage, new String[] { "uid", "restriction", "method",
"restricted", "time", "extra", "value" }, "uid=? AND time>?", new String[] {
Integer.toString(uid), sFrom }, null, null, "time DESC");
else
cursor = dbUsage.query(cTableUsage, new String[] { "uid", "restriction", "method",
"restricted", "time", "extra", "value" }, "uid=? AND restriction=? AND time>?",
new String[] { Integer.toString(uid), restrictionName, sFrom }, null, null,
"time DESC");
}
if (cursor == null)
Util.log(null, Log.WARN, "Database cursor null (usage data)");
else
try {
int count = 0;
while (count++ < cMaxUsageDataCount && cursor.moveToNext()) {
PRestriction data = new PRestriction();
data.uid = cursor.getInt(0);
data.restrictionName = cursor.getString(1);
data.methodName = cursor.getString(2);
data.restricted = (cursor.getInt(3) > 0);
data.time = cursor.getLong(4);
data.extra = cursor.getString(5);
if (cursor.isNull(6))
data.value = null;
else
data.value = cursor.getString(6);
if (userId == 0 || Util.getUserId(data.uid) == userId)
result.add(data);
}
} finally {
cursor.close();
}
dbUsage.setTransactionSuccessful();
} finally {
dbUsage.endTransaction();
}
} finally {
mLockUsage.readLock().unlock();
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
return result;
}
@Override
public void deleteUsage(int uid) throws RemoteException {
try {
enforcePermission(uid);
SQLiteDatabase dbUsage = getDbUsage();
mLockUsage.writeLock().lock();
try {
dbUsage.beginTransaction();
try {
if (uid == 0)
dbUsage.delete(cTableUsage, null, new String[] {});
else
dbUsage.delete(cTableUsage, "uid=?", new String[] { Integer.toString(uid) });
Util.log(null, Log.WARN, "Usage data deleted uid=" + uid);
dbUsage.setTransactionSuccessful();
} finally {
dbUsage.endTransaction();
}
} finally {
mLockUsage.writeLock().unlock();
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
// Settings
@Override
public void setSetting(PSetting setting) throws RemoteException {
try {
enforcePermission(setting.uid);
setSettingInternal(setting);
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
private void setSettingInternal(PSetting setting) throws RemoteException {
try {
SQLiteDatabase db = getDb();
if (db == null)
return;
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
if (setting.value == null)
db.delete(cTableSetting, "uid=? AND type=? AND name=?",
new String[] { Integer.toString(setting.uid), setting.type, setting.name });
else {
// Create record
ContentValues values = new ContentValues();
values.put("uid", setting.uid);
values.put("type", setting.type);
values.put("name", setting.name);
values.put("value", setting.value);
// Insert/update record
db.insertWithOnConflict(cTableSetting, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
if (PrivacyManager.cSettingAOSPMode.equals(setting.name))
if (setting.value == null || Boolean.toString(false).equals(setting.value))
new File("/data/system/xprivacy/aosp").delete();
else
new File("/data/system/xprivacy/aosp").createNewFile();
// Update cache
CSetting key = new CSetting(setting.uid, setting.type, setting.name);
key.setValue(setting.value);
synchronized (mSettingCache) {
if (mSettingCache.containsKey(key))
mSettingCache.remove(key);
if (setting.value != null)
mSettingCache.put(key, key);
}
// Clear restrictions for white list
if (Meta.isWhitelist(setting.type))
for (String restrictionName : PrivacyManager.getRestrictions())
for (Hook hook : PrivacyManager.getHooks(restrictionName, null))
if (setting.type.equals(hook.whitelist())) {
PRestriction restriction = new PRestriction(setting.uid, hook.getRestrictionName(),
hook.getName());
Util.log(null, Log.WARN, "Clearing cache for " + restriction);
synchronized (mRestrictionCache) {
for (CRestriction mkey : new ArrayList<CRestriction>(mRestrictionCache.keySet()))
if (mkey.isSameMethod(restriction)) {
Util.log(null, Log.WARN, "Removing " + mkey);
mRestrictionCache.remove(mkey);
}
}
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
@Override
public void setSettingList(List<PSetting> listSetting) throws RemoteException {
int uid = -1;
for (PSetting setting : listSetting)
if (uid < 0)
uid = setting.uid;
else if (uid != setting.uid)
throw new SecurityException();
enforcePermission(uid);
for (PSetting setting : listSetting)
setSettingInternal(setting);
}
@Override
public PSetting getSetting(PSetting setting) throws RemoteException {
long start = System.currentTimeMillis();
// Translate isolated uid
setting.uid = getIsolatedUid(setting.uid);
int userId = Util.getUserId(setting.uid);
// Special case
if (Meta.cTypeAccountHash.equals(setting.type))
try {
setting.type = Meta.cTypeAccount;
setting.name = Util.sha1(setting.name);
} catch (Throwable ex) {
Util.bug(null, ex);
}
// Default result
PSetting result = new PSetting(setting.uid, setting.type, setting.name, setting.value);
// Disable strict mode
ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
ThreadPolicy newPolicy = new ThreadPolicy.Builder(oldPolicy).permitDiskReads().permitDiskWrites().build();
StrictMode.setThreadPolicy(newPolicy);
try {
// No permissions enforced
// Check cache
CSetting key = new CSetting(setting.uid, setting.type, setting.name);
synchronized (mSettingCache) {
if (mSettingCache.containsKey(key)) {
result.value = mSettingCache.get(key).getValue();
if (result.value == null)
result.value = setting.value; // default value
return result;
}
}
// No persmissions required
SQLiteDatabase db = getDb();
if (db == null)
return result;
// Fallback
if (!PrivacyManager.cSettingMigrated.equals(setting.name)
&& !getSettingBool(userId, PrivacyManager.cSettingMigrated, false)) {
if (setting.uid == 0)
result.value = PrivacyProvider.getSettingFallback(setting.name, null, false);
if (result.value == null) {
result.value = PrivacyProvider.getSettingFallback(
String.format("%s.%d", setting.name, setting.uid), setting.value, false);
return result;
}
}
// Precompile statement when needed
if (stmtGetSetting == null) {
String sql = "SELECT value FROM " + cTableSetting + " WHERE uid=? AND type=? AND name=?";
stmtGetSetting = db.compileStatement(sql);
}
// Execute statement
boolean found = false;
mLock.readLock().lock();
try {
db.beginTransaction();
try {
try {
synchronized (stmtGetSetting) {
stmtGetSetting.clearBindings();
stmtGetSetting.bindLong(1, setting.uid);
stmtGetSetting.bindString(2, setting.type);
stmtGetSetting.bindString(3, setting.name);
result.value = stmtGetSetting.simpleQueryForString();
found = true;
}
} catch (SQLiteDoneException ignored) {
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.readLock().unlock();
}
// Add to cache
key.setValue(found ? result.value : null);
synchronized (mSettingCache) {
if (mSettingCache.containsKey(key))
mSettingCache.remove(key);
mSettingCache.put(key, key);
}
// Default value
if (result.value == null)
result.value = setting.value;
} catch (SQLiteException ex) {
notifyException(ex);
} catch (Throwable ex) {
Util.bug(null, ex);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
long ms = System.currentTimeMillis() - start;
Util.log(null, ms < PrivacyManager.cWarnServiceDelayMs ? Log.INFO : Log.WARN,
String.format("Get service %s %d ms", setting, ms));
return result;
}
@Override
public List<PSetting> getSettingList(PSetting selector) throws RemoteException {
List<PSetting> listSetting = new ArrayList<PSetting>();
try {
enforcePermission(selector.uid);
SQLiteDatabase db = getDb();
if (db == null)
return listSetting;
mLock.readLock().lock();
try {
db.beginTransaction();
try {
Cursor cursor;
if (selector.type == null)
cursor = db.query(cTableSetting, new String[] { "type", "name", "value" }, "uid=?",
new String[] { Integer.toString(selector.uid) }, null, null, null);
else
cursor = db.query(cTableSetting, new String[] { "type", "name", "value" }, "uid=? AND type=?",
new String[] { Integer.toString(selector.uid), selector.type }, null, null, null);
if (cursor == null)
Util.log(null, Log.WARN, "Database cursor null (settings)");
else
try {
while (cursor.moveToNext())
listSetting.add(new PSetting(selector.uid, cursor.getString(0), cursor.getString(1),
cursor.getString(2)));
} finally {
cursor.close();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.readLock().unlock();
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
return listSetting;
}
@Override
public void deleteSettings(int uid) throws RemoteException {
try {
enforcePermission(uid);
SQLiteDatabase db = getDb();
if (db == null)
return;
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.delete(cTableSetting, "uid=?", new String[] { Integer.toString(uid) });
Util.log(null, Log.WARN, "Settings deleted uid=" + uid);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
// Clear cache
synchronized (mSettingCache) {
mSettingCache.clear();
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
@Override
public void clear() throws RemoteException {
try {
enforcePermission(0);
SQLiteDatabase db = getDb();
SQLiteDatabase dbUsage = getDbUsage();
if (db == null || dbUsage == null)
return;
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("DELETE FROM " + cTableRestriction);
db.execSQL("DELETE FROM " + cTableSetting);
Util.log(null, Log.WARN, "Database cleared");
// Reset migrated
ContentValues values = new ContentValues();
values.put("uid", 0);
values.put("type", "");
values.put("name", PrivacyManager.cSettingMigrated);
values.put("value", Boolean.toString(true));
db.insertWithOnConflict(cTableSetting, null, values, SQLiteDatabase.CONFLICT_REPLACE);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
// Clear caches
synchronized (mRestrictionCache) {
mRestrictionCache.clear();
}
synchronized (mSettingCache) {
mSettingCache.clear();
}
synchronized (mAskedOnceCache) {
mAskedOnceCache.clear();
}
Util.log(null, Log.WARN, "Caches cleared");
mLockUsage.writeLock().lock();
try {
dbUsage.beginTransaction();
try {
dbUsage.execSQL("DELETE FROM " + cTableUsage);
Util.log(null, Log.WARN, "Usage database cleared");
dbUsage.setTransactionSuccessful();
} finally {
dbUsage.endTransaction();
}
} finally {
mLockUsage.writeLock().unlock();
}
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
@Override
public void flush() throws RemoteException {
try {
enforcePermission(0);
synchronized (mRestrictionCache) {
mRestrictionCache.clear();
}
synchronized (mAskedOnceCache) {
mAskedOnceCache.clear();
}
synchronized (mSettingCache) {
mSettingCache.clear();
}
Util.log(null, Log.WARN, "Service cache flushed");
} catch (Throwable ex) {
Util.bug(null, ex);
throw new RemoteException(ex.toString());
}
}
@Override
public void dump(int uid) throws RemoteException {
if (uid == 0) {
} else {
synchronized (mRestrictionCache) {
for (CRestriction crestriction : mRestrictionCache.keySet())
if (crestriction.getUid() == uid)
Util.log(null, Log.WARN, "Dump crestriction=" + crestriction);
}
synchronized (mAskedOnceCache) {
for (CRestriction crestriction : mAskedOnceCache.keySet())
if (crestriction.getUid() == uid && !crestriction.isExpired())
Util.log(null, Log.WARN, "Dump asked=" + crestriction);
}
synchronized (mSettingCache) {
for (CSetting csetting : mSettingCache.keySet())
if (csetting.getUid() == uid)
Util.log(null, Log.WARN, "Dump csetting=" + csetting);
}
}
}
// Helper classes
final class OnDemandResult {
public boolean ondemand = false;
public boolean once = false;
public boolean whitelist = false;
}
final class OnDemandDialogHolder {
public View dialog = null;
public CountDownLatch latch = new CountDownLatch(1);
public boolean reset = false;
}
// Helper methods
private OnDemandResult onDemandDialog(final Hook hook, final PRestriction restriction, final PRestriction result) {
final OnDemandResult oResult = new OnDemandResult();
try {
int userId = Util.getUserId(restriction.uid);
// Check if application
if (!PrivacyManager.isApplication(restriction.uid))
if (!getSettingBool(userId, PrivacyManager.cSettingOnDemandSystem, false))
return oResult;
// Check for exceptions
if (hook != null && !hook.canOnDemand())
return oResult;
if (!PrivacyManager.canRestrict(restriction.uid, getXUid(), restriction.restrictionName,
restriction.methodName, false))
return oResult;
// Check if enabled
if (!getSettingBool(userId, PrivacyManager.cSettingOnDemand, true))
return oResult;
if (!getSettingBool(restriction.uid, PrivacyManager.cSettingOnDemand, false))
return oResult;
// Check version
String version = getSetting(new PSetting(userId, "", PrivacyManager.cSettingVersion, "0.0")).value;
if (new Version(version).compareTo(new Version("2.1.5")) < 0)
return oResult;
// Get activity manager context
final Context context = getContext();
if (context == null)
return oResult;
long token = 0;
try {
token = Binder.clearCallingIdentity();
// Get application info
final ApplicationInfoEx appInfo = new ApplicationInfoEx(context, restriction.uid);
// Check for system application
if (appInfo.isSystem())
if (new Version(version).compareTo(new Version("2.0.38")) < 0)
return oResult;
// Check if activity manager agrees
if (!XActivityManagerService.canOnDemand())
return oResult;
// Check if activity manager locked
if (isAMLocked(restriction.uid)) {
Util.log(null, Log.WARN, "On demand locked " + restriction);
return oResult;
}
// Go ask
Util.log(null, Log.WARN, "On demand " + restriction);
mOndemandSemaphore.acquireUninterruptibly();
try {
// Check if activity manager still agrees
if (!XActivityManagerService.canOnDemand())
return oResult;
// Check if activity manager locked now
if (isAMLocked(restriction.uid)) {
Util.log(null, Log.WARN, "On demand acquired locked " + restriction);
return oResult;
}
Util.log(null, Log.WARN, "On demanding " + restriction);
// Check if method not asked before
CRestriction mkey = new CRestriction(restriction, null);
synchronized (mRestrictionCache) {
if (mRestrictionCache.containsKey(mkey)) {
CRestriction mrestriction = mRestrictionCache.get(mkey);
if (mrestriction.asked) {
Util.log(null, Log.WARN, "Already asked " + restriction);
result.restricted = mrestriction.restricted;
result.asked = true;
return oResult;
}
}
}
// Check if category not asked before (once)
CRestriction ckey = new CRestriction(restriction, null);
ckey.setMethodName(null);
synchronized (mAskedOnceCache) {
if (mAskedOnceCache.containsKey(ckey)) {
CRestriction carestriction = mAskedOnceCache.get(ckey);
if (!carestriction.isExpired()) {
Util.log(null, Log.WARN, "Already asked once category " + restriction);
result.restricted = carestriction.restricted;
result.asked = true;
return oResult;
}
}
}
// Check if method not asked before once
synchronized (mAskedOnceCache) {
if (mAskedOnceCache.containsKey(mkey)) {
CRestriction marestriction = mAskedOnceCache.get(mkey);
if (!marestriction.isExpired()) {
Util.log(null, Log.WARN, "Already asked once method " + restriction);
result.restricted = marestriction.restricted;
result.asked = true;
return oResult;
}
}
}
// Check if whitelist not asked before
if (restriction.extra != null && hook != null && hook.whitelist() != null) {
CSetting skey = new CSetting(restriction.uid, hook.whitelist(), restriction.extra);
synchronized (mSettingCache) {
if (mSettingCache.containsKey(skey)) {
String value = mSettingCache.get(skey).getValue();
if (value != null) {
Util.log(null, Log.WARN, "Already asked whitelist " + skey);
result.restricted = Boolean.parseBoolean(value);
result.asked = true;
return oResult;
}
}
for (String xextra : getXExtra(restriction, hook)) {
CSetting xkey = new CSetting(restriction.uid, hook.whitelist(), xextra);
if (mSettingCache.containsKey(xkey)) {
String value = mSettingCache.get(xkey).getValue();
if (value != null) {
Util.log(null, Log.WARN, "Already asked whitelist " + xkey);
result.restricted = Boolean.parseBoolean(value);
result.asked = true;
return oResult;
}
}
}
}
}
final OnDemandDialogHolder holder = new OnDemandDialogHolder();
// Build dialog parameters
final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_PHONE;
params.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND;
params.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE;
params.dimAmount = 0.85f;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
params.gravity = Gravity.CENTER;
// Get window manager
final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// Show dialog
mHandler.post(new Runnable() {
@Override
public void run() {
try {
// Build dialog
holder.dialog = getOnDemandView(restriction, hook, appInfo, result, context, holder,
oResult);
// Handle reset button
((Button) holder.dialog.findViewById(R.id.btnReset))
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
((ProgressBar) holder.dialog.findViewById(R.id.pbProgress))
.setProgress(cMaxOnDemandDialog * 20);
holder.reset = true;
holder.latch.countDown();
}
});
// Make dialog visible
wm.addView(holder.dialog, params);
// Update progress bar
Runnable runProgress = new Runnable() {
@Override
public void run() {
if (holder.dialog != null && holder.dialog.isShown()) {
// Update progress bar
ProgressBar progressBar = (ProgressBar) holder.dialog
.findViewById(R.id.pbProgress);
if (progressBar.getProgress() > 0) {
progressBar.incrementProgressBy(-1);
mHandler.postDelayed(this, 50);
}
// Check if activity manager locked
if (isAMLocked(restriction.uid)) {
Util.log(null, Log.WARN, "On demand dialog locked " + restriction);
((Button) holder.dialog.findViewById(R.id.btnDontKnow)).callOnClick();
}
}
}
};
mHandler.postDelayed(runProgress, 50);
// Enabled buttons after one second
boolean repeat = (SystemClock.elapsedRealtime() - mOnDemandLastAnswer < 1000);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (holder.dialog != null && holder.dialog.isShown()) {
holder.dialog.findViewById(R.id.btnAllow).setEnabled(true);
holder.dialog.findViewById(R.id.btnDontKnow).setEnabled(true);
holder.dialog.findViewById(R.id.btnDeny).setEnabled(true);
}
}
}, repeat ? 0 : 1000);
} catch (NameNotFoundException ex) {
Util.log(null, Log.WARN, ex.toString());
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
});
// Wait for choice, reset or timeout
do {
holder.reset = false;
boolean choice = holder.latch.await(cMaxOnDemandDialog, TimeUnit.SECONDS);
if (holder.reset) {
holder.latch = new CountDownLatch(1);
Util.log(null, Log.WARN, "On demand reset " + restriction);
} else if (choice)
oResult.ondemand = true;
else
Util.log(null, Log.WARN, "On demand timeout " + restriction);
} while (holder.reset);
mOnDemandLastAnswer = SystemClock.elapsedRealtime();
// Dismiss dialog
mHandler.post(new Runnable() {
@Override
public void run() {
View dialog = holder.dialog;
if (dialog != null)
wm.removeView(dialog);
}
});
} finally {
mOndemandSemaphore.release();
}
} finally {
Binder.restoreCallingIdentity(token);
}
} catch (Throwable ex) {
Util.bug(null, ex);
}
return oResult;
}
@SuppressLint("InflateParams")
private View getOnDemandView(final PRestriction restriction, final Hook hook, ApplicationInfoEx appInfo,
final PRestriction result, Context context, final OnDemandDialogHolder holder, final OnDemandResult oResult)
throws NameNotFoundException, RemoteException {
// Get resources
String self = PrivacyService.class.getPackage().getName();
Resources resources = context.getPackageManager().getResourcesForApplication(self);
// Reference views
final View view = LayoutInflater.from(context.createPackageContext(self, 0)).inflate(R.layout.ondemand, null);
ImageView ivAppIcon = (ImageView) view.findViewById(R.id.ivAppIcon);
TextView tvUid = (TextView) view.findViewById(R.id.tvUid);
TextView tvAppName = (TextView) view.findViewById(R.id.tvAppName);
TextView tvCategory = (TextView) view.findViewById(R.id.tvCategory);
TextView tvFunction = (TextView) view.findViewById(R.id.tvFunction);
TextView tvParameters = (TextView) view.findViewById(R.id.tvParameters);
TableRow rowParameters = (TableRow) view.findViewById(R.id.rowParameters);
TextView tvDefault = (TextView) view.findViewById(R.id.tvDefault);
TextView tvInfoCategory = (TextView) view.findViewById(R.id.tvInfoCategory);
final CheckBox cbExpert = (CheckBox) view.findViewById(R.id.cbExpert);
final CheckBox cbCategory = (CheckBox) view.findViewById(R.id.cbCategory);
final CheckBox cbOnce = (CheckBox) view.findViewById(R.id.cbOnce);
final Spinner spOnce = (Spinner) view.findViewById(R.id.spOnce);
final LinearLayout llWhiteList = (LinearLayout) view.findViewById(R.id.llWhiteList);
final CheckBox cbWhitelist = (CheckBox) view.findViewById(R.id.cbWhitelist);
final CheckBox cbWhitelistExtra1 = (CheckBox) view.findViewById(R.id.cbWhitelistExtra1);
final CheckBox cbWhitelistExtra2 = (CheckBox) view.findViewById(R.id.cbWhitelistExtra2);
final CheckBox cbWhitelistExtra3 = (CheckBox) view.findViewById(R.id.cbWhitelistExtra3);
ProgressBar mProgress = (ProgressBar) view.findViewById(R.id.pbProgress);
Button btnDeny = (Button) view.findViewById(R.id.btnDeny);
Button btnDontKnow = (Button) view.findViewById(R.id.btnDontKnow);
Button btnAllow = (Button) view.findViewById(R.id.btnAllow);
final int userId = Util.getUserId(Process.myUid());
boolean expert = getSettingBool(userId, PrivacyManager.cSettingODExpert, false);
boolean category = getSettingBool(userId, PrivacyManager.cSettingODCategory, true);
boolean once = getSettingBool(userId, PrivacyManager.cSettingODOnce, false);
expert = expert || !category || once;
final boolean whitelistDangerous = (hook != null && hook.isDangerous() && hook.whitelist() != null);
// Set values
if ((hook != null && hook.isDangerous()) || appInfo.isSystem())
view.setBackgroundResource(R.color.color_dangerous_dialog);
else
view.setBackgroundResource(android.R.color.background_dark);
// Application information
ivAppIcon.setImageDrawable(appInfo.getIcon(context));
tvUid.setText(Integer.toString(appInfo.getUid()));
tvAppName.setText(TextUtils.join(", ", appInfo.getApplicationName()));
// Restriction information
int catId = resources.getIdentifier("restrict_" + restriction.restrictionName, "string", self);
tvCategory.setText(resources.getString(catId));
tvFunction.setText(restriction.methodName);
if (restriction.extra == null)
rowParameters.setVisibility(View.GONE);
else
tvParameters.setText(restriction.extra);
String defaultAction = resources.getString(result.restricted ? R.string.title_deny : R.string.title_allow);
tvDefault.setText(defaultAction);
// Help
int helpId = resources.getIdentifier("restrict_help_" + restriction.restrictionName, "string", self);
tvInfoCategory.setText(resources.getString(helpId));
// Expert mode
cbExpert.setChecked(expert);
if (expert) {
for (View child : Util.getViewsByTag((ViewGroup) view, "details"))
child.setVisibility(View.VISIBLE);
for (View child : Util.getViewsByTag((ViewGroup) view, "nodetails"))
child.setVisibility(View.GONE);
}
if (expert || whitelistDangerous)
llWhiteList.setVisibility(View.VISIBLE);
// Category
cbCategory.setChecked(category);
// Once
cbOnce.setChecked(once);
int osel = Integer
.parseInt(getSetting(new PSetting(userId, "", PrivacyManager.cSettingODOnceDuration, "0")).value);
spOnce.setSelection(osel);
// Whitelisting
if (hook != null && hook.whitelist() != null && restriction.extra != null) {
cbWhitelist.setText(resources.getString(R.string.title_whitelist, restriction.extra));
cbWhitelist.setVisibility(View.VISIBLE);
String[] xextra = getXExtra(restriction, hook);
if (xextra.length > 0) {
cbWhitelistExtra1.setText(resources.getString(R.string.title_whitelist, xextra[0]));
cbWhitelistExtra1.setVisibility(View.VISIBLE);
}
if (xextra.length > 1) {
cbWhitelistExtra2.setText(resources.getString(R.string.title_whitelist, xextra[1]));
cbWhitelistExtra2.setVisibility(View.VISIBLE);
}
if (xextra.length > 2) {
cbWhitelistExtra3.setText(resources.getString(R.string.title_whitelist, xextra[2]));
cbWhitelistExtra3.setVisibility(View.VISIBLE);
}
}
cbExpert.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
setSettingBool(userId, "", PrivacyManager.cSettingODExpert, isChecked);
if (!isChecked) {
setSettingBool(userId, "", PrivacyManager.cSettingODCategory, true);
setSettingBool(userId, "", PrivacyManager.cSettingODOnce, false);
cbCategory.setChecked(true);
cbOnce.setChecked(false);
cbWhitelist.setChecked(false);
cbWhitelistExtra1.setChecked(false);
cbWhitelistExtra2.setChecked(false);
cbWhitelistExtra3.setChecked(false);
}
for (View child : Util.getViewsByTag((ViewGroup) view, "details"))
child.setVisibility(isChecked ? View.VISIBLE : View.GONE);
for (View child : Util.getViewsByTag((ViewGroup) view, "nodetails"))
child.setVisibility(isChecked ? View.GONE : View.VISIBLE);
if (!whitelistDangerous)
llWhiteList.setVisibility(isChecked ? View.VISIBLE : View.GONE);
}
});
// Category, once and whitelist exclude each other
cbCategory.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
cbWhitelist.setChecked(false);
cbWhitelistExtra1.setChecked(false);
cbWhitelistExtra2.setChecked(false);
cbWhitelistExtra3.setChecked(false);
}
}
});
cbOnce.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
cbWhitelist.setChecked(false);
cbWhitelistExtra1.setChecked(false);
cbWhitelistExtra2.setChecked(false);
cbWhitelistExtra3.setChecked(false);
}
}
});
cbWhitelist.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
cbCategory.setChecked(false);
cbOnce.setChecked(false);
cbWhitelistExtra1.setChecked(false);
cbWhitelistExtra2.setChecked(false);
cbWhitelistExtra3.setChecked(false);
}
}
});
cbWhitelistExtra1.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
cbCategory.setChecked(false);
cbOnce.setChecked(false);
cbWhitelist.setChecked(false);
cbWhitelistExtra2.setChecked(false);
cbWhitelistExtra3.setChecked(false);
}
}
});
cbWhitelistExtra2.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
cbCategory.setChecked(false);
cbOnce.setChecked(false);
cbWhitelist.setChecked(false);
cbWhitelistExtra1.setChecked(false);
cbWhitelistExtra3.setChecked(false);
}
}
});
cbWhitelistExtra3.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
cbCategory.setChecked(false);
cbOnce.setChecked(false);
cbWhitelist.setChecked(false);
cbWhitelistExtra1.setChecked(false);
cbWhitelistExtra2.setChecked(false);
}
}
});
// Setup progress bar
mProgress.setMax(cMaxOnDemandDialog * 20);
mProgress.setProgress(cMaxOnDemandDialog * 20);
btnAllow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Allow
result.restricted = false;
result.asked = true;
if (cbWhitelist.isChecked())
onDemandWhitelist(restriction, null, result, oResult, hook);
else if (cbWhitelistExtra1.isChecked())
onDemandWhitelist(restriction, getXExtra(restriction, hook)[0], result, oResult, hook);
else if (cbWhitelistExtra2.isChecked())
onDemandWhitelist(restriction, getXExtra(restriction, hook)[1], result, oResult, hook);
else if (cbWhitelistExtra3.isChecked())
onDemandWhitelist(restriction, getXExtra(restriction, hook)[2], result, oResult, hook);
else {
setSettingBool(userId, "", PrivacyManager.cSettingODCategory, cbCategory.isChecked());
setSettingBool(userId, "", PrivacyManager.cSettingODOnce, cbOnce.isChecked());
if (cbOnce.isChecked())
onDemandOnce(restriction, cbCategory.isChecked(), result, oResult, spOnce);
else
onDemandChoice(restriction, cbCategory.isChecked(), false);
}
holder.latch.countDown();
}
});
btnDontKnow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Deny once
result.restricted = !(hook != null && hook.isDangerous());
result.asked = true;
onDemandOnce(restriction, false, result, oResult, spOnce);
holder.latch.countDown();
}
});
btnDeny.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Deny
result.restricted = true;
result.asked = true;
if (cbWhitelist.isChecked())
onDemandWhitelist(restriction, null, result, oResult, hook);
else if (cbWhitelistExtra1.isChecked())
onDemandWhitelist(restriction, getXExtra(restriction, hook)[0], result, oResult, hook);
else if (cbWhitelistExtra2.isChecked())
onDemandWhitelist(restriction, getXExtra(restriction, hook)[1], result, oResult, hook);
else if (cbWhitelistExtra3.isChecked())
onDemandWhitelist(restriction, getXExtra(restriction, hook)[2], result, oResult, hook);
else {
setSettingBool(userId, "", PrivacyManager.cSettingODCategory, cbCategory.isChecked());
setSettingBool(userId, "", PrivacyManager.cSettingODOnce, cbOnce.isChecked());
if (cbOnce.isChecked())
onDemandOnce(restriction, cbCategory.isChecked(), result, oResult, spOnce);
else
onDemandChoice(restriction, cbCategory.isChecked(), true);
}
holder.latch.countDown();
}
});
return view;
}
private String[] getXExtra(PRestriction restriction, Hook hook) {
List<String> listResult = new ArrayList<String>();
if (restriction.extra != null && hook != null)
if (hook.whitelist().equals(Meta.cTypeFilename)) {
// Top folders of file name
File file = new File(restriction.extra);
for (int i = 1; i <= 3 && file != null; i++) {
String parent = file.getParent();
if (!TextUtils.isEmpty(parent))
listResult.add(parent + File.separatorChar + "*");
file = file.getParentFile();
}
} else if (hook.whitelist().equals(Meta.cTypeIPAddress)) {
// sub-domain or sub-net
int colon = restriction.extra.lastIndexOf(':');
String address = (colon >= 0 ? restriction.extra.substring(0, colon) : restriction.extra);
if (Patterns.IP_ADDRESS.matcher(address).matches()) {
int dot = address.lastIndexOf('.');
listResult.add(address.substring(0, dot) + ".*"
+ (colon >= 0 ? restriction.extra.substring(colon) : ""));
if (colon >= 0)
listResult.add(address.substring(0, dot) + ".*:*");
} else {
int dot = restriction.extra.indexOf('.');
if (dot > 0) {
listResult.add('*' + restriction.extra.substring(dot));
if (colon >= 0)
listResult.add('*' + restriction.extra.substring(dot, colon) + ":*");
}
}
} else if (hook.whitelist().equals(Meta.cTypeUrl)) {
// Top folders of file name
Uri uri = Uri.parse(restriction.extra);
if ("file".equals(uri.getScheme())) {
File file = new File(uri.getPath());
for (int i = 1; i <= 3 && file != null; i++) {
String parent = file.getParent();
if (!TextUtils.isEmpty(parent))
listResult.add(parent + File.separatorChar + "*");
file = file.getParentFile();
}
}
} else if (hook.whitelist().equals(Meta.cTypeMethod) || hook.whitelist().equals(Meta.cTypeTransaction)
|| hook.whitelist().equals(Meta.cTypeAction)) {
String[] component = restriction.extra.split(":");
if (component.length == 2)
listResult.add(component[0] + ":*");
}
return listResult.toArray(new String[0]);
}
private void onDemandWhitelist(PRestriction restriction, String xextra, PRestriction result,
OnDemandResult oResult, Hook hook) {
try {
Util.log(null, Log.WARN, (result.restricted ? "Black" : "White") + "listing " + restriction + " xextra="
+ xextra);
oResult.whitelist = true;
// Set white/black list
setSettingInternal(new PSetting(restriction.uid, hook.whitelist(), (xextra == null ? restriction.extra
: xextra), Boolean.toString(!result.restricted)));
if (!PrivacyManager.getSettingBool(restriction.uid, PrivacyManager.cSettingWhitelistNoModify, false))
if (restriction.methodName == null || !PrivacyManager.cMethodNoState.contains(restriction.methodName)) {
// Mark state as changed
setSettingInternal(new PSetting(restriction.uid, "", PrivacyManager.cSettingState,
Integer.toString(ApplicationInfoEx.STATE_CHANGED)));
// Update modification time
setSettingInternal(new PSetting(restriction.uid, "", PrivacyManager.cSettingModifyTime,
Long.toString(System.currentTimeMillis())));
}
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
private void onDemandOnce(PRestriction restriction, boolean category, PRestriction result, OnDemandResult oResult,
Spinner spOnce) {
oResult.once = true;
// Get duration
String value = (String) spOnce.getSelectedItem();
if (value == null)
result.time = new Date().getTime() + PrivacyManager.cRestrictionCacheTimeoutMs;
else {
char unit = value.charAt(value.length() - 1);
value = value.substring(0, value.length() - 1);
if (unit == 's')
result.time = new Date().getTime() + Integer.parseInt(value) * 1000;
else if (unit == 'm')
result.time = new Date().getTime() + Integer.parseInt(value) * 60 * 1000;
else
result.time = new Date().getTime() + PrivacyManager.cRestrictionCacheTimeoutMs;
try {
int userId = Util.getUserId(restriction.uid);
String sel = Integer.toString(spOnce.getSelectedItemPosition());
setSettingInternal(new PSetting(userId, "", PrivacyManager.cSettingODOnceDuration, sel));
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
Util.log(null, Log.WARN, (result.restricted ? "Deny" : "Allow") + " once " + restriction + " category="
+ category + " until=" + new Date(result.time));
CRestriction key = new CRestriction(result, null);
key.setExpiry(result.time);
if (category) {
key.setMethodName(null);
key.setExtra(null);
}
synchronized (mAskedOnceCache) {
if (mAskedOnceCache.containsKey(key))
mAskedOnceCache.remove(key);
mAskedOnceCache.put(key, key);
}
}
private void onDemandChoice(PRestriction restriction, boolean category, boolean restrict) {
try {
PRestriction result = new PRestriction(restriction);
// Get current category restriction state
boolean prevRestricted = false;
CRestriction key = new CRestriction(restriction.uid, restriction.restrictionName, null, null);
synchronized (mRestrictionCache) {
if (mRestrictionCache.containsKey(key))
prevRestricted = mRestrictionCache.get(key).restricted;
}
Util.log(null, Log.WARN, "On demand choice " + restriction + " category=" + category + " restrict="
+ restrict + " prev=" + prevRestricted);
if (category || (restrict && restrict != prevRestricted)) {
// Set category restriction
result.methodName = null;
result.restricted = restrict;
result.asked = category;
setRestrictionInternal(result);
// Clear category on change
for (Hook hook : PrivacyManager.getHooks(restriction.restrictionName, null))
if (!PrivacyManager.canRestrict(restriction.uid, getXUid(), restriction.restrictionName,
hook.getName(), false)) {
result.methodName = hook.getName();
result.restricted = false;
result.asked = true;
setRestrictionInternal(result);
} else {
result.methodName = hook.getName();
result.restricted = restrict && !hook.isDangerous();
result.asked = category || (hook.isDangerous() && hook.whitelist() == null);
setRestrictionInternal(result);
}
}
if (!category) {
// Set method restriction
result.methodName = restriction.methodName;
result.restricted = restrict;
result.asked = true;
result.extra = restriction.extra;
setRestrictionInternal(result);
}
// Mark state as changed
setSettingInternal(new PSetting(restriction.uid, "", PrivacyManager.cSettingState,
Integer.toString(ApplicationInfoEx.STATE_CHANGED)));
// Update modification time
setSettingInternal(new PSetting(restriction.uid, "", PrivacyManager.cSettingModifyTime,
Long.toString(System.currentTimeMillis())));
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
private void notifyRestricted(final PRestriction restriction) {
final Context context = getContext();
if (context != null && mHandler != null)
mHandler.post(new Runnable() {
@Override
public void run() {
long token = 0;
try {
token = Binder.clearCallingIdentity();
// Get resources
String self = PrivacyService.class.getPackage().getName();
Resources resources = context.getPackageManager().getResourcesForApplication(self);
// Notify user
String text = resources.getString(R.string.msg_restrictedby);
text += " (" + restriction.uid + " " + restriction.restrictionName + "/"
+ restriction.methodName + ")";
Toast.makeText(context, text, Toast.LENGTH_LONG).show();
} catch (NameNotFoundException ex) {
Util.bug(null, ex);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
private void notifyException(Throwable ex) {
Util.bug(null, ex);
if (mNotified)
return;
Context context = getContext();
if (context == null)
return;
try {
Intent intent = new Intent("biz.bokhorst.xprivacy.action.EXCEPTION");
intent.putExtra("Message", ex.toString());
context.sendBroadcast(intent);
NotificationManager notificationManager = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
// Build notification
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context);
notificationBuilder.setSmallIcon(R.drawable.ic_launcher);
notificationBuilder.setContentTitle(context.getString(R.string.app_name));
notificationBuilder.setContentText(ex.toString());
notificationBuilder.setWhen(System.currentTimeMillis());
notificationBuilder.setAutoCancel(true);
Notification notification = notificationBuilder.build();
// Display notification
notificationManager.notify(Util.NOTIFY_CORRUPT, notification);
mNotified = true;
} catch (Throwable exex) {
Util.bug(null, exex);
}
}
private boolean getSettingBool(int uid, String name, boolean defaultValue) throws RemoteException {
return getSettingBool(uid, "", name, defaultValue);
}
private boolean getSettingBool(int uid, String type, String name, boolean defaultValue) throws RemoteException {
String value = getSetting(new PSetting(uid, type, name, Boolean.toString(defaultValue))).value;
return Boolean.parseBoolean(value);
}
private void setSettingBool(int uid, String type, String name, boolean value) {
try {
setSettingInternal(new PSetting(uid, type, name, Boolean.toString(value)));
} catch (RemoteException ex) {
Util.bug(null, ex);
}
}
private void enforcePermission(int uid) {
if (uid >= 0)
if (Util.getUserId(uid) != Util.getUserId(Binder.getCallingUid()))
throw new SecurityException("uid=" + uid + " calling=" + Binder.getCallingUid());
int callingUid = Util.getAppId(Binder.getCallingUid());
if (callingUid != getXUid() && callingUid != Process.SYSTEM_UID)
throw new SecurityException("xuid=" + mXUid + " calling=" + Binder.getCallingUid());
}
private boolean isAMLocked(int uid) {
try {
Object am;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
am = mAm;
else {
Class<?> cam = Class.forName("com.android.server.am.ActivityManagerService");
am = cam.getMethod("self").invoke(null);
}
boolean locked = Thread.holdsLock(am);
if (locked) {
boolean freeze = getSettingBool(uid, PrivacyManager.cSettingFreeze, false);
if (!freeze)
freeze = getSettingBool(0, PrivacyManager.cSettingFreeze, false);
if (freeze)
return false;
}
return locked;
} catch (Throwable ex) {
Util.bug(null, ex);
return false;
}
}
private Context getContext() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
return mContext;
else {
// public static ActivityManagerService self()
// frameworks/base/services/java/com/android/server/am/ActivityManagerService.java
try {
Class<?> cam = Class.forName("com.android.server.am.ActivityManagerService");
Object am = cam.getMethod("self").invoke(null);
if (am == null)
return null;
Field mContext = cam.getDeclaredField("mContext");
mContext.setAccessible(true);
return (Context) mContext.get(am);
} catch (Throwable ex) {
Util.bug(null, ex);
return null;
}
}
}
private int getIsolatedUid(int uid) {
if (PrivacyManager.isIsolated(uid))
try {
Field fmIsolatedProcesses = null;
Class<?> cam = mAm.getClass();
while (cam != null && fmIsolatedProcesses == null)
try {
fmIsolatedProcesses = cam.getDeclaredField("mIsolatedProcesses");
} catch (NoSuchFieldException ignored) {
cam = cam.getSuperclass();
}
if (fmIsolatedProcesses == null)
throw new Exception(mAm.getClass().getName() + ".mIsolatedProcesses not found");
fmIsolatedProcesses.setAccessible(true);
SparseArray<?> mIsolatedProcesses = (SparseArray<?>) fmIsolatedProcesses.get(mAm);
Object processRecord = mIsolatedProcesses.get(uid);
Field fInfo = processRecord.getClass().getDeclaredField("info");
fInfo.setAccessible(true);
ApplicationInfo info = (ApplicationInfo) fInfo.get(processRecord);
Util.log(null, Log.WARN, "Translated isolated uid=" + uid + " into application uid=" + info.uid
+ " pkg=" + info.packageName);
return info.uid;
} catch (Throwable ex) {
Util.bug(null, ex);
}
return uid;
}
private int getXUid() {
if (mXUid < 0)
try {
Context context = getContext();
if (context != null) {
PackageManager pm = context.getPackageManager();
if (pm != null) {
String self = PrivacyService.class.getPackage().getName();
ApplicationInfo xInfo = pm.getApplicationInfo(self, 0);
mXUid = xInfo.uid;
}
}
} catch (Throwable ignored) {
// The package manager may not be up-to-date yet
}
return mXUid;
}
private File getDbFile() {
return new File(Environment.getDataDirectory() + File.separator + "system" + File.separator + "xprivacy"
+ File.separator + "xprivacy.db");
}
private File getDbUsageFile() {
return new File(Environment.getDataDirectory() + File.separator + "system" + File.separator + "xprivacy"
+ File.separator + "usage.db");
}
private void setupDatabase() {
try {
File dbFile = getDbFile();
// Create database folder
dbFile.getParentFile().mkdirs();
// Check database folder
if (dbFile.getParentFile().isDirectory())
Util.log(null, Log.WARN, "Database folder=" + dbFile.getParentFile());
else
Util.log(null, Log.WARN, "Does not exist folder=" + dbFile.getParentFile());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// Move database from data/xprivacy folder
File folder = new File(Environment.getDataDirectory() + File.separator + "xprivacy");
if (folder.exists()) {
File[] oldFiles = folder.listFiles();
if (oldFiles != null)
for (File file : oldFiles)
if (file.getName().startsWith("xprivacy.db") || file.getName().startsWith("usage.db")) {
File target = new File(dbFile.getParentFile() + File.separator + file.getName());
boolean status = Util.move(file, target);
Util.log(null, Log.WARN, "Moved " + file + " to " + target + " ok=" + status);
}
Util.log(null, Log.WARN, "Deleting folder=" + folder);
folder.delete();
}
// Move database from data/application folder
folder = new File(Environment.getDataDirectory() + File.separator + "data" + File.separator
+ PrivacyService.class.getPackage().getName());
if (folder.exists()) {
File[] oldFiles = folder.listFiles();
if (oldFiles != null)
for (File file : oldFiles)
if (file.getName().startsWith("xprivacy.db")) {
File target = new File(dbFile.getParentFile() + File.separator + file.getName());
boolean status = Util.move(file, target);
Util.log(null, Log.WARN, "Moved " + file + " to " + target + " ok=" + status);
}
Util.log(null, Log.WARN, "Deleting folder=" + folder);
folder.delete();
}
}
// Set database file permissions
// Owner: rwx (system)
// Group: rwx (system)
// World: ---
Util.setPermissions(dbFile.getParentFile().getAbsolutePath(), 0770, Process.SYSTEM_UID, Process.SYSTEM_UID);
File[] files = dbFile.getParentFile().listFiles();
if (files != null)
for (File file : files)
if (file.getName().startsWith("xprivacy.db") || file.getName().startsWith("usage.db"))
Util.setPermissions(file.getAbsolutePath(), 0770, Process.SYSTEM_UID, Process.SYSTEM_UID);
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
private SQLiteDatabase getDb() {
synchronized (this) {
// Check current reference
if (mDb != null && !mDb.isOpen()) {
mDb = null;
Util.log(null, Log.ERROR, "Database not open");
}
if (mDb == null)
try {
setupDatabase();
// Create/upgrade database when needed
File dbFile = getDbFile();
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
// Check database integrity
if (db.isDatabaseIntegrityOk())
Util.log(null, Log.WARN, "Database integrity ok");
else {
// http://www.sqlite.org/howtocorrupt.html
mCorrupt = true;
Util.log(null, Log.ERROR, "Database corrupt");
Cursor cursor = db.rawQuery("PRAGMA integrity_check", null);
try {
while (cursor.moveToNext()) {
String message = cursor.getString(0);
Util.log(null, Log.ERROR, message);
}
} finally {
cursor.close();
}
db.close();
// Backup database file
File dbBackup = new File(dbFile.getParentFile() + File.separator + "xprivacy.backup");
dbBackup.delete();
dbFile.renameTo(dbBackup);
File dbJournal = new File(dbFile.getAbsolutePath() + "-journal");
File dbJournalBackup = new File(dbBackup.getAbsolutePath() + "-journal");
dbJournalBackup.delete();
dbJournal.renameTo(dbJournalBackup);
Util.log(null, Log.ERROR, "Old database backup: " + dbBackup.getAbsolutePath());
// Create new database
db = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
Util.log(null, Log.ERROR, "New, empty database created");
}
// Update migration status
if (db.getVersion() > 1) {
Util.log(null, Log.WARN, "Updating migration status");
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
ContentValues values = new ContentValues();
values.put("uid", 0);
if (db.getVersion() > 9)
values.put("type", "");
values.put("name", PrivacyManager.cSettingMigrated);
values.put("value", Boolean.toString(true));
db.insertWithOnConflict(cTableSetting, null, values, SQLiteDatabase.CONFLICT_REPLACE);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
// Upgrade database if needed
if (db.needUpgrade(1)) {
Util.log(null, Log.WARN, "Creating database");
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
// http://www.sqlite.org/lang_createtable.html
db.execSQL("CREATE TABLE restriction (uid INTEGER NOT NULL, restriction TEXT NOT NULL, method TEXT NOT NULL, restricted INTEGER NOT NULL)");
db.execSQL("CREATE TABLE setting (uid INTEGER NOT NULL, name TEXT NOT NULL, value TEXT)");
db.execSQL("CREATE TABLE usage (uid INTEGER NOT NULL, restriction TEXT NOT NULL, method TEXT NOT NULL, restricted INTEGER NOT NULL, time INTEGER NOT NULL)");
db.execSQL("CREATE UNIQUE INDEX idx_restriction ON restriction(uid, restriction, method)");
db.execSQL("CREATE UNIQUE INDEX idx_setting ON setting(uid, name)");
db.execSQL("CREATE UNIQUE INDEX idx_usage ON usage(uid, restriction, method)");
db.setVersion(1);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(2)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
// Old migrated indication
db.setVersion(2);
}
if (db.needUpgrade(3)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("DELETE FROM usage WHERE method=''");
db.setVersion(3);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(4)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("DELETE FROM setting WHERE value IS NULL");
db.setVersion(4);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(5)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("DELETE FROM setting WHERE value = ''");
db.execSQL("DELETE FROM setting WHERE name = 'Random@boot' AND value = 'false'");
db.setVersion(5);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(6)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("DELETE FROM setting WHERE name LIKE 'OnDemand.%'");
db.setVersion(6);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(7)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("ALTER TABLE usage ADD COLUMN extra TEXT");
db.setVersion(7);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(8)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("DROP INDEX idx_usage");
db.execSQL("CREATE UNIQUE INDEX idx_usage ON usage(uid, restriction, method, extra)");
db.setVersion(8);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(9)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("DROP TABLE usage");
db.setVersion(9);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(10)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
db.execSQL("ALTER TABLE setting ADD COLUMN type TEXT");
db.execSQL("DROP INDEX idx_setting");
db.execSQL("CREATE UNIQUE INDEX idx_setting ON setting(uid, type, name)");
db.execSQL("UPDATE setting SET type=''");
db.setVersion(10);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
if (db.needUpgrade(11)) {
Util.log(null, Log.WARN, "Upgrading from version=" + db.getVersion());
mLock.writeLock().lock();
try {
db.beginTransaction();
try {
List<PSetting> listSetting = new ArrayList<PSetting>();
Cursor cursor = db.query(cTableSetting, new String[] { "uid", "name", "value" }, null,
null, null, null, null);
if (cursor != null)
try {
while (cursor.moveToNext()) {
int uid = cursor.getInt(0);
String name = cursor.getString(1);
String value = cursor.getString(2);
if (name.startsWith("Account.") || name.startsWith("Application.")
|| name.startsWith("Contact.") || name.startsWith("Template.")) {
int dot = name.indexOf('.');
String type = name.substring(0, dot);
listSetting
.add(new PSetting(uid, type, name.substring(dot + 1), value));
listSetting.add(new PSetting(uid, "", name, null));
} else if (name.startsWith("Whitelist.")) {
String[] component = name.split("\\.");
listSetting.add(new PSetting(uid, component[1], name.replace(
component[0] + "." + component[1] + ".", ""), value));
listSetting.add(new PSetting(uid, "", name, null));
}
}
} finally {
cursor.close();
}
for (PSetting setting : listSetting) {
Util.log(null, Log.WARN, "Converting " + setting);
if (setting.value == null)
db.delete(cTableSetting, "uid=? AND type=? AND name=?",
new String[] { Integer.toString(setting.uid), setting.type,
setting.name });
else {
// Create record
ContentValues values = new ContentValues();
values.put("uid", setting.uid);
values.put("type", setting.type);
values.put("name", setting.name);
values.put("value", setting.value);
// Insert/update record
db.insertWithOnConflict(cTableSetting, null, values,
SQLiteDatabase.CONFLICT_REPLACE);
}
}
db.setVersion(11);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} finally {
mLock.writeLock().unlock();
}
}
Util.log(null, Log.WARN, "Running VACUUM");
mLock.writeLock().lock();
try {
db.execSQL("VACUUM");
} catch (Throwable ex) {
Util.bug(null, ex);
} finally {
mLock.writeLock().unlock();
}
Util.log(null, Log.WARN, "Database version=" + db.getVersion());
mDb = db;
} catch (Throwable ex) {
mDb = null; // retry
Util.bug(null, ex);
try {
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(
"/cache/xprivacy.log", true));
outputStreamWriter.write(ex.toString());
outputStreamWriter.write("\n");
outputStreamWriter.write(Log.getStackTraceString(ex));
outputStreamWriter.write("\n");
outputStreamWriter.close();
} catch (Throwable exex) {
Util.bug(null, exex);
}
}
return mDb;
}
}
private SQLiteDatabase getDbUsage() {
synchronized (this) {
// Check current reference
if (mDbUsage != null && !mDbUsage.isOpen()) {
mDbUsage = null;
Util.log(null, Log.ERROR, "Usage database not open");
}
if (mDbUsage == null)
try {
// Create/upgrade database when needed
File dbUsageFile = getDbUsageFile();
SQLiteDatabase dbUsage = SQLiteDatabase.openOrCreateDatabase(dbUsageFile, null);
// Check database integrity
if (dbUsage.isDatabaseIntegrityOk())
Util.log(null, Log.WARN, "Usage database integrity ok");
else {
dbUsage.close();
dbUsageFile.delete();
new File(dbUsageFile + "-journal").delete();
dbUsage = SQLiteDatabase.openOrCreateDatabase(dbUsageFile, null);
Util.log(null, Log.WARN, "Deleted corrupt usage data database");
}
// Upgrade database if needed
if (dbUsage.needUpgrade(1)) {
Util.log(null, Log.WARN, "Creating usage database");
mLockUsage.writeLock().lock();
try {
dbUsage.beginTransaction();
try {
dbUsage.execSQL("CREATE TABLE usage (uid INTEGER NOT NULL, restriction TEXT NOT NULL, method TEXT NOT NULL, extra TEXT NOT NULL, restricted INTEGER NOT NULL, time INTEGER NOT NULL)");
dbUsage.execSQL("CREATE UNIQUE INDEX idx_usage ON usage(uid, restriction, method, extra)");
dbUsage.setVersion(1);
dbUsage.setTransactionSuccessful();
} finally {
dbUsage.endTransaction();
}
} finally {
mLockUsage.writeLock().unlock();
}
}
if (dbUsage.needUpgrade(2)) {
Util.log(null, Log.WARN, "Upgrading usage database from version=" + dbUsage.getVersion());
mLockUsage.writeLock().lock();
try {
dbUsage.beginTransaction();
try {
dbUsage.execSQL("ALTER TABLE usage ADD COLUMN value TEXT");
dbUsage.setVersion(2);
dbUsage.setTransactionSuccessful();
} finally {
dbUsage.endTransaction();
}
} finally {
mLockUsage.writeLock().unlock();
}
}
Util.log(null, Log.WARN, "Changing to asynchronous mode");
try {
dbUsage.rawQuery("PRAGMA synchronous=OFF", null);
} catch (Throwable ex) {
Util.bug(null, ex);
}
Util.log(null, Log.WARN, "Usage database version=" + dbUsage.getVersion());
mDbUsage = dbUsage;
} catch (Throwable ex) {
mDbUsage = null; // retry
Util.bug(null, ex);
}
return mDbUsage;
}
}
}