michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import java.io.BufferedOutputStream; michael@0: import java.io.ByteArrayOutputStream; michael@0: import java.io.File; michael@0: import java.io.IOException; michael@0: import java.io.InputStream; michael@0: import java.io.OutputStream; michael@0: import java.net.HttpURLConnection; michael@0: import java.net.URL; michael@0: import java.text.DateFormat; michael@0: import java.text.SimpleDateFormat; michael@0: import java.util.ArrayList; michael@0: import java.util.Arrays; michael@0: import java.util.Date; michael@0: import java.util.HashMap; michael@0: import java.util.Iterator; michael@0: import java.util.LinkedList; michael@0: import java.util.List; michael@0: import java.util.Locale; michael@0: import java.util.Map; michael@0: import java.util.Set; michael@0: import java.util.regex.Matcher; michael@0: import java.util.regex.Pattern; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; michael@0: import org.mozilla.gecko.background.announcements.AnnouncementsBroadcastService; michael@0: import org.mozilla.gecko.db.BrowserDB; michael@0: import org.mozilla.gecko.favicons.Favicons; michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: import org.mozilla.gecko.gfx.Layer; michael@0: import org.mozilla.gecko.gfx.LayerView; michael@0: import org.mozilla.gecko.gfx.PluginLayer; michael@0: import org.mozilla.gecko.health.HealthRecorder; michael@0: import org.mozilla.gecko.health.SessionInformation; michael@0: import org.mozilla.gecko.health.StubbedHealthRecorder; michael@0: import org.mozilla.gecko.menu.GeckoMenu; michael@0: import org.mozilla.gecko.menu.GeckoMenuInflater; michael@0: import org.mozilla.gecko.menu.MenuPanel; michael@0: import org.mozilla.gecko.mozglue.GeckoLoader; michael@0: import org.mozilla.gecko.preferences.GeckoPreferences; michael@0: import org.mozilla.gecko.prompts.PromptService; michael@0: import org.mozilla.gecko.updater.UpdateService; michael@0: import org.mozilla.gecko.updater.UpdateServiceHelper; michael@0: import org.mozilla.gecko.util.ActivityResultHandler; michael@0: import org.mozilla.gecko.util.FileUtils; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.HardwareUtils; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.util.UiAsyncTask; michael@0: import org.mozilla.gecko.webapp.EventListener; michael@0: import org.mozilla.gecko.webapp.UninstallListener; michael@0: import org.mozilla.gecko.widget.ButtonToast; michael@0: michael@0: import android.app.Activity; michael@0: import android.app.AlertDialog; michael@0: import android.app.Dialog; michael@0: import android.content.Context; michael@0: import android.content.DialogInterface; michael@0: import android.content.Intent; michael@0: import android.content.SharedPreferences; michael@0: import android.content.pm.PackageManager.NameNotFoundException; michael@0: import android.content.res.Configuration; michael@0: import android.graphics.Bitmap; michael@0: import android.graphics.BitmapFactory; michael@0: import android.graphics.RectF; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.hardware.Sensor; michael@0: import android.hardware.SensorEvent; michael@0: import android.hardware.SensorEventListener; michael@0: import android.location.Location; michael@0: import android.location.LocationListener; michael@0: import android.net.Uri; michael@0: import android.net.wifi.ScanResult; michael@0: import android.net.wifi.WifiManager; michael@0: import android.os.Build; michael@0: import android.os.Bundle; michael@0: import android.os.Handler; michael@0: import android.os.PowerManager; michael@0: import android.os.StrictMode; michael@0: import android.provider.ContactsContract; michael@0: import android.provider.MediaStore.Images.Media; michael@0: import android.telephony.CellLocation; michael@0: import android.telephony.NeighboringCellInfo; michael@0: import android.telephony.PhoneStateListener; michael@0: import android.telephony.SignalStrength; michael@0: import android.telephony.TelephonyManager; michael@0: import android.telephony.gsm.GsmCellLocation; michael@0: import android.text.TextUtils; michael@0: import android.util.AttributeSet; michael@0: import android.util.Base64; michael@0: import android.util.Log; michael@0: import android.util.SparseBooleanArray; michael@0: import android.view.Gravity; michael@0: import android.view.KeyEvent; michael@0: import android.view.Menu; michael@0: import android.view.MenuInflater; michael@0: import android.view.MenuItem; michael@0: import android.view.MotionEvent; michael@0: import android.view.OrientationEventListener; michael@0: import android.view.SurfaceHolder; michael@0: import android.view.SurfaceView; michael@0: import android.view.TextureView; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: import android.view.ViewStub; michael@0: import android.view.Window; michael@0: import android.view.WindowManager; michael@0: import android.widget.AbsoluteLayout; michael@0: import android.widget.FrameLayout; michael@0: import android.widget.ListView; michael@0: import android.widget.RelativeLayout; michael@0: import android.widget.SimpleAdapter; michael@0: import android.widget.TextView; michael@0: import android.widget.Toast; michael@0: michael@0: public abstract class GeckoApp michael@0: extends GeckoActivity michael@0: implements michael@0: ContextGetter, michael@0: GeckoAppShell.GeckoInterface, michael@0: GeckoEventListener, michael@0: GeckoMenu.Callback, michael@0: GeckoMenu.MenuPresenter, michael@0: LocationListener, michael@0: SensorEventListener, michael@0: Tabs.OnTabsChangedListener michael@0: { michael@0: private static final String LOGTAG = "GeckoApp"; michael@0: private static final int ONE_DAY_MS = 1000*60*60*24; michael@0: michael@0: private static enum StartupAction { michael@0: NORMAL, /* normal application start */ michael@0: URL, /* launched with a passed URL */ michael@0: PREFETCH /* launched with a passed URL that we prefetch */ michael@0: } michael@0: michael@0: public static final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ACTION_ALERT_CALLBACK"; michael@0: public static final String ACTION_BOOKMARK = "org.mozilla.gecko.BOOKMARK"; michael@0: public static final String ACTION_DEBUG = "org.mozilla.gecko.DEBUG"; michael@0: public static final String ACTION_LAUNCH_SETTINGS = "org.mozilla.gecko.SETTINGS"; michael@0: public static final String ACTION_LOAD = "org.mozilla.gecko.LOAD"; michael@0: public static final String ACTION_INIT_PW = "org.mozilla.gecko.INIT_PW"; michael@0: public static final String ACTION_WEBAPP_PREFIX = "org.mozilla.gecko.WEBAPP"; michael@0: michael@0: public static final String EXTRA_STATE_BUNDLE = "stateBundle"; michael@0: michael@0: public static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle"; michael@0: public static final String PREFS_OOM_EXCEPTION = "OOMException"; michael@0: public static final String PREFS_VERSION_CODE = "versionCode"; michael@0: public static final String PREFS_WAS_STOPPED = "wasStopped"; michael@0: public static final String PREFS_CRASHED = "crashed"; michael@0: public static final String PREFS_CLEANUP_TEMP_FILES = "cleanupTempFiles"; michael@0: michael@0: public static final String SAVED_STATE_IN_BACKGROUND = "inBackground"; michael@0: public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession"; michael@0: michael@0: static private final String LOCATION_URL = "https://location.services.mozilla.com/v1/submit"; michael@0: michael@0: // Delay before running one-time "cleanup" tasks that may be needed michael@0: // after a version upgrade. michael@0: private static final int CLEANUP_DEFERRAL_SECONDS = 15; michael@0: michael@0: protected RelativeLayout mMainLayout; michael@0: protected RelativeLayout mGeckoLayout; michael@0: public View getView() { return mGeckoLayout; } michael@0: private View mCameraView; michael@0: private OrientationEventListener mCameraOrientationEventListener; michael@0: public List mAppStateListeners; michael@0: protected MenuPanel mMenuPanel; michael@0: protected Menu mMenu; michael@0: protected GeckoProfile mProfile; michael@0: protected boolean mIsRestoringActivity; michael@0: michael@0: private ContactService mContactService; michael@0: private PromptService mPromptService; michael@0: private TextSelection mTextSelection; michael@0: michael@0: protected DoorHangerPopup mDoorHangerPopup; michael@0: protected FormAssistPopup mFormAssistPopup; michael@0: protected ButtonToast mToast; michael@0: michael@0: protected LayerView mLayerView; michael@0: private AbsoluteLayout mPluginContainer; michael@0: michael@0: private FullScreenHolder mFullScreenPluginContainer; michael@0: private View mFullScreenPluginView; michael@0: michael@0: private HashMap mWakeLocks = new HashMap(); michael@0: michael@0: protected boolean mShouldRestore; michael@0: protected boolean mInitialized = false; michael@0: private Telemetry.Timer mJavaUiStartupTimer; michael@0: private Telemetry.Timer mGeckoReadyStartupTimer; michael@0: michael@0: private String mPrivateBrowsingSession; michael@0: michael@0: private volatile HealthRecorder mHealthRecorder = null; michael@0: michael@0: private int mSignalStrenth; michael@0: private PhoneStateListener mPhoneStateListener = null; michael@0: private boolean mShouldReportGeoData; michael@0: private EventListener mWebappEventListener; michael@0: michael@0: abstract public int getLayout(); michael@0: abstract public boolean hasTabsSideBar(); michael@0: abstract protected String getDefaultProfileName() throws NoMozillaDirectoryException; michael@0: michael@0: private static final String RESTARTER_ACTION = "org.mozilla.gecko.restart"; michael@0: private static final String RESTARTER_CLASS = "org.mozilla.gecko.Restarter"; michael@0: michael@0: @SuppressWarnings("serial") michael@0: class SessionRestoreException extends Exception { michael@0: public SessionRestoreException(Exception e) { michael@0: super(e); michael@0: } michael@0: michael@0: public SessionRestoreException(String message) { michael@0: super(message); michael@0: } michael@0: } michael@0: michael@0: void toggleChrome(final boolean aShow) { } michael@0: michael@0: void focusChrome() { } michael@0: michael@0: @Override michael@0: public Context getContext() { michael@0: return this; michael@0: } michael@0: michael@0: @Override michael@0: public SharedPreferences getSharedPreferences() { michael@0: return GeckoSharedPrefs.forApp(this); michael@0: } michael@0: michael@0: public Activity getActivity() { michael@0: return this; michael@0: } michael@0: michael@0: public LocationListener getLocationListener() { michael@0: if (mShouldReportGeoData && mPhoneStateListener == null) { michael@0: mPhoneStateListener = new PhoneStateListener() { michael@0: public void onSignalStrengthsChanged(SignalStrength signalStrength) { michael@0: setCurrentSignalStrenth(signalStrength); michael@0: } michael@0: }; michael@0: TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); michael@0: tm.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); michael@0: } michael@0: return this; michael@0: } michael@0: michael@0: public SensorEventListener getSensorEventListener() { michael@0: return this; michael@0: } michael@0: michael@0: public View getCameraView() { michael@0: return mCameraView; michael@0: } michael@0: michael@0: public void addAppStateListener(GeckoAppShell.AppStateListener listener) { michael@0: mAppStateListeners.add(listener); michael@0: } michael@0: michael@0: public void removeAppStateListener(GeckoAppShell.AppStateListener listener) { michael@0: mAppStateListeners.remove(listener); michael@0: } michael@0: michael@0: public FormAssistPopup getFormAssistPopup() { michael@0: return mFormAssistPopup; michael@0: } michael@0: michael@0: @Override michael@0: public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { michael@0: // When a tab is closed, it is always unselected first. michael@0: // When a tab is unselected, another tab is always selected first. michael@0: switch(msg) { michael@0: case UNSELECTED: michael@0: hidePlugins(tab); michael@0: break; michael@0: michael@0: case LOCATION_CHANGE: michael@0: // We only care about location change for the selected tab. michael@0: if (!Tabs.getInstance().isSelectedTab(tab)) michael@0: break; michael@0: // Fall through... michael@0: case SELECTED: michael@0: invalidateOptionsMenu(); michael@0: if (mFormAssistPopup != null) michael@0: mFormAssistPopup.hide(); michael@0: break; michael@0: michael@0: case LOADED: michael@0: // Sync up the layer view and the tab if the tab is michael@0: // currently displayed. michael@0: LayerView layerView = mLayerView; michael@0: if (layerView != null && Tabs.getInstance().isSelectedTab(tab)) michael@0: layerView.setBackgroundColor(tab.getBackgroundColor()); michael@0: break; michael@0: michael@0: case DESKTOP_MODE_CHANGE: michael@0: if (Tabs.getInstance().isSelectedTab(tab)) michael@0: invalidateOptionsMenu(); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: public void refreshChrome() { } michael@0: michael@0: @Override michael@0: public void invalidateOptionsMenu() { michael@0: if (mMenu == null) michael@0: return; michael@0: michael@0: onPrepareOptionsMenu(mMenu); michael@0: michael@0: if (Build.VERSION.SDK_INT >= 11) michael@0: super.invalidateOptionsMenu(); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onCreateOptionsMenu(Menu menu) { michael@0: mMenu = menu; michael@0: michael@0: MenuInflater inflater = getMenuInflater(); michael@0: inflater.inflate(R.menu.gecko_app_menu, mMenu); michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public MenuInflater getMenuInflater() { michael@0: if (Build.VERSION.SDK_INT >= 11) michael@0: return new GeckoMenuInflater(this); michael@0: else michael@0: return super.getMenuInflater(); michael@0: } michael@0: michael@0: public MenuPanel getMenuPanel() { michael@0: if (mMenuPanel == null) { michael@0: onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null); michael@0: invalidateOptionsMenu(); michael@0: } michael@0: return mMenuPanel; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onMenuItemSelected(MenuItem item) { michael@0: return onOptionsItemSelected(item); michael@0: } michael@0: michael@0: @Override michael@0: public void openMenu() { michael@0: openOptionsMenu(); michael@0: } michael@0: michael@0: @Override michael@0: public void showMenu(final View menu) { michael@0: // On devices using the custom menu, focus is cleared from the menu when its tapped. michael@0: // Close and then reshow it to avoid these issues. See bug 794581 and bug 968182. michael@0: closeMenu(); michael@0: michael@0: // Post the reshow code back to the UI thread to avoid some optimizations Android michael@0: // has put in place for menus that hide/show themselves quickly. See bug 985400. michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mMenuPanel.removeAllViews(); michael@0: mMenuPanel.addView(menu); michael@0: openOptionsMenu(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public void closeMenu() { michael@0: closeOptionsMenu(); michael@0: } michael@0: michael@0: @Override michael@0: public View onCreatePanelView(int featureId) { michael@0: if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) { michael@0: if (mMenuPanel == null) { michael@0: mMenuPanel = new MenuPanel(this, null); michael@0: } else { michael@0: // Prepare the panel everytime before showing the menu. michael@0: onPreparePanel(featureId, mMenuPanel, mMenu); michael@0: } michael@0: michael@0: return mMenuPanel; michael@0: } michael@0: michael@0: return super.onCreatePanelView(featureId); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onCreatePanelMenu(int featureId, Menu menu) { michael@0: if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) { michael@0: if (mMenuPanel == null) { michael@0: mMenuPanel = (MenuPanel) onCreatePanelView(featureId); michael@0: } michael@0: michael@0: GeckoMenu gMenu = new GeckoMenu(this, null); michael@0: gMenu.setCallback(this); michael@0: gMenu.setMenuPresenter(this); michael@0: menu = gMenu; michael@0: mMenuPanel.addView(gMenu); michael@0: michael@0: return onCreateOptionsMenu(menu); michael@0: } michael@0: michael@0: return super.onCreatePanelMenu(featureId, menu); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onPreparePanel(int featureId, View view, Menu menu) { michael@0: if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) michael@0: return onPrepareOptionsMenu(menu); michael@0: michael@0: return super.onPreparePanel(featureId, view, menu); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onMenuOpened(int featureId, Menu menu) { michael@0: // exit full-screen mode whenever the menu is opened michael@0: if (mLayerView != null && mLayerView.isFullScreen()) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FullScreen:Exit", null)); michael@0: } michael@0: michael@0: if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) { michael@0: if (mMenu == null) { michael@0: // getMenuPanel() will force the creation of the menu as well michael@0: MenuPanel panel = getMenuPanel(); michael@0: onPreparePanel(featureId, panel, mMenu); michael@0: } michael@0: michael@0: // Scroll custom menu to the top michael@0: if (mMenuPanel != null) michael@0: mMenuPanel.scrollTo(0, 0); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: return super.onMenuOpened(featureId, menu); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onOptionsItemSelected(MenuItem item) { michael@0: if (item.getItemId() == R.id.quit) { michael@0: if (GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.GeckoRunning, GeckoThread.LaunchState.GeckoExiting)) { michael@0: GeckoAppShell.notifyGeckoOfEvent(GeckoEvent.createBroadcastEvent("Browser:Quit", null)); michael@0: } else { michael@0: GeckoAppShell.systemExit(); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: return super.onOptionsItemSelected(item); michael@0: } michael@0: michael@0: @Override michael@0: public void onOptionsMenuClosed(Menu menu) { michael@0: if (Build.VERSION.SDK_INT >= 11) { michael@0: mMenuPanel.removeAllViews(); michael@0: mMenuPanel.addView((GeckoMenu) mMenu); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyDown(int keyCode, KeyEvent event) { michael@0: // Handle hardware menu key presses separately so that we can show a custom menu in some cases. michael@0: if (keyCode == KeyEvent.KEYCODE_MENU) { michael@0: openOptionsMenu(); michael@0: return true; michael@0: } michael@0: michael@0: return super.onKeyDown(keyCode, event); michael@0: } michael@0: michael@0: @Override michael@0: protected void onSaveInstanceState(Bundle outState) { michael@0: super.onSaveInstanceState(outState); michael@0: michael@0: if (mToast != null) { michael@0: mToast.onSaveInstanceState(outState); michael@0: } michael@0: michael@0: outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground()); michael@0: outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession); michael@0: } michael@0: michael@0: void handleFaviconRequest(final String url) { michael@0: (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { michael@0: @Override michael@0: public String doInBackground(Void... params) { michael@0: return Favicons.getFaviconURLForPageURL(url); michael@0: } michael@0: michael@0: @Override michael@0: public void onPostExecute(String faviconUrl) { michael@0: JSONObject args = new JSONObject(); michael@0: michael@0: if (faviconUrl != null) { michael@0: try { michael@0: args.put("url", url); michael@0: args.put("faviconUrl", faviconUrl); michael@0: } catch (JSONException e) { michael@0: Log.w(LOGTAG, "Error building JSON favicon arguments.", e); michael@0: } michael@0: } michael@0: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:FaviconReturn", args.toString())); michael@0: } michael@0: }).execute(); michael@0: } michael@0: michael@0: void handleClearHistory() { michael@0: BrowserDB.clearHistory(getContentResolver()); michael@0: } michael@0: michael@0: public void addTab() { } michael@0: michael@0: public void addPrivateTab() { } michael@0: michael@0: public void showNormalTabs() { } michael@0: michael@0: public void showPrivateTabs() { } michael@0: michael@0: public void hideTabs() { } michael@0: michael@0: /** michael@0: * Close the tab UI indirectly (not as the result of a direct user michael@0: * action). This does not force the UI to close; for example in Firefox michael@0: * tablet mode it will remain open unless the user explicitly closes it. michael@0: * michael@0: * @return True if the tab UI was hidden. michael@0: */ michael@0: public boolean autoHideTabs() { return false; } michael@0: michael@0: public boolean areTabsShown() { return false; } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: try { michael@0: if (event.equals("Toast:Show")) { michael@0: final String msg = message.getString("message"); michael@0: final JSONObject button = message.optJSONObject("button"); michael@0: if (button != null) { michael@0: final String label = button.optString("label"); michael@0: final String icon = button.optString("icon"); michael@0: final String id = button.optString("id"); michael@0: showButtonToast(msg, label, icon, id); michael@0: } else { michael@0: final String duration = message.getString("duration"); michael@0: showNormalToast(msg, duration); michael@0: } michael@0: } else if (event.equals("log")) { michael@0: // generic log listener michael@0: final String msg = message.getString("msg"); michael@0: Log.d(LOGTAG, "Log: " + msg); michael@0: } else if (event.equals("Reader:FaviconRequest")) { michael@0: final String url = message.getString("url"); michael@0: handleFaviconRequest(url); michael@0: } else if (event.equals("Gecko:DelayedStartup")) { michael@0: ThreadUtils.postToBackgroundThread(new UninstallListener.DelayedStartupTask(this)); michael@0: } else if (event.equals("Gecko:Ready")) { michael@0: mGeckoReadyStartupTimer.stop(); michael@0: geckoConnected(); michael@0: michael@0: // This method is already running on the background thread, so we michael@0: // know that mHealthRecorder will exist. That doesn't stop us being michael@0: // paranoid. michael@0: // This method is cheap, so don't spawn a new runnable. michael@0: final HealthRecorder rec = mHealthRecorder; michael@0: if (rec != null) { michael@0: rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed()); michael@0: } michael@0: } else if (event.equals("ToggleChrome:Hide")) { michael@0: toggleChrome(false); michael@0: } else if (event.equals("ToggleChrome:Show")) { michael@0: toggleChrome(true); michael@0: } else if (event.equals("ToggleChrome:Focus")) { michael@0: focusChrome(); michael@0: } else if (event.equals("DOMFullScreen:Start")) { michael@0: // Local ref to layerView for thread safety michael@0: LayerView layerView = mLayerView; michael@0: if (layerView != null) { michael@0: layerView.setFullScreen(true); michael@0: } michael@0: } else if (event.equals("DOMFullScreen:Stop")) { michael@0: // Local ref to layerView for thread safety michael@0: LayerView layerView = mLayerView; michael@0: if (layerView != null) { michael@0: layerView.setFullScreen(false); michael@0: } michael@0: } else if (event.equals("Permissions:Data")) { michael@0: String host = message.getString("host"); michael@0: JSONArray permissions = message.getJSONArray("permissions"); michael@0: showSiteSettingsDialog(host, permissions); michael@0: } else if (event.equals("Session:StatePurged")) { michael@0: onStatePurged(); michael@0: } else if (event.equals("Bookmark:Insert")) { michael@0: final String url = message.getString("url"); michael@0: final String title = message.getString("title"); michael@0: final Context context = this; michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Toast.makeText(context, R.string.bookmark_added, Toast.LENGTH_SHORT).show(); michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: BrowserDB.addBookmark(getContentResolver(), title, url); michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: } else if (event.equals("Accessibility:Event")) { michael@0: GeckoAccessibility.sendAccessibilityEvent(message); michael@0: } else if (event.equals("Accessibility:Ready")) { michael@0: GeckoAccessibility.updateAccessibilitySettings(this); michael@0: } else if (event.equals("Shortcut:Remove")) { michael@0: final String url = message.getString("url"); michael@0: final String origin = message.getString("origin"); michael@0: final String title = message.getString("title"); michael@0: final String type = message.getString("shortcutType"); michael@0: GeckoAppShell.removeShortcut(title, url, origin, type); michael@0: } else if (event.equals("Share:Text")) { michael@0: String text = message.getString("text"); michael@0: GeckoAppShell.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, ""); michael@0: michael@0: // Context: Sharing via chrome list (no explicit session is active) michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST); michael@0: } else if (event.equals("Image:SetAs")) { michael@0: String src = message.getString("url"); michael@0: setImageAs(src); michael@0: } else if (event.equals("Sanitize:ClearHistory")) { michael@0: handleClearHistory(); michael@0: } else if (event.equals("Update:Check")) { michael@0: startService(new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, this, UpdateService.class)); michael@0: } else if (event.equals("Update:Download")) { michael@0: startService(new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE, null, this, UpdateService.class)); michael@0: } else if (event.equals("Update:Install")) { michael@0: startService(new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE, null, this, UpdateService.class)); michael@0: } else if (event.equals("PrivateBrowsing:Data")) { michael@0: // null strings return "null" (http://code.google.com/p/android/issues/detail?id=13830) michael@0: if (message.isNull("session")) { michael@0: mPrivateBrowsingSession = null; michael@0: } else { michael@0: mPrivateBrowsingSession = message.getString("session"); michael@0: } michael@0: } else if (event.equals("Contact:Add")) { michael@0: if (!message.isNull("email")) { michael@0: Uri contactUri = Uri.parse(message.getString("email")); michael@0: Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri); michael@0: startActivity(i); michael@0: } else if (!message.isNull("phone")) { michael@0: Uri contactUri = Uri.parse(message.getString("phone")); michael@0: Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri); michael@0: startActivity(i); michael@0: } else { michael@0: // something went wrong. michael@0: Log.e(LOGTAG, "Received Contact:Add message with no email nor phone number"); michael@0: } michael@0: } else if (event.equals("Intent:GetHandlers")) { michael@0: Intent intent = GeckoAppShell.getOpenURIIntent((Context) this, message.optString("url"), michael@0: message.optString("mime"), message.optString("action"), message.optString("title")); michael@0: String[] handlers = GeckoAppShell.getHandlersForIntent(intent); michael@0: List appList = Arrays.asList(handlers); michael@0: JSONObject handlersJSON = new JSONObject(); michael@0: handlersJSON.put("apps", new JSONArray(appList)); michael@0: EventDispatcher.sendResponse(message, handlersJSON); michael@0: } else if (event.equals("Intent:Open")) { michael@0: GeckoAppShell.openUriExternal(message.optString("url"), michael@0: message.optString("mime"), message.optString("packageName"), michael@0: message.optString("className"), message.optString("action"), message.optString("title")); michael@0: } else if (event.equals("Intent:OpenForResult")) { michael@0: Intent intent = GeckoAppShell.getOpenURIIntent(this, michael@0: message.optString("url"), michael@0: message.optString("mime"), michael@0: message.optString("action"), michael@0: message.optString("title")); michael@0: intent.setClassName(message.optString("packageName"), message.optString("className")); michael@0: michael@0: intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); michael@0: michael@0: final JSONObject originalMessage = message; michael@0: ActivityHandlerHelper.startIntentForActivity(this, michael@0: intent, michael@0: new ActivityResultHandler() { michael@0: @Override michael@0: public void onActivityResult (int resultCode, Intent data) { michael@0: JSONObject response = new JSONObject(); michael@0: michael@0: try { michael@0: if (data != null) { michael@0: response.put("extras", bundleToJSON(data.getExtras())); michael@0: } michael@0: response.put("resultCode", resultCode); michael@0: } catch (JSONException e) { michael@0: Log.w(LOGTAG, "Error building JSON response.", e); michael@0: } michael@0: michael@0: EventDispatcher.sendResponse(originalMessage, response); michael@0: } michael@0: }); michael@0: } else if (event.equals("Locale:Set")) { michael@0: setLocale(message.getString("locale")); michael@0: } else if (event.equals("NativeApp:IsDebuggable")) { michael@0: JSONObject ret = new JSONObject(); michael@0: ret.put("isDebuggable", getIsDebuggable()); michael@0: EventDispatcher.sendResponse(message, ret); michael@0: } else if (event.equals("SystemUI:Visibility")) { michael@0: setSystemUiVisible(message.getBoolean("visible")); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); michael@0: } michael@0: } michael@0: michael@0: void onStatePurged() { } michael@0: michael@0: /** michael@0: * @param aPermissions michael@0: * Array of JSON objects to represent site permissions. michael@0: * Example: { type: "offline-app", setting: "Store Offline Data", value: "Allow" } michael@0: */ michael@0: private void showSiteSettingsDialog(String aHost, JSONArray aPermissions) { michael@0: final AlertDialog.Builder builder = new AlertDialog.Builder(this); michael@0: michael@0: View customTitleView = getLayoutInflater().inflate(R.layout.site_setting_title, null); michael@0: ((TextView) customTitleView.findViewById(R.id.title)).setText(R.string.site_settings_title); michael@0: ((TextView) customTitleView.findViewById(R.id.host)).setText(aHost); michael@0: builder.setCustomTitle(customTitleView); michael@0: michael@0: // If there are no permissions to clear, show the user a message about that. michael@0: // In the future, we want to disable the menu item if there are no permissions to clear. michael@0: if (aPermissions.length() == 0) { michael@0: builder.setMessage(R.string.site_settings_no_settings); michael@0: } else { michael@0: michael@0: ArrayList > itemList = new ArrayList >(); michael@0: for (int i = 0; i < aPermissions.length(); i++) { michael@0: try { michael@0: JSONObject permObj = aPermissions.getJSONObject(i); michael@0: HashMap map = new HashMap(); michael@0: map.put("setting", permObj.getString("setting")); michael@0: map.put("value", permObj.getString("value")); michael@0: itemList.add(map); michael@0: } catch (JSONException e) { michael@0: Log.w(LOGTAG, "Exception populating settings items.", e); michael@0: } michael@0: } michael@0: michael@0: // setMultiChoiceItems doesn't support using an adapter, so we're creating a hack with michael@0: // setSingleChoiceItems and changing the choiceMode below when we create the dialog michael@0: builder.setSingleChoiceItems(new SimpleAdapter( michael@0: GeckoApp.this, michael@0: itemList, michael@0: R.layout.site_setting_item, michael@0: new String[] { "setting", "value" }, michael@0: new int[] { R.id.setting, R.id.value } michael@0: ), -1, new DialogInterface.OnClickListener() { michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int id) { } michael@0: }); michael@0: michael@0: builder.setPositiveButton(R.string.site_settings_clear, new DialogInterface.OnClickListener() { michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int id) { michael@0: ListView listView = ((AlertDialog) dialog).getListView(); michael@0: SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions(); michael@0: michael@0: // An array of the indices of the permissions we want to clear michael@0: JSONArray permissionsToClear = new JSONArray(); michael@0: for (int i = 0; i < checkedItemPositions.size(); i++) michael@0: if (checkedItemPositions.get(i)) michael@0: permissionsToClear.put(i); michael@0: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent( michael@0: "Permissions:Clear", permissionsToClear.toString())); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener(){ michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int id) { michael@0: dialog.cancel(); michael@0: } michael@0: }); michael@0: michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Dialog dialog = builder.create(); michael@0: dialog.show(); michael@0: michael@0: ListView listView = ((AlertDialog) dialog).getListView(); michael@0: if (listView != null) { michael@0: listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); michael@0: int listSize = listView.getAdapter().getCount(); michael@0: for (int i = 0; i < listSize; i++) michael@0: listView.setItemChecked(i, true); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: public void showToast(final int resId, final int duration) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Toast.makeText(GeckoApp.this, resId, duration).show(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: public void showNormalToast(final String message, final String duration) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Toast toast; michael@0: if (duration.equals("long")) { michael@0: toast = Toast.makeText(GeckoApp.this, message, Toast.LENGTH_LONG); michael@0: } else { michael@0: toast = Toast.makeText(GeckoApp.this, message, Toast.LENGTH_SHORT); michael@0: } michael@0: toast.show(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: protected ButtonToast getButtonToast() { michael@0: if (mToast != null) { michael@0: return mToast; michael@0: } michael@0: michael@0: ViewStub toastStub = (ViewStub) findViewById(R.id.toast_stub); michael@0: mToast = new ButtonToast(toastStub.inflate()); michael@0: michael@0: return mToast; michael@0: } michael@0: michael@0: void showButtonToast(final String message, final String buttonText, michael@0: final String buttonIcon, final String buttonId) { michael@0: BitmapUtils.getDrawable(GeckoApp.this, buttonIcon, new BitmapUtils.BitmapLoader() { michael@0: @Override michael@0: public void onBitmapFound(final Drawable d) { michael@0: getButtonToast().show(false, message, buttonText, d, new ButtonToast.ToastListener() { michael@0: @Override michael@0: public void onButtonClicked() { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Click", buttonId)); michael@0: } michael@0: michael@0: @Override michael@0: public void onToastHidden(ButtonToast.ReasonHidden reason) { michael@0: if (reason == ButtonToast.ReasonHidden.TIMEOUT) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Hidden", buttonId)); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private JSONObject bundleToJSON(Bundle bundle) { michael@0: JSONObject json = new JSONObject(); michael@0: if (bundle == null) { michael@0: return json; michael@0: } michael@0: michael@0: for (String key : bundle.keySet()) { michael@0: try { michael@0: json.put(key, bundle.get(key)); michael@0: } catch (JSONException e) { michael@0: Log.w(LOGTAG, "Error building JSON response.", e); michael@0: } michael@0: } michael@0: michael@0: return json; michael@0: } michael@0: michael@0: private void addFullScreenPluginView(View view) { michael@0: if (mFullScreenPluginView != null) { michael@0: Log.w(LOGTAG, "Already have a fullscreen plugin view"); michael@0: return; michael@0: } michael@0: michael@0: setFullScreen(true); michael@0: michael@0: view.setWillNotDraw(false); michael@0: if (view instanceof SurfaceView) { michael@0: ((SurfaceView) view).setZOrderOnTop(true); michael@0: } michael@0: michael@0: mFullScreenPluginContainer = new FullScreenHolder(this); michael@0: michael@0: FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( michael@0: ViewGroup.LayoutParams.FILL_PARENT, michael@0: ViewGroup.LayoutParams.FILL_PARENT, michael@0: Gravity.CENTER); michael@0: mFullScreenPluginContainer.addView(view, layoutParams); michael@0: michael@0: michael@0: FrameLayout decor = (FrameLayout)getWindow().getDecorView(); michael@0: decor.addView(mFullScreenPluginContainer, layoutParams); michael@0: michael@0: mFullScreenPluginView = view; michael@0: } michael@0: michael@0: public void addPluginView(final View view, final RectF rect, final boolean isFullScreen) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Tabs tabs = Tabs.getInstance(); michael@0: Tab tab = tabs.getSelectedTab(); michael@0: michael@0: if (isFullScreen) { michael@0: addFullScreenPluginView(view); michael@0: return; michael@0: } michael@0: michael@0: PluginLayer layer = (PluginLayer) tab.getPluginLayer(view); michael@0: if (layer == null) { michael@0: layer = new PluginLayer(view, rect, mLayerView.getRenderer().getMaxTextureSize()); michael@0: tab.addPluginLayer(view, layer); michael@0: } else { michael@0: layer.reset(rect); michael@0: layer.setVisible(true); michael@0: } michael@0: michael@0: mLayerView.addLayer(layer); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void removeFullScreenPluginView(View view) { michael@0: if (mFullScreenPluginView == null) { michael@0: Log.w(LOGTAG, "Don't have a fullscreen plugin view"); michael@0: return; michael@0: } michael@0: michael@0: if (mFullScreenPluginView != view) { michael@0: Log.w(LOGTAG, "Passed view is not the current full screen view"); michael@0: return; michael@0: } michael@0: michael@0: mFullScreenPluginContainer.removeView(mFullScreenPluginView); michael@0: michael@0: // We need do do this on the next iteration in order to avoid michael@0: // a deadlock, see comment below in FullScreenHolder michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mLayerView.showSurface(); michael@0: } michael@0: }); michael@0: michael@0: FrameLayout decor = (FrameLayout)getWindow().getDecorView(); michael@0: decor.removeView(mFullScreenPluginContainer); michael@0: michael@0: mFullScreenPluginView = null; michael@0: michael@0: GeckoScreenOrientation.getInstance().unlock(); michael@0: setFullScreen(false); michael@0: } michael@0: michael@0: public void removePluginView(final View view, final boolean isFullScreen) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Tabs tabs = Tabs.getInstance(); michael@0: Tab tab = tabs.getSelectedTab(); michael@0: michael@0: if (isFullScreen) { michael@0: removeFullScreenPluginView(view); michael@0: return; michael@0: } michael@0: michael@0: PluginLayer layer = (PluginLayer) tab.removePluginLayer(view); michael@0: if (layer != null) { michael@0: layer.destroy(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: // This method starts downloading an image synchronously and displays the Chooser activity to set the image as wallpaper. michael@0: private void setImageAs(final String aSrc) { michael@0: boolean isDataURI = aSrc.startsWith("data:"); michael@0: Bitmap image = null; michael@0: InputStream is = null; michael@0: ByteArrayOutputStream os = null; michael@0: try { michael@0: if (isDataURI) { michael@0: int dataStart = aSrc.indexOf(","); michael@0: byte[] buf = Base64.decode(aSrc.substring(dataStart+1), Base64.DEFAULT); michael@0: image = BitmapUtils.decodeByteArray(buf); michael@0: } else { michael@0: int byteRead; michael@0: byte[] buf = new byte[4192]; michael@0: os = new ByteArrayOutputStream(); michael@0: URL url = new URL(aSrc); michael@0: is = url.openStream(); michael@0: michael@0: // Cannot read from same stream twice. Also, InputStream from michael@0: // URL does not support reset. So converting to byte array. michael@0: michael@0: while((byteRead = is.read(buf)) != -1) { michael@0: os.write(buf, 0, byteRead); michael@0: } michael@0: byte[] imgBuffer = os.toByteArray(); michael@0: image = BitmapUtils.decodeByteArray(imgBuffer); michael@0: } michael@0: if (image != null) { michael@0: String path = Media.insertImage(getContentResolver(),image, null, null); michael@0: final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); michael@0: intent.addCategory(Intent.CATEGORY_DEFAULT); michael@0: intent.setData(Uri.parse(path)); michael@0: michael@0: // Removes the image from storage once the chooser activity ends. michael@0: Intent chooser = Intent.createChooser(intent, getString(R.string.set_image_chooser_title)); michael@0: ActivityResultHandler handler = new ActivityResultHandler() { michael@0: @Override michael@0: public void onActivityResult (int resultCode, Intent data) { michael@0: getContentResolver().delete(intent.getData(), null, null); michael@0: } michael@0: }; michael@0: ActivityHandlerHelper.startIntentForActivity(this, chooser, handler); michael@0: } else { michael@0: Toast.makeText((Context) this, R.string.set_image_fail, Toast.LENGTH_SHORT).show(); michael@0: } michael@0: } catch(OutOfMemoryError ome) { michael@0: Log.e(LOGTAG, "Out of Memory when converting to byte array", ome); michael@0: } catch(IOException ioe) { michael@0: Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe); michael@0: } finally { michael@0: if (is != null) { michael@0: try { michael@0: is.close(); michael@0: } catch(IOException ioe) { michael@0: Log.w(LOGTAG, "I/O Exception while closing stream", ioe); michael@0: } michael@0: } michael@0: if (os != null) { michael@0: try { michael@0: os.close(); michael@0: } catch(IOException ioe) { michael@0: Log.w(LOGTAG, "I/O Exception while closing stream", ioe); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: private int getBitmapSampleSize(BitmapFactory.Options options, int idealWidth, int idealHeight) { michael@0: int width = options.outWidth; michael@0: int height = options.outHeight; michael@0: int inSampleSize = 1; michael@0: if (height > idealHeight || width > idealWidth) { michael@0: if (width > height) { michael@0: inSampleSize = Math.round((float)height / (float)idealHeight); michael@0: } else { michael@0: inSampleSize = Math.round((float)width / (float)idealWidth); michael@0: } michael@0: } michael@0: return inSampleSize; michael@0: } michael@0: michael@0: private void hidePluginLayer(Layer layer) { michael@0: LayerView layerView = mLayerView; michael@0: layerView.removeLayer(layer); michael@0: layerView.requestRender(); michael@0: } michael@0: michael@0: private void showPluginLayer(Layer layer) { michael@0: LayerView layerView = mLayerView; michael@0: layerView.addLayer(layer); michael@0: layerView.requestRender(); michael@0: } michael@0: michael@0: public void requestRender() { michael@0: mLayerView.requestRender(); michael@0: } michael@0: michael@0: public void hidePlugins(Tab tab) { michael@0: for (Layer layer : tab.getPluginLayers()) { michael@0: if (layer instanceof PluginLayer) { michael@0: ((PluginLayer) layer).setVisible(false); michael@0: } michael@0: michael@0: hidePluginLayer(layer); michael@0: } michael@0: michael@0: requestRender(); michael@0: } michael@0: michael@0: public void showPlugins() { michael@0: Tabs tabs = Tabs.getInstance(); michael@0: Tab tab = tabs.getSelectedTab(); michael@0: michael@0: showPlugins(tab); michael@0: } michael@0: michael@0: public void showPlugins(Tab tab) { michael@0: for (Layer layer : tab.getPluginLayers()) { michael@0: showPluginLayer(layer); michael@0: michael@0: if (layer instanceof PluginLayer) { michael@0: ((PluginLayer) layer).setVisible(true); michael@0: } michael@0: } michael@0: michael@0: requestRender(); michael@0: } michael@0: michael@0: public void setFullScreen(final boolean fullscreen) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: // Hide/show the system notification bar michael@0: Window window = getWindow(); michael@0: window.setFlags(fullscreen ? michael@0: WindowManager.LayoutParams.FLAG_FULLSCREEN : 0, michael@0: WindowManager.LayoutParams.FLAG_FULLSCREEN); michael@0: michael@0: if (Build.VERSION.SDK_INT >= 11) michael@0: window.getDecorView().setSystemUiVisibility(fullscreen ? 1 : 0); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Check and start the Java profiler if MOZ_PROFILER_STARTUP env var is specified michael@0: **/ michael@0: protected void earlyStartJavaSampler(Intent intent) michael@0: { michael@0: String env = intent.getStringExtra("env0"); michael@0: for (int i = 1; env != null; i++) { michael@0: if (env.startsWith("MOZ_PROFILER_STARTUP=")) { michael@0: if (!env.endsWith("=")) { michael@0: GeckoJavaSampler.start(10, 1000); michael@0: Log.d(LOGTAG, "Profiling Java on startup"); michael@0: } michael@0: break; michael@0: } michael@0: env = intent.getStringExtra("env" + i); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Called when the activity is first created. michael@0: * michael@0: * Here we initialize all of our profile settings, Firefox Health Report, michael@0: * and other one-shot constructions. michael@0: **/ michael@0: @Override michael@0: public void onCreate(Bundle savedInstanceState) michael@0: { michael@0: GeckoAppShell.registerGlobalExceptionHandler(); michael@0: michael@0: // Enable Android Strict Mode for developers' local builds (the "default" channel). michael@0: if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) { michael@0: enableStrictMode(); michael@0: } michael@0: michael@0: // The clock starts...now. Better hurry! michael@0: mJavaUiStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_JAVAUI"); michael@0: mGeckoReadyStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_GECKOREADY"); michael@0: michael@0: final Intent intent = getIntent(); michael@0: final String args = intent.getStringExtra("args"); michael@0: michael@0: earlyStartJavaSampler(intent); michael@0: michael@0: // GeckoLoader wants to dig some environment variables out of the michael@0: // incoming intent, so pass it in here. GeckoLoader will do its michael@0: // business later and dispose of the reference. michael@0: GeckoLoader.setLastIntent(intent); michael@0: michael@0: if (mProfile == null) { michael@0: String profileName = null; michael@0: String profilePath = null; michael@0: if (args != null) { michael@0: if (args.contains("-P")) { michael@0: Pattern p = Pattern.compile("(?:-P\\s*)(\\w*)(\\s*)"); michael@0: Matcher m = p.matcher(args); michael@0: if (m.find()) { michael@0: profileName = m.group(1); michael@0: } michael@0: } michael@0: michael@0: if (args.contains("-profile")) { michael@0: Pattern p = Pattern.compile("(?:-profile\\s*)(\\S*)(\\s*)"); michael@0: Matcher m = p.matcher(args); michael@0: if (m.find()) { michael@0: profilePath = m.group(1); michael@0: } michael@0: if (profileName == null) { michael@0: try { michael@0: profileName = getDefaultProfileName(); michael@0: } catch (NoMozillaDirectoryException e) { michael@0: Log.wtf(LOGTAG, "Unable to fetch default profile name!", e); michael@0: // There's nothing at all we can do now. If the Mozilla directory michael@0: // didn't exist, then we're screwed. michael@0: // Crash here so we can fix the bug. michael@0: throw new RuntimeException(e); michael@0: } michael@0: if (profileName == null) michael@0: profileName = GeckoProfile.DEFAULT_PROFILE; michael@0: } michael@0: GeckoProfile.sIsUsingCustomProfile = true; michael@0: } michael@0: michael@0: if (profileName != null || profilePath != null) { michael@0: mProfile = GeckoProfile.get(this, profileName, profilePath); michael@0: } michael@0: } michael@0: } michael@0: michael@0: BrowserDB.initialize(getProfile().getName()); michael@0: michael@0: // Workaround for . michael@0: try { michael@0: Class.forName("android.os.AsyncTask"); michael@0: } catch (ClassNotFoundException e) {} michael@0: michael@0: MemoryMonitor.getInstance().init(getApplicationContext()); michael@0: michael@0: // GeckoAppShell is tightly coupled to us, rather than michael@0: // the app context, because various parts of Fennec (e.g., michael@0: // GeckoScreenOrientation) use GAS to access the Activity in michael@0: // the guise of fetching a Context. michael@0: // When that's fixed, `this` can change to michael@0: // `(GeckoApplication) getApplication()` here. michael@0: GeckoAppShell.setContextGetter(this); michael@0: GeckoAppShell.setGeckoInterface(this); michael@0: michael@0: ThreadUtils.setUiThread(Thread.currentThread(), new Handler()); michael@0: michael@0: Tabs.getInstance().attachToContext(this); michael@0: try { michael@0: Favicons.attachToContext(this); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Exception starting favicon cache. Corrupt resources?", e); michael@0: } michael@0: michael@0: // Did the OS locale change while we were backgrounded? If so, michael@0: // we need to die so that Gecko will re-init add-ons that touch michael@0: // the UI. michael@0: // This is using a sledgehammer to crack a nut, but it'll do for michael@0: // now. michael@0: if (BrowserLocaleManager.getInstance().systemLocaleDidChange()) { michael@0: Log.i(LOGTAG, "System locale changed. Restarting."); michael@0: doRestart(); michael@0: GeckoAppShell.systemExit(); michael@0: return; michael@0: } michael@0: michael@0: if (GeckoThread.isCreated()) { michael@0: // This happens when the GeckoApp activity is destroyed by Android michael@0: // without killing the entire application (see Bug 769269). michael@0: mIsRestoringActivity = true; michael@0: Telemetry.HistogramAdd("FENNEC_RESTORING_ACTIVITY", 1); michael@0: } michael@0: michael@0: // Fix for Bug 830557 on Tegra boards running Froyo. michael@0: // This fix must be done before doing layout. michael@0: // Assume the bug is fixed in Gingerbread and up. michael@0: if (Build.VERSION.SDK_INT < 9) { michael@0: try { michael@0: Class inputBindResultClass = michael@0: Class.forName("com.android.internal.view.InputBindResult"); michael@0: java.lang.reflect.Field creatorField = michael@0: inputBindResultClass.getField("CREATOR"); michael@0: Log.i(LOGTAG, "froyo startup fix: " + String.valueOf(creatorField.get(null))); michael@0: } catch (Exception e) { michael@0: Log.w(LOGTAG, "froyo startup fix failed", e); michael@0: } michael@0: } michael@0: michael@0: Bundle stateBundle = getIntent().getBundleExtra(EXTRA_STATE_BUNDLE); michael@0: if (stateBundle != null) { michael@0: // Use the state bundle if it was given as an intent extra. This is michael@0: // only intended to be used internally via Robocop, so a boolean michael@0: // is read from a private shared pref to prevent other apps from michael@0: // injecting states. michael@0: final SharedPreferences prefs = getSharedPreferences(); michael@0: if (prefs.getBoolean(PREFS_ALLOW_STATE_BUNDLE, false)) { michael@0: Log.i(LOGTAG, "Restoring state from intent bundle"); michael@0: prefs.edit().remove(PREFS_ALLOW_STATE_BUNDLE).commit(); michael@0: savedInstanceState = stateBundle; michael@0: } michael@0: } else if (savedInstanceState != null) { michael@0: // Bug 896992 - This intent has already been handled; reset the intent. michael@0: setIntent(new Intent(Intent.ACTION_MAIN)); michael@0: } michael@0: michael@0: super.onCreate(savedInstanceState); michael@0: michael@0: GeckoScreenOrientation.getInstance().update(getResources().getConfiguration().orientation); michael@0: michael@0: setContentView(getLayout()); michael@0: michael@0: // Set up Gecko layout. michael@0: mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout); michael@0: mMainLayout = (RelativeLayout) findViewById(R.id.main_layout); michael@0: michael@0: // Determine whether we should restore tabs. michael@0: mShouldRestore = getSessionRestoreState(savedInstanceState); michael@0: if (mShouldRestore && savedInstanceState != null) { michael@0: boolean wasInBackground = michael@0: savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false); michael@0: michael@0: // Don't log OOM-kills if only one activity was destroyed. (For example michael@0: // from "Don't keep activities" on ICS) michael@0: if (!wasInBackground && !mIsRestoringActivity) { michael@0: Telemetry.HistogramAdd("FENNEC_WAS_KILLED", 1); michael@0: } michael@0: michael@0: mPrivateBrowsingSession = savedInstanceState.getString(SAVED_STATE_PRIVATE_SESSION); michael@0: } michael@0: michael@0: // Perform background initialization. michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: final SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); michael@0: michael@0: // Wait until now to set this, because we'd rather throw an exception than michael@0: // have a caller of BrowserLocaleManager regress startup. michael@0: BrowserLocaleManager.getInstance().initialize(getApplicationContext()); michael@0: michael@0: SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs); michael@0: if (previousSession.wasKilled()) { michael@0: Telemetry.HistogramAdd("FENNEC_WAS_KILLED", 1); michael@0: } michael@0: michael@0: SharedPreferences.Editor editor = prefs.edit(); michael@0: editor.putBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false); michael@0: michael@0: // Put a flag to check if we got a normal `onSaveInstanceState` michael@0: // on exit, or if we were suddenly killed (crash or native OOM). michael@0: editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); michael@0: michael@0: editor.commit(); michael@0: michael@0: // The lifecycle of mHealthRecorder is "shortly after onCreate" michael@0: // through "onDestroy" -- essentially the same as the lifecycle michael@0: // of the activity itself. michael@0: final String profilePath = getProfile().getDir().getAbsolutePath(); michael@0: final EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher(); michael@0: Log.i(LOGTAG, "Creating HealthRecorder."); michael@0: michael@0: final String osLocale = Locale.getDefault().toString(); michael@0: String appLocale = BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(GeckoApp.this); michael@0: Log.d(LOGTAG, "OS locale is " + osLocale + ", app locale is " + appLocale); michael@0: michael@0: if (appLocale == null) { michael@0: appLocale = osLocale; michael@0: } michael@0: michael@0: mHealthRecorder = GeckoApp.this.createHealthRecorder(GeckoApp.this, michael@0: profilePath, michael@0: dispatcher, michael@0: osLocale, michael@0: appLocale, michael@0: previousSession); michael@0: michael@0: final String uiLocale = appLocale; michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: GeckoApp.this.onLocaleReady(uiLocale); michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: GeckoAppShell.setNotificationClient(makeNotificationClient()); michael@0: NotificationHelper.init(getApplicationContext()); michael@0: } michael@0: michael@0: /** michael@0: * At this point, the resource system and the rest of the browser are michael@0: * aware of the locale. michael@0: * michael@0: * Now we can display strings! michael@0: * michael@0: * You can think of this as being something like a second phase of onCreate, michael@0: * where you can do string-related operations. Use this in place of embedding michael@0: * strings in view XML. michael@0: * michael@0: * By contrast, onConfigurationChanged does some locale operations, but is in michael@0: * response to device changes. michael@0: */ michael@0: @Override michael@0: public void onLocaleReady(final String locale) { michael@0: if (!ThreadUtils.isOnUiThread()) { michael@0: throw new RuntimeException("onLocaleReady must always be called from the UI thread."); michael@0: } michael@0: michael@0: // The URL bar hint needs to be populated. michael@0: TextView urlBar = (TextView) findViewById(R.id.url_bar_title); michael@0: if (urlBar != null) { michael@0: final String hint = getResources().getString(R.string.url_bar_default_text); michael@0: urlBar.setHint(hint); michael@0: } else { michael@0: Log.d(LOGTAG, "No URL bar in GeckoApp. Not loading localized hint string."); michael@0: } michael@0: michael@0: // Allow onConfigurationChanged to take care of the rest. michael@0: onConfigurationChanged(getResources().getConfiguration()); michael@0: } michael@0: michael@0: protected void initializeChrome() { michael@0: mDoorHangerPopup = new DoorHangerPopup(this); michael@0: mPluginContainer = (AbsoluteLayout) findViewById(R.id.plugin_container); michael@0: mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup); michael@0: michael@0: if (mCameraView == null) { michael@0: if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { michael@0: mCameraView = new SurfaceView(this); michael@0: ((SurfaceView)mCameraView).getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); michael@0: } else { michael@0: mCameraView = new TextureView(this); michael@0: } michael@0: } michael@0: michael@0: if (mLayerView == null) { michael@0: LayerView layerView = (LayerView) findViewById(R.id.layer_view); michael@0: layerView.initializeView(GeckoAppShell.getEventDispatcher()); michael@0: mLayerView = layerView; michael@0: GeckoAppShell.setLayerView(layerView); michael@0: // bind the GeckoEditable instance to the new LayerView michael@0: GeckoAppShell.notifyIMEContext(GeckoEditableListener.IME_STATE_DISABLED, "", "", ""); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Loads the initial tab at Fennec startup. michael@0: * michael@0: * If Fennec was opened with an external URL, that URL will be loaded. michael@0: * Otherwise, unless there was a session restore, the default URL michael@0: * (about:home) be loaded. michael@0: * michael@0: * @param url External URL to load, or null to load the default URL michael@0: */ michael@0: protected void loadStartupTab(String url) { michael@0: if (url == null) { michael@0: if (!mShouldRestore) { michael@0: // Show about:home if we aren't restoring previous session and michael@0: // there's no external URL. michael@0: Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB); michael@0: } michael@0: } else { michael@0: // If given an external URL, load it michael@0: int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL; michael@0: Tabs.getInstance().loadUrl(url, flags); michael@0: } michael@0: } michael@0: michael@0: private void initialize() { michael@0: mInitialized = true; michael@0: michael@0: Intent intent = getIntent(); michael@0: String action = intent.getAction(); michael@0: michael@0: String passedUri = null; michael@0: final String uri = getURIFromIntent(intent); michael@0: if (!TextUtils.isEmpty(uri)) { michael@0: passedUri = uri; michael@0: } michael@0: michael@0: final boolean isExternalURL = passedUri != null && michael@0: !AboutPages.isAboutHome(passedUri); michael@0: StartupAction startupAction; michael@0: if (isExternalURL) { michael@0: startupAction = StartupAction.URL; michael@0: } else { michael@0: startupAction = StartupAction.NORMAL; michael@0: } michael@0: michael@0: // Start migrating as early as possible, can do this in michael@0: // parallel with Gecko load. michael@0: checkMigrateProfile(); michael@0: michael@0: Uri data = intent.getData(); michael@0: if (data != null && "http".equals(data.getScheme())) { michael@0: startupAction = StartupAction.PREFETCH; michael@0: ThreadUtils.postToBackgroundThread(new PrefetchRunnable(data.toString())); michael@0: } michael@0: michael@0: Tabs.registerOnTabsChangedListener(this); michael@0: michael@0: initializeChrome(); michael@0: michael@0: // If we are doing a restore, read the session data and send it to Gecko michael@0: if (!mIsRestoringActivity) { michael@0: String restoreMessage = null; michael@0: if (mShouldRestore) { michael@0: try { michael@0: // restoreSessionTabs() will create simple tab stubs with the michael@0: // URL and title for each page, but we also need to restore michael@0: // session history. restoreSessionTabs() will inject the IDs michael@0: // of the tab stubs into the JSON data (which holds the session michael@0: // history). This JSON data is then sent to Gecko so session michael@0: // history can be restored for each tab. michael@0: restoreMessage = restoreSessionTabs(isExternalURL); michael@0: } catch (SessionRestoreException e) { michael@0: // If restore failed, do a normal startup michael@0: Log.e(LOGTAG, "An error occurred during restore", e); michael@0: mShouldRestore = false; michael@0: } michael@0: } michael@0: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Restore", restoreMessage)); michael@0: } michael@0: michael@0: // External URLs should always be loaded regardless of whether Gecko is michael@0: // already running. michael@0: if (isExternalURL) { michael@0: loadStartupTab(passedUri); michael@0: } else if (!mIsRestoringActivity) { michael@0: loadStartupTab(null); michael@0: } michael@0: michael@0: // We now have tab stubs from the last session. Any future tabs should michael@0: // be animated. michael@0: Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); michael@0: michael@0: // If we're not restoring, move the session file so it can be read for michael@0: // the last tabs section. michael@0: if (!mShouldRestore) { michael@0: getProfile().moveSessionFile(); michael@0: } michael@0: michael@0: Telemetry.HistogramAdd("FENNEC_STARTUP_GECKOAPP_ACTION", startupAction.ordinal()); michael@0: michael@0: if (!mIsRestoringActivity) { michael@0: GeckoThread.setArgs(intent.getStringExtra("args")); michael@0: GeckoThread.setAction(intent.getAction()); michael@0: GeckoThread.setUri(passedUri); michael@0: } michael@0: if (!ACTION_DEBUG.equals(action) && michael@0: GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.Launched)) { michael@0: GeckoThread.createAndStart(); michael@0: } else if (ACTION_DEBUG.equals(action) && michael@0: GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.WaitForDebugger)) { michael@0: ThreadUtils.getUiHandler().postDelayed(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: GeckoThread.setLaunchState(GeckoThread.LaunchState.Launching); michael@0: GeckoThread.createAndStart(); michael@0: } michael@0: }, 1000 * 5 /* 5 seconds */); michael@0: } michael@0: michael@0: // Check if launched from data reporting notification. michael@0: if (ACTION_LAUNCH_SETTINGS.equals(action)) { michael@0: Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class); michael@0: // Copy extras. michael@0: settingsIntent.putExtras(intent); michael@0: startActivity(settingsIntent); michael@0: } michael@0: michael@0: //app state callbacks michael@0: mAppStateListeners = new LinkedList(); michael@0: michael@0: //register for events michael@0: registerEventListener("log"); michael@0: registerEventListener("Reader:ListStatusRequest"); michael@0: registerEventListener("Reader:Added"); michael@0: registerEventListener("Reader:Removed"); michael@0: registerEventListener("Reader:Share"); michael@0: registerEventListener("Reader:FaviconRequest"); michael@0: registerEventListener("onCameraCapture"); michael@0: registerEventListener("Gecko:Ready"); michael@0: registerEventListener("Gecko:DelayedStartup"); michael@0: registerEventListener("Toast:Show"); michael@0: registerEventListener("DOMFullScreen:Start"); michael@0: registerEventListener("DOMFullScreen:Stop"); michael@0: registerEventListener("ToggleChrome:Hide"); michael@0: registerEventListener("ToggleChrome:Show"); michael@0: registerEventListener("ToggleChrome:Focus"); michael@0: registerEventListener("Permissions:Data"); michael@0: registerEventListener("Session:StatePurged"); michael@0: registerEventListener("Bookmark:Insert"); michael@0: registerEventListener("Accessibility:Event"); michael@0: registerEventListener("Accessibility:Ready"); michael@0: registerEventListener("Shortcut:Remove"); michael@0: registerEventListener("Share:Text"); michael@0: registerEventListener("Image:SetAs"); michael@0: registerEventListener("Sanitize:ClearHistory"); michael@0: registerEventListener("Update:Check"); michael@0: registerEventListener("Update:Download"); michael@0: registerEventListener("Update:Install"); michael@0: registerEventListener("PrivateBrowsing:Data"); michael@0: registerEventListener("Contact:Add"); michael@0: registerEventListener("Intent:Open"); michael@0: registerEventListener("Intent:OpenForResult"); michael@0: registerEventListener("Intent:GetHandlers"); michael@0: registerEventListener("Locale:Set"); michael@0: registerEventListener("NativeApp:IsDebuggable"); michael@0: registerEventListener("SystemUI:Visibility"); michael@0: michael@0: if (mWebappEventListener == null) { michael@0: mWebappEventListener = new EventListener(); michael@0: mWebappEventListener.registerEvents(); michael@0: } michael@0: michael@0: if (SmsManager.getInstance() != null) { michael@0: SmsManager.getInstance().start(); michael@0: } michael@0: michael@0: mContactService = new ContactService(GeckoAppShell.getEventDispatcher(), this); michael@0: michael@0: mPromptService = new PromptService(this); michael@0: michael@0: mTextSelection = new TextSelection((TextSelectionHandle) findViewById(R.id.start_handle), michael@0: (TextSelectionHandle) findViewById(R.id.middle_handle), michael@0: (TextSelectionHandle) findViewById(R.id.end_handle), michael@0: GeckoAppShell.getEventDispatcher(), michael@0: this); michael@0: michael@0: PrefsHelper.getPref("app.update.autodownload", new PrefsHelper.PrefHandlerBase() { michael@0: @Override public void prefValue(String pref, String value) { michael@0: UpdateServiceHelper.registerForUpdates(GeckoApp.this, value); michael@0: } michael@0: }); michael@0: michael@0: PrefsHelper.getPref("app.geo.reportdata", new PrefsHelper.PrefHandlerBase() { michael@0: @Override public void prefValue(String pref, int value) { michael@0: if (value == 1) michael@0: mShouldReportGeoData = true; michael@0: else michael@0: mShouldReportGeoData = false; michael@0: } michael@0: }); michael@0: michael@0: // Trigger the completion of the telemetry timer that wraps activity startup, michael@0: // then grab the duration to give to FHR. michael@0: mJavaUiStartupTimer.stop(); michael@0: final long javaDuration = mJavaUiStartupTimer.getElapsed(); michael@0: michael@0: ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: final HealthRecorder rec = mHealthRecorder; michael@0: if (rec != null) { michael@0: rec.recordJavaStartupTime(javaDuration); michael@0: } michael@0: michael@0: // Record our launch time for the announcements service michael@0: // to use in assessing inactivity. michael@0: final Context context = GeckoApp.this; michael@0: AnnouncementsBroadcastService.recordLastLaunch(context); michael@0: michael@0: // Kick off our background services. We do this by invoking the broadcast michael@0: // receiver, which uses the system alarm infrastructure to perform tasks at michael@0: // intervals. michael@0: GeckoPreferences.broadcastAnnouncementsPref(context); michael@0: GeckoPreferences.broadcastHealthReportUploadPref(context); michael@0: if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.Launched)) { michael@0: return; michael@0: } michael@0: } michael@0: }, 50); michael@0: michael@0: if (mIsRestoringActivity) { michael@0: GeckoThread.setLaunchState(GeckoThread.LaunchState.GeckoRunning); michael@0: Tab selectedTab = Tabs.getInstance().getSelectedTab(); michael@0: if (selectedTab != null) michael@0: Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED); michael@0: geckoConnected(); michael@0: GeckoAppShell.setLayerClient(mLayerView.getLayerClientObject()); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Viewport:Flush", null)); michael@0: } michael@0: michael@0: if (ACTION_ALERT_CALLBACK.equals(action)) { michael@0: processAlertCallback(intent); michael@0: } michael@0: } michael@0: michael@0: private String restoreSessionTabs(final boolean isExternalURL) throws SessionRestoreException { michael@0: try { michael@0: String sessionString = getProfile().readSessionFile(false); michael@0: if (sessionString == null) { michael@0: throw new SessionRestoreException("Could not read from session file"); michael@0: } michael@0: michael@0: // If we are doing an OOM restore, parse the session data and michael@0: // stub the restored tabs immediately. This allows the UI to be michael@0: // updated before Gecko has restored. michael@0: if (mShouldRestore) { michael@0: final JSONArray tabs = new JSONArray(); michael@0: SessionParser parser = new SessionParser() { michael@0: @Override michael@0: public void onTabRead(SessionTab sessionTab) { michael@0: JSONObject tabObject = sessionTab.getTabObject(); michael@0: michael@0: int flags = Tabs.LOADURL_NEW_TAB; michael@0: flags |= ((isExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0); michael@0: flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0); michael@0: flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0); michael@0: michael@0: Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags); michael@0: tab.updateTitle(sessionTab.getTitle()); michael@0: michael@0: try { michael@0: tabObject.put("tabId", tab.getId()); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "JSON error", e); michael@0: } michael@0: tabs.put(tabObject); michael@0: } michael@0: }; michael@0: michael@0: if (mPrivateBrowsingSession == null) { michael@0: parser.parse(sessionString); michael@0: } else { michael@0: parser.parse(sessionString, mPrivateBrowsingSession); michael@0: } michael@0: michael@0: if (tabs.length() > 0) { michael@0: sessionString = new JSONObject().put("windows", new JSONArray().put(new JSONObject().put("tabs", tabs))).toString(); michael@0: } else { michael@0: throw new SessionRestoreException("No tabs could be read from session file"); michael@0: } michael@0: } michael@0: michael@0: JSONObject restoreData = new JSONObject(); michael@0: restoreData.put("sessionString", sessionString); michael@0: return restoreData.toString(); michael@0: michael@0: } catch (JSONException e) { michael@0: throw new SessionRestoreException(e); michael@0: } michael@0: } michael@0: michael@0: public GeckoProfile getProfile() { michael@0: // fall back to default profile if we didn't load a specific one michael@0: if (mProfile == null) { michael@0: mProfile = GeckoProfile.get(this); michael@0: } michael@0: return mProfile; michael@0: } michael@0: michael@0: /** michael@0: * Determine whether the session should be restored. michael@0: * michael@0: * @param savedInstanceState Saved instance state given to the activity michael@0: * @return Whether to restore michael@0: */ michael@0: protected boolean getSessionRestoreState(Bundle savedInstanceState) { michael@0: final SharedPreferences prefs = getSharedPreferences(); michael@0: boolean shouldRestore = false; michael@0: michael@0: final int versionCode = getVersionCode(); michael@0: if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) { michael@0: // If the version has changed, the user has done an upgrade, so restore michael@0: // previous tabs. michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: prefs.edit() michael@0: .putInt(PREFS_VERSION_CODE, versionCode) michael@0: .commit(); michael@0: } michael@0: }); michael@0: michael@0: shouldRestore = true; michael@0: } else if (savedInstanceState != null || michael@0: getSessionRestorePreference().equals("always") || michael@0: getRestartFromIntent()) { michael@0: // We're coming back from a background kill by the OS, the user michael@0: // has chosen to always restore, or we restarted. michael@0: shouldRestore = true; michael@0: } else if (prefs.getBoolean(GeckoApp.PREFS_CRASHED, false)) { michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: prefs.edit().putBoolean(PREFS_CRASHED, false).commit(); michael@0: } michael@0: }); michael@0: shouldRestore = true; michael@0: } michael@0: michael@0: return shouldRestore; michael@0: } michael@0: michael@0: private String getSessionRestorePreference() { michael@0: return getSharedPreferences().getString(GeckoPreferences.PREFS_RESTORE_SESSION, "quit"); michael@0: } michael@0: michael@0: private boolean getRestartFromIntent() { michael@0: return getIntent().getBooleanExtra("didRestart", false); michael@0: } michael@0: michael@0: /** michael@0: * Enable Android StrictMode checks (for supported OS versions). michael@0: * http://developer.android.com/reference/android/os/StrictMode.html michael@0: */ michael@0: private void enableStrictMode() { michael@0: if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { michael@0: return; michael@0: } michael@0: michael@0: Log.d(LOGTAG, "Enabling Android StrictMode"); michael@0: michael@0: StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() michael@0: .detectAll() michael@0: .penaltyLog() michael@0: .build()); michael@0: michael@0: StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() michael@0: .detectAll() michael@0: .penaltyLog() michael@0: .build()); michael@0: } michael@0: michael@0: public void enableCameraView() { michael@0: // Start listening for orientation events michael@0: mCameraOrientationEventListener = new OrientationEventListener(this) { michael@0: @Override michael@0: public void onOrientationChanged(int orientation) { michael@0: if (mAppStateListeners != null) { michael@0: for (GeckoAppShell.AppStateListener listener: mAppStateListeners) { michael@0: listener.onOrientationChanged(); michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: mCameraOrientationEventListener.enable(); michael@0: michael@0: // Try to make it fully transparent. michael@0: if (mCameraView instanceof SurfaceView) { michael@0: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { michael@0: mCameraView.setAlpha(0.0f); michael@0: } michael@0: } else if (mCameraView instanceof TextureView) { michael@0: mCameraView.setAlpha(0.0f); michael@0: } michael@0: ViewGroup mCameraLayout = (ViewGroup) findViewById(R.id.camera_layout); michael@0: // Some phones (eg. nexus S) need at least a 8x16 preview size michael@0: mCameraLayout.addView(mCameraView, michael@0: new AbsoluteLayout.LayoutParams(8, 16, 0, 0)); michael@0: } michael@0: michael@0: public void disableCameraView() { michael@0: if (mCameraOrientationEventListener != null) { michael@0: mCameraOrientationEventListener.disable(); michael@0: mCameraOrientationEventListener = null; michael@0: } michael@0: ViewGroup mCameraLayout = (ViewGroup) findViewById(R.id.camera_layout); michael@0: mCameraLayout.removeView(mCameraView); michael@0: } michael@0: michael@0: public String getDefaultUAString() { michael@0: return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET : michael@0: AppConstants.USER_AGENT_FENNEC_MOBILE; michael@0: } michael@0: michael@0: public String getUAStringForHost(String host) { michael@0: // With our standard UA String, we get a 200 response code and michael@0: // client-side redirect from t.co. This bot-like UA gives us a michael@0: // 301 response code michael@0: if ("t.co".equals(host)) { michael@0: return AppConstants.USER_AGENT_BOT_LIKE; michael@0: } michael@0: return getDefaultUAString(); michael@0: } michael@0: michael@0: class PrefetchRunnable implements Runnable { michael@0: private String mPrefetchUrl; michael@0: michael@0: PrefetchRunnable(String prefetchUrl) { michael@0: mPrefetchUrl = prefetchUrl; michael@0: } michael@0: michael@0: @Override michael@0: public void run() { michael@0: HttpURLConnection connection = null; michael@0: try { michael@0: URL url = new URL(mPrefetchUrl); michael@0: // data url should have an http scheme michael@0: connection = (HttpURLConnection) url.openConnection(); michael@0: connection.setRequestProperty("User-Agent", getUAStringForHost(url.getHost())); michael@0: connection.setInstanceFollowRedirects(false); michael@0: connection.setRequestMethod("GET"); michael@0: connection.connect(); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Exception prefetching URL", e); michael@0: } finally { michael@0: if (connection != null) michael@0: connection.disconnect(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void processAlertCallback(Intent intent) { michael@0: String alertName = ""; michael@0: String alertCookie = ""; michael@0: Uri data = intent.getData(); michael@0: if (data != null) { michael@0: alertName = data.getQueryParameter("name"); michael@0: if (alertName == null) michael@0: alertName = ""; michael@0: alertCookie = data.getQueryParameter("cookie"); michael@0: if (alertCookie == null) michael@0: alertCookie = ""; michael@0: } michael@0: handleNotification(ACTION_ALERT_CALLBACK, alertName, alertCookie); michael@0: } michael@0: michael@0: @Override michael@0: protected void onNewIntent(Intent intent) { michael@0: if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoExiting)) { michael@0: // We're exiting and shouldn't try to do anything else. In the case michael@0: // where we are hung while exiting, we should force the process to exit. michael@0: GeckoAppShell.systemExit(); michael@0: return; michael@0: } michael@0: michael@0: // if we were previously OOM killed, we can end up here when launching michael@0: // from external shortcuts, so set this as the intent for initialization michael@0: if (!mInitialized) { michael@0: setIntent(intent); michael@0: return; michael@0: } michael@0: michael@0: final String action = intent.getAction(); michael@0: michael@0: if (ACTION_LOAD.equals(action)) { michael@0: String uri = intent.getDataString(); michael@0: Tabs.getInstance().loadUrl(uri); michael@0: } else if (Intent.ACTION_VIEW.equals(action)) { michael@0: String uri = intent.getDataString(); michael@0: Tabs.getInstance().loadUrl(uri, Tabs.LOADURL_NEW_TAB | michael@0: Tabs.LOADURL_USER_ENTERED | michael@0: Tabs.LOADURL_EXTERNAL); michael@0: } else if (action != null && action.startsWith(ACTION_WEBAPP_PREFIX)) { michael@0: // A lightweight mechanism for loading a web page as a webapp michael@0: // without installing the app natively nor registering it in the DOM michael@0: // application registry. michael@0: String uri = getURIFromIntent(intent); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createWebappLoadEvent(uri)); michael@0: } else if (ACTION_BOOKMARK.equals(action)) { michael@0: String uri = getURIFromIntent(intent); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBookmarkLoadEvent(uri)); michael@0: } else if (Intent.ACTION_SEARCH.equals(action)) { michael@0: String uri = getURIFromIntent(intent); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri)); michael@0: } else if (ACTION_ALERT_CALLBACK.equals(action)) { michael@0: processAlertCallback(intent); michael@0: } else if (ACTION_LAUNCH_SETTINGS.equals(action)) { michael@0: // Check if launched from data reporting notification. michael@0: Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class); michael@0: // Copy extras. michael@0: settingsIntent.putExtras(intent); michael@0: startActivity(settingsIntent); michael@0: } michael@0: } michael@0: michael@0: /* michael@0: * Handles getting a uri from and intent in a way that is backwards michael@0: * compatable with our previous implementations michael@0: */ michael@0: protected String getURIFromIntent(Intent intent) { michael@0: final String action = intent.getAction(); michael@0: if (ACTION_ALERT_CALLBACK.equals(action)) michael@0: return null; michael@0: michael@0: String uri = intent.getDataString(); michael@0: if (uri != null) michael@0: return uri; michael@0: michael@0: if ((action != null && action.startsWith(ACTION_WEBAPP_PREFIX)) || ACTION_BOOKMARK.equals(action)) { michael@0: uri = intent.getStringExtra("args"); michael@0: if (uri != null && uri.startsWith("--url=")) { michael@0: uri.replace("--url=", ""); michael@0: } michael@0: } michael@0: return uri; michael@0: } michael@0: michael@0: protected int getOrientation() { michael@0: return GeckoScreenOrientation.getInstance().getAndroidOrientation(); michael@0: } michael@0: michael@0: @Override michael@0: public void onResume() michael@0: { michael@0: // After an onPause, the activity is back in the foreground. michael@0: // Undo whatever we did in onPause. michael@0: super.onResume(); michael@0: michael@0: int newOrientation = getResources().getConfiguration().orientation; michael@0: if (GeckoScreenOrientation.getInstance().update(newOrientation)) { michael@0: refreshChrome(); michael@0: } michael@0: michael@0: // User may have enabled/disabled accessibility. michael@0: GeckoAccessibility.updateAccessibilitySettings(this); michael@0: michael@0: if (mAppStateListeners != null) { michael@0: for (GeckoAppShell.AppStateListener listener: mAppStateListeners) { michael@0: listener.onResume(); michael@0: } michael@0: } michael@0: michael@0: // We use two times: a pseudo-unique wall-clock time to identify the michael@0: // current session across power cycles, and the elapsed realtime to michael@0: // track the duration of the session. michael@0: final long now = System.currentTimeMillis(); michael@0: final long realTime = android.os.SystemClock.elapsedRealtime(); michael@0: michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: // Now construct the new session on HealthRecorder's behalf. We do this here michael@0: // so it can benefit from a single near-startup prefs commit. michael@0: SessionInformation currentSession = new SessionInformation(now, realTime); michael@0: michael@0: SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); michael@0: SharedPreferences.Editor editor = prefs.edit(); michael@0: editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); michael@0: currentSession.recordBegin(editor); michael@0: editor.commit(); michael@0: michael@0: final HealthRecorder rec = mHealthRecorder; michael@0: if (rec != null) { michael@0: rec.setCurrentSession(currentSession); michael@0: } else { michael@0: Log.w(LOGTAG, "Can't record session: rec is null."); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public void onWindowFocusChanged(boolean hasFocus) { michael@0: super.onWindowFocusChanged(hasFocus); michael@0: michael@0: if (!mInitialized && hasFocus) { michael@0: initialize(); michael@0: getWindow().setBackgroundDrawable(null); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onPause() michael@0: { michael@0: final HealthRecorder rec = mHealthRecorder; michael@0: final Context context = this; michael@0: michael@0: // In some way it's sad that Android will trigger StrictMode warnings michael@0: // here as the whole point is to save to disk while the activity is not michael@0: // interacting with the user. michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); michael@0: SharedPreferences.Editor editor = prefs.edit(); michael@0: editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true); michael@0: if (rec != null) { michael@0: rec.recordSessionEnd("P", editor); michael@0: } michael@0: michael@0: // If we haven't done it before, cleanup any old files in our old temp dir michael@0: if (prefs.getBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, true)) { michael@0: File tempDir = GeckoLoader.getGREDir(GeckoApp.this); michael@0: FileUtils.delTree(tempDir, new FileUtils.NameAndAgeFilter(null, ONE_DAY_MS), false); michael@0: michael@0: editor.putBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, false); michael@0: } michael@0: michael@0: editor.commit(); michael@0: michael@0: // In theory, the first browser session will not run long enough that we need to michael@0: // prune during it and we'd rather run it when the browser is inactive so we wait michael@0: // until here to register the prune service. michael@0: GeckoPreferences.broadcastHealthReportPrune(context); michael@0: } michael@0: }); michael@0: michael@0: if (mAppStateListeners != null) { michael@0: for(GeckoAppShell.AppStateListener listener: mAppStateListeners) { michael@0: listener.onPause(); michael@0: } michael@0: } michael@0: michael@0: super.onPause(); michael@0: } michael@0: michael@0: @Override michael@0: public void onRestart() michael@0: { michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); michael@0: SharedPreferences.Editor editor = prefs.edit(); michael@0: editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); michael@0: editor.commit(); michael@0: } michael@0: }); michael@0: michael@0: super.onRestart(); michael@0: } michael@0: michael@0: @Override michael@0: public void onDestroy() michael@0: { michael@0: unregisterEventListener("log"); michael@0: unregisterEventListener("Reader:ListStatusRequest"); michael@0: unregisterEventListener("Reader:Added"); michael@0: unregisterEventListener("Reader:Removed"); michael@0: unregisterEventListener("Reader:Share"); michael@0: unregisterEventListener("Reader:FaviconRequest"); michael@0: unregisterEventListener("onCameraCapture"); michael@0: unregisterEventListener("Gecko:Ready"); michael@0: unregisterEventListener("Gecko:DelayedStartup"); michael@0: unregisterEventListener("Toast:Show"); michael@0: unregisterEventListener("DOMFullScreen:Start"); michael@0: unregisterEventListener("DOMFullScreen:Stop"); michael@0: unregisterEventListener("ToggleChrome:Hide"); michael@0: unregisterEventListener("ToggleChrome:Show"); michael@0: unregisterEventListener("ToggleChrome:Focus"); michael@0: unregisterEventListener("Permissions:Data"); michael@0: unregisterEventListener("Session:StatePurged"); michael@0: unregisterEventListener("Bookmark:Insert"); michael@0: unregisterEventListener("Accessibility:Event"); michael@0: unregisterEventListener("Accessibility:Ready"); michael@0: unregisterEventListener("Shortcut:Remove"); michael@0: unregisterEventListener("Share:Text"); michael@0: unregisterEventListener("Image:SetAs"); michael@0: unregisterEventListener("Sanitize:ClearHistory"); michael@0: unregisterEventListener("Update:Check"); michael@0: unregisterEventListener("Update:Download"); michael@0: unregisterEventListener("Update:Install"); michael@0: unregisterEventListener("PrivateBrowsing:Data"); michael@0: unregisterEventListener("Contact:Add"); michael@0: unregisterEventListener("Intent:Open"); michael@0: unregisterEventListener("Intent:GetHandlers"); michael@0: unregisterEventListener("Locale:Set"); michael@0: unregisterEventListener("NativeApp:IsDebuggable"); michael@0: unregisterEventListener("SystemUI:Visibility"); michael@0: michael@0: if (mWebappEventListener != null) { michael@0: mWebappEventListener.unregisterEvents(); michael@0: mWebappEventListener = null; michael@0: } michael@0: michael@0: deleteTempFiles(); michael@0: michael@0: if (mLayerView != null) michael@0: mLayerView.destroy(); michael@0: if (mDoorHangerPopup != null) michael@0: mDoorHangerPopup.destroy(); michael@0: if (mFormAssistPopup != null) michael@0: mFormAssistPopup.destroy(); michael@0: if (mContactService != null) michael@0: mContactService.destroy(); michael@0: if (mPromptService != null) michael@0: mPromptService.destroy(); michael@0: if (mTextSelection != null) michael@0: mTextSelection.destroy(); michael@0: NotificationHelper.destroy(); michael@0: michael@0: if (SmsManager.getInstance() != null) { michael@0: SmsManager.getInstance().stop(); michael@0: if (isFinishing()) michael@0: SmsManager.getInstance().shutdown(); michael@0: } michael@0: michael@0: final HealthRecorder rec = mHealthRecorder; michael@0: mHealthRecorder = null; michael@0: if (rec != null && rec.isEnabled()) { michael@0: // Closing a BrowserHealthRecorder could incur a write. michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: rec.close(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: Favicons.close(); michael@0: michael@0: super.onDestroy(); michael@0: michael@0: Tabs.unregisterOnTabsChangedListener(this); michael@0: } michael@0: michael@0: protected void registerEventListener(String event) { michael@0: GeckoAppShell.getEventDispatcher().registerEventListener(event, this); michael@0: } michael@0: michael@0: protected void unregisterEventListener(String event) { michael@0: GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); michael@0: } michael@0: michael@0: // Get a temporary directory, may return null michael@0: public static File getTempDirectory() { michael@0: File dir = GeckoApplication.get().getExternalFilesDir("temp"); michael@0: return dir; michael@0: } michael@0: michael@0: // Delete any files in our temporary directory michael@0: public static void deleteTempFiles() { michael@0: File dir = getTempDirectory(); michael@0: if (dir == null) michael@0: return; michael@0: File[] files = dir.listFiles(); michael@0: if (files == null) michael@0: return; michael@0: for (File file : files) { michael@0: file.delete(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onConfigurationChanged(Configuration newConfig) { michael@0: Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale); michael@0: BrowserLocaleManager.getInstance().correctLocale(this, getResources(), newConfig); michael@0: michael@0: // onConfigurationChanged is not called for 180 degree orientation changes, michael@0: // we will miss such rotations and the screen orientation will not be michael@0: // updated. michael@0: if (GeckoScreenOrientation.getInstance().update(newConfig.orientation)) { michael@0: if (mFormAssistPopup != null) michael@0: mFormAssistPopup.hide(); michael@0: refreshChrome(); michael@0: } michael@0: super.onConfigurationChanged(newConfig); michael@0: } michael@0: michael@0: public String getContentProcessName() { michael@0: return AppConstants.MOZ_CHILD_PROCESS_NAME; michael@0: } michael@0: michael@0: public void addEnvToIntent(Intent intent) { michael@0: Map envMap = System.getenv(); michael@0: Set> envSet = envMap.entrySet(); michael@0: Iterator> envIter = envSet.iterator(); michael@0: int c = 0; michael@0: while (envIter.hasNext()) { michael@0: Map.Entry entry = envIter.next(); michael@0: intent.putExtra("env" + c, entry.getKey() + "=" michael@0: + entry.getValue()); michael@0: c++; michael@0: } michael@0: } michael@0: michael@0: public void doRestart() { michael@0: doRestart(RESTARTER_ACTION, null); michael@0: } michael@0: michael@0: public void doRestart(String args) { michael@0: doRestart(RESTARTER_ACTION, args); michael@0: } michael@0: michael@0: public void doRestart(String action, String args) { michael@0: Log.d(LOGTAG, "doRestart(\"" + action + "\")"); michael@0: try { michael@0: Intent intent = new Intent(action); michael@0: intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, RESTARTER_CLASS); michael@0: /* TODO: addEnvToIntent(intent); */ michael@0: if (args != null) michael@0: intent.putExtra("args", args); michael@0: intent.putExtra("didRestart", true); michael@0: Log.d(LOGTAG, "Restart intent: " + intent.toString()); michael@0: GeckoAppShell.killAnyZombies(); michael@0: startActivity(intent); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Error effecting restart.", e); michael@0: } michael@0: michael@0: finish(); michael@0: // Give the restart process time to start before we die michael@0: GeckoAppShell.waitForAnotherGeckoProc(); michael@0: } michael@0: michael@0: public void handleNotification(String action, String alertName, String alertCookie) { michael@0: // If Gecko isn't running yet, we ignore the notification. Note that michael@0: // even if Gecko is running but it was restarted since the notification michael@0: // was created, the notification won't be handled (bug 849653). michael@0: if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { michael@0: GeckoAppShell.handleNotification(action, alertName, alertCookie); michael@0: } michael@0: } michael@0: michael@0: private void checkMigrateProfile() { michael@0: final File profileDir = getProfile().getDir(); michael@0: michael@0: if (profileDir != null) { michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Handler handler = new Handler(); michael@0: handler.postDelayed(new DeferredCleanupTask(), CLEANUP_DEFERRAL_SECONDS * 1000); michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: michael@0: private class DeferredCleanupTask implements Runnable { michael@0: // The cleanup-version setting is recorded to avoid repeating the same michael@0: // tasks on subsequent startups; CURRENT_CLEANUP_VERSION may be updated michael@0: // if we need to do additional cleanup for future Gecko versions. michael@0: michael@0: private static final String CLEANUP_VERSION = "cleanup-version"; michael@0: private static final int CURRENT_CLEANUP_VERSION = 1; michael@0: michael@0: @Override michael@0: public void run() { michael@0: long cleanupVersion = getSharedPreferences().getInt(CLEANUP_VERSION, 0); michael@0: michael@0: if (cleanupVersion < 1) { michael@0: // Reduce device storage footprint by removing .ttf files from michael@0: // the res/fonts directory: we no longer need to copy our michael@0: // bundled fonts out of the APK in order to use them. michael@0: // See https://bugzilla.mozilla.org/show_bug.cgi?id=878674. michael@0: File dir = new File("res/fonts"); michael@0: if (dir.exists() && dir.isDirectory()) { michael@0: for (File file : dir.listFiles()) { michael@0: if (file.isFile() && file.getName().endsWith(".ttf")) { michael@0: Log.i(LOGTAG, "deleting " + file.toString()); michael@0: file.delete(); michael@0: } michael@0: } michael@0: if (!dir.delete()) { michael@0: Log.w(LOGTAG, "unable to delete res/fonts directory (not empty?)"); michael@0: } else { michael@0: Log.i(LOGTAG, "res/fonts directory deleted"); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Additional cleanup needed for future versions would go here michael@0: michael@0: if (cleanupVersion != CURRENT_CLEANUP_VERSION) { michael@0: SharedPreferences.Editor editor = GeckoApp.this.getSharedPreferences().edit(); michael@0: editor.putInt(CLEANUP_VERSION, CURRENT_CLEANUP_VERSION); michael@0: editor.commit(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: public PromptService getPromptService() { michael@0: return mPromptService; michael@0: } michael@0: michael@0: @Override michael@0: public void onBackPressed() { michael@0: if (getSupportFragmentManager().getBackStackEntryCount() > 0) { michael@0: super.onBackPressed(); michael@0: return; michael@0: } michael@0: michael@0: if (autoHideTabs()) { michael@0: return; michael@0: } michael@0: michael@0: if (mDoorHangerPopup != null && mDoorHangerPopup.isShowing()) { michael@0: mDoorHangerPopup.dismiss(); michael@0: return; michael@0: } michael@0: michael@0: if (mFullScreenPluginView != null) { michael@0: GeckoAppShell.onFullScreenPluginHidden(mFullScreenPluginView); michael@0: removeFullScreenPluginView(mFullScreenPluginView); michael@0: return; michael@0: } michael@0: michael@0: if (mLayerView != null && mLayerView.isFullScreen()) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FullScreen:Exit", null)); michael@0: return; michael@0: } michael@0: michael@0: Tabs tabs = Tabs.getInstance(); michael@0: Tab tab = tabs.getSelectedTab(); michael@0: if (tab == null) { michael@0: moveTaskToBack(true); michael@0: return; michael@0: } michael@0: michael@0: if (tab.doBack()) michael@0: return; michael@0: michael@0: if (tab.isExternal()) { michael@0: moveTaskToBack(true); michael@0: tabs.closeTab(tab); michael@0: return; michael@0: } michael@0: michael@0: int parentId = tab.getParentId(); michael@0: Tab parent = tabs.getTab(parentId); michael@0: if (parent != null) { michael@0: // The back button should always return to the parent (not a sibling). michael@0: tabs.closeTab(tab, parent); michael@0: return; michael@0: } michael@0: michael@0: moveTaskToBack(true); michael@0: } michael@0: michael@0: @Override michael@0: protected void onActivityResult(int requestCode, int resultCode, Intent data) { michael@0: if (!ActivityHandlerHelper.handleActivityResult(requestCode, resultCode, data)) { michael@0: super.onActivityResult(requestCode, resultCode, data); michael@0: } michael@0: } michael@0: michael@0: public AbsoluteLayout getPluginContainer() { return mPluginContainer; } michael@0: michael@0: // Accelerometer. michael@0: @Override michael@0: public void onAccuracyChanged(Sensor sensor, int accuracy) { michael@0: } michael@0: michael@0: @Override michael@0: public void onSensorChanged(SensorEvent event) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createSensorEvent(event)); michael@0: } michael@0: michael@0: // Geolocation. michael@0: @Override michael@0: public void onLocationChanged(Location location) { michael@0: // No logging here: user-identifying information. michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createLocationEvent(location)); michael@0: if (mShouldReportGeoData) michael@0: collectAndReportLocInfo(location); michael@0: } michael@0: michael@0: public void setCurrentSignalStrenth(SignalStrength ss) { michael@0: if (ss.isGsm()) michael@0: mSignalStrenth = ss.getGsmSignalStrength(); michael@0: } michael@0: michael@0: private int getCellInfo(JSONArray cellInfo) { michael@0: TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); michael@0: if (tm == null) michael@0: return TelephonyManager.PHONE_TYPE_NONE; michael@0: List cells = tm.getNeighboringCellInfo(); michael@0: CellLocation cl = tm.getCellLocation(); michael@0: String mcc = "", mnc = ""; michael@0: if (cl instanceof GsmCellLocation) { michael@0: JSONObject obj = new JSONObject(); michael@0: GsmCellLocation gcl = (GsmCellLocation)cl; michael@0: try { michael@0: obj.put("lac", gcl.getLac()); michael@0: obj.put("cid", gcl.getCid()); michael@0: michael@0: int psc = (Build.VERSION.SDK_INT >= 9) ? gcl.getPsc() : -1; michael@0: obj.put("psc", psc); michael@0: michael@0: switch(tm.getNetworkType()) { michael@0: case TelephonyManager.NETWORK_TYPE_GPRS: michael@0: case TelephonyManager.NETWORK_TYPE_EDGE: michael@0: obj.put("radio", "gsm"); michael@0: break; michael@0: case TelephonyManager.NETWORK_TYPE_UMTS: michael@0: case TelephonyManager.NETWORK_TYPE_HSDPA: michael@0: case TelephonyManager.NETWORK_TYPE_HSUPA: michael@0: case TelephonyManager.NETWORK_TYPE_HSPA: michael@0: case TelephonyManager.NETWORK_TYPE_HSPAP: michael@0: obj.put("radio", "umts"); michael@0: break; michael@0: } michael@0: String mcc_mnc = tm.getNetworkOperator(); michael@0: if (mcc_mnc.length() > 3) { michael@0: mcc = mcc_mnc.substring(0, 3); michael@0: mnc = mcc_mnc.substring(3); michael@0: obj.put("mcc", mcc); michael@0: obj.put("mnc", mnc); michael@0: } michael@0: obj.put("asu", mSignalStrenth); michael@0: } catch(JSONException jsonex) {} michael@0: cellInfo.put(obj); michael@0: } michael@0: if (cells != null) { michael@0: for (NeighboringCellInfo nci : cells) { michael@0: try { michael@0: JSONObject obj = new JSONObject(); michael@0: obj.put("lac", nci.getLac()); michael@0: obj.put("cid", nci.getCid()); michael@0: obj.put("psc", nci.getPsc()); michael@0: obj.put("mcc", mcc); michael@0: obj.put("mnc", mnc); michael@0: michael@0: int dbm; michael@0: switch(nci.getNetworkType()) { michael@0: case TelephonyManager.NETWORK_TYPE_GPRS: michael@0: case TelephonyManager.NETWORK_TYPE_EDGE: michael@0: obj.put("radio", "gsm"); michael@0: break; michael@0: case TelephonyManager.NETWORK_TYPE_UMTS: michael@0: case TelephonyManager.NETWORK_TYPE_HSDPA: michael@0: case TelephonyManager.NETWORK_TYPE_HSUPA: michael@0: case TelephonyManager.NETWORK_TYPE_HSPA: michael@0: case TelephonyManager.NETWORK_TYPE_HSPAP: michael@0: obj.put("radio", "umts"); michael@0: break; michael@0: } michael@0: michael@0: obj.put("asu", nci.getRssi()); michael@0: cellInfo.put(obj); michael@0: } catch(JSONException jsonex) {} michael@0: } michael@0: } michael@0: return tm.getPhoneType(); michael@0: } michael@0: michael@0: private static boolean shouldLog(final ScanResult sr) { michael@0: return sr.SSID == null || !sr.SSID.endsWith("_nomap"); michael@0: } michael@0: michael@0: private void collectAndReportLocInfo(Location location) { michael@0: final JSONObject locInfo = new JSONObject(); michael@0: WifiManager wm = (WifiManager)getSystemService(Context.WIFI_SERVICE); michael@0: wm.startScan(); michael@0: try { michael@0: JSONArray cellInfo = new JSONArray(); michael@0: michael@0: String radioType = getRadioTypeName(getCellInfo(cellInfo)); michael@0: if (radioType != null) { michael@0: locInfo.put("radio", radioType); michael@0: } michael@0: michael@0: locInfo.put("lon", location.getLongitude()); michael@0: locInfo.put("lat", location.getLatitude()); michael@0: michael@0: // If we have an accuracy, round it up to the next meter. michael@0: if (location.hasAccuracy()) { michael@0: locInfo.put("accuracy", (int) Math.ceil(location.getAccuracy())); michael@0: } michael@0: michael@0: // If we have an altitude, round it to the nearest meter. michael@0: if (location.hasAltitude()) { michael@0: locInfo.put("altitude", Math.round(location.getAltitude())); michael@0: } michael@0: michael@0: // Reduce timestamp precision so as to expose less PII. michael@0: DateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.US); michael@0: locInfo.put("time", df.format(new Date(location.getTime()))); michael@0: locInfo.put("cell", cellInfo); michael@0: michael@0: JSONArray wifiInfo = new JSONArray(); michael@0: List aps = wm.getScanResults(); michael@0: if (aps != null) { michael@0: for (ScanResult ap : aps) { michael@0: if (!shouldLog(ap)) michael@0: continue; michael@0: michael@0: JSONObject obj = new JSONObject(); michael@0: obj.put("key", ap.BSSID); michael@0: obj.put("frequency", ap.frequency); michael@0: obj.put("signal", ap.level); michael@0: wifiInfo.put(obj); michael@0: } michael@0: } michael@0: locInfo.put("wifi", wifiInfo); michael@0: } catch (JSONException jsonex) { michael@0: Log.w(LOGTAG, "json exception", jsonex); michael@0: return; michael@0: } michael@0: michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: public void run() { michael@0: try { michael@0: URL url = new URL(LOCATION_URL); michael@0: HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); michael@0: try { michael@0: urlConnection.setDoOutput(true); michael@0: michael@0: // Workaround for a bug in Android HttpURLConnection. When the library michael@0: // reuses a stale connection, the connection may fail with an EOFException. michael@0: if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT <= 18) { michael@0: urlConnection.setRequestProperty("Connection", "Close"); michael@0: } michael@0: michael@0: JSONArray batch = new JSONArray(); michael@0: batch.put(locInfo); michael@0: JSONObject wrapper = new JSONObject(); michael@0: wrapper.put("items", batch); michael@0: byte[] bytes = wrapper.toString().getBytes(); michael@0: urlConnection.setFixedLengthStreamingMode(bytes.length); michael@0: OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream()); michael@0: out.write(bytes); michael@0: out.flush(); michael@0: } catch (JSONException jsonex) { michael@0: Log.e(LOGTAG, "error wrapping data as a batch", jsonex); michael@0: } catch (IOException ioex) { michael@0: Log.e(LOGTAG, "error submitting data", ioex); michael@0: } finally { michael@0: urlConnection.disconnect(); michael@0: } michael@0: } catch (IOException ioex) { michael@0: Log.e(LOGTAG, "error submitting data", ioex); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private static String getRadioTypeName(int phoneType) { michael@0: switch (phoneType) { michael@0: case TelephonyManager.PHONE_TYPE_CDMA: michael@0: return "cdma"; michael@0: michael@0: case TelephonyManager.PHONE_TYPE_GSM: michael@0: return "gsm"; michael@0: michael@0: case TelephonyManager.PHONE_TYPE_NONE: michael@0: case TelephonyManager.PHONE_TYPE_SIP: michael@0: // These devices have no radio. michael@0: return null; michael@0: michael@0: default: michael@0: Log.e(LOGTAG, "", new IllegalArgumentException("Unexpected PHONE_TYPE: " + phoneType)); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onProviderDisabled(String provider) michael@0: { michael@0: } michael@0: michael@0: @Override michael@0: public void onProviderEnabled(String provider) michael@0: { michael@0: } michael@0: michael@0: @Override michael@0: public void onStatusChanged(String provider, int status, Bundle extras) michael@0: { michael@0: } michael@0: michael@0: // Called when a Gecko Hal WakeLock is changed michael@0: public void notifyWakeLockChanged(String topic, String state) { michael@0: PowerManager.WakeLock wl = mWakeLocks.get(topic); michael@0: if (state.equals("locked-foreground") && wl == null) { michael@0: PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); michael@0: wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, topic); michael@0: wl.acquire(); michael@0: mWakeLocks.put(topic, wl); michael@0: } else if (!state.equals("locked-foreground") && wl != null) { michael@0: wl.release(); michael@0: mWakeLocks.remove(topic); michael@0: } michael@0: } michael@0: michael@0: public void notifyCheckUpdateResult(String result) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Update:CheckResult", result)); michael@0: } michael@0: michael@0: protected void geckoConnected() { michael@0: mLayerView.geckoConnected(); michael@0: mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER); michael@0: } michael@0: michael@0: public void setAccessibilityEnabled(boolean enabled) { michael@0: } michael@0: michael@0: public static class MainLayout extends RelativeLayout { michael@0: private TouchEventInterceptor mTouchEventInterceptor; michael@0: private MotionEventInterceptor mMotionEventInterceptor; michael@0: michael@0: public MainLayout(Context context, AttributeSet attrs) { michael@0: super(context, attrs); michael@0: } michael@0: michael@0: public void setTouchEventInterceptor(TouchEventInterceptor interceptor) { michael@0: mTouchEventInterceptor = interceptor; michael@0: } michael@0: michael@0: public void setMotionEventInterceptor(MotionEventInterceptor interceptor) { michael@0: mMotionEventInterceptor = interceptor; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onInterceptTouchEvent(MotionEvent event) { michael@0: if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) { michael@0: return true; michael@0: } michael@0: return super.onInterceptTouchEvent(event); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onTouchEvent(MotionEvent event) { michael@0: if (mTouchEventInterceptor != null && mTouchEventInterceptor.onTouch(this, event)) { michael@0: return true; michael@0: } michael@0: return super.onTouchEvent(event); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onGenericMotionEvent(MotionEvent event) { michael@0: if (mMotionEventInterceptor != null && mMotionEventInterceptor.onInterceptMotionEvent(this, event)) { michael@0: return true; michael@0: } michael@0: return super.onGenericMotionEvent(event); michael@0: } michael@0: michael@0: @Override michael@0: public void setDrawingCacheEnabled(boolean enabled) { michael@0: // Instead of setting drawing cache in the view itself, we simply michael@0: // enable drawing caching on its children. This is mainly used in michael@0: // animations (see PropertyAnimator) michael@0: super.setChildrenDrawnWithCacheEnabled(enabled); michael@0: } michael@0: } michael@0: michael@0: private class FullScreenHolder extends FrameLayout { michael@0: michael@0: public FullScreenHolder(Context ctx) { michael@0: super(ctx); michael@0: } michael@0: michael@0: @Override michael@0: public void addView(View view, int index) { michael@0: /** michael@0: * This normally gets called when Flash adds a separate SurfaceView michael@0: * for the video. It is unhappy if we have the LayerView underneath michael@0: * it for some reason so we need to hide that. Hiding the LayerView causes michael@0: * its surface to be destroyed, which causes a pause composition michael@0: * event to be sent to Gecko. We synchronously wait for that to be michael@0: * processed. Simultaneously, however, Flash is waiting on a mutex so michael@0: * the post() below is an attempt to avoid a deadlock. michael@0: */ michael@0: super.addView(view, index); michael@0: michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mLayerView.hideSurface(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * The methods below are simply copied from what Android WebKit does. michael@0: * It wasn't ever called in my testing, but might as well michael@0: * keep it in case it is for some reason. The methods michael@0: * all return true because we don't want any events michael@0: * leaking out from the fullscreen view. michael@0: */ michael@0: @Override michael@0: public boolean onKeyDown(int keyCode, KeyEvent event) { michael@0: if (event.isSystem()) { michael@0: return super.onKeyDown(keyCode, event); michael@0: } michael@0: mFullScreenPluginView.onKeyDown(keyCode, event); michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyUp(int keyCode, KeyEvent event) { michael@0: if (event.isSystem()) { michael@0: return super.onKeyUp(keyCode, event); michael@0: } michael@0: mFullScreenPluginView.onKeyUp(keyCode, event); michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onTouchEvent(MotionEvent event) { michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onTrackballEvent(MotionEvent event) { michael@0: mFullScreenPluginView.onTrackballEvent(event); michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: protected NotificationClient makeNotificationClient() { michael@0: // Don't use a notification service; we may be killed in the background michael@0: // during downloads. michael@0: return new AppNotificationClient(getApplicationContext()); michael@0: } michael@0: michael@0: private int getVersionCode() { michael@0: int versionCode = 0; michael@0: try { michael@0: versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode; michael@0: } catch (NameNotFoundException e) { michael@0: Log.wtf(LOGTAG, getPackageName() + " not found", e); michael@0: } michael@0: return versionCode; michael@0: } michael@0: michael@0: protected boolean getIsDebuggable() { michael@0: // Return false so Fennec doesn't appear to be debuggable. WebappImpl michael@0: // then overrides this and returns the value of android:debuggable for michael@0: // the webapp APK, so webapps get the behavior supported by this method michael@0: // (i.e. automatic configuration and enabling of the remote debugger). michael@0: return false; michael@0: michael@0: // If we ever want to expose this for Fennec, here's how we would do it: michael@0: // int flags = 0; michael@0: // try { michael@0: // flags = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.flags; michael@0: // } catch (NameNotFoundException e) { michael@0: // Log.wtf(LOGTAG, getPackageName() + " not found", e); michael@0: // } michael@0: // return (flags & android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0; michael@0: } michael@0: michael@0: // FHR reason code for a session end prior to a restart for a michael@0: // locale change. michael@0: private static final String SESSION_END_LOCALE_CHANGED = "L"; michael@0: michael@0: /** michael@0: * Use BrowserLocaleManager to change our persisted and current locales, michael@0: * and poke HealthRecorder to tell it of our changed state. michael@0: */ michael@0: private void setLocale(final String locale) { michael@0: if (locale == null) { michael@0: return; michael@0: } michael@0: final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale); michael@0: if (resultant == null) { michael@0: return; michael@0: } michael@0: michael@0: final boolean startNewSession = true; michael@0: final boolean shouldRestart = false; michael@0: michael@0: // If the HealthRecorder is not yet initialized (unlikely), the locale change won't michael@0: // trigger a session transition and subsequent events will be recorded in an environment michael@0: // with the wrong locale. michael@0: final HealthRecorder rec = mHealthRecorder; michael@0: if (rec != null) { michael@0: rec.onAppLocaleChanged(resultant); michael@0: rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED); michael@0: } michael@0: michael@0: if (!shouldRestart) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: GeckoApp.this.onLocaleReady(resultant); michael@0: } michael@0: }); michael@0: return; michael@0: } michael@0: michael@0: // Do this in the background so that the health recorder has its michael@0: // time to finish. michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: GeckoApp.this.doRestart(); michael@0: GeckoApp.this.finish(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void setSystemUiVisible(final boolean visible) { michael@0: if (Build.VERSION.SDK_INT < 14) { michael@0: return; michael@0: } michael@0: michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: if (visible) { michael@0: mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); michael@0: } else { michael@0: mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: protected HealthRecorder createHealthRecorder(final Context context, michael@0: final String profilePath, michael@0: final EventDispatcher dispatcher, michael@0: final String osLocale, michael@0: final String appLocale, michael@0: final SessionInformation previousSession) { michael@0: // GeckoApp does not need to record any health information - return a stub. michael@0: return new StubbedHealthRecorder(); michael@0: } michael@0: }