/* ============================================================================ Author : Anton Antonov 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 . ============================================================================ */ 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.geolocation.GeoLocation; import com.rhomobile.rhodes.mainview.MainView; 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.SmsUriHandler; import com.rhomobile.rhodes.uri.TelUriHandler; import com.rhomobile.rhodes.uri.UriHandler; import com.rhomobile.rhodes.uri.VideoUriHandler; import android.app.Activity; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.graphics.Bitmap; 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.JsResult; import android.webkit.WebChromeClient; 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 int MAX_PROGRESS = 10000; private static boolean ENABLE_LOADING_INDICATION = true; private boolean needGeoLocationRestart = false; 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 Boolean contentChanged = null; private Vector uriHandlers = new Vector(); //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 static native void loadUrl(String url); public static native void navigateBack(); public native void doRequest(String url); public native static void makeLink(String src, String dst); private native void initClassLoader(ClassLoader c); private void initRootPath() { Log.d(TAG, "Check if the SD card is mounted..."); String state = Environment.getExternalStorageState(); Log.d(TAG, "Storage state: " + state); boolean hasSDCard = Environment.MEDIA_MOUNTED.equals(state); rootPath = hasSDCard ? sdcardRootPath() : phoneMemoryRootPath(); } public String getRootPath() { return rootPath; } private String phoneMemoryRootPath() { String pkgName = getPackageName(); try { ApplicationInfo info = getPackageManager().getApplicationInfo(pkgName, 0); String path = info.dataDir + "/rhodata/"; return path; } catch (NameNotFoundException e) { throw new RuntimeException("Internal error: package " + pkgName + " not found: " + e.getMessage()); } } private String sdcardRootPath() { String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath(); String pkgName = getPackageName(); String path = sdPath + "/rhomobile/" + pkgName + "/"; return path; } public static String getBlobPath() { return RhodesInstance.getInstance().getRootPath() + "db/db-files"; } 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(getRootPath(), 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()); */ } public boolean isNameChanged() { try { FileSource as = new AssetsSource(getResources().getAssets()); FileSource fs = new FileSource(); return !Utils.isContentsEquals(as, "name", fs, new File(getRootPath(), "name").getPath()); } catch (IOException e) { return true; } } public boolean isBundleChanged() { if (contentChanged == null) { try { String rp = getRootPath(); FileSource as = new AssetsSource(getResources().getAssets()); FileSource fs = new FileSource(); if (isNameChanged()) contentChanged = new Boolean(true); else contentChanged = new Boolean(!Utils.isContentsEquals(as, "hash", fs, new File(rp, "hash").getPath())); } catch (IOException e) { contentChanged = new Boolean(true); } } return contentChanged.booleanValue(); } private void copyFilesFromBundle() { try { if (isBundleChanged()) { Logger.D(TAG, "Copying required files from bundle"); boolean nameChanged = isNameChanged(); String rp = getRootPath(); FileSource as = new AssetsSource(getResources().getAssets()); String items[] = {"apps", "lib", "db", "hash", "name"}; for (int i = 0; i != items.length; ++i) { String item = items[i]; File f = new File(rp, item); Logger.D(TAG, "Copy '" + item + "' to '" + f.getAbsolutePath() + "'"); Utils.copyRecursively(as, item, f, nameChanged); /* String src = sdf.getAbsolutePath(); String dst = phf.getAbsolutePath(); Logger.D(TAG, "Make symlink from '" + src + "' to '" + dst + "'"); makeLink(src, dst); */ } contentChanged = new Boolean(true); 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 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); 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.setFocusableInTouchMode(true); w.setWebViewClient(new WebViewClient() { private boolean splashHidden = false; @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return handleUrlLoading(url); } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { if (ENABLE_LOADING_INDICATION) getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 0); super.onPageStarted(view, url, favicon); } @Override public void onPageFinished(WebView view, String url) { // Set title Rhodes r = RhodesInstance.getInstance(); String title = view.getTitle(); r.setTitle(title); // Hide splash screen if (!splashHidden && url.startsWith("http://")) { hideSplashScreen(); splashHidden = true; } if (ENABLE_LOADING_INDICATION) getWindow().setFeatureInt(Window.FEATURE_PROGRESS, MAX_PROGRESS); super.onPageFinished(view, url); } }); w.setWebChromeClient(new WebChromeClient() { @Override public void onProgressChanged(WebView view, int newProgress) { if (ENABLE_LOADING_INDICATION) { newProgress *= 100; if (newProgress < 0) newProgress = 0; if (newProgress > MAX_PROGRESS) newProgress = MAX_PROGRESS; getWindow().setFeatureInt(Window.FEATURE_PROGRESS, newProgress); } super.onProgressChanged(view, newProgress); } @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { return false; } @Override public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { return false; } }); 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() { splashScreen = new SplashScreen(this); 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); Logger.T(TAG, "+++ onCreate"); Thread ct = Thread.currentThread(); ct.setPriority(Thread.MAX_PRIORITY); uiThreadId = ct.getId(); RhodesInstance.setInstance(this); initClassLoader(this.getClassLoader()); initRootPath(); try { copyFromBundle("apps/rhoconfig.txt"); } catch (IOException e1) { Logger.E("Rhodes", e1); finish(); return; } createRhodesApp(getRootPath()); 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); ENABLE_LOADING_INDICATION = !RhoConf.getBool("disable_loading_indication"); if (ENABLE_LOADING_INDICATION) this.requestWindowFeature(Window.FEATURE_PROGRESS); outerFrame = new FrameLayout(this); this.setContentView(outerFrame, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); Logger.I("Rhodes", "Loading..."); showSplashScreen(); // Increase WebView rendering priority WebView w = new WebView(this); WebSettings webSettings = w.getSettings(); webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH); // Get screen width/height 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; // TODO: detect camera availability isCameraAvailable = true; // Register custom uri handlers here uriHandlers.addElement(new MailUriHandler(this)); uriHandlers.addElement(new TelUriHandler(this)); uriHandlers.addElement(new SmsUriHandler(this)); uriHandlers.addElement(new VideoUriHandler(this)); Thread init = new Thread(new Runnable() { public void run() { copyFilesFromBundle(); startRhodesApp(); } }); init.start(); } @Override protected void onRestart() { super.onRestart(); Logger.T(TAG, "+++ onRestart"); } @Override protected void onStart() { super.onStart(); Logger.T(TAG, "+++ onStart"); } @Override protected void onResume() { super.onResume(); if (needGeoLocationRestart) GeoLocation.isKnownPosition(); Logger.T(TAG, "+++ onResume"); } @Override protected void onPause() { Logger.T(TAG, "+++ onPause"); super.onPause(); } @Override protected void onStop() { Logger.T(TAG, "+++ onStop"); needGeoLocationRestart = GeoLocation.isAvailable(); GeoLocation.stop(); super.onStop(); } @Override protected void onDestroy() { Logger.T(TAG, "+++ onDestroy"); stopSelf(); super.onDestroy(); } @Override public void onConfigurationChanged(Configuration newConfig) { Logger.T(TAG, "+++ onConfigurationChanged"); super.onConfigurationChanged(newConfig); } @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; } private static String getCurrentCountry() { String cl = Locale.getDefault().getCountry(); return cl; } 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("country")) return getCurrentCountry(); 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()); } static { NativeLibraries.load(); } }