package biz.bokhorst.xprivacy;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.annotation.SuppressLint;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Process;
import android.util.Log;

// This code is legacy and only used for updating to newer versions

@SuppressWarnings("deprecation")
@SuppressLint({ "DefaultLocale", "WorldReadableFiles", "Registered" })
public class PrivacyProvider extends ContentProvider {
	private static final String AUTHORITY = "biz.bokhorst.xprivacy.provider";
	private static final String PREF_RESTRICTION = AUTHORITY;
	private static final String PREF_USAGE = AUTHORITY + ".usage";
	private static final String PREF_SETTINGS = AUTHORITY + ".settings";
	private static final String PATH_RESTRICTION = "restriction";
	private static final String PATH_USAGE = "usage";
	private static final String PATH_SETTINGS = "settings";

	public static final Uri URI_RESTRICTION = Uri.parse("content://" + AUTHORITY + "/" + PATH_RESTRICTION);
	public static final Uri URI_USAGE = Uri.parse("content://" + AUTHORITY + "/" + PATH_USAGE);
	public static final Uri URI_SETTING = Uri.parse("content://" + AUTHORITY + "/" + PATH_SETTINGS);

	public static final String COL_UID = "Uid";
	public static final String COL_RESTRICTION = "Restriction";
	public static final String COL_RESTRICTED = "Restricted";
	public static final String COL_METHOD = "Method";
	public static final String COL_USED = "Used";
	public static final String COL_SETTING = "Setting";
	public static final String COL_VALUE = "Value";

	private static final UriMatcher sUriMatcher;
	private static final int TYPE_RESTRICTION = 1;
	private static final int TYPE_USAGE = 2;
	private static final int TYPE_SETTING = 3;

	private static Object mFallbackRestrictionLock = new Object();
	private static Object mFallbackSettingsLock = new Object();
	private static int mFallbackRestrictionsUid = 0;
	private static long mFallbackRestrictionsTime = 0;
	private static long mFallbackSettingsTime = 0;
	private static SharedPreferencesEx mFallbackRestrictions = null;
	private static SharedPreferencesEx mFallbackSettings = null;

	private static ExecutorService mExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

	static {
		sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
		sUriMatcher.addURI(AUTHORITY, PATH_RESTRICTION, TYPE_RESTRICTION);
		sUriMatcher.addURI(AUTHORITY, PATH_USAGE, TYPE_USAGE);
		sUriMatcher.addURI(AUTHORITY, PATH_SETTINGS, TYPE_SETTING);
	}

	@Override
	public boolean onCreate() {
		try {
			convertRestrictions(getContext());
			convertSettings(getContext());
			fixFilePermissions();
		} catch (Throwable ex) {
			Util.bug(null, ex);
		}
		return true;
	}

	@Override
	public String getType(Uri uri) {
		if (sUriMatcher.match(uri) == TYPE_RESTRICTION)
			return String.format("vnd.android.cursor.dir/%s.%s", AUTHORITY, PATH_RESTRICTION);
		else if (sUriMatcher.match(uri) == TYPE_USAGE)
			return String.format("vnd.android.cursor.dir/%s.%s", AUTHORITY, PATH_USAGE);
		else if (sUriMatcher.match(uri) == TYPE_SETTING)
			return String.format("vnd.android.cursor.dir/%s.%s", AUTHORITY, PATH_SETTINGS);
		throw new IllegalArgumentException();
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
		if (sUriMatcher.match(uri) == TYPE_RESTRICTION && selectionArgs != null && selectionArgs.length >= 2) {
			// Query restrictions
			String restrictionName = selection;
			int uid = Integer.parseInt(selectionArgs[0]);
			boolean usage = Boolean.parseBoolean(selectionArgs[1]);
			String methodName = (selectionArgs.length >= 3 ? selectionArgs[2] : null);

			return queryRestrictions(uid, restrictionName, methodName, usage);
		} else if (sUriMatcher.match(uri) == TYPE_USAGE && selectionArgs != null && selectionArgs.length >= 1) {
			// Query usage
			String restrictionName = selection;
			int uid = Integer.parseInt(selectionArgs[0]);
			String methodName = (selectionArgs.length >= 2 ? selectionArgs[1] : null);

			return queryUsage(uid, restrictionName, methodName);
		} else if (sUriMatcher.match(uri) == TYPE_SETTING && selectionArgs == null)
			return querySettings(selection);

		throw new IllegalArgumentException(uri.toString());
	}

	private Cursor queryRestrictions(final int uid, final String restrictionName, final String methodName, boolean usage) {
		MatrixCursor cursor = new MatrixCursor(new String[] { COL_UID, COL_RESTRICTION, COL_METHOD, COL_RESTRICTED });

		// Build restriction list
		List<String> listRestrictionName;
		if (restrictionName == null)
			listRestrictionName = PrivacyManager.getRestrictions();
		else {
			listRestrictionName = new ArrayList<String>();
			listRestrictionName.add(restrictionName);
		}

		if (uid == 0) {
			// Process applications
			PackageManager pm = getContext().getPackageManager();
			for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) {
				SharedPreferences prefs = getContext().getSharedPreferences(PREF_RESTRICTION + "." + appInfo.uid,
						Context.MODE_WORLD_READABLE);

				// Process restrictions
				for (String eRestrictionName : listRestrictionName)
					if (getRestricted(eRestrictionName, null, prefs)) {
						// Category
						cursor.addRow(new Object[] { appInfo.uid, eRestrictionName, null, true });

						// Exceptions
						for (Hook md : PrivacyManager.getHooks(eRestrictionName, null)) {
							boolean restricted = getRestricted(eRestrictionName, md.getName(), prefs);
							if (!restricted || md.isDangerous())
								cursor.addRow(new Object[] { appInfo.uid, eRestrictionName, md.getName(), restricted });
						}
					}
			}
		} else {
			SharedPreferences prefs = getContext().getSharedPreferences(PREF_RESTRICTION + "." + uid,
					Context.MODE_WORLD_READABLE);

			// Process restrictions
			boolean restricted = false;
			if (methodName != null && methodName.equals("*")) {
				for (String eRestrictionName : listRestrictionName) {
					boolean eRestricted = getRestricted(eRestrictionName, null, prefs);
					cursor.addRow(new Object[] { uid, eRestrictionName, null, Boolean.toString(eRestricted) });
					for (Hook md : PrivacyManager.getHooks(eRestrictionName, null)) {
						eRestricted = getRestricted(eRestrictionName, md.getName(), prefs);
						cursor.addRow(new Object[] { uid, eRestrictionName, md.getName(), Boolean.toString(eRestricted) });
					}
				}
			} else {
				for (String eRestrictionName : listRestrictionName) {
					boolean eRestricted = getRestricted(eRestrictionName, methodName, prefs);
					cursor.addRow(new Object[] { uid, eRestrictionName, methodName, Boolean.toString(eRestricted) });
					restricted = (restricted || eRestricted);
				}
			}

			// Update usage data
			if (usage && restrictionName != null && methodName != null && !methodName.equals("*")) {
				final boolean isRestricted = restricted;
				final long timeStamp = new Date().getTime();
				mExecutor.execute(new Runnable() {
					public void run() {
						updateUsage(uid, restrictionName, methodName, isRestricted, timeStamp);
					}
				});
			}
		}

		return cursor;
	}

	private static boolean getRestricted(String restrictionName, String methodName, SharedPreferences prefs) {
		// Check for restriction
		boolean restricted = prefs.getBoolean(getRestrictionPref(restrictionName), false);

		// Check for exception
		if (restricted && methodName != null)
			if (prefs.getBoolean(getExceptionPref(restrictionName, methodName), false))
				restricted = false;

		return restricted;
	}

	private Cursor queryUsage(int uid, String restrictionName, String methodName) {
		MatrixCursor cursor = new MatrixCursor(new String[] { COL_UID, COL_RESTRICTION, COL_METHOD, COL_RESTRICTED,
				COL_USED });

		List<String> listRestriction;
		if (restrictionName == null)
			listRestriction = PrivacyManager.getRestrictions();
		else {
			listRestriction = new ArrayList<String>();
			listRestriction.add(restrictionName);
		}

		if (uid == 0) {
			// All
			for (String eRestrictionName : PrivacyManager.getRestrictions()) {
				SharedPreferences prefs = getContext().getSharedPreferences(PREF_USAGE + "." + eRestrictionName,
						Context.MODE_PRIVATE);
				for (String prefName : prefs.getAll().keySet())
					if (prefName.startsWith(COL_USED)) {
						String[] prefParts = prefName.split("\\.");
						int rUid = Integer.parseInt(prefParts[1]);
						String rMethodName = prefName.substring(prefParts[0].length() + 1 + prefParts[1].length() + 1);
						getUsage(rUid, eRestrictionName, rMethodName, cursor);
					}
			}
		} else {
			// Selected restrictions/methods
			for (String eRestrictionName : listRestriction)
				if (methodName == null)
					for (Hook md : PrivacyManager.getHooks(eRestrictionName, null))
						getUsage(uid, eRestrictionName, md.getName(), cursor);
				else
					getUsage(uid, eRestrictionName, methodName, cursor);
		}
		return cursor;
	}

	private void getUsage(int uid, String restrictionName, String methodName, MatrixCursor cursor) {
		SharedPreferences prefs = getContext().getSharedPreferences(PREF_USAGE + "." + restrictionName,
				Context.MODE_PRIVATE);
		String values = prefs.getString(getUsagePref(uid, methodName), null);
		if (values != null) {
			String[] value = values.split(":");
			long timeStamp = Long.parseLong(value[0]);
			boolean restricted = Boolean.parseBoolean(value[1]);
			cursor.addRow(new Object[] { uid, restrictionName, methodName, restricted, timeStamp });
		}
	}

	private Cursor querySettings(String name) {
		SharedPreferences prefs = getContext().getSharedPreferences(PREF_SETTINGS, Context.MODE_WORLD_READABLE);
		MatrixCursor cursor = new MatrixCursor(new String[] { COL_SETTING, COL_VALUE });
		if (name == null)
			for (String settingKey : prefs.getAll().keySet())
				try {
					cursor.addRow(new Object[] { getSettingName(settingKey), prefs.getString(settingKey, null) });
				} catch (Throwable ex) {
					// Legacy boolean
				}
		else
			cursor.addRow(new Object[] { name, prefs.getString(getSettingPref(name), null) });
		return cursor;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		// Check access
		enforcePermission();

		throw new IllegalArgumentException(uri.toString());
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
		if (sUriMatcher.match(uri) == TYPE_RESTRICTION) {
			// Check access
			enforcePermission();

			// Get arguments
			String restrictionName = selection;
			int uid = values.getAsInteger(COL_UID);
			String methodName = values.getAsString(COL_METHOD);
			boolean restricted = Boolean.parseBoolean(values.getAsString(COL_RESTRICTED));
			updateRestriction(getContext(), uid, restrictionName, methodName, !restricted);

			return 1; // rows
		} else if (sUriMatcher.match(uri) == TYPE_USAGE) {
			Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

			// Get arguments
			int uid = values.getAsInteger(COL_UID);
			String restrictionName = values.getAsString(PrivacyProvider.COL_RESTRICTION);
			String methodName = values.getAsString(COL_METHOD);
			boolean restricted = false;
			if (values.containsKey(PrivacyProvider.COL_RESTRICTED))
				restricted = values.getAsBoolean(PrivacyProvider.COL_RESTRICTED);
			long timeStamp = values.getAsLong(PrivacyProvider.COL_USED);
			Util.log(null, Log.INFO,
					String.format("Update usage data %d/%s/%s=%b", uid, restrictionName, methodName, restricted));

			// Update usage data
			if (methodName != null)
				updateUsage(uid, restrictionName, methodName, restricted, timeStamp);

			return 1;
		} else if (sUriMatcher.match(uri) == TYPE_SETTING) {
			// Check access
			enforcePermission();

			// Get arguments
			String settingName = selection;
			String value = values.getAsString(COL_VALUE);

			// Update setting
			updateSetting(settingName, value);

			return 1;
		}

		throw new IllegalArgumentException(uri.toString());
	}

	private static void updateRestriction(Context context, int uid, String restrictionName, String methodName,
			boolean allowed) {
		// Update restriction
		SharedPreferences prefs = context.getSharedPreferences(PREF_RESTRICTION + "." + uid,
				Context.MODE_WORLD_READABLE);
		SharedPreferences.Editor editor = prefs.edit();
		if (methodName == null || !allowed)
			editor.putBoolean(getRestrictionPref(restrictionName), !allowed);
		if (methodName != null)
			editor.putBoolean(getExceptionPref(restrictionName, methodName), allowed);
		editor.commit();
		setPrefFileReadable(PREF_RESTRICTION, uid);
	}

	private void updateUsage(final int uid, final String restrictionName, final String methodName,
			final boolean restricted, long timeStamp) {
		SharedPreferences prefs = getContext().getSharedPreferences(PREF_USAGE + "." + restrictionName,
				Context.MODE_PRIVATE);
		SharedPreferences.Editor editor = prefs.edit();
		String prefName = getUsagePref(uid, methodName);
		String prefValue = String.format("%d:%b", timeStamp, restricted);
		editor.remove(prefName);
		editor.putString(prefName, prefValue);
		editor.commit();
	}

	private void updateSetting(String name, String value) {
		SharedPreferences prefs = getContext().getSharedPreferences(PREF_SETTINGS, Context.MODE_WORLD_READABLE);
		SharedPreferences.Editor editor = prefs.edit();
		if (value == null)
			editor.remove(getSettingPref(name));
		else
			editor.putString(getSettingPref(name), value);
		editor.commit();
		setPrefFileReadable(PREF_SETTINGS);
	}

	@Override
	public int delete(Uri uri, String where, String[] selectionArgs) {
		// Check access
		enforcePermission();

		if (sUriMatcher.match(uri) == TYPE_RESTRICTION) {
			int uid = Integer.parseInt(selectionArgs[0]);
			return deleteRestrictions(uid);
		} else if (sUriMatcher.match(uri) == TYPE_USAGE) {
			int uid = Integer.parseInt(selectionArgs[0]);
			return deleteUsage(uid);
		} else if (sUriMatcher.match(uri) == TYPE_SETTING) {
			int uid = Integer.parseInt(selectionArgs[0]);
			return deleteSettings(uid);
		}

		throw new IllegalArgumentException(uri.toString());
	}

	private int deleteRestrictions(int uid) {
		int rows = 0;
		SharedPreferences prefs = getContext().getSharedPreferences(PREF_RESTRICTION + "." + uid,
				Context.MODE_WORLD_READABLE);
		SharedPreferences.Editor editor = prefs.edit();
		for (String pref : prefs.getAll().keySet()) {
			Util.log(null, Log.INFO, "Removed restriction=" + pref + " uid=" + uid);
			editor.remove(pref);
			rows++;
		}
		editor.commit();
		setPrefFileReadable(PREF_RESTRICTION, uid);
		return rows;
	}

	private int deleteUsage(int uid) {
		int rows = 0;
		String sUid = Integer.toString(uid);
		for (String restrictionName : PrivacyManager.getRestrictions()) {
			SharedPreferences prefs = getContext().getSharedPreferences(PREF_USAGE + "." + restrictionName,
					Context.MODE_PRIVATE);
			SharedPreferences.Editor editor = prefs.edit();
			for (String pref : prefs.getAll().keySet()) {
				String[] component = pref.split("\\.");
				if (uid == 0 || (component.length >= 2 && component[1].equals(sUid))) {
					Util.log(null, Log.INFO, "Removed usage=" + pref);
					editor.remove(pref);
					rows++;
				}
			}
			editor.commit();
		}
		return rows;
	}

	private int deleteSettings(int uid) {
		int rows = 0;
		String sUid = Integer.toString(uid);
		SharedPreferences prefs = getContext().getSharedPreferences(PREF_SETTINGS, Context.MODE_WORLD_READABLE);
		SharedPreferences.Editor editor = prefs.edit();
		for (String pref : prefs.getAll().keySet()) {
			String[] component = pref.split("\\.");
			if (component.length >= 3 && component[2].equals(sUid)) {
				Util.log(null, Log.INFO, "Removed setting=" + pref + " uid=" + uid);
				editor.remove(pref);
				rows++;
			}
		}
		editor.commit();
		setPrefFileReadable(PREF_SETTINGS);
		return rows;
	}

	// The following methods are used as fallback, when:
	// - there is no context (Java threads)
	// - the content provider cannot be queried (PackageManagerService)

	public static boolean getRestrictedFallback(XHook hook, int uid, String restrictionName, String methodName) {
		try {
			long now = new Date().getTime();
			File file = new File(getPrefFileName(PREF_RESTRICTION, uid));
			if (!file.exists())
				Util.log(null, Log.INFO, "Not found file=" + file.getAbsolutePath());

			synchronized (mFallbackRestrictionLock) {
				if (mFallbackRestrictions == null || mFallbackRestrictionsUid != uid) {
					// Initial load
					mFallbackRestrictions = new SharedPreferencesEx(file);
					mFallbackRestrictionsUid = uid;
					mFallbackRestrictionsTime = now;
					long ms = System.currentTimeMillis() - now;
					Util.log(null, Log.INFO, "Load fallback restrictions uid=" + uid + "/" + mFallbackRestrictionsUid
							+ " " + ms + " ms");
				} else if (mFallbackRestrictionsTime + PrivacyManager.cRestrictionCacheTimeoutMs < now) {
					// Check update
					mFallbackRestrictions.reload();
					mFallbackRestrictionsUid = uid;
					mFallbackRestrictionsTime = now;
					long ms = System.currentTimeMillis() - now;
					Util.log(null, Log.INFO, "Reload fallback restrictions uid=" + uid + " " + ms + " ms");
				}
			}

			return getRestricted(restrictionName, methodName, mFallbackRestrictions);
		} catch (Throwable ex) {
			Util.bug(hook, ex);
			return false;
		}
	}

	public static String getSettingFallback(String name, String defaultValue, boolean log) {
		try {
			long now = new Date().getTime();
			File file = new File(getPrefFileName(PREF_SETTINGS));
			if (!file.exists())
				if (log)
					Util.log(null, Log.INFO, "Not found file=" + file.getAbsolutePath());

			synchronized (mFallbackSettingsLock) {
				// Initial load
				if (mFallbackSettings == null) {
					mFallbackSettings = new SharedPreferencesEx(file);
					mFallbackSettingsTime = now;
					if (log) {
						long ms = System.currentTimeMillis() - now;
						Util.log(null, Log.INFO, "Load fallback settings uid=" + Binder.getCallingUid() + " " + ms
								+ " ms");
					}
				}

				// Get update
				if (mFallbackSettingsTime + PrivacyManager.cSettingCacheTimeoutMs < now) {
					mFallbackSettings.reload();
					mFallbackSettingsTime = now;
					if (log) {
						long ms = System.currentTimeMillis() - now;
						Util.log(null, Log.INFO, "Reload fallback settings uid=" + Binder.getCallingUid() + " " + ms
								+ " ms");
					}
				}
			}

			return mFallbackSettings.getString(getSettingPref(name), defaultValue);
		} catch (Throwable ex) {
			if (log)
				Util.bug(null, ex);
			return defaultValue;
		}
	}

	public static void flush() {
		Util.log(null, Log.INFO, "Flush uid=" + Binder.getCallingUid());
		synchronized (mFallbackRestrictionLock) {
			mFallbackRestrictions = null;
		}
		synchronized (mFallbackSettingsLock) {
			mFallbackSettings = null;
		}
	}

	// Helper methods

	private void enforcePermission() throws SecurityException {
		if (Binder.getCallingUid() != Process.myUid())
			throw new SecurityException();
	}

	private static String getPrefFileName(String preference) {
		return Util.getUserDataDirectory(Process.myUid()) + File.separator + "shared_prefs" + File.separator
				+ preference + ".xml";
	}

	private static String getPrefFileName(String preference, int uid) {
		return Util.getUserDataDirectory(uid) + File.separator + "shared_prefs" + File.separator + preference + "."
				+ uid + ".xml";
	}

	private static void setPrefFileReadable(String preference) {
		new File(getPrefFileName(preference)).setReadable(true, false);
	}

	private static void setPrefFileReadable(String preference, int uid) {
		new File(getPrefFileName(preference, uid)).setReadable(true, false);
	}

	public static void fixFilePermissions() {
		File folder = new File(Util.getUserDataDirectory(Process.myUid()) + File.separator + "shared_prefs");
		folder.setReadable(true, false);
		File[] files = folder.listFiles();
		if (files != null)
			for (File file : files)
				if (file.getName().startsWith("biz.bokhorst.xprivacy.provider.") && file.getName().endsWith(".xml")
						&& !file.getName().contains(".usage."))
					file.setReadable(true, false);
	}

	private static String getRestrictionPref(String restrictionName) {
		return String.format("%s.%s", COL_RESTRICTED, restrictionName);
	}

	private static String getExceptionPref(String restrictionName, String methodName) {
		return String.format("%s.%s.%s", COL_METHOD, restrictionName, methodName);
	}

	private static String getUsagePref(int uid, String methodName) {
		return String.format("%s.%d.%s", COL_USED, uid, methodName);
	}

	private static String getSettingPref(String settingName) {
		return String.format("%s.%s", COL_SETTING, settingName);
	}

	private static String getSettingName(String settingKey) {
		return settingKey.substring(COL_SETTING.length() + 1);
	}

	private static void convertRestrictions(Context context) throws IOException {
		File source = new File(Util.getUserDataDirectory(Process.myUid()) + File.separator + "shared_prefs"
				+ File.separator + "biz.bokhorst.xprivacy.provider.xml");
		File backup = new File(source.getAbsoluteFile() + ".orig");
		if (source.exists() && !backup.exists()) {
			Util.log(null, Log.WARN, "Converting restrictions");
			SharedPreferences prefs = context.getSharedPreferences(PREF_RESTRICTION, Context.MODE_WORLD_READABLE);
			for (String key : prefs.getAll().keySet()) {
				String[] component = key.split("\\.");
				if (key.startsWith(COL_RESTRICTED)) {
					String restrictionName = component[1];
					String value = prefs.getString(key, null);
					List<String> listRestriction = new ArrayList<String>(Arrays.asList(value.split(",")));
					listRestriction.remove(0);
					for (String uid : listRestriction)
						updateRestriction(context, Integer.parseInt(uid), restrictionName, null, false);
				} else if (key.startsWith(COL_METHOD)) {
					int uid = Integer.parseInt(component[1]);
					String restrictionName = component[2];
					String methodName = component[3];
					boolean value = prefs.getBoolean(key, false);
					updateRestriction(context, uid, restrictionName, methodName, value);
				} else
					Util.log(null, Log.WARN, "Unknown key=" + key);
			}

			// Backup old file
			Util.log(null, Log.INFO, "Backup name=" + backup.getAbsolutePath());
			Util.copy(source, backup);
		}
	}

	private static void convertSettings(Context context) throws IOException {
		if (new File(getPrefFileName(PREF_SETTINGS)).exists()) {
			SharedPreferences prefs = context.getSharedPreferences(PREF_SETTINGS, Context.MODE_WORLD_READABLE);
			SharedPreferences.Editor editor = prefs.edit();
			for (String key : prefs.getAll().keySet())
				try {
					String value = prefs.getString(key, null);
					if (PrivacyManager.cValueRandomLegacy.equals(value))
						editor.putString(key, PrivacyManager.cValueRandom);
				} catch (Throwable ex) {
				}

			editor.commit();
			setPrefFileReadable(PREF_SETTINGS);
		}
	}

	private static void splitSettings(Context context) {
		File prefFile = new File(getPrefFileName(PREF_SETTINGS));
		File migratedFile = new File(prefFile + ".migrated");
		if (prefFile.exists() && !migratedFile.exists()) {
			Util.log(null, Log.WARN, "Splitting " + prefFile);

			SharedPreferences prefs = context.getSharedPreferences(PREF_SETTINGS, Context.MODE_WORLD_READABLE);
			for (String settingKey : prefs.getAll().keySet())
				try {
					int uid = 0;
					String name = getSettingName(settingKey);
					String value = prefs.getString(settingKey, "");

					// Decode setting
					String[] component = name.split("\\.");
					if (name.startsWith("Account.") || name.startsWith("Application.") || name.startsWith("Contact.")) {
						try {
							// name.uid.key
							uid = Integer.parseInt(component[1]);
							name = component[0];
							for (int i = 2; i < component.length; i++)
								name += "." + component[i];
						} catch (NumberFormatException ignored) {
							// Initial uid/name will be used
						}
					} else if (component.length > 1) {
						try {
							// name.x.y.z.uid
							uid = Integer.parseInt(component[component.length - 1]);
							name = component[0];
							for (int i = 1; i < component.length - 1; i++)
								name += "." + component[i];
						} catch (NumberFormatException ignored) {
							// Initial uid/name will be used
						}
					}

					SharedPreferences aprefs = context.getSharedPreferences(PREF_SETTINGS + "." + uid,
							Context.MODE_WORLD_READABLE);
					SharedPreferences.Editor editor = aprefs.edit();
					editor.putString(name, value);
					editor.commit();
				} catch (Throwable ex) {
					// Legacy boolean
					Util.bug(null, ex);
				}

			prefFile.renameTo(migratedFile);
		}
	}

	// Migration

	public static void migrateLegacy(Context context) throws IOException {
		convertSettings(context);
		convertRestrictions(context);
		splitSettings(context);
	}

	public static List<PRestriction> migrateRestrictions(Context context, int uid) {
		List<PRestriction> listWork = new ArrayList<PRestriction>();

		File prefFile = new File(getPrefFileName(PREF_RESTRICTION, uid));
		File migratedFile = new File(prefFile + ".migrated");
		if (prefFile.exists() && !migratedFile.exists()) {
			Util.log(null, Log.WARN, "Migrating " + prefFile);

			SharedPreferences prefs = context.getSharedPreferences(PREF_RESTRICTION + "." + uid,
					Context.MODE_WORLD_READABLE);

			// Process restrictions
			for (String restrictionName : PrivacyManager.getRestrictions())
				if (getRestricted(restrictionName, null, prefs)) {
					// Category
					listWork.add(new PRestriction(uid, restrictionName, null, true));

					// Exceptions
					for (Hook md : PrivacyManager.getHooks(restrictionName, null)) {
						boolean restricted = getRestricted(restrictionName, md.getName(), prefs);
						if (!restricted || md.isDangerous())
							listWork.add(new PRestriction(uid, restrictionName, md.getName(), restricted));
					}
				}
		}

		return listWork;
	}

	public static void finishMigrateRestrictions(int uid) {
		File prefFile = new File(getPrefFileName(PREF_RESTRICTION, uid));
		File migratedFile = new File(prefFile + ".migrated");
		prefFile.renameTo(migratedFile);
	}

	public static List<PSetting> migrateSettings(Context context, int uid) {
		// Process settings
		List<PSetting> listWork = new ArrayList<PSetting>();

		File prefFile = new File(getPrefFileName(PREF_SETTINGS, uid));
		File migratedFile = new File(prefFile + ".migrated");
		if (prefFile.exists() && !migratedFile.exists()) {
			Util.log(null, Log.WARN, "Migrating " + prefFile);

			SharedPreferences prefs = context.getSharedPreferences(PREF_SETTINGS + "." + uid,
					Context.MODE_WORLD_READABLE);
			for (String name : prefs.getAll().keySet())
				try {
					String value = prefs.getString(name, null);
					if (value != null && !"".equals(value)) {
						String type;
						if (name.startsWith("Account.") || name.startsWith("Application.")
								|| name.startsWith("Contact.") || name.startsWith("Template.")) {
							int dot = name.indexOf('.');
							type = name.substring(0, dot);
							name = name.substring(dot + 1);
						} else
							type = "";
						listWork.add(new PSetting(uid, type, name, value));
					}
				} catch (Throwable ex) {
					// Legacy boolean
					Util.bug(null, ex);
				}
		}

		return listWork;
	}

	public static void finishMigrateSettings(int uid) {
		File prefFile = new File(getPrefFileName(PREF_SETTINGS, uid));
		File migratedFile = new File(prefFile + ".migrated");
		prefFile.renameTo(migratedFile);
	}
}