diff --git a/embedding/android/GeckoApp.java b/embedding/android/GeckoApp.java index 3c167358e156..1c6bf1377c53 100644 --- a/embedding/android/GeckoApp.java +++ b/embedding/android/GeckoApp.java @@ -40,6 +40,14 @@ package org.mozilla.gecko; +import org.mozilla.gecko.gfx.GeckoSoftwareLayerClient; +import org.mozilla.gecko.gfx.IntRect; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.gfx.PlaceholderLayerClient; +import org.mozilla.gecko.Tab.HistoryEntry; + import java.io.*; import java.util.*; import java.util.zip.*; @@ -64,6 +72,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.BitmapDrawable; import android.widget.*; import android.hardware.*; +import android.location.*; import android.util.*; import android.net.*; @@ -76,7 +85,7 @@ import android.content.SharedPreferences.*; import dalvik.system.*; abstract public class GeckoApp - extends Activity implements GeckoEventListener + extends Activity implements GeckoEventListener, SensorEventListener, LocationListener { private static final String LOG_NAME = "GeckoApp"; @@ -88,7 +97,6 @@ abstract public class GeckoApp private LinearLayout mMainLayout; private RelativeLayout mGeckoLayout; - public static GeckoSurfaceView surfaceView; public static SurfaceView cameraView; public static GeckoApp mAppContext; public static boolean mFullScreen = false; @@ -103,6 +111,10 @@ abstract public class GeckoApp private static boolean sIsGeckoReady = false; private IntentFilter mBatteryFilter; private BroadcastReceiver mBatteryReceiver; + private Geocoder mGeocoder; + private Address mLastGeoAddress; + private static LayerController mLayerController; + private static GeckoSoftwareLayerClient mSoftwareLayerClient; boolean mUserDefinedProfile = false; public interface OnTabsChangedListener { @@ -124,8 +136,8 @@ abstract public class GeckoApp static Vector sExtraMenuItems = new Vector(); - enum LaunchState {Launching, WaitButton, - Launched, GeckoRunning, GeckoExiting}; + public enum LaunchState {Launching, WaitButton, + Launched, GeckoRunning, GeckoExiting}; private static LaunchState sLaunchState = LaunchState.Launching; private static boolean sTryCatchAttached = false; @@ -133,7 +145,7 @@ abstract public class GeckoApp private static final int AWESOMEBAR_REQUEST = 2; private static final int CAMERA_CAPTURE_REQUEST = 3; - static boolean checkLaunchState(LaunchState checkState) { + public static boolean checkLaunchState(LaunchState checkState) { synchronized(sLaunchState) { return sLaunchState == checkState; } @@ -539,28 +551,43 @@ abstract public class GeckoApp } } + public String getStartupBitmapFilePath() { + File file = new File(Environment.getExternalStorageDirectory(), + "lastScreen.png"); + return file.toString(); + } + private void rememberLastScreen(boolean sync) { if (mUserDefinedProfile) return; - if (surfaceView == null) - return; Tab tab = Tabs.getInstance().getSelectedTab(); if (tab == null) return; - Tab.HistoryEntry he = tab.getLastHistoryEntry(); - if (he != null) { - SharedPreferences prefs = getSharedPreferences("GeckoApp", MODE_PRIVATE); - Editor editor = prefs.edit(); - - editor.putString("last-uri", he.mUri); - editor.putString("last-title", he.mTitle); + HistoryEntry lastHistoryEntry = tab.getLastHistoryEntry(); + if (lastHistoryEntry == null) + return; - Log.i(LOG_NAME, "Saving:: " + he.mUri + " " + he.mTitle); - editor.commit(); - surfaceView.saveLast(sync); - } + SharedPreferences prefs = getSharedPreferences("GeckoApp", 0); + Editor editor = prefs.edit(); + + String uri = lastHistoryEntry.mUri; + String title = lastHistoryEntry.mTitle; + + editor.putString("last-uri", uri); + editor.putString("last-title", title); + + Log.i(LOG_NAME, "Saving:: " + uri + " " + title); + editor.commit(); + + GeckoEvent event = new GeckoEvent(); + event.mType = GeckoEvent.SAVE_STATE; + event.mCharacters = getStartupBitmapFilePath(); + if (sync) + GeckoAppShell.sendEventToGeckoSync(event); + else + GeckoAppShell.sendEventToGecko(event); } private void loadFavicon(final Tab tab) { @@ -714,6 +741,7 @@ abstract public class GeckoApp final int tabId = message.getInt("tabID"); final String uri = message.getString("uri"); final String title = message.getString("title"); + final JSONObject jsonPageSize = message.getJSONObject("pageSize"); final CharSequence titleText = title; handleContentLoaded(tabId, uri, title); Log.i(LOG_NAME, "URI - " + uri + ", title - " + title); @@ -787,6 +815,12 @@ abstract public class GeckoApp sMenu.findItem(R.id.preferences).setEnabled(true); } }); + } else if (event.equals("PanZoom:Ack")) { + final IntRect rect = new IntRect(message.getJSONObject("rect")); + mSoftwareLayerClient.jsPanZoomCompleted(rect); + } else if (event.equals("PanZoom:Resize")) { + final IntSize size = new IntSize(message.getJSONObject("size")); + mSoftwareLayerClient.setPageSize(size); } else if (event.equals("ToggleChrome:Hide")) { mMainHandler.post(new Runnable() { public void run() { @@ -920,7 +954,6 @@ abstract public class GeckoApp public void run() { if (Tabs.getInstance().isSelectedTab(tab)) mBrowserToolbar.setProgressVisibility(false); - surfaceView.hideStartupBitmap(); onTabsChanged(); } }); @@ -1085,17 +1118,33 @@ abstract public class GeckoApp cameraView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } - if (surfaceView == null) { - surfaceView = new GeckoSurfaceView(this); - mGeckoLayout.addView(surfaceView); - } else if (mGeckoLayout.getChildCount() == 0) { - //surfaceView still holds to the old one during rotation. re-add it to new activity - ((ViewGroup) surfaceView.getParent()).removeAllViews(); - mGeckoLayout.addView(surfaceView); + if (mLayerController == null) { + /* + * Create a layer client so that Gecko will have a buffer to draw into, but don't hook + * it up to the layer controller yet. + */ + mSoftwareLayerClient = new GeckoSoftwareLayerClient(this); + + /* + * Hook a placeholder layer client up to the layer controller so that the user can pan + * and zoom a cached screenshot of the previous page. This call will return null if + * there is no cached screenshot; in that case, we have no choice but to display a + * checkerboard. + * + * TODO: Fall back to a built-in screenshot of the Fennec Start page for a nice first- + * run experience, perhaps? + */ + PlaceholderLayerClient placeholderClient = mUserDefinedProfile ? + null : PlaceholderLayerClient.createInstance(this); + if (placeholderClient != null) { + mLayerController = new LayerController(this, placeholderClient); + placeholderClient.init(); + } else { + mLayerController = new LayerController(this, null); + } + + mGeckoLayout.addView(mLayerController.getView()); } - - if (!mUserDefinedProfile) - surfaceView.loadStartupBitmap(); Log.w(LOGTAG, "zerdatime " + new Date().getTime() + " - UI almost up"); @@ -1140,6 +1189,8 @@ abstract public class GeckoApp GeckoAppShell.registerGeckoEventListener("Preferences:Data", GeckoApp.mAppContext); GeckoAppShell.registerGeckoEventListener("Gecko:Ready", GeckoApp.mAppContext); GeckoAppShell.registerGeckoEventListener("Toast:Show", GeckoApp.mAppContext); + GeckoAppShell.registerGeckoEventListener("PanZoom:Ack", GeckoApp.mAppContext); + GeckoAppShell.registerGeckoEventListener("PanZoom:Resize", GeckoApp.mAppContext); GeckoAppShell.registerGeckoEventListener("ToggleChrome:Hide", GeckoApp.mAppContext); GeckoAppShell.registerGeckoEventListener("ToggleChrome:Show", GeckoApp.mAppContext); @@ -1388,7 +1439,7 @@ abstract public class GeckoApp Intent intent = new Intent(action); intent.setClassName(getPackageName(), getPackageName() + ".Restarter"); - addEnvToIntent(intent); + /* TODO: addEnvToIntent(intent); */ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); Log.i(LOG_NAME, intent.toString()); @@ -1660,4 +1711,89 @@ abstract public class GeckoApp GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Load", url)); } } + + public GeckoSoftwareLayerClient getSoftwareLayerClient() { return mSoftwareLayerClient; } + public LayerController getLayerController() { return mLayerController; } + + // accelerometer + public void onAccuracyChanged(Sensor sensor, int accuracy) + { + } + + public void onSensorChanged(SensorEvent event) + { + Log.w(LOGTAG, "onSensorChanged "+event); + GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); + } + + private class GeocoderTask extends AsyncTask { + protected Void doInBackground(Location... location) { + try { + List
addresses = mGeocoder.getFromLocation(location[0].getLatitude(), + location[0].getLongitude(), 1); + // grab the first address. in the future, + // may want to expose multiple, or filter + // for best. + mLastGeoAddress = addresses.get(0); + GeckoAppShell.sendEventToGecko(new GeckoEvent(location[0], mLastGeoAddress)); + } catch (Exception e) { + Log.w(LOGTAG, "GeocoderTask "+e); + } + return null; + } + } + + // geolocation + public void onLocationChanged(Location location) + { + Log.w(LOGTAG, "onLocationChanged "+location); + if (mGeocoder == null) + mGeocoder = new Geocoder(mLayerController.getView().getContext(), Locale.getDefault()); + + if (mLastGeoAddress == null) { + new GeocoderTask().execute(location); + } + else { + float[] results = new float[1]; + Location.distanceBetween(location.getLatitude(), + location.getLongitude(), + mLastGeoAddress.getLatitude(), + mLastGeoAddress.getLongitude(), + results); + // pfm value. don't want to slam the + // geocoder with very similar values, so + // only call after about 100m + if (results[0] > 100) + new GeocoderTask().execute(location); + } + + GeckoAppShell.sendEventToGecko(new GeckoEvent(location, mLastGeoAddress)); + } + + public void onProviderDisabled(String provider) + { + } + + public void onProviderEnabled(String provider) + { + } + + public void onStatusChanged(String provider, int status, Bundle extras) + { + } + + public void connectGeckoLayerClient() { + new Timer("Gecko Wait").schedule(new TimerTask() { + public void run() { + GeckoApp.mAppContext.runOnUiThread(new Runnable() { + public void run() { + LayerController layerController = getLayerController(); + layerController.setLayerClient(mSoftwareLayerClient); + mSoftwareLayerClient.init(); /* Attaches the new root layer. */ + GeckoAppShell.scheduleRedraw(); + } + }); + } + }, 3000); + } } diff --git a/embedding/android/GeckoAppShell.java b/embedding/android/GeckoAppShell.java index 8a6a3e327824..650813f65bf9 100644 --- a/embedding/android/GeckoAppShell.java +++ b/embedding/android/GeckoAppShell.java @@ -38,6 +38,12 @@ package org.mozilla.gecko; +import org.mozilla.gecko.gfx.FloatPoint; +import org.mozilla.gecko.gfx.GeckoSoftwareLayerClient; +import org.mozilla.gecko.gfx.IntPoint; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.LayerView; + import java.io.*; import java.lang.reflect.*; import java.nio.*; @@ -90,15 +96,11 @@ public class GeckoAppShell static private boolean gRestartScheduled = false; static private PromptService gPromptService = null; - static private final Timer mIMETimer = new Timer(); + static private GeckoInputConnection mInputConnection = null; + static private final HashMap mAlertNotifications = new HashMap(); - static private final int NOTIFY_IME_RESETINPUTSTATE = 0; - static private final int NOTIFY_IME_SETOPENSTATE = 1; - static private final int NOTIFY_IME_CANCELCOMPOSITION = 2; - static private final int NOTIFY_IME_FOCUSCHANGE = 3; - /* Keep in sync with constants found here: http://mxr.mozilla.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl */ @@ -118,7 +120,8 @@ public class GeckoAppShell public static native void nativeRun(String args); // helper methods - public static native void setSurfaceView(GeckoSurfaceView sv); + // public static native void setSurfaceView(GeckoSurfaceView sv); + public static native void setSoftwareLayerClient(GeckoSoftwareLayerClient client); public static native void putenv(String map); public static native void onResume(); public static native void onLowMemory(); @@ -414,8 +417,8 @@ public class GeckoAppShell // run gecko -- it will spawn its own thread GeckoAppShell.nativeInit(); - // Tell Gecko where the target surface view is for rendering - GeckoAppShell.setSurfaceView(GeckoApp.surfaceView); + // Tell Gecko where the target byte buffer is for rendering + GeckoAppShell.setSoftwareLayerClient(GeckoApp.mAppContext.getSoftwareLayerClient()); // First argument is the .apk path String combinedArgs = apkPath + " -greomni " + apkPath; @@ -424,10 +427,53 @@ public class GeckoAppShell if (url != null) combinedArgs += " -remote " + url; + /* TODO: Is this complexity necessary? */ + new Timer("Gecko Setup").schedule(new TimerTask() { + public void run() { + GeckoApp.mAppContext.runOnUiThread(new Runnable() { + public void run() { + geckoLoaded(); + } + }); + } + }, 0); + // and go GeckoAppShell.nativeRun(combinedArgs); } + // Called on the UI thread after Gecko loads. + private static void geckoLoaded() { + GeckoApp.mAppContext.connectGeckoLayerClient(); + + final LayerController layerController = GeckoApp.mAppContext.getLayerController(); + LayerView v = layerController.getView(); + mInputConnection = new GeckoInputConnection(v); + v.setInputConnectionHandler(mInputConnection); + + layerController.setOnTouchListener(new View.OnTouchListener() { + public boolean onTouch(View view, MotionEvent event) { + float origX = event.getX(); + float origY = event.getY(); + /* Transform the point to the layer offset. */ + FloatPoint eventPoint = new FloatPoint(origX, origY); + FloatPoint geckoPoint = layerController.convertViewPointToLayerPoint(eventPoint); + event.setLocation((int)Math.round(geckoPoint.x), (int)Math.round(geckoPoint.y)); + + GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); + + /* Restore the view coordinates in case the caller further processes this event */ + event.setLocation(origX, origY); + return true; + } + }); + + GeckoEvent event = new GeckoEvent(GeckoEvent.SIZE_CHANGED, + LayerController.TILE_WIDTH, LayerController.TILE_HEIGHT, + LayerController.TILE_WIDTH, LayerController.TILE_HEIGHT); + GeckoAppShell.sendEventToGecko(event); + } + private static GeckoEvent mLastDrawEvent; private static void sendPendingEventsToGecko() { @@ -440,7 +486,7 @@ public class GeckoAppShell } public static void sendEventToGecko(GeckoEvent e) { - if (GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning)) { + if (GeckoApp.mAppContext.checkLaunchState(GeckoApp.LaunchState.GeckoRunning)) { notifyGeckoOfEvent(e); } else { gPendingEvents.addLast(e); @@ -460,145 +506,24 @@ public class GeckoAppShell */ public static void scheduleRedraw() { // Redraw everything - scheduleRedraw(0, -1, -1, -1, -1); + Rect rect = new Rect(0, 0, LayerController.TILE_WIDTH, LayerController.TILE_HEIGHT); + GeckoEvent event = new GeckoEvent(GeckoEvent.DRAW, rect); + event.mNativeWindow = 0; + sendEventToGecko(event); } - public static void scheduleRedraw(int nativeWindow, int x, int y, int w, int h) { - GeckoEvent e; - - if (x == -1) { - e = new GeckoEvent(GeckoEvent.DRAW, null); - } else { - e = new GeckoEvent(GeckoEvent.DRAW, new Rect(x, y, w, h)); - } - - e.mNativeWindow = nativeWindow; - - sendEventToGecko(e); - } - - /* Delay updating IME states (see bug 573800) */ - private static final class IMEStateUpdater extends TimerTask - { - static private IMEStateUpdater instance; - private boolean mEnable, mReset; - - static private IMEStateUpdater getInstance() { - if (instance == null) { - instance = new IMEStateUpdater(); - mIMETimer.schedule(instance, 200); - } - return instance; - } - - static public synchronized void enableIME() { - getInstance().mEnable = true; - } - - static public synchronized void resetIME() { - getInstance().mReset = true; - } - - public void run() { - synchronized(IMEStateUpdater.class) { - instance = null; - } - - InputMethodManager imm = (InputMethodManager) - GeckoApp.surfaceView.getContext().getSystemService( - Context.INPUT_METHOD_SERVICE); - if (imm == null) - return; - - if (mReset) - imm.restartInput(GeckoApp.surfaceView); - - if (!mEnable) - return; - - int state = GeckoApp.surfaceView.mIMEState; - if (state != GeckoSurfaceView.IME_STATE_DISABLED && - state != GeckoSurfaceView.IME_STATE_PLUGIN) - imm.showSoftInput(GeckoApp.surfaceView, 0); - else - imm.hideSoftInputFromWindow( - GeckoApp.surfaceView.getWindowToken(), 0); - } - } public static void notifyIME(int type, int state) { - if (GeckoApp.surfaceView == null) - return; - - switch (type) { - case NOTIFY_IME_RESETINPUTSTATE: - // Composition event is already fired from widget. - // So reset IME flags. - GeckoApp.surfaceView.inputConnection.reset(); - - // Don't use IMEStateUpdater for reset. - // Because IME may not work showSoftInput() - // after calling restartInput() immediately. - // So we have to call showSoftInput() delay. - InputMethodManager imm = (InputMethodManager) - GeckoApp.surfaceView.getContext().getSystemService( - Context.INPUT_METHOD_SERVICE); - if (imm == null) { - // no way to reset IME status directly - IMEStateUpdater.resetIME(); - } else { - imm.restartInput(GeckoApp.surfaceView); - } - - // keep current enabled state - IMEStateUpdater.enableIME(); - break; - - case NOTIFY_IME_CANCELCOMPOSITION: - IMEStateUpdater.resetIME(); - break; - - case NOTIFY_IME_FOCUSCHANGE: - IMEStateUpdater.resetIME(); - break; - } + mInputConnection.notifyIME(type, state); } public static void notifyIMEEnabled(int state, String typeHint, - String actionHint, boolean landscapeFS) - { - if (GeckoApp.surfaceView == null) - return; - - /* When IME is 'disabled', IME processing is disabled. - In addition, the IME UI is hidden */ - GeckoApp.surfaceView.mIMEState = state; - GeckoApp.surfaceView.mIMETypeHint = typeHint; - GeckoApp.surfaceView.mIMEActionHint = actionHint; - GeckoApp.surfaceView.mIMELandscapeFS = landscapeFS; - IMEStateUpdater.enableIME(); + String actionHint, boolean landscapeFS) { + mInputConnection.notifyIMEEnabled(state, typeHint, actionHint, landscapeFS); } public static void notifyIMEChange(String text, int start, int end, int newEnd) { - if (GeckoApp.surfaceView == null || - GeckoApp.surfaceView.inputConnection == null) - return; - - InputMethodManager imm = (InputMethodManager) - GeckoApp.surfaceView.getContext().getSystemService( - Context.INPUT_METHOD_SERVICE); - if (imm == null) - return; - - // Log.d("GeckoAppJava", String.format("IME: notifyIMEChange: t=%s s=%d ne=%d oe=%d", - // text, start, newEnd, end)); - - if (newEnd < 0) - GeckoApp.surfaceView.inputConnection.notifySelectionChange( - imm, start, end); - else - GeckoApp.surfaceView.inputConnection.notifyTextChange( - imm, text, start, end, newEnd); + mInputConnection.notifyIMEChange(text, start, end, newEnd); } private static CountDownLatch sGeckoPendingAcks = null; @@ -627,8 +552,8 @@ public class GeckoAppShell static Sensor gOrientationSensor = null; public static void enableDeviceMotion(boolean enable) { - SensorManager sm = (SensorManager) - GeckoApp.surfaceView.getContext().getSystemService(Context.SENSOR_SERVICE); + LayerView v = GeckoApp.mAppContext.getLayerController().getView(); + SensorManager sm = (SensorManager) v.getContext().getSystemService(Context.SENSOR_SERVICE); if (gAccelerometerSensor == null || gOrientationSensor == null) { gAccelerometerSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); @@ -637,23 +562,24 @@ public class GeckoAppShell if (enable) { if (gAccelerometerSensor != null) - sm.registerListener(GeckoApp.surfaceView, gAccelerometerSensor, SensorManager.SENSOR_DELAY_GAME); + sm.registerListener(GeckoApp.mAppContext, gAccelerometerSensor, SensorManager.SENSOR_DELAY_GAME); if (gOrientationSensor != null) - sm.registerListener(GeckoApp.surfaceView, gOrientationSensor, SensorManager.SENSOR_DELAY_GAME); + sm.registerListener(GeckoApp.mAppContext, gOrientationSensor, SensorManager.SENSOR_DELAY_GAME); } else { if (gAccelerometerSensor != null) - sm.unregisterListener(GeckoApp.surfaceView, gAccelerometerSensor); + sm.unregisterListener(GeckoApp.mAppContext, gAccelerometerSensor); if (gOrientationSensor != null) - sm.unregisterListener(GeckoApp.surfaceView, gOrientationSensor); + sm.unregisterListener(GeckoApp.mAppContext, gOrientationSensor); } } public static void enableLocation(final boolean enable) { getMainHandler().post(new Runnable() { public void run() { - GeckoSurfaceView view = GeckoApp.surfaceView; + LayerView v = GeckoApp.mAppContext.getLayerController().getView(); + LocationManager lm = (LocationManager) - view.getContext().getSystemService(Context.LOCATION_SERVICE); + GeckoApp.mAppContext.getSystemService(Context.LOCATION_SERVICE); if (enable) { Criteria crit = new Criteria(); @@ -665,11 +591,11 @@ public class GeckoAppShell Looper l = Looper.getMainLooper(); Location loc = lm.getLastKnownLocation(provider); if (loc != null) { - view.onLocationChanged(loc); + GeckoApp.mAppContext.onLocationChanged(loc); } - lm.requestLocationUpdates(provider, 100, (float).5, view, l); + lm.requestLocationUpdates(provider, 100, (float).5, GeckoApp.mAppContext, l); } else { - lm.removeUpdates(view); + lm.removeUpdates(GeckoApp.mAppContext); } } }); @@ -680,24 +606,19 @@ public class GeckoAppShell } public static void returnIMEQueryResult(String result, int selectionStart, int selectionLength) { - GeckoApp.surfaceView.inputConnection.mSelectionStart = selectionStart; - GeckoApp.surfaceView.inputConnection.mSelectionLength = selectionLength; - try { - GeckoApp.surfaceView.inputConnection.mQueryResult.put(result); - } catch (InterruptedException e) { - } + mInputConnection.returnIMEQueryResult(result, selectionStart, selectionLength); } static void onAppShellReady() { // mLaunchState can only be Launched at this point - GeckoApp.setLaunchState(GeckoApp.LaunchState.GeckoRunning); + GeckoApp.mAppContext.setLaunchState(GeckoApp.LaunchState.GeckoRunning); sendPendingEventsToGecko(); } static void onXreExit() { // mLaunchState can only be Launched or GeckoRunning at this point - GeckoApp.setLaunchState(GeckoApp.LaunchState.GeckoExiting); + GeckoApp.mAppContext.setLaunchState(GeckoApp.LaunchState.GeckoExiting); Log.i("GeckoAppJava", "XRE exited"); if (gRestartScheduled) { GeckoApp.mAppContext.doRestart(); @@ -761,8 +682,7 @@ public class GeckoAppShell } static String[] getHandlersForIntent(Intent intent) { - PackageManager pm = - GeckoApp.surfaceView.getContext().getPackageManager(); + PackageManager pm = GeckoApp.mAppContext.getPackageManager(); List list = pm.queryIntentActivities(intent, 0); int numAttr = 4; String[] ret = new String[list.size() * numAttr]; @@ -861,7 +781,7 @@ public class GeckoAppShell intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); try { - GeckoApp.surfaceView.getContext().startActivity(intent); + GeckoApp.mAppContext.startActivity(intent); return true; } catch(ActivityNotFoundException e) { return false; @@ -880,7 +800,7 @@ public class GeckoAppShell getHandler().post(new Runnable() { @SuppressWarnings("deprecation") public void run() { - Context context = GeckoApp.surfaceView.getContext(); + Context context = GeckoApp.mAppContext; String text = null; if (android.os.Build.VERSION.SDK_INT >= 11) { android.content.ClipboardManager cm = (android.content.ClipboardManager) @@ -913,7 +833,7 @@ public class GeckoAppShell getHandler().post(new Runnable() { @SuppressWarnings("deprecation") public void run() { - Context context = GeckoApp.surfaceView.getContext(); + Context context = GeckoApp.mAppContext; if (android.os.Build.VERSION.SDK_INT >= 11) { android.content.ClipboardManager cm = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); @@ -1057,21 +977,19 @@ public class GeckoAppShell } public static void performHapticFeedback(boolean aIsLongPress) { - GeckoApp.surfaceView. - performHapticFeedback(aIsLongPress ? - HapticFeedbackConstants.LONG_PRESS : - HapticFeedbackConstants.VIRTUAL_KEY); + // TODO } public static void showInputMethodPicker() { - InputMethodManager imm = (InputMethodManager) GeckoApp.surfaceView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = (InputMethodManager) + GeckoApp.mAppContext.getSystemService(Context.INPUT_METHOD_SERVICE); imm.showInputMethodPicker(); } public static void setKeepScreenOn(final boolean on) { GeckoApp.mAppContext.runOnUiThread(new Runnable() { public void run() { - GeckoApp.surfaceView.setKeepScreenOn(on); + // TODO } }); } @@ -1245,7 +1163,7 @@ public class GeckoAppShell } public static void scanMedia(String aFile, String aMimeType) { - Context context = GeckoApp.surfaceView.getContext(); + Context context = GeckoApp.mAppContext; GeckoMediaScannerClient client = new GeckoMediaScannerClient(context, aFile, aMimeType); } @@ -1257,7 +1175,7 @@ public class GeckoAppShell if (aExt != null && aExt.length() > 1 && aExt.charAt(0) == '.') aExt = aExt.substring(1); - PackageManager pm = GeckoApp.surfaceView.getContext().getPackageManager(); + PackageManager pm = GeckoApp.mAppContext.getPackageManager(); Drawable icon = getDrawableForExtension(pm, aExt); if (icon == null) { // Use a generic icon @@ -1662,8 +1580,11 @@ public class GeckoAppShell if (!accessibilityManager.isEnabled()) return; + LayerController layerController = GeckoApp.mAppContext.getLayerController(); + LayerView layerView = layerController.getView(); + AccessibilityEvent event = AccessibilityEvent.obtain(eventType); - event.setClassName(GeckoApp.surfaceView.getClass().getName() + "$" + role); + event.setClassName(layerView.getClass().getName() + "$" + role); event.setPackageName(GeckoApp.mAppContext.getPackageName()); event.setEnabled(enabled); event.setChecked(checked); diff --git a/embedding/android/GeckoEvent.java b/embedding/android/GeckoEvent.java index 286130ce6817..a00deb0897f6 100644 --- a/embedding/android/GeckoEvent.java +++ b/embedding/android/GeckoEvent.java @@ -203,14 +203,14 @@ public class GeckoEvent { rangeForeColor, rangeBackColor); } - public GeckoEvent(int etype, Rect dirty) { + public GeckoEvent(int etype, Rect rect) { if (etype != DRAW) { mType = INVALID; return; } mType = etype; - mRect = dirty; + mRect = rect; } public GeckoEvent(int etype, int w, int h, int screenw, int screenh) { diff --git a/embedding/android/GeckoInputConnection.java b/embedding/android/GeckoInputConnection.java index 2137465ea031..6b1bbcfe7e79 100644 --- a/embedding/android/GeckoInputConnection.java +++ b/embedding/android/GeckoInputConnection.java @@ -42,6 +42,10 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; +import org.mozilla.gecko.gfx.InputConnectionHandler; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + import android.os.*; import android.app.*; import android.text.*; @@ -51,11 +55,15 @@ import android.view.inputmethod.*; import android.content.*; import android.R; +import android.text.method.TextKeyListener; +import android.text.method.KeyListener; + import android.util.*; + public class GeckoInputConnection extends BaseInputConnection - implements TextWatcher + implements TextWatcher, InputConnectionHandler { private class ChangeNotification { public String mText; @@ -81,25 +89,31 @@ public class GeckoInputConnection public GeckoInputConnection (View targetView) { super(targetView, true); mQueryResult = new SynchronousQueue(); + + mEditableFactory = Editable.Factory.getInstance(); + initEditable(""); + mIMEState = IME_STATE_DISABLED; + mIMETypeHint = ""; + mIMEActionHint = ""; } @Override public boolean beginBatchEdit() { - //Log.d("GeckoAppJava", "IME: beginBatchEdit"); + Log.d("GeckoAppJava", "IME: beginBatchEdit"); mBatchMode = true; return true; } @Override public boolean commitCompletion(CompletionInfo text) { - //Log.d("GeckoAppJava", "IME: commitCompletion"); + Log.d("GeckoAppJava", "IME: commitCompletion"); return commitText(text.getText(), 1); } @Override public boolean commitText(CharSequence text, int newCursorPosition) { - //Log.d("GeckoAppJava", "IME: commitText"); + Log.d("GeckoAppJava", "IME: commitText"); setComposingText(text, newCursorPosition); finishComposingText(); @@ -109,7 +123,7 @@ public class GeckoInputConnection @Override public boolean deleteSurroundingText(int leftLength, int rightLength) { - //Log.d("GeckoAppJava", "IME: deleteSurroundingText"); + Log.d("GeckoAppJava", "IME: deleteSurroundingText"); if (leftLength == 0 && rightLength == 0) return true; @@ -168,13 +182,13 @@ public class GeckoInputConnection @Override public boolean endBatchEdit() { - //Log.d("GeckoAppJava", "IME: endBatchEdit"); + Log.d("GeckoAppJava", "IME: endBatchEdit"); mBatchMode = false; if (!mBatchChanges.isEmpty()) { InputMethodManager imm = (InputMethodManager) - GeckoApp.surfaceView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + GeckoApp.mAppContext.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) { for (ChangeNotification n : mBatchChanges) { if (n.mText != null) @@ -190,7 +204,7 @@ public class GeckoInputConnection @Override public boolean finishComposingText() { - //Log.d("GeckoAppJava", "IME: finishComposingText"); + Log.d("GeckoAppJava", "IME: finishComposingText"); if (mComposing) { // Set style to none @@ -215,7 +229,7 @@ public class GeckoInputConnection @Override public int getCursorCapsMode(int reqModes) { - //Log.d("GeckoAppJava", "IME: getCursorCapsMode"); + Log.d("GeckoAppJava", "IME: getCursorCapsMode"); return 0; } @@ -230,7 +244,7 @@ public class GeckoInputConnection @Override public boolean performContextMenuAction(int id) { - //Log.d("GeckoAppJava", "IME: performContextMenuAction"); + Log.d("GeckoAppJava", "IME: performContextMenuAction"); // First we need to ask Gecko to tell us the full contents of the // text field we're about to operate on. @@ -290,7 +304,7 @@ public class GeckoInputConnection if (!GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning)) return null; - //Log.d("GeckoAppJava", "IME: getExtractedText"); + Log.d("GeckoAppJava", "IME: getExtractedText"); ExtractedText extract = new ExtractedText(); extract.flags = 0; @@ -342,7 +356,7 @@ public class GeckoInputConnection @Override public CharSequence getTextAfterCursor(int length, int flags) { - //Log.d("GeckoAppJava", "IME: getTextAfterCursor"); + Log.d("GeckoAppJava", "IME: getTextAfterCursor"); GeckoAppShell.sendEventToGecko( new GeckoEvent(GeckoEvent.IME_GET_SELECTION, 0, 0)); @@ -379,14 +393,14 @@ public class GeckoInputConnection @Override public CharSequence getTextBeforeCursor(int length, int flags) { - //Log.d("GeckoAppJava", "IME: getTextBeforeCursor"); + Log.d("GeckoAppJava", "IME: getTextBeforeCursor"); return getTextAfterCursor(-length, flags); } @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { - //Log.d("GeckoAppJava", "IME: setComposingText"); + Log.d("GeckoAppJava", "IME: setComposingText"); enableChangeNotifications(); @@ -520,7 +534,7 @@ public class GeckoInputConnection @Override public boolean setComposingRegion(int start, int end) { - //Log.d("GeckoAppJava", "IME: setComposingRegion(start=" + start + ", end=" + end + ")"); + Log.d("GeckoAppJava", "IME: setComposingRegion(start=" + start + ", end=" + end + ")"); if (start < 0 || end < start) return true; @@ -554,7 +568,7 @@ public class GeckoInputConnection @Override public boolean setSelection(int start, int end) { - //Log.d("GeckoAppJava", "IME: setSelection"); + Log.d("GeckoAppJava", "IME: setSelection"); if (mComposing) { /* Translate to fake selection positions */ @@ -602,8 +616,8 @@ public class GeckoInputConnection public void notifyTextChange(InputMethodManager imm, String text, int start, int oldEnd, int newEnd) { - // Log.d("GeckoAppShell", String.format("IME: notifyTextChange: text=%s s=%d ne=%d oe=%d", - // text, start, newEnd, oldEnd)); + Log.d("GeckoAppShell", String.format("IME: notifyTextChange: text=%s s=%d ne=%d oe=%d", + text, start, newEnd, oldEnd)); if (!mChangeNotificationsEnabled) return; @@ -616,8 +630,10 @@ public class GeckoInputConnection // If there are pending changes, that means this text is not the most up-to-date version // and we'll step on ourselves if we change the editable right now. - if (mNumPendingChanges == 0 && !text.contentEquals(GeckoApp.surfaceView.mEditable)) - GeckoApp.surfaceView.setEditable(text); + View v = GeckoApp.mAppContext.getLayerController().getView(); + + if (mNumPendingChanges == 0 && !text.contentEquals(mEditable)) + setEditable(text); if (mUpdateRequest == null) return; @@ -636,13 +652,12 @@ public class GeckoInputConnection mUpdateExtract.text = text.substring(0, newEnd); mUpdateExtract.startOffset = 0; - imm.updateExtractedText(GeckoApp.surfaceView, - mUpdateRequest.token, mUpdateExtract); + imm.updateExtractedText(v, mUpdateRequest.token, mUpdateExtract); } public void notifySelectionChange(InputMethodManager imm, int start, int end) { - // Log.d("GeckoAppJava", String.format("IME: notifySelectionChange: s=%d e=%d", start, end)); + Log.d("GeckoAppJava", String.format("IME: notifySelectionChange: s=%d e=%d", start, end)); if (!mChangeNotificationsEnabled) return; @@ -652,22 +667,23 @@ public class GeckoInputConnection return; } + View v = GeckoApp.mAppContext.getLayerController().getView(); if (mComposing) - imm.updateSelection(GeckoApp.surfaceView, - mCompositionStart + mCompositionSelStart, - mCompositionStart + mCompositionSelStart + mCompositionSelLen, - mCompositionStart, - mCompositionStart + mComposingText.length()); + imm.updateSelection(v, + mCompositionStart + mCompositionSelStart, + mCompositionStart + mCompositionSelStart + mCompositionSelLen, + mCompositionStart, + mCompositionStart + mComposingText.length()); else - imm.updateSelection(GeckoApp.surfaceView, start, end, -1, -1); + imm.updateSelection(v, start, end, -1, -1); // We only change the selection if we are relatively sure that the text we have is // up-to-date. Bail out if we are stil expecting changes. if (mNumPendingChanges > 0) return; - int maxLen = GeckoApp.surfaceView.mEditable.length(); - Selection.setSelection(GeckoApp.surfaceView.mEditable, + int maxLen = mEditable.length(); + Selection.setSelection(mEditable, Math.min(start, maxLen), Math.min(end, maxLen)); } @@ -684,8 +700,8 @@ public class GeckoInputConnection // TextWatcher public void onTextChanged(CharSequence s, int start, int before, int count) { - // Log.d("GeckoAppShell", String.format("IME: onTextChanged: t=%s s=%d b=%d l=%d", - // s, start, before, count)); + Log.d("GeckoAppShell", String.format("IME: onTextChanged: t=%s s=%d b=%d l=%d", + s, start, before, count)); mNumPendingChanges++; GeckoAppShell.sendEventToGecko( @@ -732,6 +748,331 @@ public class GeckoInputConnection mChangeNotificationsEnabled = true; } + + public InputConnection onCreateInputConnection(EditorInfo outAttrs) + { + Log.d("GeckoAppJava", "IME: handleCreateInputConnection called"); + + outAttrs.inputType = InputType.TYPE_CLASS_TEXT; + outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE; + outAttrs.actionLabel = null; + mKeyListener = TextKeyListener.getInstance(); + + if (mIMEState == IME_STATE_PASSWORD) + outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD; + else if (mIMETypeHint.equalsIgnoreCase("url")) + outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI; + else if (mIMETypeHint.equalsIgnoreCase("email")) + outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + else if (mIMETypeHint.equalsIgnoreCase("search")) + outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; + else if (mIMETypeHint.equalsIgnoreCase("tel")) + outAttrs.inputType = InputType.TYPE_CLASS_PHONE; + else if (mIMETypeHint.equalsIgnoreCase("number") || + mIMETypeHint.equalsIgnoreCase("range")) + outAttrs.inputType = InputType.TYPE_CLASS_NUMBER; + else if (mIMETypeHint.equalsIgnoreCase("datetime") || + mIMETypeHint.equalsIgnoreCase("datetime-local")) + outAttrs.inputType = InputType.TYPE_CLASS_DATETIME | + InputType.TYPE_DATETIME_VARIATION_NORMAL; + else if (mIMETypeHint.equalsIgnoreCase("date")) + outAttrs.inputType = InputType.TYPE_CLASS_DATETIME | + InputType.TYPE_DATETIME_VARIATION_DATE; + else if (mIMETypeHint.equalsIgnoreCase("time")) + outAttrs.inputType = InputType.TYPE_CLASS_DATETIME | + InputType.TYPE_DATETIME_VARIATION_TIME; + + if (mIMEActionHint.equalsIgnoreCase("go")) + outAttrs.imeOptions = EditorInfo.IME_ACTION_GO; + else if (mIMEActionHint.equalsIgnoreCase("done")) + outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; + else if (mIMEActionHint.equalsIgnoreCase("next")) + outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT; + else if (mIMEActionHint.equalsIgnoreCase("search")) + outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; + else if (mIMEActionHint.equalsIgnoreCase("send")) + outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND; + else if (mIMEActionHint != null && mIMEActionHint.length() != 0) + outAttrs.actionLabel = mIMEActionHint; + + if (mIMELandscapeFS == false) + outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI; + + reset(); + return this; + } + + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + return processKeyDown(keyCode, event, true); + case KeyEvent.ACTION_UP: + return processKeyUp(keyCode, event, true); + case KeyEvent.ACTION_MULTIPLE: + return onKeyMultiple(keyCode, event.getRepeatCount(), event); + } + return false; + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + return processKeyDown(keyCode, event, false); + } + + private boolean processKeyDown(int keyCode, KeyEvent event, boolean isPreIme) { + switch (keyCode) { + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_SEARCH: + return false; + case KeyEvent.KEYCODE_DEL: + // See comments in GeckoInputConnection.onKeyDel + if (onKeyDel()) { + return true; + } + break; + case KeyEvent.KEYCODE_ENTER: + if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 && + mIMEActionHint.equalsIgnoreCase("next")) + event = new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB); + break; + default: + break; + } + + if (isPreIme && mIMEState != IME_STATE_DISABLED && + (event.getMetaState() & KeyEvent.META_ALT_ON) == 0) + // Let active IME process pre-IME key events + return false; + + View v = GeckoApp.mAppContext.getLayerController().getView(); + + // KeyListener returns true if it handled the event for us. + if (mIMEState == IME_STATE_DISABLED || + keyCode == KeyEvent.KEYCODE_ENTER || + keyCode == KeyEvent.KEYCODE_DEL || + (event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 || + !mKeyListener.onKeyDown(v, mEditable, keyCode, event)) + GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); + return true; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + return processKeyUp(keyCode, event, false); + } + + private boolean processKeyUp(int keyCode, KeyEvent event, boolean isPreIme) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_SEARCH: + case KeyEvent.KEYCODE_MENU: + return false; + default: + break; + } + + if (isPreIme && mIMEState != IME_STATE_DISABLED && + (event.getMetaState() & KeyEvent.META_ALT_ON) == 0) + // Let active IME process pre-IME key events + return false; + View v = GeckoApp.mAppContext.getLayerController().getView(); + + if (mIMEState == IME_STATE_DISABLED || + keyCode == KeyEvent.KEYCODE_ENTER || + keyCode == KeyEvent.KEYCODE_DEL || + (event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 || + !mKeyListener.onKeyUp(v, mEditable, keyCode, event)) + GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); + return true; + } + + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); + return true; + } + + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + View v = GeckoApp.mAppContext.getLayerController().getView(); + switch (keyCode) { + case KeyEvent.KEYCODE_MENU: + InputMethodManager imm = (InputMethodManager) + v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.toggleSoftInputFromWindow(v.getWindowToken(), + imm.SHOW_FORCED, 0); + return true; + default: + break; + } + return false; + } + + + public void notifyIME(int type, int state) { + + View v = GeckoApp.mAppContext.getLayerController().getView(); + + Log.d("GeckoAppJava", "notifyIME"); + + if (v == null) + return; + + Log.d("GeckoAppJava", "notifyIME v!= null"); + + switch (type) { + case NOTIFY_IME_RESETINPUTSTATE: + + Log.d("GeckoAppJava", "notifyIME = reset"); + // Composition event is already fired from widget. + // So reset IME flags. + reset(); + + // Don't use IMEStateUpdater for reset. + // Because IME may not work showSoftInput() + // after calling restartInput() immediately. + // So we have to call showSoftInput() delay. + InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + // no way to reset IME status directly + IMEStateUpdater.resetIME(); + } else { + imm.restartInput(v); + } + + // keep current enabled state + IMEStateUpdater.enableIME(); + break; + + case NOTIFY_IME_CANCELCOMPOSITION: + Log.d("GeckoAppJava", "notifyIME = cancel"); + IMEStateUpdater.resetIME(); + break; + + case NOTIFY_IME_FOCUSCHANGE: + Log.d("GeckoAppJava", "notifyIME = focus"); + IMEStateUpdater.resetIME(); + break; + } + } + + public void notifyIMEEnabled(int state, String typeHint, + String actionHint, boolean landscapeFS) + { + View v = GeckoApp.mAppContext.getLayerController().getView(); + + if (v == null) + return; + + /* When IME is 'disabled', IME processing is disabled. + In addition, the IME UI is hidden */ + mIMEState = state; + mIMETypeHint = typeHint; + mIMEActionHint = actionHint; + mIMELandscapeFS = landscapeFS; + IMEStateUpdater.enableIME(); + } + + + public void notifyIMEChange(String text, int start, int end, int newEnd) { + View v = GeckoApp.mAppContext.getLayerController().getView(); + + if (v == null) + return; + + InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) + return; + + Log.d("GeckoAppJava", String.format("IME: notifyIMEChange: t=%s s=%d ne=%d oe=%d", + text, start, newEnd, end)); + + if (newEnd < 0) + notifySelectionChange(imm, start, end); + else + notifyTextChange(imm, text, start, end, newEnd); + } + + + public void returnIMEQueryResult(String result, int selectionStart, int selectionLength) { + mSelectionStart = selectionStart; + mSelectionLength = selectionLength; + try { + mQueryResult.put(result); + } catch (InterruptedException e) {} + } + + static private final Timer mIMETimer = new Timer(); + + static private final int NOTIFY_IME_RESETINPUTSTATE = 0; + static private final int NOTIFY_IME_SETOPENSTATE = 1; + static private final int NOTIFY_IME_CANCELCOMPOSITION = 2; + static private final int NOTIFY_IME_FOCUSCHANGE = 3; + + + /* Delay updating IME states (see bug 573800) */ + private static final class IMEStateUpdater extends TimerTask + { + static private IMEStateUpdater instance; + private boolean mEnable, mReset; + + static private IMEStateUpdater getInstance() { + if (instance == null) { + instance = new IMEStateUpdater(); + mIMETimer.schedule(instance, 200); + } + return instance; + } + + static public synchronized void enableIME() { + getInstance().mEnable = true; + } + + static public synchronized void resetIME() { + getInstance().mReset = true; + } + + public void run() { + Log.d("GeckoAppJava", "IME: run()"); + synchronized(IMEStateUpdater.class) { + instance = null; + } + + View v = GeckoApp.mAppContext.getLayerController().getView(); + Log.d("GeckoAppJava", "IME: v="+v); + + InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) + return; + + if (mReset) + imm.restartInput(v); + + if (!mEnable) + return; + + if (mIMEState != IME_STATE_DISABLED && + mIMEState != IME_STATE_PLUGIN) + imm.showSoftInput(v, 0); + else + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + } + + public void setEditable(String contents) + { + mEditable.removeSpan(this); + mEditable.replace(0, mEditable.length(), contents); + mEditable.setSpan(this, 0, contents.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + Selection.setSelection(mEditable, contents.length()); + } + + public void initEditable(String contents) + { + mEditable = mEditableFactory.newEditable(contents); + mEditable.setSpan(this, 0, contents.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + Selection.setSelection(mEditable, contents.length()); + } + // Is a composition active? boolean mComposing; // Composition text when a composition is active @@ -747,6 +1088,20 @@ public class GeckoInputConnection // Number of in flight changes int mNumPendingChanges; + // IME stuff + public static final int IME_STATE_DISABLED = 0; + public static final int IME_STATE_ENABLED = 1; + public static final int IME_STATE_PASSWORD = 2; + public static final int IME_STATE_PLUGIN = 3; + + KeyListener mKeyListener; + Editable mEditable; + Editable.Factory mEditableFactory; + static int mIMEState; + static String mIMETypeHint; + static String mIMEActionHint; + static boolean mIMELandscapeFS; + private boolean mBatchMode; private boolean mChangeNotificationsEnabled = true; diff --git a/embedding/android/GeckoSurfaceView.java b/embedding/android/GeckoSurfaceView.java deleted file mode 100644 index 79504716cfbc..000000000000 --- a/embedding/android/GeckoSurfaceView.java +++ /dev/null @@ -1,827 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Mozilla Android code. - * - * The Initial Developer of the Original Code is Mozilla Foundation. - * Portions created by the Initial Developer are Copyright (C) 2010 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Vladimir Vukicevic - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -package org.mozilla.gecko; - -import java.io.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.locks.*; -import java.util.concurrent.atomic.*; -import java.util.zip.*; -import java.nio.*; - -import android.os.*; -import android.app.*; -import android.text.*; -import android.text.method.*; -import android.view.*; -import android.view.inputmethod.*; -import android.content.*; -import android.graphics.*; -import android.widget.*; -import android.hardware.*; -import android.location.*; -import android.graphics.drawable.*; -import android.content.res.*; -import android.util.*; - -/* - * GeckoSurfaceView implements a GL surface view, - * similar to GLSurfaceView. However, since we - * already have a thread for Gecko, we don't really want - * a separate renderer thread that GLSurfaceView provides. - */ -class GeckoSurfaceView - extends SurfaceView - implements SurfaceHolder.Callback, SensorEventListener, LocationListener -{ - private static final String LOG_FILE_NAME = "GeckoSurfaceView"; - - public GeckoSurfaceView(Context context) { - super(context, null, android.R.style.Theme_Light_NoTitleBar); - - getHolder().addCallback(this); - inputConnection = new GeckoInputConnection(this); - gestureScanner = new GeckoGestureDetector(context); - setFocusable(true); - setFocusableInTouchMode(true); - - DisplayMetrics metrics = new DisplayMetrics(); - GeckoApp.mAppContext.getWindowManager(). - getDefaultDisplay().getMetrics(metrics); - mWidth = metrics.widthPixels; - mHeight = metrics.heightPixels; - mBufferWidth = 0; - mBufferHeight = 0; - - mSurfaceLock = new ReentrantLock(); - - mEditableFactory = Editable.Factory.getInstance(); - initEditable(""); - mIMEState = IME_STATE_DISABLED; - mIMETypeHint = ""; - mIMEActionHint = ""; - } - - protected void finalize() throws Throwable { - super.finalize(); - } - - /* - * Called on main thread - */ - - public String getStartupBitmapFilePath() { - File file = new File(Environment.getExternalStorageDirectory(), - "lastScreen.png"); - return file.toString(); - } - - public void hideStartupBitmap() { - Log.e(LOG_FILE_NAME, "!!! hideStartupBitmap !!!"); - if (mShowingLoadScreen == false) - return; - - mStartupBitmap = null; - mShowingLoadScreen = false; - - surfaceCreated(getHolder()); - surfaceChanged(getHolder(), mFormat, mWidth, mHeight); - } - - public void showStartupBitmap() { - Log.e(LOG_FILE_NAME, "!!! showStartupBitmap !!!"); - mShowingLoadScreen = true; - } - - public void loadStartupBitmap() { - // This is blocking on the main thread and that is - // okay. we want to get this image in as soon as - // possible so that we can paint it to the screen. - String filePath = getStartupBitmapFilePath(); - mStartupBitmap = BitmapFactory.decodeFile(filePath); - } - - public void drawStartupBitmap(SurfaceHolder holder, int width, int height) { - Log.e(LOG_FILE_NAME, "!!! drawStartupBitmap !!!"); - - Canvas c = holder.lockCanvas(); - if (c == null) { - Log.e(LOG_FILE_NAME, "!!! NO CANVAS !!!"); - return; - } - - if (mStartupBitmap == null) { - Resources res = getResources(); - Drawable drawable = res.getDrawable(R.drawable.start); - drawable.setBounds(0, 0, width, height); - drawable.draw(c); - - Paint paint = new Paint(); - c.drawText("Place holder. Missing screenshot.", 10.0f, 20.0f, paint); - } else { - Drawable drawable = new BitmapDrawable(mStartupBitmap); - drawable.setBounds(0, 0, width, height); - drawable.draw(c); - } - holder.unlockCanvasAndPost(c); - } - - public void draw(SurfaceHolder holder, ByteBuffer buffer) { - Log.e(LOG_FILE_NAME, "!!! draw1 !!!"); - - if (buffer == null || buffer.capacity() != (mWidth * mHeight * 2)) - return; - - synchronized (mSoftwareBuffer) { - if (buffer != mSoftwareBuffer || mSoftwareBufferCopy == null) - return; - - Canvas c = holder.lockCanvas(); - if (c == null) - return; - mSoftwareBufferCopy.copyPixelsFromBuffer(buffer); - c.drawBitmap(mSoftwareBufferCopy, 0, 0, null); - holder.unlockCanvasAndPost(c); - } - } - - public void draw(SurfaceHolder holder, Bitmap bitmap) { - Log.e(LOG_FILE_NAME, "!!! draw2 !!!"); - - if (bitmap == null || - bitmap.getWidth() != mWidth || bitmap.getHeight() != mHeight) - return; - - synchronized (mSoftwareBitmap) { - if (bitmap != mSoftwareBitmap) - return; - - Canvas c = holder.lockCanvas(); - if (c == null) - return; - c.drawBitmap(bitmap, 0, 0, null); - holder.unlockCanvasAndPost(c); - } - } - - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - Log.i(LOG_FILE_NAME, "!!! surfaceChanged: fmt: " + format + " dim: " + width + " " + height); - - mFormat = format; - mWidth = width; - mHeight = height; - - if (mShowingLoadScreen) { - drawStartupBitmap(holder, width, height); - return; - } - - // On pre-Honeycomb, force exactly one frame of the previous size - // to render because the surface change is only seen by GLES after we - // have swapped the back buffer (i.e. the buffer size only changes - // after the next swap buffer). We need to make sure Gecko's view - // resizes when Android's buffer resizes. - // In Honeycomb, the buffer size changes immediately, so rendering a - // frame of the previous size is unnecessary (and wrong). - if (mDrawMode == DRAW_GLES_2 && - (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB)) { - // When we get a surfaceChange event, we have 0 to n paint events - // waiting in the Gecko event queue. We will make the first - // succeed and the abort the others. - mDrawSingleFrame = true; - if (!mInDrawing) { - // Queue at least one paint event in case none are queued. - GeckoAppShell.scheduleRedraw(); - } - GeckoAppShell.geckoEventSync(); - mDrawSingleFrame = false; - mAbortDraw = false; - } - - mSurfaceLock.lock(); - - if (mInDrawing) { - Log.w(LOG_FILE_NAME, "!! surfaceChanged while mInDrawing is true!"); - } - - boolean invalidSize; - - if (width == 0 || height == 0) { - mSoftwareBitmap = null; - mSoftwareBuffer = null; - mSoftwareBufferCopy = null; - invalidSize = true; - } else { - invalidSize = false; - } - - boolean doSyncDraw = - mDrawMode == DRAW_2D && - !invalidSize && - GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning); - mSyncDraw = doSyncDraw; - - mSurfaceValid = true; - - Log.i(LOG_FILE_NAME, "!! surfaceChanged: fmt: " + format + " dim: " + width + " " + height); - - try { - DisplayMetrics metrics = new DisplayMetrics(); - GeckoApp.mAppContext.getWindowManager().getDefaultDisplay().getMetrics(metrics); - - GeckoEvent e = new GeckoEvent(GeckoEvent.SIZE_CHANGED, width, height, - metrics.widthPixels, metrics.heightPixels); - GeckoAppShell.sendEventToGecko(e); - } finally { - mSurfaceLock.unlock(); - } - - if (doSyncDraw) { - GeckoAppShell.scheduleRedraw(); - - Object syncDrawObject = null; - try { - syncDrawObject = mSyncDraws.take(); - } catch (InterruptedException ie) { - Log.e(LOG_FILE_NAME, "!! Threw exception while getting sync draw bitmap/buffer: ", ie); - } - if (syncDrawObject != null) { - if (syncDrawObject instanceof Bitmap) - draw(holder, (Bitmap)syncDrawObject); - else - draw(holder, (ByteBuffer)syncDrawObject); - } else { - Log.e(LOG_FILE_NAME, "!! Synchronised draw object is null"); - } - } else if (!mShowingLoadScreen) { - // Make sure a frame is drawn before we return - // otherwise we see artifacts or a black screen - GeckoAppShell.scheduleRedraw(); - GeckoAppShell.geckoEventSync(); - } - - // if the surface changed size and we have the soft keyboard up, make sure - // the focused input field is still in view or it might get hidden behind the - // keyboard and be really hard to use - if (mIMEState == IME_STATE_ENABLED) { - GeckoAppShell.sendEventToGecko(new GeckoEvent("ScrollTo:FocusedInput", null)); - } - } - - public void surfaceCreated(SurfaceHolder holder) { - // Delay sending this event if we are painting the - // load screen. The native access paint path will - // paint directly to the screen and we will see a - // black screen while content is initally being - // drawn. - if (!mShowingLoadScreen) { - Log.i(LOG_FILE_NAME, "!! surface created"); - GeckoEvent e = new GeckoEvent(GeckoEvent.SURFACE_CREATED); - GeckoAppShell.sendEventToGecko(e); - } - } - - public void saveLast(boolean sync) { - Log.i(LOG_FILE_NAME, "!! save last"); - GeckoEvent event = new GeckoEvent(); - event.mType = GeckoEvent.SAVE_STATE; - event.mCharacters = getStartupBitmapFilePath(); - if (sync) - GeckoAppShell.sendEventToGeckoSync(event); - else - GeckoAppShell.sendEventToGecko(event); - } - - public void surfaceDestroyed(SurfaceHolder holder) { - Log.i(LOG_FILE_NAME, "!! surface destroyed"); - mSurfaceValid = false; - mSoftwareBuffer = null; - mSoftwareBufferCopy = null; - mSoftwareBitmap = null; - GeckoEvent e = new GeckoEvent(GeckoEvent.SURFACE_DESTROYED); - if (mDrawMode == DRAW_GLES_2) { - // Ensure GL cleanup occurs before we return. - GeckoAppShell.sendEventToGeckoSync(e); - } else { - GeckoAppShell.sendEventToGecko(e); - } - } - - public Bitmap getSoftwareDrawBitmap() { - if (mSoftwareBitmap == null || - mSoftwareBitmap.getHeight() != mHeight || - mSoftwareBitmap.getWidth() != mWidth) { - mSoftwareBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.RGB_565); - } - - mDrawMode = DRAW_2D; - return mSoftwareBitmap; - } - - public ByteBuffer getSoftwareDrawBuffer() { - // We store pixels in 565 format, so two bytes per pixel (explaining - // the * 2 in the following check/allocation) - if (mSoftwareBuffer == null || - mSoftwareBuffer.capacity() != (mWidth * mHeight * 2)) { - mSoftwareBuffer = ByteBuffer.allocateDirect(mWidth * mHeight * 2); - } - - if (mSoftwareBufferCopy == null || - mSoftwareBufferCopy.getHeight() != mHeight || - mSoftwareBufferCopy.getWidth() != mWidth) { - mSoftwareBufferCopy = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.RGB_565); - } - - mDrawMode = DRAW_2D; - return mSoftwareBuffer; - } - - public Surface getSurface() { - return getHolder().getSurface(); - } - - /* - * Called on Gecko thread - */ - - public static final int DRAW_ERROR = 0; - public static final int DRAW_GLES_2 = 1; - public static final int DRAW_2D = 2; - // Drawing is disable when the surface buffer - // has changed size but we haven't yet processed the - // resize event. - public static final int DRAW_DISABLED = 3; - - public int beginDrawing() { - - if (mInDrawing) { - Log.e(LOG_FILE_NAME, "!! Recursive beginDrawing call!"); - return DRAW_ERROR; - } - - // Once we drawn our first frame after resize we can ignore - // the other draw events until we handle the resize events. - if (mAbortDraw) { - return DRAW_DISABLED; - } - - /* Grab the lock, which we'll hold while we're drawing. - * It gets released in endDrawing(), and is also used in surfaceChanged - * to make sure that we don't change our surface details while - * we're in the middle of drawing (and especially in the middle of - * executing beginDrawing/endDrawing). - * - * We might not need to hold this lock in between - * beginDrawing/endDrawing, and might just be able to make - * surfaceChanged, beginDrawing, and endDrawing synchronized, - * but this way is safer for now. - */ - mSurfaceLock.lock(); - - if (!mSurfaceValid) { - Log.e(LOG_FILE_NAME, "!! Surface not valid"); - mSurfaceLock.unlock(); - return DRAW_ERROR; - } - - mInDrawing = true; - mDrawMode = DRAW_GLES_2; - return DRAW_GLES_2; - } - - public void endDrawing() { - if (!mInDrawing) { - Log.e(LOG_FILE_NAME, "!! endDrawing without beginDrawing!"); - return; - } - - if (mDrawSingleFrame) - mAbortDraw = true; - - try { - if (!mSurfaceValid) { - Log.e(LOG_FILE_NAME, "!! endDrawing with false mSurfaceValid"); - return; - } - } finally { - mInDrawing = false; - - if (!mSurfaceLock.isHeldByCurrentThread()) - Log.e(LOG_FILE_NAME, "!! endDrawing while mSurfaceLock not held by current thread!"); - - mSurfaceLock.unlock(); - } - } - - /* How this works: - * Whenever we want to draw, we want to be sure that we do not lock - * the canvas unless we're sure we can draw. Locking the canvas clears - * the canvas to black in most cases, causing a black flash. - * At the same time, the surface can resize/disappear at any moment - * unless the canvas is locked. - * Draws originate from a different thread so the surface could change - * at any moment while we try to draw until we lock the canvas. - * - * Also, never try to lock the canvas while holding the surface lock - * unless you're in SurfaceChanged, in which case the canvas was already - * locked. Surface lock -> Canvas lock will lead to AB-BA deadlocks. - */ - public void draw2D(Bitmap bitmap, int width, int height) { - // mSurfaceLock ensures that we get mSyncDraw/mSoftwareBitmap/etc. - // set correctly before determining whether we should do a sync draw - Log.e(LOG_FILE_NAME, "!!! draw2d1 !!!"); - mSurfaceLock.lock(); - try { - if (mSyncDraw) { - if (bitmap != mSoftwareBitmap || width != mWidth || height != mHeight) - return; - mSyncDraw = false; - try { - mSyncDraws.put(bitmap); - } catch (InterruptedException ie) { - Log.e(LOG_FILE_NAME, "!! Threw exception while getting sync draws queue: ", ie); - } - return; - } - } finally { - mSurfaceLock.unlock(); - } - - draw(getHolder(), bitmap); - } - - public void draw2D(ByteBuffer buffer, int stride) { - Log.e(LOG_FILE_NAME, "!!! draw2d2 !!!"); - mSurfaceLock.lock(); - try { - if (mSyncDraw) { - if (buffer != mSoftwareBuffer || stride != (mWidth * 2)) - return; - mSyncDraw = false; - try { - mSyncDraws.put(buffer); - } catch (InterruptedException ie) { - Log.e(LOG_FILE_NAME, "!! Threw exception while getting sync bitmaps queue: ", ie); - } - return; - } - } finally { - mSurfaceLock.unlock(); - } - - draw(getHolder(), buffer); - } - - @Override - public boolean onCheckIsTextEditor () { - return false; - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - outAttrs.inputType = InputType.TYPE_CLASS_TEXT; - outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE; - outAttrs.actionLabel = null; - mKeyListener = TextKeyListener.getInstance(); - - if (mIMEState == IME_STATE_PASSWORD) - outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD; - else if (mIMETypeHint.equalsIgnoreCase("url")) - outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI; - else if (mIMETypeHint.equalsIgnoreCase("email")) - outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; - else if (mIMETypeHint.equalsIgnoreCase("search")) - outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; - else if (mIMETypeHint.equalsIgnoreCase("tel")) - outAttrs.inputType = InputType.TYPE_CLASS_PHONE; - else if (mIMETypeHint.equalsIgnoreCase("number") || - mIMETypeHint.equalsIgnoreCase("range")) - outAttrs.inputType = InputType.TYPE_CLASS_NUMBER; - else if (mIMETypeHint.equalsIgnoreCase("datetime") || - mIMETypeHint.equalsIgnoreCase("datetime-local")) - outAttrs.inputType = InputType.TYPE_CLASS_DATETIME | - InputType.TYPE_DATETIME_VARIATION_NORMAL; - else if (mIMETypeHint.equalsIgnoreCase("date")) - outAttrs.inputType = InputType.TYPE_CLASS_DATETIME | - InputType.TYPE_DATETIME_VARIATION_DATE; - else if (mIMETypeHint.equalsIgnoreCase("time")) - outAttrs.inputType = InputType.TYPE_CLASS_DATETIME | - InputType.TYPE_DATETIME_VARIATION_TIME; - - if (mIMEActionHint.equalsIgnoreCase("go")) - outAttrs.imeOptions = EditorInfo.IME_ACTION_GO; - else if (mIMEActionHint.equalsIgnoreCase("done")) - outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; - else if (mIMEActionHint.equalsIgnoreCase("next")) - outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT; - else if (mIMEActionHint.equalsIgnoreCase("search")) - outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; - else if (mIMEActionHint.equalsIgnoreCase("send")) - outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND; - else if (mIMEActionHint != null && mIMEActionHint.length() != 0) - outAttrs.actionLabel = mIMEActionHint; - - if (mIMELandscapeFS == false) - outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI; - - inputConnection.reset(); - return inputConnection; - } - - public void setEditable(String contents) - { - mEditable.removeSpan(inputConnection); - mEditable.replace(0, mEditable.length(), contents); - mEditable.setSpan(inputConnection, 0, contents.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - Selection.setSelection(mEditable, contents.length()); - } - - public void initEditable(String contents) - { - mEditable = mEditableFactory.newEditable(contents); - mEditable.setSpan(inputConnection, 0, contents.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - Selection.setSelection(mEditable, contents.length()); - } - - // accelerometer - public void onAccuracyChanged(Sensor sensor, int accuracy) - { - } - - public void onSensorChanged(SensorEvent event) - { - GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); - } - - private class GeocoderTask extends AsyncTask { - protected Void doInBackground(Location... location) { - try { - List
addresses = mGeocoder.getFromLocation(location[0].getLatitude(), - location[0].getLongitude(), 1); - // grab the first address. in the future, - // may want to expose multiple, or filter - // for best. - mLastGeoAddress = addresses.get(0); - GeckoAppShell.sendEventToGecko(new GeckoEvent(location[0], mLastGeoAddress)); - } catch (Exception e) { - Log.w(LOG_FILE_NAME, "GeocoderTask "+e); - } - return null; - } - } - - // geolocation - public void onLocationChanged(Location location) - { - if (mGeocoder == null) - mGeocoder = new Geocoder(getContext(), Locale.getDefault()); - - if (mLastGeoAddress == null) { - new GeocoderTask().execute(location); - } - else { - float[] results = new float[1]; - Location.distanceBetween(location.getLatitude(), - location.getLongitude(), - mLastGeoAddress.getLatitude(), - mLastGeoAddress.getLongitude(), - results); - // pfm value. don't want to slam the - // geocoder with very similar values, so - // only call after about 100m - if (results[0] > 100) - new GeocoderTask().execute(location); - } - - GeckoAppShell.sendEventToGecko(new GeckoEvent(location, mLastGeoAddress)); - } - - public void onProviderDisabled(String provider) - { - } - - public void onProviderEnabled(String provider) - { - } - - public void onStatusChanged(String provider, int status, Bundle extras) - { - } - - // event stuff - public boolean onTouchEvent(MotionEvent event) { - requestFocus(FOCUS_UP, null); - GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); - return gestureScanner.onTouchEvent(event); - } - - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - if (event.isSystem()) - return super.onKeyPreIme(keyCode, event); - - switch (event.getAction()) { - case KeyEvent.ACTION_DOWN: - return processKeyDown(keyCode, event, true); - case KeyEvent.ACTION_UP: - return processKeyUp(keyCode, event, true); - case KeyEvent.ACTION_MULTIPLE: - return onKeyMultiple(keyCode, event.getRepeatCount(), event); - } - return super.onKeyPreIme(keyCode, event); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return processKeyDown(keyCode, event, false); - } - - private boolean processKeyDown(int keyCode, KeyEvent event, boolean isPreIme) { - switch (keyCode) { - case KeyEvent.KEYCODE_MENU: - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_VOLUME_UP: - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_SEARCH: - return false; - case KeyEvent.KEYCODE_DEL: - // See comments in GeckoInputConnection.onKeyDel - if (inputConnection != null && - inputConnection.onKeyDel()) { - return true; - } - break; - case KeyEvent.KEYCODE_ENTER: - if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 && - mIMEActionHint.equalsIgnoreCase("next")) - event = new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB); - break; - default: - break; - } - - if (isPreIme && mIMEState != IME_STATE_DISABLED && - (event.getMetaState() & KeyEvent.META_ALT_ON) == 0) - // Let active IME process pre-IME key events - return false; - - // KeyListener returns true if it handled the event for us. - if (mIMEState == IME_STATE_DISABLED || - keyCode == KeyEvent.KEYCODE_ENTER || - keyCode == KeyEvent.KEYCODE_DEL || - (event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 || - !mKeyListener.onKeyDown(this, mEditable, keyCode, event)) - GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); - return true; - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - return processKeyUp(keyCode, event, false); - } - - private boolean processKeyUp(int keyCode, KeyEvent event, boolean isPreIme) { - switch (keyCode) { - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_SEARCH: - case KeyEvent.KEYCODE_MENU: - return false; - default: - break; - } - - if (isPreIme && mIMEState != IME_STATE_DISABLED && - (event.getMetaState() & KeyEvent.META_ALT_ON) == 0) - // Let active IME process pre-IME key events - return false; - - if (mIMEState == IME_STATE_DISABLED || - keyCode == KeyEvent.KEYCODE_ENTER || - keyCode == KeyEvent.KEYCODE_DEL || - (event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 || - !mKeyListener.onKeyUp(this, mEditable, keyCode, event)) - GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); - return true; - } - - @Override - public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { - GeckoAppShell.sendEventToGecko(new GeckoEvent(event)); - return true; - } - - @Override - public boolean onKeyLongPress(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_MENU: - InputMethodManager imm = (InputMethodManager) - getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.toggleSoftInputFromWindow(getWindowToken(), - imm.SHOW_FORCED, 0); - return true; - default: - break; - } - return false; - } - - // Is this surface valid for drawing into? - boolean mSurfaceValid; - - // Are we actively between beginDrawing/endDrawing? - boolean mInDrawing; - - // Used to finish the current buffer before changing the surface size - boolean mDrawSingleFrame = false; - boolean mAbortDraw = false; - - // Are we waiting for a buffer to draw in surfaceChanged? - boolean mSyncDraw; - - // True if gecko requests a buffer - int mDrawMode; - - static boolean mShowingLoadScreen = true; - - // let's not change stuff around while we're in the middle of - // starting drawing, ending drawing, or changing surface - // characteristics - ReentrantLock mSurfaceLock; - - // Surface format, from surfaceChanged. Largely - // useless. - int mFormat; - - // the dimensions of the surface - int mWidth; - int mHeight; - - // the dimensions of the buffer we're using for drawing, - // that is the software buffer or the EGLSurface - int mBufferWidth; - int mBufferHeight; - - // IME stuff - public static final int IME_STATE_DISABLED = 0; - public static final int IME_STATE_ENABLED = 1; - public static final int IME_STATE_PASSWORD = 2; - public static final int IME_STATE_PLUGIN = 3; - - GeckoInputConnection inputConnection; - GeckoGestureDetector gestureScanner; - KeyListener mKeyListener; - Editable mEditable; - Editable.Factory mEditableFactory; - int mIMEState; - String mIMETypeHint; - String mIMEActionHint; - boolean mIMELandscapeFS; - - // Software rendering - Bitmap mSoftwareBitmap; - ByteBuffer mSoftwareBuffer; - Bitmap mSoftwareBufferCopy; - Bitmap mStartupBitmap; - - Geocoder mGeocoder; - Address mLastGeoAddress; - - final SynchronousQueue mSyncDraws = new SynchronousQueue(); -} - diff --git a/embedding/android/Makefile.in b/embedding/android/Makefile.in index 74e209f59d8b..6429d8171a65 100644 --- a/embedding/android/Makefile.in +++ b/embedding/android/Makefile.in @@ -61,8 +61,6 @@ JAVAFILES = \ GeckoEventListener.java \ GeckoInputConnection.java \ GeckoPreferences.java \ - GeckoSurfaceView.java \ - GeckoGestureDetector.java \ GlobalHistory.java \ PromptService.java \ SurfaceInfo.java \ @@ -70,6 +68,29 @@ JAVAFILES = \ Tabs.java \ TabsTray.java \ GeckoBatteryManager.java \ + gfx/BufferedCairoImage.java \ + gfx/CairoImage.java \ + gfx/CairoUtils.java \ + gfx/FloatPoint.java \ + gfx/FloatRect.java \ + gfx/GeckoSoftwareLayerClient.java \ + gfx/InputConnectionHandler.java \ + gfx/IntPoint.java \ + gfx/IntRect.java \ + gfx/IntSize.java \ + gfx/Layer.java \ + gfx/LayerClient.java \ + gfx/LayerController.java \ + gfx/LayerRenderer.java \ + gfx/LayerView.java \ + gfx/NinePatchTileLayer.java \ + gfx/PlaceholderLayerClient.java \ + gfx/SingleTileLayer.java \ + gfx/TextureReaper.java \ + gfx/TextLayer.java \ + gfx/TileLayer.java \ + ui/PanZoomController.java \ + ui/ViewportController.java \ $(NULL) PROCESSEDJAVAFILES = \ @@ -289,6 +310,8 @@ MOZ_ANDROID_DRAWABLES += embedding/android/resources/drawable/address_bar_bg.xml embedding/android/resources/drawable/tabs_plus.png \ embedding/android/resources/drawable/tabs_menu.png \ embedding/android/resources/drawable/tabs_tray_bg.9.png \ + embedding/android/resources/drawable/checkerboard.png \ + embedding/android/resources/drawable/shadow.png \ $(NULL) diff --git a/embedding/android/gfx/BufferedCairoImage.java b/embedding/android/gfx/BufferedCairoImage.java new file mode 100644 index 000000000000..8c18d7567a82 --- /dev/null +++ b/embedding/android/gfx/BufferedCairoImage.java @@ -0,0 +1,73 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.CairoImage; +import org.mozilla.gecko.gfx.CairoUtils; +import android.graphics.Bitmap; +import java.nio.ByteBuffer; + +/** A Cairo image that simply saves a buffer of pixel data. */ +public class BufferedCairoImage extends CairoImage { + private ByteBuffer mBuffer; + private int mWidth, mHeight, mFormat; + + /** Creates a buffered Cairo image from a byte buffer. */ + public BufferedCairoImage(ByteBuffer inBuffer, int inWidth, int inHeight, int inFormat) { + mBuffer = inBuffer; mWidth = inWidth; mHeight = inHeight; mFormat = inFormat; + } + + /** Creates a buffered Cairo image from an Android bitmap. */ + public BufferedCairoImage(Bitmap bitmap) { + mFormat = CairoUtils.bitmapConfigToCairoFormat(bitmap.getConfig()); + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + mBuffer = ByteBuffer.allocateDirect(mWidth * mHeight * 4); + bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + } + + @Override + public ByteBuffer lockBuffer() { return mBuffer; } + @Override + public int getWidth() { return mWidth; } + @Override + public int getHeight() { return mHeight; } + @Override + public int getFormat() { return mFormat; } +} + diff --git a/embedding/android/gfx/CairoImage.java b/embedding/android/gfx/CairoImage.java new file mode 100644 index 000000000000..7bf61b9d815c --- /dev/null +++ b/embedding/android/gfx/CairoImage.java @@ -0,0 +1,60 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import java.nio.ByteBuffer; + +/* + * A bitmap with pixel data in one of the formats that Cairo understands. + */ +public abstract class CairoImage { + public abstract ByteBuffer lockBuffer(); + public void unlockBuffer() { /* By default, a no-op. */ } + + public abstract int getWidth(); + public abstract int getHeight(); + public abstract int getFormat(); + + public static final int FORMAT_INVALID = -1; + public static final int FORMAT_ARGB32 = 0; + public static final int FORMAT_RGB24 = 1; + public static final int FORMAT_A8 = 2; + public static final int FORMAT_A1 = 3; + public static final int FORMAT_RGB16_565 = 4; +} + diff --git a/embedding/android/gfx/CairoUtils.java b/embedding/android/gfx/CairoUtils.java new file mode 100644 index 000000000000..8ef0054687f2 --- /dev/null +++ b/embedding/android/gfx/CairoUtils.java @@ -0,0 +1,120 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.CairoImage; +import android.graphics.Bitmap; +import javax.microedition.khronos.opengles.GL10; + +/** + * Utility methods useful when displaying Cairo bitmaps using OpenGL ES. + */ +public class CairoUtils { + private CairoUtils() { /* Don't call me. */ } + + public static int cairoFormatToGLInternalFormat(int cairoFormat) { + switch (cairoFormat) { + case CairoImage.FORMAT_ARGB32: + return GL10.GL_RGBA; + case CairoImage.FORMAT_RGB24: + case CairoImage.FORMAT_RGB16_565: + return GL10.GL_RGB; + case CairoImage.FORMAT_A8: + case CairoImage.FORMAT_A1: + throw new RuntimeException("Cairo FORMAT_A1 and FORMAT_A8 unsupported"); + default: + throw new RuntimeException("Unknown Cairo format"); + } + } + + public static int cairoFormatToGLFormat(int cairoFormat) { + switch (cairoFormat) { + case CairoImage.FORMAT_ARGB32: + return GL10.GL_RGBA; + case CairoImage.FORMAT_RGB24: + case CairoImage.FORMAT_RGB16_565: + return GL10.GL_RGB; + case CairoImage.FORMAT_A8: + case CairoImage.FORMAT_A1: + return GL10.GL_ALPHA; + default: + throw new RuntimeException("Unknown Cairo format"); + } + } + + public static int cairoFormatToGLType(int cairoFormat) { + switch (cairoFormat) { + case CairoImage.FORMAT_ARGB32: + case CairoImage.FORMAT_RGB24: + case CairoImage.FORMAT_A8: + return GL10.GL_UNSIGNED_BYTE; + case CairoImage.FORMAT_A1: + throw new RuntimeException("Cairo FORMAT_A1 unsupported in Android OpenGL"); + case CairoImage.FORMAT_RGB16_565: + return GL10.GL_UNSIGNED_SHORT_5_6_5; + default: + throw new RuntimeException("Unknown Cairo format"); + } + } + + public static int bitsPerPixelForCairoFormat(int cairoFormat) { + switch (cairoFormat) { + case CairoImage.FORMAT_A1: return 1; + case CairoImage.FORMAT_A8: return 8; + case CairoImage.FORMAT_RGB16_565: return 16; + case CairoImage.FORMAT_RGB24: return 24; + case CairoImage.FORMAT_ARGB32: return 32; + default: + throw new RuntimeException("Unknown Cairo format"); + } + } + + public static int bitmapConfigToCairoFormat(Bitmap.Config config) { + if (config == null) + return CairoImage.FORMAT_ARGB32; /* Droid Pro fix. */ + + switch (config) { + case ALPHA_8: return CairoImage.FORMAT_A8; + case ARGB_4444: throw new RuntimeException("ARGB_444 unsupported"); + case ARGB_8888: return CairoImage.FORMAT_ARGB32; + case RGB_565: return CairoImage.FORMAT_RGB16_565; + default: throw new RuntimeException("Unknown Skia bitmap config"); + } + } +} + diff --git a/embedding/android/gfx/FloatPoint.java b/embedding/android/gfx/FloatPoint.java new file mode 100644 index 000000000000..5775fa0d28be --- /dev/null +++ b/embedding/android/gfx/FloatPoint.java @@ -0,0 +1,71 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.IntPoint; + +public class FloatPoint { + public final float x, y; + + public FloatPoint(float inX, float inY) { + x = inX; y = inY; + } + + public FloatPoint(IntPoint intPoint) { + x = intPoint.x; y = intPoint.y; + } + + @Override + public String toString() { + return "(" + x + ", " + y + ")"; + } + + public FloatPoint add(FloatPoint other) { + return new FloatPoint(x + other.x, y + other.y); + } + + public FloatPoint subtract(FloatPoint other) { + return new FloatPoint(x - other.x, y - other.y); + } + + public FloatPoint scale(float factor) { + return new FloatPoint(x * factor, y * factor); + } +} + + diff --git a/embedding/android/gfx/FloatRect.java b/embedding/android/gfx/FloatRect.java new file mode 100644 index 000000000000..efa99ef15b75 --- /dev/null +++ b/embedding/android/gfx/FloatRect.java @@ -0,0 +1,98 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.FloatPoint; +import org.mozilla.gecko.gfx.IntRect; + +public class FloatRect { + public final float x, y, width, height; + + public FloatRect(float inX, float inY, float inWidth, float inHeight) { + x = inX; y = inY; width = inWidth; height = inHeight; + } + + public FloatRect(IntRect intRect) { + x = intRect.x; y = intRect.y; width = intRect.width; height = intRect.height; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof FloatRect)) + return false; + FloatRect otherRect = (FloatRect)other; + return x == otherRect.x && y == otherRect.y && + width == otherRect.width && height == otherRect.height; + } + + public float getRight() { return x + width; } + public float getBottom() { return y + height; } + + public FloatPoint getOrigin() { return new FloatPoint(x, y); } + public FloatPoint getCenter() { return new FloatPoint(x + width / 2, y + height / 2); } + + /** Returns the intersection of this rectangle with another rectangle. */ + public FloatRect intersect(FloatRect other) { + float left = Math.max(x, other.x); + float top = Math.max(y, other.y); + float right = Math.min(getRight(), other.getRight()); + float bottom = Math.min(getBottom(), other.getBottom()); + return new FloatRect(left, top, Math.max(right - left, 0), Math.max(bottom - top, 0)); + } + + /** Returns true if and only if the given rectangle is fully enclosed within this one. */ + public boolean contains(FloatRect other) { + return x <= other.x && y <= other.y && + getRight() >= other.getRight() && + getBottom() >= other.getBottom(); + } + + /** Contracts a rectangle by the given number of units in each direction, from the center. */ + public FloatRect contract(float lessWidth, float lessHeight) { + float halfWidth = width / 2.0f - lessWidth, halfHeight = height / 2.0f - lessHeight; + FloatPoint center = getCenter(); + return new FloatRect(center.x - halfWidth, center.y - halfHeight, + halfWidth * 2.0f, halfHeight * 2.0f); + } + + /** Scales all four dimensions of this rectangle by the given factor. */ + public FloatRect scaleAll(float factor) { + return new FloatRect(x * factor, y * factor, width * factor, height * factor); + } +} + diff --git a/embedding/android/gfx/GeckoSoftwareLayerClient.java b/embedding/android/gfx/GeckoSoftwareLayerClient.java new file mode 100644 index 000000000000..d5db9982e711 --- /dev/null +++ b/embedding/android/gfx/GeckoSoftwareLayerClient.java @@ -0,0 +1,273 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.CairoImage; +import org.mozilla.gecko.gfx.FloatRect; +import org.mozilla.gecko.gfx.IntRect; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerClient; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.LayerRenderer; +import org.mozilla.gecko.gfx.SingleTileLayer; +import org.mozilla.gecko.ui.ViewportController; +import org.mozilla.gecko.GeckoApp; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; +import android.content.Context; +import android.graphics.Point; +import android.util.Log; +import java.nio.ByteBuffer; +import java.util.concurrent.Semaphore; +import java.util.Timer; +import java.util.TimerTask; + +/** + * Transfers a software-rendered Gecko to an ImageLayer so that it can be rendered by our + * compositor. + * + * TODO: Throttle down Gecko's priority when we pan and zoom. + */ +public class GeckoSoftwareLayerClient extends LayerClient { + private Context mContext; + private int mWidth, mHeight, mFormat; + private ByteBuffer mBuffer; + private Semaphore mBufferSemaphore; + private SingleTileLayer mTileLayer; + private ViewportController mViewportController; + + private FloatRect mGeckoVisibleRect; + /* The viewport rect that Gecko is currently displaying. */ + + private IntRect mJSPanningToRect; + /* The rect that we just told chrome JavaScript to pan to. */ + + private boolean mWaitingForJSPanZoom; + /* This will be set to true if we are waiting on the chrome JavaScript to finish panning or + * zooming before we can render. */ + + private CairoImage mCairoImage; + + /* The initial page width and height that we use before a page is loaded. */ + private static final int PAGE_WIDTH = 980; /* Matches MobileSafari. */ + private static final int PAGE_HEIGHT = 1500; + + public GeckoSoftwareLayerClient(Context context) { + mContext = context; + + mViewportController = new ViewportController(new IntSize(PAGE_WIDTH, PAGE_HEIGHT), + new FloatRect(0, 0, 1, 1)); + + mWidth = LayerController.TILE_WIDTH; + mHeight = LayerController.TILE_HEIGHT; + mFormat = CairoImage.FORMAT_RGB16_565; + + mBuffer = ByteBuffer.allocateDirect(mWidth * mHeight * 2); + mBufferSemaphore = new Semaphore(1); + + mWaitingForJSPanZoom = false; + + mCairoImage = new CairoImage() { + @Override + public ByteBuffer lockBuffer() { + try { + mBufferSemaphore.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return mBuffer; + } + @Override + public void unlockBuffer() { + mBufferSemaphore.release(); + } + @Override + public int getWidth() { return mWidth; } + @Override + public int getHeight() { return mHeight; } + @Override + public int getFormat() { return mFormat; } + }; + + mTileLayer = new SingleTileLayer(); + } + + /** Attaches the root layer to the layer controller so that Gecko appears. */ + @Override + public void init() { + getLayerController().setRoot(mTileLayer); + } + + public void beginDrawing() { + /* no-op */ + } + + /* + * TODO: Would be cleaner if this took an android.graphics.Rect instead, but that would require + * a little more JNI magic. + */ + public void endDrawing(int x, int y, int width, int height) { + LayerController controller = getLayerController(); + //controller.unzoom(); /* FIXME */ + controller.notifyViewOfGeometryChange(); + + mViewportController.setVisibleRect(mGeckoVisibleRect); + + if (mGeckoVisibleRect != null) { + FloatRect layerRect = mViewportController.untransformVisibleRect(mGeckoVisibleRect, + getPageSize()); + mTileLayer.origin = layerRect.getOrigin(); + } + + repaint(new IntRect(x, y, width, height)); + } + + private void repaint(IntRect rect) { + mTileLayer.paintSubimage(mCairoImage, rect); + } + + /** Called whenever the chrome JS finishes panning or zooming to some location. */ + public void jsPanZoomCompleted(IntRect rect) { + mGeckoVisibleRect = new FloatRect(rect); + if (mWaitingForJSPanZoom) + render(); + } + + /** + * Acquires a lock on the back buffer and returns it, blocking until it's unlocked. This + * function is for Gecko to use. + */ + public ByteBuffer lockBuffer() { + try { + mBufferSemaphore.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return mBuffer; + } + + /** + * Releases the lock on the back buffer. After this call, it is forbidden for Gecko to touch + * the buffer. This function is, again, for Gecko to use. + */ + public void unlockBuffer() { + mBufferSemaphore.release(); + } + + /** Called whenever the page changes size. */ + public void setPageSize(IntSize pageSize) { + mViewportController.setPageSize(pageSize); + getLayerController().setPageSize(pageSize); + } + + @Override + public void geometryChanged() { + mViewportController.setVisibleRect(getTransformedVisibleRect()); + render(); + } + + @Override + public IntSize getPageSize() { return mViewportController.getPageSize(); } + + @Override + public void render() { + LayerController layerController = getLayerController(); + FloatRect visibleRect = layerController.getVisibleRect(); + FloatRect tileRect = mViewportController.widenRect(visibleRect); + tileRect = mViewportController.clampRect(tileRect); + + IntSize pageSize = layerController.getPageSize(); + FloatRect viewportRect = mViewportController.transformVisibleRect(tileRect, pageSize); + + /* Prevent null pointer exceptions at the start. */ + if (mGeckoVisibleRect == null) + mGeckoVisibleRect = viewportRect; + + if (!getLayerController().getRedrawHint()) + return; + + /* If Gecko's visible rect is the same as our visible rect, then we can actually kick off a + * draw event. */ + if (mGeckoVisibleRect.equals(viewportRect)) { + mWaitingForJSPanZoom = false; + mJSPanningToRect = null; + GeckoAppShell.scheduleRedraw(); + return; + } + + /* Otherwise, we need to get Gecko's visible rect equal to our visible rect before we can + * safely draw. If we're just waiting for chrome JavaScript to catch up, we do nothing. + * This check avoids bombarding the chrome JavaScript with messages. */ + IntRect panToRect = new IntRect((int)Math.round(viewportRect.x), + (int)Math.round(viewportRect.y), + LayerController.TILE_WIDTH, + LayerController.TILE_HEIGHT); + + if (mWaitingForJSPanZoom && mJSPanningToRect != null && + mJSPanningToRect.equals(panToRect)) { + return; + } + + /* We send Gecko a message telling it to move its visible rect to the appropriate spot and + * set a flag to remind us to try the redraw again. */ + + GeckoAppShell.sendEventToGecko(new GeckoEvent("PanZoom:PanZoom", + "{\"x\": " + panToRect.x + ", \"y\": " + panToRect.y + + ", \"width\": " + panToRect.width + ", \"height\": " + panToRect.height + + ", \"zoomFactor\": " + getZoomFactor() + "}")); + + mJSPanningToRect = panToRect; + mWaitingForJSPanZoom = true; + } + + /* Returns the dimensions of the box in page coordinates that the user is viewing. */ + private FloatRect getTransformedVisibleRect() { + LayerController layerController = getLayerController(); + return mViewportController.transformVisibleRect(layerController.getVisibleRect(), + layerController.getPageSize()); + } + + private float getZoomFactor() { + return 1.0f; // FIXME + /*LayerController layerController = getLayerController(); + return mViewportController.getZoomFactor(layerController.getVisibleRect(), + layerController.getPageSize(), + layerController.getScreenSize());*/ + } +} + diff --git a/embedding/android/gfx/InputConnectionHandler.java b/embedding/android/gfx/InputConnectionHandler.java new file mode 100644 index 000000000000..6fef53fb0535 --- /dev/null +++ b/embedding/android/gfx/InputConnectionHandler.java @@ -0,0 +1,15 @@ +package org.mozilla.gecko.gfx; + +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.KeyEvent; + +public interface InputConnectionHandler +{ + InputConnection onCreateInputConnection(EditorInfo outAttrs); + boolean onKeyPreIme(int keyCode, KeyEvent event); + boolean onKeyDown(int keyCode, KeyEvent event); + boolean onKeyLongPress(int keyCode, KeyEvent event); + boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event); + boolean onKeyUp(int keyCode, KeyEvent event); +} diff --git a/embedding/android/gfx/IntPoint.java b/embedding/android/gfx/IntPoint.java new file mode 100644 index 000000000000..6e3848c87c66 --- /dev/null +++ b/embedding/android/gfx/IntPoint.java @@ -0,0 +1,60 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +public class IntPoint { + public final int x, y; + + public IntPoint(int inX, int inY) { x = inX; y = inY; } + + @Override + public String toString() { return "(" + x + ", " + y + ")"; } + + /** Returns the result of adding the given point to this point. */ + public IntPoint add(IntPoint other) { return new IntPoint(x + other.x, y + other.y); } + + /** Returns the result of subtracting the given point from this point. */ + public IntPoint subtract(IntPoint other) { return new IntPoint(x - other.x, y - other.y); } + + /** Returns the result of multiplying both components by the given scalar. */ + public IntPoint scale(float scale) { + return new IntPoint((int)Math.round((float)x * scale), (int)Math.round((float)y * scale)); + } +} + + diff --git a/embedding/android/gfx/IntRect.java b/embedding/android/gfx/IntRect.java new file mode 100644 index 000000000000..1ce9614a6679 --- /dev/null +++ b/embedding/android/gfx/IntRect.java @@ -0,0 +1,94 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.IntPoint; +import org.json.JSONException; +import org.json.JSONObject; + +public class IntRect implements Cloneable { + public final int x, y, width, height; + + public IntRect(int inX, int inY, int inWidth, int inHeight) { + x = inX; y = inY; width = inWidth; height = inHeight; + } + + public IntRect(JSONObject json) { + try { + x = json.getInt("x"); + y = json.getInt("y"); + width = json.getInt("width"); + height = json.getInt("height"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object clone() { return new IntRect(x, y, width, height); } + + @Override + public boolean equals(Object other) { + if (!(other instanceof IntRect)) + return false; + IntRect otherRect = (IntRect)other; + return x == otherRect.x && y == otherRect.y && width == otherRect.width && + height == otherRect.height; + } + + @Override + public String toString() { return "(" + x + "," + y + "," + width + "," + height + ")"; } + + public IntPoint getOrigin() { return new IntPoint(x, y); } + public IntPoint getCenter() { return new IntPoint(x + width / 2, y + height / 2); } + + public int getRight() { return x + width; } + public int getBottom() { return y + height; } + + /** Contracts a rectangle by the given number of units in each direction, from the center. */ + public IntRect contract(int lessWidth, int lessHeight) { + float halfWidth = width / 2.0f - lessWidth, halfHeight = height / 2.0f - lessHeight; + IntPoint center = getCenter(); + return new IntRect((int)Math.round((float)center.x - halfWidth), + (int)Math.round((float)center.y - halfHeight), + (int)Math.round(halfWidth * 2.0f), + (int)Math.round(halfHeight * 2.0f)); + } +} + + diff --git a/embedding/android/GeckoGestureDetector.java b/embedding/android/gfx/IntSize.java similarity index 54% rename from embedding/android/GeckoGestureDetector.java rename to embedding/android/gfx/IntSize.java index 1eaae7cb4439..d84788b40841 100644 --- a/embedding/android/GeckoGestureDetector.java +++ b/embedding/android/gfx/IntSize.java @@ -15,11 +15,11 @@ * The Original Code is Mozilla Android code. * * The Initial Developer of the Original Code is Mozilla Foundation. - * Portions created by the Initial Developer are Copyright (C) 2011 + * Portions created by the Initial Developer are Copyright (C) 2009-2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): - * Wes Johnston + * Patrick Walton * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -35,62 +35,31 @@ * * ***** END LICENSE BLOCK ***** */ -package org.mozilla.gecko; +package org.mozilla.gecko.gfx; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.content.Context; -import android.view.View; -import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; -import android.util.Log; -class GeckoGestureDetector implements GestureDetector.OnGestureListener { - private GestureDetector mDetector; - private static final String LOG_FILE_NAME = "GeckoGestureDetector"; - public GeckoGestureDetector(Context aContext) { - mDetector = new GestureDetector(aContext, this); - } +public class IntSize { + public final int width, height; - public boolean onTouchEvent(MotionEvent event) { - return mDetector.onTouchEvent(event); - } + public IntSize(int inWidth, int inHeight) { width = inWidth; height = inHeight; } - @Override - public boolean onDown(MotionEvent e) { - return true; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - return true; - } - - @Override - public void onLongPress(MotionEvent motionEvent) { - JSONObject ret = new JSONObject(); + public IntSize(JSONObject json) { try { - ret.put("x", motionEvent.getX()); - ret.put("y", motionEvent.getY()); - } catch(Exception ex) { - Log.w(LOG_FILE_NAME, "Error building return: " + ex); + width = json.getInt("width"); + height = json.getInt("height"); + } catch (JSONException e) { + throw new RuntimeException(e); } - - GeckoEvent e = new GeckoEvent("Gesture:LongPress", ret.toString()); - GeckoAppShell.sendEventToGecko(e); } @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - return true; - } + public String toString() { return "(" + width + "," + height + ")"; } - @Override - public void onShowPress(MotionEvent e) { - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - return true; + public IntSize scale(float factor) { + return new IntSize((int)Math.round(width * factor), + (int)Math.round(height * factor)); } } + diff --git a/embedding/android/gfx/Layer.java b/embedding/android/gfx/Layer.java new file mode 100644 index 000000000000..b8cb94dbef41 --- /dev/null +++ b/embedding/android/gfx/Layer.java @@ -0,0 +1,65 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.FloatPoint; +import javax.microedition.khronos.opengles.GL10; + +public abstract class Layer { + public FloatPoint origin; + + public Layer() { + origin = new FloatPoint(0.0f, 0.0f); + } + + /** Draws the layer. Automatically applies the translation. */ + public final void draw(GL10 gl) { + gl.glPushMatrix(); + gl.glTranslatef(origin.x, origin.y, 0.0f); + onDraw(gl); + gl.glPopMatrix(); + } + + /** + * Subclasses implement this method to perform drawing. + * + * Invariant: The current matrix mode must be GL_MODELVIEW both before and after this call. + */ + protected abstract void onDraw(GL10 gl); +} + diff --git a/embedding/android/gfx/LayerClient.java b/embedding/android/gfx/LayerClient.java new file mode 100644 index 000000000000..97756f7b0210 --- /dev/null +++ b/embedding/android/gfx/LayerClient.java @@ -0,0 +1,64 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.IntRect; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; + +/** + * A layer client provides tiles and manages other information used by the layer controller. + */ +public abstract class LayerClient { + private LayerController mLayerController; + + public abstract void geometryChanged(); + public abstract IntSize getPageSize(); + + /** Called whenever the page changes size. */ + public abstract void setPageSize(IntSize pageSize); + + public abstract void init(); + protected abstract void render(); + + public LayerController getLayerController() { return mLayerController; } + public void setLayerController(LayerController layerController) { + mLayerController = layerController; + } +} + diff --git a/embedding/android/gfx/LayerController.java b/embedding/android/gfx/LayerController.java new file mode 100644 index 000000000000..f2af34a83b5a --- /dev/null +++ b/embedding/android/gfx/LayerController.java @@ -0,0 +1,288 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.FloatPoint; +import org.mozilla.gecko.gfx.FloatRect; +import org.mozilla.gecko.gfx.IntRect; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.Layer; +import org.mozilla.gecko.gfx.LayerClient; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.ui.PanZoomController; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; +import android.view.MotionEvent; +import android.view.GestureDetector; +import android.view.ScaleGestureDetector; +import android.view.View.OnTouchListener; +import java.util.ArrayList; + +/** + * The layer controller manages a tile that represents the visible page. It does panning and + * zooming natively by delegating to a panning/zooming controller. Touch events can be dispatched + * to a higher-level view. + */ +public class LayerController { + private Layer mRootLayer; /* The root layer. */ + private LayerView mView; /* The main rendering view. */ + private Context mContext; /* The current context. */ + private FloatRect mVisibleRect; /* The current visible region. */ + private IntSize mScreenSize; /* The screen size of the viewport. */ + private IntSize mPageSize; /* The current page size. */ + + private PanZoomController mPanZoomController; + /* + * The panning and zooming controller, which interprets pan and zoom gestures for us and + * updates our visible rect appropriately. + */ + + private OnTouchListener mOnTouchListener; /* The touch listener. */ + private LayerClient mLayerClient; /* The layer client. */ + + public static final int TILE_WIDTH = 1024; + public static final int TILE_HEIGHT = 2048; + /* NB: These must be powers of two due to the OpenGL ES 1.x restriction on NPOT textures. */ + + private static final int DANGER_ZONE_X = 150; + private static final int DANGER_ZONE_Y = 300; + /* If the visible rect is within the danger zone (measured in pixels from each edge of a tile), + * we start aggressively redrawing to minimize checkerboarding. */ + + public LayerController(Context context, LayerClient layerClient) { + mContext = context; + + mVisibleRect = new FloatRect(0.0f, 0.0f, 1.0f, 1.0f); + /* Gets filled in when the surface changes. */ + + mScreenSize = new IntSize(1, 1); + + if (layerClient != null) + setLayerClient(layerClient); + else + mPageSize = new IntSize(LayerController.TILE_WIDTH, LayerController.TILE_HEIGHT); + + mPanZoomController = new PanZoomController(this); + mView = new LayerView(context, this); + } + + public void setRoot(Layer layer) { mRootLayer = layer; } + + public void setLayerClient(LayerClient layerClient) { + mLayerClient = layerClient; + mPageSize = layerClient.getPageSize(); + layerClient.setLayerController(this); + } + + public Layer getRoot() { return mRootLayer; } + public LayerView getView() { return mView; } + public Context getContext() { return mContext; } + public FloatRect getVisibleRect() { return mVisibleRect; } + public IntSize getScreenSize() { return mScreenSize; } + public IntSize getPageSize() { return mPageSize; } + + public Bitmap getCheckerboardPattern() { return getDrawable("checkerboard"); } + public Bitmap getShadowPattern() { return getDrawable("shadow"); } + + public GestureDetector.OnGestureListener getGestureListener() { return mPanZoomController; } + public ScaleGestureDetector.OnScaleGestureListener getScaleGestureListener() { return mPanZoomController; } + + private Bitmap getDrawable(String name) { + Resources resources = mContext.getResources(); + int resourceID = resources.getIdentifier(name, "drawable", mContext.getPackageName()); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + return BitmapFactory.decodeResource(mContext.getResources(), resourceID, options); + } + + /* + * Note that the zoom factor of the layer controller differs from the zoom factor of the layer + * client (i.e. the page). + */ + public float getZoomFactor() { return (float)mScreenSize.width / mVisibleRect.width; } + + /** + * The view calls this to indicate that the screen changed size. + * + * TODO: Refactor this to use an interface. Expose that interface only to the view and not + * to the layer client. That way, the layer client won't be tempted to call this, which might + * result in an infinite loop. + */ + public void setScreenSize(int width, int height) { + float zoomFactor = getZoomFactor(); /* Must come first. */ + + mScreenSize = new IntSize(width, height); + setVisibleRect(mVisibleRect.x, mVisibleRect.y, width / zoomFactor, height / zoomFactor); + + notifyLayerClientOfGeometryChange(); + } + + public void setNeedsDisplay() { + // TODO + } + + public void scrollTo(float x, float y) { + setVisibleRect(x, y, mVisibleRect.width, mVisibleRect.height); + } + + public void setVisibleRect(float x, float y, float width, float height) { + mVisibleRect = new FloatRect(x, y, width, height); + setNeedsDisplay(); + } + + /** + * Sets the zoom factor to 1, adjusting the visible rect accordingly. The Gecko layer client + * calls this function after a zoom has completed and Gecko is done rendering the new visible + * region. + */ + public void unzoom() { + float zoomFactor = getZoomFactor(); + mVisibleRect = new FloatRect(Math.round(mVisibleRect.x * zoomFactor), + Math.round(mVisibleRect.y * zoomFactor), + mScreenSize.width, + mScreenSize.height); + mPageSize = mPageSize.scale(zoomFactor); + setNeedsDisplay(); + } + + public void setPageSize(IntSize size) { + mPageSize = size.scale(getZoomFactor()); + mView.notifyRendererOfPageSizeChange(); + } + + public boolean post(Runnable action) { return mView.post(action); } + + public void setOnTouchListener(OnTouchListener onTouchListener) { + mOnTouchListener = onTouchListener; + } + + /** + * The view as well as the controller itself use this method to notify the layer client that + * the geometry changed. + */ + public void notifyLayerClientOfGeometryChange() { + if (mLayerClient != null) + mLayerClient.geometryChanged(); + } + + // Informs the view and the panning and zooming controller that the geometry changed. + public void notifyViewOfGeometryChange() { + mView.geometryChanged(); + mPanZoomController.geometryChanged(); + } + + /** + * Returns true if this controller is fine with performing a redraw operation or false if it + * would prefer that the action didn't take place. + */ + public boolean getRedrawHint() { + return aboutToCheckerboard(); + } + + private FloatRect getTileRect() { + return new FloatRect(mRootLayer.origin.x, mRootLayer.origin.y, TILE_WIDTH, TILE_HEIGHT); + } + + // Returns true if a checkerboard is about to be visible. + private boolean aboutToCheckerboard() { + IntRect pageRect = new IntRect(0, 0, mPageSize.width, mPageSize.height); + IntRect adjustedPageRect = pageRect.contract(DANGER_ZONE_X, DANGER_ZONE_Y); + FloatRect visiblePageRect = mVisibleRect.intersect(new FloatRect(adjustedPageRect)); + FloatRect adjustedTileRect = getTileRect().contract(DANGER_ZONE_X, DANGER_ZONE_Y); + return !adjustedTileRect.contains(visiblePageRect); + } + + /** Returns the given rect, clamped to the boundaries of a tile. */ + public FloatRect clampRect(FloatRect rect) { + float x = clamp(0, rect.x, mPageSize.width - LayerController.TILE_WIDTH); + float y = clamp(0, rect.y, mPageSize.height - LayerController.TILE_HEIGHT); + return new FloatRect(x, y, rect.width, rect.height); + } + + private float clamp(float min, float value, float max) { + if (max < min) + return min; + return (value < min) ? min : (value > max) ? max : value; + } + + // Returns the coordinates of a tile, scaled by the given factor, centered on the given rect. + private static FloatRect widenRect(FloatRect rect, float scaleFactor) { + FloatPoint center = rect.getCenter(); + float halfTileWidth = TILE_WIDTH * scaleFactor / 2.0f; + float halfTileHeight = TILE_HEIGHT * scaleFactor / 2.0f; + return new FloatRect(center.x - halfTileWidth, center.y - halfTileHeight, + halfTileWidth, halfTileHeight); + } + + /** Returns the coordinates of a tile centered on the given rect. */ + public static FloatRect widenRect(FloatRect rect) { + return widenRect(rect, 1.0f); + } + + /** + * Converts a point from layer view coordinates to layer coordinates. In other words, given a + * point measured in pixels from the top left corner of the layer view, returns the point in + * pixels measured from the top left corner of the root layer, in the coordinate system of the + * layer itself. This method is used by the viewport controller as part of the process of + * translating touch events to Gecko's coordinate system. + */ + public FloatPoint convertViewPointToLayerPoint(FloatPoint viewPoint) { + if (mRootLayer == null) + return null; + + // Undo the transforms. + FloatPoint scaledPoint = viewPoint.scale(1.0f / getZoomFactor()); + return mVisibleRect.getOrigin().add(scaledPoint).subtract(mRootLayer.origin); + } + + /* + * Gesture detection. This is handled only at a high level in this class; we dispatch to the + * pan/zoom controller to do the dirty work. + */ + + public boolean onTouchEvent(MotionEvent event) { + boolean result = mPanZoomController.onTouchEvent(event); + if (mOnTouchListener != null) + result = mOnTouchListener.onTouch(mView, event) || result; + return result; + } +} + diff --git a/embedding/android/gfx/LayerRenderer.java b/embedding/android/gfx/LayerRenderer.java new file mode 100644 index 000000000000..24eb1ad8a548 --- /dev/null +++ b/embedding/android/gfx/LayerRenderer.java @@ -0,0 +1,202 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.BufferedCairoImage; +import org.mozilla.gecko.gfx.FloatRect; +import org.mozilla.gecko.gfx.IntRect; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.gfx.NinePatchTileLayer; +import org.mozilla.gecko.gfx.SingleTileLayer; +import org.mozilla.gecko.gfx.TextureReaper; +import org.mozilla.gecko.gfx.TextLayer; +import org.mozilla.gecko.gfx.TileLayer; +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import java.nio.ByteBuffer; + +/** + * The layer renderer implements the rendering logic for a layer view. + */ +public class LayerRenderer implements GLSurfaceView.Renderer { + private static final float BACKGROUND_COLOR_R = 0.81f; + private static final float BACKGROUND_COLOR_G = 0.81f; + private static final float BACKGROUND_COLOR_B = 0.81f; + + private LayerView mView; + private SingleTileLayer mCheckerboardLayer; + private NinePatchTileLayer mShadowLayer; + private TextLayer mFPSLayer; + + // FPS display + private long mFrameCountTimestamp; + private int mFrameCount; // number of frames since last timestamp + + public LayerRenderer(LayerView view) { + mView = view; + + /* FIXME: Layers should not be directly connected to the layer controller. */ + LayerController controller = view.getController(); + mCheckerboardLayer = new SingleTileLayer(true); + mCheckerboardLayer.paintImage(new BufferedCairoImage(controller.getCheckerboardPattern())); + mShadowLayer = new NinePatchTileLayer(controller); + mShadowLayer.paintImage(new BufferedCairoImage(controller.getShadowPattern())); + mFPSLayer = new TextLayer(new IntSize(64, 32)); + mFPSLayer.setText("-- FPS"); + + mFrameCountTimestamp = System.currentTimeMillis(); + mFrameCount = 0; + } + + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + gl.glClearDepthf(1.0f); /* FIXME: Is this needed? */ + gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST); + gl.glShadeModel(GL10.GL_SMOOTH); /* FIXME: Is this needed? */ + gl.glDisable(GL10.GL_DITHER); + gl.glEnable(GL10.GL_TEXTURE_2D); + } + + public void onDrawFrame(GL10 gl) { + checkFPS(); + TextureReaper.get().reap(gl); + + LayerController controller = mView.getController(); + + /* Draw the background. */ + gl.glClearColor(BACKGROUND_COLOR_R, BACKGROUND_COLOR_G, BACKGROUND_COLOR_B, 1.0f); + gl.glClear(GL10.GL_COLOR_BUFFER_BIT); + + /* Draw the drop shadow. */ + setupPageTransform(gl); + mShadowLayer.draw(gl); + + /* Draw the checkerboard. */ + IntRect pageRect = clampToScreen(getPageRect()); + IntSize screenSize = controller.getScreenSize(); + gl.glEnable(GL10.GL_SCISSOR_TEST); + gl.glScissor(pageRect.x, screenSize.height - (pageRect.y + pageRect.height), + pageRect.width, pageRect.height); + + gl.glLoadIdentity(); + mCheckerboardLayer.draw(gl); + + /* Draw the layer the client added to us. */ + setupPageTransform(gl); + + Layer rootLayer = controller.getRoot(); + if (rootLayer != null) + rootLayer.draw(gl); + + gl.glDisable(GL10.GL_SCISSOR_TEST); + + /* Draw the FPS. */ + gl.glLoadIdentity(); + gl.glEnable(GL10.GL_BLEND); + mFPSLayer.draw(gl); + gl.glDisable(GL10.GL_BLEND); + } + + public void pageSizeChanged() { + mShadowLayer.recreateVertexBuffers(); + } + + private void setupPageTransform(GL10 gl) { + LayerController controller = mView.getController(); + FloatRect visibleRect = controller.getVisibleRect(); + float zoomFactor = controller.getZoomFactor(); + + gl.glLoadIdentity(); + gl.glScalef(zoomFactor, zoomFactor, 1.0f); + gl.glTranslatef(-visibleRect.x, -visibleRect.y, 0.0f); + } + + private IntRect getPageRect() { + LayerController controller = mView.getController(); + float zoomFactor = controller.getZoomFactor(); + FloatRect visibleRect = controller.getVisibleRect(); + IntSize pageSize = controller.getPageSize(); + + return new IntRect((int)Math.round(-zoomFactor * visibleRect.x), + (int)Math.round(-zoomFactor * visibleRect.y), + (int)Math.round(zoomFactor * pageSize.width), + (int)Math.round(zoomFactor * pageSize.height)); + } + + private IntRect clampToScreen(IntRect rect) { + LayerController controller = mView.getController(); + IntSize screenSize = controller.getScreenSize(); + + int left = Math.max(0, rect.x); + int top = Math.max(0, rect.y); + int right = Math.min(screenSize.width, rect.getRight()); + int bottom = Math.min(screenSize.height, rect.getBottom()); + return new IntRect(left, top, right - left, bottom - top); + } + + public void onSurfaceChanged(GL10 gl, int width, int height) { + gl.glViewport(0, 0, width, height); + gl.glMatrixMode(GL10.GL_PROJECTION); + gl.glLoadIdentity(); + gl.glOrthof(0.0f, (float)width, (float)height, 0.0f, -10.0f, 10.0f); + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glLoadIdentity(); + + mView.setScreenSize(width, height); + + /* TODO: Throw away tile images? */ + } + + private void checkFPS() { + if (System.currentTimeMillis() >= mFrameCountTimestamp + 1000) { + mFrameCountTimestamp = System.currentTimeMillis(); + mFPSLayer.setText(mFrameCount + " FPS"); + mFrameCount = 0; + } else { + mFrameCount++; + } + } +} + diff --git a/embedding/android/gfx/LayerView.java b/embedding/android/gfx/LayerView.java new file mode 100644 index 000000000000..0c479f22f5f5 --- /dev/null +++ b/embedding/android/gfx/LayerView.java @@ -0,0 +1,148 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.InputConnectionHandler; +import org.mozilla.gecko.gfx.LayerController; +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.ScaleGestureDetector; + +/** + * A view rendered by the layer compositor. + * + * This view delegates to LayerRenderer to actually do the drawing. Its role is largely that of a + * mediator between the LayerRenderer and the LayerController. + */ +public class LayerView extends GLSurfaceView { + private Context mContext; + private LayerController mController; + private InputConnectionHandler mInputConnectionHandler; + private LayerRenderer mRenderer; + private GestureDetector mGestureDetector; + private ScaleGestureDetector mScaleGestureDetector; + + public LayerView(Context context, LayerController controller) { + super(context); + + mContext = context; + mController = controller; + mRenderer = new LayerRenderer(this); + setRenderer(mRenderer); + mGestureDetector = new GestureDetector(context, controller.getGestureListener()); + mScaleGestureDetector = new ScaleGestureDetector(context, controller.getScaleGestureListener()); + mInputConnectionHandler = null; + + setFocusable(true); + setFocusableInTouchMode(true); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mGestureDetector.onTouchEvent(event)) + return true; + mScaleGestureDetector.onTouchEvent(event); + if (mScaleGestureDetector.isInProgress()) + return true; + return mController.onTouchEvent(event); + } + + public LayerController getController() { return mController; } + public void geometryChanged() { /* TODO: Schedule a redraw. */ } + + public void notifyRendererOfPageSizeChange() { + mRenderer.pageSizeChanged(); + } + + /** The LayerRenderer calls this to indicate that the window has changed size. */ + public void setScreenSize(int width, int height) { + mController.setScreenSize(width, height); + } + + public void setInputConnectionHandler(InputConnectionHandler handler) { + mInputConnectionHandler = handler; + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onCreateInputConnection(outAttrs); + return null; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyPreIme(keyCode, event); + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyDown(keyCode, event); + return false; + } + + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyLongPress(keyCode, event); + return false; + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyMultiple(keyCode, repeatCount, event); + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onKeyUp(keyCode, event); + return false; + } +} + diff --git a/embedding/android/gfx/NinePatchTileLayer.java b/embedding/android/gfx/NinePatchTileLayer.java new file mode 100644 index 000000000000..648cfd801760 --- /dev/null +++ b/embedding/android/gfx/NinePatchTileLayer.java @@ -0,0 +1,176 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.TileLayer; +import javax.microedition.khronos.opengles.GL10; +import java.nio.FloatBuffer; + +/** + * Encapsulates the logic needed to draw a nine-patch bitmap using OpenGL ES. + * + * For more information on nine-patch bitmaps, see the following document: + * http://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch + */ +public class NinePatchTileLayer extends TileLayer { + private FloatBuffer mSideTexCoordBuffer, mSideVertexBuffer; + private FloatBuffer mTopTexCoordBuffer, mTopVertexBuffer; + private LayerController mLayerController; + + private static final int PATCH_SIZE = 16; + private static final int TEXTURE_SIZE = 48; + + /* + * We divide the nine-patch bitmap up into the "sides" and the "tops": + * + * Top + * | + * v + * +---+---+---+ + * | | | | + * | +---+ | + * | |XXX| | <-- Side + * | +---+ | + * | | | | + * +---+---+---+ + */ + + private static final float[] SIDE_TEX_COORDS = { + 0.0f, 0.0f, + 0.25f, 0.0f, + 0.0f, 0.25f, + 0.25f, 0.25f, + 0.0f, 0.50f, + 0.25f, 0.50f, + 0.0f, 0.75f, + 0.25f, 0.75f, + }; + + private static final float[] TOP_TEX_COORDS = { + 0.25f, 0.0f, + 0.50f, 0.0f, + 0.25f, 0.25f, + 0.50f, 0.25f, + }; + + public NinePatchTileLayer(LayerController layerController) { + super(false); + + mLayerController = layerController; + + mSideTexCoordBuffer = createBuffer(SIDE_TEX_COORDS); + mTopTexCoordBuffer = createBuffer(TOP_TEX_COORDS); + + recreateVertexBuffers(); + } + + public void recreateVertexBuffers() { + IntSize pageSize = mLayerController.getPageSize(); + + float[] sideVertices = { + -PATCH_SIZE, -PATCH_SIZE, 0.0f, + 0.0f, -PATCH_SIZE, 0.0f, + -PATCH_SIZE, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, + -PATCH_SIZE, pageSize.height, 0.0f, + 0.0f, pageSize.height, 0.0f, + -PATCH_SIZE, PATCH_SIZE + pageSize.height, 0.0f, + 0.0f, PATCH_SIZE + pageSize.height, 0.0f + }; + + float[] topVertices = { + 0.0f, -PATCH_SIZE, 0.0f, + pageSize.width, -PATCH_SIZE, 0.0f, + 0.0f, 0.0f, 0.0f, + pageSize.width, 0.0f, 0.0f + }; + + mSideVertexBuffer = createBuffer(sideVertices); + mTopVertexBuffer = createBuffer(topVertices); + } + + @Override + protected void onTileDraw(GL10 gl) { + IntSize pageSize = mLayerController.getPageSize(); + + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glEnable(GL10.GL_BLEND); + + gl.glBindTexture(GL10.GL_TEXTURE_2D, getTextureID()); + + /* Left side */ + drawTriangles(gl, mSideVertexBuffer, mSideTexCoordBuffer, 8); + + /* Top */ + drawTriangles(gl, mTopVertexBuffer, mTopTexCoordBuffer, 4); + + /* Right side */ + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glPushMatrix(); + gl.glTranslatef(pageSize.width + PATCH_SIZE, 0.0f, 0.0f); + gl.glMatrixMode(GL10.GL_TEXTURE); + gl.glPushMatrix(); + gl.glTranslatef(0.50f, 0.0f, 0.0f); + + drawTriangles(gl, mSideVertexBuffer, mSideTexCoordBuffer, 8); + + gl.glMatrixMode(GL10.GL_TEXTURE); + gl.glPopMatrix(); + gl.glMatrixMode(GL10.GL_MODELVIEW); /* Not strictly necessary, but here for clarity. */ + gl.glPopMatrix(); + + /* Bottom */ + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glPushMatrix(); + gl.glTranslatef(0.0f, pageSize.height + PATCH_SIZE, 0.0f); + gl.glMatrixMode(GL10.GL_TEXTURE); + gl.glPushMatrix(); + gl.glTranslatef(0.0f, 0.50f, 0.0f); + + drawTriangles(gl, mTopVertexBuffer, mTopTexCoordBuffer, 4); + + gl.glMatrixMode(GL10.GL_TEXTURE); + gl.glPopMatrix(); + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glPopMatrix(); + + gl.glDisable(GL10.GL_BLEND); + } +} diff --git a/embedding/android/gfx/PlaceholderLayerClient.java b/embedding/android/gfx/PlaceholderLayerClient.java new file mode 100644 index 000000000000..a920792ed208 --- /dev/null +++ b/embedding/android/gfx/PlaceholderLayerClient.java @@ -0,0 +1,102 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.BufferedCairoImage; +import org.mozilla.gecko.gfx.CairoUtils; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerClient; +import org.mozilla.gecko.gfx.SingleTileLayer; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Environment; +import android.util.Log; +import java.io.File; +import java.nio.ByteBuffer; + +/** + * A stand-in for Gecko that renders cached content of the previous page. We use this until Gecko + * is up, then we hand off control to it. + */ +public class PlaceholderLayerClient extends LayerClient { + private Context mContext; + private IntSize mPageSize; + private int mWidth, mHeight, mFormat; + private ByteBuffer mBuffer; + + private PlaceholderLayerClient(Context context, Bitmap bitmap) { + mContext = context; + mPageSize = new IntSize(995, 1250); /* TODO */ + + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + mFormat = CairoUtils.bitmapConfigToCairoFormat(bitmap.getConfig()); + mBuffer = ByteBuffer.allocateDirect(mWidth * mHeight * 4); + bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + } + + public static PlaceholderLayerClient createInstance(Context context) { + File path = new File(Environment.getExternalStorageDirectory(), "lastScreen.png"); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + Bitmap bitmap = BitmapFactory.decodeFile("" + path, options); + if (bitmap == null) + return null; + + return new PlaceholderLayerClient(context, bitmap); + } + + public void init() { + SingleTileLayer tileLayer = new SingleTileLayer(); + getLayerController().setRoot(tileLayer); + tileLayer.paintImage(new BufferedCairoImage(mBuffer, mWidth, mHeight, mFormat)); + } + + @Override + public void geometryChanged() { /* no-op */ } + @Override + public IntSize getPageSize() { return mPageSize; } + @Override + public void render() { /* no-op */ } + + /** Called whenever the page changes size. */ + @Override + public void setPageSize(IntSize pageSize) { mPageSize = pageSize; } +} + diff --git a/embedding/android/gfx/SingleTileLayer.java b/embedding/android/gfx/SingleTileLayer.java new file mode 100644 index 000000000000..417233f785dd --- /dev/null +++ b/embedding/android/gfx/SingleTileLayer.java @@ -0,0 +1,107 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.CairoImage; +import org.mozilla.gecko.gfx.CairoUtils; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.TileLayer; +import android.util.Log; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import javax.microedition.khronos.opengles.GL10; + +/** + * Encapsulates the logic needed to draw a single textured tile. + */ +public class SingleTileLayer extends TileLayer { + private FloatBuffer mTexCoordBuffer, mVertexBuffer; + + private static final float[] VERTICES = { + 0.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 0.0f + }; + + private static final float[] TEX_COORDS = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f + }; + + public SingleTileLayer() { this(false); } + + public SingleTileLayer(boolean repeat) { + super(repeat); + + mVertexBuffer = createBuffer(VERTICES); + mTexCoordBuffer = createBuffer(TEX_COORDS); + } + + @Override + protected void onTileDraw(GL10 gl) { + IntSize size = getSize(); + + if (repeats()) { + gl.glMatrixMode(GL10.GL_TEXTURE); + gl.glPushMatrix(); + gl.glScalef(LayerController.TILE_WIDTH / size.width, + LayerController.TILE_HEIGHT / size.height, + 1.0f); + + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glScalef(LayerController.TILE_WIDTH, LayerController.TILE_HEIGHT, 1.0f); + } else { + gl.glScalef(size.width, size.height, 1.0f); + } + + gl.glBindTexture(GL10.GL_TEXTURE_2D, getTextureID()); + drawTriangles(gl, mVertexBuffer, mTexCoordBuffer, 4); + + if (repeats()) { + gl.glMatrixMode(GL10.GL_TEXTURE); + gl.glPopMatrix(); + gl.glMatrixMode(GL10.GL_MODELVIEW); + } + } +} + diff --git a/embedding/android/gfx/TextLayer.java b/embedding/android/gfx/TextLayer.java new file mode 100644 index 000000000000..189a3efb87d5 --- /dev/null +++ b/embedding/android/gfx/TextLayer.java @@ -0,0 +1,99 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.BufferedCairoImage; +import org.mozilla.gecko.gfx.CairoImage; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.SingleTileLayer; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.util.Log; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +/** + * Draws text on a layer. This is used for the frame rate meter. + */ +public class TextLayer extends SingleTileLayer { + private ByteBuffer mBuffer; + private BufferedCairoImage mImage; + private IntSize mSize; + private String mText; + + public TextLayer(IntSize size) { + super(false); + + mBuffer = ByteBuffer.allocateDirect(size.width * size.height * 4); + mSize = size; + mImage = new BufferedCairoImage(mBuffer, size.width, size.height, + CairoImage.FORMAT_ARGB32); + mText = ""; + } + + public void setText(String text) { + mText = text; + renderText(); + paintImage(mImage); + } + + private void renderText() { + Bitmap bitmap = Bitmap.createBitmap(mSize.width, mSize.height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Paint textPaint = new Paint(); + textPaint.setAntiAlias(true); + textPaint.setColor(Color.WHITE); + textPaint.setFakeBoldText(true); + textPaint.setTextSize(18.0f); + textPaint.setTypeface(Typeface.DEFAULT_BOLD); + float width = textPaint.measureText(mText) + 18.0f; + + Paint backgroundPaint = new Paint(); + backgroundPaint.setColor(Color.argb(127, 0, 0, 0)); + canvas.drawRect(0.0f, 0.0f, width, 18.0f + 6.0f, backgroundPaint); + + canvas.drawText(mText, 6.0f, 18.0f, textPaint); + + bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + } +} + diff --git a/embedding/android/gfx/TextureReaper.java b/embedding/android/gfx/TextureReaper.java new file mode 100644 index 000000000000..0527ae73811d --- /dev/null +++ b/embedding/android/gfx/TextureReaper.java @@ -0,0 +1,71 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import javax.microedition.khronos.opengles.GL10; +import java.util.ArrayList; + +/** Manages a list of dead tiles, so we don't leak resources. */ +public class TextureReaper { + private static TextureReaper sSharedInstance; + private ArrayList mDeadTextureIDs; + + private TextureReaper() { mDeadTextureIDs = new ArrayList(); } + + public static TextureReaper get() { + if (sSharedInstance == null) + sSharedInstance = new TextureReaper(); + return sSharedInstance; + } + + public void add(int[] textureIDs) { + for (int textureID : textureIDs) + mDeadTextureIDs.add(textureID); + } + + public void reap(GL10 gl) { + int[] deadTextureIDs = new int[mDeadTextureIDs.size()]; + for (int i = 0; i < deadTextureIDs.length; i++) + deadTextureIDs[i] = mDeadTextureIDs.get(i); + mDeadTextureIDs.clear(); + + gl.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0); + } +} + + diff --git a/embedding/android/gfx/TileLayer.java b/embedding/android/gfx/TileLayer.java new file mode 100644 index 000000000000..0a706b113eb9 --- /dev/null +++ b/embedding/android/gfx/TileLayer.java @@ -0,0 +1,189 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.gfx.CairoImage; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.Layer; +import org.mozilla.gecko.gfx.TextureReaper; +import android.util.Log; +import javax.microedition.khronos.opengles.GL10; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +/** + * Base class for tile layers, which encapsulate the logic needed to draw textured tiles in OpenGL + * ES. + */ +public abstract class TileLayer extends Layer { + private CairoImage mImage; + private boolean mRepeat; + private IntSize mSize; + private int[] mTextureIDs; + + private IntRect mTextureUploadRect; + /* The rect that needs to be uploaded to the texture. */ + + public TileLayer(boolean repeat) { + super(); + mRepeat = repeat; + mTextureUploadRect = null; + } + + public IntSize getSize() { return mSize; } + + protected boolean repeats() { return mRepeat; } + protected int getTextureID() { return mTextureIDs[0]; } + + @Override + protected void finalize() throws Throwable { + if (mTextureIDs != null) + TextureReaper.get().add(mTextureIDs); + } + + /** + * Subclasses implement this method to perform tile drawing. + * + * Invariant: The current matrix mode must be GL_MODELVIEW both before and after this call. + */ + protected abstract void onTileDraw(GL10 gl); + + @Override + protected void onDraw(GL10 gl) { + if (mImage == null) + return; + if (mTextureUploadRect != null) + uploadTexture(gl); + + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glPushMatrix(); + + onTileDraw(gl); + + gl.glPopMatrix(); + gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); + } + + public void paintSubimage(CairoImage image, IntRect rect) { + mImage = image; + mTextureUploadRect = rect; + + /* + * Assert that the image has a power-of-two size. OpenGL ES < 2.0 doesn't support NPOT + * textures and OpenGL ES doesn't seem to let us efficiently slice up a NPOT bitmap. + */ + int width = mImage.getWidth(), height = mImage.getHeight(); + assert (width & (width - 1)) == 0; + assert (height & (height - 1)) == 0; + } + + public void paintImage(CairoImage image) { + paintSubimage(image, new IntRect(0, 0, image.getWidth(), image.getHeight())); + } + + private void uploadTexture(GL10 gl) { + boolean newTexture = mTextureIDs == null; + if (newTexture) { + mTextureIDs = new int[1]; + gl.glGenTextures(mTextureIDs.length, mTextureIDs, 0); + } + + int width = mImage.getWidth(), height = mImage.getHeight(); + mSize = new IntSize(width, height); + + int cairoFormat = mImage.getFormat(); + int internalFormat = CairoUtils.cairoFormatToGLInternalFormat(cairoFormat); + int format = CairoUtils.cairoFormatToGLFormat(cairoFormat); + int type = CairoUtils.cairoFormatToGLType(cairoFormat); + + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIDs[0]); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); + + int repeatMode = mRepeat ? GL10.GL_REPEAT : GL10.GL_CLAMP_TO_EDGE; + gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, repeatMode); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, repeatMode); + + ByteBuffer buffer = mImage.lockBuffer(); + try { + if (newTexture) { + /* The texture is new; we have to upload the whole image. */ + gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, internalFormat, mSize.width, mSize.height, 0, + format, type, buffer); + } else { + /* + * The texture is already existing, so upload only the changed rect. We have to + * widen to the full width of the texture because we can't count on the device + * having support for GL_EXT_unpack_subimage, and going line-by-line is too slow. + */ + Buffer viewBuffer = buffer.slice(); + int bpp = CairoUtils.bitsPerPixelForCairoFormat(cairoFormat) / 8; + viewBuffer.position(mTextureUploadRect.y * width * bpp); + + gl.glTexSubImage2D(gl.GL_TEXTURE_2D, + 0, 0, mTextureUploadRect.y, width, mTextureUploadRect.height, + format, type, viewBuffer); + } + } finally { + mImage.unlockBuffer(); + } + + mTextureUploadRect = null; + } + + protected static FloatBuffer createBuffer(float[] values) { + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(values.length * 4); + byteBuffer.order(ByteOrder.nativeOrder()); + + FloatBuffer floatBuffer = byteBuffer.asFloatBuffer(); + floatBuffer.put(values); + floatBuffer.position(0); + return floatBuffer; + } + + protected static void drawTriangles(GL10 gl, FloatBuffer vertexBuffer, + FloatBuffer texCoordBuffer, int count) { + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); + gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texCoordBuffer); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, count); + } +} + diff --git a/embedding/android/resources/drawable/checkerboard.png b/embedding/android/resources/drawable/checkerboard.png new file mode 100644 index 000000000000..57cfbe80fdcf Binary files /dev/null and b/embedding/android/resources/drawable/checkerboard.png differ diff --git a/embedding/android/resources/drawable/shadow.png b/embedding/android/resources/drawable/shadow.png new file mode 100644 index 000000000000..3ce69155c6b5 Binary files /dev/null and b/embedding/android/resources/drawable/shadow.png differ diff --git a/embedding/android/ui/PanZoomController.java b/embedding/android/ui/PanZoomController.java new file mode 100644 index 000000000000..6f61565a5ce7 --- /dev/null +++ b/embedding/android/ui/PanZoomController.java @@ -0,0 +1,577 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.ui; + +import org.json.JSONObject; +import org.mozilla.gecko.gfx.FloatPoint; +import org.mozilla.gecko.gfx.FloatRect; +import org.mozilla.gecko.gfx.IntPoint; +import org.mozilla.gecko.gfx.IntRect; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import java.util.Timer; +import java.util.TimerTask; + +/* + * Handles the kinetic scrolling and zooming physics for a layer controller. + * + * Many ideas are from Joe Hewitt's Scrollability: + * https://github.com/joehewitt/scrollability/ + */ +public class PanZoomController + extends GestureDetector.SimpleOnGestureListener + implements ScaleGestureDetector.OnScaleGestureListener +{ + private static final String LOG_NAME = "PanZoomController"; + + private LayerController mController; + + private static final float FRICTION = 0.97f; + // Animation stops if the velocity is below this value. + private static final float STOPPED_THRESHOLD = 4.0f; + // The percentage of the surface which can be overscrolled before it must snap back. + private static final float SNAP_LIMIT = 0.75f; + // The rate of deceleration when the surface has overscrolled. + private static final float OVERSCROLL_DECEL_RATE = 0.04f; + // The duration of animation when bouncing back. + private static final int SNAP_TIME = 150; + // The number of subdivisions we should consider when plotting the ease-out transition. Higher + // values make the animation more accurate, but slower to plot. + private static final int SUBDIVISION_COUNT = 1000; + + private long mLastTimestamp; + private Timer mFlingTimer; + private Axis mX, mY; + /* The span at the first zoom event (in unzoomed page coordinates). */ + private float mInitialZoomSpan; + /* The zoom focus at the first zoom event (in unzoomed page coordinates). */ + private FloatPoint mInitialZoomFocus; + + private enum PanZoomState { + NOTHING, /* no touch-start events received */ + FLING, /* all touches removed, but we're still scrolling page */ + TOUCHING, /* one touch-start event received */ + PANNING, /* touch-start followed by move */ + PANNING_HOLD, /* in panning, but haven't moved. + * similar to TOUCHING but after starting a pan */ + PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */ + } + + private PanZoomState mState; + + public PanZoomController(LayerController controller) { + mController = controller; + mX = new Axis(); mY = new Axis(); + mState = PanZoomState.NOTHING; + + populatePositionAndLength(); + } + + public boolean onTouchEvent(MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: return onTouchStart(event); + case MotionEvent.ACTION_MOVE: return onTouchMove(event); + case MotionEvent.ACTION_UP: return onTouchEnd(event); + case MotionEvent.ACTION_CANCEL: return onTouchCancel(event); + default: return false; + } + } + + public void geometryChanged() { + populatePositionAndLength(); + } + + /* + * Panning/scrolling + */ + + private boolean onTouchStart(MotionEvent event) { + switch (mState) { + case FLING: + if (mFlingTimer != null) { + mFlingTimer.cancel(); + mFlingTimer = null; + } + // fall through + case NOTHING: + mState = PanZoomState.TOUCHING; + mX.touchPos = event.getX(0); + mY.touchPos = event.getY(0); + return false; + case TOUCHING: + case PANNING: + case PANNING_HOLD: + case PINCHING: + mState = PanZoomState.PINCHING; + return false; + } + Log.e(LOG_NAME, "Unhandled case " + mState + " in onTouchStart"); + return false; + } + + private boolean onTouchMove(MotionEvent event) { + switch (mState) { + case NOTHING: + case FLING: + // should never happen + Log.e(LOG_NAME, "Received impossible touch move while in " + mState); + return false; + case TOUCHING: + mLastTimestamp = System.currentTimeMillis(); + // fall through + case PANNING_HOLD: + mState = PanZoomState.PANNING; + // fall through + case PANNING: + track(event, System.currentTimeMillis()); + return true; + case PINCHING: + // scale gesture listener will handle this + return false; + } + Log.e(LOG_NAME, "Unhandled case " + mState + " in onTouchMove"); + return false; + } + + private boolean onTouchEnd(MotionEvent event) { + switch (mState) { + case NOTHING: + case FLING: + // should never happen + Log.e(LOG_NAME, "Received impossible touch end while in " + mState); + return false; + case TOUCHING: + mState = PanZoomState.NOTHING; + // TODO: send click to gecko + return false; + case PANNING: + case PANNING_HOLD: + mState = PanZoomState.FLING; + fling(System.currentTimeMillis()); + return true; + case PINCHING: + int points = event.getPointerCount(); + if (points == 1) { + // last touch up + mState = PanZoomState.NOTHING; + } else if (points == 2) { + int pointRemovedIndex = event.getActionIndex(); + int pointRemainingIndex = 1 - pointRemovedIndex; // kind of a hack + mState = PanZoomState.TOUCHING; + mX.touchPos = event.getX(pointRemainingIndex); + mY.touchPos = event.getY(pointRemainingIndex); + } else { + // still pinching, do nothing + } + return true; + } + Log.e(LOG_NAME, "Unhandled case " + mState + " in onTouchEnd"); + return false; + } + + private boolean onTouchCancel(MotionEvent event) { + mState = PanZoomState.NOTHING; + return false; + } + + private void track(MotionEvent event, long timestamp) { + long timeStep = timestamp - mLastTimestamp; + mLastTimestamp = timestamp; + + float zoomFactor = mController.getZoomFactor(); + mX.velocity = (mX.touchPos - event.getX(0)) / zoomFactor; + mY.velocity = (mY.touchPos - event.getY(0)) / zoomFactor; + mX.touchPos = event.getX(0); mY.touchPos = event.getY(0); + + float absVelocity = (float)Math.sqrt(mX.velocity * mX.velocity + + mY.velocity * mY.velocity); + if (absVelocity < STOPPED_THRESHOLD) + mState = PanZoomState.PANNING_HOLD; + + mX.applyEdgeResistance(); mX.displace(); + mY.applyEdgeResistance(); mY.displace(); + updatePosition(); + } + + private void fling(long timestamp) { + long timeStep = timestamp - mLastTimestamp; + mLastTimestamp = timestamp; + + if (mState != PanZoomState.FLING) + mX.velocity = mY.velocity = 0.0f; + + mX.displace(); mY.displace(); + + if (mFlingTimer != null) + mFlingTimer.cancel(); + + mX.startFling(); mY.startFling(); + + mFlingTimer = new Timer(); + mFlingTimer.scheduleAtFixedRate(new TimerTask() { + public void run() { mController.post(new FlingRunnable()); } + }, 0, 1000L/60L); + } + + private void updatePosition() { + mController.scrollTo(mX.viewportPos, mY.viewportPos); + mController.notifyLayerClientOfGeometryChange(); + } + + // Populates the viewport info and length in the axes. + private void populatePositionAndLength() { + IntSize pageSize = mController.getPageSize(); + FloatRect visibleRect = mController.getVisibleRect(); + IntSize screenSize = mController.getScreenSize(); + + mX.setPageLength(pageSize.width); + mX.viewportPos = visibleRect.x; + mX.setViewportLength(visibleRect.width); + + mY.setPageLength(pageSize.height); + mY.viewportPos = visibleRect.y; + mY.setViewportLength(visibleRect.height); + } + + // The callback that performs the fling animation. + private class FlingRunnable implements Runnable { + public void run() { + populatePositionAndLength(); + mX.advanceFling(); mY.advanceFling(); + + // If both X and Y axes are overscrolled, we have to wait until both axes have stopped + // to snap back to avoid a jarring effect. + boolean waitingToSnapX = mX.getFlingState() == Axis.FlingStates.WAITING_TO_SNAP; + boolean waitingToSnapY = mY.getFlingState() == Axis.FlingStates.WAITING_TO_SNAP; + if ((mX.getOverscroll() == Axis.Overscroll.PLUS || mX.getOverscroll() == Axis.Overscroll.MINUS) && + (mY.getOverscroll() == Axis.Overscroll.PLUS || mY.getOverscroll() == Axis.Overscroll.MINUS)) + { + if (waitingToSnapX && waitingToSnapY) { + mX.startSnap(); mY.startSnap(); + } + } else { + if (waitingToSnapX) + mX.startSnap(); + if (waitingToSnapY) + mY.startSnap(); + } + + mX.displace(); mY.displace(); + updatePosition(); + + if (mX.getFlingState() == Axis.FlingStates.STOPPED && + mY.getFlingState() == Axis.FlingStates.STOPPED) { + stop(); + } + } + + private void stop() { + mState = PanZoomState.NOTHING; + if (mFlingTimer != null) { + mFlingTimer.cancel(); + mFlingTimer = null; + } + } + } + + private float computeElasticity(float excess, float viewportLength) { + return 1.0f - excess / (viewportLength * SNAP_LIMIT); + } + + // Physics information for one axis (X or Y). + private static class Axis { + public enum FlingStates { + STOPPED, + SCROLLING, + WAITING_TO_SNAP, + SNAPPING, + } + + public enum Overscroll { + NONE, + MINUS, // Overscrolled in the negative direction + PLUS, // Overscrolled in the positive direction + BOTH, // Overscrolled in both directions (page is zoomed to smaller than screen) + } + + public float touchPos; /* Position of the last touch. */ + public float velocity; /* Velocity in this direction. */ + + private FlingStates mFlingState; /* The fling state we're in on this axis. */ + private EaseOutAnimation mSnapAnim; /* The animation when the page is snapping back. */ + + /* These three need to be kept in sync with the layer controller. */ + public float viewportPos; + private float mViewportLength; + private int mScreenLength; + private int mPageLength; + + public FlingStates getFlingState() { return mFlingState; } + + public void setViewportLength(float viewportLength) { mViewportLength = viewportLength; } + public void setScreenLength(int screenLength) { mScreenLength = screenLength; } + public void setPageLength(int pageLength) { mPageLength = pageLength; } + + private float getViewportEnd() { return viewportPos + mViewportLength; } + + public Overscroll getOverscroll() { + boolean minus = (viewportPos < 0.0f); + boolean plus = (getViewportEnd() > mPageLength); + if (minus && plus) + return Overscroll.BOTH; + else if (minus) + return Overscroll.MINUS; + else if (plus) + return Overscroll.PLUS; + else + return Overscroll.NONE; + } + + // Returns the amount that the page has been overscrolled. If the page hasn't been + // overscrolled on this axis, returns 0. + private float getExcess() { + switch (getOverscroll()) { + case MINUS: return Math.min(-viewportPos, mPageLength - getViewportEnd()); + case PLUS: return Math.min(viewportPos, getViewportEnd() - mPageLength); + default: return 0.0f; + } + } + + // Applies resistance along the edges when tracking. + public void applyEdgeResistance() { + float excess = getExcess(); + if (excess > 0.0f) + velocity *= SNAP_LIMIT - excess / mViewportLength; + } + + public void startFling() { mFlingState = FlingStates.SCROLLING; } + + // Advances a fling animation by one step. + public void advanceFling() { + switch (mFlingState) { + case SCROLLING: + scroll(); + return; + case WAITING_TO_SNAP: + // We don't do anything until the controller switches us into the snapping state. + return; + case SNAPPING: + snap(); + return; + } + } + + // Performs one frame of a scroll operation if applicable. + private void scroll() { + // If we aren't overscrolled, just apply friction. + float excess = getExcess(); + if (excess == 0.0f) { + velocity *= FRICTION; + if (Math.abs(velocity) < 0.1f) { + velocity = 0.0f; + mFlingState = FlingStates.STOPPED; + } + return; + } + + // Otherwise, decrease the velocity linearly. + float elasticity = 1.0f - excess / (mViewportLength * SNAP_LIMIT); + if (getOverscroll() == Overscroll.MINUS) + velocity = Math.min((velocity + OVERSCROLL_DECEL_RATE) * elasticity, 0.0f); + else // must be Overscroll.PLUS + velocity = Math.max((velocity - OVERSCROLL_DECEL_RATE) * elasticity, 0.0f); + + if (Math.abs(velocity) < 0.3f) { + velocity = 0.0f; + mFlingState = FlingStates.WAITING_TO_SNAP; + } + } + + // Starts a snap-into-place operation. + public void startSnap() { + switch (getOverscroll()) { + case MINUS: + mSnapAnim = new EaseOutAnimation(viewportPos, viewportPos + getExcess()); + break; + case PLUS: + mSnapAnim = new EaseOutAnimation(viewportPos, viewportPos - getExcess()); + break; + default: + throw new RuntimeException("Not overscrolled at startSnap()"); + } + + mFlingState = FlingStates.SNAPPING; + } + + // Performs one frame of a snap-into-place operation. + private void snap() { + mSnapAnim.advance(); + viewportPos = mSnapAnim.getPosition(); + + if (mSnapAnim.getFinished()) { + mSnapAnim = null; + mFlingState = FlingStates.STOPPED; + } + } + + // Performs displacement of the viewport position according to the current velocity. + public void displace() { viewportPos += velocity; } + } + + private static class EaseOutAnimation { + private float[] mFrames; + private float mPosition; + private float mOrigin; + private float mDest; + private long mTimestamp; + private boolean mFinished; + + public EaseOutAnimation(float position, float dest) { + mPosition = mOrigin = position; + mDest = dest; + mFrames = new float[SNAP_TIME]; + mTimestamp = System.currentTimeMillis(); + mFinished = false; + plot(position, dest, mFrames); + } + + public float getPosition() { return mPosition; } + public boolean getFinished() { return mFinished; } + + private void advance() { + int frame = (int)(System.currentTimeMillis() - mTimestamp); + if (frame >= SNAP_TIME) { + mPosition = mDest; + mFinished = true; + return; + } + + mPosition = mFrames[frame]; + } + + private static void plot(float from, float to, float[] frames) { + int nextX = 0; + for (int i = 0; i < SUBDIVISION_COUNT; i++) { + float t = (float)i / (float)SUBDIVISION_COUNT; + float xPos = (3.0f*t*t - 2.0f*t*t*t) * (float)frames.length; + if ((int)xPos < nextX) + continue; + + int oldX = nextX; + nextX = (int)xPos; + + float yPos = 1.74f*t*t - 0.74f*t*t*t; + float framePos = from + (to - from) * yPos; + + while (oldX < nextX) + frames[oldX++] = framePos; + + if (nextX >= frames.length) + break; + } + + // Pad out any remaining frames. + while (nextX < frames.length) { + frames[nextX] = frames[nextX - 1]; + nextX++; + } + } + } + + /* + * Zooming + */ + @Override + public boolean onScale(ScaleGestureDetector detector) { + mState = PanZoomState.PINCHING; + float newZoom = detector.getCurrentSpan() / mInitialZoomSpan; + + IntSize screenSize = mController.getScreenSize(); + float x = mInitialZoomFocus.x - (detector.getFocusX() / newZoom); + float y = mInitialZoomFocus.y - (detector.getFocusY() / newZoom); + float width = screenSize.width / newZoom; + float height = screenSize.height / newZoom; + mController.setVisibleRect(x, y, width, height); + mController.notifyLayerClientOfGeometryChange(); + populatePositionAndLength(); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + mState = PanZoomState.PINCHING; + FloatRect initialZoomRect = mController.getVisibleRect(); + float initialZoom = mController.getZoomFactor(); + + mInitialZoomFocus = + new FloatPoint(initialZoomRect.x + (detector.getFocusX() / initialZoom), + initialZoomRect.y + (detector.getFocusY() / initialZoom)); + mInitialZoomSpan = detector.getCurrentSpan() / initialZoom; + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mState = PanZoomState.PANNING_HOLD; + mLastTimestamp = System.currentTimeMillis(); + mX.touchPos = detector.getFocusX(); + mY.touchPos = detector.getFocusY(); + } + + @Override + public void onLongPress(MotionEvent motionEvent) { + JSONObject ret = new JSONObject(); + try { + FloatPoint point = new FloatPoint(motionEvent.getX(), motionEvent.getY()); + point = mController.convertViewPointToLayerPoint(point); + ret.put("x", (int)Math.round(point.x)); + ret.put("y", (int)Math.round(point.y)); + } catch(Exception ex) { + Log.w(LOG_NAME, "Error building return: " + ex); + } + + GeckoEvent e = new GeckoEvent("Gesture:LongPress", ret.toString()); + GeckoAppShell.sendEventToGecko(e); + } +} diff --git a/embedding/android/ui/ViewportController.java b/embedding/android/ui/ViewportController.java new file mode 100644 index 000000000000..e534cde64971 --- /dev/null +++ b/embedding/android/ui/ViewportController.java @@ -0,0 +1,112 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.ui; + +import org.mozilla.gecko.gfx.FloatPoint; +import org.mozilla.gecko.gfx.FloatRect; +import org.mozilla.gecko.gfx.IntPoint; +import org.mozilla.gecko.gfx.IntRect; +import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.LayerController; + +/** Manages the dimensions of the page viewport. */ +public class ViewportController { + private IntSize mPageSize; + private FloatRect mVisibleRect; + + public ViewportController(IntSize pageSize, FloatRect visibleRect) { + mPageSize = pageSize; + mVisibleRect = visibleRect; + } + + private float clamp(float min, float value, float max) { + if (max < min) + return min; + return (value < min) ? min : (value > max) ? max : value; + } + + /** Returns the given rect, clamped to the boundaries of a tile. */ + public FloatRect clampRect(FloatRect rect) { + float x = clamp(0, rect.x, mPageSize.width - LayerController.TILE_WIDTH); + float y = clamp(0, rect.y, mPageSize.height - LayerController.TILE_HEIGHT); + return new FloatRect(x, y, rect.width, rect.height); + } + + /** Returns the coordinates of a tile centered on the given rect. */ + public static FloatRect widenRect(FloatRect rect) { + FloatPoint center = rect.getCenter(); + return new FloatRect(center.x - LayerController.TILE_WIDTH / 2, + center.y - LayerController.TILE_HEIGHT / 2, + LayerController.TILE_WIDTH, + LayerController.TILE_HEIGHT); + } + + /** + * Given the layer controller's visible rect, page size, and screen size, returns the zoom + * factor. + */ + public float getZoomFactor(FloatRect layerVisibleRect, IntSize layerPageSize, + IntSize screenSize) { + FloatRect transformed = transformVisibleRect(layerVisibleRect, layerPageSize); + return (float)screenSize.width / transformed.width; + } + + /** + * Given the visible rectangle that the user is viewing and the layer controller's page size, + * returns the dimensions of the box that this corresponds to on the page. + */ + public FloatRect transformVisibleRect(FloatRect layerVisibleRect, IntSize layerPageSize) { + float zoomFactor = (float)layerPageSize.width / (float)mPageSize.width; + return layerVisibleRect.scaleAll(1.0f / zoomFactor); + } + + /** + * Given the visible rectangle that the user is viewing and the layer controller's page size, + * returns the dimensions in layer coordinates that this corresponds to. + */ + public FloatRect untransformVisibleRect(FloatRect viewportVisibleRect, IntSize layerPageSize) { + float zoomFactor = (float)layerPageSize.width / (float)mPageSize.width; + return viewportVisibleRect.scaleAll(zoomFactor); + } + + public IntSize getPageSize() { return mPageSize; } + public void setPageSize(IntSize pageSize) { mPageSize = pageSize; } + public FloatRect getVisibleRect() { return mVisibleRect; } + public void setVisibleRect(FloatRect visibleRect) { mVisibleRect = visibleRect; } +} + diff --git a/gfx/thebes/GLContextProviderEGL.cpp b/gfx/thebes/GLContextProviderEGL.cpp index b9c27b9365fa..0f6f466b8e43 100644 --- a/gfx/thebes/GLContextProviderEGL.cpp +++ b/gfx/thebes/GLContextProviderEGL.cpp @@ -1869,6 +1869,7 @@ CreateSurfaceForWindow(nsIWidget *aWidget, EGLConfig config) { EGLSurface surface; +#ifdef PCWALTON_BROKEN #ifdef DEBUG sEGLLibrary.DumpEGLConfig(config); @@ -1887,6 +1888,8 @@ CreateSurfaceForWindow(nsIWidget *aWidget, EGLConfig config) printf_stderr("got surface %p\n", surface); #else surface = sEGLLibrary.fCreateWindowSurface(EGL_DISPLAY(), config, GET_NATIVE_WINDOW(aWidget), 0); +#endif + #endif return surface; diff --git a/mobile/chrome/content/browser.js b/mobile/chrome/content/browser.js index 208e6151ccb7..918fca44bdfc 100644 --- a/mobile/chrome/content/browser.js +++ b/mobile/chrome/content/browser.js @@ -76,6 +76,20 @@ const kMaxKineticSpeed = 9; // it's larger than this, lock the slow-moving axis. const kAxisLockRatio = 5; +// The element tag names that are considered to receive input. Mouse-down +// events directed to one of these are allowed to go through. +const kElementsReceivingInput = { + applet: true, + audio: true, + button: true, + embed: true, + input: true, + map: true, + select: true, + textarea: true, + video: true +}; + function dump(a) { Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a); } @@ -146,6 +160,7 @@ var BrowserApp = { Services.obs.addObserver(this, "Preferences:Set", false); Services.obs.addObserver(this, "ScrollTo:FocusedInput", false); Services.obs.addObserver(this, "Sanitize:ClearAll", false); + Services.obs.addObserver(this, "PanZoom:PanZoom", false); Services.obs.addObserver(this, "FullScreen:Exit", false); Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false); @@ -255,6 +270,12 @@ var BrowserApp = { return null; }, + getPageSizeForBrowser: function getPageSizeForBrowser(aBrowser) { + let html = aBrowser.contentDocument.documentElement; + let body = aBrowser.contentDocument.body; + return { width: body.scrollWidth, height: body.scrollHeight }; + }, + loadURI: function loadURI(aURI, aParams) { let browser = this.selectedBrowser; if (!browser) @@ -478,6 +499,20 @@ var BrowserApp = { focused.scrollIntoView(false); }, + panZoom: function(aData) { + let data = JSON.parse(aData); + + let browser = this.selectedBrowser; + + /*let documentElement = browser.contentDocument.documentElement; + documentElement.style.MozTransform = 'translate(-' + data.x + 'px, -' + data.y + 'px) ' + + 'translate(-50%, -50%) scale(' + data.zoomFactor + ') ' + + 'translate(50%, 50%)';*/ + browser.contentWindow.scrollTo(data.x, data.y); + + sendMessageToJava({ gecko: { type: "PanZoom:Ack", rect: data } }); + }, + updateScrollbarsFor: function(aElement) { // only draw the scrollbars if we're scrolling the root content element let doc = this.selectedBrowser.contentDocument; @@ -518,6 +553,13 @@ var BrowserApp = { this.horizScroller.setAttribute("panning", ""); }, + /* FIXME: Awful hack to tide us over until the display port is usable. */ + fakeDisplayPort: function(aBrowser) { + let html = aBrowser.contentDocument.documentElement; + html.style.width = '980px'; + html.style.height = '1500px'; + }, + observe: function(aSubject, aTopic, aData) { let browser = this.selectedBrowser; if (!browser) @@ -552,6 +594,8 @@ var BrowserApp = { this.scrollToFocusedInput(browser); } else if (aTopic == "Sanitize:ClearAll") { Sanitizer.sanitize(); + } else if (aTopic == "PanZoom:PanZoom") { + this.panZoom(aData); } else if (aTopic == "FullScreen:Exit") { browser.contentDocument.mozCancelFullScreen(); } @@ -896,6 +940,8 @@ Tab.prototype = { this.browser = document.createElement("browser"); this.browser.setAttribute("type", "content"); + this.browser.setAttribute("width", "980"); + this.browser.setAttribute("height", "480"); BrowserApp.deck.appendChild(this.browser); this.browser.stop(); @@ -971,7 +1017,7 @@ Tab.prototype = { state: aStateFlags } }; - + sendMessageToJava(message); } }, @@ -1080,12 +1126,11 @@ var BrowserEventHandler = { window.addEventListener("mouseup", this, true); window.addEventListener("mousemove", this, true); - BrowserApp.deck.addEventListener("MozMagnifyGestureStart", this, true); - BrowserApp.deck.addEventListener("MozMagnifyGestureUpdate", this, true); BrowserApp.deck.addEventListener("DOMContentLoaded", this, true); BrowserApp.deck.addEventListener("DOMLinkAdded", this, true); BrowserApp.deck.addEventListener("DOMTitleChanged", this, true); BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false); + BrowserApp.deck.addEventListener("MozScrolledAreaChanged", this, true); }, handleEvent: function(aEvent) { @@ -1118,6 +1163,9 @@ var BrowserEventHandler = { browser.removeEventListener("pagehide", listener, true); }, true); } + + BrowserApp.fakeDisplayPort(browser); + break; } @@ -1205,6 +1253,8 @@ var BrowserEventHandler = { if (this.panElement) this.panning = true; + if (!this._elementReceivesInput(aEvent.target)) + aEvent.preventDefault(); // Stops selection. break; case "mousemove": @@ -1281,9 +1331,6 @@ var BrowserEventHandler = { break; case "mouseup": - if (!this.panning) - break; - this.panning = false; // hide the scrollbars in case we're done scrolling. if the @@ -1471,41 +1518,22 @@ var BrowserEventHandler = { } break; - case "MozMagnifyGestureStart": - this._pinchDelta = 0; - this.zoomCallbackFired = true; - break; + case "MozScrolledAreaChanged": + dump("### Resize!"); - case "MozMagnifyGestureUpdate": - if (!aEvent.delta) - break; - - this._pinchDelta += aEvent.delta; - - if ((Math.abs(this._pinchDelta) >= 1) && this.zoomCallbackFired) { - // pinchDelta is the difference in pixels since the last call, so can - // be viewed as the number of extra/fewer pixels visible. - // - // We can work out the new zoom level by looking at the window width - // and height, and the existing zoom level. - let currentZoom = BrowserApp.selectedBrowser.markupDocumentViewer.fullZoom; - let currentSize = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2)); - let newZoom = ((currentSize * currentZoom) + this._pinchDelta) / currentSize; - - let self = this; - let callback = { - onBeforePaint: function zoomCallback(timeStamp) { - BrowserApp.selectedBrowser.markupDocumentViewer.fullZoom = newZoom; - self.zoomCallbackFired = true; - } - }; - - this._pinchDelta = 0; - - // Use mozRequestAnimationFrame to stop from flooding fullZoom - this.zoomCallbackFired = false; - window.mozRequestAnimationFrame(callback); + /* TODO: Only for tab in foreground */ + let browser = BrowserApp.getBrowserForDocument(aEvent.target); + if (!browser) { + dump("### Resize: No browser!"); + return; } + + sendMessageToJava({ + gecko: { + type: "PanZoom:Resize", + size: BrowserApp.getPageSizeForBrowser(browser) + } + }); break; } }, @@ -1560,16 +1588,14 @@ var BrowserEventHandler = { /* Element is scrollable if its scroll-size exceeds its client size, and: * - It has overflow 'auto' or 'scroll' * - It's a textarea - * - It's an HTML/BODY node + * We don't consider HTML/BODY nodes here, since Java pans those. */ if (checkElem) { if (((elem.scrollHeight > elem.clientHeight) || (elem.scrollWidth > elem.clientWidth)) && (elem.style.overflow == 'auto' || elem.style.overflow == 'scroll' || - elem.localName == 'textarea' || - elem.localName == 'html' || - elem.localName == 'body')) { + elem.localName == 'textarea')) { scrollable = true; break; } @@ -1591,6 +1617,12 @@ var BrowserEventHandler = { return elem; }, + _elementReceivesInput: function(aElement) { + return aElement instanceof Element && + (kElementsReceivingInput.hasOwnProperty(aElement.tagName.toLowerCase()) || + aElement.contentEditable === "true" || aElement.contentEditable === ""); + }, + _scrollElementBy: function(elem, x, y) { elem.scrollTop = elem.scrollTop + y; elem.scrollLeft = elem.scrollLeft + x; diff --git a/other-licenses/android/APKOpen.cpp b/other-licenses/android/APKOpen.cpp index a21ea572a465..9af3d94a8510 100644 --- a/other-licenses/android/APKOpen.cpp +++ b/other-licenses/android/APKOpen.cpp @@ -233,6 +233,7 @@ SHELL_WRAPPER1(nativeRun, jstring) SHELL_WRAPPER1(notifyGeckoOfEvent, jobject) SHELL_WRAPPER0(processNextNativeEvent) SHELL_WRAPPER1(setSurfaceView, jobject) +SHELL_WRAPPER1(setSoftwareLayerClient, jobject) SHELL_WRAPPER0(onResume) SHELL_WRAPPER0(onLowMemory) SHELL_WRAPPER3(callObserver, jstring, jstring, jstring) @@ -640,6 +641,7 @@ loadLibs(const char *apkName) GETFUNC(notifyGeckoOfEvent); GETFUNC(processNextNativeEvent); GETFUNC(setSurfaceView); + GETFUNC(setSoftwareLayerClient); GETFUNC(onResume); GETFUNC(onLowMemory); GETFUNC(callObserver); diff --git a/widget/src/android/AndroidBridge.cpp b/widget/src/android/AndroidBridge.cpp index 3c20e1f9d33b..8771d737daef 100644 --- a/widget/src/android/AndroidBridge.cpp +++ b/widget/src/android/AndroidBridge.cpp @@ -772,9 +772,9 @@ AndroidBridge::GetAccessibilityEnabled() } void -AndroidBridge::SetSurfaceView(jobject obj) +AndroidBridge::SetSoftwareLayerClient(jobject obj) { - mSurfaceView.Init(obj); + mSoftwareLayerClient.Init(obj); } void @@ -784,51 +784,6 @@ AndroidBridge::ShowInputMethodPicker() mJNIEnv->CallStaticVoidMethod(mGeckoAppShellClass, jShowInputMethodPicker); } -void * -AndroidBridge::CallEglCreateWindowSurface(void *dpy, void *config, AndroidGeckoSurfaceView &sview) -{ - ALOG_BRIDGE("AndroidBridge::CallEglCreateWindowSurface"); - AutoLocalJNIFrame jniFrame; - - /* - * This is basically: - * - * s = EGLContext.getEGL().eglCreateWindowSurface(new EGLDisplayImpl(dpy), - * new EGLConfigImpl(config), - * view.getHolder(), null); - * return s.mEGLSurface; - * - * We can't do it from java, because the EGLConfigImpl constructor is private. - */ - - jobject surfaceHolder = sview.GetSurfaceHolder(); - if (!surfaceHolder) - return nsnull; - - // grab some fields and methods we'll need - jmethodID constructConfig = mJNIEnv->GetMethodID(jEGLConfigImplClass, "", "(I)V"); - jmethodID constructDisplay = mJNIEnv->GetMethodID(jEGLDisplayImplClass, "", "(I)V"); - - jmethodID getEgl = mJNIEnv->GetStaticMethodID(jEGLContextClass, "getEGL", "()Ljavax/microedition/khronos/egl/EGL;"); - jmethodID createWindowSurface = mJNIEnv->GetMethodID(jEGL10Class, "eglCreateWindowSurface", "(Ljavax/microedition/khronos/egl/EGLDisplay;Ljavax/microedition/khronos/egl/EGLConfig;Ljava/lang/Object;[I)Ljavax/microedition/khronos/egl/EGLSurface;"); - - jobject egl = mJNIEnv->CallStaticObjectMethod(jEGLContextClass, getEgl); - - jobject jdpy = mJNIEnv->NewObject(jEGLDisplayImplClass, constructDisplay, (int) dpy); - jobject jconf = mJNIEnv->NewObject(jEGLConfigImplClass, constructConfig, (int) config); - - // make the call - jobject surf = mJNIEnv->CallObjectMethod(egl, createWindowSurface, jdpy, jconf, surfaceHolder, NULL); - if (!surf) - return nsnull; - - jfieldID sfield = mJNIEnv->GetFieldID(jEGLSurfaceImplClass, "mEGLSurface", "I"); - - jint realSurface = mJNIEnv->GetIntField(surf, sfield); - - return (void*) realSurface; -} - bool AndroidBridge::GetStaticIntField(const char *className, const char *fieldName, PRInt32* aInt) { diff --git a/widget/src/android/AndroidBridge.h b/widget/src/android/AndroidBridge.h index 66aba2a3b979..ee9e3307cd2e 100644 --- a/widget/src/android/AndroidBridge.h +++ b/widget/src/android/AndroidBridge.h @@ -40,6 +40,7 @@ #include #include +#include #include "nsCOMPtr.h" #include "nsCOMArray.h" @@ -149,8 +150,8 @@ public: void ScheduleRestart(); - void SetSurfaceView(jobject jobj); - AndroidGeckoSurfaceView& SurfaceView() { return mSurfaceView; } + void SetSoftwareLayerClient(jobject jobj); + AndroidGeckoSoftwareLayerClient &GetSoftwareLayerClient() { return mSoftwareLayerClient; } bool GetHandlersForURL(const char *aURL, nsIMutableArray* handlersArray = nsnull, @@ -247,9 +248,6 @@ public: int mEntries; }; - /* See GLHelpers.java as to why this is needed */ - void *CallEglCreateWindowSurface(void *dpy, void *config, AndroidGeckoSurfaceView& surfaceView); - bool GetStaticStringField(const char *classID, const char *field, nsAString &result); bool GetStaticIntField(const char *className, const char *fieldName, PRInt32* aInt); @@ -314,8 +312,8 @@ protected: JNIEnv *mJNIEnv; void *mThread; - // the GeckoSurfaceView - AndroidGeckoSurfaceView mSurfaceView; + // the software rendering layer client + AndroidGeckoSoftwareLayerClient mSoftwareLayerClient; // the GeckoAppShell java class jclass mGeckoAppShellClass; diff --git a/widget/src/android/AndroidJNI.cpp b/widget/src/android/AndroidJNI.cpp index 92df3db0b25e..bed63aede46b 100644 --- a/widget/src/android/AndroidJNI.cpp +++ b/widget/src/android/AndroidJNI.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include "nsAppShell.h" #include "nsWindow.h" @@ -67,7 +68,7 @@ extern "C" { NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_nativeInit(JNIEnv *, jclass); NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_notifyGeckoOfEvent(JNIEnv *, jclass, jobject event); NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_processNextNativeEvent(JNIEnv *, jclass); - NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_setSurfaceView(JNIEnv *jenv, jclass, jobject sv); + NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_setSoftwareLayerClient(JNIEnv *jenv, jclass, jobject sv); NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_onResume(JNIEnv *, jclass); NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_onLowMemory(JNIEnv *, jclass); NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_callObserver(JNIEnv *, jclass, jstring observerKey, jstring topic, jstring data); @@ -87,6 +88,7 @@ extern "C" { NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_nativeInit(JNIEnv *jenv, jclass jc) { + ALOG("Native init!"); AndroidBridge::ConstructBridge(jenv, jc); } @@ -94,8 +96,9 @@ NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_notifyGeckoOfEvent(JNIEnv *jenv, jclass jc, jobject event) { // poke the appshell - if (nsAppShell::gAppShell) + if (nsAppShell::gAppShell) { nsAppShell::gAppShell->PostEvent(new AndroidGeckoEvent(jenv, event)); + } } NS_EXPORT void JNICALL @@ -107,9 +110,11 @@ Java_org_mozilla_gecko_GeckoAppShell_processNextNativeEvent(JNIEnv *jenv, jclass } NS_EXPORT void JNICALL -Java_org_mozilla_gecko_GeckoAppShell_setSurfaceView(JNIEnv *jenv, jclass, jobject obj) +Java_org_mozilla_gecko_GeckoAppShell_setSoftwareLayerClient(JNIEnv *jenv, jclass, jobject obj) { - AndroidBridge::Bridge()->SetSurfaceView(jenv->NewGlobalRef(obj)); + ALOG("setSoftwareLayerClient before"); + AndroidBridge::Bridge()->SetSoftwareLayerClient(jenv->NewGlobalRef(obj)); + ALOG("setSoftwareLayerClient after"); } NS_EXPORT void JNICALL diff --git a/widget/src/android/AndroidJavaWrappers.cpp b/widget/src/android/AndroidJavaWrappers.cpp index 5a1ca90e7abf..c6cb02d74212 100644 --- a/widget/src/android/AndroidJavaWrappers.cpp +++ b/widget/src/android/AndroidJavaWrappers.cpp @@ -103,15 +103,11 @@ jmethodID AndroidAddress::jGetSubLocalityMethod; jmethodID AndroidAddress::jGetSubThoroughfareMethod; jmethodID AndroidAddress::jGetThoroughfareMethod; -jclass AndroidGeckoSurfaceView::jGeckoSurfaceViewClass = 0; -jmethodID AndroidGeckoSurfaceView::jBeginDrawingMethod = 0; -jmethodID AndroidGeckoSurfaceView::jEndDrawingMethod = 0; -jmethodID AndroidGeckoSurfaceView::jDraw2DBitmapMethod = 0; -jmethodID AndroidGeckoSurfaceView::jDraw2DBufferMethod = 0; -jmethodID AndroidGeckoSurfaceView::jGetSoftwareDrawBitmapMethod = 0; -jmethodID AndroidGeckoSurfaceView::jGetSoftwareDrawBufferMethod = 0; -jmethodID AndroidGeckoSurfaceView::jGetSurfaceMethod = 0; -jmethodID AndroidGeckoSurfaceView::jGetHolderMethod = 0; +jclass AndroidGeckoSoftwareLayerClient::jGeckoSoftwareLayerClientClass = 0; +jmethodID AndroidGeckoSoftwareLayerClient::jLockBufferMethod = 0; +jmethodID AndroidGeckoSoftwareLayerClient::jUnlockBufferMethod = 0; +jmethodID AndroidGeckoSoftwareLayerClient::jBeginDrawingMethod = 0; +jmethodID AndroidGeckoSoftwareLayerClient::jEndDrawingMethod = 0; #define JNI() (AndroidBridge::JNI()) @@ -131,10 +127,11 @@ void mozilla::InitAndroidJavaWrappers(JNIEnv *jEnv) { AndroidGeckoEvent::InitGeckoEventClass(jEnv); - AndroidGeckoSurfaceView::InitGeckoSurfaceViewClass(jEnv); AndroidPoint::InitPointClass(jEnv); + AndroidRect::InitRectClass(jEnv); AndroidLocation::InitLocationClass(jEnv); AndroidAddress::InitAddressClass(jEnv); + AndroidGeckoSoftwareLayerClient::InitGeckoSoftwareLayerClientClass(jEnv); } void @@ -173,23 +170,6 @@ AndroidGeckoEvent::InitGeckoEventClass(JNIEnv *jEnv) jAddressField = getField("mAddress", "Landroid/location/Address;"); } -void -AndroidGeckoSurfaceView::InitGeckoSurfaceViewClass(JNIEnv *jEnv) -{ - initInit(); - - jGeckoSurfaceViewClass = getClassGlobalRef("org/mozilla/gecko/GeckoSurfaceView"); - - jBeginDrawingMethod = getMethod("beginDrawing", "()I"); - jGetSoftwareDrawBitmapMethod = getMethod("getSoftwareDrawBitmap", "()Landroid/graphics/Bitmap;"); - jGetSoftwareDrawBufferMethod = getMethod("getSoftwareDrawBuffer", "()Ljava/nio/ByteBuffer;"); - jEndDrawingMethod = getMethod("endDrawing", "()V"); - jDraw2DBitmapMethod = getMethod("draw2D", "(Landroid/graphics/Bitmap;II)V"); - jDraw2DBufferMethod = getMethod("draw2D", "(Ljava/nio/ByteBuffer;I)V"); - jGetSurfaceMethod = getMethod("getSurface", "()Landroid/view/Surface;"); - jGetHolderMethod = getMethod("getHolder", "()Landroid/view/SurfaceHolder;"); -} - void AndroidLocation::InitLocationClass(JNIEnv *jEnv) { @@ -304,6 +284,20 @@ AndroidRect::InitRectClass(JNIEnv *jEnv) jRightField = getField("right", "I"); } +void +AndroidGeckoSoftwareLayerClient::InitGeckoSoftwareLayerClientClass(JNIEnv *jEnv) +{ + initInit(); + + jGeckoSoftwareLayerClientClass = + getClassGlobalRef("org/mozilla/gecko/gfx/GeckoSoftwareLayerClient"); + + jLockBufferMethod = getMethod("lockBuffer", "()Ljava/nio/ByteBuffer;"); + jUnlockBufferMethod = getMethod("unlockBuffer", "()V"); + jBeginDrawingMethod = getMethod("beginDrawing", "()V"); + jEndDrawingMethod = getMethod("endDrawing", "(IIII)V"); +} + #undef initInit #undef initClassGlobalRef #undef getField @@ -423,7 +417,9 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jobject jobj) break; case DRAW: + ALOG("### Draw, before ReadRectField"); ReadRectField(jenv); + ALOG("### Draw, after ReadRectField"); break; case ORIENTATION_EVENT: @@ -480,10 +476,10 @@ AndroidGeckoEvent::Init(int aType) } void -AndroidGeckoEvent::Init(int x1, int y1, int x2, int y2) +AndroidGeckoEvent::Init(int aType, const nsIntRect &aRect) { - mType = DRAW; - mRect.SetEmpty(); + mType = aType; + mRect = aRect; } void @@ -499,64 +495,6 @@ AndroidGeckoEvent::Init(AndroidGeckoEvent *aResizeEvent) mP1.y = aResizeEvent->mP1.y; } -void -AndroidGeckoSurfaceView::Init(jobject jobj) -{ - NS_ASSERTION(wrapped_obj == nsnull, "Init called on non-null wrapped_obj!"); - - wrapped_obj = jobj; -} - -int -AndroidGeckoSurfaceView::BeginDrawing() -{ - NS_ASSERTION(!isNull(), "BeginDrawing called on null surfaceview!"); - - return JNI()->CallIntMethod(wrapped_obj, jBeginDrawingMethod); -} - -void -AndroidGeckoSurfaceView::EndDrawing() -{ - JNI()->CallVoidMethod(wrapped_obj, jEndDrawingMethod); -} - -void -AndroidGeckoSurfaceView::Draw2D(jobject bitmap, int width, int height) -{ - JNI()->CallVoidMethod(wrapped_obj, jDraw2DBitmapMethod, bitmap, width, height); -} - -void -AndroidGeckoSurfaceView::Draw2D(jobject buffer, int stride) -{ - JNI()->CallVoidMethod(wrapped_obj, jDraw2DBufferMethod, buffer, stride); -} - -jobject -AndroidGeckoSurfaceView::GetSoftwareDrawBitmap() -{ - return JNI()->CallObjectMethod(wrapped_obj, jGetSoftwareDrawBitmapMethod); -} - -jobject -AndroidGeckoSurfaceView::GetSoftwareDrawBuffer() -{ - return JNI()->CallObjectMethod(wrapped_obj, jGetSoftwareDrawBufferMethod); -} - -jobject -AndroidGeckoSurfaceView::GetSurface() -{ - return JNI()->CallObjectMethod(wrapped_obj, jGetSurfaceMethod); -} - -jobject -AndroidGeckoSurfaceView::GetSurfaceHolder() -{ - return JNI()->CallObjectMethod(wrapped_obj, jGetHolderMethod); -} - void AndroidPoint::Init(JNIEnv *jenv, jobject jobj) { @@ -574,17 +512,72 @@ AndroidPoint::Init(JNIEnv *jenv, jobject jobj) } void -AndroidRect::Init(JNIEnv *jenv, jobject jobj) +AndroidGeckoSoftwareLayerClient::Init(jobject jobj) { NS_ASSERTION(wrapped_obj == nsnull, "Init called on non-null wrapped_obj!"); wrapped_obj = jobj; +} + +jobject +AndroidGeckoSoftwareLayerClient::LockBuffer() +{ + NS_ASSERTION(!isNull(), "LockBuffer() called on null software layer client!"); + + return JNI()->CallObjectMethod(wrapped_obj, jLockBufferMethod); +} + +unsigned char * +AndroidGeckoSoftwareLayerClient::LockBufferBits() +{ + return reinterpret_cast(JNI()->GetDirectBufferAddress(LockBuffer())); +} + +void +AndroidGeckoSoftwareLayerClient::UnlockBuffer() +{ + NS_ASSERTION(!isNull(), "UnlockBuffer() called on null software layer client!"); + + JNI()->CallVoidMethod(wrapped_obj, jUnlockBufferMethod); +} + +void +AndroidGeckoSoftwareLayerClient::BeginDrawing() +{ + NS_ASSERTION(!isNull(), "BeginDrawing() called on null software layer client!"); + + return JNI()->CallVoidMethod(wrapped_obj, jBeginDrawingMethod); +} + +void +AndroidGeckoSoftwareLayerClient::EndDrawing(const nsIntRect &aRect) +{ + NS_ASSERTION(!isNull(), "EndDrawing() called on null software layer client!"); + + return JNI()->CallVoidMethod(wrapped_obj, jEndDrawingMethod, aRect.x, aRect.y, aRect.width, + aRect.height); +} + +void +AndroidRect::Init(JNIEnv *jenv, jobject jobj) +{ + NS_ASSERTION(wrapped_obj == nsnull, "Init called on non-null wrapped_obj!"); + + ALOG("AndroidRect::Init point a"); + + wrapped_obj = jobj; + + ALOG("AndroidRect::Init point b"); if (jobj) { + ALOG("AndroidRect::Init point c"); + mTop = jenv->GetIntField(jobj, jTopField); mLeft = jenv->GetIntField(jobj, jLeftField); mRight = jenv->GetIntField(jobj, jRightField); mBottom = jenv->GetIntField(jobj, jBottomField); + + ALOG("AndroidRect::Init point d"); } else { mTop = 0; mLeft = 0; diff --git a/widget/src/android/AndroidJavaWrappers.h b/widget/src/android/AndroidJavaWrappers.h index 9467f7967312..afec2458d358 100644 --- a/widget/src/android/AndroidJavaWrappers.h +++ b/widget/src/android/AndroidJavaWrappers.h @@ -46,7 +46,7 @@ #include "nsRect.h" #include "nsString.h" -//#define FORCE_ALOG 1 +#define FORCE_ALOG 1 #ifndef ALOG #if defined(DEBUG) || defined(FORCE_ALOG) @@ -149,48 +149,27 @@ protected: static jfieldID jTopField; }; -class AndroidGeckoSurfaceView : public WrappedJavaObject -{ +class AndroidGeckoSoftwareLayerClient : public WrappedJavaObject { public: - static void InitGeckoSurfaceViewClass(JNIEnv *jEnv); - - AndroidGeckoSurfaceView() { } - AndroidGeckoSurfaceView(jobject jobj) { - Init(jobj); - } + static void InitGeckoSoftwareLayerClientClass(JNIEnv *jEnv); void Init(jobject jobj); - enum { - DRAW_ERROR = 0, - DRAW_GLES_2 = 1, - DRAW_2D = 2, - DRAW_DISABLED = 3 - }; + AndroidGeckoSoftwareLayerClient() {} + AndroidGeckoSoftwareLayerClient(jobject jobj) { Init(jobj); } - int BeginDrawing(); - jobject GetSoftwareDrawBitmap(); - jobject GetSoftwareDrawBuffer(); - void EndDrawing(); - void Draw2D(jobject bitmap, int width, int height); - void Draw2D(jobject buffer, int stride); + jobject LockBuffer(); + unsigned char *LockBufferBits(); + void UnlockBuffer(); + void BeginDrawing(); + void EndDrawing(const nsIntRect &aRect); - jobject GetSurface(); - - // must have a JNI local frame when calling this, - // and you'd better know what you're doing - jobject GetSurfaceHolder(); - -protected: - static jclass jGeckoSurfaceViewClass; +private: + static jclass jGeckoSoftwareLayerClientClass; + static jmethodID jLockBufferMethod; + static jmethodID jUnlockBufferMethod; static jmethodID jBeginDrawingMethod; static jmethodID jEndDrawingMethod; - static jmethodID jDraw2DBitmapMethod; - static jmethodID jDraw2DBufferMethod; - static jmethodID jGetSoftwareDrawBitmapMethod; - static jmethodID jGetSoftwareDrawBufferMethod; - static jmethodID jGetSurfaceMethod; - static jmethodID jGetHolderMethod; }; class AndroidKeyEvent @@ -385,8 +364,8 @@ public: AndroidGeckoEvent(int aType) { Init(aType); } - AndroidGeckoEvent(int x1, int y1, int x2, int y2) { - Init(x1, y1, x2, y2); + AndroidGeckoEvent(int aType, const nsIntRect &aRect) { + Init(aType, aRect); } AndroidGeckoEvent(JNIEnv *jenv, jobject jobj) { Init(jenv, jobj); @@ -397,7 +376,7 @@ public: void Init(JNIEnv *jenv, jobject jobj); void Init(int aType); - void Init(int x1, int y1, int x2, int y2); + void Init(int aType, const nsIntRect &aRect); void Init(AndroidGeckoEvent *aResizeEvent); int Action() { return mAction; } diff --git a/widget/src/android/nsAppShell.cpp b/widget/src/android/nsAppShell.cpp index 9d50114cadca..4bce7137cb7a 100644 --- a/widget/src/android/nsAppShell.cpp +++ b/widget/src/android/nsAppShell.cpp @@ -60,6 +60,8 @@ #include "prlog.h" #endif +#define DEBUG_ANDROID_EVENTS 1 + #ifdef DEBUG_ANDROID_EVENTS #define EVLOG(args...) ALOG(args) #else diff --git a/widget/src/android/nsWindow.cpp b/widget/src/android/nsWindow.cpp index 262bc648e131..3e1ddabb35a2 100644 --- a/widget/src/android/nsWindow.cpp +++ b/widget/src/android/nsWindow.cpp @@ -76,6 +76,12 @@ using mozilla::unused; #include "imgIEncoder.h" +#include "nsStringGlue.h" + +// NB: Keep these in sync with LayerController.java in embedding/android/. +#define TILE_WIDTH 1024 +#define TILE_HEIGHT 2048 + using namespace mozilla; NS_IMPL_ISUPPORTS_INHERITED0(nsWindow, nsBaseWidget) @@ -135,7 +141,6 @@ static nsRefPtr sGLContext; static bool sFailedToCreateGLContext = false; static bool sValidSurface; static bool sSurfaceExists = false; -static void *sNativeWindow = nsnull; // Multitouch swipe thresholds in inches static const double SWIPE_MAX_PINCH_DELTA_INCHES = 0.4; @@ -296,6 +301,15 @@ nsWindow::ConfigureChildren(const nsTArray& config) return NS_OK; } +void +nsWindow::RedrawAll() +{ + nsIntRect entireRect(0, 0, TILE_WIDTH, TILE_HEIGHT); + AndroidGeckoEvent *event = new AndroidGeckoEvent(AndroidGeckoEvent::DRAW, + entireRect); + nsAppShell::gAppShell->PostEvent(event); +} + NS_IMETHODIMP nsWindow::SetParent(nsIWidget *aNewParent) { @@ -316,7 +330,7 @@ nsWindow::SetParent(nsIWidget *aNewParent) // if we are now in the toplevel window's hierarchy, schedule a redraw if (FindTopLevel() == TopWindow()) - nsAppShell::gAppShell->PostEvent(new AndroidGeckoEvent(-1, -1, -1, -1)); + RedrawAll(); return NS_OK; } @@ -384,7 +398,7 @@ nsWindow::Show(bool aState) } } } else if (FindTopLevel() == TopWindow()) { - nsAppShell::gAppShell->PostEvent(new AndroidGeckoEvent(-1, -1, -1, -1)); + RedrawAll(); } #ifdef ACCESSIBILITY @@ -511,7 +525,7 @@ nsWindow::Resize(PRInt32 aX, // Should we skip honoring aRepaint here? if (aRepaint && FindTopLevel() == TopWindow()) - nsAppShell::gAppShell->PostEvent(new AndroidGeckoEvent(-1, -1, -1, -1)); + RedrawAll(); return NS_OK; } @@ -564,7 +578,8 @@ NS_IMETHODIMP nsWindow::Invalidate(const nsIntRect &aRect, bool aIsSynchronous) { - nsAppShell::gAppShell->PostEvent(new AndroidGeckoEvent(-1, -1, -1, -1)); + AndroidGeckoEvent *event = new AndroidGeckoEvent(AndroidGeckoEvent::DRAW, aRect); + nsAppShell::gAppShell->PostEvent(event); return NS_OK; } @@ -633,7 +648,7 @@ nsWindow::BringToFront() // force a window resize nsAppShell::gAppShell->ResendLastResizeEvent(this); - nsAppShell::gAppShell->PostEvent(new AndroidGeckoEvent(-1, -1, -1, -1)); + RedrawAll(); } NS_IMETHODIMP @@ -797,7 +812,8 @@ nsWindow::DrawToFile(const nsAString &path) return PR_FALSE; } - bool result = DrawTo(imgSurface); + nsIntRect boundsRect(0, 0, mBounds.width, mBounds.height); + bool result = DrawTo(imgSurface, boundsRect); NS_ENSURE_TRUE(result, PR_FALSE); nsCOMPtr encoder = do_CreateInstance("@mozilla.org/image/encoder;2?type=image/png"); @@ -957,27 +973,12 @@ nsWindow::OnGlobalAndroidEvent(AndroidGeckoEvent *ae) case AndroidGeckoEvent::SURFACE_CREATED: sSurfaceExists = true; - - if (AndroidBridge::Bridge()->HasNativeWindowAccess()) { - AndroidGeckoSurfaceView& sview(AndroidBridge::Bridge()->SurfaceView()); - jobject surface = sview.GetSurface(); - if (surface) { - sNativeWindow = AndroidBridge::Bridge()->AcquireNativeWindow(surface); - if (sNativeWindow) { - AndroidBridge::Bridge()->SetNativeWindowFormat(sNativeWindow, AndroidBridge::WINDOW_FORMAT_RGB_565); - } - } - } break; case AndroidGeckoEvent::SURFACE_DESTROYED: if (sGLContext && sValidSurface) { sGLContext->ReleaseSurface(); } - if (sNativeWindow) { - AndroidBridge::Bridge()->ReleaseNativeWindow(sNativeWindow); - sNativeWindow = nsnull; - } sSurfaceExists = false; sValidSurface = false; break; @@ -1013,11 +1014,15 @@ nsWindow::OnAndroidEvent(AndroidGeckoEvent *ae) } bool -nsWindow::DrawTo(gfxASurface *targetSurface) +nsWindow::DrawTo(gfxASurface *targetSurface, const nsIntRect &invalidRect) { if (!mIsVisible) return false; + nsWindowType windowType; + GetWindowType(windowType); + ALOG("Window type is %d", (int)windowType); + nsEventStatus status; nsIntRect boundsRect(0, 0, mBounds.width, mBounds.height); @@ -1035,7 +1040,7 @@ nsWindow::DrawTo(gfxASurface *targetSurface) // If we have no covering child, then we need to render this. if (coveringChildIndex == -1) { nsPaintEvent event(true, NS_PAINT, this); - event.region = boundsRect; + event.region = boundsRect.Intersect(invalidRect); switch (GetLayerManager(nsnull)->GetBackendType()) { case LayerManager::LAYERS_BASIC: { nsRefPtr ctx = new gfxContext(targetSurface); @@ -1090,7 +1095,7 @@ nsWindow::DrawTo(gfxASurface *targetSurface) targetSurface->SetDeviceOffset(offset + gfxPoint(mChildren[i]->mBounds.x, mChildren[i]->mBounds.y)); - bool ok = mChildren[i]->DrawTo(targetSurface); + bool ok = mChildren[i]->DrawTo(targetSurface, invalidRect); if (!ok) { ALOG("nsWindow[%p]::DrawTo child %d[%p] returned FALSE!", (void*) this, i, (void*)mChildren[i]); @@ -1106,11 +1111,6 @@ nsWindow::DrawTo(gfxASurface *targetSurface) void nsWindow::OnDraw(AndroidGeckoEvent *ae) { - - if (!sSurfaceExists) { - return; - } - if (!IsTopLevel()) { ALOG("##### redraw for window %p, which is not a toplevel window -- sending to toplevel!", (void*) this); DumpWindows(); @@ -1125,96 +1125,27 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae) AndroidBridge::AutoLocalJNIFrame jniFrame; - AndroidGeckoSurfaceView& sview(AndroidBridge::Bridge()->SurfaceView()); - - NS_ASSERTION(!sview.isNull(), "SurfaceView is null!"); - if (GetLayerManager(nsnull)->GetBackendType() == LayerManager::LAYERS_BASIC) { - if (sNativeWindow) { - unsigned char *bits; - int width, height, format, stride; - if (!AndroidBridge::Bridge()->LockWindow(sNativeWindow, &bits, &width, &height, &format, &stride)) { - ALOG("failed to lock buffer - skipping draw"); - return; - } + AndroidGeckoSoftwareLayerClient &client = + AndroidBridge::Bridge()->GetSoftwareLayerClient(); - if (!bits || format != AndroidBridge::WINDOW_FORMAT_RGB_565 || - width != mBounds.width || height != mBounds.height) { + client.BeginDrawing(); + unsigned char *bits = client.LockBufferBits(); - ALOG("surface is not expected dimensions or format - skipping draw"); - AndroidBridge::Bridge()->UnlockWindow(sNativeWindow); - return; - } - - nsRefPtr targetSurface = - new gfxImageSurface(bits, - gfxIntSize(mBounds.width, mBounds.height), - stride * 2, - gfxASurface::ImageFormatRGB16_565); - if (targetSurface->CairoStatus()) { - ALOG("### Failed to create a valid surface from the bitmap"); - } else { - DrawTo(targetSurface); - } - - AndroidBridge::Bridge()->UnlockWindow(sNativeWindow); - } else if (AndroidBridge::Bridge()->HasNativeBitmapAccess()) { - jobject bitmap = sview.GetSoftwareDrawBitmap(); - if (!bitmap) { - ALOG("no bitmap to draw into - skipping draw"); - return; - } - - if (!AndroidBridge::Bridge()->ValidateBitmap(bitmap, mBounds.width, mBounds.height)) - return; - - void *buf = AndroidBridge::Bridge()->LockBitmap(bitmap); - if (buf == nsnull) { - ALOG("### Software drawing, but failed to lock bitmap."); - return; - } - - nsRefPtr targetSurface = - new gfxImageSurface((unsigned char *)buf, - gfxIntSize(mBounds.width, mBounds.height), - mBounds.width * 2, - gfxASurface::ImageFormatRGB16_565); - if (targetSurface->CairoStatus()) { - ALOG("### Failed to create a valid surface from the bitmap"); - } else { - DrawTo(targetSurface); - } - - AndroidBridge::Bridge()->UnlockBitmap(bitmap); - sview.Draw2D(bitmap, mBounds.width, mBounds.height); + nsRefPtr targetSurface = + new gfxImageSurface(bits, gfxIntSize(TILE_WIDTH, TILE_HEIGHT), TILE_WIDTH * 2, + gfxASurface::ImageFormatRGB16_565); + if (targetSurface->CairoStatus()) { + ALOG("### Failed to create a valid surface from the bitmap"); } else { - jobject bytebuf = sview.GetSoftwareDrawBuffer(); - if (!bytebuf) { - ALOG("no buffer to draw into - skipping draw"); - return; - } - - void *buf = AndroidBridge::JNI()->GetDirectBufferAddress(bytebuf); - int cap = AndroidBridge::JNI()->GetDirectBufferCapacity(bytebuf); - if (!buf || cap != (mBounds.width * mBounds.height * 2)) { - ALOG("### Software drawing, but unexpected buffer size %d expected %d (or no buffer %p)!", cap, mBounds.width * mBounds.height * 2, buf); - return; - } - - nsRefPtr targetSurface = - new gfxImageSurface((unsigned char *)buf, - gfxIntSize(mBounds.width, mBounds.height), - mBounds.width * 2, - gfxASurface::ImageFormatRGB16_565); - if (targetSurface->CairoStatus()) { - ALOG("### Failed to create a valid surface"); - } else { - DrawTo(targetSurface); - } - - sview.Draw2D(bytebuf, mBounds.width * 2); + DrawTo(targetSurface, ae->Rect()); } + + client.UnlockBuffer(); + client.EndDrawing(ae->Rect()); } else { + ALOG("### GL layers are disabled for now in the native UI Fennec"); +#if 0 int drawType = sview.BeginDrawing(); if (drawType == AndroidGeckoSurfaceView::DRAW_DISABLED) { @@ -1234,9 +1165,10 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae) NS_ASSERTION(sGLContext, "Drawing with GLES without a GL context?"); - DrawTo(nsnull); + DrawTo(nsnull, ae->P0(), ae->Alpha()); sview.EndDrawing(); +#endif } } diff --git a/widget/src/android/nsWindow.h b/widget/src/android/nsWindow.h index e9140ceacb39..bee1bfd694cb 100644 --- a/widget/src/android/nsWindow.h +++ b/widget/src/android/nsWindow.h @@ -178,7 +178,7 @@ public: protected: void BringToFront(); nsWindow *FindTopLevel(); - bool DrawTo(gfxASurface *targetSurface); + bool DrawTo(gfxASurface *targetSurface, const nsIntRect &aRect); bool DrawToFile(const nsAString &path); bool IsTopLevel(); void OnIMEAddRange(mozilla::AndroidGeckoEvent *ae); @@ -220,6 +220,7 @@ private: void DispatchGestureEvent(PRUint32 msg, PRUint32 direction, double delta, const nsIntPoint &refPoint, PRUint64 time); void HandleSpecialKey(mozilla::AndroidGeckoEvent *ae); + void RedrawAll(); #ifdef ACCESSIBILITY nsRefPtr mRootAccessible;