<commit_before>/*
 ============================================================================
 Author	    : <NAME>
 Version	: 1.0
 Copyright  : Copyright (C) 2008 Rhomobile. All rights reserved.

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ============================================================================
 */
package com.rhomobile.rhodes;

import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Vector;

import com.rhomobile.rhodes.Utils.AssetsSource;
import com.rhomobile.rhodes.Utils.FileSource;
import com.rhomobile.rhodes.mainview.MainView;
import com.rhomobile.rhodes.mainview.SimpleMainView;
import com.rhomobile.rhodes.ui.AboutDialog;
import com.rhomobile.rhodes.ui.LogOptionsDialog;
import com.rhomobile.rhodes.ui.LogViewDialog;
import com.rhomobile.rhodes.uri.MailUriHandler;
import com.rhomobile.rhodes.uri.TelUriHandler;
import com.rhomobile.rhodes.uri.UriHandler;
import com.rhomobile.rhodes.uri.VideoUriHandler;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.telephony.TelephonyManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.ViewGroup.LayoutParams;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.os.Process;

public class Rhodes extends Activity {

	private static final String TAG = "Rhodes";
	
	public static final String INTENT_EXTRA_PREFIX = "com.rhomobile.rhodes.";
	
	public static final int RHO_SPLASH_VIEW = 1;
	public static final int RHO_MAIN_VIEW = 2;
	public static final int RHO_TOOLBAR_VIEW = 3;
	
	public static int WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_FULLSCREEN;
	public static int WINDOW_MASK = WindowManager.LayoutParams.FLAG_FULLSCREEN;
	
	private static final String LOADING_PAGE = "file:///android_asset/apps/app/loading.html";
	
	private long uiThreadId;
	public long getUiThreadId() {
		return uiThreadId;
	}
	
	private final Handler uiHandler = new Handler();
	
	private static int screenWidth;
	private static int screenHeight;
	
	private static float screenPpiX;
	private static float screenPpiY;
	
	private static boolean isCameraAvailable;

	private FrameLayout outerFrame;
	private MainView mainView;
	
	private SplashScreen splashScreen = null;
	
	private Vector<UriHandler> uriHandlers = new Vector<UriHandler>();

	private String sdCardError = "Application can not access the SD card while it's mounted. Please unmount the device and stop the adb server before launching the app.";
	
	private RhoMenu appMenu = null;
		
	private String rootPath = null;
	
	public native void createRhodesApp(String path);
	public native void startRhodesApp();
	public native void stopRhodesApp();
	
	public native void doSyncAllSources(boolean v);
	
	public native String getOptionsUrl();
	public native String getStartUrl();
	public native String getCurrentUrl();
	public native String getAppBackUrl();
	
	public native String normalizeUrl(String url);
	
	public native void doRequest(String url);
	
	public native static void makeLink(String src, String dst);
	
	private void initRootPath() {
		//rootPath = phoneMemoryRootPath();
		rootPath = sdcardRootPath();
	}
	
	public String getRootPath() {
		return rootPath;
	}
	
	/*
	private String phoneMemoryRootPath() {
		return "/data/data/" + getPackageName() + "/data/";
	}
	*/
	
	private String sdcardRootPath() {
		return Environment.getExternalStorageDirectory() + "/rhomobile/" + getPackageName() + "/";
	}
	
	private RhoLogConf m_rhoLogConf = new RhoLogConf();
	public RhoLogConf getLogConf() {
		return m_rhoLogConf;
	}
	
	private boolean checkSDCard() {
		Log.d(TAG, "Check if the SD card is mounted...");
		String state = Environment.getExternalStorageState();
		Log.d(TAG, "Storage state: " + state);
		if(!Environment.MEDIA_MOUNTED.equals(state)) {
			new AlertDialog.Builder(this)
				.setTitle("SD card error")
				.setMessage(sdCardError)
				.setCancelable(false)
				.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int which) {
						Log.e(this.getClass().getSimpleName(), "Exit - SD card is not accessible");
						Process.killProcess(Process.myPid());
					}					
				})
				.create()
				.show();
			return false;
		}
		Log.d(TAG, "SD card check passed, going on");
		return true;
	}
	
	private void copyFromBundle(String file) throws IOException {
		File target = new File(sdcardRootPath(), file);
		if (target.exists())
			return;
		FileSource as = new AssetsSource(getResources().getAssets());
		Utils.copyRecursively(as, file, target, true);
		
		/*
		int idx = file.indexOf('/');
		String dir = idx == -1 ? file : file.substring(0, idx);
		
		File sdPath = new File(sdcardRootPath(), dir);
		File phPath = new File(phoneMemoryRootPath(), dir);
		phPath.getParentFile().mkdirs();
		makeLink(sdPath.getAbsolutePath(), phPath.getAbsolutePath());
		*/
	}
	
	private void copyFilesFromBundle() {
		try {
			//String phRootPath = phoneMemoryRootPath();
			String sdRootPath = sdcardRootPath();
			
			boolean nameChanged = false;
			boolean contentChanged = true;
			
			FileSource as = new AssetsSource(getResources().getAssets());
			FileSource fs = new FileSource();
			
			nameChanged = !Utils.isContentsEquals(as, "name", fs, new File(sdRootPath, "name").getPath());
			if (nameChanged)
				contentChanged = true;
			else
				contentChanged = !Utils.isContentsEquals(as, "hash", fs, new File(sdRootPath, "hash").getPath());
			
			if (contentChanged) {
				Logger.D(TAG, "Copying required files from bundle to sdcard");
				
				/*
				File phrf = new File(phRootPath);
				if (!phrf.exists())
					phrf.mkdirs();
				phrf = null;
				*/

				String items[] = {"apps", "lib", "db", "hash", "name"};
				for (int i = 0; i != items.length; ++i) {
					String item = items[i];
					//File phf = new File(phRootPath, item);
					File sdf = new File(sdRootPath, item);
					Logger.D(TAG, "Copy '" + item + "' to '" + sdf + "'");
					Utils.copyRecursively(as, item, sdf, nameChanged);
					/*
					String src = sdf.getAbsolutePath();
					String dst = phf.getAbsolutePath();
					Logger.D(TAG, "Make symlink from '" + src + "' to '" + dst + "'");
					makeLink(src, dst);
					*/
				}
				
				File dbfiles = new File(rootPath + "apps/public/db-files");
				if (!dbfiles.exists())
					dbfiles.mkdirs();
				dbfiles = null;
				
				Logger.D(TAG, "All files copied");
			}
			else
				Logger.D(TAG, "No need to copy files to SD card");
		} catch (IOException e) {
			Logger.E(TAG, e);
			return;
		}
	}
	
	private boolean handleUrlLoading(String url) {
		Enumeration<UriHandler> e = uriHandlers.elements();
		while (e.hasMoreElements()) {
			UriHandler handler = e.nextElement();
			try {
				if (handler.handle(url))
					return true;
			}
			catch (Exception ex) {
				continue;
			}
		}
		
		return false;
	}
	
	public WebView createWebView() {
		WebView w = new WebView(this);
		
		try {
			w.loadUrl(LOADING_PAGE);
		}
		catch (Exception e) {
			// Ignore
		}
		
		WebSettings webSettings = w.getSettings();
		webSettings.setSavePassword(false);
		webSettings.setSaveFormData(false);
		webSettings.setJavaScriptEnabled(true);
		webSettings.setJavaScriptCanOpenWindowsAutomatically(false);
		webSettings.setSupportZoom(false);
		webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
		webSettings.setSupportMultipleWindows(false);
		// webSettings.setLoadsImagesAutomatically(true);

		w.setVerticalScrollBarEnabled(true);
		w.setHorizontalScrollBarEnabled(true);
		w.setVerticalScrollbarOverlay(true);
		w.setHorizontalScrollbarOverlay(true);

		w.setWebViewClient(new WebViewClient() {

			private boolean realPageLoaded = false;
			
			@Override
			public boolean shouldOverrideUrlLoading(WebView view, String url) {
				return handleUrlLoading(url);
			}
			
			@Override
			public void onPageFinished(WebView view, String url) {
				// Clear page until we get real page loaded
				if (!(realPageLoaded || url.equals(LOADING_PAGE)))
					realPageLoaded = true;
				if (!realPageLoaded)
					view.clearHistory();
				// Set title
				Rhodes r = RhodesInstance.getInstance();
				String title = view.getTitle();
				r.setTitle(title);
				// Hide splash screen
				if (url.startsWith("http://"))
					hideSplashScreen();
			}

		});

		return w;
	}
	
	public void setMainView(MainView v) {
		View main = outerFrame.findViewById(RHO_MAIN_VIEW);
		if (main != null)
			outerFrame.removeView(main);
		mainView = v;
		main = mainView.getView();
		if (outerFrame.findViewById(RHO_SPLASH_VIEW) != null)
			main.setVisibility(View.INVISIBLE);
		outerFrame.addView(main);
	}
	
	public MainView getMainView() {
		return mainView;
	}
	
	private static class PerformOnUiThread implements Runnable {
		
		private Runnable runnable;
		
		public PerformOnUiThread(Runnable r) {
			runnable = r;
		}
		
		public void run() {
			try {
				runnable.run();
			}
			catch (Exception e) {
				Logger.E("Rhodes", "PerformOnUiThread failed: " + e.getMessage());
			}
			finally {
				synchronized (runnable) {
					runnable.notify();
				}
			}
		}
	};
	
	public static void performOnUiThread(Runnable r, boolean wait) {
		try {
			Rhodes rhodes = RhodesInstance.getInstance();
			if (!wait) {
				rhodes.uiHandler.post(r);
			}
			else {
				long thrId = Thread.currentThread().getId();
				if (rhodes.getUiThreadId() == thrId) {
					// We are already in UI thread
					r.run();
				}
				else {
					// Post request to UI thread and wait when it would be done
					synchronized (r) {
						rhodes.uiHandler.post(new PerformOnUiThread(r));
						r.wait();
					}
				}
			}
		}
		catch (Exception e) {
			Logger.E("Rhodes", "performOnUiThread failed: " + e.getMessage());
		}
	}
	
	private void showSplashScreen(String file) throws IOException {
		splashScreen = new SplashScreen(this, file);
		splashScreen.start(outerFrame);
	}
	
	public void hideSplashScreen() {
		if (splashScreen != null) {
			splashScreen.hide(outerFrame);
			splashScreen = null;
		}
		View view = mainView.getView();
		view.setVisibility(View.VISIBLE);
		view.requestFocus();
	}
	
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		if (!checkSDCard()) {
			finish();
			return;
		}
		
		uiThreadId = Thread.currentThread().getId();
		RhodesInstance.setInstance(this);
		
		initRootPath();
		try {
			copyFromBundle("apps/rhoconfig.txt");
		} catch (IOException e1) {
			Logger.E("Rhodes", e1);
			finish();
			return;
		}
		createRhodesApp(rootPath);
		
		boolean fullScreen = true;
		if (RhoConf.isExist("full_screen"))
			fullScreen = RhoConf.getBool("full_screen");
		if (!fullScreen) {
			WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN;
			WINDOW_MASK = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN;
		}
		getWindow().setFlags(WINDOW_FLAGS, WINDOW_MASK);

		boolean disableScreenRotation = RhoConf.getBool("disable_screen_rotation");
		this.setRequestedOrientation(disableScreenRotation ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT :
			ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
		
		this.requestWindowFeature(Window.FEATURE_PROGRESS);

		outerFrame = new FrameLayout(this);
		this.setContentView(outerFrame, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
		
		Logger.I("Rhodes", "Loading...");
		
		WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
		Display d = wm.getDefaultDisplay();
		screenHeight = d.getHeight();
		screenWidth = d.getWidth();
		
		DisplayMetrics metrics = new DisplayMetrics();
		d.getMetrics(metrics);
		screenPpiX = metrics.xdpi;
		screenPpiY = metrics.ydpi;
		
		try {
			showSplashScreen("apps/app/loading.png");
		}
		catch (Exception e) {
			setMainView(new SimpleMainView());
		}
		
		// TODO: detect camera availability
		isCameraAvailable = true;
		
		// Register custom uri handlers here
		uriHandlers.addElement(new MailUriHandler(this));
		uriHandlers.addElement(new TelUriHandler(this));
		uriHandlers.addElement(new VideoUriHandler(this));
		
		Thread init = new Thread(new Runnable() {

			public void run() {
				copyFilesFromBundle();
				startRhodesApp();
			}
			
		});
		init.start();
	}

	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
	}
	
	@Override
	protected void onPause() {
		super.onPause();
	}
	
	@Override
	protected void onResume() {
		super.onResume();
	}
	
	@Override
	protected void onDestroy() {
		stopSelf();
		
		super.onDestroy();
	}
	
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		switch (keyCode) {
		case KeyEvent.KEYCODE_BACK:
			mainView.back(mainView.activeTab());
			return true;
		case KeyEvent.KEYCODE_HOME:
			stopSelf();
			return true;
		}
		
		return super.onKeyDown(keyCode, event);
	}
	
	@Override
	public boolean onPrepareOptionsMenu(Menu menu) {
		super.onPrepareOptionsMenu(menu);
		appMenu = new RhoMenu(menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		if (appMenu == null)
			return false;
		return appMenu.onMenuItemSelected(item);
	}
	
	public static void showAboutDialog() {
		performOnUiThread(new Runnable() {
			public void run() {
				final AboutDialog aboutDialog = new AboutDialog(RhodesInstance.getInstance());
				aboutDialog.setTitle("About");
				aboutDialog.setCanceledOnTouchOutside(true);
				aboutDialog.setCancelable(true);
				aboutDialog.show();
			}
		}, false);
	}
	
	public static void showLogView() {
		performOnUiThread(new Runnable() {
			public void run() {
				final LogViewDialog logViewDialog = new LogViewDialog(RhodesInstance.getInstance());
				logViewDialog.setTitle("Log View");
				logViewDialog.setCancelable(true);
				logViewDialog.show();
			}
		}, false);
	}
	
	public static void showLogOptions() {
		performOnUiThread(new Runnable() {
			public void run() {
				final LogOptionsDialog logOptionsDialog = new LogOptionsDialog(RhodesInstance.getInstance());
				logOptionsDialog.setTitle("Logging Options");
				logOptionsDialog.setCancelable(true);
				logOptionsDialog.show();
			}
		}, false);
	}
	
	// Called from native code
	public static void deleteFilesInFolder(String folder) {
		String[] children = new File(folder).list();
		for (int i = 0; i != children.length; ++i)
			Utils.deleteRecursively(new File(folder, children[i]));
	}
	
	private static boolean hasNetwork() {
		if (!Capabilities.NETWORK_STATE_ENABLED) {
			Logger.E(TAG, "Capability NETWORK_STATE disabled");
			return false;
		}
		
		Context ctx = RhodesInstance.getInstance();
		ConnectivityManager conn = (ConnectivityManager)ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
		if (conn == null)
			return false;
		
		NetworkInfo[] info = conn.getAllNetworkInfo();
		if (info == null)
			return false;
		
		for (int i = 0, lim = info.length; i < lim; ++i) {
			if (info[i].getState() == NetworkInfo.State.CONNECTED)
				return true;
		}
		
		return false;
	}
	
	private static String getCurrentLocale() {
		String locale = Locale.getDefault().getLanguage();
		if (locale.length() == 0)
			locale = "en";
		return locale;
	}
	
	public static int getScreenWidth() {
		return screenWidth;
	}
	
	public static int getScreenHeight() {
		return screenHeight;
	}
	
	public static Object getProperty(String name) {
		if (name.equalsIgnoreCase("platform"))
			return "ANDROID";
		else if (name.equalsIgnoreCase("locale"))
			return getCurrentLocale();
		else if (name.equalsIgnoreCase("screen_width"))
			return new Integer(getScreenWidth());
		else if (name.equalsIgnoreCase("screen_height"))
			return new Integer(getScreenHeight());
		else if (name.equalsIgnoreCase("has_camera"))
			return new Boolean(isCameraAvailable);
		else if (name.equalsIgnoreCase("has_network"))
			return hasNetwork();
		else if (name.equalsIgnoreCase("ppi_x"))
			return new Float(screenPpiX);
		else if (name.equalsIgnoreCase("ppi_y"))
			return new Float(screenPpiY);
		else if (name.equalsIgnoreCase("phone_number")) {
			TelephonyManager manager = (TelephonyManager)RhodesInstance.getInstance().
				getSystemService(Context.TELEPHONY_SERVICE);
			String number = manager.getLine1Number();
			return number;
		}
		
		return null;
	}
	
	public static void exit() {
		RhodesInstance.getInstance().stopSelf();
	}
	
	private void stopSelf() {
		//stopRhodesApp();
		Process.killProcess(Process.myPid());
	}
	<commit_msg>'device_name' and 'os_version' supported for Android<commit_after>/*
 ============================================================================
 Author	    : <NAME>
 Version	: 1.0
 Copyright  : Copyright (C) 2008 Rhomobile. All rights reserved.

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ============================================================================
 */
package com.rhomobile.rhodes;

import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Vector;

import com.rhomobile.rhodes.Utils.AssetsSource;
import com.rhomobile.rhodes.Utils.FileSource;
import com.rhomobile.rhodes.mainview.MainView;
import com.rhomobile.rhodes.mainview.SimpleMainView;
import com.rhomobile.rhodes.ui.AboutDialog;
import com.rhomobile.rhodes.ui.LogOptionsDialog;
import com.rhomobile.rhodes.ui.LogViewDialog;
import com.rhomobile.rhodes.uri.MailUriHandler;
import com.rhomobile.rhodes.uri.TelUriHandler;
import com.rhomobile.rhodes.uri.UriHandler;
import com.rhomobile.rhodes.uri.VideoUriHandler;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.telephony.TelephonyManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.ViewGroup.LayoutParams;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.os.Process;

public class Rhodes extends Activity {

	private static final String TAG = "Rhodes";
	
	public static final String INTENT_EXTRA_PREFIX = "com.rhomobile.rhodes.";
	
	public static final int RHO_SPLASH_VIEW = 1;
	public static final int RHO_MAIN_VIEW = 2;
	public static final int RHO_TOOLBAR_VIEW = 3;
	
	public static int WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_FULLSCREEN;
	public static int WINDOW_MASK = WindowManager.LayoutParams.FLAG_FULLSCREEN;
	
	private static final String LOADING_PAGE = "file:///android_asset/apps/app/loading.html";
	
	private long uiThreadId;
	public long getUiThreadId() {
		return uiThreadId;
	}
	
	private final Handler uiHandler = new Handler();
	
	private static int screenWidth;
	private static int screenHeight;
	
	private static float screenPpiX;
	private static float screenPpiY;
	
	private static boolean isCameraAvailable;

	private FrameLayout outerFrame;
	private MainView mainView;
	
	private SplashScreen splashScreen = null;
	
	private Vector<UriHandler> uriHandlers = new Vector<UriHandler>();

	private String sdCardError = "Application can not access the SD card while it's mounted. Please unmount the device and stop the adb server before launching the app.";
	
	private RhoMenu appMenu = null;
		
	private String rootPath = null;
	
	public native void createRhodesApp(String path);
	public native void startRhodesApp();
	public native void stopRhodesApp();
	
	public native void doSyncAllSources(boolean v);
	
	public native String getOptionsUrl();
	public native String getStartUrl();
	public native String getCurrentUrl();
	public native String getAppBackUrl();
	
	public native String normalizeUrl(String url);
	
	public native void doRequest(String url);
	
	public native static void makeLink(String src, String dst);
	
	private void initRootPath() {
		//rootPath = phoneMemoryRootPath();
		rootPath = sdcardRootPath();
	}
	
	public String getRootPath() {
		return rootPath;
	}
	
	/*
	private String phoneMemoryRootPath() {
		return "/data/data/" + getPackageName() + "/data/";
	}
	*/
	
	private String sdcardRootPath() {
		return Environment.getExternalStorageDirectory() + "/rhomobile/" + getPackageName() + "/";
	}
	
	private RhoLogConf m_rhoLogConf = new RhoLogConf();
	public RhoLogConf getLogConf() {
		return m_rhoLogConf;
	}
	
	private boolean checkSDCard() {
		Log.d(TAG, "Check if the SD card is mounted...");
		String state = Environment.getExternalStorageState();
		Log.d(TAG, "Storage state: " + state);
		if(!Environment.MEDIA_MOUNTED.equals(state)) {
			new AlertDialog.Builder(this)
				.setTitle("SD card error")
				.setMessage(sdCardError)
				.setCancelable(false)
				.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int which) {
						Log.e(this.getClass().getSimpleName(), "Exit - SD card is not accessible");
						Process.killProcess(Process.myPid());
					}					
				})
				.create()
				.show();
			return false;
		}
		Log.d(TAG, "SD card check passed, going on");
		return true;
	}
	
	private void copyFromBundle(String file) throws IOException {
		File target = new File(sdcardRootPath(), file);
		if (target.exists())
			return;
		FileSource as = new AssetsSource(getResources().getAssets());
		Utils.copyRecursively(as, file, target, true);
		
		/*
		int idx = file.indexOf('/');
		String dir = idx == -1 ? file : file.substring(0, idx);
		
		File sdPath = new File(sdcardRootPath(), dir);
		File phPath = new File(phoneMemoryRootPath(), dir);
		phPath.getParentFile().mkdirs();
		makeLink(sdPath.getAbsolutePath(), phPath.getAbsolutePath());
		*/
	}
	
	private void copyFilesFromBundle() {
		try {
			//String phRootPath = phoneMemoryRootPath();
			String sdRootPath = sdcardRootPath();
			
			boolean nameChanged = false;
			boolean contentChanged = true;
			
			FileSource as = new AssetsSource(getResources().getAssets());
			FileSource fs = new FileSource();
			
			nameChanged = !Utils.isContentsEquals(as, "name", fs, new File(sdRootPath, "name").getPath());
			if (nameChanged)
				contentChanged = true;
			else
				contentChanged = !Utils.isContentsEquals(as, "hash", fs, new File(sdRootPath, "hash").getPath());
			
			if (contentChanged) {
				Logger.D(TAG, "Copying required files from bundle to sdcard");
				
				/*
				File phrf = new File(phRootPath);
				if (!phrf.exists())
					phrf.mkdirs();
				phrf = null;
				*/

				String items[] = {"apps", "lib", "db", "hash", "name"};
				for (int i = 0; i != items.length; ++i) {
					String item = items[i];
					//File phf = new File(phRootPath, item);
					File sdf = new File(sdRootPath, item);
					Logger.D(TAG, "Copy '" + item + "' to '" + sdf + "'");
					Utils.copyRecursively(as, item, sdf, nameChanged);
					/*
					String src = sdf.getAbsolutePath();
					String dst = phf.getAbsolutePath();
					Logger.D(TAG, "Make symlink from '" + src + "' to '" + dst + "'");
					makeLink(src, dst);
					*/
				}
				
				File dbfiles = new File(rootPath + "apps/public/db-files");
				if (!dbfiles.exists())
					dbfiles.mkdirs();
				dbfiles = null;
				
				Logger.D(TAG, "All files copied");
			}
			else
				Logger.D(TAG, "No need to copy files to SD card");
		} catch (IOException e) {
			Logger.E(TAG, e);
			return;
		}
	}
	
	private boolean handleUrlLoading(String url) {
		Enumeration<UriHandler> e = uriHandlers.elements();
		while (e.hasMoreElements()) {
			UriHandler handler = e.nextElement();
			try {
				if (handler.handle(url))
					return true;
			}
			catch (Exception ex) {
				continue;
			}
		}
		
		return false;
	}
	
	public WebView createWebView() {
		WebView w = new WebView(this);
		
		try {
			w.loadUrl(LOADING_PAGE);
		}
		catch (Exception e) {
			// Ignore
		}
		
		WebSettings webSettings = w.getSettings();
		webSettings.setSavePassword(false);
		webSettings.setSaveFormData(false);
		webSettings.setJavaScriptEnabled(true);
		webSettings.setJavaScriptCanOpenWindowsAutomatically(false);
		webSettings.setSupportZoom(false);
		webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
		webSettings.setSupportMultipleWindows(false);
		// webSettings.setLoadsImagesAutomatically(true);

		w.setVerticalScrollBarEnabled(true);
		w.setHorizontalScrollBarEnabled(true);
		w.setVerticalScrollbarOverlay(true);
		w.setHorizontalScrollbarOverlay(true);

		w.setWebViewClient(new WebViewClient() {

			private boolean realPageLoaded = false;
			
			@Override
			public boolean shouldOverrideUrlLoading(WebView view, String url) {
				return handleUrlLoading(url);
			}
			
			@Override
			public void onPageFinished(WebView view, String url) {
				// Clear page until we get real page loaded
				if (!(realPageLoaded || url.equals(LOADING_PAGE)))
					realPageLoaded = true;
				if (!realPageLoaded)
					view.clearHistory();
				// Set title
				Rhodes r = RhodesInstance.getInstance();
				String title = view.getTitle();
				r.setTitle(title);
				// Hide splash screen
				if (url.startsWith("http://"))
					hideSplashScreen();
			}

		});

		return w;
	}
	
	public void setMainView(MainView v) {
		View main = outerFrame.findViewById(RHO_MAIN_VIEW);
		if (main != null)
			outerFrame.removeView(main);
		mainView = v;
		main = mainView.getView();
		if (outerFrame.findViewById(RHO_SPLASH_VIEW) != null)
			main.setVisibility(View.INVISIBLE);
		outerFrame.addView(main);
	}
	
	public MainView getMainView() {
		return mainView;
	}
	
	private static class PerformOnUiThread implements Runnable {
		
		private Runnable runnable;
		
		public PerformOnUiThread(Runnable r) {
			runnable = r;
		}
		
		public void run() {
			try {
				runnable.run();
			}
			catch (Exception e) {
				Logger.E("Rhodes", "PerformOnUiThread failed: " + e.getMessage());
			}
			finally {
				synchronized (runnable) {
					runnable.notify();
				}
			}
		}
	};
	
	public static void performOnUiThread(Runnable r, boolean wait) {
		try {
			Rhodes rhodes = RhodesInstance.getInstance();
			if (!wait) {
				rhodes.uiHandler.post(r);
			}
			else {
				long thrId = Thread.currentThread().getId();
				if (rhodes.getUiThreadId() == thrId) {
					// We are already in UI thread
					r.run();
				}
				else {
					// Post request to UI thread and wait when it would be done
					synchronized (r) {
						rhodes.uiHandler.post(new PerformOnUiThread(r));
						r.wait();
					}
				}
			}
		}
		catch (Exception e) {
			Logger.E("Rhodes", "performOnUiThread failed: " + e.getMessage());
		}
	}
	
	private void showSplashScreen(String file) throws IOException {
		splashScreen = new SplashScreen(this, file);
		splashScreen.start(outerFrame);
	}
	
	public void hideSplashScreen() {
		if (splashScreen != null) {
			splashScreen.hide(outerFrame);
			splashScreen = null;
		}
		View view = mainView.getView();
		view.setVisibility(View.VISIBLE);
		view.requestFocus();
	}
	
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		if (!checkSDCard()) {
			finish();
			return;
		}
		
		uiThreadId = Thread.currentThread().getId();
		RhodesInstance.setInstance(this);
		
		initRootPath();
		try {
			copyFromBundle("apps/rhoconfig.txt");
		} catch (IOException e1) {
			Logger.E("Rhodes", e1);
			finish();
			return;
		}
		createRhodesApp(rootPath);
		
		boolean fullScreen = true;
		if (RhoConf.isExist("full_screen"))
			fullScreen = RhoConf.getBool("full_screen");
		if (!fullScreen) {
			WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN;
			WINDOW_MASK = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN;
		}
		getWindow().setFlags(WINDOW_FLAGS, WINDOW_MASK);

		boolean disableScreenRotation = RhoConf.getBool("disable_screen_rotation");
		this.setRequestedOrientation(disableScreenRotation ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT :
			ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
		
		this.requestWindowFeature(Window.FEATURE_PROGRESS);

		outerFrame = new FrameLayout(this);
		this.setContentView(outerFrame, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
		
		Logger.I("Rhodes", "Loading...");
		
		WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
		Display d = wm.getDefaultDisplay();
		screenHeight = d.getHeight();
		screenWidth = d.getWidth();
		
		DisplayMetrics metrics = new DisplayMetrics();
		d.getMetrics(metrics);
		screenPpiX = metrics.xdpi;
		screenPpiY = metrics.ydpi;
		
		try {
			showSplashScreen("apps/app/loading.png");
		}
		catch (Exception e) {
			setMainView(new SimpleMainView());
		}
		
		// TODO: detect camera availability
		isCameraAvailable = true;
		
		// Register custom uri handlers here
		uriHandlers.addElement(new MailUriHandler(this));
		uriHandlers.addElement(new TelUriHandler(this));
		uriHandlers.addElement(new VideoUriHandler(this));
		
		Thread init = new Thread(new Runnable() {

			public void run() {
				copyFilesFromBundle();
				startRhodesApp();
			}
			
		});
		init.start();
	}

	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
	}
	
	@Override
	protected void onPause() {
		super.onPause();
	}
	
	@Override
	protected void onResume() {
		super.onResume();
	}
	
	@Override
	protected void onDestroy() {
		stopSelf();
		
		super.onDestroy();
	}
	
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		switch (keyCode) {
		case KeyEvent.KEYCODE_BACK:
			mainView.back(mainView.activeTab());
			return true;
		case KeyEvent.KEYCODE_HOME:
			stopSelf();
			return true;
		}
		
		return super.onKeyDown(keyCode, event);
	}
	
	@Override
	public boolean onPrepareOptionsMenu(Menu menu) {
		super.onPrepareOptionsMenu(menu);
		appMenu = new RhoMenu(menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		if (appMenu == null)
			return false;
		return appMenu.onMenuItemSelected(item);
	}
	
	public static void showAboutDialog() {
		performOnUiThread(new Runnable() {
			public void run() {
				final AboutDialog aboutDialog = new AboutDialog(RhodesInstance.getInstance());
				aboutDialog.setTitle("About");
				aboutDialog.setCanceledOnTouchOutside(true);
				aboutDialog.setCancelable(true);
				aboutDialog.show();
			}
		}, false);
	}
	
	public static void showLogView() {
		performOnUiThread(new Runnable() {
			public void run() {
				final LogViewDialog logViewDialog = new LogViewDialog(RhodesInstance.getInstance());
				logViewDialog.setTitle("Log View");
				logViewDialog.setCancelable(true);
				logViewDialog.show();
			}
		}, false);
	}
	
	public static void showLogOptions() {
		performOnUiThread(new Runnable() {
			public void run() {
				final LogOptionsDialog logOptionsDialog = new LogOptionsDialog(RhodesInstance.getInstance());
				logOptionsDialog.setTitle("Logging Options");
				logOptionsDialog.setCancelable(true);
				logOptionsDialog.show();
			}
		}, false);
	}
	
	// Called from native code
	public static void deleteFilesInFolder(String folder) {
		String[] children = new File(folder).list();
		for (int i = 0; i != children.length; ++i)
			Utils.deleteRecursively(new File(folder, children[i]));
	}
	
	private static boolean hasNetwork() {
		if (!Capabilities.NETWORK_STATE_ENABLED) {
			Logger.E(TAG, "Capability NETWORK_STATE disabled");
			return false;
		}
		
		Context ctx = RhodesInstance.getInstance();
		ConnectivityManager conn = (ConnectivityManager)ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
		if (conn == null)
			return false;
		
		NetworkInfo[] info = conn.getAllNetworkInfo();
		if (info == null)
			return false;
		
		for (int i = 0, lim = info.length; i < lim; ++i) {
			if (info[i].getState() == NetworkInfo.State.CONNECTED)
				return true;
		}
		
		return false;
	}
	
	private static String getCurrentLocale() {
		String locale = Locale.getDefault().getLanguage();
		if (locale.length() == 0)
			locale = "en";
		return locale;
	}
	
	public static int getScreenWidth() {
		return screenWidth;
	}
	
	public static int getScreenHeight() {
		return screenHeight;
	}
	
	public static Object getProperty(String name) {
		if (name.equalsIgnoreCase("platform"))
			return "ANDROID";
		else if (name.equalsIgnoreCase("locale"))
			return getCurrentLocale();
		else if (name.equalsIgnoreCase("screen_width"))
			return new Integer(getScreenWidth());
		else if (name.equalsIgnoreCase("screen_height"))
			return new Integer(getScreenHeight());
		else if (name.equalsIgnoreCase("has_camera"))
			return new Boolean(isCameraAvailable);
		else if (name.equalsIgnoreCase("has_network"))
			return hasNetwork();
		else if (name.equalsIgnoreCase("ppi_x"))
			return new Float(screenPpiX);
		else if (name.equalsIgnoreCase("ppi_y"))
			return new Float(screenPpiY);
		else if (name.equalsIgnoreCase("phone_number")) {
			TelephonyManager manager = (TelephonyManager)RhodesInstance.getInstance().
				getSystemService(Context.TELEPHONY_SERVICE);
			String number = manager.getLine1Number();
			return number;
		}
		else if (name.equalsIgnoreCase("device_name")) {
			return Build.DEVICE;
		}
		else if (name.equalsIgnoreCase("os_version")) {
			return Build.VERSION.RELEASE;
		}
		
		return null;
	}
	
	public static void exit() {
		RhodesInstance.getInstance().stopSelf();
	}
	
	private void stopSelf() {
		//stopRhodesApp();
		Process.killProcess(Process.myPid());
	}
	
{'fraction_non_alphanumeric': 0.12177933134126402, 'fraction_numerical': 0.0010712070812426001, 'mean_word_length': 4.088939893845933, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5901149', 'n_tokens_mistral': 12726, 'n_tokens_neox': 10747, 'n_words': 2645}