diff --git a/support/android/apk/app/build.gradle b/support/android/apk/app/build.gradle index d4e3530311e..92af29e9369 100644 --- a/support/android/apk/app/build.gradle +++ b/support/android/apk/app/build.gradle @@ -211,6 +211,9 @@ dependencies { googlevrCompile 'com.google.vr:sdk-base:1.70.0' googlevrCompile(name:'GVRService', ext:'aar') oculusvrCompile(name:'OVRService', ext:'aar') + + // compile is deprecated. Will become "implementation" once we upgrade graddle. + compile 'com.android.support.constraint:constraint-layout:1.0.0' } // Utility methods diff --git a/support/android/apk/app/src/main/AndroidManifest.xml b/support/android/apk/app/src/main/AndroidManifest.xml index 355c8d4efc4..727b68871ec 100644 --- a/support/android/apk/app/src/main/AndroidManifest.xml +++ b/support/android/apk/app/src/main/AndroidManifest.xml @@ -3,13 +3,13 @@ - + - - + + = android.os.Build.VERSION_CODES.O) { + File sdcard = getExternalFilesDir(""); + String host = sdcard.toPath().resolve("android_hosts").toString(); + try { + Os.setenv("HOST_FILE", host, false); + } catch (ErrnoException e) { + e.printStackTrace(); + } } - // Handle full screen preference - if (mFullScreen) { - addFullScreenListener(); + String args = getIntent().getStringExtra("servoargs"); + if (args != null) { + mServoView.setServoArgs(args); } + + setupUrlField(); } - @Override - protected void onStop() { - Log.d(LOGTAG, "onStop"); - super.onStop(); + private void setupUrlField() { + mUrlField.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + loadUrlFromField(); + mServoView.requestFocus(); + return true; + } + return false; + }); + mUrlField.setOnFocusChangeListener((v, hasFocus) -> { + if(v.getId() == R.id.urlfield && !hasFocus) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + assert imm != null; + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + }); } - @Override - protected void onPause() { - Log.d(LOGTAG, "onPause"); - super.onPause(); - } + private void loadUrlFromField() { + String text = mUrlField.getText().toString(); + text = text.trim(); + String uri; - @Override - protected void onResume() { - Log.d(LOGTAG, "onPause"); - if (mFullScreen) { - setFullScreen(); - } - super.onResume(); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (hasFocus && mFullScreen) { - setFullScreen(); - } - } - - // keep the device's screen turned on and bright. - private void keepScreenOn() { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - // Dim toolbar and make the view fullscreen - private void setFullScreen() { - int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // Hides navigation bar - | View.SYSTEM_UI_FLAG_FULLSCREEN; // Hides status bar - if( android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { - flags |= getImmersiveFlag(); + if (text.contains(" ") || !text.contains(".")) { + uri = URLUtil.composeSearchUrl(text, "https://duckduckgo.com/html/?q=%s", "%s"); } else { - flags |= View.SYSTEM_UI_FLAG_LOW_PROFILE; + uri = URLUtil.guessUrl(text); } - getWindow().getDecorView().setSystemUiVisibility(flags); + + mServoView.loadUri(Uri.parse(uri)); } - @TargetApi(19) - private int getImmersiveFlag() { - return View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + public void onReloadClicked(View v) { + mServoView.reload(); } - private void addFullScreenListener() { - View decorView = getWindow().getDecorView(); - decorView.setOnSystemUiVisibilityChangeListener( - new View.OnSystemUiVisibilityChangeListener() { - public void onSystemUiVisibilityChange(int visibility) { - if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - setFullScreen(); - } - } - }); + public void onBackClicked(View v) { + mServoView.goBack(); } - private String loadAsset(String file) { - InputStream is = null; - BufferedReader reader = null; - try { - is = getAssets().open(file); - reader = new BufferedReader(new InputStreamReader(is)); - StringBuilder result = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - result.append(line).append('\n'); - } - return result.toString(); - } catch (IOException e) { - Log.e(LOGTAG, Log.getStackTraceString(e)); - return null; - } - finally { - try { - if (reader != null) { - reader.close(); - } - if (is != null) { - is.close(); - } - } catch (Exception e) { - Log.e(LOGTAG, Log.getStackTraceString(e)); - } - } + public void onForwardClicked(View v) { + mServoView.goForward(); } - private JSONObject loadPreferences() { - String json = loadAsset("prefs.json"); - try { - return new JSONObject(json); - } catch (JSONException e) { - Log.e(LOGTAG, Log.getStackTraceString(e)); - return new JSONObject(); - } + public void onStopClicked(View v) { + mServoView.stop(); } - private File getAppDataDir() { - File file = getExternalFilesDir(null); - return file != null ? file : getFilesDir(); + @Override + public void onLoadStarted() { + mReloadButton.setEnabled(false); + mStopButton.setEnabled(true); + mReloadButton.setVisibility(View.GONE); + mStopButton.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.VISIBLE); } - private void set_url(String url) { - try { - File file = new File(getAppDataDir() + "/android_params"); - if (!file.exists()) { - file.createNewFile(); - } - PrintStream out = new PrintStream(new FileOutputStream(file, false)); - out.println("# The first line here should be the \"servo\" argument (without quotes) and the"); - out.println("# last should be the URL to load."); - out.println("# Blank lines and those beginning with a '#' are ignored."); - out.println("# Each line should be a separate parameter as would be parsed by the shell."); - out.println("# For example, \"servo -p 10 http://en.wikipedia.org/wiki/Rust\" would take 4"); - out.println("# lines (the \"-p\" and \"10\" are separate even though they are related)."); - out.println("servo"); - out.println("-w"); - String absUrl = url.replace("file:///storage/emulated/0/", "/sdcard/"); - out.println(absUrl); - out.flush(); - out.close(); - } catch (Exception e) { - Log.e(LOGTAG, Log.getStackTraceString(e)); - } + @Override + public void onLoadEnded() { + mReloadButton.setEnabled(true); + mStopButton.setEnabled(false); + mReloadButton.setVisibility(View.VISIBLE); + mStopButton.setVisibility(View.GONE); + mProgressBar.setVisibility(View.INVISIBLE); } + + @Override + public void onTitleChanged(String title) { + } + + @Override + public void onUrlChanged(String url) { + mUrlField.setText(url); + } + + @Override + public void onHistoryChanged(boolean canGoBack, boolean canGoForward) { + mBackButton.setEnabled(canGoBack); + mFwdButton.setEnabled(canGoForward); + } + } diff --git a/support/android/apk/app/src/main/java/com/mozilla/servoview/NativeServo.java b/support/android/apk/app/src/main/java/com/mozilla/servoview/NativeServo.java new file mode 100644 index 00000000000..51f27b24d6f --- /dev/null +++ b/support/android/apk/app/src/main/java/com/mozilla/servoview/NativeServo.java @@ -0,0 +1,57 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package com.mozilla.servoview; + +import android.app.Activity; + +/** + * Maps /ports/libsimpleservo API + */ +public class NativeServo { + public native String version(); + public native void init(Activity activity, + String args, + String url, + WakeupCallback wakeup, + ReadFileCallback readfile, + ServoCallbacks callbacks, + int width, int height, boolean log); + public native void setBatchMode(boolean mode); + public native void performUpdates(); + public native void resize(int width, int height); + public native void reload(); + public native void stop(); + public native void goBack(); + public native void goForward(); + public native void loadUri(String uri); + public native void scrollStart(int dx, int dy, int x, int y); + public native void scroll(int dx, int dy, int x, int y); + public native void scrollEnd(int dx, int dy, int x, int y); + public native void click(int x, int y); + + NativeServo() { + System.loadLibrary("c++_shared"); + System.loadLibrary("simpleservo"); + } + + public interface ReadFileCallback { + byte[] readfile(String file); + } + + public interface WakeupCallback { + void wakeup(); + } + + public interface ServoCallbacks { + void flush(); + void onLoadStarted(); + void onLoadEnded(); + void onTitleChanged(String title); + void onUrlChanged(String url); + void onHistoryChanged(boolean canGoBack, boolean canGoForward); + void onAnimatingChanged(boolean animating); + } +} diff --git a/support/android/apk/app/src/main/java/com/mozilla/servoview/ServoGLRenderer.java b/support/android/apk/app/src/main/java/com/mozilla/servoview/ServoGLRenderer.java new file mode 100644 index 00000000000..dc06c14e512 --- /dev/null +++ b/support/android/apk/app/src/main/java/com/mozilla/servoview/ServoGLRenderer.java @@ -0,0 +1,32 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package com.mozilla.servoview; + +import android.opengl.GLES31; +import android.opengl.GLSurfaceView; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +public class ServoGLRenderer implements GLSurfaceView.Renderer { + + private final ServoView mView; + + ServoGLRenderer(ServoView view) { + mView = view; + } + + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + mView.onGLReady(); + } + + public void onDrawFrame(GL10 unused) { + } + + public void onSurfaceChanged(GL10 unused, int width, int height) { + GLES31.glViewport(0, 0, width, height); + mView.onSurfaceResized(width, height); + } +} diff --git a/support/android/apk/app/src/main/java/com/mozilla/servoview/ServoView.java b/support/android/apk/app/src/main/java/com/mozilla/servoview/ServoView.java new file mode 100644 index 00000000000..2cf2c257a78 --- /dev/null +++ b/support/android/apk/app/src/main/java/com/mozilla/servoview/ServoView.java @@ -0,0 +1,281 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package com.mozilla.servoview; + +import android.app.Activity; +import android.os.Build; +import android.content.Context; +import android.content.res.AssetManager; +import android.net.Uri; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Choreographer; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.widget.OverScroller; +import java.io.IOException; +import java.io.InputStream; + +public class ServoView extends GLSurfaceView implements GestureDetector.OnGestureListener, Choreographer.FrameCallback { + + private static final String LOGTAG = "ServoView"; + + private Activity mActivity; + private NativeServo mServo; + private Client mClient = null; + private Uri mInitialUri = null; + private boolean mAnimating; + private String mServoArgs = ""; + + public ServoView(Context context, AttributeSet attrs) { + super(context, attrs); + mActivity = (Activity) context; + setFocusable(true); + setFocusableInTouchMode(true); + setWillNotCacheDrawing(false); + setEGLContextClientVersion(3); + setEGLConfigChooser(8, 8, 8, 8, 24, 0); + ServoGLRenderer mRenderer = new ServoGLRenderer(this); + setRenderer(mRenderer); + mServo = new NativeServo(); + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + initGestures(context); + } + + public void setServoArgs(String args) { + mServoArgs = args; + } + + public void reload() { + queueEvent(() -> mServo.reload()); + } + + public void goBack() { + queueEvent(() -> mServo.goBack()); + } + + public void goForward() { + queueEvent(() -> mServo.goForward()); + } + + public void stop() { + queueEvent(() -> mServo.stop()); + } + + public void onSurfaceResized(int width, int height) { + queueEvent(() -> mServo.resize(width, height)); + } + + public void loadUri(Uri uri) { + if (mServo != null) { + queueEvent(() -> mServo.loadUri(uri.toString())); + } else { + mInitialUri = uri; + } + } + + class WakeupCallback implements NativeServo.WakeupCallback { + public void wakeup() { + queueEvent(() -> mServo.performUpdates()); + }; + } + + class ReadFileCallback implements NativeServo.ReadFileCallback { + public byte[] readfile(String file) { + try { + AssetManager assetMgr = getContext().getResources().getAssets(); + InputStream stream = assetMgr.open(file); + byte[] bytes = new byte[stream.available()]; + stream.read(bytes); + stream.close(); + return bytes; + } catch (IOException e) { + Log.e(LOGTAG, e.getMessage()); + return null; + } + } + } + + class ServoCallbacks implements NativeServo.ServoCallbacks { + public void flush() { + requestRender(); + } + + public void onLoadStarted() { + if (mClient != null) { + post(() -> mClient.onLoadStarted()); + } + } + + public void onLoadEnded() { + if (mClient != null) { + post(() -> mClient.onLoadEnded()); + } + } + + public void onTitleChanged(final String title) { + if (mClient != null) { + post(() -> mClient.onTitleChanged(title)); + } + } + + public void onUrlChanged(final String url) { + if (mClient != null) { + post(() -> mClient.onUrlChanged(url)); + } + } + + public void onHistoryChanged(final boolean canGoBack, final boolean canGoForward) { + if (mClient != null) { + post(() -> mClient.onHistoryChanged(canGoBack, canGoForward)); + } + } + + public void onAnimatingChanged(final boolean animating) { + if (!mAnimating && animating) { + post(() -> Choreographer.getInstance().postFrameCallback(ServoView.this)); + } + mAnimating = animating; + } + } + + public void onGLReady() { + final WakeupCallback c1 = new WakeupCallback(); + final ReadFileCallback c2 = new ReadFileCallback(); + final ServoCallbacks c3 = new ServoCallbacks(); + final boolean showLogs = true; + int width = getWidth(); + int height = getHeight(); + queueEvent(() -> { + String uri = mInitialUri == null ? null : mInitialUri.toString(); + mServo.init(mActivity, mServoArgs, uri, c1, c2, c3, width, height, showLogs); + }); + } + + public interface Client { + void onLoadStarted(); + void onLoadEnded(); + void onTitleChanged(String title); + void onUrlChanged(String url); + void onHistoryChanged(boolean canGoBack, boolean canGoForward); + } + + public void setClient(Client client) { + mClient = client; + } + + // Scroll and click + + private GestureDetector mGestureDetector; + private OverScroller mScroller; + private int mLastX = 0; + private int mCurX = 0; + private int mLastY = 0; + private int mCurY = 0; + private boolean mFlinging; + + private void initGestures(Context context) { + mGestureDetector = new GestureDetector(context, this); + mScroller = new OverScroller(context); + } + + @Override + public void doFrame(long frameTimeNanos) { + + if (mScroller.isFinished() && mFlinging) { + mFlinging = false; + queueEvent(() -> mServo.scrollEnd(0, 0, mCurX, mCurY)); + if (!mAnimating) { + // Not scrolling. Not animating. We don't need to schedule + // another frame. + return; + } + } + + if (mFlinging) { + mScroller.computeScrollOffset(); + mCurX = mScroller.getCurrX(); + mCurY = mScroller.getCurrY(); + } + + int dx = mCurX - mLastX; + int dy = mCurY - mLastY; + + mLastX = mCurX; + mLastY = mCurY; + + if (dx != 0 || dy != 0) { + queueEvent(() -> mServo.scroll(dx, dy, mCurX, mCurY)); + } else { + if (mAnimating) { + requestRender(); + } + } + + Choreographer.getInstance().postFrameCallback(this); + } + + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + mFlinging = true; + + // FIXME: magic values + // https://github.com/servo/servo/issues/20361 + int mPageWidth = 80000; + int mPageHeight = 80000; + mCurX = velocityX < 0 ? mPageWidth : 0; + mLastX = mCurX; + mCurY = velocityY < 0 ? mPageHeight : 0; + mLastY = mCurY; + mScroller.fling(mCurX, mCurY, (int)velocityX, (int)velocityY, 0, mPageWidth, 0, mPageHeight); + return true; + } + + public boolean onDown(MotionEvent e) { + mScroller.forceFinished(true); + return true; + } + + public boolean onTouchEvent(final MotionEvent e) { + mGestureDetector.onTouchEvent(e); + + int action = e.getActionMasked(); + switch(action) { + case (MotionEvent.ACTION_DOWN): + mCurX = (int)e.getX(); + mLastX = mCurX; + mCurY = (int)e.getY(); + mLastY = mCurY; + mScroller.forceFinished(true); + queueEvent(() -> mServo.scrollStart(0, 0, mCurX, mCurY)); + Choreographer.getInstance().postFrameCallback(this); + return true; + case (MotionEvent.ACTION_MOVE): + mCurX = (int)e.getX(); + mCurY = (int)e.getY(); + return true; + case (MotionEvent.ACTION_UP): + case (MotionEvent.ACTION_CANCEL): + if (!mFlinging) { + queueEvent(() -> mServo.scrollEnd(0, 0, mCurX, mCurY)); + Choreographer.getInstance().removeFrameCallback(this); + } + return true; + default: + return true; + } + } + + public boolean onSingleTapUp(MotionEvent e) { + queueEvent(() -> mServo.click((int)e.getX(), (int)e.getY())); + return false; + } + + public void onLongPress(MotionEvent e) { } + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return true; } + public void onShowPress(MotionEvent e) { } + +} diff --git a/support/android/apk/app/src/main/res/layout/activity_main.xml b/support/android/apk/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000000..2d8fab9c663 --- /dev/null +++ b/support/android/apk/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,94 @@ + + + + + + + +