/* ============================================================================ 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.Hashtable; import java.util.Locale; 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 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.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.telephony.TelephonyManager; import android.util.Log; import android.view.Display; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; 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 long uiThreadId; public long getUiThreadId() { return uiThreadId; } private final Handler uiHandler = new Handler(); private static int screenWidth; private static int screenHeight; private static boolean isCameraAvailable; private FrameLayout outerFrame; private MainView mainView; private SplashScreen splashScreen = null; private Hashtable uriHandlers = new Hashtable(); 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 String rootPath = null; public native void createRhodesApp(String path); public native void startRhodesApp(); public native void stopRhodesApp(); private 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) { Uri uri = Uri.parse(url); UriHandler handler = uriHandlers.get(uri.getScheme()); if (handler == null) return false; handler.handle(uri); return true; } public WebView createWebView() { WebView w = new WebView(this); try { w.loadUrl("file:///android_asset/apps/app/loading.html"); } 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.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return handleUrlLoading(url); } @Override public void onPageFinished(WebView view, String url) { 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(); try { showSplashScreen("apps/app/loading.png"); } catch (Exception e) { setMainView(new SimpleMainView()); } // TODO: detect camera availability isCameraAvailable = true; // Register custom uri handlers here UriHandler[] handlers = { new MailUriHandler(this), new TelUriHandler(this) }; for (int i = 0; i < handlers.length; ++i) uriHandlers.put(handlers[i].scheme(), handlers[i]); 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 onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater mi = new MenuInflater(getApplication()); mi.inflate(AndroidR.menu.options, menu); return true; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: goBack(); return true; case KeyEvent.KEYCODE_HOME: stopSelf(); return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case AndroidR.id.about: AboutDialog aboutDialog = new AboutDialog(this); aboutDialog.setTitle("About"); aboutDialog.setCanceledOnTouchOutside(true); aboutDialog.setCancelable(true); aboutDialog.show(); return true; case AndroidR.id.navigation_back: goBack(); return true; case AndroidR.id.navigation_forward: goForward(); return true; case AndroidR.id.navigation_home: mainView.navigate(getStartUrl(), mainView.activeTab()); return true; case AndroidR.id.options: String curUrl = getOptionsUrl(); mainView.navigate(curUrl, mainView.activeTab()); return true; case AndroidR.id.refresh: mainView.reload(mainView.activeTab()); return true; case AndroidR.id.sync: doSyncAllSources(true); return true; case AndroidR.id.logview: showLogView(); return true; case AndroidR.id.logoptions: showLogOptions(); return true; case AndroidR.id.exit: stopSelf(); return true; } return false; } private void goBack() { mainView.back(mainView.activeTab()); } private void goForward() { mainView.forward(mainView.activeTab()); } 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])); } public static void showNetworkIndicator(boolean v) { // No GUI indicator } private static boolean hasNetwork() { 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("phone_number")) { TelephonyManager manager = (TelephonyManager)RhodesInstance.getInstance(). getSystemService(Context.TELEPHONY_SERVICE); String number = manager.getLine1Number(); return number; } return null; } public void stopSelf() { stopRhodesApp(); Process.killProcess(Process.myPid()); } static { // Load native implementation of rhodes System.loadLibrary("rhodes"); } }