summaryrefslogtreecommitdiffstats
path: root/android/source/src
diff options
context:
space:
mode:
Diffstat (limited to 'android/source/src')
-rw-r--r--android/source/src/java/org/libreoffice/AboutDialogFragment.java102
-rw-r--r--android/source/src/java/org/libreoffice/ColorPaletteAdapter.java135
-rw-r--r--android/source/src/java/org/libreoffice/ColorPaletteListener.java6
-rw-r--r--android/source/src/java/org/libreoffice/ColorPickerAdapter.java162
-rw-r--r--android/source/src/java/org/libreoffice/DocumentPartView.java21
-rw-r--r--android/source/src/java/org/libreoffice/DocumentPartViewListAdapter.java50
-rw-r--r--android/source/src/java/org/libreoffice/FontController.java446
-rw-r--r--android/source/src/java/org/libreoffice/FormattingController.java502
-rw-r--r--android/source/src/java/org/libreoffice/InvalidationHandler.java768
-rw-r--r--android/source/src/java/org/libreoffice/LOEvent.java177
-rw-r--r--android/source/src/java/org/libreoffice/LOKitInputConnectionHandler.java74
-rw-r--r--android/source/src/java/org/libreoffice/LOKitShell.java169
-rw-r--r--android/source/src/java/org/libreoffice/LOKitThread.java449
-rw-r--r--android/source/src/java/org/libreoffice/LOKitTileProvider.java815
-rw-r--r--android/source/src/java/org/libreoffice/LibreOfficeApplication.java33
-rw-r--r--android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java1113
-rw-r--r--android/source/src/java/org/libreoffice/LocaleHelper.java57
-rw-r--r--android/source/src/java/org/libreoffice/PDFDocumentAdapter.java86
-rw-r--r--android/source/src/java/org/libreoffice/PasswordDialogFragment.java56
-rw-r--r--android/source/src/java/org/libreoffice/PresentationActivity.java177
-rw-r--r--android/source/src/java/org/libreoffice/SearchController.java82
-rw-r--r--android/source/src/java/org/libreoffice/SettingsActivity.java63
-rw-r--r--android/source/src/java/org/libreoffice/SettingsListenerModel.java56
-rw-r--r--android/source/src/java/org/libreoffice/ThumbnailCreator.java121
-rw-r--r--android/source/src/java/org/libreoffice/TileIdentifier.java92
-rw-r--r--android/source/src/java/org/libreoffice/TileProvider.java205
-rw-r--r--android/source/src/java/org/libreoffice/TileProviderFactory.java31
-rw-r--r--android/source/src/java/org/libreoffice/ToolbarController.java276
-rw-r--r--android/source/src/java/org/libreoffice/UNOCommandsController.java85
-rw-r--r--android/source/src/java/org/libreoffice/UnitConverter.java16
-rw-r--r--android/source/src/java/org/libreoffice/canvas/AdjustLengthLine.java103
-rw-r--r--android/source/src/java/org/libreoffice/canvas/BitmapHandle.java63
-rw-r--r--android/source/src/java/org/libreoffice/canvas/CalcHeaderCell.java66
-rw-r--r--android/source/src/java/org/libreoffice/canvas/CalcSelectionBox.java111
-rw-r--r--android/source/src/java/org/libreoffice/canvas/CanvasElement.java45
-rw-r--r--android/source/src/java/org/libreoffice/canvas/CanvasElementImplRequirement.java25
-rw-r--r--android/source/src/java/org/libreoffice/canvas/CommonCanvasElement.java46
-rw-r--r--android/source/src/java/org/libreoffice/canvas/Cursor.java56
-rw-r--r--android/source/src/java/org/libreoffice/canvas/GraphicSelection.java295
-rw-r--r--android/source/src/java/org/libreoffice/canvas/GraphicSelectionHandle.java146
-rw-r--r--android/source/src/java/org/libreoffice/canvas/ImageUtils.java29
-rw-r--r--android/source/src/java/org/libreoffice/canvas/PageNumberRect.java64
-rw-r--r--android/source/src/java/org/libreoffice/canvas/SelectionHandle.java73
-rw-r--r--android/source/src/java/org/libreoffice/canvas/SelectionHandleEnd.java22
-rw-r--r--android/source/src/java/org/libreoffice/canvas/SelectionHandleMiddle.java34
-rw-r--r--android/source/src/java/org/libreoffice/canvas/SelectionHandleStart.java34
-rw-r--r--android/source/src/java/org/libreoffice/overlay/CalcHeadersController.java281
-rw-r--r--android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java278
-rw-r--r--android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java271
-rw-r--r--android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java552
-rw-r--r--android/source/src/java/org/libreoffice/ui/FileUtilities.java158
-rw-r--r--android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java457
-rw-r--r--android/source/src/java/org/libreoffice/ui/PageView.java69
-rw-r--r--android/source/src/java/org/libreoffice/ui/RecentFile.java25
-rw-r--r--android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java93
-rw-r--r--android/source/src/java/org/mozilla/gecko/OnInterceptTouchListener.java14
-rw-r--r--android/source/src/java/org/mozilla/gecko/OnSlideSwipeListener.java94
-rw-r--r--android/source/src/java/org/mozilla/gecko/ZoomConstraints.java30
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/Axis.java337
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java83
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java35
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/CairoImage.java28
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/CairoUtils.java51
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/ComposedTileLayer.java290
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java760
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java67
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java30
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/FixedZoomTileLayer.java31
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/FloatSize.java53
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/GLController.java215
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java356
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java241
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java15
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/IntSize.java73
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java1087
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/Layer.java218
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java453
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/LayerView.java337
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java131
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/PanZoomController.java36
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java26
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/PointUtils.java53
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/RectUtils.java110
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java143
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java451
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java322
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java154
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/SubTile.java254
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java78
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TextLayer.java69
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TextureGenerator.java77
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TextureReaper.java62
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TileLayer.java176
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java306
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java173
-rw-r--r--android/source/src/java/org/mozilla/gecko/util/FloatUtils.java41
96 files changed, 17381 insertions, 0 deletions
diff --git a/android/source/src/java/org/libreoffice/AboutDialogFragment.java b/android/source/src/java/org/libreoffice/AboutDialogFragment.java
new file mode 100644
index 0000000000..0d9fc45856
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/AboutDialogFragment.java
@@ -0,0 +1,102 @@
+/*
+ *
+ * * This file is part of the LibreOffice project.
+ * * 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 org.libreoffice;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.view.View;
+import android.widget.TextView;
+
+public class AboutDialogFragment extends DialogFragment {
+
+ @NonNull @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+
+ @SuppressLint("InflateParams") //suppressed because the view will be placed in a dialog
+ View messageView = getActivity().getLayoutInflater().inflate(R.layout.about, null, false);
+
+ // When linking text, force to always use default color. This works
+ // around a pressed color state bug.
+ TextView textView = messageView.findViewById(R.id.about_credits);
+ int defaultColor = textView.getTextColors().getDefaultColor();
+ textView.setTextColor(defaultColor);
+
+ // Take care of placeholders and set text in version and vendor text views.
+ try
+ {
+ String versionName = getActivity().getPackageManager()
+ .getPackageInfo(getActivity().getPackageName(), 0).versionName;
+ String version = String.format(getString(R.string.app_version), versionName, BuildConfig.BUILD_ID_SHORT);
+ @SuppressWarnings("deprecation") // since 24 with additional option parameter
+ Spanned versionString = Html.fromHtml(version);
+ TextView versionView = messageView.findViewById(R.id.about_version);
+ versionView.setText(versionString);
+ versionView.setMovementMethod(LinkMovementMethod.getInstance());
+ TextView vendorView = messageView.findViewById(R.id.about_vendor);
+ String vendor = getString(R.string.app_vendor).replace("$VENDOR", BuildConfig.VENDOR);
+ vendorView.setText(vendor);
+ }
+ catch (PackageManager.NameNotFoundException e)
+ {
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder .setIcon(R.mipmap.ic_launcher)
+ .setTitle(R.string.app_name)
+ .setView(messageView)
+ .setNegativeButton(R.string.about_license, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ loadFromAbout(R.raw.license);
+ dialog.dismiss();
+ }
+ })
+ .setPositiveButton(R.string.about_notice, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ loadFromAbout(R.raw.notice);
+ dialog.dismiss();
+ }
+ });
+
+ // when privacy policy URL is set (via '--with-privacy-policy-url=<url>' autogen option),
+ // add button to open that URL
+ final String privacyUrl = BuildConfig.PRIVACY_POLICY_URL;
+ if (!privacyUrl.isEmpty() && privacyUrl != "undefined") {
+ builder.setNeutralButton(R.string.about_privacy_policy, (DialogInterface dialog, int id) -> {
+ Intent openPrivacyUrlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(privacyUrl));
+ startActivity(openPrivacyUrlIntent);
+ dialog.dismiss();
+ });
+ }
+
+ return builder.create();
+ }
+
+ private void loadFromAbout(int resourceId) {
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("android.resource://" + BuildConfig.APPLICATION_ID + "/" + resourceId));
+ String packageName = getActivity().getApplicationContext().getPackageName();
+ ComponentName componentName = new ComponentName(packageName, LibreOfficeMainActivity.class.getName());
+ i.setComponent(componentName);
+ getActivity().startActivity(i);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/ColorPaletteAdapter.java b/android/source/src/java/org/libreoffice/ColorPaletteAdapter.java
new file mode 100644
index 0000000000..16d8a97786
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ColorPaletteAdapter.java
@@ -0,0 +1,135 @@
+package org.libreoffice;
+
+import android.content.Context;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageButton;
+
+
+public class ColorPaletteAdapter extends RecyclerView.Adapter<ColorPaletteAdapter.ColorPaletteViewHolder> {
+
+ private int[][] color_palette;
+ private final Context mContext;
+ private int upperSelectedBox = -1;
+ private int selectedBox = 0;
+ private boolean animate;
+ private final ColorPaletteListener colorPaletteListener;
+
+ public ColorPaletteAdapter(Context mContext, ColorPaletteListener colorPaletteListener) {
+ this.mContext = mContext;
+ this.color_palette = new int[11][8];
+ this.colorPaletteListener = colorPaletteListener;
+ }
+
+ @Override
+ public ColorPaletteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View item = LayoutInflater.from(mContext).inflate(R.layout.colorbox, parent, false);
+ return new ColorPaletteViewHolder(item);
+ }
+
+
+ public int getSelectedBox() {
+ return selectedBox;
+ }
+
+ public int getUpperSelectedBox() {
+ return upperSelectedBox;
+ }
+
+ @Override
+ public void onBindViewHolder(final ColorPaletteViewHolder holder, int position) {
+
+ holder.colorBox.setBackgroundColor(color_palette[upperSelectedBox][position]);
+ if (selectedBox == position) {
+ holder.colorBox.setImageResource(R.drawable.ic_done_all_white_12dp);
+ } else {
+ holder.colorBox.setImageDrawable(null);
+ }
+
+ holder.colorBox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ setPosition(holder.getAdapterPosition());
+ }
+ });
+ if (animate) //it will only animate when the upper color box is selected
+ setAnimation(holder.colorBox);
+
+ }
+
+ private void setAnimation(View viewToAnimate) {
+ Animation animation = AnimationUtils.loadAnimation(mContext, android.R.anim.fade_in);
+ viewToAnimate.startAnimation(animation);
+ }
+
+ @Override
+ public int getItemCount() {
+ return color_palette[0].length;
+ }
+
+ private void setPosition(int position) {
+ this.selectedBox = position;
+ colorPaletteListener.applyColor(color_palette[upperSelectedBox][position]);
+ animate = false;
+ updateAdapter();
+ }
+
+ public void setPosition(int upperSelectedBox, int position) {
+ if (this.upperSelectedBox != upperSelectedBox) {
+ this.upperSelectedBox = upperSelectedBox;
+ this.selectedBox = position;
+ colorPaletteListener.applyColor(color_palette[upperSelectedBox][position]);
+ animate = true;
+ updateAdapter();
+ }
+ }
+
+ /*
+ this is for InvalidationHandler when .uno:FontColor is captured
+ */
+ public void changePosition(int upperSelectedBox, int position) {
+ if(this.upperSelectedBox != upperSelectedBox){
+ this.upperSelectedBox = upperSelectedBox;
+ animate=true;
+ }
+
+ this.selectedBox = position;
+
+ updateAdapter();
+
+ }
+
+ public void setColorPalette(int[][] color_palette) {
+ this.color_palette = color_palette;
+ this.upperSelectedBox = 0;
+ this.selectedBox = 0;
+ }
+
+ private void updateAdapter(){
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ ColorPaletteAdapter.this.notifyDataSetChanged();
+ }
+ });
+ }
+
+
+ class ColorPaletteViewHolder extends RecyclerView.ViewHolder {
+
+ ImageButton colorBox;
+
+ public ColorPaletteViewHolder(View itemView) {
+ super(itemView);
+ colorBox = itemView.findViewById(R.id.fontColorBox);
+ }
+ }
+
+
+}
diff --git a/android/source/src/java/org/libreoffice/ColorPaletteListener.java b/android/source/src/java/org/libreoffice/ColorPaletteListener.java
new file mode 100644
index 0000000000..a79a19e5c9
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ColorPaletteListener.java
@@ -0,0 +1,6 @@
+package org.libreoffice;
+
+public interface ColorPaletteListener {
+ void applyColor(int color);
+ void updateColorPickerPosition(int color);
+}
diff --git a/android/source/src/java/org/libreoffice/ColorPickerAdapter.java b/android/source/src/java/org/libreoffice/ColorPickerAdapter.java
new file mode 100644
index 0000000000..a17dd264fb
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ColorPickerAdapter.java
@@ -0,0 +1,162 @@
+package org.libreoffice;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+
+public class ColorPickerAdapter extends RecyclerView.Adapter<ColorPickerAdapter.ColorPickerViewHolder> {
+
+ private final Context mContext;
+ private final ColorPaletteAdapter colorPaletteAdapter;
+ private final ColorPaletteListener colorPaletteListener;
+ private final int[] colorList;
+ private final int[][] colorPalette = new int[11][8];
+
+ public ColorPickerAdapter(Context mContext, final ColorPaletteAdapter colorPaletteAdapter, ColorPaletteListener colorPaletteListener) {
+ this.mContext = mContext;
+ this.colorPaletteAdapter = colorPaletteAdapter;
+ this.colorPaletteListener = colorPaletteListener;
+ Resources r = mContext.getResources();
+ this.colorList = r.getIntArray(R.array.fontcolors);
+ initializeColorPalette();
+
+
+ }
+
+ @Override
+ public ColorPickerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View item = LayoutInflater.from(mContext).inflate(R.layout.colorbox, parent, false);
+ return new ColorPickerViewHolder(item);
+
+ }
+
+ @Override
+ public void onBindViewHolder(final ColorPickerViewHolder holder, int position) {
+ holder.colorBox.setBackgroundColor(colorList[position]);
+
+ if (colorPaletteAdapter.getUpperSelectedBox() == position
+ && colorPaletteAdapter.getSelectedBox() >= 0) {
+ holder.colorBox.setImageResource(R.drawable.ic_done_white_12dp);
+ } else {
+ holder.colorBox.setImageDrawable(null);
+ }
+
+ holder.colorBox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ setPosition(holder.getAdapterPosition());
+ colorPaletteListener.applyColor(colorList[holder.getAdapterPosition()]);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return colorList.length;
+ }
+
+
+ private void setPosition(int position) {
+ selectSubColor(position, position==0?0:3);
+ colorPaletteListener.applyColor(colorList[position]);
+ updateAdapter();
+ }
+
+ /**
+ * Switches to first palette, but doesn't mark any color as selected.
+ * Use this if no color in the palette matches the actual one.
+ */
+ public void unselectColors() {
+ colorPaletteAdapter.changePosition(0, -1);
+ updateAdapter();
+ }
+
+ private void selectSubColor(int position1, int position2) {
+ colorPaletteAdapter.setPosition(position1, position2);
+ }
+
+ private void initializeColorPalette() {
+
+ for (int i = 0; i < 11; i++) {
+ int red = Color.red(colorList[i]);
+ int green = Color.green(colorList[i]);
+ int blue = Color.blue(colorList[i]);
+
+ int red_tint = red;
+ int green_tint = green;
+ int blue_tint = blue;
+
+ int red_shade = red;
+ int green_shade = green;
+ int blue_shade = blue;
+ if (i == 0) {
+ colorPalette[0][0] = colorList[i];
+ for (int k = 1; k < 7; k++) {
+ red_tint = (int) (red_tint + (255 - red_tint) * 0.25);
+ green_tint = (int) (green_tint + (255 - green_tint) * 0.25);
+ blue_tint = (int) (blue_tint + (255 - blue_tint) * 0.25);
+ colorPalette[i][k] = (Color.rgb(red_tint, green_tint, blue_tint));
+ }
+ } else {
+ colorPalette[i][3] = colorList[i];
+ for (int k = 2; k >= 0; k--) {
+ red_shade = (int) (red_shade * 0.75);
+ green_shade = (int) (green_shade * 0.75);
+ blue_shade = (int) (blue_shade * 0.75);
+ colorPalette[i][k] = (Color.rgb(red_shade, green_shade, blue_shade));
+ }
+ for (int k = 4; k < 7; k++) {
+ red_tint = (int) (red_tint + (255 - red_tint) * 0.45);
+ green_tint = (int) (green_tint + (255 - green_tint) * 0.45);
+ blue_tint = (int) (blue_tint + (255 - blue_tint) * 0.45);
+ colorPalette[i][k] = (Color.rgb(red_tint, green_tint, blue_tint));
+ }
+ }
+ colorPalette[i][7] = Color.WHITE; // last one is always white
+ }
+ colorPaletteAdapter.setColorPalette(colorPalette);
+ }
+
+ public void findSelectedTextColor(int color) {
+ // try to find and highlight the color in the existing palettes
+ for (int i = 0; i < 11; i++) {
+ for (int k = 0; k < 8; k++) {
+ if (colorPalette[i][k] == color) {
+ colorPaletteAdapter.changePosition(i, k);
+ updateAdapter();
+ return;
+ }
+ }
+ }
+
+ // no color in the palettes matched
+ unselectColors();
+ }
+
+ private void updateAdapter(){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ ColorPickerAdapter.this.notifyDataSetChanged();
+ }
+ });
+
+ }
+
+ class ColorPickerViewHolder extends RecyclerView.ViewHolder {
+
+ ImageButton colorBox;
+
+ public ColorPickerViewHolder(View itemView) {
+ super(itemView);
+ this.colorBox = itemView.findViewById(R.id.fontColorBox);
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/DocumentPartView.java b/android/source/src/java/org/libreoffice/DocumentPartView.java
new file mode 100644
index 0000000000..f1ce71900d
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/DocumentPartView.java
@@ -0,0 +1,21 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+public class DocumentPartView {
+ public int partIndex;
+ public String partName;
+
+ public DocumentPartView(int partIndex, String partName) {
+ this.partIndex = partIndex;
+ this.partName = partName;
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/DocumentPartViewListAdapter.java b/android/source/src/java/org/libreoffice/DocumentPartViewListAdapter.java
new file mode 100644
index 0000000000..a0ed871a40
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/DocumentPartViewListAdapter.java
@@ -0,0 +1,50 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.app.Activity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.List;
+
+public class DocumentPartViewListAdapter extends ArrayAdapter<DocumentPartView> {
+
+ private final Activity activity;
+ private final ThumbnailCreator thumbnailCollector;
+
+ public DocumentPartViewListAdapter(Activity activity, int resource, List<DocumentPartView> objects) {
+ super(activity, resource, objects);
+ this.activity = activity;
+ this.thumbnailCollector = new ThumbnailCreator();
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ if (view == null) {
+ LayoutInflater layoutInflater = activity.getLayoutInflater();
+ view = layoutInflater.inflate(R.layout.document_part_list_layout, null);
+ }
+
+ DocumentPartView documentPartView = getItem(position);
+ TextView textView = view.findViewById(R.id.text);
+ textView.setText(documentPartView.partName);
+
+ ImageView imageView = view.findViewById(R.id.image);
+ thumbnailCollector.createThumbnail(position, imageView);
+
+ return view;
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/FontController.java b/android/source/src/java/org/libreoffice/FontController.java
new file mode 100644
index 0000000000..72f35d8b42
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/FontController.java
@@ -0,0 +1,446 @@
+package org.libreoffice;
+
+import android.graphics.Color;
+import android.graphics.Rect;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.Spinner;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+public class FontController implements AdapterView.OnItemSelectedListener {
+
+ /** -1 as value in ".uno:Color" et al. means "automatic color"/no color set. */
+ private static final int COLOR_AUTO = -1;
+
+ private boolean mFontNameSpinnerSet = false;
+ private boolean mFontSizeSpinnerSet = false;
+ private final LibreOfficeMainActivity mActivity;
+ private final ArrayList<String> mFontList = new ArrayList<>();
+ private final ArrayList<String> mFontSizes = new ArrayList<>();
+ private final HashMap<String, ArrayList<String>> mAllFontSizes = new HashMap<>();
+
+ private String mCurrentFontSelected = null;
+ private String mCurrentFontSizeSelected = null;
+
+ public FontController(LibreOfficeMainActivity activity) {
+ mActivity = activity;
+ }
+ private BottomSheetBehavior colorPickerBehavior;
+ private BottomSheetBehavior backColorPickerBehavior;
+ private BottomSheetBehavior toolBarBottomBehavior;
+ private ColorPickerAdapter colorPickerAdapter;
+ private ColorPickerAdapter backColorPickerAdapter;
+
+ final ColorPaletteListener colorPaletteListener = new ColorPaletteListener() {
+ @Override
+ public void applyColor(int color) {
+ sendFontColorChange(color, false);
+ }
+
+ @Override
+ public void updateColorPickerPosition(int color) {
+ if (colorPickerAdapter == null) {
+ return;
+ }
+ if (color == COLOR_AUTO) {
+ colorPickerAdapter.unselectColors();
+ changeFontColorBoxColor(Color.TRANSPARENT);
+ return;
+ }
+ final int colorWithAlpha = color | 0xFF000000;
+ colorPickerAdapter.findSelectedTextColor(colorWithAlpha);
+ changeFontColorBoxColor(colorWithAlpha);
+ }
+ };
+
+ final ColorPaletteListener backColorPaletteListener = new ColorPaletteListener() {
+ @Override
+ public void applyColor(int color) {
+ sendFontBackColorChange(color, false);
+ }
+
+ @Override
+ public void updateColorPickerPosition(int color) {
+ if (backColorPickerAdapter == null) {
+ return;
+ }
+ if (color == COLOR_AUTO) {
+ backColorPickerAdapter.unselectColors();
+ changeFontBackColorBoxColor(Color.TRANSPARENT);
+ return;
+ }
+ final int colorWithAlpha = color | 0xFF000000;
+ backColorPickerAdapter.findSelectedTextColor(colorWithAlpha);
+ changeFontBackColorBoxColor(colorWithAlpha);
+ }
+ };
+
+ private void changeFontColorBoxColor(final int color){
+ final ImageButton fontColorPickerButton = mActivity.findViewById(R.id.font_color_picker_button);
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ fontColorPickerButton.setBackgroundColor(color);
+ }
+ });
+ }
+
+ private void changeFontBackColorBoxColor(final int color){
+ final ImageButton fontBackColorPickerButton = mActivity.findViewById(R.id.font_back_color_picker_button);
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ fontBackColorPickerButton.setBackgroundColor(color);
+ }
+ });
+ }
+
+ private void sendFontChange(String fontName) {
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "string");
+ valueJson.put("value", fontName);
+ json.put("CharFontName.FamilyName", valueJson);
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:CharFontName", json.toString()));
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendFontSizeChange(String fontSize) {
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "float");
+ valueJson.put("value", fontSize);
+ json.put("FontHeight.Height", valueJson);
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:FontHeight", json.toString()));
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendFontColorChange(int color, boolean keepAlpha){
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "long");
+ valueJson.put("value", keepAlpha ? color : 0x00FFFFFF & color);
+ json.put("Color", valueJson);
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Color", json.toString()));
+ changeFontColorBoxColor(color);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /*
+ * 0x00FFFFFF & color operation removes the alpha which is FF,
+ * if we don't remove it, the color value becomes negative which is not recognized by LOK
+ */
+ private void sendFontBackColorChange(int color, boolean keepAlpha) {
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "long");
+ valueJson.put("value", keepAlpha ? color : 0x00FFFFFF & color);
+ if(mActivity.getTileProvider().isSpreadsheet()){
+ json.put("BackgroundColor", valueJson);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:BackgroundColor", json.toString()));
+ }else {
+ json.put("CharBackColor", valueJson);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:CharBackColor", json.toString()));
+ }
+
+ changeFontBackColorBoxColor(color);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ if (mFontList.isEmpty() || !mFontNameSpinnerSet)
+ return;
+ if (parent == mActivity.findViewById(R.id.font_name_spinner)) {
+ String currentFontSelected = parent.getItemAtPosition(pos).toString();
+ if (!currentFontSelected.equals(mCurrentFontSelected)) {
+ mCurrentFontSelected = currentFontSelected;
+ sendFontChange(mCurrentFontSelected);
+ }
+ } else if (parent == mActivity.findViewById(R.id.font_size_spinner)) {
+ String currentFontSizeSelected = parent.getItemAtPosition(pos).toString();
+ if (!currentFontSizeSelected.equals(mCurrentFontSizeSelected)) {
+ mCurrentFontSizeSelected = currentFontSizeSelected;
+ sendFontSizeChange(mCurrentFontSizeSelected);
+ }
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView parent) {
+ // Do nothing.
+ }
+
+ public void parseJson(String json) {
+ mFontList.clear();
+ mAllFontSizes.clear();
+ try {
+ JSONObject jObject = new JSONObject(json);
+ JSONObject jObject2 = jObject.getJSONObject("commandValues");
+ Iterator<String> keys = jObject2.keys();
+ ArrayList<String> fontSizes;
+ while (keys.hasNext()) {
+ String key = keys.next();
+ mFontList.add(key);
+ JSONArray array = jObject2.getJSONArray(key);
+ fontSizes = new ArrayList<>();
+ for (int i = 0; i < array.length(); i++) {
+ fontSizes.add(array.getString(i));
+ }
+ mAllFontSizes.put(key, fontSizes);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void setupFontViews() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ setupFontNameSpinner();
+ setupFontSizeSpinner();
+ setupColorPicker();
+ setupBackColorPicker();
+ }
+ });
+ }
+
+ private void setupFontNameSpinner() {
+ Spinner fontSpinner = mActivity.findViewById(R.id.font_name_spinner);
+ ArrayAdapter<String> dataAdapter = new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mFontList);
+ dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ fontSpinner.setAdapter(dataAdapter);
+ }
+
+ private void setupFontSizeSpinner() {
+ Spinner fontSizeSpinner = mActivity.findViewById(R.id.font_size_spinner);
+ ArrayAdapter<String> dataAdapter = new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mFontSizes);
+ dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ fontSizeSpinner.setAdapter(dataAdapter);
+ }
+
+ private void setupColorPicker(){
+ LinearLayout colorPickerLayout = mActivity.findViewById(R.id.toolbar_color_picker);
+
+ RecyclerView recyclerView = colorPickerLayout.findViewById(R.id.fontColorView);
+ GridLayoutManager gridLayoutManager = new GridLayoutManager(mActivity, 11, GridLayoutManager.VERTICAL, true);
+ recyclerView.setHasFixedSize(true);
+ recyclerView.setLayoutManager(gridLayoutManager);
+
+
+
+ RecyclerView recyclerView2 = colorPickerLayout.findViewById(R.id.fontColorViewSub);
+ GridLayoutManager gridLayoutManager2 = new GridLayoutManager(mActivity,4);
+ recyclerView2.setHasFixedSize(true);
+ recyclerView2.addItemDecoration(new RecyclerView.ItemDecoration() {
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.bottom = 3;
+ outRect.top = 3;
+ outRect.left = 3;
+ outRect.right = 3;
+ }
+ });
+ recyclerView2.setLayoutManager(gridLayoutManager2);
+
+ ColorPaletteAdapter colorPaletteAdapter = new ColorPaletteAdapter(mActivity, colorPaletteListener);
+ recyclerView2.setAdapter(colorPaletteAdapter);
+
+ this.colorPickerAdapter = new ColorPickerAdapter(mActivity, colorPaletteAdapter, colorPaletteListener);
+ recyclerView.setAdapter(colorPickerAdapter);
+ RelativeLayout fontColorPicker = mActivity.findViewById(R.id.font_color_picker);
+ ImageButton fontColorPickerButton = mActivity.findViewById(R.id.font_color_picker_button);
+ View.OnClickListener clickListener = new View.OnClickListener(){
+ @Override
+ public void onClick(View view) {
+ toolBarBottomBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ colorPickerBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ mActivity.findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ }
+ };
+ LinearLayout toolbarBottomLayout = mActivity.findViewById(R.id.toolbar_bottom);
+ colorPickerBehavior = BottomSheetBehavior.from(colorPickerLayout);
+ toolBarBottomBehavior = BottomSheetBehavior.from(toolbarBottomLayout);
+
+ ImageButton pickerGoBackButton = colorPickerLayout.findViewById(R.id.button_go_back_color_picker);
+ pickerGoBackButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toolBarBottomBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ colorPickerBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ }
+ });
+
+
+ fontColorPicker.setOnClickListener(clickListener);
+ fontColorPickerButton.setOnClickListener(clickListener);
+
+ final Button autoColorButton = colorPickerLayout.findViewById(R.id.button_auto_color);
+ autoColorButton.setOnClickListener(view -> {
+ sendFontColorChange(COLOR_AUTO, true);
+ });
+ }
+
+ private void setupBackColorPicker(){
+ LinearLayout backColorPickerLayout = mActivity.findViewById(R.id.toolbar_back_color_picker);
+
+ RecyclerView recyclerView = backColorPickerLayout.findViewById(R.id.fontColorView);
+ GridLayoutManager gridLayoutManager = new GridLayoutManager(mActivity, 11, GridLayoutManager.VERTICAL, true);
+ recyclerView.setHasFixedSize(true);
+ recyclerView.setLayoutManager(gridLayoutManager);
+
+
+
+ RecyclerView recyclerView2 = backColorPickerLayout.findViewById(R.id.fontColorViewSub);
+ GridLayoutManager gridLayoutManager2 = new GridLayoutManager(mActivity,4);
+ recyclerView2.setHasFixedSize(true);
+ recyclerView2.addItemDecoration(new RecyclerView.ItemDecoration() {
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.bottom = 3;
+ outRect.top = 3;
+ outRect.left = 3;
+ outRect.right = 3;
+ }
+ });
+ recyclerView2.setLayoutManager(gridLayoutManager2);
+
+ ColorPaletteAdapter colorPaletteAdapter = new ColorPaletteAdapter(mActivity, backColorPaletteListener);
+ recyclerView2.setAdapter(colorPaletteAdapter);
+
+ this.backColorPickerAdapter = new ColorPickerAdapter(mActivity, colorPaletteAdapter, backColorPaletteListener);
+ recyclerView.setAdapter(backColorPickerAdapter);
+ RelativeLayout fontColorPicker = mActivity.findViewById(R.id.font_back_color_picker);
+ ImageButton fontColorPickerButton = mActivity.findViewById(R.id.font_back_color_picker_button);
+ View.OnClickListener clickListener = new View.OnClickListener(){
+ @Override
+ public void onClick(View view) {
+ toolBarBottomBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ backColorPickerBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ mActivity.findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ }
+ };
+ LinearLayout toolbarBottomLayout = mActivity.findViewById(R.id.toolbar_bottom);
+ backColorPickerBehavior = BottomSheetBehavior.from(backColorPickerLayout);
+ toolBarBottomBehavior = BottomSheetBehavior.from(toolbarBottomLayout);
+
+ ImageButton pickerGoBackButton = backColorPickerLayout.findViewById(R.id.button_go_back_color_picker);
+ pickerGoBackButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toolBarBottomBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ backColorPickerBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ }
+ });
+
+
+ fontColorPicker.setOnClickListener(clickListener);
+ fontColorPickerButton.setOnClickListener(clickListener);
+
+ final Button autoColorButton = backColorPickerLayout.findViewById(R.id.button_auto_color);
+ autoColorButton.setOnClickListener(view -> {
+ sendFontBackColorChange(COLOR_AUTO, true);
+ });
+ }
+
+ public void selectFont(final String fontName) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ selectFontCurrentThread(fontName);
+ }
+ });
+ }
+
+ private void selectFontCurrentThread(String fontName) {
+ Spinner spinner = mActivity.findViewById(R.id.font_name_spinner);
+ if (!mFontNameSpinnerSet) {
+ spinner.setOnItemSelectedListener(this);
+ mFontNameSpinnerSet = true;
+ }
+
+ if (fontName.equals(mCurrentFontSelected))
+ return;
+
+ int position = mFontList.indexOf(fontName);
+ if (position != -1) {
+ mCurrentFontSelected = fontName;
+ spinner.setSelection(position,false);
+ }
+
+ resetFontSizes(fontName);
+ }
+
+ private void resetFontSizes(String fontName) {
+ if (mAllFontSizes.get(fontName) != null) {
+ mFontSizes.clear();
+ mFontSizes.addAll(mAllFontSizes.get(fontName));
+ Spinner spinner = mActivity.findViewById(R.id.font_size_spinner);
+ ArrayAdapter<?> arrayAdapter = (ArrayAdapter<?>)spinner.getAdapter();
+ arrayAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void selectFontSize(final String fontSize) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ selectFontSizeCurrentThread(fontSize);
+ }
+ });
+ }
+
+ private void selectFontSizeCurrentThread(String fontSize) {
+ Spinner spinner = mActivity.findViewById(R.id.font_size_spinner);
+ if (!mFontSizeSpinnerSet) {
+ spinner.setOnItemSelectedListener(this);
+ mFontSizeSpinnerSet = true;
+ }
+
+ if (fontSize.equals(mCurrentFontSizeSelected))
+ return;
+
+ int position = mFontSizes.indexOf(fontSize);
+ if (position != -1) {
+ mCurrentFontSizeSelected = fontSize;
+ spinner.setSelection(position, false);
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/FormattingController.java b/android/source/src/java/org/libreoffice/FormattingController.java
new file mode 100644
index 0000000000..49e81eb697
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/FormattingController.java
@@ -0,0 +1,502 @@
+package org.libreoffice;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import com.google.android.material.snackbar.Snackbar;
+import androidx.core.content.FileProvider;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.kit.Document;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import static org.libreoffice.SearchController.addProperty;
+
+class FormattingController implements View.OnClickListener {
+ private static final String LOGTAG = ToolbarController.class.getSimpleName();
+ private static final int TAKE_PHOTO = 1;
+ private static final int SELECT_PHOTO = 2;
+ private static final int IMAGE_BUFFER_SIZE = 4 * 1024;
+
+ private final LibreOfficeMainActivity mContext;
+ private String mCurrentPhotoPath;
+
+ FormattingController(LibreOfficeMainActivity context) {
+ mContext = context;
+
+ mContext.findViewById(R.id.button_insertFormatListBullets).setOnClickListener(this);
+ mContext.findViewById(R.id.button_insertFormatListNumbering).setOnClickListener(this);
+ mContext.findViewById(R.id.button_increaseIndent).setOnClickListener(this);
+ mContext.findViewById(R.id.button_decreaseIndent).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_bold).setOnClickListener(this);
+ mContext.findViewById(R.id.button_italic).setOnClickListener(this);
+ mContext.findViewById(R.id.button_strikethrough).setOnClickListener(this);
+ mContext.findViewById(R.id.button_underlined).setOnClickListener(this);
+ mContext.findViewById(R.id.button_clearformatting).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_align_left).setOnClickListener(this);
+ mContext.findViewById(R.id.button_align_center).setOnClickListener(this);
+ mContext.findViewById(R.id.button_align_right).setOnClickListener(this);
+ mContext.findViewById(R.id.button_align_justify).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_insert_line).setOnClickListener(this);
+ mContext.findViewById(R.id.button_insert_rect).setOnClickListener(this);
+ mContext.findViewById(R.id.button_insert_picture).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_insert_table).setOnClickListener(this);
+ mContext.findViewById(R.id.button_delete_table).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_font_shrink).setOnClickListener(this);
+ mContext.findViewById(R.id.button_font_grow).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_subscript).setOnClickListener(this);
+ mContext.findViewById(R.id.button_superscript).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ ImageButton button = (ImageButton) view;
+
+ if (button.isSelected()) {
+ button.getBackground().setState(new int[]{-android.R.attr.state_selected});
+ } else {
+ button.getBackground().setState(new int[]{android.R.attr.state_selected});
+ }
+
+ final int buttonId = button.getId();
+ if (buttonId == R.id.button_insertFormatListBullets) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DefaultBullet"));
+ } else if (buttonId == R.id.button_insertFormatListNumbering) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DefaultNumbering"));
+ } else if (buttonId == R.id.button_increaseIndent) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:IncrementIndent"));
+ } else if (buttonId == R.id.button_decreaseIndent) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DecrementIndent"));
+ } else if (buttonId == R.id.button_bold) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Bold"));
+ } else if (buttonId == R.id.button_italic) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Italic"));
+ } else if (buttonId == R.id.button_strikethrough) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Strikeout"));
+ } else if (buttonId == R.id.button_clearformatting) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:ResetAttributes"));
+ } else if (buttonId == R.id.button_underlined) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:UnderlineDouble"));
+ } else if (buttonId == R.id.button_align_left) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:LeftPara"));
+ } else if (buttonId == R.id.button_align_center) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:CenterPara"));
+ } else if (buttonId == R.id.button_align_right) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:RightPara"));
+ } else if (buttonId == R.id.button_align_justify) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:JustifyPara"));
+ } else if (buttonId == R.id.button_insert_line) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Line"));
+ } else if (buttonId == R.id.button_insert_rect) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Rect"));
+ } else if (buttonId == R.id.button_font_shrink) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Shrink"));
+ } else if (buttonId == R.id.button_font_grow) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Grow"));
+ } else if (buttonId == R.id.button_subscript) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SubScript"));
+ }else if (buttonId == R.id.button_superscript) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SuperScript"));
+ } else if (buttonId == R.id.button_insert_picture) {
+ insertPicture();
+ } else if (buttonId == R.id.button_insert_table) {
+ insertTable();
+ } else if (buttonId == R.id.button_delete_table) {
+ deleteTable();
+ }
+ }
+
+ void onToggleStateChanged(final int type, final boolean selected) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ Integer buttonId;
+ switch (type) {
+ case Document.BOLD:
+ buttonId = R.id.button_bold;
+ break;
+ case Document.ITALIC:
+ buttonId = R.id.button_italic;
+ break;
+ case Document.UNDERLINE:
+ buttonId = R.id.button_underlined;
+ break;
+ case Document.STRIKEOUT:
+ buttonId = R.id.button_strikethrough;
+ break;
+ case Document.ALIGN_LEFT:
+ buttonId = R.id.button_align_left;
+ break;
+ case Document.ALIGN_CENTER:
+ buttonId = R.id.button_align_center;
+ break;
+ case Document.ALIGN_RIGHT:
+ buttonId = R.id.button_align_right;
+ break;
+ case Document.ALIGN_JUSTIFY:
+ buttonId = R.id.button_align_justify;
+ break;
+ case Document.BULLET_LIST:
+ buttonId = R.id.button_insertFormatListBullets;
+ break;
+ case Document.NUMBERED_LIST:
+ buttonId = R.id.button_insertFormatListNumbering;
+ break;
+ default:
+ Log.e(LOGTAG, "Uncaptured state change type: " + type);
+ return;
+ }
+
+ ImageButton button = mContext.findViewById(buttonId);
+ button.setSelected(selected);
+ if (selected) {
+ button.getBackground().setState(new int[]{android.R.attr.state_selected});
+ } else {
+ button.getBackground().setState(new int[]{-android.R.attr.state_selected});
+ }
+ }
+ });
+ }
+
+ private void insertPicture() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ String[] options = {mContext.getResources().getString(R.string.take_photo),
+ mContext.getResources().getString(R.string.select_photo)};
+ builder.setItems(options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case 0:
+ dispatchTakePictureIntent();
+ break;
+ case 1:
+ sendImagePickingIntent();
+ break;
+ default:
+ sendImagePickingIntent();
+ }
+ }
+ });
+ builder.show();
+ }
+
+ private void insertTable() {
+ final AlertDialog.Builder insertTableBuilder = new AlertDialog.Builder(mContext);
+ insertTableBuilder.setTitle(R.string.insert_table);
+ LayoutInflater layoutInflater = mContext.getLayoutInflater();
+ View numberPicker = layoutInflater.inflate(R.layout.number_picker, null);
+ final int minValue = 1;
+ final int maxValue = 20;
+ TextView npRowPositive = numberPicker.findViewById(R.id.number_picker_rows_positive);
+ TextView npRowNegative = numberPicker.findViewById(R.id.number_picker_rows_negative);
+ TextView npColPositive = numberPicker.findViewById(R.id.number_picker_cols_positive);
+ TextView npColNegative = numberPicker.findViewById(R.id.number_picker_cols_negative);
+ final TextView npRowCount = numberPicker.findViewById(R.id.number_picker_row_count);
+ final TextView npColCount = numberPicker.findViewById(R.id.number_picker_col_count);
+
+ View.OnClickListener positiveButtonClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int rowCount = Integer.parseInt(npRowCount.getText().toString());
+ int colCount = Integer.parseInt(npColCount.getText().toString());
+ final int id = v.getId();
+ if (id == R.id.number_picker_rows_positive && rowCount < maxValue) {
+ npRowCount.setText(String.valueOf(++rowCount));
+ } else if (id == R.id.number_picker_cols_positive && colCount < maxValue) {
+ npColCount.setText(String.valueOf(++colCount));
+ }
+ }
+ };
+
+ View.OnClickListener negativeButtonClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int rowCount = Integer.parseInt(npRowCount.getText().toString());
+ int colCount = Integer.parseInt(npColCount.getText().toString());
+ final int id = v.getId();
+ if (id == R.id.number_picker_rows_negative && rowCount > minValue) {
+ npRowCount.setText(String.valueOf(--rowCount));
+ } else if (id == R.id.number_picker_cols_negative && colCount > minValue) {
+ npColCount.setText(String.valueOf(--colCount));
+ }
+ }
+ };
+
+ npRowPositive.setOnClickListener(positiveButtonClickListener);
+ npColPositive.setOnClickListener(positiveButtonClickListener);
+ npRowNegative.setOnClickListener(negativeButtonClickListener);
+ npColNegative.setOnClickListener(negativeButtonClickListener);
+
+ insertTableBuilder.setView(numberPicker);
+ insertTableBuilder.setNeutralButton(R.string.alert_cancel, null);
+ insertTableBuilder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ try {
+ JSONObject cols = new JSONObject();
+ cols.put("type", "long");
+ cols.put("value", Integer.valueOf(npColCount.getText().toString()));
+ JSONObject rows = new JSONObject();
+ rows.put("type","long");
+ rows.put("value",Integer.valueOf(npRowCount.getText().toString()));
+ JSONObject params = new JSONObject();
+ params.put("Columns", cols);
+ params.put("Rows", rows);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertTable",params.toString()));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ }
+ });
+
+ AlertDialog.Builder insertBuilder = new AlertDialog.Builder(mContext);
+ insertBuilder.setTitle(R.string.select_insert_options);
+ insertBuilder.setNeutralButton(R.string.alert_cancel, null);
+ final int[] selectedItem = new int[1];
+ insertBuilder.setSingleChoiceItems(mContext.getResources().getStringArray(R.array.insertrowscolumns), -1, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ selectedItem[0] = which;
+ }
+ });
+ insertBuilder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (selectedItem[0]){
+ case 0:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertRowsBefore"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 1:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertRowsAfter"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 2:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertColumnsBefore"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 3:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertColumnsAfter"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 4:
+ insertTableBuilder.show();
+ break;
+
+ }
+ }
+ });
+ insertBuilder.show();
+ }
+
+ private void deleteTable() {
+ AlertDialog.Builder deleteBuilder = new AlertDialog.Builder(mContext);
+ deleteBuilder.setTitle(R.string.select_delete_options);
+ deleteBuilder.setNeutralButton(R.string.alert_cancel,null);
+ final int[] selectedItem = new int[1];
+ deleteBuilder.setSingleChoiceItems(mContext.getResources().getStringArray(R.array.deleterowcolumns), -1, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ selectedItem[0] = which;
+ }
+ });
+ deleteBuilder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (selectedItem[0]){
+ case 0:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteRows"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 1:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteColumns"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 2:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteTable"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ }
+ }
+ });
+ deleteBuilder.show();
+ }
+
+ private void sendImagePickingIntent() {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType("image/*");
+ mContext.startActivityForResult(Intent.createChooser(intent,
+ mContext.getResources().getString(R.string.select_photo_title)), SELECT_PHOTO);
+ }
+
+ private void dispatchTakePictureIntent() {
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
+ Snackbar.make(mContext.findViewById(R.id.button_insert_picture),
+ mContext.getResources().getString(R.string.no_camera_found), Snackbar.LENGTH_SHORT).show();
+ return;
+ }
+ Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ // Ensure that there's a camera activity to handle the intent
+ if (takePictureIntent.resolveActivity(mContext.getPackageManager()) != null) {
+ // Create the File where the photo should go
+ File photoFile = null;
+ try {
+ photoFile = createImageFile();
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ // Continue only if the File was successfully created
+ if (photoFile != null) {
+ Uri photoURI = FileProvider.getUriForFile(mContext,
+ mContext.getPackageName() + ".fileprovider",
+ photoFile);
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
+ // Grant permissions to potential photo/camera apps (for some Android versions)
+ List<ResolveInfo> resInfoList = mContext.getPackageManager()
+ .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ for (ResolveInfo resolveInfo : resInfoList) {
+ String packageName = resolveInfo.activityInfo.packageName;
+ mContext.grantUriPermission(packageName, photoURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ mContext.startActivityForResult(takePictureIntent, TAKE_PHOTO);
+ }
+ }
+ }
+
+ void handleActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == TAKE_PHOTO && resultCode == Activity.RESULT_OK) {
+ compressAndInsertImage();
+ } else if (requestCode == SELECT_PHOTO && resultCode == Activity.RESULT_OK) {
+ getFileFromURI(data.getData());
+ compressAndInsertImage();
+ }
+ }
+
+ void compressAndInsertImage() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ String[] options = {mContext.getResources().getString(R.string.compress_photo_smallest_size),
+ mContext.getResources().getString(R.string.compress_photo_medium_size),
+ mContext.getResources().getString(R.string.compress_photo_max_quality),
+ mContext.getResources().getString(R.string.compress_photo_no_compress)};
+ builder.setTitle(mContext.getResources().getString(R.string.compress_photo_title));
+ builder.setItems(options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int compressGrade;
+ switch (which) {
+ case 0:
+ compressGrade = 0;
+ break;
+ case 1:
+ compressGrade = 50;
+ break;
+ case 2:
+ compressGrade = 100;
+ break;
+ case 3:
+ compressGrade = -1;
+ break;
+ default:
+ compressGrade = -1;
+ }
+ compressImage(compressGrade);
+ sendInsertGraphic();
+ }
+ });
+ builder.show();
+ }
+
+ private void getFileFromURI(Uri uri) {
+ try {
+ InputStream input = mContext.getContentResolver().openInputStream(uri);
+ mCurrentPhotoPath = createImageFile().getAbsolutePath();
+ FileOutputStream output = new FileOutputStream(mCurrentPhotoPath);
+ if (input != null) {
+ byte[] buffer = new byte[IMAGE_BUFFER_SIZE];
+ int read;
+ while ((read = input.read(buffer)) != -1) {
+ output.write(buffer, 0, read);
+ }
+ input.close();
+ }
+ output.flush();
+ output.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendInsertGraphic() {
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "FileName", "string", "file://" + mCurrentPhotoPath);
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertGraphic", rootJson.toString()));
+ LOKitShell.sendEvent(new LOEvent(LOEvent.REFRESH));
+ mContext.setDocumentChanged(true);
+ }
+
+ private void compressImage(int grade) {
+ if (grade < 0 || grade > 100) {
+ return;
+ }
+ mContext.showProgressSpinner();
+ Bitmap bmp = BitmapFactory.decodeFile(mCurrentPhotoPath);
+ try {
+ mCurrentPhotoPath = createImageFile().getAbsolutePath();
+ FileOutputStream out = new FileOutputStream(mCurrentPhotoPath);
+ bmp.compress(Bitmap.CompressFormat.JPEG, grade, out);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ mContext.hideProgressSpinner();
+ }
+
+ private File createImageFile() throws IOException {
+ // Create an image file name
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
+ String imageFileName = "JPEG_" + timeStamp + "_";
+ File storageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+ File image = File.createTempFile(
+ imageFileName, /* prefix */
+ ".jpg", /* suffix */
+ storageDir /* directory */
+ );
+ // Save a file: path for use with ACTION_VIEW intents
+ mCurrentPhotoPath = image.getAbsolutePath();
+ return image;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/InvalidationHandler.java b/android/source/src/java/org/libreoffice/InvalidationHandler.java
new file mode 100644
index 0000000000..c48127cce6
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/InvalidationHandler.java
@@ -0,0 +1,768 @@
+package org.libreoffice;
+
+import android.content.Intent;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.canvas.SelectionHandle;
+import org.libreoffice.kit.Document;
+import org.libreoffice.kit.Office;
+import org.libreoffice.overlay.DocumentOverlay;
+import org.mozilla.gecko.gfx.GeckoLayerClient;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses (interprets) and handles invalidation messages from LibreOffice.
+ */
+public class InvalidationHandler implements Document.MessageCallback, Office.MessageCallback {
+ private static final String LOGTAG = InvalidationHandler.class.getSimpleName();
+ private final DocumentOverlay mDocumentOverlay;
+ private final GeckoLayerClient mLayerClient;
+ private OverlayState mState;
+ private boolean mKeyEvent = false;
+ private final LibreOfficeMainActivity mContext;
+
+ private int currentTotalPageNumber = 0; // total page number of the current document
+
+ public InvalidationHandler(LibreOfficeMainActivity context) {
+ mContext = context;
+ mDocumentOverlay = mContext.getDocumentOverlay();
+ mLayerClient = mContext.getLayerClient();
+ mState = OverlayState.NONE;
+ }
+
+ /**
+ * Processes callback message
+ *
+ * @param messageID - ID of the message
+ * @param payload - additional invalidation message payload
+ */
+ @Override
+ public void messageRetrieved(int messageID, String payload) {
+ if (!LOKitShell.isEditingEnabled()) {
+ // enable handling of hyperlinks and search result even in the Viewer
+ if (messageID != Document.CALLBACK_INVALIDATE_TILES
+ && messageID != Document.CALLBACK_DOCUMENT_PASSWORD
+ && messageID != Document.CALLBACK_HYPERLINK_CLICKED
+ && messageID != Document.CALLBACK_SEARCH_RESULT_SELECTION
+ && messageID != Document.CALLBACK_SC_FOLLOW_JUMP
+ && messageID != Document.CALLBACK_TEXT_SELECTION
+ && messageID != Document.CALLBACK_TEXT_SELECTION_START
+ && messageID != Document.CALLBACK_TEXT_SELECTION_END)
+ return;
+ }
+ switch (messageID) {
+ case Document.CALLBACK_INVALIDATE_TILES:
+ invalidateTiles(payload);
+ break;
+ case Document.CALLBACK_UNO_COMMAND_RESULT:
+ unoCommandResult(payload);
+ break;
+ case Document.CALLBACK_INVALIDATE_VISIBLE_CURSOR:
+ invalidateCursor(payload);
+ break;
+ case Document.CALLBACK_TEXT_SELECTION:
+ textSelection(payload);
+ break;
+ case Document.CALLBACK_TEXT_SELECTION_START:
+ textSelectionStart(payload);
+ break;
+ case Document.CALLBACK_TEXT_SELECTION_END:
+ textSelectionEnd(payload);
+ break;
+ case Document.CALLBACK_CURSOR_VISIBLE:
+ cursorVisibility(payload);
+ break;
+ case Document.CALLBACK_GRAPHIC_SELECTION:
+ graphicSelection(payload);
+ break;
+ case Document.CALLBACK_HYPERLINK_CLICKED:
+ if (!payload.startsWith("http://") && !payload.startsWith("https://")) {
+ payload = "http://" + payload;
+ }
+ Intent urlIntent = new Intent(Intent.ACTION_VIEW);
+ urlIntent.setData(Uri.parse(payload));
+ mContext.startActivity(urlIntent);
+ break;
+ case Document.CALLBACK_STATE_CHANGED:
+ stateChanged(payload);
+ break;
+ case Document.CALLBACK_SEARCH_RESULT_SELECTION:
+ searchResultSelection(payload);
+ // when doing a search, CALLBACK_SEARCH_RESULT_SELECTION is called in addition
+ // to the CALLBACK_TEXT_SELECTION{,_START,_END} callbacks and the handling of
+ // the previous 3 makes the cursor shown in addition to the selection rectangle,
+ // so hide the cursor again to just show the selection rectangle for the search result
+ mDocumentOverlay.hideCursor();
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.MIDDLE);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.START);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.END);
+ break;
+ case Document.CALLBACK_SEARCH_NOT_FOUND:
+ Log.d(LOGTAG, "LOK_CALLBACK: Search not found.");
+ // this callback is never caught. Hope someone fix this.
+ break;
+ case Document.CALLBACK_CELL_CURSOR:
+ invalidateCellCursor(payload);
+ break;
+ case Document.CALLBACK_SC_FOLLOW_JUMP:
+ jumpToCell(payload);
+ break;
+ case Document.CALLBACK_INVALIDATE_HEADER:
+ invalidateHeader();
+ break;
+ case Document.CALLBACK_CELL_ADDRESS:
+ cellAddress(payload);
+ break;
+ case Document.CALLBACK_CELL_FORMULA:
+ cellFormula(payload);
+ break;
+ case Document.CALLBACK_DOCUMENT_PASSWORD:
+ documentPassword();
+ break;
+ case Document.CALLBACK_DOCUMENT_SIZE_CHANGED:
+ pageSizeChanged(payload);
+ default:
+
+ Log.d(LOGTAG, "LOK_CALLBACK uncaught: " + messageID + " : " + payload);
+ }
+ }
+
+ private void unoCommandResult(String payload) {
+ try {
+ JSONObject payloadObject = new JSONObject(payload);
+ if (payloadObject.getString("commandName").equals(".uno:Save")) {
+ if (payloadObject.getString("success").equals("true")) {
+ mContext.saveFileToOriginalSource();
+ }
+ }else if(payloadObject.getString("commandName").equals(".uno:Name") ||
+ payloadObject.getString("commandName").equals(".uno:RenamePage")){
+ //success returns false even though its true for some reason,
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.getTileProvider().resetParts();
+ mContext.getDocumentPartViewListAdapter().notifyDataSetChanged();
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ Toast.makeText(mContext, mContext.getString(R.string.part_name_changed), Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else if(payloadObject.getString("commandName").equals(".uno:Remove") ||
+ payloadObject.getString("commandName").equals(".uno:DeletePage") ) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.getTileProvider().resetParts();
+ mContext.getDocumentPartViewListAdapter().notifyDataSetChanged();
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ Toast.makeText(mContext, mContext.getString(R.string.part_deleted), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }catch(JSONException e){
+ e.printStackTrace();
+ }
+ }
+
+ private void cellFormula(final String payload) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ ((EditText)mContext.findViewById(R.id.calc_formula)).setText(payload);
+ }
+ });
+ }
+
+ private void cellAddress(final String payload) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ ((EditText)mContext.findViewById(R.id.calc_address)).setText(payload);
+ }
+ });
+ }
+
+ private void invalidateHeader() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_CALC_HEADERS));
+ }
+
+ private void documentPassword() {
+ mContext.setPasswordProtected(true);
+ mContext.promptForPassword();
+ synchronized (this) {
+ try {
+ this.wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ mContext.setPassword();
+ }
+
+ private void invalidateCellCursor(String payload) {
+ RectF cellCursorRect = convertPayloadToRectangle(payload);
+
+ if (cellCursorRect != null) {
+ mDocumentOverlay.showCellSelection(cellCursorRect);
+ moveViewportToMakeSelectionVisible(cellCursorRect);
+ }
+ }
+
+ private void jumpToCell(String payload) {
+ RectF cellCursorRect = convertPayloadCellToRectangle(payload);
+
+ if (cellCursorRect != null) {
+ moveViewportToMakeSelectionVisible(cellCursorRect);
+ }
+ }
+
+ /**
+ * Handles the search result selection message, which is a JSONObject
+ *
+ * @param payload
+ */
+ private void searchResultSelection(String payload) {
+ RectF selectionRectangle = null;
+ try {
+ JSONObject collectiveResult = new JSONObject(payload);
+ JSONArray searchResult = collectiveResult.getJSONArray("searchResultSelection");
+ if (searchResult.length() == 1) {
+ String rectangle = searchResult.getJSONObject(0).getString("rectangles");
+ selectionRectangle = convertPayloadToRectangle(rectangle);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ if (selectionRectangle != null) {
+ moveViewportToMakeSelectionVisible(selectionRectangle);
+ }
+ }
+
+ /**
+ * Move the viewport to show the selection. The selection will appear at the
+ * viewport position depending on where the selection is relative to the
+ * viewport (either selection is above, below, on left or right). The difference
+ * between this method and moveViewportToMakeCursorVisible() is that this method
+ * takes into account the width and height of the selection and zooms out
+ * accordingly.
+ *
+ * @param selectionRectangle - selection position on the document
+ */
+ public void moveViewportToMakeSelectionVisible(RectF selectionRectangle) {
+ RectF moveToRect = mLayerClient.getViewportMetrics().getCssViewport();
+ if (moveToRect.contains(selectionRectangle)) {
+ return;
+ }
+
+ float newLeft = moveToRect.left;
+ float newTop = moveToRect.top;
+
+ // if selection rectangle is wider or taller than current viewport, we need to zoom out
+ float oldZoom = mLayerClient.getViewportMetrics().getZoomFactor();
+ float widthRatio = 1f;
+ float heightRatio = 1f;
+ if (moveToRect.width() < selectionRectangle.width()) {
+ widthRatio = selectionRectangle.width() / moveToRect.width() / 0.85f; // 0.85f gives some margin (must < 0.9)
+ }
+ if (moveToRect.height() < selectionRectangle.height()) {
+ heightRatio = selectionRectangle.height() / moveToRect.height() / 0.45f; // 0.45f gives some margin (must < 0.5)
+ }
+ float newZoom = widthRatio > heightRatio ? oldZoom/widthRatio : oldZoom/heightRatio;
+
+ // if selection is out of viewport we need to adjust accordingly
+ if (selectionRectangle.right < moveToRect.left || selectionRectangle.left < moveToRect.left) {
+ newLeft = selectionRectangle.left - (moveToRect.width() * 0.1f) * oldZoom / newZoom; // 0.1f gives left margin
+ } else if (selectionRectangle.right > moveToRect.right || selectionRectangle.left > moveToRect.right) {
+ newLeft = selectionRectangle.right - (moveToRect.width() * 0.9f) * oldZoom / newZoom; // 0.9f gives right margin
+ }
+
+ if (selectionRectangle.top < moveToRect.top || selectionRectangle.bottom < moveToRect.top) {
+ newTop = selectionRectangle.top - (moveToRect.height() * 0.1f) * oldZoom / newZoom; // 0.1f gives top margin
+ } else if (selectionRectangle.bottom > moveToRect.bottom || selectionRectangle.top > moveToRect.bottom){
+ newTop = selectionRectangle.bottom - (moveToRect.height() * 0.5f) * oldZoom / newZoom; // 0.5 f gives bottom margin
+ }
+
+ LOKitShell.moveViewportTo(mContext, new PointF(newLeft, newTop), newZoom);
+ }
+
+ private void pageSizeChanged(String payload){
+ if(mContext.getTileProvider().isTextDocument()){
+ String[] bounds = payload.split(",");
+ int pageWidth = Integer.parseInt(bounds[0]);
+ int pageHeight = Integer.parseInt(bounds[1].trim());
+ LOKitShell.sendEvent(new LOEvent(LOEvent.PAGE_SIZE_CHANGED, pageWidth, pageHeight));
+ }
+ }
+
+ private void stateChanged(String payload) {
+ String[] parts = payload.split("=");
+ if (parts.length < 2) {
+ Log.e(LOGTAG, "LOK_CALLBACK_STATE_CHANGED unexpected payload: " + payload);
+ return;
+ }
+ final String value = parts[1];
+ boolean pressed = Boolean.parseBoolean(value);
+ if (!mContext.getTileProvider().isReady()) {
+ Log.w(LOGTAG, "tile provider not ready, ignoring payload "+payload);
+ return;
+ }
+ if (parts[0].equals(".uno:Bold")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.BOLD, pressed);
+ } else if (parts[0].equals(".uno:Italic")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ITALIC, pressed);
+ } else if (parts[0].equals(".uno:Underline")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.UNDERLINE, pressed);
+ } else if (parts[0].equals(".uno:Strikeout")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.STRIKEOUT, pressed);
+ } else if (parts[0].equals(".uno:CharFontName")) {
+ mContext.getFontController().selectFont(value);
+ } else if (parts[0].equals(".uno:FontHeight")) {
+ mContext.getFontController().selectFontSize(value);
+ } else if (parts[0].equals(".uno:LeftPara")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ALIGN_LEFT, pressed);
+ } else if (parts[0].equals(".uno:CenterPara")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ALIGN_CENTER, pressed);
+ } else if (parts[0].equals(".uno:RightPara")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ALIGN_RIGHT, pressed);
+ } else if (parts[0].equals(".uno:JustifyPara")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ALIGN_JUSTIFY, pressed);
+ } else if (parts[0].equals(".uno:DefaultBullet")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.BULLET_LIST, pressed);
+ } else if (parts[0].equals(".uno:DefaultNumbering")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.NUMBERED_LIST, pressed);
+ } else if (parts[0].equals(".uno:Color")) {
+ mContext.getFontController().colorPaletteListener.updateColorPickerPosition(Integer.parseInt(value));
+ } else if (mContext.getTileProvider().isTextDocument() && (parts[0].equals(".uno:BackColor") || parts[0].equals(".uno:CharBackColor"))) {
+ mContext.getFontController().backColorPaletteListener.updateColorPickerPosition(Integer.parseInt(value));
+ } else if (mContext.getTileProvider().isPresentation() && parts[0].equals(".uno:CharBackColor")) {
+ mContext.getFontController().backColorPaletteListener.updateColorPickerPosition(Integer.parseInt(value));
+ } else if (mContext.getTileProvider().isSpreadsheet() && parts[0].equals(".uno:BackgroundColor")) {
+ mContext.getFontController().backColorPaletteListener.updateColorPickerPosition(Integer.parseInt(value));
+ } else if (parts[0].equals(".uno:StatePageNumber")) {
+ // get the total page number and compare to the current value and update accordingly
+ String[] splitStrings = parts[1].split(" ");
+ int totalPageNumber = Integer.valueOf(splitStrings[splitStrings.length - 1]);
+ if (totalPageNumber != currentTotalPageNumber) {
+ currentTotalPageNumber = totalPageNumber;
+ // update part page rectangles stored in DocumentOverlayView object
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_PART_PAGE_RECT));
+ }
+ } else {
+ Log.d(LOGTAG, "LOK_CALLBACK_STATE_CHANGED type uncatched: " + payload);
+ }
+ }
+
+ /**
+ * Parses the payload text with rectangle coordinates and converts to rectangle in pixel coordinates
+ *
+ * @param payload - invalidation message payload text
+ * @return rectangle in pixel coordinates
+ */
+ public RectF convertPayloadToRectangle(String payload) {
+ String payloadWithoutWhitespace = payload.replaceAll("\\s", ""); // remove all whitespace from the string
+
+ if (payloadWithoutWhitespace.isEmpty() || payloadWithoutWhitespace.equals("EMPTY")) {
+ return null;
+ }
+
+ String[] coordinates = payloadWithoutWhitespace.split(",");
+
+ if (coordinates.length != 4) {
+ return null;
+ }
+ return convertPayloadToRectangle(coordinates);
+ }
+
+ /**
+ * Parses the payload text with rectangle coordinates and converts to rectangle in pixel coordinates
+ *
+ * @param payload - invalidation message payload text
+ * @return rectangle in pixel coordinates
+ */
+ public RectF convertPayloadCellToRectangle(String payload) {
+ String payloadWithoutWhitespace = payload.replaceAll("\\s", ""); // remove all whitespace from the string
+
+ if (payloadWithoutWhitespace.isEmpty() || payloadWithoutWhitespace.equals("EMPTY")) {
+ return null;
+ }
+
+ String[] coordinates = payloadWithoutWhitespace.split(",");
+
+ if (coordinates.length != 6 ) {
+ return null;
+ }
+ return convertPayloadToRectangle(coordinates);
+ }
+
+ /**
+ * Converts rectangle coordinates to rectangle in pixel coordinates
+ *
+ * @param coordinates - the first four items defines the rectangle
+ * @return rectangle in pixel coordinates
+ */
+ public RectF convertPayloadToRectangle(String[] coordinates) {
+ if (coordinates.length < 4 ) {
+ return null;
+ }
+
+ int x = Integer.decode(coordinates[0]);
+ int y = Integer.decode(coordinates[1]);
+ int width = Integer.decode(coordinates[2]);
+ int height = Integer.decode(coordinates[3]);
+
+ float dpi = LOKitShell.getDpi(mContext);
+
+ return new RectF(
+ LOKitTileProvider.twipToPixel(x, dpi),
+ LOKitTileProvider.twipToPixel(y, dpi),
+ LOKitTileProvider.twipToPixel(x + width, dpi),
+ LOKitTileProvider.twipToPixel(y + height, dpi)
+ );
+ }
+
+ /**
+ * Parses the payload text with more rectangles (separated by ';') and converts to a list of rectangles.
+ *
+ * @param payload - invalidation message payload text
+ * @return list of rectangles
+ */
+ public List<RectF> convertPayloadToRectangles(String payload) {
+ List<RectF> rectangles = new ArrayList<RectF>();
+ String[] rectangleArray = payload.split(";");
+
+ for (String coordinates : rectangleArray) {
+ RectF rectangle = convertPayloadToRectangle(coordinates);
+ if (rectangle != null) {
+ rectangles.add(rectangle);
+ }
+
+ }
+
+ return rectangles;
+ }
+
+ /**
+ * Handles the tile invalidation message
+ *
+ * @param payload
+ */
+ private void invalidateTiles(String payload) {
+ RectF rectangle = convertPayloadToRectangle(payload);
+ if (rectangle != null) {
+ LOKitShell.sendTileInvalidationRequest(rectangle);
+ }
+ }
+
+ /**
+ * Handles the cursor invalidation message
+ *
+ * @param payload
+ */
+ private synchronized void invalidateCursor(String payload) {
+ RectF cursorRectangle = convertPayloadToRectangle(payload);
+ if (cursorRectangle != null) {
+ mDocumentOverlay.positionCursor(cursorRectangle);
+ mDocumentOverlay.positionHandle(SelectionHandle.HandleType.MIDDLE, cursorRectangle);
+
+ if (mState == OverlayState.TRANSITION || mState == OverlayState.CURSOR) {
+ changeStateTo(OverlayState.CURSOR);
+ }
+
+ if (mKeyEvent) {
+ moveViewportToMakeCursorVisible(cursorRectangle);
+ mKeyEvent = false;
+ }
+ }
+ }
+
+ /**
+ * Move the viewport to show the cursor. The cursor will appear at the
+ * viewport position depending on where the cursor is relative to the
+ * viewport (either cursor is above, below, on left or right).
+ *
+ * @param cursorRectangle - cursor position on the document
+ */
+ public void moveViewportToMakeCursorVisible(RectF cursorRectangle) {
+ RectF moveToRect = mLayerClient.getViewportMetrics().getCssViewport();
+ if (moveToRect.contains(cursorRectangle)) {
+ return;
+ }
+
+ float newLeft = moveToRect.left;
+ float newTop = moveToRect.top;
+
+ if (cursorRectangle.right < moveToRect.left || cursorRectangle.left < moveToRect.left) {
+ newLeft = cursorRectangle.left - (moveToRect.width() * 0.1f);
+ } else if (cursorRectangle.right > moveToRect.right || cursorRectangle.left > moveToRect.right) {
+ newLeft = cursorRectangle.right - (moveToRect.width() * 0.9f);
+ }
+
+ if (cursorRectangle.top < moveToRect.top || cursorRectangle.bottom < moveToRect.top) {
+ newTop = cursorRectangle.top - (moveToRect.height() * 0.1f);
+ } else if (cursorRectangle.bottom > moveToRect.bottom || cursorRectangle.top > moveToRect.bottom) {
+ newTop = cursorRectangle.bottom - (moveToRect.height() / 2.0f);
+ }
+
+ LOKitShell.moveViewportTo(mContext, new PointF(newLeft, newTop), null);
+ }
+
+ /**
+ * Handles the text selection start message
+ *
+ * @param payload
+ */
+ private synchronized void textSelectionStart(String payload) {
+ RectF selectionRect = convertPayloadToRectangle(payload);
+ if (selectionRect != null) {
+ mDocumentOverlay.positionHandle(SelectionHandle.HandleType.START, selectionRect);
+ }
+ }
+
+ /**
+ * Handles the text selection end message
+ *
+ * @param payload
+ */
+ private synchronized void textSelectionEnd(String payload) {
+ RectF selectionRect = convertPayloadToRectangle(payload);
+ if (selectionRect != null) {
+ mDocumentOverlay.positionHandle(SelectionHandle.HandleType.END, selectionRect);
+ }
+ }
+
+ /**
+ * Handles the text selection message
+ *
+ * @param payload
+ */
+ private synchronized void textSelection(String payload) {
+ if (payload.isEmpty() || payload.equals("EMPTY")) {
+ if (mState == OverlayState.SELECTION) {
+ changeStateTo(OverlayState.TRANSITION);
+ }
+ mDocumentOverlay.changeSelections(Collections.<RectF>emptyList());
+ if (mContext.getTileProvider().isSpreadsheet()) {
+ mDocumentOverlay.showHeaderSelection(null);
+ }
+ mContext.getToolbarController().showHideClipboardCutAndCopy(false);
+ } else {
+ List<RectF> rectangles = convertPayloadToRectangles(payload);
+ if (mState != OverlayState.SELECTION) {
+ changeStateTo(OverlayState.TRANSITION);
+ }
+ changeStateTo(OverlayState.SELECTION);
+ mDocumentOverlay.changeSelections(rectangles);
+ if (mContext.getTileProvider().isSpreadsheet()) {
+ mDocumentOverlay.showHeaderSelection(rectangles.get(0));
+ }
+ String selectedText = mContext.getTileProvider().getTextSelection("");
+ mContext.getToolbarController().showClipboardActions(selectedText);
+ }
+ }
+
+ /**
+ * Handles the cursor visibility message
+ *
+ * @param payload
+ */
+ private synchronized void cursorVisibility(String payload) {
+ if (payload.equals("true")) {
+ mDocumentOverlay.showCursor();
+ if (mState != OverlayState.SELECTION) {
+ mDocumentOverlay.showHandle(SelectionHandle.HandleType.MIDDLE);
+ }
+ } else if (payload.equals("false")) {
+ mDocumentOverlay.hideCursor();
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.MIDDLE);
+ }
+ }
+
+ /**
+ * Handles the graphic selection change message
+ *
+ * @param payload
+ */
+ private void graphicSelection(String payload) {
+ if (payload.isEmpty() || payload.equals("EMPTY")) {
+ if (mState == OverlayState.GRAPHIC_SELECTION) {
+ changeStateTo(OverlayState.TRANSITION);
+ }
+ } else {
+ RectF rectangle = convertPayloadToRectangle(payload);
+ mDocumentOverlay.changeGraphicSelection(rectangle);
+ if (mState != OverlayState.GRAPHIC_SELECTION) {
+ changeStateTo(OverlayState.TRANSITION);
+ }
+ changeStateTo(OverlayState.GRAPHIC_SELECTION);
+ }
+ }
+
+ /**
+ * Trigger a transition to a new overlay state.
+ *
+ * @param next - new state to transition to
+ */
+ public synchronized void changeStateTo(OverlayState next) {
+ changeState(mState, next);
+ }
+
+ /**
+ * Executes a transition from old overlay state to a new overlay state.
+ *
+ * @param previous - old state
+ * @param next - new state
+ */
+ private synchronized void changeState(OverlayState previous, OverlayState next) {
+ mState = next;
+ handleGeneralChangeState(previous, next);
+ switch (next) {
+ case CURSOR:
+ handleCursorState(previous);
+ break;
+ case SELECTION:
+ handleSelectionState(previous);
+ break;
+ case GRAPHIC_SELECTION:
+ handleGraphicSelectionState(previous);
+ break;
+ case TRANSITION:
+ handleTransitionState(previous);
+ break;
+ case NONE:
+ handleNoneState(previous);
+ break;
+ }
+ }
+
+ /**
+ * Handle a general transition - executed for all transitions.
+ */
+ private void handleGeneralChangeState(OverlayState previous, OverlayState next) {
+ if (previous == OverlayState.NONE &&
+ !mContext.getToolbarController().getEditModeStatus()) {
+ mContext.getToolbarController().switchToEditMode();
+ } else if (next == OverlayState.NONE &&
+ mContext.getToolbarController().getEditModeStatus()) {
+ mContext.getToolbarController().switchToViewMode();
+ }
+ }
+
+ /**
+ * Handle a transition to OverlayState.NONE state.
+ */
+ private void handleNoneState(OverlayState previous) {
+ if (previous == OverlayState.NONE) {
+ return;
+ }
+
+ // Just hide everything
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.START);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.END);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.MIDDLE);
+ mDocumentOverlay.hideSelections();
+ mDocumentOverlay.hideCursor();
+ mDocumentOverlay.hideGraphicSelection();
+ mContext.hideSoftKeyboard();
+ }
+
+ /**
+ * Handle a transition to OverlayState.SELECTION state.
+ */
+ private void handleSelectionState(OverlayState previous) {
+ mDocumentOverlay.showHandle(SelectionHandle.HandleType.START);
+ mDocumentOverlay.showHandle(SelectionHandle.HandleType.END);
+ mDocumentOverlay.showSelections();
+ }
+
+ /**
+ * Handle a transition to OverlayState.CURSOR state.
+ */
+ private void handleCursorState(OverlayState previous) {
+ mContext.showSoftKeyboardOrFormattingToolbar();
+ if (previous == OverlayState.TRANSITION) {
+ mDocumentOverlay.showHandle(SelectionHandle.HandleType.MIDDLE);
+ mDocumentOverlay.showCursor();
+ }
+ }
+
+ /**
+ * Handle a transition to OverlayState.TRANSITION state.
+ */
+ private void handleTransitionState(OverlayState previous) {
+ switch (previous) {
+ case SELECTION:
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.START);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.END);
+ mDocumentOverlay.hideSelections();
+ break;
+ case CURSOR:
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.MIDDLE);
+ break;
+ case GRAPHIC_SELECTION:
+ mDocumentOverlay.hideGraphicSelection();
+ break;
+ }
+ }
+
+ /**
+ * Handle a transition to OverlayState.GRAPHIC_SELECTION state.
+ */
+ private void handleGraphicSelectionState(OverlayState previous) {
+ mDocumentOverlay.showGraphicSelection();
+ mContext.hideSoftKeyboard();
+ }
+
+ /**
+ * The current state the overlay is in.
+ */
+ public OverlayState getCurrentState() {
+ return mState;
+ }
+
+ /**
+ * A key event happened (i.e. user started typing).
+ */
+ public void keyEvent() {
+ mKeyEvent = true;
+ }
+
+ /**
+ * The states the overlay.
+ */
+ public enum OverlayState {
+ /**
+ * State where the overlay is empty
+ */
+ NONE,
+ /**
+ * In-between state where we need to transition to a new overlay state.
+ * In this state we properly disable the older state and wait to transition
+ * to a new state triggered by an invalidation.
+ */
+ TRANSITION,
+ /**
+ * State where we operate with the cursor.
+ */
+ CURSOR,
+ /**
+ * State where we operate the graphic selection.
+ */
+ GRAPHIC_SELECTION,
+ /**
+ * State where we operate the text selection.
+ */
+ SELECTION
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOEvent.java b/android/source/src/java/org/libreoffice/LOEvent.java
new file mode 100644
index 0000000000..d1170eee12
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOEvent.java
@@ -0,0 +1,177 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.view.KeyEvent;
+
+import org.libreoffice.canvas.SelectionHandle;
+import org.mozilla.gecko.gfx.ComposedTileLayer;
+
+/**
+ * Events and data that is queued and processed by LOKitThread.
+ */
+public class LOEvent implements Comparable<LOEvent> {
+ public static final int SIZE_CHANGED = 1;
+ public static final int CHANGE_PART = 2;
+ public static final int LOAD = 3;
+ public static final int CLOSE = 4;
+ public static final int TILE_REEVALUATION_REQUEST = 5;
+ public static final int THUMBNAIL = 6;
+ public static final int TILE_INVALIDATION = 7;
+ public static final int TOUCH = 8;
+ public static final int KEY_EVENT = 9;
+ public static final int CHANGE_HANDLE_POSITION = 10;
+ public static final int SWIPE_RIGHT = 11;
+ public static final int SWIPE_LEFT = 12;
+ public static final int NAVIGATION_CLICK = 13;
+ public static final int UNO_COMMAND = 14;
+ public static final int LOAD_NEW = 16;
+ public static final int SAVE_AS = 17;
+ public static final int UPDATE_PART_PAGE_RECT = 18;
+ public static final int UPDATE_ZOOM_CONSTRAINTS = 19;
+ public static final int UPDATE_CALC_HEADERS = 20;
+ public static final int REFRESH = 21;
+ public static final int PAGE_SIZE_CHANGED = 22;
+ public static final int UNO_COMMAND_NOTIFY = 23;
+ public static final int SAVE_COPY_AS = 24;
+
+
+ public final int mType;
+ public int mPriority = 0;
+ private String mTypeString;
+
+ public ThumbnailCreator.ThumbnailCreationTask mTask;
+ public int mPartIndex;
+ public String mString;
+ public String filePath;
+ public String fileType;
+ public ComposedTileLayer mComposedTileLayer;
+ public String mTouchType;
+ public PointF mDocumentCoordinate;
+ public KeyEvent mKeyEvent;
+ public RectF mInvalidationRect;
+ public SelectionHandle.HandleType mHandleType;
+ public String mValue;
+ public int mPageWidth;
+ public int mPageHeight;
+ public boolean mNotify;
+
+ public LOEvent(int type) {
+ mType = type;
+ }
+
+ public LOEvent(int type, ComposedTileLayer composedTileLayer) {
+ mType = type;
+ mTypeString = "Tile Reevaluation";
+ mComposedTileLayer = composedTileLayer;
+ }
+
+ public LOEvent(int type, String someString) {
+ mType = type;
+ mTypeString = "String";
+ mString = someString;
+ mValue = null;
+ }
+
+ public LOEvent(int type, String someString, boolean notify) {
+ mType = type;
+ mTypeString = "String";
+ mString = someString;
+ mValue = null;
+ mNotify = notify;
+ }
+
+ public LOEvent(int type, String someString, String value, boolean notify) {
+ mType = type;
+ mTypeString = "String";
+ mString = someString;
+ mValue = value;
+ mNotify = notify;
+ }
+
+ public LOEvent(int type, String key, String value) {
+ mType = type;
+ mTypeString = "key / value";
+ mString = key;
+ mValue = value;
+ }
+
+ public LOEvent(String filePath, int type) {
+ mType = type;
+ mTypeString = "Load";
+ this.filePath = filePath;
+ }
+
+ public LOEvent(String filePath, String fileType, int type) {
+ mType = type;
+ mTypeString = "Load New/Save As";
+ this.filePath = filePath;
+ this.fileType = fileType;
+ }
+
+ public LOEvent(int type, int partIndex) {
+ mType = type;
+ mPartIndex = partIndex;
+ mTypeString = "Change part";
+ }
+
+ public LOEvent(int type, ThumbnailCreator.ThumbnailCreationTask task) {
+ mType = type;
+ mTask = task;
+ mTypeString = "Thumbnail";
+ }
+
+ public LOEvent(int type, String touchType, PointF documentTouchCoordinate) {
+ mType = type;
+ mTypeString = "Touch";
+ mTouchType = touchType;
+ mDocumentCoordinate = documentTouchCoordinate;
+ }
+
+ public LOEvent(int type, KeyEvent keyEvent) {
+ mType = type;
+ mTypeString = "Key Event";
+ mKeyEvent = keyEvent;
+ }
+
+ public LOEvent(int type, RectF rect) {
+ mType = type;
+ mTypeString = "Tile Invalidation";
+ mInvalidationRect = rect;
+ }
+
+ public LOEvent(int type, SelectionHandle.HandleType handleType, PointF documentCoordinate) {
+ mType = type;
+ mHandleType = handleType;
+ mDocumentCoordinate = documentCoordinate;
+ }
+
+ public LOEvent(int type, int pageWidth, int pageHeight){
+ mType = type;
+ mPageWidth = pageWidth;
+ mPageHeight = pageHeight;
+ }
+
+ public String getTypeString() {
+ if (mTypeString == null) {
+ return "Event type: " + mType;
+ }
+ return mTypeString;
+ }
+
+ @Override
+ public int compareTo(LOEvent another) {
+ return mPriority - another.mPriority;
+ }
+
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOKitInputConnectionHandler.java b/android/source/src/java/org/libreoffice/LOKitInputConnectionHandler.java
new file mode 100644
index 0000000000..7b50ef5ff7
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitInputConnectionHandler.java
@@ -0,0 +1,74 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import org.mozilla.gecko.gfx.InputConnectionHandler;
+
+/**
+ * Implementation of InputConnectionHandler. When a key event happens it is
+ * directed to this class which is then directed further to LOKitThread.
+ */
+public class LOKitInputConnectionHandler implements InputConnectionHandler {
+ private static final String LOGTAG = LOKitInputConnectionHandler.class.getSimpleName();
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return null;
+ }
+
+ /**
+ * When key pre-Ime happens.
+ */
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * When key down event happens.
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ LOKitShell.sendKeyEvent(event);
+ return false;
+ }
+
+ /**
+ * When key long press event happens.
+ */
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * When key multiple event happens. Key multiple event is triggered when
+ * non-ascii characters are entered on soft keyboard.
+ */
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ LOKitShell.sendKeyEvent(event);
+ return false;
+ }
+
+ /**
+ * When key up event happens.
+ */
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ LOKitShell.sendKeyEvent(event);
+ return false;
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOKitShell.java b/android/source/src/java/org/libreoffice/LOKitShell.java
new file mode 100644
index 0000000000..f6a76228e0
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitShell.java
@@ -0,0 +1,169 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.view.KeyEvent;
+
+import org.libreoffice.canvas.SelectionHandle;
+import org.mozilla.gecko.gfx.ComposedTileLayer;
+
+/**
+ * Common static LOKit functions, functions to send events.
+ */
+public class LOKitShell {
+ public static float getDpi(Context context) {
+ LOKitTileProvider tileProvider = ((LibreOfficeMainActivity)context).getTileProvider();
+ if (tileProvider != null && tileProvider.isSpreadsheet())
+ return 96f;
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ return metrics.density * 160;
+ }
+
+ // Get a Handler for the main java thread
+ public static Handler getMainHandler() {
+ return LibreOfficeApplication.getMainHandler();
+ }
+
+ public static void showProgressSpinner(final LibreOfficeMainActivity context) {
+ getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ context.showProgressSpinner();
+ }
+ });
+ }
+
+ public static void hideProgressSpinner(final LibreOfficeMainActivity context) {
+ getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ context.hideProgressSpinner();
+ }
+ });
+ }
+
+ public static int getMemoryClass(Context context) {
+ ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ return activityManager.getMemoryClass() * 1024 * 1024;
+ }
+
+ public static boolean isEditingEnabled() {
+ return !LibreOfficeMainActivity.isReadOnlyMode();
+ }
+
+ // EVENTS
+
+ /**
+ * Make sure LOKitThread is running and send event to it.
+ */
+ public static void sendEvent(LOEvent event) {
+ LibreOfficeMainActivity.loKitThread.queueEvent(event);
+ }
+
+ public static void sendThumbnailEvent(ThumbnailCreator.ThumbnailCreationTask task) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.THUMBNAIL, task));
+ }
+
+ /**
+ * Send touch event to LOKitThread.
+ */
+ public static void sendTouchEvent(String touchType, PointF documentTouchCoordinate) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.TOUCH, touchType, documentTouchCoordinate));
+ }
+
+ /**
+ * Send key event to LOKitThread.
+ */
+ public static void sendKeyEvent(KeyEvent event) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.KEY_EVENT, event));
+ }
+
+ public static void sendSizeChangedEvent(int width, int height) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.SIZE_CHANGED));
+ }
+
+ public static void sendSwipeRightEvent() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.SWIPE_RIGHT));
+ }
+
+ public static void sendSwipeLeftEvent() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.SWIPE_LEFT));
+ }
+
+ public static void sendChangePartEvent(int part) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.CHANGE_PART, part));
+ }
+
+ public static void sendLoadEvent(String inputFilePath) {
+ LOKitShell.sendEvent(new LOEvent(inputFilePath, LOEvent.LOAD));
+ }
+
+ public static void sendNewDocumentLoadEvent(String newDocumentPath, String newDocumentType) {
+ LOKitShell.sendEvent(new LOEvent(newDocumentPath, newDocumentType, LOEvent.LOAD_NEW));
+ }
+
+ public static void sendSaveAsEvent(String filePath, String fileFormat) {
+ LOKitShell.sendEvent(new LOEvent(filePath, fileFormat, LOEvent.SAVE_AS));
+ }
+
+ public static void sendSaveCopyAsEvent(String filePath, String fileFormat) {
+ LOKitShell.sendEvent(new LOEvent(filePath, fileFormat, LOEvent.SAVE_COPY_AS));
+ }
+
+ public static void sendCloseEvent() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.CLOSE));
+ }
+
+ /**
+ * Send tile reevaluation to LOKitThread.
+ */
+ public static void sendTileReevaluationRequest(ComposedTileLayer composedTileLayer) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.TILE_REEVALUATION_REQUEST, composedTileLayer));
+ }
+
+ /**
+ * Send tile invalidation to LOKitThread.
+ */
+ public static void sendTileInvalidationRequest(RectF rect) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.TILE_INVALIDATION, rect));
+ }
+
+ /**
+ * Send change handle position event to LOKitThread.
+ */
+ public static void sendChangeHandlePositionEvent(SelectionHandle.HandleType handleType, PointF documentCoordinate) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.CHANGE_HANDLE_POSITION, handleType, documentCoordinate));
+ }
+
+ public static void sendNavigationClickEvent() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.NAVIGATION_CLICK));
+ }
+
+ /**
+ * Move the viewport to the desired point (top-left), and change the zoom level.
+ * Ensure this runs on the UI thread.
+ */
+ public static void moveViewportTo(final LibreOfficeMainActivity context, final PointF position, final Float zoom) {
+ context.getLayerClient().post(new Runnable() {
+ @Override
+ public void run() {
+ context.getLayerClient().moveTo(position, zoom);
+ }
+ });
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOKitThread.java b/android/source/src/java/org/libreoffice/LOKitThread.java
new file mode 100644
index 0000000000..fd40c30891
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitThread.java
@@ -0,0 +1,449 @@
+package org.libreoffice;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import org.libreoffice.canvas.SelectionHandle;
+import org.mozilla.gecko.ZoomConstraints;
+import org.mozilla.gecko.gfx.CairoImage;
+import org.mozilla.gecko.gfx.ComposedTileLayer;
+import org.mozilla.gecko.gfx.GeckoLayerClient;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+import org.mozilla.gecko.gfx.SubTile;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/*
+ * Thread that communicates with LibreOffice through LibreOfficeKit JNI interface. The thread
+ * consumes events from other threads (mainly the UI thread) and acts accordingly.
+ */
+class LOKitThread extends Thread {
+ private static final String LOGTAG = LOKitThread.class.getSimpleName();
+
+ private final LinkedBlockingQueue<LOEvent> mEventQueue = new LinkedBlockingQueue<LOEvent>();
+
+ private TileProvider mTileProvider;
+ private InvalidationHandler mInvalidationHandler;
+ private ImmutableViewportMetrics mViewportMetrics;
+ private GeckoLayerClient mLayerClient;
+ private final LibreOfficeMainActivity mContext;
+
+ LOKitThread(LibreOfficeMainActivity context) {
+ mContext = context;
+ mInvalidationHandler = null;
+ TileProviderFactory.initialize();
+ }
+
+ /**
+ * Starting point of the thread. Processes events that gather in the queue.
+ */
+ @Override
+ public void run() {
+ while (true) {
+ LOEvent event;
+ try {
+ event = mEventQueue.take();
+ processEvent(event);
+ } catch (InterruptedException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+ }
+
+ /**
+ * Viewport changed, Recheck if tiles need to be added / removed.
+ */
+ private void tileReevaluationRequest(ComposedTileLayer composedTileLayer) {
+ if (mTileProvider == null) {
+ return;
+ }
+ List<SubTile> tiles = new ArrayList<SubTile>();
+
+ mLayerClient.beginDrawing();
+ composedTileLayer.addNewTiles(tiles);
+ mLayerClient.endDrawing();
+
+ for (SubTile tile : tiles) {
+ TileIdentifier tileId = tile.id;
+ CairoImage image = mTileProvider.createTile(tileId.x, tileId.y, tileId.size, tileId.zoom);
+ mLayerClient.beginDrawing();
+ if (image != null) {
+ tile.setImage(image);
+ }
+ mLayerClient.endDrawing();
+ mLayerClient.forceRender();
+ }
+
+ mLayerClient.beginDrawing();
+ composedTileLayer.markTiles();
+ composedTileLayer.clearMarkedTiles();
+ mLayerClient.endDrawing();
+ mLayerClient.forceRender();
+ }
+
+ /**
+ * Invalidate tiles that intersect the input rect.
+ */
+ private void tileInvalidation(RectF rect) {
+ if (mLayerClient == null || mTileProvider == null) {
+ return;
+ }
+
+ mLayerClient.beginDrawing();
+
+ List<SubTile> tiles = new ArrayList<SubTile>();
+ mLayerClient.invalidateTiles(tiles, rect);
+
+ for (SubTile tile : tiles) {
+ CairoImage image = mTileProvider.createTile(tile.id.x, tile.id.y, tile.id.size, tile.id.zoom);
+ tile.setImage(image);
+ tile.invalidate();
+ }
+ mLayerClient.endDrawing();
+ mLayerClient.forceRender();
+ }
+
+ /**
+ * Handle the geometry change + draw.
+ */
+ private void redraw(boolean resetZoomAndPosition) {
+ if (mLayerClient == null || mTileProvider == null) {
+ // called too early...
+ return;
+ }
+
+ mLayerClient.setPageRect(0, 0, mTileProvider.getPageWidth(), mTileProvider.getPageHeight());
+ mViewportMetrics = mLayerClient.getViewportMetrics();
+ mLayerClient.setViewportMetrics(mViewportMetrics);
+
+ if (resetZoomAndPosition) {
+ zoomAndRepositionTheDocument();
+ }
+
+ mLayerClient.forceRedraw();
+ mLayerClient.forceRender();
+ }
+
+ /**
+ * Reposition the view (zoom and position) when the document is firstly shown. This is document type dependent.
+ */
+ private void zoomAndRepositionTheDocument() {
+ if (mTileProvider.isSpreadsheet()) {
+ // Don't do anything for spreadsheets - show at 100%
+ } else if (mTileProvider.isTextDocument()) {
+ // Always zoom text document to the beginning of the document and centered by width
+ float centerY = mViewportMetrics.getCssViewport().centerY();
+ mLayerClient.zoomTo(new RectF(0, centerY, mTileProvider.getPageWidth(), centerY));
+ } else {
+ // Other documents - always show the whole document on the screen,
+ // regardless of document shape and orientation.
+ if (mViewportMetrics.getViewport().width() < mViewportMetrics.getViewport().height()) {
+ mLayerClient.zoomTo(mTileProvider.getPageWidth(), 0);
+ } else {
+ mLayerClient.zoomTo(0, mTileProvider.getPageHeight());
+ }
+ }
+ }
+
+ /**
+ * Invalidate everything + handle the geometry change
+ */
+ private void refresh(boolean resetZoomAndPosition) {
+ mLayerClient.clearAndResetlayers();
+ redraw(resetZoomAndPosition);
+ updatePartPageRectangles();
+ if (mTileProvider != null && mTileProvider.isSpreadsheet()) {
+ updateCalcHeaders();
+ }
+ }
+
+ /**
+ * Update part page rectangles which hold positions of each document page.
+ * Result is stored in DocumentOverlayView class.
+ */
+ private void updatePartPageRectangles() {
+ if (mTileProvider == null) {
+ Log.d(LOGTAG, "mTileProvider==null when calling updatePartPageRectangles");
+ return;
+ }
+ String partPageRectString = ((LOKitTileProvider) mTileProvider).getPartPageRectangles();
+ List<RectF> partPageRectangles = mInvalidationHandler.convertPayloadToRectangles(partPageRectString);
+ mContext.getDocumentOverlay().setPartPageRectangles(partPageRectangles);
+ }
+
+ private void updatePageSize(int pageWidth, int pageHeight){
+ mTileProvider.setDocumentSize(pageWidth, pageHeight);
+ redraw(true);
+ }
+
+ private void updateZoomConstraints() {
+ if (mTileProvider == null) return;
+ mLayerClient = mContext.getLayerClient();
+ // Set default zoom to the page width and min zoom so that the whole page is visible
+ final float pageHeightZoom = mLayerClient.getViewportMetrics().getHeight() / mTileProvider.getPageHeight();
+ final float pageWidthZoom = mLayerClient.getViewportMetrics().getWidth() / mTileProvider.getPageWidth();
+ final float minZoom = Math.min(pageWidthZoom, pageHeightZoom);
+ mLayerClient.setZoomConstraints(new ZoomConstraints(pageWidthZoom, minZoom, 0f));
+ }
+
+ /**
+ * Change part of the document.
+ */
+ private void changePart(int partIndex) {
+ LOKitShell.showProgressSpinner(mContext);
+ mTileProvider.changePart(partIndex);
+ mViewportMetrics = mLayerClient.getViewportMetrics();
+ // mLayerClient.setViewportMetrics(mViewportMetrics.scaleTo(0.9f, new PointF()));
+ refresh(true);
+ LOKitShell.hideProgressSpinner(mContext);
+ }
+
+ /**
+ * Handle load document event.
+ * @param filePath - filePath to where the document is located
+ * @return Whether the document has been loaded successfully.
+ */
+ private boolean loadDocument(String filePath) {
+ mLayerClient = mContext.getLayerClient();
+
+ mInvalidationHandler = new InvalidationHandler(mContext);
+ mTileProvider = TileProviderFactory.create(mContext, mInvalidationHandler, filePath);
+
+ if (mTileProvider.isReady()) {
+ LOKitShell.showProgressSpinner(mContext);
+ updateZoomConstraints();
+ refresh(true);
+ LOKitShell.hideProgressSpinner(mContext);
+ return true;
+ } else {
+ closeDocument();
+ return false;
+ }
+ }
+
+ /**
+ * Handle load new document event.
+ * @param filePath - filePath to where new document is to be created
+ * @param fileType - fileType what type of new document is to be loaded
+ */
+ private void loadNewDocument(String filePath, String fileType) {
+ boolean ok = loadDocument(fileType);
+ if (ok) {
+ mTileProvider.saveDocumentAs(filePath, true);
+ }
+ }
+
+ /**
+ * Save the currently loaded document.
+ */
+ private void saveDocumentAs(String filePath, String fileType, boolean bTakeOwnership) {
+ if (mTileProvider == null) {
+ Log.e(LOGTAG, "Error in saving, Tile Provider instance is null");
+ } else {
+ mTileProvider.saveDocumentAs(filePath, fileType, bTakeOwnership);
+ }
+ }
+
+ /**
+ * Close the currently loaded document.
+ */
+ private void closeDocument() {
+ if (mTileProvider != null) {
+ mTileProvider.close();
+ mTileProvider = null;
+ }
+ }
+
+ /**
+ * Process the input event.
+ */
+ private void processEvent(LOEvent event) {
+ switch (event.mType) {
+ case LOEvent.LOAD:
+ loadDocument(event.filePath);
+ break;
+ case LOEvent.LOAD_NEW:
+ loadNewDocument(event.filePath, event.fileType);
+ break;
+ case LOEvent.SAVE_AS:
+ saveDocumentAs(event.filePath, event.fileType, true);
+ break;
+ case LOEvent.SAVE_COPY_AS:
+ saveDocumentAs(event.filePath, event.fileType, false);
+ break;
+ case LOEvent.CLOSE:
+ closeDocument();
+ break;
+ case LOEvent.SIZE_CHANGED:
+ redraw(false);
+ break;
+ case LOEvent.CHANGE_PART:
+ changePart(event.mPartIndex);
+ break;
+ case LOEvent.TILE_INVALIDATION:
+ tileInvalidation(event.mInvalidationRect);
+ break;
+ case LOEvent.THUMBNAIL:
+ createThumbnail(event.mTask);
+ break;
+ case LOEvent.TOUCH:
+ touch(event.mTouchType, event.mDocumentCoordinate);
+ break;
+ case LOEvent.KEY_EVENT:
+ keyEvent(event.mKeyEvent);
+ break;
+ case LOEvent.TILE_REEVALUATION_REQUEST:
+ tileReevaluationRequest(event.mComposedTileLayer);
+ break;
+ case LOEvent.CHANGE_HANDLE_POSITION:
+ changeHandlePosition(event.mHandleType, event.mDocumentCoordinate);
+ break;
+ case LOEvent.SWIPE_LEFT:
+ if (null != mTileProvider) onSwipeLeft();
+ break;
+ case LOEvent.SWIPE_RIGHT:
+ if (null != mTileProvider) onSwipeRight();
+ break;
+ case LOEvent.NAVIGATION_CLICK:
+ mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.NONE);
+ break;
+ case LOEvent.UNO_COMMAND:
+ if (null == mTileProvider)
+ Log.e(LOGTAG, "no mTileProvider when trying to process "+event.mValue+" from UNO_COMMAND "+event.mString);
+ else
+ mTileProvider.postUnoCommand(event.mString, event.mValue);
+ break;
+ case LOEvent.UPDATE_PART_PAGE_RECT:
+ updatePartPageRectangles();
+ break;
+ case LOEvent.UPDATE_ZOOM_CONSTRAINTS:
+ updateZoomConstraints();
+ break;
+ case LOEvent.UPDATE_CALC_HEADERS:
+ updateCalcHeaders();
+ break;
+ case LOEvent.UNO_COMMAND_NOTIFY:
+ if (null == mTileProvider)
+ Log.e(LOGTAG, "no mTileProvider when trying to process "+event.mValue+" from UNO_COMMAND "+event.mString);
+ else
+ mTileProvider.postUnoCommand(event.mString, event.mValue, event.mNotify);
+ break;
+ case LOEvent.REFRESH:
+ refresh(false);
+ break;
+ case LOEvent.PAGE_SIZE_CHANGED:
+ updatePageSize(event.mPageWidth, event.mPageHeight);
+ break;
+ }
+ }
+
+ private void updateCalcHeaders() {
+ if (null == mTileProvider) return;
+ LOKitTileProvider tileProvider = (LOKitTileProvider)mTileProvider;
+ String values = tileProvider.getCalcHeaders();
+ mContext.getCalcHeadersController().setHeaders(values);
+ }
+
+ /**
+ * Request a change of the handle position.
+ */
+ private void changeHandlePosition(SelectionHandle.HandleType handleType, PointF documentCoordinate) {
+ switch (handleType) {
+ case MIDDLE:
+ mTileProvider.setTextSelectionReset(documentCoordinate);
+ break;
+ case START:
+ mTileProvider.setTextSelectionStart(documentCoordinate);
+ break;
+ case END:
+ mTileProvider.setTextSelectionEnd(documentCoordinate);
+ break;
+ }
+ }
+
+ /**
+ * Processes key events.
+ */
+ private void keyEvent(KeyEvent keyEvent) {
+ if (!LOKitShell.isEditingEnabled()) {
+ return;
+ }
+ if (mTileProvider == null) {
+ return;
+ }
+ mInvalidationHandler.keyEvent();
+ mTileProvider.sendKeyEvent(keyEvent);
+ }
+
+ /**
+ * Process swipe left event.
+ */
+ private void onSwipeLeft() {
+ mTileProvider.onSwipeLeft();
+ }
+
+ /**
+ * Process swipe right event.
+ */
+ private void onSwipeRight() {
+ mTileProvider.onSwipeRight();
+ }
+
+ /**
+ * Processes touch events.
+ */
+ private void touch(String touchType, PointF documentCoordinate) {
+ if (mTileProvider == null || mViewportMetrics == null) {
+ return;
+ }
+
+ // to handle hyperlinks, enable single tap even in the Viewer
+ boolean editing = LOKitShell.isEditingEnabled();
+ float zoomFactor = mViewportMetrics.getZoomFactor();
+
+ if (touchType.equals("LongPress")) {
+ mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.TRANSITION);
+ mTileProvider.mouseButtonDown(documentCoordinate, 1, zoomFactor);
+ mTileProvider.mouseButtonUp(documentCoordinate, 1, zoomFactor);
+ mTileProvider.mouseButtonDown(documentCoordinate, 2, zoomFactor);
+ mTileProvider.mouseButtonUp(documentCoordinate, 2, zoomFactor);
+ } else if (touchType.equals("SingleTap")) {
+ mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.TRANSITION);
+ mTileProvider.mouseButtonDown(documentCoordinate, 1, zoomFactor);
+ mTileProvider.mouseButtonUp(documentCoordinate, 1, zoomFactor);
+ } else if (touchType.equals("GraphicSelectionStart") && editing) {
+ mTileProvider.setGraphicSelectionStart(documentCoordinate);
+ } else if (touchType.equals("GraphicSelectionEnd") && editing) {
+ mTileProvider.setGraphicSelectionEnd(documentCoordinate);
+ }
+ }
+
+ /**
+ * Create thumbnail for the requested document task.
+ */
+ private void createThumbnail(final ThumbnailCreator.ThumbnailCreationTask task) {
+ final Bitmap bitmap = task.getThumbnail(mTileProvider);
+ task.applyBitmap(bitmap);
+ }
+
+ /**
+ * Queue an event.
+ */
+ public void queueEvent(LOEvent event) {
+ mEventQueue.add(event);
+ }
+
+ /**
+ * Clear all events in the queue (used when document is closed).
+ */
+ public void clearQueue() {
+ mEventQueue.clear();
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOKitTileProvider.java b/android/source/src/java/org/libreoffice/LOKitTileProvider.java
new file mode 100644
index 0000000000..5d1cf12209
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitTileProvider.java
@@ -0,0 +1,815 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.os.Build;
+import android.print.PrintAttributes;
+import android.print.PrintDocumentAdapter;
+import android.print.PrintManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.widget.Toast;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.kit.DirectBufferAllocator;
+import org.libreoffice.kit.Document;
+import org.libreoffice.kit.LibreOfficeKit;
+import org.libreoffice.kit.Office;
+import org.mozilla.gecko.gfx.BufferedCairoImage;
+import org.mozilla.gecko.gfx.CairoImage;
+import org.mozilla.gecko.gfx.IntSize;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+
+/**
+ * LOKit implementation of TileProvider.
+ */
+class LOKitTileProvider implements TileProvider {
+ private static final String LOGTAG = LOKitTileProvider.class.getSimpleName();
+ private static final int TILE_SIZE = 256;
+ private final float mTileWidth;
+ private final float mTileHeight;
+ private String mInputFile;
+ private Office mOffice;
+ private Document mDocument;
+ private final boolean mIsReady;
+ private final LibreOfficeMainActivity mContext;
+
+ private final float mDPI;
+ private float mWidthTwip;
+ private float mHeightTwip;
+
+ private final Document.MessageCallback mMessageCallback;
+
+ private final long objectCreationTime = System.currentTimeMillis();
+
+ /**
+ * Initialize LOKit and load the document.
+ * @param messageCallback - callback for messages retrieved from LOKit
+ * @param input - input path of the document
+ */
+ LOKitTileProvider(LibreOfficeMainActivity context, InvalidationHandler messageCallback, String input) {
+ mContext = context;
+ mMessageCallback = messageCallback;
+
+ LibreOfficeKit.putenv("SAL_LOG=+WARN+INFO");
+ LibreOfficeKit.init(mContext);
+
+ mOffice = new Office(LibreOfficeKit.getLibreOfficeKitHandle());
+ mOffice.setMessageCallback(messageCallback);
+ mOffice.setOptionalFeatures(Document.LOK_FEATURE_DOCUMENT_PASSWORD);
+ mContext.setTileProvider(this);
+ mInputFile = input;
+
+ Log.i(LOGTAG, "====> Loading file '" + input + "'");
+
+ File fileToBeEncoded = new File(input);
+ String encodedFileName = android.net.Uri.encode(fileToBeEncoded.getName());
+
+ mDocument = mOffice.documentLoad(
+ (new File(fileToBeEncoded.getParent(),encodedFileName)).getPath()
+ );
+
+ if (mDocument == null && !mContext.isPasswordProtected()) {
+ Log.i(LOGTAG, "====> mOffice.documentLoad() returned null, trying to restart 'Office' and loading again");
+ mOffice.destroy();
+ Log.i(LOGTAG, "====> mOffice.destroy() done");
+ ByteBuffer handle = LibreOfficeKit.getLibreOfficeKitHandle();
+ Log.i(LOGTAG, "====> getLibreOfficeKitHandle() = " + handle);
+ mOffice = new Office(handle);
+ Log.i(LOGTAG, "====> new Office created");
+ mOffice.setMessageCallback(messageCallback);
+ mOffice.setOptionalFeatures(Document.LOK_FEATURE_DOCUMENT_PASSWORD);
+ Log.i(LOGTAG, "====> setup Lokit callback and optional features (password support)");
+ mDocument = mOffice.documentLoad(
+ (new File(fileToBeEncoded.getParent(),encodedFileName)).getPath()
+ );
+ }
+
+ Log.i(LOGTAG, "====> mDocument = " + mDocument);
+
+ mDPI = LOKitShell.getDpi(mContext);
+ mTileWidth = pixelToTwip(TILE_SIZE, mDPI);
+ mTileHeight = pixelToTwip(TILE_SIZE, mDPI);
+
+ if (mDocument != null)
+ mDocument.initializeForRendering();
+
+ if (checkDocument()) {
+ postLoad();
+ mIsReady = true;
+ } else {
+ mIsReady = false;
+ }
+ }
+
+ /**
+ * Triggered after the document is loaded.
+ */
+ private void postLoad() {
+ mDocument.setMessageCallback(mMessageCallback);
+
+ resetParts();
+ // Writer documents always have one part, so hide the navigation drawer.
+ if (mDocument.getDocumentType() == Document.DOCTYPE_TEXT) {
+ mContext.disableNavigationDrawer();
+ mContext.getToolbarController().hideItem(R.id.action_parts);
+ }
+
+ // Enable headers for Calc documents
+ if (mDocument.getDocumentType() == Document.DOCTYPE_SPREADSHEET) {
+ mContext.initializeCalcHeaders();
+ }
+
+ mDocument.setPart(0);
+
+ setupDocumentFonts();
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.getDocumentPartViewListAdapter().notifyDataSetChanged();
+ }
+ });
+ }
+
+ public void addPart(){
+ int parts = mDocument.getParts();
+ if(mDocument.getDocumentType() == Document.DOCTYPE_SPREADSHEET){
+ try{
+ JSONObject jsonObject = new JSONObject();
+ JSONObject values = new JSONObject();
+ JSONObject values2 = new JSONObject();
+ values.put("type", "long");
+ values.put("value", 0); //add to the last
+ values2.put("type", "string");
+ values2.put("value", "");
+ jsonObject.put("Name", values2);
+ jsonObject.put("Index", values);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Insert", jsonObject.toString()));
+ }catch (JSONException e) {
+ e.printStackTrace();
+ }
+ } else if (mDocument.getDocumentType() == Document.DOCTYPE_PRESENTATION){
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertPage"));
+ }
+
+ String partName = mDocument.getPartName(parts);
+ if (partName.isEmpty()) {
+ partName = getGenericPartName(parts);
+ }
+ mDocument.setPart(parts);
+ resetDocumentSize();
+ final DocumentPartView partView = new DocumentPartView(parts, partName);
+ mContext.getDocumentPartView().add(partView);
+ }
+
+ public void resetParts(){
+ mContext.getDocumentPartView().clear();
+ if (mDocument.getDocumentType() != Document.DOCTYPE_TEXT) {
+ int parts = mDocument.getParts();
+ for (int i = 0; i < parts; i++) {
+ String partName = mDocument.getPartName(i);
+
+ if (partName.isEmpty()) {
+ partName = getGenericPartName(i);
+ }
+ Log.i(LOGTAG, "resetParts: " + partName);
+ mDocument.setPart(i);
+ resetDocumentSize();
+ final DocumentPartView partView = new DocumentPartView(i, partName);
+ mContext.getDocumentPartView().add(partView);
+ }
+ }
+ }
+
+ public void renamePart(String partName) {
+ try{
+ for(int i=0; i<mDocument.getParts(); i++){
+ if(mContext.getDocumentPartView().get(i).partName.equals(partName)){
+ //part name must be unique
+ Toast.makeText(mContext, mContext.getString(R.string.name_already_used), Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ JSONObject parameter = new JSONObject();
+ JSONObject name = new JSONObject();
+ name.put("type", "string");
+ name.put("value", partName);
+ parameter.put("Name", name);
+ if(isPresentation()){
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:RenamePage", parameter.toString(),true));
+ }else {
+ JSONObject index = new JSONObject();
+ index.put("type","long");
+ index.put("value", getCurrentPartNumber()+1);
+ parameter.put("Index", index);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Name", parameter.toString(),true));
+ }
+ }catch (JSONException e){
+ e.printStackTrace();
+ }
+ }
+
+ public void removePart() {
+ try{
+ if (!isSpreadsheet() && !isPresentation()) {
+ //document must be spreadsheet or presentation
+ return;
+ }
+
+ if(isPresentation()){
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:DeletePage", true));
+ return;
+ }
+
+ if(getPartsCount() < 2){
+ return;
+ }
+
+ JSONObject parameter = new JSONObject();
+ JSONObject index = new JSONObject();
+ index.put("type","long");
+ index.put("value", getCurrentPartNumber()+1);
+ parameter.put("Index", index);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Remove", parameter.toString(),true));
+ }catch (JSONException e){
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public boolean saveDocumentAs(final String filePath, String format, boolean takeOwnership) {
+ String options = "";
+ if (takeOwnership) {
+ options = "TakeOwnership";
+ }
+
+ final String newFilePath = "file://" + filePath;
+ Log.d("saveFilePathURL", newFilePath);
+ LOKitShell.showProgressSpinner(mContext);
+ mDocument.saveAs(newFilePath, format, options);
+ final boolean ok;
+ if (!mOffice.getError().isEmpty()){
+ ok = true;
+ Log.e("Save Error", mOffice.getError());
+ if (format.equals("svg")) {
+ // error in creating temp slideshow svg file
+ Log.d(LOGTAG, "Error in creating temp slideshow svg file");
+ } else if(format.equals("pdf")){
+ Log.d(LOGTAG, "Error in creating pdf file");
+ } else {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ // There was some error
+ mContext.showCustomStatusMessage(mContext.getString(R.string.unable_to_save));
+ }
+ });
+ }
+ } else {
+ ok = false;
+ if (format.equals("svg")) {
+ // successfully created temp slideshow svg file
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.startPresentation(newFilePath);
+ }
+ });
+ } else if (takeOwnership) {
+ mInputFile = filePath;
+ }
+ }
+ LOKitShell.hideProgressSpinner(mContext);
+ return ok;
+ }
+
+ @Override
+ public boolean saveDocumentAs(final String filePath, boolean takeOwnership) {
+ final int docType = mDocument.getDocumentType();
+ if (docType == Document.DOCTYPE_TEXT)
+ return saveDocumentAs(filePath, "odt", takeOwnership);
+ else if (docType == Document.DOCTYPE_SPREADSHEET)
+ return saveDocumentAs(filePath, "ods", takeOwnership);
+ else if (docType == Document.DOCTYPE_PRESENTATION)
+ return saveDocumentAs(filePath, "odp", takeOwnership);
+ else if (docType == Document.DOCTYPE_DRAWING)
+ return saveDocumentAs(filePath, "odg", takeOwnership);
+
+ Log.w(LOGTAG, "Cannot determine file format from document. Not saving.");
+ return false;
+ }
+
+ public void printDocument() {
+ String mInputFileName = (new File(mInputFile)).getName();
+ String file = mInputFileName.substring(0,(mInputFileName.length()-3))+"pdf";
+ String cacheFile = mContext.getExternalCacheDir().getAbsolutePath() + "/" + file;
+ mDocument.saveAs("file://"+cacheFile,"pdf","");
+ try {
+ PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
+ PrintDocumentAdapter printAdapter = new PDFDocumentAdapter(mContext, cacheFile);
+ printManager.print("Document", printAdapter, new PrintAttributes.Builder().build());
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void saveDocument(){
+ mContext.saveDocument();
+ }
+
+ private void setupDocumentFonts() {
+ String values = mDocument.getCommandValues(".uno:CharFontName");
+ if (values == null || values.isEmpty())
+ return;
+
+ mContext.getFontController().parseJson(values);
+ mContext.getFontController().setupFontViews();
+ }
+
+ private String getGenericPartName(int i) {
+ if (mDocument == null) {
+ return "";
+ }
+ switch (mDocument.getDocumentType()) {
+ case Document.DOCTYPE_DRAWING:
+ case Document.DOCTYPE_TEXT:
+ return mContext.getString(R.string.page) + " " + (i + 1);
+ case Document.DOCTYPE_SPREADSHEET:
+ return mContext.getString(R.string.sheet) + " " + (i + 1);
+ case Document.DOCTYPE_PRESENTATION:
+ return mContext.getString(R.string.slide) + " " + (i + 1);
+ case Document.DOCTYPE_OTHER:
+ default:
+ return mContext.getString(R.string.part) + " " + (i + 1);
+ }
+ }
+
+ static float twipToPixel(float input, float dpi) {
+ return input / 1440.0f * dpi;
+ }
+
+ private static float pixelToTwip(float input, float dpi) {
+ return (input / dpi) * 1440.0f;
+ }
+
+
+ /**
+ * @see TileProvider#getPartsCount()
+ */
+ @Override
+ public int getPartsCount() {
+ return mDocument.getParts();
+ }
+
+ /**
+ * Wrapper for getPartPageRectangles() JNI function.
+ */
+ public String getPartPageRectangles() {
+ return mDocument.getPartPageRectangles();
+ }
+
+ /**
+ * Fetch Calc header information.
+ */
+ public String getCalcHeaders() {
+ long nX = 0;
+ long nY = 0;
+ long nWidth = mDocument.getDocumentWidth();
+ long nHeight = mDocument.getDocumentHeight();
+ return mDocument.getCommandValues(".uno:ViewRowColumnHeaders?x=" + nX + "&y=" + nY
+ + "&width=" + nWidth + "&height=" + nHeight);
+ }
+
+ /**
+ * @see TileProvider#onSwipeLeft()
+ */
+ @Override
+ public void onSwipeLeft() {
+ if (mDocument.getDocumentType() == Document.DOCTYPE_PRESENTATION &&
+ getCurrentPartNumber() < getPartsCount()-1) {
+ LOKitShell.sendChangePartEvent(getCurrentPartNumber()+1);
+ }
+ }
+
+ /**
+ * @see TileProvider#onSwipeRight()
+ */
+ @Override
+ public void onSwipeRight() {
+ if (mDocument.getDocumentType() == Document.DOCTYPE_PRESENTATION &&
+ getCurrentPartNumber() > 0) {
+ LOKitShell.sendChangePartEvent(getCurrentPartNumber()-1);
+ }
+ }
+
+ private boolean checkDocument() {
+ String error = null;
+ boolean ret;
+
+ if (mDocument == null || !mOffice.getError().isEmpty()) {
+ error = "Cannot open " + mInputFile + ": " + mOffice.getError();
+ ret = false;
+ } else {
+ ret = resetDocumentSize();
+ if (!ret) {
+ error = "Document returned an invalid size or the document is empty.";
+ }
+ }
+
+ if (!ret && !mContext.isPasswordProtected()) {
+ final String message = error;
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.showAlertDialog(message);
+ }
+ });
+ } else if (!ret && mContext.isPasswordProtected()) {
+ mContext.finish();
+ }
+
+ return ret;
+ }
+
+ private boolean resetDocumentSize() {
+ mWidthTwip = mDocument.getDocumentWidth();
+ mHeightTwip = mDocument.getDocumentHeight();
+
+ if (mWidthTwip == 0 || mHeightTwip == 0) {
+ Log.e(LOGTAG, "Document size zero - last error: " + mOffice.getError());
+ return false;
+ } else {
+ Log.i(LOGTAG, "Reset document size: " + mDocument.getDocumentWidth() + " x " + mDocument.getDocumentHeight());
+ }
+
+ return true;
+ }
+
+ @Override
+ public void setDocumentSize(int pageWidth, int pageHeight){
+ mWidthTwip = pageWidth;
+ mHeightTwip = pageHeight;
+ }
+
+ /**
+ * @see TileProvider#getPageWidth()
+ */
+ @Override
+ public int getPageWidth() {
+ return (int) twipToPixel(mWidthTwip, mDPI);
+ }
+
+ /**
+ * @see TileProvider#getPageHeight()
+ */
+ @Override
+ public int getPageHeight() {
+ return (int) twipToPixel(mHeightTwip, mDPI);
+ }
+
+ /**
+ * @see TileProvider#isReady()
+ */
+ @Override
+ public boolean isReady() {
+ return mIsReady;
+ }
+
+ /**
+ * @see TileProvider#createTile(float, float, org.mozilla.gecko.gfx.IntSize, float)
+ */
+ @Override
+ public CairoImage createTile(float x, float y, IntSize tileSize, float zoom) {
+ ByteBuffer buffer = DirectBufferAllocator.guardedAllocate(tileSize.width * tileSize.height * 4);
+ if (buffer == null)
+ return null;
+
+ CairoImage image = new BufferedCairoImage(buffer, tileSize.width, tileSize.height, CairoImage.FORMAT_ARGB32);
+ rerenderTile(image, x, y, tileSize, zoom);
+ return image;
+ }
+
+ /**
+ * @see TileProvider#rerenderTile(org.mozilla.gecko.gfx.CairoImage, float, float, org.mozilla.gecko.gfx.IntSize, float)
+ */
+ @Override
+ public void rerenderTile(CairoImage image, float x, float y, IntSize tileSize, float zoom) {
+ if (mDocument != null && image.getBuffer() != null) {
+ float twipX = pixelToTwip(x, mDPI) / zoom;
+ float twipY = pixelToTwip(y, mDPI) / zoom;
+ float twipWidth = mTileWidth / zoom;
+ float twipHeight = mTileHeight / zoom;
+ long start = System.currentTimeMillis() - objectCreationTime;
+
+ //Log.i(LOGTAG, "paintTile >> @" + start + " (" + tileSize.width + " " + tileSize.height + " " + (int) twipX + " " + (int) twipY + " " + (int) twipWidth + " " + (int) twipHeight + ")");
+ mDocument.paintTile(image.getBuffer(), tileSize.width, tileSize.height, (int) twipX, (int) twipY, (int) twipWidth, (int) twipHeight);
+
+ long stop = System.currentTimeMillis() - objectCreationTime;
+ //Log.i(LOGTAG, "paintTile << @" + stop + " elapsed: " + (stop - start));
+ } else {
+ if (mDocument == null) {
+ Log.e(LOGTAG, "Document is null!!");
+ }
+ }
+ }
+
+ /**
+ * @see TileProvider#thumbnail(int)
+ */
+ @Override
+ public Bitmap thumbnail(int size) {
+ int widthPixel = getPageWidth();
+ int heightPixel = getPageHeight();
+
+ if (widthPixel > heightPixel) {
+ double ratio = heightPixel / (double) widthPixel;
+ widthPixel = size;
+ heightPixel = (int) (widthPixel * ratio);
+ } else {
+ double ratio = widthPixel / (double) heightPixel;
+ heightPixel = size;
+ widthPixel = (int) (heightPixel * ratio);
+ }
+
+ Log.w(LOGTAG, "Thumbnail size: " + getPageWidth() + " " + getPageHeight() + " " + widthPixel + " " + heightPixel);
+
+ ByteBuffer buffer = ByteBuffer.allocateDirect(widthPixel * heightPixel * 4);
+ if (mDocument != null)
+ mDocument.paintTile(buffer, widthPixel, heightPixel, 0, 0, (int) mWidthTwip, (int) mHeightTwip);
+
+ Bitmap bitmap = null;
+ try {
+ bitmap = Bitmap.createBitmap(widthPixel, heightPixel, Bitmap.Config.ARGB_8888);
+ bitmap.copyPixelsFromBuffer(buffer);
+ } catch (IllegalArgumentException e) {
+ Log.e(LOGTAG, "width (" + widthPixel + ") and height (" + heightPixel + ") must not be 0! (ToDo: likely timing issue)");
+ }
+ if (bitmap == null) {
+ Log.w(LOGTAG, "Thumbnail not created!");
+ }
+ return bitmap;
+ }
+
+ /**
+ * @see TileProvider#close()
+ */
+ @Override
+ public void close() {
+ Log.i(LOGTAG, "Document destroyed: " + mInputFile);
+ if (mDocument != null) {
+ mDocument.destroy();
+ mDocument = null;
+ }
+ }
+
+ /**
+ * @see TileProvider#isDrawing()
+ */
+ @Override
+ public boolean isDrawing() {
+ return mDocument != null && mDocument.getDocumentType() == Document.DOCTYPE_DRAWING;
+ }
+
+ /**
+ * @see TileProvider#isTextDocument()
+ */
+ @Override
+ public boolean isTextDocument() {
+ return mDocument != null && mDocument.getDocumentType() == Document.DOCTYPE_TEXT;
+ }
+
+ /**
+ * @see TileProvider#isSpreadsheet()
+ */
+ @Override
+ public boolean isSpreadsheet() {
+ return mDocument != null && mDocument.getDocumentType() == Document.DOCTYPE_SPREADSHEET;
+ }
+
+ /**
+ * @see TileProvider#isPresentation()
+ */
+ @Override
+ public boolean isPresentation(){
+ return mDocument != null && mDocument.getDocumentType() == Document.DOCTYPE_PRESENTATION;
+ }
+
+ /**
+ * Returns the Unicode character generated by this event or 0.
+ */
+ private int getCharCode(KeyEvent keyEvent) {
+ switch (keyEvent.getKeyCode())
+ {
+ case KeyEvent.KEYCODE_DEL:
+ case KeyEvent.KEYCODE_ENTER:
+ return 0;
+ }
+ return keyEvent.getUnicodeChar();
+ }
+
+ /**
+ * Returns the integer code representing the key of the event (non-zero for
+ * control keys).
+ */
+ private int getKeyCode(KeyEvent keyEvent) {
+ switch (keyEvent.getKeyCode()) {
+ case KeyEvent.KEYCODE_DEL:
+ return com.sun.star.awt.Key.BACKSPACE;
+ case KeyEvent.KEYCODE_ENTER:
+ return com.sun.star.awt.Key.RETURN;
+ }
+ return 0;
+ }
+
+ /**
+ * @see TileProvider#sendKeyEvent(android.view.KeyEvent)
+ */
+ @Override
+ public void sendKeyEvent(KeyEvent keyEvent) {
+ switch (keyEvent.getAction()) {
+ case KeyEvent.ACTION_MULTIPLE:
+ String keyString = keyEvent.getCharacters();
+ for (int i = 0; i < keyString.length(); i++) {
+ int codePoint = keyString.codePointAt(i);
+ mDocument.postKeyEvent(Document.KEY_EVENT_PRESS, codePoint, getKeyCode(keyEvent));
+ }
+ break;
+ case KeyEvent.ACTION_DOWN:
+ mDocument.postKeyEvent(Document.KEY_EVENT_PRESS, getCharCode(keyEvent), getKeyCode(keyEvent));
+ break;
+ case KeyEvent.ACTION_UP:
+ mDocument.postKeyEvent(Document.KEY_EVENT_RELEASE, getCharCode(keyEvent), getKeyCode(keyEvent));
+ break;
+ }
+ }
+
+ private void mouseButton(int type, PointF inDocument, int numberOfClicks, float zoomFactor) {
+ int x = (int) pixelToTwip(inDocument.x, mDPI);
+ int y = (int) pixelToTwip(inDocument.y, mDPI);
+
+ mDocument.setClientZoom(TILE_SIZE, TILE_SIZE, (int) (mTileWidth / zoomFactor), (int) (mTileHeight / zoomFactor));
+ mDocument.postMouseEvent(type, x, y, numberOfClicks, Document.MOUSE_BUTTON_LEFT, Document.KEYBOARD_MODIFIER_NONE);
+ }
+
+ /**
+ * @see TileProvider#mouseButtonDown(android.graphics.PointF, int, float)
+ */
+ @Override
+ public void mouseButtonDown(PointF documentCoordinate, int numberOfClicks, float zoomFactor) {
+ mouseButton(Document.MOUSE_EVENT_BUTTON_DOWN, documentCoordinate, numberOfClicks, zoomFactor);
+ }
+
+ /**
+ * @see TileProvider#mouseButtonUp(android.graphics.PointF, int, float)
+ */
+ @Override
+ public void mouseButtonUp(PointF documentCoordinate, int numberOfClicks, float zoomFactor) {
+ mouseButton(Document.MOUSE_EVENT_BUTTON_UP, documentCoordinate, numberOfClicks, zoomFactor);
+ }
+
+ /**
+ * @param command UNO command string
+ * @param arguments Arguments to UNO command
+ */
+ @Override
+ public void postUnoCommand(String command, String arguments) {
+ postUnoCommand(command, arguments, false);
+ }
+
+ /**
+ * @param command
+ * @param arguments
+ * @param notifyWhenFinished
+ */
+ @Override
+ public void postUnoCommand(String command, String arguments, boolean notifyWhenFinished) {
+ mDocument.postUnoCommand(command, arguments, notifyWhenFinished);
+ }
+
+ private void setTextSelection(int type, PointF documentCoordinate) {
+ int x = (int) pixelToTwip(documentCoordinate.x, mDPI);
+ int y = (int) pixelToTwip(documentCoordinate.y, mDPI);
+ mDocument.setTextSelection(type, x, y);
+ }
+
+ /**
+ * @see TileProvider#setTextSelectionStart(android.graphics.PointF)
+ */
+ @Override
+ public void setTextSelectionStart(PointF documentCoordinate) {
+ setTextSelection(Document.SET_TEXT_SELECTION_START, documentCoordinate);
+ }
+
+ /**
+ * @see TileProvider#setTextSelectionEnd(android.graphics.PointF)
+ */
+ @Override
+ public void setTextSelectionEnd(PointF documentCoordinate) {
+ setTextSelection(Document.SET_TEXT_SELECTION_END, documentCoordinate);
+ }
+
+ /**
+ * @see TileProvider#setTextSelectionReset(android.graphics.PointF)
+ */
+ @Override
+ public void setTextSelectionReset(PointF documentCoordinate) {
+ setTextSelection(Document.SET_TEXT_SELECTION_RESET, documentCoordinate);
+ }
+
+ /**
+ * @param mimeType
+ * @return
+ */
+ @Override
+ public String getTextSelection(String mimeType) {
+ return mDocument.getTextSelection(mimeType);
+ }
+
+ /**
+ * paste
+ * @param mimeType
+ * @param data
+ * @return
+ */
+ @Override
+ public boolean paste(String mimeType, String data) {
+ return mDocument.paste(mimeType, data);
+ }
+
+
+ /**
+ * @see org.libreoffice.TileProvider#setGraphicSelectionStart(android.graphics.PointF)
+ */
+ @Override
+ public void setGraphicSelectionStart(PointF documentCoordinate) {
+ setGraphicSelection(Document.SET_GRAPHIC_SELECTION_START, documentCoordinate);
+ }
+
+ /**
+ * @see org.libreoffice.TileProvider#setGraphicSelectionEnd(android.graphics.PointF)
+ */
+ @Override
+ public void setGraphicSelectionEnd(PointF documentCoordinate) {
+ setGraphicSelection(Document.SET_GRAPHIC_SELECTION_END, documentCoordinate);
+ }
+
+ private void setGraphicSelection(int type, PointF documentCoordinate) {
+ int x = (int) pixelToTwip(documentCoordinate.x, mDPI);
+ int y = (int) pixelToTwip(documentCoordinate.y, mDPI);
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ mDocument.setGraphicSelection(type, x, y);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
+ }
+
+ /**
+ * @see TileProvider#changePart(int)
+ */
+ @Override
+ public void changePart(int partIndex) {
+ if (mDocument == null)
+ return;
+
+ mDocument.setPart(partIndex);
+ resetDocumentSize();
+ }
+
+ /**
+ * @see TileProvider#getCurrentPartNumber()
+ */
+ @Override
+ public int getCurrentPartNumber() {
+ if (mDocument == null)
+ return 0;
+
+ return mDocument.getPart();
+ }
+
+ public void setDocumentPassword(String url, String password) {
+ mOffice.setDocumentPassword(url, password);
+ }
+
+ public Document.MessageCallback getMessageCallback() {
+ return mMessageCallback;
+ }
+}
+
+// vim:set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/android/source/src/java/org/libreoffice/LibreOfficeApplication.java b/android/source/src/java/org/libreoffice/LibreOfficeApplication.java
new file mode 100644
index 0000000000..ebe54cf27c
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LibreOfficeApplication.java
@@ -0,0 +1,33 @@
+/*
+ *
+ * * This file is part of the LibreOffice project.
+ * *
+ * * 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 org.libreoffice;
+
+import android.content.Context;
+import android.os.Handler;
+import androidx.multidex.MultiDexApplication;
+
+public class LibreOfficeApplication extends MultiDexApplication {
+
+ private static Handler mainHandler;
+
+ public LibreOfficeApplication() {
+ mainHandler = new Handler();
+ }
+
+ public static Handler getMainHandler() {
+ return mainHandler;
+ }
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(LocaleHelper.onAttach(base));
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java b/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java
new file mode 100644
index 0000000000..23bf8d27b6
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java
@@ -0,0 +1,1113 @@
+package org.libreoffice;
+
+import android.app.AlertDialog;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.AssetManager;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.DocumentsContract;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.snackbar.Snackbar;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import android.text.InputType;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TabHost;
+import android.widget.Toast;
+
+import org.libreoffice.overlay.CalcHeadersController;
+import org.libreoffice.overlay.DocumentOverlay;
+import org.libreoffice.ui.FileUtilities;
+import org.libreoffice.ui.LibreOfficeUIActivity;
+import org.mozilla.gecko.gfx.GeckoLayerClient;
+import org.mozilla.gecko.gfx.LayerView;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Main activity of the LibreOffice App. It is started in the UI thread.
+ */
+public class LibreOfficeMainActivity extends AppCompatActivity implements SettingsListenerModel.OnSettingsPreferenceChangedListener {
+
+ private static final String LOGTAG = "LibreOfficeMainActivity";
+ public static final String ENABLE_EXPERIMENTAL_PREFS_KEY = "ENABLE_EXPERIMENTAL";
+ private static final String ASSETS_EXTRACTED_PREFS_KEY = "ASSETS_EXTRACTED";
+ private static final String ENABLE_DEVELOPER_PREFS_KEY = "ENABLE_DEVELOPER";
+ private static final int REQUEST_CODE_SAVEAS = 12345;
+ private static final int REQUEST_CODE_EXPORT_TO_PDF = 12346;
+
+ //TODO "public static" is a temporary workaround
+ public static LOKitThread loKitThread;
+
+ private GeckoLayerClient mLayerClient;
+
+ private static boolean mIsExperimentalMode;
+ private static boolean mIsDeveloperMode;
+ private static boolean mbISReadOnlyMode;
+
+ private DrawerLayout mDrawerLayout;
+ Toolbar toolbarTop;
+
+ private ListView mDrawerList;
+ private final List<DocumentPartView> mDocumentPartView = new ArrayList<DocumentPartView>();
+ private DocumentPartViewListAdapter mDocumentPartViewListAdapter;
+ private DocumentOverlay mDocumentOverlay;
+ /** URI to save the document to. */
+ private Uri mDocumentUri;
+ /** Temporary local copy of the document. */
+ private File mTempFile = null;
+ private File mTempSlideShowFile = null;
+
+ BottomSheetBehavior bottomToolbarSheetBehavior;
+ BottomSheetBehavior toolbarColorPickerBottomSheetBehavior;
+ BottomSheetBehavior toolbarBackColorPickerBottomSheetBehavior;
+ private FormattingController mFormattingController;
+ private ToolbarController mToolbarController;
+ private FontController mFontController;
+ private SearchController mSearchController;
+ private UNOCommandsController mUNOCommandsController;
+ private CalcHeadersController mCalcHeadersController;
+ private LOKitTileProvider mTileProvider;
+ private String mPassword;
+ private boolean mPasswordProtected;
+ private boolean mbSkipNextRefresh;
+
+ public GeckoLayerClient getLayerClient() {
+ return mLayerClient;
+ }
+
+ public static boolean isExperimentalMode() {
+ return mIsExperimentalMode;
+ }
+
+ public static boolean isDeveloperMode() {
+ return mIsDeveloperMode;
+ }
+
+ private boolean isKeyboardOpen = false;
+ private boolean isFormattingToolbarOpen = false;
+ private boolean isSearchToolbarOpen = false;
+ private static boolean isDocumentChanged = false;
+ private boolean isUNOCommandsToolbarOpen = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.w(LOGTAG, "onCreate..");
+ super.onCreate(savedInstanceState);
+
+ SettingsListenerModel.getInstance().setListener(this);
+ updatePreferences();
+
+ setContentView(R.layout.activity_main);
+
+ toolbarTop = findViewById(R.id.toolbar);
+ hideBottomToolbar();
+
+ mToolbarController = new ToolbarController(this, toolbarTop);
+ mFormattingController = new FormattingController(this);
+ toolbarTop.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LOKitShell.sendNavigationClickEvent();
+ }
+ });
+
+ mFontController = new FontController(this);
+ mSearchController = new SearchController(this);
+ mUNOCommandsController = new UNOCommandsController(this);
+
+ loKitThread = new LOKitThread(this);
+ loKitThread.start();
+
+ mLayerClient = new GeckoLayerClient(this);
+ LayerView layerView = findViewById(R.id.layer_view);
+ mLayerClient.setView(layerView);
+ layerView.setInputConnectionHandler(new LOKitInputConnectionHandler());
+ mLayerClient.notifyReady();
+
+ layerView.setOnKeyListener(new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View view, int i, KeyEvent keyEvent) {
+ if(!isReadOnlyMode() && keyEvent.getKeyCode() != KeyEvent.KEYCODE_BACK){
+ setDocumentChanged(true);
+ }
+ return false;
+ }
+ });
+
+ // create TextCursorLayer
+ mDocumentOverlay = new DocumentOverlay(this, layerView);
+
+ mbISReadOnlyMode = !isExperimentalMode();
+
+ final Uri docUri = getIntent().getData();
+ if (docUri != null) {
+ if (docUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
+ || docUri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
+ final boolean isReadOnlyDoc = (getIntent().getFlags() & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == 0;
+ mbISReadOnlyMode = !isExperimentalMode() || isReadOnlyDoc;
+ Log.d(LOGTAG, "SCHEME_CONTENT: getPath(): " + docUri.getPath());
+
+ String displayName = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), docUri);
+ toolbarTop.setTitle(displayName);
+
+ } else if (docUri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
+ mbISReadOnlyMode = true;
+ Log.d(LOGTAG, "SCHEME_FILE: getPath(): " + docUri.getPath());
+ toolbarTop.setTitle(docUri.getLastPathSegment());
+ }
+ // create a temporary local copy to work with
+ boolean copyOK = copyFileToTemp(docUri) && mTempFile != null;
+ if (!copyOK) {
+ // TODO: can't open the file
+ Log.e(LOGTAG, "couldn't create temporary file from " + docUri);
+ return;
+ }
+
+ // if input doc is a template, a new doc is created and a proper URI to save to
+ // will only be available after a "Save As"
+ if (isTemplate(docUri)) {
+ toolbarTop.setTitle(R.string.default_document_name);
+ } else {
+ mDocumentUri = docUri;
+ }
+
+ LOKitShell.sendLoadEvent(mTempFile.getPath());
+ } else if (getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY) != null) {
+ // New document type string is not null, meaning we want to open a new document
+ String newDocumentType = getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY);
+ // create a temporary local file, will be copied to the actual URI when saving
+ loadNewDocument(newDocumentType);
+ toolbarTop.setTitle(getString(R.string.default_document_name));
+ } else {
+ Log.e(LOGTAG, "No document specified. This should never happen.");
+ return;
+ }
+ // the loadDocument/loadNewDocument event already triggers a refresh as well,
+ // so there's no need to do another refresh in 'onStart'
+ mbSkipNextRefresh = true;
+
+ mDrawerLayout = findViewById(R.id.drawer_layout);
+
+ if (mDocumentPartViewListAdapter == null) {
+ mDrawerList = findViewById(R.id.left_drawer);
+
+ mDocumentPartViewListAdapter = new DocumentPartViewListAdapter(this, R.layout.document_part_list_layout, mDocumentPartView);
+ mDrawerList.setAdapter(mDocumentPartViewListAdapter);
+ mDrawerList.setOnItemClickListener(new DocumentPartClickListener());
+ }
+
+ mToolbarController.setupToolbars();
+
+ TabHost host = findViewById(R.id.toolbarTabHost);
+ host.setup();
+
+ TabHost.TabSpec spec = host.newTabSpec(getString(R.string.tabhost_character));
+ spec.setContent(R.id.tab_character);
+ spec.setIndicator(getString(R.string.tabhost_character));
+ host.addTab(spec);
+
+ spec = host.newTabSpec(getString(R.string.tabhost_paragraph));
+ spec.setContent(R.id.tab_paragraph);
+ spec.setIndicator(getString(R.string.tabhost_paragraph));
+ host.addTab(spec);
+
+ spec = host.newTabSpec(getString(R.string.tabhost_insert));
+ spec.setContent(R.id.tab_insert);
+ spec.setIndicator(getString(R.string.tabhost_insert));
+ host.addTab(spec);
+
+ spec = host.newTabSpec(getString(R.string.tabhost_style));
+ spec.setContent(R.id.tab_style);
+ spec.setIndicator(getString(R.string.tabhost_style));
+ host.addTab(spec);
+
+ LinearLayout bottomToolbarLayout = findViewById(R.id.toolbar_bottom);
+ LinearLayout toolbarColorPickerLayout = findViewById(R.id.toolbar_color_picker);
+ LinearLayout toolbarBackColorPickerLayout = findViewById(R.id.toolbar_back_color_picker);
+ bottomToolbarSheetBehavior = BottomSheetBehavior.from(bottomToolbarLayout);
+ toolbarColorPickerBottomSheetBehavior = BottomSheetBehavior.from(toolbarColorPickerLayout);
+ toolbarBackColorPickerBottomSheetBehavior = BottomSheetBehavior.from(toolbarBackColorPickerLayout);
+ bottomToolbarSheetBehavior.setHideable(true);
+ toolbarColorPickerBottomSheetBehavior.setHideable(true);
+ toolbarBackColorPickerBottomSheetBehavior.setHideable(true);
+ }
+
+ private void updatePreferences() {
+ SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ mIsExperimentalMode = BuildConfig.ALLOW_EDITING
+ && sPrefs.getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY, false);
+ mIsDeveloperMode = mIsExperimentalMode
+ && sPrefs.getBoolean(ENABLE_DEVELOPER_PREFS_KEY, false);
+ if (sPrefs.getInt(ASSETS_EXTRACTED_PREFS_KEY, 0) != BuildConfig.VERSION_CODE) {
+ if(copyFromAssets(getAssets(), "unpack", getApplicationInfo().dataDir)) {
+ sPrefs.edit().putInt(ASSETS_EXTRACTED_PREFS_KEY, BuildConfig.VERSION_CODE).apply();
+ }
+ }
+ }
+
+ // Loads a new Document and saves it to a temporary file
+ private void loadNewDocument(String newDocumentType) {
+ String tempFileName = "LibreOffice_" + UUID.randomUUID().toString();
+ mTempFile = new File(this.getCacheDir(), tempFileName);
+ LOKitShell.sendNewDocumentLoadEvent(mTempFile.getPath(), newDocumentType);
+ }
+
+ public RectF getCurrentCursorPosition() {
+ return mDocumentOverlay.getCurrentCursorPosition();
+ }
+
+ private boolean copyFileToTemp(Uri documentUri) {
+ // CSV files need a .csv suffix to be opened in Calc.
+ String suffix = null;
+ String intentType = getIntent().getType();
+ // K-9 mail uses the first, GMail uses the second variant.
+ if ("text/comma-separated-values".equals(intentType) || "text/csv".equals(intentType))
+ suffix = ".csv";
+
+ try {
+ mTempFile = File.createTempFile("LibreOffice", suffix, this.getCacheDir());
+ final FileOutputStream outputStream = new FileOutputStream(mTempFile);
+ return copyUriToStream(documentUri, outputStream);
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Save the document.
+ */
+ public void saveDocument() {
+ Toast.makeText(this, R.string.message_saving, Toast.LENGTH_SHORT).show();
+ // local save
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Save", true));
+ }
+
+ /**
+ * Open file chooser and save the document to the URI
+ * selected there.
+ */
+ public void saveDocumentAs() {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ String mimeType = getODFMimeTypeForDocument();
+ intent.setType(mimeType);
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mDocumentUri);
+
+ startActivityForResult(intent, REQUEST_CODE_SAVEAS);
+ }
+
+ /**
+ * Saves the document under the given URI using ODF format
+ * and uses that URI from now on for all operations.
+ * @param newUri URI to save the document and use from now on.
+ */
+ private void saveDocumentAs(Uri newUri) {
+ mDocumentUri = newUri;
+ // save in ODF format
+ mTileProvider.saveDocumentAs(mTempFile.getPath(), true);
+ saveFileToOriginalSource();
+
+ String displayName = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), mDocumentUri);
+ toolbarTop.setTitle(displayName);
+ mbISReadOnlyMode = !isExperimentalMode();
+ getToolbarController().setupToolbars();
+ }
+
+ public void exportToPDF() {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType(FileUtilities.MIMETYPE_PDF);
+ // suggest directory and file name based on the doc
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mDocumentUri);
+ final String displayName = toolbarTop.getTitle().toString();
+ final String suggestedFileName = FileUtilities.stripExtensionFromFileName(displayName) + ".pdf";
+ intent.putExtra(Intent.EXTRA_TITLE, suggestedFileName);
+
+ startActivityForResult(intent, REQUEST_CODE_EXPORT_TO_PDF);
+ }
+
+ private void exportToPDF(final Uri uri) {
+ boolean exportOK = false;
+ File tempFile = null;
+ try {
+ tempFile = File.createTempFile("LibreOffice_", ".pdf");
+ mTileProvider.saveDocumentAs(tempFile.getAbsolutePath(),"pdf", false);
+
+ try {
+ FileInputStream inputStream = new FileInputStream(tempFile);
+ exportOK = copyStreamToUri(inputStream, uri);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (tempFile != null && tempFile.exists()) {
+ tempFile.delete();
+ }
+ }
+
+ final int msgId = exportOK ? R.string.pdf_export_finished : R.string.unable_to_export_pdf;
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ showCustomStatusMessage(getString(msgId));
+ }
+ });
+ }
+
+ /**
+ * Returns the ODF MIME type that can be used for the current document,
+ * regardless of whether the document is an ODF Document or not
+ * (e.g. returns FileUtilities.MIMETYPE_OPENDOCUMENT_TEXT for a DOCX file).
+ * @return MIME type, or empty string, if no appropriate MIME type could be found.
+ */
+ private String getODFMimeTypeForDocument() {
+ if (mTileProvider.isTextDocument())
+ return FileUtilities.MIMETYPE_OPENDOCUMENT_TEXT;
+ else if (mTileProvider.isSpreadsheet())
+ return FileUtilities.MIMETYPE_OPENDOCUMENT_SPREADSHEET;
+ else if (mTileProvider.isPresentation())
+ return FileUtilities.MIMETYPE_OPENDOCUMENT_PRESENTATION;
+ else if (mTileProvider.isDrawing())
+ return FileUtilities.MIMETYPE_OPENDOCUMENT_GRAPHICS;
+ else {
+ Log.w(LOGTAG, "Cannot determine MIME type to use.");
+ return "";
+ }
+ }
+
+ /**
+ * Returns whether the MIME type for the URI is considered one for a document template.
+ */
+ private boolean isTemplate(final Uri documentUri) {
+ final String mimeType = getContentResolver().getType(documentUri);
+ return FileUtilities.isTemplateMimeType(mimeType);
+ }
+
+ public void saveFileToOriginalSource() {
+ if (mTempFile == null || mDocumentUri == null || !mDocumentUri.getScheme().equals(ContentResolver.SCHEME_CONTENT))
+ return;
+
+ boolean copyOK = false;
+ try {
+ final FileInputStream inputStream = new FileInputStream(mTempFile);
+ copyOK = copyStreamToUri(inputStream, mDocumentUri);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ if (copyOK) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(LibreOfficeMainActivity.this, R.string.message_saved,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ setDocumentChanged(false);
+ } else {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(LibreOfficeMainActivity.this, R.string.message_saving_failed,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.i(LOGTAG, "onResume..");
+ // check for config change
+ updatePreferences();
+ if (mToolbarController.getEditModeStatus() && isExperimentalMode()) {
+ mToolbarController.switchToEditMode();
+ } else {
+ mToolbarController.switchToViewMode();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ Log.i(LOGTAG, "onPause..");
+ super.onPause();
+ }
+
+ @Override
+ protected void onStart() {
+ Log.i(LOGTAG, "onStart..");
+ super.onStart();
+ if (!mbSkipNextRefresh) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.REFRESH));
+ }
+ mbSkipNextRefresh = false;
+ }
+
+ @Override
+ protected void onStop() {
+ Log.i(LOGTAG, "onStop..");
+ hideSoftKeyboardDirect();
+ super.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(LOGTAG, "onDestroy..");
+ LOKitShell.sendCloseEvent();
+ mLayerClient.destroy();
+ super.onDestroy();
+
+ if (isFinishing()) { // Not an orientation change
+ if (mTempFile != null) {
+ // noinspection ResultOfMethodCallIgnored
+ mTempFile.delete();
+ }
+ if (mTempSlideShowFile != null && mTempSlideShowFile.exists()) {
+ // noinspection ResultOfMethodCallIgnored
+ mTempSlideShowFile.delete();
+ }
+ }
+ }
+ @Override
+ public void onBackPressed() {
+ if (!isDocumentChanged) {
+ super.onBackPressed();
+ return;
+ }
+
+
+ DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which){
+ case DialogInterface.BUTTON_POSITIVE:
+ mTileProvider.saveDocument();
+ isDocumentChanged=false;
+ onBackPressed();
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ //CANCEL
+ break;
+ case DialogInterface.BUTTON_NEUTRAL:
+ //NO
+ isDocumentChanged=false;
+ onBackPressed();
+ break;
+ }
+ }
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.save_alert_dialog_title)
+ .setPositiveButton(R.string.save_document, dialogClickListener)
+ .setNegativeButton(R.string.action_cancel, dialogClickListener)
+ .setNeutralButton(R.string.no_save_document, dialogClickListener)
+ .show();
+
+ }
+
+ public List<DocumentPartView> getDocumentPartView() {
+ return mDocumentPartView;
+ }
+
+ public void disableNavigationDrawer() {
+ // Only the original thread that created mDrawerLayout should touch its views.
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, mDrawerList);
+ }
+ });
+ }
+
+ public DocumentPartViewListAdapter getDocumentPartViewListAdapter() {
+ return mDocumentPartViewListAdapter;
+ }
+
+ /**
+ * Show software keyboard.
+ * Force the request on main thread.
+ */
+ public void showSoftKeyboard() {
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if(!isKeyboardOpen) showSoftKeyboardDirect();
+ else hideSoftKeyboardDirect();
+ }
+ });
+
+ }
+
+ private void showSoftKeyboardDirect() {
+ LayerView layerView = findViewById(R.id.layer_view);
+
+ if (layerView.requestFocus()) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.showSoftInput(layerView, InputMethodManager.SHOW_FORCED);
+ }
+ isKeyboardOpen=true;
+ isSearchToolbarOpen=false;
+ isFormattingToolbarOpen=false;
+ isUNOCommandsToolbarOpen=false;
+ hideBottomToolbar();
+ }
+
+ public void showSoftKeyboardOrFormattingToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (findViewById(R.id.toolbar_bottom).getVisibility() != View.VISIBLE
+ && findViewById(R.id.toolbar_color_picker).getVisibility() != View.VISIBLE) {
+ showSoftKeyboardDirect();
+ }
+ }
+ });
+ }
+
+ /**
+ * Hides software keyboard on UI thread.
+ */
+ public void hideSoftKeyboard() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ hideSoftKeyboardDirect();
+ }
+ });
+ }
+
+ /**
+ * Hides software keyboard.
+ */
+ private void hideSoftKeyboardDirect() {
+ if (getCurrentFocus() != null) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
+ isKeyboardOpen=false;
+ }
+ }
+
+ public void showBottomToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ bottomToolbarSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ }
+ });
+ }
+
+ public void hideBottomToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ bottomToolbarSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ toolbarColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ toolbarBackColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
+ isFormattingToolbarOpen=false;
+ isSearchToolbarOpen=false;
+ isUNOCommandsToolbarOpen=false;
+ }
+ });
+ }
+
+ public void showFormattingToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (isFormattingToolbarOpen) {
+ hideFormattingToolbar();
+ } else {
+ showBottomToolbar();
+ findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.formatting_toolbar).setVisibility(View.VISIBLE);
+ findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
+ hideSoftKeyboardDirect();
+ isSearchToolbarOpen=false;
+ isFormattingToolbarOpen=true;
+ isUNOCommandsToolbarOpen=false;
+ }
+
+ }
+ });
+ }
+
+ public void hideFormattingToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ hideBottomToolbar();
+ }
+ });
+ }
+
+ public void showSearchToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (isSearchToolbarOpen) {
+ hideSearchToolbar();
+ } else {
+ showBottomToolbar();
+ findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
+ toolbarColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ toolbarBackColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ findViewById(R.id.search_toolbar).setVisibility(View.VISIBLE);
+ findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
+ hideSoftKeyboardDirect();
+ isFormattingToolbarOpen=false;
+ isSearchToolbarOpen=true;
+ isUNOCommandsToolbarOpen=false;
+ }
+ }
+ });
+ }
+
+ public void hideSearchToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ hideBottomToolbar();
+ }
+ });
+ }
+
+ public void showUNOCommandsToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if(isUNOCommandsToolbarOpen){
+ hideUNOCommandsToolbar();
+ }else{
+ showBottomToolbar();
+ findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.UNO_commands_toolbar).setVisibility(View.VISIBLE);
+ hideSoftKeyboardDirect();
+ isFormattingToolbarOpen=false;
+ isSearchToolbarOpen=false;
+ isUNOCommandsToolbarOpen=true;
+ }
+ }
+ });
+ }
+
+ public void hideUNOCommandsToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ hideBottomToolbar();
+ }
+ });
+ }
+
+ public void showProgressSpinner() {
+ findViewById(R.id.loadingPanel).setVisibility(View.VISIBLE);
+ }
+
+ public void hideProgressSpinner() {
+ findViewById(R.id.loadingPanel).setVisibility(View.GONE);
+ }
+
+ public void showAlertDialog(String message) {
+
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(LibreOfficeMainActivity.this);
+
+ alertDialogBuilder.setTitle(R.string.error);
+ alertDialogBuilder.setMessage(message);
+ alertDialogBuilder.setNeutralButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ finish();
+ }
+ });
+
+ AlertDialog alertDialog = alertDialogBuilder.create();
+ alertDialog.show();
+ }
+
+ public DocumentOverlay getDocumentOverlay() {
+ return mDocumentOverlay;
+ }
+
+ public CalcHeadersController getCalcHeadersController() {
+ return mCalcHeadersController;
+ }
+
+ public ToolbarController getToolbarController() {
+ return mToolbarController;
+ }
+
+ public FontController getFontController() {
+ return mFontController;
+ }
+
+ public FormattingController getFormattingController() {
+ return mFormattingController;
+ }
+
+ public void openDrawer() {
+ mDrawerLayout.openDrawer(mDrawerList);
+ hideBottomToolbar();
+ }
+
+ public void showAbout() {
+ AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
+ aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
+ }
+
+ public void addPart(){
+ mTileProvider.addPart();
+ mDocumentPartViewListAdapter.notifyDataSetChanged();
+ setDocumentChanged(true);
+ }
+
+ public void renamePart(){
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.enter_part_name);
+ final EditText input = new EditText(this);
+ input.setInputType(InputType.TYPE_CLASS_TEXT);
+ builder.setView(input);
+
+ builder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mTileProvider.renamePart( input.getText().toString());
+ }
+ });
+ builder.setNegativeButton(R.string.alert_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+
+ builder.show();
+ }
+
+ public void deletePart() {
+ mTileProvider.removePart();
+ }
+
+ public void showSettings() {
+ startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
+ }
+
+ public boolean isDrawerEnabled() {
+ boolean isDrawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
+ boolean isDrawerLocked = mDrawerLayout.getDrawerLockMode(mDrawerList) != DrawerLayout.LOCK_MODE_UNLOCKED;
+ return !isDrawerOpen && !isDrawerLocked;
+ }
+
+ @Override
+ public void settingsPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.matches(ENABLE_EXPERIMENTAL_PREFS_KEY)) {
+ Log.d(LOGTAG, "Editing Preference Changed");
+ mIsExperimentalMode = sharedPreferences.getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY, false);
+ }
+ }
+
+ public void promptForPassword() {
+ PasswordDialogFragment passwordDialogFragment = new PasswordDialogFragment();
+ passwordDialogFragment.setLOMainActivity(this);
+ passwordDialogFragment.show(getSupportFragmentManager(), "PasswordDialogFragment");
+ }
+
+ // this function can only be called in InvalidationHandler.java
+ public void setPassword() {
+ mTileProvider.setDocumentPassword("file://" + mTempFile.getPath(), mPassword);
+ }
+
+ // setTileProvider is meant to let main activity have a handle of LOKit when dealing with password
+ public void setTileProvider(LOKitTileProvider loKitTileProvider) {
+ mTileProvider = loKitTileProvider;
+ }
+
+ public LOKitTileProvider getTileProvider() {
+ return mTileProvider;
+ }
+
+ public void savePassword(String pwd) {
+ mPassword = pwd;
+ synchronized (mTileProvider.getMessageCallback()) {
+ mTileProvider.getMessageCallback().notifyAll();
+ }
+ }
+
+ public void setPasswordProtected(boolean b) {
+ mPasswordProtected = b;
+ }
+
+ public boolean isPasswordProtected() {
+ return mPasswordProtected;
+ }
+
+ public void initializeCalcHeaders() {
+ mCalcHeadersController = new CalcHeadersController(this, mLayerClient.getView());
+ mCalcHeadersController.setupHeaderPopupView();
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ findViewById(R.id.calc_header_top_left).setVisibility(View.VISIBLE);
+ findViewById(R.id.calc_header_row).setVisibility(View.VISIBLE);
+ findViewById(R.id.calc_header_column).setVisibility(View.VISIBLE);
+ findViewById(R.id.calc_address).setVisibility(View.VISIBLE);
+ findViewById(R.id.calc_formula).setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ public static boolean isReadOnlyMode() {
+ return mbISReadOnlyMode;
+ }
+
+ public boolean hasLocationForSave() {
+ return mDocumentUri != null;
+ }
+
+ public static void setDocumentChanged (boolean changed) {
+ isDocumentChanged = changed;
+ }
+
+ private class DocumentPartClickListener implements android.widget.AdapterView.OnItemClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ DocumentPartView partView = mDocumentPartViewListAdapter.getItem(position);
+ LOKitShell.sendChangePartEvent(partView.partIndex);
+ mDrawerLayout.closeDrawer(mDrawerList);
+ }
+ }
+
+ private static boolean copyFromAssets(AssetManager assetManager,
+ String fromAssetPath, String targetDir) {
+ try {
+ String[] files = assetManager.list(fromAssetPath);
+
+ boolean res = true;
+ for (String file : files) {
+ String[] dirOrFile = assetManager.list(fromAssetPath + "/" + file);
+ if ( dirOrFile.length == 0) {
+ // noinspection ResultOfMethodCallIgnored
+ new File(targetDir).mkdirs();
+ res &= copyAsset(assetManager,
+ fromAssetPath + "/" + file,
+ targetDir + "/" + file);
+ } else
+ res &= copyFromAssets(assetManager,
+ fromAssetPath + "/" + file,
+ targetDir + "/" + file);
+ }
+ return res;
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(LOGTAG, "copyFromAssets failed: " + e.getMessage());
+ return false;
+ }
+ }
+
+ private static boolean copyAsset(AssetManager assetManager, String fromAssetPath, String toPath) {
+ ReadableByteChannel source = null;
+ FileChannel dest = null;
+ try {
+ try {
+ source = Channels.newChannel(assetManager.open(fromAssetPath));
+ dest = new FileOutputStream(toPath).getChannel();
+ long bytesTransferred = 0;
+ // might not copy all at once, so make sure everything gets copied...
+ ByteBuffer buffer = ByteBuffer.allocate(4096);
+ while (source.read(buffer) > 0) {
+ buffer.flip();
+ bytesTransferred += dest.write(buffer);
+ buffer.clear();
+ }
+ Log.v(LOGTAG, "Success copying " + fromAssetPath + " to " + toPath + " bytes: " + bytesTransferred);
+ return true;
+ } finally {
+ if (dest != null) dest.close();
+ if (source != null) source.close();
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(LOGTAG, "file " + fromAssetPath + " not found! " + e.getMessage());
+ return false;
+ } catch (IOException e) {
+ Log.e(LOGTAG, "failed to copy file " + fromAssetPath + " from assets to " + toPath + " - " + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Copies everything from the given input stream to the given output stream
+ * and closes both streams in the end.
+ * @return Whether copy operation was successful.
+ */
+ private boolean copyStream(InputStream inputStream, OutputStream outputStream) {
+ try {
+ byte[] buffer = new byte[4096];
+ int readBytes = inputStream.read(buffer);
+ while (readBytes != -1) {
+ outputStream.write(buffer, 0, readBytes);
+ readBytes = inputStream.read(buffer);
+ }
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ inputStream.close();
+ outputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Copies everything from the given Uri to the given OutputStream
+ * and closes the OutputStream in the end.
+ * The copy operation runs in a separate thread, but the method only returns
+ * after the thread has finished its execution.
+ * This can be used to copy in a blocking way when network access is involved,
+ * which is not allowed from the main thread, but that may happen when an underlying
+ * DocumentsProvider (like the NextCloud one) does network access.
+ */
+ private boolean copyUriToStream(final Uri inputUri, final OutputStream outputStream) {
+ class CopyThread extends Thread {
+ /** Whether copy operation was successful. */
+ private boolean result = false;
+
+ @Override
+ public void run() {
+ final ContentResolver contentResolver = getContentResolver();
+ try {
+ InputStream inputStream = contentResolver.openInputStream(inputUri);
+ result = copyStream(inputStream, outputStream);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ CopyThread copyThread = new CopyThread();
+ copyThread.start();
+ try {
+ // wait for copy operation to finish
+ // NOTE: might be useful to add some indicator in UI for long copy operations involving network...
+ copyThread.join();
+ } catch(InterruptedException e) {
+ e.printStackTrace();
+ }
+ return copyThread.result;
+ }
+
+ /**
+ * Copies everything from the given InputStream to the given URI and closes the
+ * InputStream in the end.
+ * @see LibreOfficeMainActivity#copyUriToStream(Uri, OutputStream)
+ * which does the same thing the other way around.
+ */
+ private boolean copyStreamToUri(final InputStream inputStream, final Uri outputUri) {
+ class CopyThread extends Thread {
+ /** Whether copy operation was successful. */
+ private boolean result = false;
+
+ @Override
+ public void run() {
+ final ContentResolver contentResolver = getContentResolver();
+ try {
+ OutputStream outputStream = contentResolver.openOutputStream(outputUri);
+ result = copyStream(inputStream, outputStream);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ CopyThread copyThread = new CopyThread();
+ copyThread.start();
+ try {
+ // wait for copy operation to finish
+ // NOTE: might be useful to add some indicator in UI for long copy operations involving network...
+ copyThread.join();
+ } catch(InterruptedException e) {
+ e.printStackTrace();
+ }
+ return copyThread.result;
+ }
+
+ public void showCustomStatusMessage(String message){
+ Snackbar.make(mDrawerLayout, message, Snackbar.LENGTH_LONG).show();
+ }
+
+ public void preparePresentation() {
+ if (getExternalCacheDir() != null) {
+ String tempPath = getExternalCacheDir().getPath() + "/" + mTempFile.getName() + ".svg";
+ mTempSlideShowFile = new File(tempPath);
+ if (mTempSlideShowFile.exists() && !isDocumentChanged) {
+ startPresentation("file://" + tempPath);
+ } else {
+ LOKitShell.sendSaveCopyAsEvent(tempPath, "svg");
+ }
+ }
+ }
+
+ public void startPresentation(String tempPath) {
+ Intent intent = new Intent(this, PresentationActivity.class);
+ intent.setData(Uri.parse(tempPath));
+ startActivity(intent);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_CODE_SAVEAS && resultCode == RESULT_OK) {
+ final Uri fileUri = data.getData();
+ saveDocumentAs(fileUri);
+ } else if (requestCode == REQUEST_CODE_EXPORT_TO_PDF && resultCode == RESULT_OK) {
+ final Uri fileUri = data.getData();
+ exportToPDF(fileUri);
+ } else {
+ mFormattingController.handleActivityResult(requestCode, resultCode, data);
+ hideBottomToolbar();
+ }
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LocaleHelper.java b/android/source/src/java/org/libreoffice/LocaleHelper.java
new file mode 100644
index 0000000000..a87c63f099
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LocaleHelper.java
@@ -0,0 +1,57 @@
+package org.libreoffice;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Build;
+import android.preference.PreferenceManager;
+
+import java.util.Locale;
+
+public class LocaleHelper {
+
+ private static final String SELECTED_LANG = "org.libreoffice.selected.lang";
+ // value for language that indicates that system's default language should be used
+ public static final String SYSTEM_DEFAULT_LANGUAGE = "SYSTEM_DEFAULT_LANGUAGE";
+
+ public static Context onAttach(Context context){
+ String lang = getPersistedData(context, Locale.getDefault().getLanguage());
+ return setLocale(context, lang);
+ }
+
+ public static Context setLocale(Context context, String lang) {
+ persist(context, lang);
+ return updateResources(context, lang);
+ }
+
+ @SuppressWarnings("deprecation")
+ private static Context updateResources(Context context, String lang) {
+ Locale locale;
+ if (lang.equals(SYSTEM_DEFAULT_LANGUAGE)) {
+ locale = Locale.getDefault();
+ } else {
+ locale = new Locale(lang);
+ }
+ Locale.setDefault(locale);
+
+ Resources res = context.getResources();
+ Configuration cfg = res.getConfiguration();
+ cfg.locale = locale;
+ cfg.setLayoutDirection(locale);
+
+ res.updateConfiguration(cfg, res.getDisplayMetrics());
+ return context;
+ }
+
+ private static void persist(Context context, String lang) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ preferences.edit().putString(SELECTED_LANG, lang);
+ preferences.edit().apply();
+ }
+
+ private static String getPersistedData(Context context, String lang) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return preferences.getString(SELECTED_LANG, lang);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/PDFDocumentAdapter.java b/android/source/src/java/org/libreoffice/PDFDocumentAdapter.java
new file mode 100644
index 0000000000..2ce167ce3a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/PDFDocumentAdapter.java
@@ -0,0 +1,86 @@
+package org.libreoffice;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.print.PageRange;
+import android.print.PrintAttributes;
+import android.print.PrintDocumentAdapter;
+import android.print.PrintDocumentInfo;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+@TargetApi(19)
+public class PDFDocumentAdapter extends PrintDocumentAdapter{
+ Context mContext;
+ String pdfFile;
+
+ public PDFDocumentAdapter(Context mContext, String pdfFile) {
+ this.mContext = mContext;
+ this.pdfFile = pdfFile;
+ }
+
+ @Override
+ public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {
+ if (cancellationSignal.isCanceled()) {
+ callback.onLayoutCancelled();
+ }
+ else {
+ File f = new File(pdfFile);
+ PrintDocumentInfo.Builder builder=
+ new PrintDocumentInfo.Builder(f.getName());
+ builder.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
+ .setPageCount(PrintDocumentInfo.PAGE_COUNT_UNKNOWN)
+ .build();
+ callback.onLayoutFinished(builder.build(),
+ !newAttributes.equals(oldAttributes));
+ }
+ }
+
+ @Override
+ public void onWrite(PageRange[] pages, ParcelFileDescriptor destination, CancellationSignal cancellationSignal, WriteResultCallback callback) {
+ InputStream in=null;
+ OutputStream out=null;
+ try {
+ File file = new File(pdfFile);
+ in = new FileInputStream(file);
+ out=new FileOutputStream(destination.getFileDescriptor());
+
+ byte[] buf=new byte[in.available()];
+ int size;
+
+ while ((size=in.read(buf)) >= 0
+ && !cancellationSignal.isCanceled()) {
+ out.write(buf, 0, size);
+ }
+
+ if (cancellationSignal.isCanceled()) {
+ callback.onWriteCancelled();
+ }
+ else {
+ callback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES });
+ }
+ }
+ catch (Exception e) {
+ callback.onWriteFailed(e.getMessage());
+ e.printStackTrace();
+ }
+ finally {
+ try {
+ in.close();
+ out.close();
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/android/source/src/java/org/libreoffice/PasswordDialogFragment.java b/android/source/src/java/org/libreoffice/PasswordDialogFragment.java
new file mode 100644
index 0000000000..08bc7f5968
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/PasswordDialogFragment.java
@@ -0,0 +1,56 @@
+package org.libreoffice;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+public class PasswordDialogFragment extends DialogFragment {
+
+ private LibreOfficeMainActivity mContext;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+
+ final View dialogView = inflater.inflate(R.layout.password_dialog, null);
+
+ builder.setView(dialogView)
+ .setPositiveButton(R.string.action_pwd_dialog_OK, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String pwd = ((EditText)dialogView.findViewById(R.id.password)).getText().toString();
+ mContext.savePassword(pwd);
+ }
+ })
+ .setNegativeButton(R.string.action_pwd_dialog_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mContext.savePassword(null);
+ }
+ }).setTitle(R.string.action_pwd_dialog_title);
+
+ return builder.create();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ getDialog().setCanceledOnTouchOutside(false);
+ setCancelable(false);
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ public void setLOMainActivity(LibreOfficeMainActivity context) {
+ mContext = context;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/PresentationActivity.java b/android/source/src/java/org/libreoffice/PresentationActivity.java
new file mode 100644
index 0000000000..ede7c0c401
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/PresentationActivity.java
@@ -0,0 +1,177 @@
+package org.libreoffice;
+
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import androidx.core.view.GestureDetectorCompat;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.webkit.WebView;
+import android.widget.Button;
+import android.widget.ImageButton;
+
+public class PresentationActivity extends AppCompatActivity {
+
+ private static final String LOGTAG = PresentationActivity.class.getSimpleName();
+ WebView mWebView;
+ View mGestureView;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ View decorView = getWindow().getDecorView();
+ // Hide the status bar.
+ int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
+ decorView.setSystemUiVisibility(uiOptions);
+
+ setContentView(R.layout.presentation_mode);
+
+ // get intent and url
+ Intent intent = getIntent();
+ String filePath = intent.getDataString();
+
+ // set up WebView
+ mWebView = findViewById(R.id.presentation_view);
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return true;
+ }
+ });
+
+ // set up buttons within presentation_gesture_view
+ ImageButton prevButton = findViewById(R.id.slide_show_nav_prev);
+ ImageButton nextButton = findViewById(R.id.slide_show_nav_next);
+ Button backButton = findViewById(R.id.slide_show_nav_back);
+
+ prevButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ pageLeft();
+ }
+ });
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ pageRight();
+ }
+ });
+ backButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onBackPressed();
+ }
+ });
+
+ // set up presentation_gesture_view
+ mGestureView = findViewById(R.id.presentation_gesture_view);
+ final GestureDetectorCompat gestureDetector =
+ new GestureDetectorCompat(this, new PresentationGestureViewListener());
+ mGestureView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return gestureDetector.onTouchEvent(event);
+ }
+ });
+
+ // load url
+ mWebView.loadUrl(filePath);
+ }
+
+ private class PresentationGestureViewListener extends GestureDetector.SimpleOnGestureListener {
+ private static final int SWIPE_VELOCITY_THRESHOLD = 100;
+ private static final int SCROLL_THRESHOLD = 10; // if scrollCounter is larger than this, a page switch is triggered
+ private int scrollCounter = 0; // a counter for measuring scrolling distance
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ boolean result = false;
+ try {
+ float diffY = e2.getY() - e1.getY();
+ float diffX = e2.getX() - e1.getX();
+ if (Math.abs(diffX) > Math.abs(diffY)) {
+ if (Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
+ if (diffX > 0) {
+ pageRight();
+ } else {
+ pageLeft();
+ }
+ result = true;
+ }
+ }
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ }
+ return result;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ boolean result = false;
+ try {
+ float diffY = e2.getY() - e1.getY();
+ float diffX = e2.getX() - e1.getX();
+ if (Math.abs(diffX) < Math.abs(diffY)) {
+ if (distanceY > 0) {
+ scrollCounter++;
+ if (scrollCounter >= SCROLL_THRESHOLD) {
+ pageRight();
+ scrollCounter = 0;
+ }
+ } else {
+ scrollCounter--;
+ if (scrollCounter <= -SCROLL_THRESHOLD) {
+ pageLeft();
+ scrollCounter = 0;
+ }
+ }
+ result = true;
+ }
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ }
+ return result;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (e.getX() < mGestureView.getWidth()/3) {
+ pageLeft();
+ } else if (e.getX() < mGestureView.getWidth()*2/3) {
+ hideControlButtons();
+ } else {
+ pageRight();
+ }
+ return true;
+ }
+ }
+
+ private void hideControlButtons() {
+ View[] views= {findViewById(R.id.slide_show_nav_prev),findViewById(R.id.slide_show_nav_next),findViewById(R.id.slide_show_nav_back)} ;
+ for (View view : views) {
+ if (view.getVisibility() == View.GONE) {
+ view.setVisibility(View.VISIBLE);
+ } else if (view.getVisibility() == View.VISIBLE) {
+ view.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void pageLeft() {
+ mWebView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT));
+ }
+
+ private void pageRight() {
+ mWebView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT));
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/SearchController.java b/android/source/src/java/org/libreoffice/SearchController.java
new file mode 100644
index 0000000000..6095e1fd2a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/SearchController.java
@@ -0,0 +1,82 @@
+package org.libreoffice;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class SearchController implements View.OnClickListener {
+ private final LibreOfficeMainActivity mActivity;
+
+ private enum SearchDirection {
+ UP, DOWN
+ }
+
+ SearchController(LibreOfficeMainActivity activity) {
+ mActivity = activity;
+
+ activity.findViewById(R.id.button_search_up).setOnClickListener(this);
+ activity.findViewById(R.id.button_search_down).setOnClickListener(this);
+
+ ((EditText) mActivity.findViewById(R.id.search_string)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ // search downward when the "search button" on keyboard is pressed,
+ SearchDirection direction = SearchDirection.DOWN;
+ String searchText = ((EditText) mActivity.findViewById(R.id.search_string)).getText().toString();
+ float x = mActivity.getCurrentCursorPosition().centerX();
+ float y = mActivity.getCurrentCursorPosition().centerY();
+ search(searchText, direction, x, y);
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ private void search(String searchString, SearchDirection direction, float x, float y) {
+ try {
+ JSONObject rootJson = new JSONObject();
+
+ addProperty(rootJson, "SearchItem.SearchString", "string", searchString);
+ addProperty(rootJson, "SearchItem.Backward", "boolean", direction == SearchDirection.UP ? "true" : "false");
+ addProperty(rootJson, "SearchItem.SearchStartPointX", "long", String.valueOf((long) UnitConverter.pixelToTwip(x, LOKitShell.getDpi(mActivity))));
+ addProperty(rootJson, "SearchItem.SearchStartPointY", "long", String.valueOf((long) UnitConverter.pixelToTwip(y, LOKitShell.getDpi(mActivity))));
+ addProperty(rootJson, "SearchItem.Command", "long", String.valueOf(0)); // search all == 1
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:ExecuteSearch", rootJson.toString()));
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void addProperty(JSONObject json, String parentValue, String type, String value) throws JSONException {
+ JSONObject child = new JSONObject();
+ child.put("type", type);
+ child.put("value", value);
+ json.put(parentValue, child);
+ }
+
+ @Override
+ public void onClick(View view) {
+ ImageButton button = (ImageButton) view;
+
+ SearchDirection direction = SearchDirection.DOWN;
+ if (button.getId() == R.id.button_search_up) {
+ direction = SearchDirection.UP;
+ }
+
+ String searchText = ((EditText) mActivity.findViewById(R.id.search_string)).getText().toString();
+
+ float x = mActivity.getCurrentCursorPosition().centerX();
+ float y = mActivity.getCurrentCursorPosition().centerY();
+ search(searchText, direction, x, y);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/SettingsActivity.java b/android/source/src/java/org/libreoffice/SettingsActivity.java
new file mode 100644
index 0000000000..5623abc2e5
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/SettingsActivity.java
@@ -0,0 +1,63 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceGroup;
+
+public class SettingsActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Display the fragment as the main content.
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new SettingsFragment())
+ .commit();
+ }
+
+ public static class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.libreoffice_preferences);
+ if(!BuildConfig.ALLOW_EDITING) {
+ PreferenceGroup generalGroup = (PreferenceGroup) findPreference("PREF_CATEGORY_GENERAL");
+ generalGroup.removePreference(generalGroup.findPreference("ENABLE_EXPERIMENTAL"));
+ generalGroup.removePreference(generalGroup.findPreference("ENABLE_DEVELOPER"));
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getPreferenceScreen().getSharedPreferences()
+ .registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ getPreferenceScreen().getSharedPreferences()
+ .unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ SettingsListenerModel.getInstance().changePreferenceState(sharedPreferences, key);
+ if(key.equals("DISPLAY_LANGUAGE")){
+ getActivity().recreate();
+ }
+ }
+ }
+}
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/SettingsListenerModel.java b/android/source/src/java/org/libreoffice/SettingsListenerModel.java
new file mode 100644
index 0000000000..1b5a909e1e
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/SettingsListenerModel.java
@@ -0,0 +1,56 @@
+/*
+ *
+ * * This file is part of the LibreOffice project.
+ * * 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 org.libreoffice;
+
+import android.content.SharedPreferences;
+
+public class SettingsListenerModel {
+
+ public interface OnSettingsPreferenceChangedListener {
+ void settingsPreferenceChanged(SharedPreferences sharedPreferences, String key);
+ }
+
+ private static SettingsListenerModel mInstance;
+ private OnSettingsPreferenceChangedListener mListener;
+ private SharedPreferences sharedPreferences;
+ private String key;
+
+ private SettingsListenerModel() {}
+
+ public static SettingsListenerModel getInstance() {
+ if(mInstance == null) {
+ mInstance = new SettingsListenerModel();
+ }
+ return mInstance;
+ }
+
+ public void setListener(OnSettingsPreferenceChangedListener listener) {
+ mListener = listener;
+ }
+
+ public void changePreferenceState(SharedPreferences sharedPreferences, String key) {
+ if(mListener != null) {
+ this.sharedPreferences = sharedPreferences;
+ this.key = key;
+ notifyPreferenceChange(sharedPreferences, key);
+ }
+ }
+
+ public SharedPreferences getSharedPreferences() {
+ return sharedPreferences;
+ }
+
+ public String getKey(){
+ return key;
+ }
+
+ private void notifyPreferenceChange(SharedPreferences preferences, String key) {
+ mListener.settingsPreferenceChanged(preferences, key);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/ThumbnailCreator.java b/android/source/src/java/org/libreoffice/ThumbnailCreator.java
new file mode 100644
index 0000000000..c0c097747c
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ThumbnailCreator.java
@@ -0,0 +1,121 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.widget.ImageView;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Create thumbnails for the parts of the document.
+ */
+public class ThumbnailCreator {
+ private static final String LOG_TAG = ThumbnailCreator.class.getSimpleName();
+ private static final int THUMBNAIL_SIZE = 256;
+
+ private static boolean needsThumbnailCreation(int partNumber, ImageView imageView) {
+ ThumbnailCreationTask thumbnailCreationTask = currentThumbnailCreationTask(imageView);
+
+ if (thumbnailCreationTask == null) {
+ return true;
+ }
+
+ if (thumbnailCreationTask.partNumber != partNumber) {
+ thumbnailCreationTask.cancel();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private static ThumbnailCreationTask currentThumbnailCreationTask(ImageView imageView) {
+ if (imageView == null) {
+ return null;
+ }
+ Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof ThumbnailDrawable) {
+ return ((ThumbnailDrawable) drawable).thumbnailCreationTask.get();
+ } else {
+ return null;
+ }
+ }
+
+ public void createThumbnail(int partNumber, ImageView imageView) {
+ if (needsThumbnailCreation(partNumber, imageView)) {
+ ThumbnailCreationTask task = new ThumbnailCreationTask(imageView, partNumber);
+ ThumbnailDrawable thumbnailDrawable = new ThumbnailDrawable(task);
+ imageView.setImageDrawable(thumbnailDrawable);
+ imageView.setMinimumHeight(THUMBNAIL_SIZE);
+ LOKitShell.sendThumbnailEvent(task);
+ }
+ }
+
+ static class ThumbnailDrawable extends ColorDrawable {
+ public final WeakReference<ThumbnailCreationTask> thumbnailCreationTask;
+
+ public ThumbnailDrawable(ThumbnailCreationTask thumbnailCreationTask) {
+ super(Color.WHITE);
+ this.thumbnailCreationTask = new WeakReference<ThumbnailCreationTask>(thumbnailCreationTask);
+ }
+ }
+
+ class ThumbnailCreationTask{
+ private final WeakReference<ImageView> imageViewReference;
+ private final int partNumber;
+ private boolean cancelled = false;
+
+ public ThumbnailCreationTask(ImageView imageView, int partNumber) {
+ imageViewReference = new WeakReference<ImageView>(imageView);
+ this.partNumber = partNumber;
+ }
+
+ public void cancel() {
+ cancelled = true;
+ }
+
+ public Bitmap getThumbnail(TileProvider tileProvider) {
+ int currentPart = tileProvider.getCurrentPartNumber();
+ tileProvider.changePart(partNumber);
+ final Bitmap bitmap = tileProvider.thumbnail(THUMBNAIL_SIZE);
+ tileProvider.changePart(currentPart);
+ return bitmap;
+ }
+
+ private void changeBitmap(Bitmap bitmap) {
+ if (cancelled) {
+ bitmap = null;
+ }
+
+ if (imageViewReference == null) {
+ return;
+ }
+ ImageView imageView = imageViewReference.get();
+ ThumbnailCreationTask thumbnailCreationTask = currentThumbnailCreationTask(imageView);
+ if (this == thumbnailCreationTask) {
+ imageView.setImageBitmap(bitmap);
+ }
+ }
+
+ public void applyBitmap(final Bitmap bitmap) {
+ // run on UI thread
+ LibreOfficeApplication.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ changeBitmap(bitmap);
+ }
+ });
+ }
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/TileIdentifier.java b/android/source/src/java/org/libreoffice/TileIdentifier.java
new file mode 100644
index 0000000000..9f6fc5605a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/TileIdentifier.java
@@ -0,0 +1,92 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import org.mozilla.gecko.gfx.IntSize;
+
+/**
+ * Identifies the tile by its position (x and y coordinate on the document), zoom and tile size (currently static)
+ */
+public class TileIdentifier {
+ public final int x;
+ public final int y;
+ public final float zoom;
+ public final IntSize size;
+
+ public TileIdentifier(int x, int y, float zoom, IntSize size) {
+ this.x = x;
+ this.y = y;
+ this.zoom = zoom;
+ this.size = size;
+ }
+
+ /**
+ * Returns a rectangle of the tiles position in scaled coordinates.
+ */
+ public RectF getRectF() {
+ return new RectF(x, y, x + size.width, y + size.height);
+ }
+
+ /**
+ * Returns a rectangle of the tiles position in non-scaled coordinates (coordinates as the zoom would be 1).
+ */
+ public RectF getCSSRectF() {
+ float cssX = x / zoom;
+ float cssY = y / zoom;
+ float cssSizeW = size.width / zoom;
+ float cssSizeH = size.height / zoom;
+ return new RectF(cssX, cssY, cssX + cssSizeW, cssY + cssSizeH);
+ }
+
+ /**
+ * Returns an integer rectangle of the tiles position in non-scaled and rounded coordinates (coordinates as the zoom would be 1).
+ */
+ public Rect getCSSRect() {
+ float cssX = x / zoom;
+ float cssY = y / zoom;
+ float sizeW = size.width / zoom;
+ float sizeH = size.height / zoom;
+ return new Rect(
+ (int) cssX, (int) cssY,
+ (int) (cssX + sizeW),
+ (int) (cssY + sizeH) );
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TileIdentifier that = (TileIdentifier) o;
+
+ if (x != that.x) return false;
+ if (y != that.y) return false;
+ if (Float.compare(that.zoom, zoom) != 0) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = x;
+ result = 31 * result + y;
+ result = 31 * result + (zoom != +0.0f ? Float.floatToIntBits(zoom) : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TileIdentifier (%d, %d) z=%f s=(%d, %d)", x, y, zoom, size.width, size.height);
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/TileProvider.java b/android/source/src/java/org/libreoffice/TileProvider.java
new file mode 100644
index 0000000000..c979a9883c
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/TileProvider.java
@@ -0,0 +1,205 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.view.KeyEvent;
+
+import org.mozilla.gecko.gfx.CairoImage;
+import org.mozilla.gecko.gfx.IntSize;
+
+/**
+ * Provides the tiles and other document information.
+ */
+public interface TileProvider {
+
+ /**
+ * Save the current document under the given path.
+ * @param takeOwnership Whether to take ownership of the new file,
+ * i.e. whether the current document is changed to the
+ * newly saved document (takeOwnership = true),
+ * as compared to just saving a copy of the current document
+ * or exporting to a different file format.
+ * Must be 'false' when using this method for export to e.g. PNG or PDF.
+ * @return Whether saving was successful.
+ */
+ boolean saveDocumentAs(String filePath, String format, boolean takeOwnership);
+
+ /**
+ * Saves the current document under the given path,
+ * using the default file format.
+ * @param takeOwnership (s. documentation for
+ * 'saveDocumentAs(String filePath, String format, boolean takeOwnership)')
+ */
+ boolean saveDocumentAs(String filePath, boolean takeOwnership);
+
+ /**
+ * Returns the page width in pixels.
+ */
+ int getPageWidth();
+
+ /**
+ * Returns the page height in pixels.
+ */
+ int getPageHeight();
+
+ boolean isReady();
+
+ CairoImage createTile(float x, float y, IntSize tileSize, float zoom);
+
+ /**
+ * Rerender and overwrite tile's image buffer directly
+ */
+ void rerenderTile(CairoImage image, float x, float y, IntSize tileSize, float zoom);
+
+ /**
+ * Change the document part to the one specified by the partIndex input parameter.
+ *
+ * @param partIndex - part index to change to
+ */
+ void changePart(int partIndex);
+
+ /**
+ * Get the current document part number.
+ *
+ * @return
+ */
+ int getCurrentPartNumber();
+
+ /**
+ * Get the total number of parts.
+ */
+ int getPartsCount();
+
+ Bitmap thumbnail(int size);
+
+ /**
+ * Closes the document.
+ */
+ void close();
+
+ /**
+ * Returns true if the current open document is a drawing.
+ */
+ boolean isDrawing();
+
+ /**
+ * Returns true if the current open document is a text document.
+ */
+ boolean isTextDocument();
+
+ /**
+ * Returns true if the current open document is a spreadsheet.
+ */
+ boolean isSpreadsheet();
+
+ /**
+ * Returns true if the current open document is a presentation
+ */
+ boolean isPresentation();
+
+ /**
+ * Trigger a key event.
+ *
+ * @param keyEvent - contains information about key event
+ */
+ void sendKeyEvent(KeyEvent keyEvent);
+
+ /**
+ * Trigger a mouse button down event.
+ *
+ * @param documentCoordinate - coordinate relative to the document where the mouse button should be triggered
+ * @param numberOfClicks - number of clicks (1 - single click, 2 - double click)
+ */
+ void mouseButtonDown(PointF documentCoordinate, int numberOfClicks, float zoomFactor);
+
+
+ /**
+ * Trigger a swipe left event.
+ */
+ void onSwipeLeft();
+
+ /**
+ * Trigger a swipe left event.
+ */
+ void onSwipeRight();
+
+ /**
+ * Trigger a mouse button up event.
+ *
+ * @param documentCoordinate - coordinate relative to the document where the mouse button should be triggered
+ * @param numberOfClicks - number of clicks (1 - single click, 2 - double click)
+ */
+ void mouseButtonUp(PointF documentCoordinate, int numberOfClicks, float zoomFactor);
+
+ /**
+ * Post a UNO command to LOK.
+ *
+ * @param command - the .uno: command, like ".uno:Bold"
+ */
+ void postUnoCommand(String command, String arguments);
+
+ /**
+ * This is the actual reference to the function in LOK, used for getting notified when uno:save event finishes
+ * @param command
+ * @param arguments
+ * @param notifyWhenFinished
+ */
+ void postUnoCommand(String command, String arguments, boolean notifyWhenFinished);
+
+ /**
+ * Send text selection start coordinate.
+ * @param documentCoordinate
+ */
+ void setTextSelectionStart(PointF documentCoordinate);
+
+ /**
+ * Send text selection end coordinate.
+ * @param documentCoordinate
+ */
+ void setTextSelectionEnd(PointF documentCoordinate);
+
+ /**
+ * get selected text
+ * @param mimeType
+ */
+ String getTextSelection(String mimeType);
+
+ /**
+ * copy
+ * @param mimeType
+ * @param data
+ * @return
+ */
+ boolean paste(String mimeType, String data);
+ /**
+ * Send text selection reset coordinate.
+ * @param documentCoordinate
+ */
+ void setTextSelectionReset(PointF documentCoordinate);
+
+ /**
+ * Send a request to change start the change of graphic selection.
+ */
+ void setGraphicSelectionStart(PointF documentCoordinate);
+
+ /**
+ * Send a request to change end the change of graphic selection...
+ */
+ void setGraphicSelectionEnd(PointF documentCoordinate);
+
+ /**
+ * Set the new page size of the document when changed
+ */
+ void setDocumentSize(int pageWidth, int pageHeight);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/TileProviderFactory.java b/android/source/src/java/org/libreoffice/TileProviderFactory.java
new file mode 100644
index 0000000000..3219ce2b4a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/TileProviderFactory.java
@@ -0,0 +1,31 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+
+import org.libreoffice.kit.LibreOfficeKit;
+
+/**
+ * Create a desired instance of TileProvider.
+ */
+public class TileProviderFactory {
+
+ private TileProviderFactory() {
+ }
+
+ public static void initialize() {
+ LibreOfficeKit.initializeLibrary();
+ }
+
+ public static TileProvider create(LibreOfficeMainActivity context, InvalidationHandler invalidationHandler, String filename) {
+ return new LOKitTileProvider(context, invalidationHandler, filename);
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ToolbarController.java b/android/source/src/java/org/libreoffice/ToolbarController.java
new file mode 100644
index 0000000000..603d225816
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ToolbarController.java
@@ -0,0 +1,276 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import androidx.appcompat.widget.Toolbar;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+/**
+ * Controls the changes to the toolbar.
+ */
+public class ToolbarController implements Toolbar.OnMenuItemClickListener {
+ private static final String LOGTAG = ToolbarController.class.getSimpleName();
+ private final Toolbar mToolbarTop;
+
+ private final LibreOfficeMainActivity mContext;
+ private final Menu mMainMenu;
+
+ private boolean isEditModeOn = false;
+ private String clipboardText = null;
+ ClipboardManager clipboardManager;
+ ClipData clipData;
+
+ public ToolbarController(LibreOfficeMainActivity context, Toolbar toolbarTop) {
+ mToolbarTop = toolbarTop;
+ mContext = context;
+
+ mToolbarTop.inflateMenu(R.menu.main);
+ mToolbarTop.setOnMenuItemClickListener(this);
+ switchToViewMode();
+
+ mMainMenu = mToolbarTop.getMenu();
+ clipboardManager = (ClipboardManager)mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ }
+
+ private void enableMenuItem(final int menuItemId, final boolean enabled) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ MenuItem menuItem = mMainMenu.findItem(menuItemId);
+ if (menuItem != null) {
+ menuItem.setEnabled(enabled);
+ } else {
+ Log.e(LOGTAG, "MenuItem not found.");
+ }
+ }
+ });
+ }
+
+ public void setEditModeOn(boolean enabled) {
+ isEditModeOn = enabled;
+ }
+
+ public boolean getEditModeStatus() {
+ return isEditModeOn;
+ }
+
+ /**
+ * Change the toolbar to edit mode.
+ */
+ void switchToEditMode() {
+ if (!LOKitShell.isEditingEnabled())
+ return;
+
+ setEditModeOn(true);
+ // Ensure the change is done on UI thread
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.setGroupVisible(R.id.group_edit_actions, true);
+ if (!LibreOfficeMainActivity.isDeveloperMode() && mMainMenu.findItem(R.id.action_UNO_commands) != null) {
+ mMainMenu.findItem(R.id.action_UNO_commands).setVisible(false);
+ } else {
+ mMainMenu.findItem(R.id.action_UNO_commands).setVisible(true);
+ }
+ if(mContext.getTileProvider() != null && mContext.getTileProvider().isSpreadsheet()){
+ mMainMenu.setGroupVisible(R.id.group_spreadsheet_options, true);
+ } else if(mContext.getTileProvider() != null && mContext.getTileProvider().isPresentation()){
+ mMainMenu.setGroupVisible(R.id.group_presentation_options, true);
+ }
+ mToolbarTop.setNavigationIcon(R.drawable.ic_check);
+ mToolbarTop.setLogo(null);
+ }
+ });
+ }
+
+ /**
+ * Show clipboard Actions on the toolbar
+ * */
+ void showClipboardActions(final String value){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if(value != null){
+ mMainMenu.setGroupVisible(R.id.group_edit_actions, false);
+ mMainMenu.setGroupVisible(R.id.group_edit_clipboard, true);
+ if(getEditModeStatus()){
+ showHideClipboardCutAndCopy(true);
+ } else {
+ mMainMenu.findItem(R.id.action_cut).setVisible(false);
+ mMainMenu.findItem(R.id.action_paste).setVisible(false);
+ }
+ clipboardText = value;
+ }
+ }
+ });
+ }
+
+ void hideClipboardActions(){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.setGroupVisible(R.id.group_edit_actions, getEditModeStatus());
+ mMainMenu.setGroupVisible(R.id.group_edit_clipboard, false);
+ }
+ });
+ }
+
+ void showHideClipboardCutAndCopy(final boolean option){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.findItem(R.id.action_copy).setVisible(option);
+ mMainMenu.findItem(R.id.action_cut).setVisible(option);
+ }
+ });
+ }
+
+ /**
+ * Change the toolbar to view mode.
+ */
+ void switchToViewMode() {
+ // Ensure the change is done on UI thread
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.setGroupVisible(R.id.group_edit_actions, false);
+ mToolbarTop.setNavigationIcon(R.mipmap.ic_launcher);
+ mToolbarTop.setLogo(null);
+ setEditModeOn(false);
+ mContext.hideBottomToolbar();
+ mContext.hideSoftKeyboard();
+ if(mContext.getTileProvider() != null && mContext.getTileProvider().isSpreadsheet()){
+ mMainMenu.setGroupVisible(R.id.group_spreadsheet_options, false);
+ } else if(mContext.getTileProvider() != null && mContext.getTileProvider().isPresentation()){
+ mMainMenu.setGroupVisible(R.id.group_presentation_options, false);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.action_keyboard) {
+ mContext.showSoftKeyboard();
+ } else if (itemId == R.id.action_format) {
+ mContext.showFormattingToolbar();
+ } else if (itemId == R.id.action_about) {
+ mContext.showAbout();
+ return true;
+ } else if (itemId == R.id.action_save) {
+ mContext.getTileProvider().saveDocument();
+ return true;
+ } else if (itemId == R.id.action_save_as) {
+ mContext.saveDocumentAs();
+ return true;
+ } else if (itemId == R.id.action_parts) {
+ mContext.openDrawer();
+ return true;
+ } else if (itemId == R.id.action_exportToPDF) {
+ mContext.exportToPDF();
+ return true;
+ } else if (itemId == R.id.action_print) {
+ mContext.getTileProvider().printDocument();
+ return true;
+ } else if (itemId == R.id.action_settings) {
+ mContext.showSettings();
+ return true;
+ } else if (itemId == R.id.action_search) {
+ mContext.showSearchToolbar();
+ return true;
+ } else if (itemId == R.id.action_undo) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Undo"));
+ return true;
+ } else if (itemId == R.id.action_redo) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Redo"));
+ return true;
+ } else if (itemId == R.id.action_presentation) {
+ mContext.preparePresentation();
+ return true;
+ } else if (itemId == R.id.action_add_slide || itemId == R.id.action_add_worksheet) {
+ mContext.addPart();
+ return true;
+ } else if (itemId == R.id.action_rename_worksheet || itemId == R.id.action_rename_slide) {
+ mContext.renamePart();
+ return true;
+ } else if (itemId == R.id.action_delete_worksheet || itemId == R.id.action_delete_slide) {
+ mContext.deletePart();
+ return true;
+ } else if (itemId == R.id.action_back) {
+ hideClipboardActions();
+ return true;
+ } else if (itemId == R.id.action_copy) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Copy"));
+ clipData = ClipData.newPlainText("clipboard data", clipboardText);
+ clipboardManager.setPrimaryClip(clipData);
+ Toast.makeText(mContext, mContext.getResources().getString(R.string.action_text_copied), Toast.LENGTH_SHORT).show();
+ return true;
+ } else if (itemId == R.id.action_paste) {
+ clipData = clipboardManager.getPrimaryClip();
+ ClipData.Item clipItem = clipData.getItemAt(0);
+ mContext.setDocumentChanged(true);
+ return mContext.getTileProvider().paste("text/plain;charset=utf-16", clipItem.getText().toString());
+ } else if (itemId == R.id.action_cut) {
+ clipData = ClipData.newPlainText("clipboard data", clipboardText);
+ clipboardManager.setPrimaryClip(clipData);
+ LOKitShell.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
+ mContext.setDocumentChanged(true);
+ return true;
+ } else if (itemId == R.id.action_UNO_commands) {
+ mContext.showUNOCommandsToolbar();
+ return true;
+ }
+ return false;
+ }
+
+ void setupToolbars() {
+ if (LibreOfficeMainActivity.isExperimentalMode()) {
+ boolean enableSaveEntry = !LibreOfficeMainActivity.isReadOnlyMode() && mContext.hasLocationForSave();
+ enableMenuItem(R.id.action_save, enableSaveEntry);
+ if (LibreOfficeMainActivity.isReadOnlyMode()) {
+ // show message in case experimental mode is enabled (i.e. editing is supported in general),
+ // but current document is readonly
+ Toast.makeText(mContext, mContext.getString(R.string.readonly_file), Toast.LENGTH_LONG).show();
+ }
+ } else {
+ hideItem(R.id.action_save);
+ }
+ mMainMenu.findItem(R.id.action_parts).setVisible(mContext.isDrawerEnabled());
+ }
+
+ public void showItem(final int item){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.findItem(item).setVisible(true);
+
+ }
+ });
+ }
+
+ public void hideItem(final int item){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.findItem(item).setVisible(false);
+
+ }
+ });
+ }
+
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/UNOCommandsController.java b/android/source/src/java/org/libreoffice/UNOCommandsController.java
new file mode 100644
index 0000000000..cba67732cc
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/UNOCommandsController.java
@@ -0,0 +1,85 @@
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice;
+
+import android.content.DialogInterface;
+import androidx.appcompat.app.AlertDialog;
+import android.text.method.ScrollingMovementMethod;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Scroller;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import static org.libreoffice.SearchController.addProperty;
+
+class UNOCommandsController implements View.OnClickListener {
+ private final LibreOfficeMainActivity mActivity;
+ private JSONObject mRootJSON = new JSONObject();
+
+
+ UNOCommandsController(LibreOfficeMainActivity activity) {
+ mActivity = activity;
+
+ activity.findViewById(R.id.button_send_UNO_commands).setOnClickListener(this);
+ activity.findViewById(R.id.button_send_UNO_commands_clear).setOnClickListener(this);
+ activity.findViewById(R.id.button_send_UNO_commands_show).setOnClickListener(this);
+ activity.findViewById(R.id.button_add_property).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view.getId() == R.id.button_send_UNO_commands) {
+ String cmdText = ((EditText) mActivity.findViewById(R.id.UNO_commands_string)).getText().toString();
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:" + cmdText, mRootJSON.toString()));
+ } else if (view.getId() == R.id.button_add_property) {
+ String parentValue = ((EditText) mActivity.findViewById(R.id.UNO_commands_string_parent_value)).getText().toString();
+ String type = ((EditText) mActivity.findViewById(R.id.UNO_commands_string_type)).getText().toString();
+ String value = ((EditText) mActivity.findViewById(R.id.UNO_commands_string_value)).getText().toString();
+ try {
+ addProperty(mRootJSON, parentValue, type, value);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ showCommandDialog();
+ } else if (view.getId() == R.id.button_send_UNO_commands_clear) {
+ mRootJSON = new JSONObject();
+ ((EditText) mActivity.findViewById(R.id.UNO_commands_string_parent_value)).setText("");
+ ((EditText) mActivity.findViewById(R.id.UNO_commands_string_type)).setText("");
+ ((EditText) mActivity.findViewById(R.id.UNO_commands_string_value)).setText("");
+ showCommandDialog();
+ } else if (view.getId() == R.id.button_send_UNO_commands_show) {
+ showCommandDialog();
+ }
+ }
+
+ private void showCommandDialog() {
+ try {
+ AlertDialog dialog = new AlertDialog.Builder(mActivity)
+ .setTitle(R.string.current_uno_command)
+ .setMessage(mRootJSON.toString(2))
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .show();
+ TextView textView = dialog.findViewById(android.R.id.message);
+ if (textView != null) {
+ textView.setScroller(new Scroller(mActivity));
+ textView.setVerticalScrollBarEnabled(true);
+ textView.setMovementMethod(new ScrollingMovementMethod());
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/UnitConverter.java b/android/source/src/java/org/libreoffice/UnitConverter.java
new file mode 100644
index 0000000000..f668021b0c
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/UnitConverter.java
@@ -0,0 +1,16 @@
+package org.libreoffice;
+
+
+public class UnitConverter {
+ public static float twipToPixel(float input, float dpi) {
+ return input / 1440.0f * dpi;
+ }
+
+ public static float pixelToTwip(float input, float dpi) {
+ return (input / dpi) * 1440.0f;
+ }
+
+ public static float twipsToHMM(float twips) {
+ return (twips * 127 + 36) / 72;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/AdjustLengthLine.java b/android/source/src/java/org/libreoffice/canvas/AdjustLengthLine.java
new file mode 100644
index 0000000000..a6f8cb17c1
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/AdjustLengthLine.java
@@ -0,0 +1,103 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.LOEvent;
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.overlay.CalcHeadersView;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+
+import static org.libreoffice.SearchController.addProperty;
+import static org.libreoffice.UnitConverter.pixelToTwip;
+import static org.libreoffice.UnitConverter.twipsToHMM;
+
+public class AdjustLengthLine extends CommonCanvasElement {
+
+ private static final float STROKE_WIDTH = 4f;
+ private static final float TOUCH_VICINITY_RADIUS = 24f;
+
+ private LibreOfficeMainActivity mContext;
+ private CalcHeadersView mCalcHeadersView;
+ private boolean mIsRow;
+ private PointF mScreenPosition;
+ private float mWidth;
+ private float mHeight;
+ private Paint mPaint;
+ private PointF mStartScreenPosition;
+ private int mIndex;
+
+ public AdjustLengthLine(LibreOfficeMainActivity context, CalcHeadersView view, boolean isRow, float width, float height) {
+ super();
+ mContext = context;
+ mCalcHeadersView = view;
+ mIsRow = isRow;
+ mWidth = width;
+ mHeight = height;
+ mPaint = new Paint();
+ mPaint.setColor(Color.BLACK);
+ mPaint.setStrokeWidth(STROKE_WIDTH);
+ }
+
+ @Override
+ public boolean onHitTest(float x, float y) {
+ if (mIsRow) {
+ return mScreenPosition.y - TOUCH_VICINITY_RADIUS < y &&
+ y < mScreenPosition.y + TOUCH_VICINITY_RADIUS;
+ } else {
+ return mScreenPosition.x - TOUCH_VICINITY_RADIUS < x &&
+ x < mScreenPosition.x + TOUCH_VICINITY_RADIUS;
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (mIsRow) {
+ canvas.drawLine(0f, mScreenPosition.y, mWidth, mScreenPosition.y, mPaint);
+ } else {
+ canvas.drawLine(mScreenPosition.x, 0f, mScreenPosition.x, mHeight, mPaint);
+ }
+ }
+
+ public void dragStart(PointF point) {
+ }
+
+ public void dragging(PointF point) {
+ mScreenPosition = point;
+ }
+
+ public void dragEnd(PointF point) {
+ ImmutableViewportMetrics viewportMetrics = mContext.getLayerClient().getViewportMetrics();
+ float zoom = viewportMetrics.zoomFactor;
+
+ PointF documentDistance = new PointF(pixelToTwip((point.x-mStartScreenPosition.x)/zoom, LOKitShell.getDpi(mContext)),
+ pixelToTwip((point.y-mStartScreenPosition.y)/zoom, LOKitShell.getDpi(mContext)));
+
+ try {
+ JSONObject rootJson = new JSONObject();
+ if (mIsRow) {
+ addProperty(rootJson, "Row", "long", String.valueOf(mIndex));
+ addProperty(rootJson, "RowHeight", "unsigned short", String.valueOf(Math.round(documentDistance.y > 0 ? twipsToHMM(documentDistance.y) : 0)));
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:RowHeight", rootJson.toString()));
+ } else {
+ addProperty(rootJson, "Column", "long", String.valueOf(mIndex));
+ addProperty(rootJson, "ColumnWidth", "unsigned short", String.valueOf(documentDistance.x > 0 ? twipsToHMM(documentDistance.x) : 0));
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:ColumnWidth", rootJson.toString()));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void setScreenRect(RectF position) {
+ mScreenPosition = new PointF(position.right, position.bottom);
+ mStartScreenPosition = new PointF(position.left, position.top);
+ mIndex = 1 + mCalcHeadersView.getIndexFromPointOfTouch(new PointF(position.centerX(), position.centerY()));
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/BitmapHandle.java b/android/source/src/java/org/libreoffice/canvas/BitmapHandle.java
new file mode 100644
index 0000000000..51f6f7cf86
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/BitmapHandle.java
@@ -0,0 +1,63 @@
+package org.libreoffice.canvas;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import androidx.core.content.ContextCompat;
+
+/**
+ * Bitmap handle canvas element is used to show a handle on the screen.
+ * The handle visual comes from the bitmap, which must be provided in time
+ * of construction.
+ */
+public abstract class BitmapHandle extends CommonCanvasElement {
+ public final RectF mDocumentPosition;
+ private final Bitmap mBitmap;
+ final RectF mScreenPosition;
+
+ BitmapHandle(Bitmap bitmap) {
+ mBitmap = bitmap;
+ mScreenPosition = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+ mDocumentPosition = new RectF();
+ }
+
+ /**
+ * Return a bitmap for a drawable id.
+ */
+ static Bitmap getBitmapForDrawable(Context context, int drawableId) {
+ Drawable drawable = ContextCompat.getDrawable(context, drawableId);
+
+ return ImageUtils.getBitmapForDrawable(drawable);
+ }
+
+ /**
+ * Draw the bitmap handle to the canvas.
+ * @param canvas - the canvas
+ */
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawBitmap(mBitmap, mScreenPosition.left, mScreenPosition.top, null);
+ }
+
+ /**
+ * Test if the bitmap has been hit.
+ * @param x - x coordinate
+ * @param y - y coordinate
+ * @return true if the bitmap has been hit
+ */
+ @Override
+ public boolean onHitTest(float x, float y) {
+ return mScreenPosition.contains(x, y);
+ }
+
+ /**
+ * Change the position of the handle.
+ * @param x - x coordinate
+ * @param y - y coordinate
+ */
+ public void reposition(float x, float y) {
+ mScreenPosition.offsetTo(x, y);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/CalcHeaderCell.java b/android/source/src/java/org/libreoffice/canvas/CalcHeaderCell.java
new file mode 100644
index 0000000000..a285234bc8
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/CalcHeaderCell.java
@@ -0,0 +1,66 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.text.TextPaint;
+
+public class CalcHeaderCell extends CommonCanvasElement {
+ private final TextPaint mTextPaint = new TextPaint();
+
+ private final Paint mFramePaint = new Paint();
+ private final Paint mBgPaint = new Paint();
+ private final RectF mBounds;
+ private final Rect mTextBounds = new Rect();
+ private final String mText;
+
+ public CalcHeaderCell(float left, float top, float width, float height, String text, boolean selected) {
+ mBounds = new RectF(left, top, left + width, top + height);
+
+ mFramePaint.setStyle(Style.STROKE);
+ mFramePaint.setColor(Color.BLACK);
+
+ mBgPaint.setStyle(Style.FILL);
+ mBgPaint.setColor(Color.GRAY);
+ // draw background more intensely when cell is selected
+ if (selected) {
+ mBgPaint.setAlpha(100);
+ } else {
+ mBgPaint.setAlpha(25);
+ }
+
+ mTextPaint.setColor(Color.BLACK);
+ mTextPaint.setTextSize(24f); // hard coded for now
+ mTextPaint.setTextAlign(Paint.Align.CENTER);
+ mText = text;
+
+ mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
+ }
+
+ /**
+ * Implement hit test here
+ *
+ * @param x - x coordinate of the
+ * @param y - y coordinate of the
+ */
+ @Override
+ public boolean onHitTest(float x, float y) {
+ return false;
+ }
+
+ /**
+ * Called inside draw if the element is visible. Override this method to
+ * draw the element on the canvas.
+ *
+ * @param canvas - the canvas
+ */
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawRect(mBounds, mBgPaint);
+ canvas.drawRect(mBounds, mFramePaint);
+ canvas.drawText(mText, mBounds.centerX(), mBounds.centerY() - mTextBounds.centerY(), mTextPaint);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/CalcSelectionBox.java b/android/source/src/java/org/libreoffice/canvas/CalcSelectionBox.java
new file mode 100644
index 0000000000..af31d708d4
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/CalcSelectionBox.java
@@ -0,0 +1,111 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+
+/**
+ * CalcSelectionBox is the selection frame for the current highlighted area/cells
+ * in Calc.
+ */
+
+public class CalcSelectionBox extends CommonCanvasElement {
+ private static final long MINIMUM_HANDLE_UPDATE_TIME = 50 * 1000000;
+ private static final float CIRCLE_HANDLE_RADIUS = 8f;
+
+ public RectF mDocumentPosition;
+
+ private LibreOfficeMainActivity mContext;
+ private RectF mScreenPosition;
+ private long mLastTime = 0;
+ private Paint mPaint;
+ private Paint mCirclePaint;
+
+ public CalcSelectionBox(LibreOfficeMainActivity context) {
+ mContext = context;
+ mScreenPosition = new RectF();
+ mDocumentPosition = new RectF();
+ mPaint = new Paint();
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setColor(Color.BLACK);
+ mPaint.setStrokeWidth(2f);
+ mCirclePaint = new Paint();
+ mCirclePaint.setColor(Color.BLACK);
+ mCirclePaint.setStyle(Paint.Style.FILL);
+ }
+
+ /**
+ * Start of a touch and drag action on the box.
+ */
+ public void dragStart(PointF point) {}
+
+ /**
+ * End of a touch and drag action on the box.
+ */
+ public void dragEnd(PointF point) {}
+
+ /**
+ * Box has been dragged.
+ */
+ public void dragging(PointF point) {
+ long currentTime = System.nanoTime();
+ if (currentTime - mLastTime > MINIMUM_HANDLE_UPDATE_TIME) {
+ mLastTime = currentTime;
+ signalHandleMove(point.x, point.y);
+ }
+ }
+
+ /**
+ * Signal to move the handle to a new position to LO.
+ */
+ private void signalHandleMove(float newX, float newY) {
+ ImmutableViewportMetrics viewportMetrics = mContext.getLayerClient().getViewportMetrics();
+ float zoom = viewportMetrics.zoomFactor;
+ PointF origin = viewportMetrics.getOrigin();
+
+ PointF documentPoint = new PointF((newX+origin.x)/zoom , (newY+origin.y)/zoom);
+
+ if (documentPoint.x < mDocumentPosition.left || documentPoint.y < mDocumentPosition.top) {
+ LOKitShell.sendChangeHandlePositionEvent(SelectionHandle.HandleType.START, documentPoint);
+ } else if (documentPoint.x > mDocumentPosition.right || documentPoint.y > mDocumentPosition.bottom){
+ LOKitShell.sendChangeHandlePositionEvent(SelectionHandle.HandleType.END, documentPoint);
+ }
+ }
+
+ @Override
+ public boolean onHitTest(float x, float y) {
+ return mScreenPosition.contains(x, y);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawRect(mScreenPosition, mPaint);
+ canvas.drawCircle(mScreenPosition.left, mScreenPosition.top, CIRCLE_HANDLE_RADIUS, mCirclePaint);
+ canvas.drawCircle(mScreenPosition.right, mScreenPosition.bottom, CIRCLE_HANDLE_RADIUS, mCirclePaint);
+ }
+
+ public void reposition(RectF rect) {
+ mScreenPosition = rect;
+ }
+
+ @Override
+ public boolean contains(float x, float y) {
+ // test if in range of the box or the circular handles
+ boolean inRange = new RectF(mScreenPosition.left - CIRCLE_HANDLE_RADIUS,
+ mScreenPosition.top - CIRCLE_HANDLE_RADIUS,
+ mScreenPosition.left + CIRCLE_HANDLE_RADIUS,
+ mScreenPosition.top + CIRCLE_HANDLE_RADIUS).contains(x, y)
+ || new RectF(mScreenPosition.right - CIRCLE_HANDLE_RADIUS,
+ mScreenPosition.bottom - CIRCLE_HANDLE_RADIUS,
+ mScreenPosition.right + CIRCLE_HANDLE_RADIUS,
+ mScreenPosition.bottom + CIRCLE_HANDLE_RADIUS).contains(x, y)
+ || onHitTest(x, y);
+ return inRange && isVisible();
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/CanvasElement.java b/android/source/src/java/org/libreoffice/canvas/CanvasElement.java
new file mode 100644
index 0000000000..51e8801f6b
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/CanvasElement.java
@@ -0,0 +1,45 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+
+/**
+ * Canvas element is an element (or part) that is drawn canvas and can
+ * potentially be interacted with.
+ */
+public interface CanvasElement {
+ /**
+ * Called when the element needs to be draw no the canvas. This method
+ * should call onDraw when conditions to draw are satisfied.
+ *
+ * @param canvas - the canvas
+ */
+ void draw(Canvas canvas);
+
+ /**
+ * Hit test - returns true if the object has been hit
+ * @param x - x coordinate of the
+ * @param y - y coordinate of the
+ */
+ boolean contains(float x, float y);
+
+ /**
+ * Return if element is visible.
+ */
+ boolean isVisible();
+
+ /**
+ * Set element visibility.
+ * @param visible - is element visible
+ */
+ void setVisible(boolean visible);
+}
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ \ No newline at end of file
diff --git a/android/source/src/java/org/libreoffice/canvas/CanvasElementImplRequirement.java b/android/source/src/java/org/libreoffice/canvas/CanvasElementImplRequirement.java
new file mode 100644
index 0000000000..26789e8d89
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/CanvasElementImplRequirement.java
@@ -0,0 +1,25 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+
+/**
+ * The interface defines a set of method that a typical CanvasElement
+ * implementation should implement.
+ */
+interface CanvasElementImplRequirement {
+
+ /**
+ * Implement hit test here
+ * @param x - x coordinate of the
+ * @param y - y coordinate of the
+ */
+ boolean onHitTest(float x, float y);
+
+ /**
+ * Called inside draw if the element is visible. Override this method to
+ * draw the element on the canvas.
+ *
+ * @param canvas - the canvas
+ */
+ void onDraw(Canvas canvas);
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/CommonCanvasElement.java b/android/source/src/java/org/libreoffice/canvas/CommonCanvasElement.java
new file mode 100644
index 0000000000..6b40ae4ba9
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/CommonCanvasElement.java
@@ -0,0 +1,46 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+
+/**
+ * Common implementation to canvas elements.
+ */
+public abstract class CommonCanvasElement implements CanvasElement, CanvasElementImplRequirement {
+
+ private boolean mVisible = false;
+
+ /**
+ * Is element visible?
+ */
+ @Override
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ /**
+ * Set element visibility.
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ mVisible = visible;
+ }
+
+ /**
+ * Trigger drawing the element on the canvas.
+ */
+ @Override
+ public void draw(Canvas canvas) {
+ if (isVisible()) {
+ onDraw(canvas);
+ }
+ }
+
+ /**
+ * Hit test. Return true if the element was hit. Directly return false if
+ * the element is invisible.
+ */
+ @Override
+ public boolean contains(float x, float y) {
+ return isVisible() && onHitTest(x, y);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/Cursor.java b/android/source/src/java/org/libreoffice/canvas/Cursor.java
new file mode 100644
index 0000000000..1cd30edb75
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/Cursor.java
@@ -0,0 +1,56 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+/**
+ * Handles the cursor drawing on the canvas.
+ */
+public class Cursor extends CommonCanvasElement {
+ private static final float CURSOR_WIDTH = 2f;
+ private final Paint mCursorPaint = new Paint();
+ public RectF mPosition = new RectF();
+ public RectF mScaledPosition = new RectF();
+ public int mAlpha = 0;
+
+ /**
+ * Construct the cursor and set the default values.
+ */
+ public Cursor() {
+ mCursorPaint.setColor(Color.BLACK);
+ mCursorPaint.setAlpha(0xFF);
+ }
+
+ /**
+ * Hit test for cursor, always false.
+ */
+ @Override
+ public boolean onHitTest(float x, float y) {
+ return false;
+ }
+
+ /**
+ * Draw the cursor.
+ */
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawRect(mScaledPosition, mCursorPaint);
+ }
+
+ /**
+ * Reposition the cursor on screen.
+ */
+ public void reposition(RectF rect) {
+ mScaledPosition = rect;
+ mScaledPosition.right = mScaledPosition.left + CURSOR_WIDTH;
+ }
+
+ /**
+ * Cycle the alpha color of the cursor, makes the
+ */
+ public void cycleAlpha() {
+ mCursorPaint.setAlpha(mCursorPaint.getAlpha() == 0 ? 0xFF : 0);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/GraphicSelection.java b/android/source/src/java/org/libreoffice/canvas/GraphicSelection.java
new file mode 100644
index 0000000000..8d773b2ea2
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/GraphicSelection.java
@@ -0,0 +1,295 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.mozilla.gecko.gfx.LayerView;
+
+import static org.libreoffice.canvas.GraphicSelectionHandle.HandlePosition;
+
+/**
+ * This class is responsible to draw and reposition the selection
+ * rectangle.
+ */
+public class GraphicSelection extends CommonCanvasElement {
+ private final Paint mPaintStroke;
+ private final Paint mPaintFill;
+ public RectF mRectangle = new RectF();
+ public RectF mScaledRectangle = new RectF();
+ private RectF mDrawRectangle = new RectF();
+ private DragType mType = DragType.NONE;
+ private PointF mStartDragPosition;
+
+ private GraphicSelectionHandle mHandles[] = new GraphicSelectionHandle[8];
+ private GraphicSelectionHandle mDragHandle = null;
+ private boolean mTriggerSinglePress = false;
+ private LibreOfficeMainActivity mContext;
+
+ /**
+ * Construct the graphic selection.
+ */
+ public GraphicSelection(LibreOfficeMainActivity context) {
+ mContext = context;
+ // Create the paint, which is needed at drawing
+ mPaintStroke = new Paint();
+ mPaintStroke.setStyle(Paint.Style.STROKE);
+ mPaintStroke.setColor(Color.GRAY);
+ mPaintStroke.setStrokeWidth(2);
+ mPaintStroke.setAntiAlias(true);
+
+ mPaintFill = new Paint();
+ mPaintFill.setStyle(Paint.Style.FILL);
+ mPaintFill.setColor(Color.WHITE);
+ mPaintFill.setAlpha(200);
+ mPaintFill.setAntiAlias(true);
+
+ // Create the handles of the selection
+ mHandles[0] = new GraphicSelectionHandle(HandlePosition.TOP_LEFT);
+ mHandles[1] = new GraphicSelectionHandle(HandlePosition.TOP);
+ mHandles[2] = new GraphicSelectionHandle(HandlePosition.TOP_RIGHT);
+ mHandles[3] = new GraphicSelectionHandle(HandlePosition.LEFT);
+ mHandles[4] = new GraphicSelectionHandle(HandlePosition.RIGHT);
+ mHandles[5] = new GraphicSelectionHandle(HandlePosition.BOTTOM_LEFT);
+ mHandles[6] = new GraphicSelectionHandle(HandlePosition.BOTTOM);
+ mHandles[7] = new GraphicSelectionHandle(HandlePosition.BOTTOM_RIGHT);
+ }
+
+ /**
+ * Viewport has changed, reposition the selection to the new rectangle.
+ * @param scaledRectangle - rectangle of selection position on the document
+ */
+ public void reposition(RectF scaledRectangle) {
+ mScaledRectangle = scaledRectangle;
+ mDrawRectangle = scaledRectangle; // rectangle that will be draw
+
+ // reposition the handles too
+ mHandles[0].reposition(scaledRectangle.left, scaledRectangle.top);
+ mHandles[1].reposition(scaledRectangle.centerX(), scaledRectangle.top);
+ mHandles[2].reposition(scaledRectangle.right, scaledRectangle.top);
+ mHandles[3].reposition(scaledRectangle.left, scaledRectangle.centerY());
+ mHandles[4].reposition(scaledRectangle.right, scaledRectangle.centerY());
+ mHandles[5].reposition(scaledRectangle.left, scaledRectangle.bottom);
+ mHandles[6].reposition(scaledRectangle.centerX(), scaledRectangle.bottom);
+ mHandles[7].reposition(scaledRectangle.right, scaledRectangle.bottom);
+ }
+
+ /**
+ * Hit test for the selection.
+ * @see org.libreoffice.canvas.CanvasElement#draw(android.graphics.Canvas)
+ */
+ @Override
+ public boolean onHitTest(float x, float y) {
+ // Check if handle was hit
+ for (GraphicSelectionHandle handle : mHandles) {
+ if (handle.contains(x, y)) {
+ return true;
+ }
+ }
+ return mScaledRectangle.contains(x, y);
+ }
+
+ /**
+ * Draw the selection on the canvas.
+ * @see org.libreoffice.canvas.CanvasElement#draw(android.graphics.Canvas)
+ */
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawRect(mDrawRectangle, mPaintStroke);
+ if (mType != DragType.NONE) {
+ canvas.drawRect(mDrawRectangle, mPaintFill);
+ }
+ for (GraphicSelectionHandle handle : mHandles) {
+ handle.draw(canvas);
+ }
+ }
+
+ /**
+ * Dragging on the screen has started.
+ * @param position - position where the dragging started
+ */
+ public void dragStart(PointF position) {
+ mDragHandle = null;
+ mType = DragType.NONE;
+ for (GraphicSelectionHandle handle : mHandles) {
+ if (handle.contains(position.x, position.y)) {
+ mDragHandle = handle;
+ mDragHandle.select();
+ mType = DragType.EXTEND;
+ sendGraphicSelectionStart(handle.mPosition);
+ }
+ }
+ if (mDragHandle == null) {
+ mType = DragType.MOVE;
+ sendGraphicSelectionStart(position);
+ }
+ mStartDragPosition = position;
+ mTriggerSinglePress = true;
+ }
+
+ /**
+ * Dragging is in process.
+ * @param position - position of the drag
+ */
+ public void dragging(PointF position) {
+ if (mType == DragType.MOVE) {
+ float deltaX = position.x - mStartDragPosition.x;
+ float deltaY = position.y - mStartDragPosition.y;
+
+ mDrawRectangle = new RectF(mScaledRectangle);
+ mDrawRectangle.offset(deltaX, deltaY);
+ } else if (mType == DragType.EXTEND) {
+ adaptDrawRectangle(position.x, position.y);
+ }
+ mTriggerSinglePress = false;
+ }
+
+ /**
+ * Dragging has ended.
+ * @param position - last position of the drag
+ */
+ public void dragEnd(PointF position) {
+ PointF point = new PointF();
+ if (mDragHandle != null) {
+ point.x = mDragHandle.mPosition.x;
+ point.y = mDragHandle.mPosition.y;
+ mDragHandle.reset();
+ mDragHandle = null;
+ } else {
+ point.x = mStartDragPosition.x;
+ point.y = mStartDragPosition.y;
+ }
+ float deltaX = position.x - mStartDragPosition.x;
+ float deltaY = position.y - mStartDragPosition.y;
+ point.offset(deltaX, deltaY);
+
+ sendGraphicSelectionEnd(point);
+
+ if (mTriggerSinglePress && mDragHandle == null) {
+ onSinglePress(point);
+ mTriggerSinglePress = false;
+ }
+
+ mDrawRectangle = mScaledRectangle;
+ mType = DragType.NONE;
+ }
+
+ /**
+ * Adapt the selection depending on which handle was dragged.
+ */
+ private void adaptDrawRectangle(float x, float y) {
+ mDrawRectangle = new RectF(mScaledRectangle);
+ switch(mDragHandle.getHandlePosition()) {
+ case TOP_LEFT:
+ mDrawRectangle.left = x;
+ mDrawRectangle.top = y;
+ break;
+ case TOP:
+ mDrawRectangle.top = y;
+ break;
+ case TOP_RIGHT:
+ mDrawRectangle.right = x;
+ mDrawRectangle.top = y;
+ break;
+ case LEFT:
+ mDrawRectangle.left = x;
+ break;
+ case RIGHT:
+ mDrawRectangle.right = x;
+ break;
+ case BOTTOM_LEFT:
+ mDrawRectangle.left = x;
+ mDrawRectangle.bottom = y;
+ break;
+ case BOTTOM:
+ mDrawRectangle.bottom = y;
+ break;
+ case BOTTOM_RIGHT:
+ mDrawRectangle.right = x;
+ mDrawRectangle.bottom = y;
+ break;
+ }
+ }
+
+ /**
+ * Send graphic selection start event to LOKitTread.
+ * @param screenPosition - screen position of the selection
+ */
+ private void sendGraphicSelectionStart(PointF screenPosition) {
+ sendGraphicSelection("GraphicSelectionStart", screenPosition);
+ }
+
+ /**
+ * Send graphic selection end event to LOKitTread.
+ * @param screenPosition - screen position of the selection
+ */
+ private void sendGraphicSelectionEnd(PointF screenPosition) {
+ sendGraphicSelection("GraphicSelectionEnd", screenPosition);
+ }
+
+ /**
+ * Send graphic selection event to LOKitTread.
+ * @param type - type of the graphic selection
+ * @param screenPosition - screen position of the selection
+ */
+ private void sendGraphicSelection(String type, PointF screenPosition)
+ {
+ LayerView layerView = mContext.getLayerClient().getView();
+ if (layerView != null) {
+ // Position is in screen coordinates. We need to convert them to
+ // document coordinates.
+ PointF documentPoint = layerView.getLayerClient().convertViewPointToLayerPoint(screenPosition);
+ LOKitShell.sendTouchEvent(type, documentPoint);
+ }
+ }
+
+ /**
+ * When a single press (no dragging happened) was performed.
+ */
+ private void onSinglePress(PointF screenPosition) {
+ sendGraphicSelection("LongPress", screenPosition);
+ }
+
+ /**
+ * Set the visibility of the graphic selection.
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ for (GraphicSelectionHandle handle: mHandles) {
+ handle.setVisible(visible);
+ }
+ }
+
+ /**
+ * Reset the selection.
+ */
+ public void reset() {
+ mDragHandle = null;
+ for (GraphicSelectionHandle handle : mHandles) {
+ handle.reset();
+ }
+ }
+
+ /**
+ * Type of the selection dragging.
+ */
+ public enum DragType {
+ NONE,
+ MOVE,
+ EXTEND
+ }
+}
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/canvas/GraphicSelectionHandle.java b/android/source/src/java/org/libreoffice/canvas/GraphicSelectionHandle.java
new file mode 100644
index 0000000000..68b445af6f
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/GraphicSelectionHandle.java
@@ -0,0 +1,146 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+/**
+ * This class is responsible to draw the selection handles, track the handle
+ * position and perform a hit test to determine if the selection handle was
+ * touched.
+ */
+public class GraphicSelectionHandle extends CommonCanvasElement {
+ /**
+ * The factor used to inflate the hit area.
+ */
+ private final float HIT_AREA_INFLATE_FACTOR = 1.75f;
+
+ private final HandlePosition mHandlePosition;
+ public PointF mPosition = new PointF();
+ private float mRadius = 20.0f;
+ private Paint mStrokePaint = new Paint();
+ private Paint mFillPaint = new Paint();
+ private Paint mSelectedFillPaint = new Paint();
+ private RectF mHitRect = new RectF();
+ private boolean mSelected = false;
+
+ /**
+ * Construct the handle - set the handle position on the selection.
+ * @param position - the handle position on the selection
+ */
+ public GraphicSelectionHandle(HandlePosition position) {
+ mHandlePosition = position;
+
+ mStrokePaint.setStyle(Paint.Style.STROKE);
+ mStrokePaint.setColor(Color.GRAY);
+ mStrokePaint.setStrokeWidth(3);
+ mStrokePaint.setAntiAlias(true);
+
+ mFillPaint.setStyle(Paint.Style.FILL);
+ mFillPaint.setColor(Color.WHITE);
+ mFillPaint.setAlpha(200);
+ mFillPaint.setAntiAlias(true);
+
+ mSelectedFillPaint.setStyle(Paint.Style.FILL);
+ mSelectedFillPaint.setColor(Color.GRAY);
+ mSelectedFillPaint.setAlpha(200);
+ mSelectedFillPaint.setAntiAlias(true);
+ }
+
+ /**
+ * The position of the handle.
+ * @return
+ */
+ public HandlePosition getHandlePosition() {
+ return mHandlePosition;
+ }
+
+ /**
+ * Draws the handle to the canvas.
+ *
+ * @see org.libreoffice.canvas.CanvasElement#draw(android.graphics.Canvas)
+ */
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (mSelected) {
+ drawFilledCircle(canvas, mPosition.x, mPosition.y, mRadius, mStrokePaint, mSelectedFillPaint);
+ } else {
+ drawFilledCircle(canvas, mPosition.x, mPosition.y, mRadius, mStrokePaint, mFillPaint);
+ }
+ }
+
+ /**
+ * Draw a filled and stroked circle to the canvas.
+ */
+ private void drawFilledCircle(Canvas canvas, float x, float y, float radius, Paint strokePaint, Paint fillPaint) {
+ canvas.drawCircle(x, y, radius, fillPaint);
+ canvas.drawCircle(x, y, radius, strokePaint);
+ }
+
+ /**
+ * Viewport has changed, reposition the handle to the input coordinates.
+ */
+ public void reposition(float x, float y) {
+ mPosition.x = x;
+ mPosition.y = y;
+
+ // inflate the radius by HIT_AREA_INFLATE_FACTOR
+ float inflatedRadius = mRadius * HIT_AREA_INFLATE_FACTOR;
+
+ // reposition the hit area rectangle
+ mHitRect.left = mPosition.x - inflatedRadius;
+ mHitRect.right = mPosition.x + inflatedRadius;
+ mHitRect.top = mPosition.y - inflatedRadius;
+ mHitRect.bottom = mPosition.y + inflatedRadius;
+ }
+
+ /**
+ * Hit test for the handle.
+ * @see org.libreoffice.canvas.CanvasElement#draw(android.graphics.Canvas)
+ */
+ @Override
+ public boolean onHitTest(float x, float y) {
+ return mHitRect.contains(x, y);
+ }
+
+ /**
+ * Mark the handle as selected.
+ */
+ public void select() {
+ mSelected = true;
+ }
+
+ /**
+ * Reset the selection for the handle.
+ */
+ public void reset() {
+ mSelected = false;
+ }
+
+ /**
+ * All possible handle positions. The selection rectangle has 8 possible
+ * handles.
+ */
+ public enum HandlePosition {
+ TOP_LEFT,
+ TOP,
+ TOP_RIGHT,
+ RIGHT,
+ BOTTOM_RIGHT,
+ BOTTOM,
+ BOTTOM_LEFT,
+ LEFT
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ \ No newline at end of file
diff --git a/android/source/src/java/org/libreoffice/canvas/ImageUtils.java b/android/source/src/java/org/libreoffice/canvas/ImageUtils.java
new file mode 100644
index 0000000000..ecda9b77c5
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/ImageUtils.java
@@ -0,0 +1,29 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+
+class ImageUtils {
+ static Bitmap getBitmapForDrawable(Drawable drawable) {
+ drawable = drawable.mutate();
+
+ int width = !drawable.getBounds().isEmpty() ?
+ drawable.getBounds().width() : drawable.getIntrinsicWidth();
+
+ width = width <= 0 ? 1 : width;
+
+ int height = !drawable.getBounds().isEmpty() ?
+ drawable.getBounds().height() : drawable.getIntrinsicHeight();
+
+ height = height <= 0 ? 1 : height;
+
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return bitmap;
+ }
+}
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/canvas/PageNumberRect.java b/android/source/src/java/org/libreoffice/canvas/PageNumberRect.java
new file mode 100644
index 0000000000..62de88ea54
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/PageNumberRect.java
@@ -0,0 +1,64 @@
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.TextPaint;
+
+/*
+ * A canvas element on DocumentOverlayView. Shows a rectangle with current page
+ * number and total page number inside of it.
+ */
+public class PageNumberRect extends CommonCanvasElement {
+ private String mPageNumberString;
+ private TextPaint mPageNumberRectPaint = new TextPaint();
+ private Paint mBgPaint = new Paint();
+ private Rect mTextBounds = new Rect();
+ private float mBgMargin = 5f;
+
+ public PageNumberRect() {
+ mBgPaint.setColor(Color.BLACK);
+ mBgPaint.setAlpha(100);
+ mPageNumberRectPaint.setColor(Color.WHITE);
+ }
+
+ /**
+ * Implement hit test here
+ *
+ * @param x - x coordinate of the
+ * @param y - y coordinate of the
+ */
+ @Override
+ public boolean onHitTest(float x, float y) {
+ return false;
+ }
+
+ /**
+ * Called inside draw if the element is visible. Override this method to
+ * draw the element on the canvas.
+ *
+ * @param canvas - the canvas
+ */
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawRect(canvas.getWidth()*0.1f - mBgMargin,
+ canvas.getHeight()*0.1f - mTextBounds.height() - mBgMargin,
+ mTextBounds.width() + canvas.getWidth()*0.1f + mBgMargin,
+ canvas.getHeight()*0.1f + mBgMargin,
+ mBgPaint);
+ canvas.drawText(mPageNumberString, canvas.getWidth()*0.1f, canvas.getHeight()*0.1f, mPageNumberRectPaint);
+ }
+
+ public void setPageNumberString (String pageNumberString) {
+ mPageNumberString = pageNumberString;
+ mPageNumberRectPaint.getTextBounds(mPageNumberString, 0, mPageNumberString.length(), mTextBounds);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/SelectionHandle.java b/android/source/src/java/org/libreoffice/canvas/SelectionHandle.java
new file mode 100644
index 0000000000..ddd16fe5eb
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/SelectionHandle.java
@@ -0,0 +1,73 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+
+/**
+ * Selection handle is a common class for "start", "middle" and "end" types
+ * of selection handles.
+ */
+public abstract class SelectionHandle extends BitmapHandle {
+ private static final long MINIMUM_HANDLE_UPDATE_TIME = 50 * 1000000;
+
+ private final PointF mDragStartPoint = new PointF();
+ private final PointF mDragDocumentPosition = new PointF();
+ private long mLastTime = 0;
+
+ private LibreOfficeMainActivity mContext;
+
+ public SelectionHandle(LibreOfficeMainActivity context, Bitmap bitmap) {
+ super(bitmap);
+ mContext = context;
+ }
+
+ /**
+ * Start of a touch and drag action on the handle.
+ */
+ public void dragStart(PointF point) {
+ mDragStartPoint.x = point.x;
+ mDragStartPoint.y = point.y;
+ mDragDocumentPosition.x = mDocumentPosition.left;
+ mDragDocumentPosition.y = mDocumentPosition.top;
+ }
+
+ /**
+ * End of a touch and drag action on the handle.
+ */
+ public void dragEnd(PointF point) {
+ }
+
+ /**
+ * Handle has been dragged.
+ */
+ public void dragging(PointF point) {
+ long currentTime = System.nanoTime();
+ if (currentTime - mLastTime > MINIMUM_HANDLE_UPDATE_TIME) {
+ mLastTime = currentTime;
+ signalHandleMove(point.x, point.y);
+ }
+ }
+
+ /**
+ * Signal to move the handle to a new position to LO.
+ */
+ private void signalHandleMove(float newX, float newY) {
+ ImmutableViewportMetrics viewportMetrics = mContext.getLayerClient().getViewportMetrics();
+ float zoom = viewportMetrics.zoomFactor;
+
+ float deltaX = (newX - mDragStartPoint.x) / zoom;
+ float deltaY = (newY - mDragStartPoint.y) / zoom;
+
+ PointF documentPoint = new PointF(mDragDocumentPosition.x + deltaX, mDragDocumentPosition.y + deltaY);
+
+ LOKitShell.sendChangeHandlePositionEvent(getHandleType(), documentPoint);
+ }
+
+ public abstract HandleType getHandleType();
+
+ public enum HandleType { START, MIDDLE, END }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/SelectionHandleEnd.java b/android/source/src/java/org/libreoffice/canvas/SelectionHandleEnd.java
new file mode 100644
index 0000000000..b85b80fc95
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/SelectionHandleEnd.java
@@ -0,0 +1,22 @@
+package org.libreoffice.canvas;
+
+import org.libreoffice.LibreOfficeMainActivity;
+
+import org.libreoffice.R;
+
+/**
+ * Selection handle for showing and manipulating the end of a selection.
+ */
+public class SelectionHandleEnd extends SelectionHandle {
+ public SelectionHandleEnd(LibreOfficeMainActivity context) {
+ super(context, getBitmapForDrawable(context, R.drawable.handle_alias_end));
+ }
+
+ /**
+ * Define the type of the handle.
+ */
+ @Override
+ public HandleType getHandleType() {
+ return HandleType.END;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/SelectionHandleMiddle.java b/android/source/src/java/org/libreoffice/canvas/SelectionHandleMiddle.java
new file mode 100644
index 0000000000..76bdf9110a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/SelectionHandleMiddle.java
@@ -0,0 +1,34 @@
+package org.libreoffice.canvas;
+
+import org.libreoffice.LibreOfficeMainActivity;
+
+import org.libreoffice.R;
+
+/**
+ * Selection handle that is used to manipulate the cursor.
+ */
+public class SelectionHandleMiddle extends SelectionHandle {
+ public SelectionHandleMiddle(LibreOfficeMainActivity context) {
+ super(context, getBitmapForDrawable(context, R.drawable.handle_alias_middle));
+ }
+
+ /**
+ * Change the position of the handle on the screen. Take into account the
+ * handle alignment to the center.
+ */
+ @Override
+ public void reposition(float x, float y) {
+ super.reposition(x, y);
+ // align to the center
+ float offset = mScreenPosition.width() / 2.0f;
+ mScreenPosition.offset(-offset, 0);
+ }
+
+ /**
+ * Define the type of the handle.
+ */
+ @Override
+ public HandleType getHandleType() {
+ return HandleType.MIDDLE;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/SelectionHandleStart.java b/android/source/src/java/org/libreoffice/canvas/SelectionHandleStart.java
new file mode 100644
index 0000000000..ad28826f64
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/SelectionHandleStart.java
@@ -0,0 +1,34 @@
+package org.libreoffice.canvas;
+
+import org.libreoffice.LibreOfficeMainActivity;
+
+import org.libreoffice.R;
+
+/**
+ * Selection handle for showing and manipulating the start of a selection.
+ */
+public class SelectionHandleStart extends SelectionHandle {
+ public SelectionHandleStart(LibreOfficeMainActivity context) {
+ super(context, getBitmapForDrawable(context, R.drawable.handle_alias_start));
+ }
+
+ /**
+ * Change the position of the handle on the screen. Take into account the
+ * handle alignment to the right.
+ */
+ @Override
+ public void reposition(float x, float y) {
+ super.reposition(x, y);
+ // align to the right
+ float offset = mScreenPosition.width();
+ mScreenPosition.offset(-offset, 0);
+ }
+
+ /**
+ * Define the type of the handle.
+ */
+ @Override
+ public HandleType getHandleType() {
+ return HandleType.START;
+ }
+} \ No newline at end of file
diff --git a/android/source/src/java/org/libreoffice/overlay/CalcHeadersController.java b/android/source/src/java/org/libreoffice/overlay/CalcHeadersController.java
new file mode 100644
index 0000000000..8b99c292cb
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/CalcHeadersController.java
@@ -0,0 +1,281 @@
+package org.libreoffice.overlay;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import com.google.android.material.snackbar.Snackbar;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.Button;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.LOEvent;
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.R;
+import org.mozilla.gecko.gfx.LayerView;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+
+import static org.libreoffice.SearchController.addProperty;
+
+public class CalcHeadersController {
+ private static final String LOGTAG = CalcHeadersController.class.getSimpleName();
+
+ private final CalcHeadersView mCalcRowHeadersView;
+ private final CalcHeadersView mCalcColumnHeadersView;
+
+ private LibreOfficeMainActivity mContext;
+
+ public CalcHeadersController(LibreOfficeMainActivity context, final LayerView layerView) {
+ mContext = context;
+ mContext.getDocumentOverlay().setCalcHeadersController(this);
+ mCalcRowHeadersView = context.findViewById(R.id.calc_header_row);
+ mCalcColumnHeadersView = context.findViewById(R.id.calc_header_column);
+ if (mCalcColumnHeadersView == null || mCalcRowHeadersView == null) {
+ Log.e(LOGTAG, "Failed to initialize Calc headers - View is null");
+ } else {
+ mCalcRowHeadersView.initialize(layerView, true);
+ mCalcColumnHeadersView.initialize(layerView, false);
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_CALC_HEADERS));
+ context.findViewById(R.id.calc_header_top_left).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectAll"));
+ if (mCalcColumnHeadersView == null) return;
+ mCalcColumnHeadersView.showHeaderPopup(new PointF());
+ }
+ });
+ ((EditText)context.findViewById(R.id.calc_address)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO) {
+ String text = v.getText().toString();
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "ToPoint", "string", text);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:GoToCell", rootJson.toString()));
+ mContext.hideSoftKeyboard();
+ layerView.requestFocus();
+ }
+ return true;
+ }
+ });
+ ((EditText)context.findViewById(R.id.calc_formula)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO) {
+ String text = v.getText().toString();
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "StringName", "string", text);
+ addProperty(rootJson, "DontCommit", "boolean", String.valueOf(false));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:EnterString", rootJson.toString()));
+ mContext.hideSoftKeyboard();
+ layerView.requestFocus();
+ mContext.setDocumentChanged(true);
+ }
+ return true;
+ }
+ });
+ // manually select A1 for address bar and formula bar to update when calc first opens
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "ToPoint", "string", "A1");
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:GoToCell", rootJson.toString()));
+ }
+
+ public void setupHeaderPopupView() {
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ String[] rowOrColumn = {"Row","Column"};
+ CalcHeadersView[] headersViews= {mCalcRowHeadersView, mCalcColumnHeadersView};
+ for (int i = 0; i < rowOrColumn.length; i++) {
+ // create popup window
+ final String tempName = rowOrColumn[i];
+ final CalcHeadersView tempView = headersViews[i];
+ final View headerPopupView = inflater.inflate(R.layout.calc_header_popup, null);
+ final PopupWindow popupWindow = new PopupWindow(headerPopupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
+ @Override
+ public void onDismiss() {
+ headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_dialog).setVisibility(View.GONE);
+ popupWindow.setFocusable(false);
+ }
+ });
+ popupWindow.setOutsideTouchable(true);
+ popupWindow.setBackgroundDrawable(new ColorDrawable());
+ popupWindow.setAnimationStyle(android.R.style.Animation_Dialog);
+ tempView.setHeaderPopupWindow(popupWindow);
+ // set up child views in the popup window
+ headerPopupView.findViewById(R.id.calc_header_popup_insert).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Insert"+tempName+"s"));
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_delete).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Delete"+tempName+"s"));
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_hide).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Hide"+tempName));
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_show).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Show"+tempName));
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_optimal_length).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ View view = headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_dialog);
+ if (view.getVisibility() == View.VISIBLE) {
+ view.setVisibility(View.GONE);
+ popupWindow.setFocusable(false);
+ popupWindow.update();
+ } else {
+ popupWindow.dismiss();
+ view.setVisibility(View.VISIBLE);
+ popupWindow.setFocusable(true);
+ popupWindow.showAtLocation(tempView, Gravity.CENTER, 0, 0);
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ Snackbar.make(tempView, R.string.calc_alert_double_click_optimal_length, Snackbar.LENGTH_LONG).show();
+ }
+ });
+ }
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String text = ((EditText)headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_text)).getText().toString();
+ tempView.sendOptimalLengthRequest(text);
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_adjust_length).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mContext.getDocumentOverlay().showAdjustLengthLine(tempView == mCalcRowHeadersView, tempView);
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ ((Button)headerPopupView.findViewById(R.id.calc_header_popup_adjust_length))
+ .setText(tempView == mCalcRowHeadersView ? R.string.calc_adjust_height : R.string.calc_adjust_width);
+ ((Button)headerPopupView.findViewById(R.id.calc_header_popup_optimal_length))
+ .setText(tempView == mCalcRowHeadersView ? R.string.calc_optimal_height : R.string.calc_optimal_width);
+
+ }
+ }
+
+ public void setHeaders(String headers) {
+ HeaderInfo parsedHeaders = parseHeaderInfo(headers);
+ if (parsedHeaders != null) {
+ mCalcRowHeadersView.setHeaders(parsedHeaders.rowLabels, parsedHeaders.rowDimens);
+ mCalcColumnHeadersView.setHeaders(parsedHeaders.columnLabels, parsedHeaders.columnDimens);
+ showHeaders();
+ } else {
+ Log.e(LOGTAG, "Parse header info JSON failed.");
+ }
+ }
+
+ public void showHeaders() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mCalcColumnHeadersView.invalidate();
+ mCalcRowHeadersView.invalidate();
+ }
+ });
+ }
+
+ private HeaderInfo parseHeaderInfo(String headers) {
+ HeaderInfo headerInfo = new HeaderInfo();
+ try {
+ JSONObject collectiveResult = new JSONObject(headers);
+ JSONArray rowResult = collectiveResult.getJSONArray("rows");
+ for (int i = 0; i < rowResult.length(); i++) {
+ headerInfo.rowLabels.add(rowResult.getJSONObject(i).getString("text"));
+ headerInfo.rowDimens.add(BigDecimal.valueOf(rowResult.getJSONObject(i).getLong("size")).floatValue());
+ }
+ JSONArray columnResult = collectiveResult.getJSONArray("columns");
+ for (int i = 0; i < columnResult.length(); i++) {
+ headerInfo.columnLabels.add(columnResult.getJSONObject(i).getString("text"));
+ headerInfo.columnDimens.add(BigDecimal.valueOf(columnResult.getJSONObject(i).getLong("size")).floatValue());
+ }
+ return headerInfo;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public void showHeaderSelection(RectF cellCursorRect) {
+ mCalcRowHeadersView.setHeaderSelection(cellCursorRect);
+ mCalcColumnHeadersView.setHeaderSelection(cellCursorRect);
+ showHeaders();
+ }
+
+ public void setPendingRowOrColumnSelectionToShowUp(boolean b) {
+ mCalcRowHeadersView.setPendingRowOrColumnSelectionToShowUp(b);
+ mCalcColumnHeadersView.setPendingRowOrColumnSelectionToShowUp(b);
+ }
+
+ public boolean pendingRowOrColumnSelectionToShowUp() {
+ return mCalcColumnHeadersView.pendingRowOrColumnSelectionToShowUp()
+ || mCalcRowHeadersView.pendingRowOrColumnSelectionToShowUp();
+ }
+
+ private class HeaderInfo {
+ ArrayList<String> rowLabels;
+ ArrayList<Float> rowDimens;
+ ArrayList<String> columnLabels;
+ ArrayList<Float> columnDimens;
+ private HeaderInfo() {
+ rowLabels = new ArrayList<String>();
+ rowDimens = new ArrayList<Float>();
+ columnDimens = new ArrayList<Float>();
+ columnLabels = new ArrayList<String>();
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java b/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java
new file mode 100644
index 0000000000..98af7a9554
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java
@@ -0,0 +1,278 @@
+package org.libreoffice.overlay;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import androidx.core.view.GestureDetectorCompat;
+import android.util.AttributeSet;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.PopupWindow;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.LOEvent;
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.canvas.CalcHeaderCell;
+import org.libreoffice.kit.Document;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+import org.mozilla.gecko.gfx.LayerView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import static org.libreoffice.SearchController.addProperty;
+
+public class CalcHeadersView extends View {
+ private static final String LOGTAG = CalcHeadersView.class.getSimpleName();
+
+ private boolean mInitialized;
+ private LayerView mLayerView;
+ private boolean mIsRow; // true if this is for row headers, false for column
+ private ArrayList<String> mLabels;
+ private ArrayList<Float> mDimens;
+ private RectF mCellCursorRect;
+ private boolean mPendingRowOrColumnSelectionToShowUp;
+ private GestureDetectorCompat mDetector;
+ private PopupWindow mPopupWindow;
+ private int mPrevScrollIndex = -1;
+
+ public CalcHeadersView(Context context) {
+ super(context);
+ }
+
+ public CalcHeadersView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CalcHeadersView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void initialize(LayerView layerView, boolean isRow) {
+ if (!mInitialized) {
+ mLayerView = layerView;
+ mIsRow = isRow;
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mDetector = new GestureDetectorCompat(getContext(), new HeaderGestureListener());
+ }
+ });
+
+ setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mPrevScrollIndex = -1; // clear mPrevScrollIndex to default
+ }
+ return mDetector.onTouchEvent(event);
+ }
+ });
+
+ mInitialized = true;
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mInitialized && mDimens != null && mLabels != null) {
+ updateHeaders(canvas);
+ }
+ }
+
+ private void updateHeaders(Canvas canvas) {
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ float zoom = metrics.getZoomFactor();
+ PointF origin = metrics.getOrigin();
+
+ // Draw headers
+ boolean inRangeOfVisibleHeaders = false; // a helper variable for skipping unnecessary onDraw()'s
+ float top,bottom,left,right;
+ for (int i = 1; i < mLabels.size(); i++) {
+ if (mDimens.get(i).equals(mDimens.get(i-1))) continue;
+ if (mIsRow) {
+ top = -origin.y + zoom*mDimens.get(i-1);
+ bottom = -origin.y + zoom*mDimens.get(i);
+ if (top <= getHeight() && bottom >= 0) {
+ inRangeOfVisibleHeaders = true;
+ boolean isSelected = mCellCursorRect != null && bottom > mCellCursorRect.top - origin.y && top < mCellCursorRect.bottom - origin.y;
+ new CalcHeaderCell(0f, top, getWidth(), bottom - top, mLabels.get(i), isSelected).onDraw(canvas);
+ } else {
+ if (inRangeOfVisibleHeaders) {
+ break;
+ }
+ }
+ } else {
+ left = -origin.x + zoom*mDimens.get(i-1);
+ right = -origin.x + zoom*mDimens.get(i);
+ if (left <= getWidth() && right >= 0) {
+ boolean isSelected = mCellCursorRect != null && right > mCellCursorRect.left - origin.x && left < mCellCursorRect.right - origin.x;
+ new CalcHeaderCell(left, 0f, right - left, getHeight(), mLabels.get(i), isSelected).onDraw(canvas);
+ } else {
+ if (inRangeOfVisibleHeaders) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle a single tap event on a header cell.
+ * Selects whole row/column.
+ */
+ private void highlightRowOrColumn(PointF point, boolean shift) {
+ int index = getIndexFromPointOfTouch(point);
+ try {
+ JSONObject rootJson = new JSONObject();
+ if (shift) {
+ addProperty(rootJson, "Modifier", "unsigned short",
+ String.valueOf(Document.KEYBOARD_MODIFIER_SHIFT));
+ } else {
+ addProperty(rootJson, "Modifier", "unsigned short", "0");
+ }
+ if (mIsRow) {
+ addProperty(rootJson, "Row", "unsigned short", String.valueOf(index));
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectRow", rootJson.toString()));
+ } else {
+ addProperty(rootJson, "Col", "unsigned short", String.valueOf(index));
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectColumn", rootJson.toString()));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ // At this point, InvalidationHandler.java will have received two callbacks.
+ // One is for text selection (first) and the other for cell selection (second).
+ // The second will override the first on headers which is not wanted.
+ // setPendingRowOrColumnSelectionToShowUp(true) will skip the second call.
+ setPendingRowOrColumnSelectionToShowUp(true);
+ }
+
+ public int getIndexFromPointOfTouch(PointF point) {
+ int searchedIndex, index;
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ float zoom = metrics.getZoomFactor();
+ PointF origin = metrics.getOrigin();
+ if (mIsRow) {
+ searchedIndex = Collections.binarySearch(mDimens, (point.y+origin.y)/zoom);
+ } else {
+ searchedIndex = Collections.binarySearch(mDimens, (point.x+origin.x)/zoom);
+ }
+ // converting searched index to real index on headers
+ if (searchedIndex < 0) {
+ index = - searchedIndex - 2;
+ } else {
+ index = searchedIndex;
+ }
+ return index;
+ }
+
+ public void setPendingRowOrColumnSelectionToShowUp(boolean b) {
+ mPendingRowOrColumnSelectionToShowUp = b;
+ }
+
+ public boolean pendingRowOrColumnSelectionToShowUp() {
+ return mPendingRowOrColumnSelectionToShowUp;
+ }
+
+ public void setHeaders(ArrayList<String> labels, ArrayList<Float> dimens) {
+ mLabels = labels;
+ mDimens = dimens;
+ }
+
+ public void setHeaderSelection(RectF cellCursorRect) {
+ mCellCursorRect = cellCursorRect;
+ }
+
+ public void showHeaderPopup(PointF point) {
+ if (mPopupWindow == null ||
+ !LibreOfficeMainActivity.isExperimentalMode()) return;
+ if (mIsRow) {
+ mPopupWindow.showAsDropDown(this, getWidth()*3/2, -getHeight()+(int)point.y);
+ } else {
+ mPopupWindow.showAsDropDown(this, (int)point.x, getHeight()/2);
+ }
+ }
+
+ public void dismissPopupWindow() {
+ if (mPopupWindow == null) return;
+ mPopupWindow.dismiss();
+ }
+
+ public void setHeaderPopupWindow(PopupWindow popupWindow) {
+ if (mPopupWindow != null) return;
+ mPopupWindow = popupWindow;
+ }
+
+ public void sendOptimalLengthRequest(String text) {
+ JSONObject rootJson = new JSONObject();
+ if (mIsRow) {
+ try {
+ addProperty(rootJson, "aExtraHeight", "unsigned short", text);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SetOptimalRowHeight", rootJson.toString()));
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ }
+ } else {
+ try {
+ addProperty(rootJson, "aExtraWidth", "unsigned short", text);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SetOptimalColumnWidth", rootJson.toString()));
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+
+ private class HeaderGestureListener extends SimpleOnGestureListener {
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ PointF pointOfTouch = new PointF(e.getX(), e.getY());
+ highlightRowOrColumn(pointOfTouch, false);
+ showHeaderPopup(pointOfTouch);
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ PointF point2 = new PointF(e2.getX(), e2.getY());
+ if (mPrevScrollIndex != getIndexFromPointOfTouch(point2)) {
+ mPrevScrollIndex = getIndexFromPointOfTouch(point2);
+ highlightRowOrColumn(point2, true);
+ dismissPopupWindow();
+ showHeaderPopup(point2);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ PointF pointOfTouch = new PointF(e.getX(), e.getY());
+ highlightRowOrColumn(pointOfTouch, false);
+ if (mIsRow) {
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "aExtraHeight", "unsigned short", String.valueOf(0));
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND,".uno:SetOptimalRowHeight", rootJson.toString()));
+ } else {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND,".uno:SetOptimalColumnWidthDirect"));
+ }
+ showHeaderPopup(pointOfTouch);
+ return true;
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java b/android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java
new file mode 100644
index 0000000000..f977866a28
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java
@@ -0,0 +1,271 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.overlay;
+
+import android.graphics.RectF;
+import android.util.Log;
+
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.R;
+import org.libreoffice.canvas.SelectionHandle;
+import org.mozilla.gecko.gfx.Layer;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.util.FloatUtils;
+
+import java.util.List;
+
+/**
+ * The DocumentOverlay is an overlay over the document. This class is responsible
+ * to setup the document overlay view, report visibility and position of its elements
+ * when they change and report any changes to the viewport.
+ */
+public class DocumentOverlay {
+ private static final String LOGTAG = DocumentOverlay.class.getSimpleName();
+
+ private final DocumentOverlayView mDocumentOverlayView;
+ private final DocumentOverlayLayer mDocumentOverlayLayer;
+
+ private final long hidePageNumberRectDelayInMilliseconds = 500;
+
+ /**
+ * DocumentOverlayLayer responsibility is to get the changes to the viewport
+ * and report them to DocumentOverlayView.
+ */
+ private class DocumentOverlayLayer extends Layer {
+ private float mViewLeft;
+ private float mViewTop;
+ private float mViewZoom;
+
+ /**
+ * @see Layer#draw(org.mozilla.gecko.gfx.Layer.RenderContext)
+ */
+ @Override
+ public void draw(final RenderContext context) {
+ if (FloatUtils.fuzzyEquals(mViewLeft, context.viewport.left)
+ && FloatUtils.fuzzyEquals(mViewTop, context.viewport.top)
+ && FloatUtils.fuzzyEquals(mViewZoom, context.zoomFactor)) {
+ return;
+ }
+
+ mViewLeft = context.viewport.left;
+ mViewTop = context.viewport.top;
+ mViewZoom = context.zoomFactor;
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.repositionWithViewport(mViewLeft, mViewTop, mViewZoom);
+ }
+ });
+ }
+ }
+
+ public DocumentOverlay(LibreOfficeMainActivity context, LayerView layerView) {
+ mDocumentOverlayView = context.findViewById(R.id.text_cursor_view);
+ mDocumentOverlayLayer = new DocumentOverlayLayer();
+ if (mDocumentOverlayView == null) {
+ Log.e(LOGTAG, "Failed to initialize TextCursorLayer - CursorView is null");
+ }
+ layerView.addLayer(mDocumentOverlayLayer);
+ mDocumentOverlayView.initialize(layerView);
+ }
+
+ public void setPartPageRectangles(List<RectF> rectangles) {
+ mDocumentOverlayView.setPartPageRectangles(rectangles);
+ }
+
+ /**
+ * Show the cursor at the defined cursor position on the overlay.
+ */
+ public void showCursor() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.showCursor();
+ }
+ });
+ }
+
+ /**
+ * Hide the cursor at the defined cursor position on the overlay.
+ */
+ public void hideCursor() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.hideCursor();
+ }
+ });
+ }
+
+ /**
+ * Show the page number rectangle on the overlay.
+ */
+ public void showPageNumberRect() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.showPageNumberRect();
+ }
+ });
+ }
+
+ /**
+ * Hide the page number rectangle on the overlay.
+ */
+ public void hidePageNumberRect() {
+ LOKitShell.getMainHandler().postDelayed(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.hidePageNumberRect();
+ }
+ }, hidePageNumberRectDelayInMilliseconds);
+ }
+
+ /**
+ * Position the cursor to the input position on the overlay.
+ */
+ public void positionCursor(final RectF position) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.changeCursorPosition(position);
+ }
+ });
+ }
+
+ /**
+ * Show selections on the overlay.
+ */
+ public void showSelections() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.showSelections();
+ }
+ });
+ }
+
+ /**
+ * Hide selections on the overlay.
+ */
+ public void hideSelections() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.hideSelections();
+ }
+ });
+ }
+
+ /**
+ * Change the list of selections.
+ */
+ public void changeSelections(final List<RectF> selections) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.changeSelections(selections);
+ }
+ });
+ }
+
+ /**
+ * Show the graphic selection on the overlay.
+ */
+ public void showGraphicSelection() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.showGraphicSelection();
+ }
+ });
+ }
+
+ /**
+ * Hide the graphic selection.
+ */
+ public void hideGraphicSelection() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.hideGraphicSelection();
+ }
+ });
+ }
+
+ /**
+ * Change the graphic selection rectangle to the input rectangle.
+ */
+ public void changeGraphicSelection(final RectF rectangle) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.changeGraphicSelection(rectangle);
+ }
+ });
+ }
+
+ /**
+ * Show the handle (of input type) on the overlay.
+ */
+ public void showHandle(final SelectionHandle.HandleType type) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.showHandle(type);
+ }
+ });
+ }
+
+ /**
+ * Hide the handle (of input type).
+ */
+ public void hideHandle(final SelectionHandle.HandleType type) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.hideHandle(type);
+ }
+ });
+ }
+
+ /**
+ * Position the handle (of input type) position to the input rectangle.
+ */
+ public void positionHandle(final SelectionHandle.HandleType type, final RectF rectangle) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.positionHandle(type, rectangle);
+ }
+ });
+ }
+
+ public RectF getCurrentCursorPosition() {
+ return mDocumentOverlayView.getCurrentCursorPosition();
+ }
+
+ public void setCalcHeadersController(CalcHeadersController calcHeadersController) {
+ mDocumentOverlayView.setCalcHeadersController(calcHeadersController);
+ }
+
+ public void showCellSelection(final RectF cellCursorRect) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.showCellSelection(cellCursorRect);
+ }
+ });
+ }
+
+ public void showHeaderSelection(final RectF cellCursorRect) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ mDocumentOverlayView.showHeaderSelection(cellCursorRect);
+ }
+ });
+ }
+
+ public void showAdjustLengthLine(final boolean isRow, final CalcHeadersView view) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mDocumentOverlayView.showAdjustLengthLine(isRow, view);
+ }
+ });
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java b/android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java
new file mode 100644
index 0000000000..086108cd90
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java
@@ -0,0 +1,552 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.overlay;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.R;
+import org.libreoffice.canvas.AdjustLengthLine;
+import org.libreoffice.canvas.CalcSelectionBox;
+import org.libreoffice.canvas.Cursor;
+import org.libreoffice.canvas.GraphicSelection;
+import org.libreoffice.canvas.PageNumberRect;
+import org.libreoffice.canvas.SelectionHandle;
+import org.libreoffice.canvas.SelectionHandleEnd;
+import org.libreoffice.canvas.SelectionHandleMiddle;
+import org.libreoffice.canvas.SelectionHandleStart;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.RectUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Document overlay view is responsible for showing the client drawn overlay
+ * elements like cursor, selection and graphic selection, and manipulate them.
+ */
+public class DocumentOverlayView extends View implements View.OnTouchListener {
+ private static final String LOGTAG = DocumentOverlayView.class.getSimpleName();
+
+ private static final int CURSOR_BLINK_TIME = 500;
+
+ private boolean mInitialized = false;
+
+ private List<RectF> mSelections = new ArrayList<RectF>();
+ private List<RectF> mScaledSelections = new ArrayList<RectF>();
+ private Paint mSelectionPaint = new Paint();
+ private boolean mSelectionsVisible;
+
+ private GraphicSelection mGraphicSelection;
+
+ private boolean mGraphicSelectionMove = false;
+
+ private LayerView mLayerView;
+
+ private SelectionHandle mHandleMiddle;
+ private SelectionHandle mHandleStart;
+ private SelectionHandle mHandleEnd;
+
+ private Cursor mCursor;
+
+ private SelectionHandle mDragHandle = null;
+
+ private List<RectF> mPartPageRectangles;
+ private PageNumberRect mPageNumberRect;
+ private boolean mPageNumberAvailable = false;
+ private int previousIndex = 0; // previous page number, used to compare with the current
+ private CalcHeadersController mCalcHeadersController;
+
+ private CalcSelectionBox mCalcSelectionBox;
+ private boolean mCalcSelectionBoxDragging;
+ private AdjustLengthLine mAdjustLengthLine;
+ private boolean mAdjustLengthLineDragging;
+
+ public DocumentOverlayView(Context context) {
+ super(context);
+ }
+
+ public DocumentOverlayView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DocumentOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Initialize the selection and cursor view.
+ */
+ public void initialize(LayerView layerView) {
+ if (!mInitialized) {
+ setOnTouchListener(this);
+ mLayerView = layerView;
+
+ mCursor = new Cursor();
+ mCursor.setVisible(false);
+
+ mSelectionPaint.setColor(Color.BLUE);
+ mSelectionPaint.setAlpha(50);
+ mSelectionsVisible = false;
+
+ mGraphicSelection = new GraphicSelection((LibreOfficeMainActivity) getContext());
+ mGraphicSelection.setVisible(false);
+
+ postDelayed(cursorAnimation, CURSOR_BLINK_TIME);
+
+ mHandleMiddle = new SelectionHandleMiddle((LibreOfficeMainActivity) getContext());
+ mHandleStart = new SelectionHandleStart((LibreOfficeMainActivity) getContext());
+ mHandleEnd = new SelectionHandleEnd((LibreOfficeMainActivity) getContext());
+
+ mInitialized = true;
+ }
+ }
+
+ /**
+ * Change the cursor position.
+ * @param position - new position of the cursor
+ */
+ public void changeCursorPosition(RectF position) {
+ if (RectUtils.fuzzyEquals(mCursor.mPosition, position)) {
+ return;
+ }
+ mCursor.mPosition = position;
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ }
+
+ /**
+ * Change the text selection rectangles.
+ * @param selectionRects - list of text selection rectangles
+ */
+ public void changeSelections(List<RectF> selectionRects) {
+ mSelections = selectionRects;
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ }
+
+ /**
+ * Change the graphic selection rectangle.
+ * @param rectangle - new graphic selection rectangle
+ */
+ public void changeGraphicSelection(RectF rectangle) {
+ if (RectUtils.fuzzyEquals(mGraphicSelection.mRectangle, rectangle)) {
+ return;
+ }
+
+ mGraphicSelection.mRectangle = rectangle;
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ }
+
+ public void repositionWithViewport(float x, float y, float zoom) {
+ RectF rect = convertToScreen(mCursor.mPosition, x, y, zoom);
+ mCursor.reposition(rect);
+
+ rect = convertToScreen(mHandleMiddle.mDocumentPosition, x, y, zoom);
+ mHandleMiddle.reposition(rect.left, rect.bottom);
+
+ rect = convertToScreen(mHandleStart.mDocumentPosition, x, y, zoom);
+ mHandleStart.reposition(rect.left, rect.bottom);
+
+ rect = convertToScreen(mHandleEnd.mDocumentPosition, x, y, zoom);
+ mHandleEnd.reposition(rect.left, rect.bottom);
+
+ mScaledSelections.clear();
+ for (RectF selection : mSelections) {
+ RectF scaledSelection = convertToScreen(selection, x, y, zoom);
+ mScaledSelections.add(scaledSelection);
+ }
+
+ if (mCalcSelectionBox != null) {
+ rect = convertToScreen(mCalcSelectionBox.mDocumentPosition, x, y, zoom);
+ mCalcSelectionBox.reposition(rect);
+ }
+
+ if (mGraphicSelection != null && mGraphicSelection.mRectangle != null) {
+ RectF scaledGraphicSelection = convertToScreen(mGraphicSelection.mRectangle, x, y, zoom);
+ mGraphicSelection.reposition(scaledGraphicSelection);
+ }
+
+ invalidate();
+ }
+
+ /**
+ * Convert the input rectangle from document to screen coordinates
+ * according to current viewport data (x, y, zoom).
+ */
+ private static RectF convertToScreen(RectF inputRect, float x, float y, float zoom) {
+ RectF rect = RectUtils.scale(inputRect, zoom);
+ rect.offset(-x, -y);
+ return rect;
+ }
+
+ /**
+ * Set part page rectangles and initialize a page number rectangle object
+ * (canvas element).
+ */
+ public void setPartPageRectangles (List<RectF> rectangles) {
+ mPartPageRectangles = rectangles;
+ mPageNumberRect = new PageNumberRect();
+ mPageNumberAvailable = true;
+ }
+
+ /**
+ * Drawing on canvas.
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ mCursor.draw(canvas);
+
+ if (mPageNumberAvailable) {
+ mPageNumberRect.draw(canvas);
+ }
+
+ mHandleMiddle.draw(canvas);
+ mHandleStart.draw(canvas);
+ mHandleEnd.draw(canvas);
+
+ if (mSelectionsVisible) {
+ for (RectF selection : mScaledSelections) {
+ canvas.drawRect(selection, mSelectionPaint);
+ }
+ }
+
+ if (mCalcSelectionBox != null) {
+ mCalcSelectionBox.draw(canvas);
+ }
+
+ mGraphicSelection.draw(canvas);
+
+ if (mCalcHeadersController != null) {
+ mCalcHeadersController.showHeaders();
+ }
+
+ if (mAdjustLengthLine != null) {
+ mAdjustLengthLine.draw(canvas);
+ }
+ }
+
+ /**
+ * Cursor animation function. Switch the alpha between opaque and fully transparent.
+ */
+ private Runnable cursorAnimation = new Runnable() {
+ public void run() {
+ if (mCursor.isVisible()) {
+ mCursor.cycleAlpha();
+ invalidate();
+ }
+ postDelayed(cursorAnimation, CURSOR_BLINK_TIME);
+ }
+ };
+
+ /**
+ * Show the cursor on the view.
+ */
+ public void showCursor() {
+ if (!mCursor.isVisible()) {
+ mCursor.setVisible(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Hide the cursor.
+ */
+ public void hideCursor() {
+ if (mCursor.isVisible()) {
+ mCursor.setVisible(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Calculate and show page number according to current viewport position.
+ * In particular, this function compares the middle point of the
+ * view port with page rectangles and finds out which page the user
+ * is currently on. It does not update the associated canvas element
+ * unless there is a change of page number.
+ */
+ public void showPageNumberRect() {
+ if (null == mPartPageRectangles) return;
+ PointF midPoint = mLayerView.getLayerClient().convertViewPointToLayerPoint(new PointF(getWidth()/2f, getHeight()/2f));
+ int index = previousIndex;
+ // search which page the user in currently on. can enhance the search algorithm to binary search if necessary
+ for (RectF page : mPartPageRectangles) {
+ if (page.top < midPoint.y && midPoint.y < page.bottom) {
+ index = mPartPageRectangles.indexOf(page) + 1;
+ break;
+ }
+ }
+ // index == 0 applies to non-text document, i.e. don't show page info on non-text docs
+ if (index == 0) {
+ return;
+ }
+ // if page rectangle canvas element is not visible or the page number is changed, show
+ if (!mPageNumberRect.isVisible() || index != previousIndex) {
+ previousIndex = index;
+ String pageNumberString = getContext().getString(R.string.page) + " " + index + "/" + mPartPageRectangles.size();
+ mPageNumberRect.setPageNumberString(pageNumberString);
+ mPageNumberRect.setVisible(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Hide page number rectangle canvas element.
+ */
+ public void hidePageNumberRect() {
+ if (null == mPageNumberRect) return;
+ if (mPageNumberRect.isVisible()) {
+ mPageNumberRect.setVisible(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Show text selection rectangles.
+ */
+ public void showSelections() {
+ if (!mSelectionsVisible) {
+ mSelectionsVisible = true;
+ invalidate();
+ }
+ }
+
+ /**
+ * Hide text selection rectangles.
+ */
+ public void hideSelections() {
+ if (mSelectionsVisible) {
+ mSelectionsVisible = false;
+ invalidate();
+ }
+ }
+
+ /**
+ * Show the graphic selection on the view.
+ */
+ public void showGraphicSelection() {
+ if (!mGraphicSelection.isVisible()) {
+ mGraphicSelectionMove = false;
+ mGraphicSelection.reset();
+ mGraphicSelection.setVisible(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Hide the graphic selection.
+ */
+ public void hideGraphicSelection() {
+ if (mGraphicSelection.isVisible()) {
+ mGraphicSelection.setVisible(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Handle the triggered touch event.
+ */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ PointF point = new PointF(event.getX(), event.getY());
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ if (mAdjustLengthLine != null && !mAdjustLengthLine.contains(point.x, point.y)) {
+ mAdjustLengthLine.setVisible(false);
+ invalidate();
+ }
+ if (mGraphicSelection.isVisible()) {
+ // Check if inside graphic selection was hit
+ if (mGraphicSelection.contains(point.x, point.y)) {
+ mGraphicSelectionMove = true;
+ mGraphicSelection.dragStart(point);
+ invalidate();
+ return true;
+ }
+ } else {
+ if (mHandleStart.contains(point.x, point.y)) {
+ mHandleStart.dragStart(point);
+ mDragHandle = mHandleStart;
+ return true;
+ } else if (mHandleEnd.contains(point.x, point.y)) {
+ mHandleEnd.dragStart(point);
+ mDragHandle = mHandleEnd;
+ return true;
+ } else if (mHandleMiddle.contains(point.x, point.y)) {
+ mHandleMiddle.dragStart(point);
+ mDragHandle = mHandleMiddle;
+ return true;
+ } else if (mCalcSelectionBox != null &&
+ mCalcSelectionBox.contains(point.x, point.y) &&
+ !mHandleStart.isVisible()) {
+ mCalcSelectionBox.dragStart(point);
+ mCalcSelectionBoxDragging = true;
+ return true;
+ } else if (mAdjustLengthLine != null &&
+ mAdjustLengthLine.contains(point.x, point.y)) {
+ mAdjustLengthLine.dragStart(point);
+ mAdjustLengthLineDragging = true;
+ return true;
+ }
+ }
+ }
+ case MotionEvent.ACTION_UP: {
+ if (mGraphicSelection.isVisible() && mGraphicSelectionMove) {
+ mGraphicSelection.dragEnd(point);
+ mGraphicSelectionMove = false;
+ invalidate();
+ return true;
+ } else if (mDragHandle != null) {
+ mDragHandle.dragEnd(point);
+ mDragHandle = null;
+ } else if (mCalcSelectionBoxDragging) {
+ mCalcSelectionBox.dragEnd(point);
+ mCalcSelectionBoxDragging = false;
+ } else if (mAdjustLengthLineDragging) {
+ mAdjustLengthLine.dragEnd(point);
+ mAdjustLengthLineDragging = false;
+ invalidate();
+ }
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (mGraphicSelection.isVisible() && mGraphicSelectionMove) {
+ mGraphicSelection.dragging(point);
+ invalidate();
+ return true;
+ } else if (mDragHandle != null) {
+ mDragHandle.dragging(point);
+ } else if (mCalcSelectionBoxDragging) {
+ mCalcSelectionBox.dragging(point);
+ } else if (mAdjustLengthLineDragging) {
+ mAdjustLengthLine.dragging(point);
+ invalidate();
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Change the handle document position.
+ * @param type - the type of the handle
+ * @param position - the new document position
+ */
+ public void positionHandle(SelectionHandle.HandleType type, RectF position) {
+ SelectionHandle handle = getHandleForType(type);
+ if (RectUtils.fuzzyEquals(handle.mDocumentPosition, position)) {
+ return;
+ }
+
+ RectUtils.assign(handle.mDocumentPosition, position);
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ }
+
+ /**
+ * Hide the handle.
+ * @param type - type of the handle
+ */
+ public void hideHandle(SelectionHandle.HandleType type) {
+ SelectionHandle handle = getHandleForType(type);
+ if (handle.isVisible()) {
+ handle.setVisible(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Show the handle.
+ * @param type - type of the handle
+ */
+ public void showHandle(SelectionHandle.HandleType type) {
+ SelectionHandle handle = getHandleForType(type);
+ if (!handle.isVisible()) {
+ handle.setVisible(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns the handle instance for the input type.
+ */
+ private SelectionHandle getHandleForType(SelectionHandle.HandleType type) {
+ switch(type) {
+ case START:
+ return mHandleStart;
+ case END:
+ return mHandleEnd;
+ case MIDDLE:
+ return mHandleMiddle;
+ }
+ return null;
+ }
+
+ public RectF getCurrentCursorPosition() {
+ return mCursor.mPosition;
+ }
+
+ public void setCalcHeadersController(CalcHeadersController calcHeadersController) {
+ mCalcHeadersController = calcHeadersController;
+ mCalcSelectionBox = new CalcSelectionBox((LibreOfficeMainActivity) getContext());
+ }
+
+ public void showCellSelection(RectF cellCursorRect) {
+ if (mCalcHeadersController == null || mCalcSelectionBox == null) return;
+ if (RectUtils.fuzzyEquals(mCalcSelectionBox.mDocumentPosition, cellCursorRect)) {
+ return;
+ }
+
+ // show selection on main GL view (i.e. in the document)
+ RectUtils.assign(mCalcSelectionBox.mDocumentPosition, cellCursorRect);
+ mCalcSelectionBox.setVisible(true);
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+
+ // show selection on headers
+ if (!mCalcHeadersController.pendingRowOrColumnSelectionToShowUp()) {
+ showHeaderSelection(cellCursorRect);
+ } else {
+ mCalcHeadersController.setPendingRowOrColumnSelectionToShowUp(false);
+ }
+ }
+
+ public void showHeaderSelection(RectF rect) {
+ if (mCalcHeadersController == null) return;
+ mCalcHeadersController.showHeaderSelection(rect);
+ }
+
+ public void showAdjustLengthLine(boolean isRow, final CalcHeadersView view) {
+ mAdjustLengthLine = new AdjustLengthLine((LibreOfficeMainActivity) getContext(), view, isRow, getWidth(), getHeight());
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ RectF position = convertToScreen(mCalcSelectionBox.mDocumentPosition, metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ mAdjustLengthLine.setScreenRect(position);
+ mAdjustLengthLine.setVisible(true);
+ invalidate();
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/FileUtilities.java b/android/source/src/java/org/libreoffice/ui/FileUtilities.java
new file mode 100644
index 0000000000..7fc8c3c84e
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/FileUtilities.java
@@ -0,0 +1,158 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.ui;
+
+import java.util.Map;
+import java.util.HashMap;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+import android.util.Log;
+
+public class FileUtilities {
+
+ private static final String LOGTAG = FileUtilities.class.getSimpleName();
+
+ // These have to be in sync with the file_view_modes resource.
+ static final int DOC = 0;
+ static final int CALC = 1;
+ static final int IMPRESS = 2;
+ static final int DRAWING = 3;
+
+ static final int UNKNOWN = 10;
+
+ public static final String MIMETYPE_OPENDOCUMENT_TEXT = "application/vnd.oasis.opendocument.text";
+ public static final String MIMETYPE_OPENDOCUMENT_SPREADSHEET = "application/vnd.oasis.opendocument.spreadsheet";
+ public static final String MIMETYPE_OPENDOCUMENT_PRESENTATION = "application/vnd.oasis.opendocument.presentation";
+ public static final String MIMETYPE_OPENDOCUMENT_GRAPHICS = "application/vnd.oasis.opendocument.graphics";
+ public static final String MIMETYPE_PDF = "application/pdf";
+
+ private static final Map<String, Integer> mExtnMap = new HashMap<String, Integer>();
+ static {
+ // Please keep this in sync with AndroidManifest.xml
+ // and 'SUPPORTED_MIME_TYPES' in LibreOfficeUIActivity.java
+
+ // ODF
+ mExtnMap.put(".odt", DOC);
+ mExtnMap.put(".odg", DRAWING);
+ mExtnMap.put(".odp", IMPRESS);
+ mExtnMap.put(".ods", CALC);
+ mExtnMap.put(".fodt", DOC);
+ mExtnMap.put(".fodg", DRAWING);
+ mExtnMap.put(".fodp", IMPRESS);
+ mExtnMap.put(".fods", CALC);
+
+ // ODF templates
+ mExtnMap.put(".ott", DOC);
+ mExtnMap.put(".otg", DRAWING);
+ mExtnMap.put(".otp", IMPRESS);
+ mExtnMap.put(".ots", CALC);
+
+ // MS
+ mExtnMap.put(".rtf", DOC);
+ mExtnMap.put(".doc", DOC);
+ mExtnMap.put(".vsd", DRAWING);
+ mExtnMap.put(".vsdx", DRAWING);
+ mExtnMap.put(".pub", DRAWING);
+ mExtnMap.put(".ppt", IMPRESS);
+ mExtnMap.put(".pps", IMPRESS);
+ mExtnMap.put(".xls", CALC);
+
+ // MS templates
+ mExtnMap.put(".dot", DOC);
+ mExtnMap.put(".pot", IMPRESS);
+ mExtnMap.put(".xlt", CALC);
+
+ // OOXML
+ mExtnMap.put(".docx", DOC);
+ mExtnMap.put(".pptx", IMPRESS);
+ mExtnMap.put(".ppsx", IMPRESS);
+ mExtnMap.put(".xlsx", CALC);
+
+ // OOXML templates
+ mExtnMap.put(".dotx", DOC);
+ mExtnMap.put(".potx", IMPRESS);
+ mExtnMap.put(".xltx", CALC);
+
+ // Other
+ mExtnMap.put(".csv", CALC);
+ mExtnMap.put(".wps", DOC);
+ mExtnMap.put(".key", IMPRESS);
+ mExtnMap.put(".abw", DOC);
+ mExtnMap.put(".pmd", DRAWING);
+ mExtnMap.put(".emf", DRAWING);
+ mExtnMap.put(".svm", DRAWING);
+ mExtnMap.put(".wmf", DRAWING);
+ mExtnMap.put(".svg", DRAWING);
+ }
+
+ public static String getExtension(String filename) {
+ if (filename == null)
+ return "";
+ int nExt = filename.lastIndexOf('.');
+ if (nExt < 0)
+ return "";
+ return filename.substring(nExt);
+ }
+
+ private static int lookupExtension(String filename) {
+ String extn = getExtension(filename);
+ if (!mExtnMap.containsKey(extn))
+ return UNKNOWN;
+ return mExtnMap.get(extn);
+ }
+
+ static int getType(String filename) {
+ int type = lookupExtension (filename);
+ Log.d(LOGTAG, "extn : " + filename + " -> " + type);
+ return type;
+ }
+
+ /**
+ * Returns whether the passed MIME type is one for a document template.
+ */
+ public static boolean isTemplateMimeType(final String mimeType) {
+ // this works for ODF and OOXML template MIME types
+ return mimeType != null && mimeType.endsWith("template");
+ }
+
+ public static String stripExtensionFromFileName(final String fileName)
+ {
+ return fileName.split("\\.[A-Za-z0-9]*$")[0];
+ }
+
+ /**
+ * Tries to retrieve the display (which should be the document name)
+ * for the given URI using the given resolver.
+ */
+ public static String retrieveDisplayNameForDocumentUri(ContentResolver resolver, Uri docUri) {
+ String displayName = "";
+ // try to retrieve original file name
+ Cursor cursor = null;
+ try {
+ String[] columns = {OpenableColumns.DISPLAY_NAME};
+ cursor = resolver.query(docUri, columns, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ displayName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
+ }
+ } catch (SecurityException e) {
+ // thrown e.g. when Uri has become invalid, e.g. corresponding file has been deleted
+ Log.i(LOGTAG, "SecurityException when trying to receive display name for Uri " + docUri);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return displayName;
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java b/android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java
new file mode 100644
index 0000000000..bc5203d9c6
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java
@@ -0,0 +1,457 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.ui;
+
+import android.Manifest;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.view.ViewCompat;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.appcompat.widget.Toolbar;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.OvershootInterpolator;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.libreoffice.AboutDialogFragment;
+import org.libreoffice.BuildConfig;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.LocaleHelper;
+import org.libreoffice.R;
+import org.libreoffice.SettingsActivity;
+import org.libreoffice.SettingsListenerModel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class LibreOfficeUIActivity extends AppCompatActivity implements SettingsListenerModel.OnSettingsPreferenceChangedListener, View.OnClickListener{
+ public enum DocumentType {
+ WRITER,
+ CALC,
+ IMPRESS,
+ DRAW,
+ INVALID
+ }
+
+ private static final String LOGTAG = LibreOfficeUIActivity.class.getSimpleName();
+
+ public static final String EXPLORER_PREFS_KEY = "EXPLORER_PREFS";
+ private static final String RECENT_DOCUMENTS_KEY = "RECENT_DOCUMENT_URIS";
+ // delimiter used for storing multiple URIs in a string
+ private static final String RECENT_DOCUMENTS_DELIMITER = " ";
+ private static final String DISPLAY_LANGUAGE = "DISPLAY_LANGUAGE";
+
+ public static final String NEW_DOC_TYPE_KEY = "NEW_DOC_TYPE_KEY";
+ public static final String NEW_WRITER_STRING_KEY = "private:factory/swriter";
+ public static final String NEW_IMPRESS_STRING_KEY = "private:factory/simpress";
+ public static final String NEW_CALC_STRING_KEY = "private:factory/scalc";
+ public static final String NEW_DRAW_STRING_KEY = "private:factory/sdraw";
+
+ // keep this in sync with 'AndroidManifext.xml'
+ private static final String[] SUPPORTED_MIME_TYPES = {
+ "application/vnd.oasis.opendocument.text",
+ "application/vnd.oasis.opendocument.graphics",
+ "application/vnd.oasis.opendocument.presentation",
+ "application/vnd.oasis.opendocument.spreadsheet",
+ "application/vnd.oasis.opendocument.text-flat-xml",
+ "application/vnd.oasis.opendocument.graphics-flat-xml",
+ "application/vnd.oasis.opendocument.presentation-flat-xml",
+ "application/vnd.oasis.opendocument.spreadsheet-flat-xml",
+ "application/vnd.oasis.opendocument.text-template",
+ "application/vnd.oasis.opendocument.spreadsheet-template",
+ "application/vnd.oasis.opendocument.graphics-template",
+ "application/vnd.oasis.opendocument.presentation-template",
+ "application/rtf",
+ "text/rtf",
+ "application/msword",
+ "application/vnd.ms-powerpoint",
+ "application/vnd.ms-excel",
+ "application/vnd.visio",
+ "application/vnd.visio.xml",
+ "application/x-mspublisher",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+ "application/vnd.openxmlformats-officedocument.presentationml.template",
+ "text/csv",
+ "text/comma-separated-values",
+ "application/vnd.ms-works",
+ "application/vnd.apple.keynote",
+ "application/x-abiword",
+ "application/x-pagemaker",
+ "image/x-emf",
+ "image/x-svm",
+ "image/x-wmf",
+ "image/svg+xml",
+ };
+
+ private static final int REQUEST_CODE_OPEN_FILECHOOSER = 12345;
+
+ private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 0;
+
+ private Animation fabOpenAnimation;
+ private Animation fabCloseAnimation;
+ private boolean isFabMenuOpen = false;
+ private FloatingActionButton editFAB;
+ private FloatingActionButton writerFAB;
+ private FloatingActionButton drawFAB;
+ private FloatingActionButton impressFAB;
+ private FloatingActionButton calcFAB;
+ private LinearLayout drawLayout;
+ private LinearLayout writerLayout;
+ private LinearLayout impressLayout;
+ private LinearLayout calcLayout;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ readPreferences();
+ SettingsListenerModel.getInstance().setListener(this);
+
+ // init UI
+ createUI();
+ fabOpenAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_open);
+ fabCloseAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_close);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ Log.i(LOGTAG, "no permission to read external storage - asking for permission");
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ PERMISSION_WRITE_EXTERNAL_STORAGE);
+ }
+ }
+
+ @Override
+ protected void attachBaseContext(Context newBase) {
+ super.attachBaseContext(LocaleHelper.onAttach(newBase));
+ }
+
+ public void createUI() {
+ setContentView(R.layout.activity_document_browser);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+
+ if (actionBar != null) {
+ actionBar.setIcon(R.mipmap.ic_launcher);
+ }
+
+ editFAB = findViewById(R.id.editFAB);
+ editFAB.setOnClickListener(this);
+ // allow creating new docs only when experimental editing is enabled
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final boolean bEditingEnabled = BuildConfig.ALLOW_EDITING && preferences.getBoolean(LibreOfficeMainActivity.ENABLE_EXPERIMENTAL_PREFS_KEY, false);
+ editFAB.setVisibility(bEditingEnabled ? View.VISIBLE : View.INVISIBLE);
+
+ impressFAB = findViewById(R.id.newImpressFAB);
+ impressFAB.setOnClickListener(this);
+ writerFAB = findViewById(R.id.newWriterFAB);
+ writerFAB.setOnClickListener(this);
+ calcFAB = findViewById(R.id.newCalcFAB);
+ calcFAB.setOnClickListener(this);
+ drawFAB = findViewById(R.id.newDrawFAB);
+ drawFAB.setOnClickListener(this);
+ writerLayout = findViewById(R.id.writerLayout);
+ impressLayout = findViewById(R.id.impressLayout);
+ calcLayout = findViewById(R.id.calcLayout);
+ drawLayout = findViewById(R.id.drawLayout);
+ TextView openFileView = findViewById(R.id.open_file_button);
+ openFileView.setOnClickListener(this);
+
+
+ RecyclerView recentRecyclerView = findViewById(R.id.list_recent);
+
+ SharedPreferences prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
+ String recentPref = prefs.getString(RECENT_DOCUMENTS_KEY, "");
+ String[] recentFileStrings = recentPref.split(RECENT_DOCUMENTS_DELIMITER);
+
+ final List<RecentFile> recentFiles = new ArrayList<>();
+ for (String recentFileString : recentFileStrings) {
+ Uri uri = Uri.parse(recentFileString);
+ String filename = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), uri);
+ if (!filename.isEmpty()) {
+ recentFiles.add(new RecentFile(uri, filename));
+ }
+ }
+
+ recentRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
+ recentRecyclerView.setAdapter(new RecentFilesAdapter(this, recentFiles));
+ }
+
+ private void expandFabMenu() {
+ ViewCompat.animate(editFAB).rotation(45.0F).withLayer().setDuration(300).setInterpolator(new OvershootInterpolator(10.0F)).start();
+ drawLayout.startAnimation(fabOpenAnimation);
+ impressLayout.startAnimation(fabOpenAnimation);
+ writerLayout.startAnimation(fabOpenAnimation);
+ calcLayout.startAnimation(fabOpenAnimation);
+ writerFAB.setClickable(true);
+ impressFAB.setClickable(true);
+ drawFAB.setClickable(true);
+ calcFAB.setClickable(true);
+ isFabMenuOpen = true;
+ }
+
+ private void collapseFabMenu() {
+ ViewCompat.animate(editFAB).rotation(0.0F).withLayer().setDuration(300).setInterpolator(new OvershootInterpolator(10.0F)).start();
+ writerLayout.startAnimation(fabCloseAnimation);
+ impressLayout.startAnimation(fabCloseAnimation);
+ drawLayout.startAnimation(fabCloseAnimation);
+ calcLayout.startAnimation(fabCloseAnimation);
+ writerFAB.setClickable(false);
+ impressFAB.setClickable(false);
+ drawFAB.setClickable(false);
+ calcFAB.setClickable(false);
+ isFabMenuOpen = false;
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (isFabMenuOpen) {
+ collapseFabMenu();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_CODE_OPEN_FILECHOOSER && resultCode == RESULT_OK) {
+ final Uri fileUri = data.getData();
+ openDocument(fileUri);
+ }
+ }
+
+ private void showSystemFilePickerAndOpenFile() {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, SUPPORTED_MIME_TYPES);
+
+ try {
+ startActivityForResult(intent, REQUEST_CODE_OPEN_FILECHOOSER);
+ } catch (ActivityNotFoundException e) {
+ Log.w(LOGTAG, "No activity available that can handle the intent to open a document.");
+ }
+ }
+
+ public void openDocument(final Uri documentUri) {
+ // "forward" to LibreOfficeMainActivity to open the file
+ Intent intent = new Intent(Intent.ACTION_VIEW, documentUri);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+ addDocumentToRecents(documentUri);
+
+ String packageName = getApplicationContext().getPackageName();
+ ComponentName componentName = new ComponentName(packageName,
+ LibreOfficeMainActivity.class.getName());
+ intent.setComponent(componentName);
+ startActivity(intent);
+ }
+
+ private void loadNewDocument(DocumentType docType) {
+ final String newDocumentType;
+ if (docType == DocumentType.WRITER) {
+ newDocumentType = NEW_WRITER_STRING_KEY;
+ } else if (docType == DocumentType.CALC) {
+ newDocumentType = NEW_CALC_STRING_KEY;
+ } else if (docType == DocumentType.IMPRESS) {
+ newDocumentType = NEW_IMPRESS_STRING_KEY;
+ } else if (docType == DocumentType.DRAW) {
+ newDocumentType = NEW_DRAW_STRING_KEY;
+ } else {
+ Log.w(LOGTAG, "invalid document type passed to loadNewDocument method. Ignoring request");
+ return;
+ }
+
+ Intent intent = new Intent(LibreOfficeUIActivity.this, LibreOfficeMainActivity.class);
+ intent.putExtra(NEW_DOC_TYPE_KEY, newDocumentType);
+ startActivity(intent);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.view_menu, menu);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.action_about) {
+ AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
+ aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
+ return true;
+ }
+ if (itemId == R.id.action_settings) {
+ startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ public void readPreferences(){
+ SharedPreferences defaultPrefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
+ final String displayLanguage = defaultPrefs.getString(DISPLAY_LANGUAGE, LocaleHelper.SYSTEM_DEFAULT_LANGUAGE);
+ LocaleHelper.setLocale(this, displayLanguage);
+ }
+
+ @Override
+ public void settingsPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ readPreferences();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(LOGTAG, "onResume");
+ createUI();
+ }
+
+ private void addDocumentToRecents(Uri fileUri) {
+ SharedPreferences prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
+
+ // preserve permissions across device reboots,
+ // s. https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions
+ getContentResolver().takePersistableUriPermission(fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+ String newRecent = fileUri.toString();
+ List<String> recentsList = new ArrayList<>(Arrays.asList(prefs.getString(RECENT_DOCUMENTS_KEY, "").split(RECENT_DOCUMENTS_DELIMITER)));
+
+ // remove string if present, so that it doesn't appear multiple times
+ recentsList.remove(newRecent);
+
+ // put the new value in the first place
+ recentsList.add(0, newRecent);
+
+ /*
+ * 4 because the number of recommended items in App Shortcuts is 4, and also
+ * because it's a good number of recent items in general
+ */
+ final int RECENTS_SIZE = 4;
+
+ while (recentsList.size() > RECENTS_SIZE) {
+ recentsList.remove(RECENTS_SIZE);
+ }
+
+ // serialize to String that can be set for pref
+ String value = TextUtils.join(RECENT_DOCUMENTS_DELIMITER, recentsList);
+ prefs.edit().putString(RECENT_DOCUMENTS_KEY, value).apply();
+
+ //update app shortcuts (7.0 and above)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
+ ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
+
+ //Remove all shortcuts, and apply new ones.
+ shortcutManager.removeAllDynamicShortcuts();
+
+ ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
+ for (String recentDoc : recentsList) {
+ Uri docUri = Uri.parse(recentDoc);
+ String filename = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), docUri);
+ if (filename.isEmpty()) {
+ continue;
+ }
+
+ //find the appropriate drawable
+ int drawable = 0;
+ switch (FileUtilities.getType(filename)) {
+ case FileUtilities.DOC:
+ drawable = R.drawable.writer;
+ break;
+ case FileUtilities.CALC:
+ drawable = R.drawable.calc;
+ break;
+ case FileUtilities.DRAWING:
+ drawable = R.drawable.draw;
+ break;
+ case FileUtilities.IMPRESS:
+ drawable = R.drawable.impress;
+ break;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, docUri);
+ String packageName = this.getApplicationContext().getPackageName();
+ ComponentName componentName = new ComponentName(packageName, LibreOfficeMainActivity.class.getName());
+ intent.setComponent(componentName);
+
+ ShortcutInfo shortcut = new ShortcutInfo.Builder(this, filename)
+ .setShortLabel(filename)
+ .setLongLabel(filename)
+ .setIcon(Icon.createWithResource(this, drawable))
+ .setIntent(intent)
+ .build();
+
+ shortcuts.add(shortcut);
+ }
+ shortcutManager.setDynamicShortcuts(shortcuts);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == R.id.editFAB) {
+ if (isFabMenuOpen) {
+ collapseFabMenu();
+ } else {
+ expandFabMenu();
+ }
+ } else if (id == R.id.open_file_button) {
+ showSystemFilePickerAndOpenFile();
+ } else if (id == R.id.newWriterFAB) {
+ loadNewDocument(DocumentType.WRITER);
+ } else if (id == R.id.newImpressFAB) {
+ loadNewDocument(DocumentType.IMPRESS);
+ } else if (id == R.id.newCalcFAB) {
+ loadNewDocument(DocumentType.CALC);
+ } else if (id == R.id.newDrawFAB) {
+ loadNewDocument(DocumentType.DRAW);
+ }
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/PageView.java b/android/source/src/java/org/libreoffice/ui/PageView.java
new file mode 100644
index 0000000000..4c3f695622
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/PageView.java
@@ -0,0 +1,69 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+
+package org.libreoffice.ui;
+
+import org.libreoffice.R;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+public class PageView extends View{
+ private Bitmap bmp;
+ private Paint mPaintBlack;
+ private static final String LOGTAG = "PageView";
+
+ public PageView(Context context ) {
+ super(context);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);
+ initialise();
+ }
+ public PageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);
+ Log.d(LOGTAG, bmp.toString());
+ initialise();
+ }
+ public PageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);//load a "page"
+ initialise();
+ }
+
+ private void initialise(){
+ mPaintBlack = new Paint();
+ mPaintBlack.setARGB(255, 0, 0, 0);
+ Log.d(LOGTAG, " Doing some set-up");
+ }
+
+ public void setBitmap(Bitmap bmp){
+ this.bmp = bmp;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Log.d(LOGTAG, "Draw");
+ Log.d(LOGTAG, Integer.toString(bmp.getHeight()));
+ if( bmp != null ){
+ int horizontalMargin = (int) (canvas.getWidth()*0.1);
+ //int verticalMargin = (int) (canvas.getHeight()*0.1);
+ int verticalMargin = horizontalMargin;
+ canvas.drawBitmap(bmp, new Rect(0, 0, bmp.getWidth(), bmp.getHeight()),
+ new Rect(horizontalMargin,verticalMargin,canvas.getWidth()-horizontalMargin,
+ canvas.getHeight()-verticalMargin),
+ mPaintBlack);//
+ }
+ if( bmp == null)
+ canvas.drawText(getContext().getString(R.string.bmp_null), 100, 100, new Paint());
+ }
+
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/RecentFile.java b/android/source/src/java/org/libreoffice/ui/RecentFile.java
new file mode 100644
index 0000000000..fdcc688aa1
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/RecentFile.java
@@ -0,0 +1,25 @@
+package org.libreoffice.ui;
+
+import android.net.Uri;
+
+/**
+ * An entry for a recently used file in the RecentFilesAdapter.
+ */
+public class RecentFile {
+
+ private final Uri uri;
+ private final String displayName;
+
+ public RecentFile(Uri docUri, String name) {
+ uri = docUri;
+ displayName = name;
+ }
+
+ public Uri getUri() {
+ return uri;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java b/android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java
new file mode 100644
index 0000000000..ef00b9fb6c
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java
@@ -0,0 +1,93 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.libreoffice.ui;
+
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.libreoffice.R;
+
+import java.util.List;
+
+class RecentFilesAdapter extends RecyclerView.Adapter<RecentFilesAdapter.ViewHolder> {
+
+ private final LibreOfficeUIActivity mActivity;
+ private final List<RecentFile> recentFiles;
+
+ RecentFilesAdapter(LibreOfficeUIActivity activity, List<RecentFile> recentFiles) {
+ this.mActivity = activity;
+ this.recentFiles = recentFiles;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View item = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_recent_files, parent, false);
+ return new ViewHolder(item);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ final RecentFile entry = recentFiles.get(position);
+
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mActivity.openDocument(entry.getUri());
+ }
+ });
+
+ final String filename = entry.getDisplayName();
+ holder.textView.setText(filename);
+
+ int compoundDrawableInt = 0;
+
+ switch (FileUtilities.getType(filename)) {
+ case FileUtilities.DOC:
+ compoundDrawableInt = R.drawable.writer;
+ break;
+ case FileUtilities.CALC:
+ compoundDrawableInt = R.drawable.calc;
+ break;
+ case FileUtilities.DRAWING:
+ compoundDrawableInt = R.drawable.draw;
+ break;
+ case FileUtilities.IMPRESS:
+ compoundDrawableInt = R.drawable.impress;
+ break;
+ }
+
+ // set icon if known filetype was detected
+ if (compoundDrawableInt != 0)
+ holder.imageView.setImageDrawable(ContextCompat.getDrawable(mActivity, compoundDrawableInt));
+ }
+
+ @Override
+ public int getItemCount() {
+ return recentFiles.size();
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+
+ TextView textView;
+ ImageView imageView;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ this.textView = itemView.findViewById(R.id.textView);
+ this.imageView = itemView.findViewById(R.id.imageView);
+ }
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/OnInterceptTouchListener.java b/android/source/src/java/org/mozilla/gecko/OnInterceptTouchListener.java
new file mode 100644
index 0000000000..d0cd3d48a9
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/OnInterceptTouchListener.java
@@ -0,0 +1,14 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface OnInterceptTouchListener extends View.OnTouchListener {
+ /** Override this method for a chance to consume events before the view or its children */
+ public boolean onInterceptTouchEvent(View view, MotionEvent event);
+}
diff --git a/android/source/src/java/org/mozilla/gecko/OnSlideSwipeListener.java b/android/source/src/java/org/mozilla/gecko/OnSlideSwipeListener.java
new file mode 100644
index 0000000000..29f50ebf49
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/OnSlideSwipeListener.java
@@ -0,0 +1,94 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * 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 org.mozilla.gecko;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.util.Log;
+
+import org.libreoffice.LOKitShell;
+import org.mozilla.gecko.gfx.GeckoLayerClient;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+
+
+public class OnSlideSwipeListener implements OnTouchListener {
+ private static String LOGTAG = OnSlideSwipeListener.class.getName();
+
+ private final GestureDetector mGestureDetector;
+ private GeckoLayerClient mLayerClient;
+
+ public OnSlideSwipeListener(Context ctx, GeckoLayerClient client){
+ mGestureDetector = new GestureDetector(ctx, new GestureListener());
+ mLayerClient = client;
+ }
+
+ private final class GestureListener extends SimpleOnGestureListener {
+
+ private static final int SWIPE_THRESHOLD = 100;
+ private static final int SWIPE_VELOCITY_THRESHOLD = 100;
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
+ // Check if the page is already zoomed-in.
+ // Disable swiping gesture if that's the case.
+ ImmutableViewportMetrics viewportMetrics = mLayerClient.getViewportMetrics();
+ if (viewportMetrics.viewportRectLeft > viewportMetrics.pageRectLeft ||
+ viewportMetrics.viewportRectRight < viewportMetrics.pageRectRight) {
+ return false;
+ }
+
+ // Otherwise, the page is smaller than viewport, perform swipe
+ // gesture.
+ try {
+ float diffY = e2.getY() - e1.getY();
+ float diffX = e2.getX() - e1.getX();
+ if (Math.abs(diffX) > Math.abs(diffY)) {
+ if (Math.abs(diffX) > SWIPE_THRESHOLD
+ && Math.abs(velX) > SWIPE_VELOCITY_THRESHOLD) {
+ if (diffX > 0) {
+ onSwipeRight();
+ } else {
+ onSwipeLeft();
+ }
+ }
+ }
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ }
+ return false;
+ }
+ }
+
+ public void onSwipeRight() {
+ Log.d(LOGTAG, "onSwipeRight");
+ LOKitShell.sendSwipeRightEvent();
+ }
+
+ public void onSwipeLeft() {
+ Log.d(LOGTAG, "onSwipeLeft");
+ LOKitShell.sendSwipeLeftEvent();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent me) {
+ return mGestureDetector.onTouchEvent(me);
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/mozilla/gecko/ZoomConstraints.java b/android/source/src/java/org/mozilla/gecko/ZoomConstraints.java
new file mode 100644
index 0000000000..dbe2788272
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/ZoomConstraints.java
@@ -0,0 +1,30 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko;
+
+public final class ZoomConstraints {
+ private final float mDefaultZoom;
+ private final float mMinZoom;
+ private final float mMaxZoom;
+
+ public ZoomConstraints(float defaultZoom, float minZoom, float maxZoom) {
+ mDefaultZoom = defaultZoom;
+ mMinZoom = minZoom;
+ mMaxZoom = maxZoom;
+ }
+
+ public final float getDefaultZoom() {
+ return mDefaultZoom;
+ }
+
+ public final float getMinZoom() {
+ return mMinZoom;
+ }
+
+ public final float getMaxZoom() {
+ return mMaxZoom;
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/Axis.java b/android/source/src/java/org/mozilla/gecko/gfx/Axis.java
new file mode 100644
index 0000000000..d4a7ac2ce5
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/Axis.java
@@ -0,0 +1,337 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.util.Log;
+import android.view.View;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import java.util.Map;
+
+/**
+ * This class represents the physics for one axis of movement (i.e. either
+ * horizontal or vertical). It tracks the different properties of movement
+ * like displacement, velocity, viewport dimensions, etc. pertaining to
+ * a particular axis.
+ */
+abstract class Axis {
+ private static final String LOGTAG = "GeckoAxis";
+
+ private static final String PREF_SCROLLING_FRICTION_SLOW = "ui.scrolling.friction_slow";
+ private static final String PREF_SCROLLING_FRICTION_FAST = "ui.scrolling.friction_fast";
+ private static final String PREF_SCROLLING_MAX_EVENT_ACCELERATION = "ui.scrolling.max_event_acceleration";
+ private static final String PREF_SCROLLING_OVERSCROLL_DECEL_RATE = "ui.scrolling.overscroll_decel_rate";
+ private static final String PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT = "ui.scrolling.overscroll_snap_limit";
+ private static final String PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE = "ui.scrolling.min_scrollable_distance";
+
+ // This fraction of velocity remains after every animation frame when the velocity is low.
+ private static float FRICTION_SLOW;
+ // This fraction of velocity remains after every animation frame when the velocity is high.
+ private static float FRICTION_FAST;
+ // Below this velocity (in pixels per frame), the friction starts increasing from FRICTION_FAST
+ // to FRICTION_SLOW.
+ private static float VELOCITY_THRESHOLD;
+ // The maximum velocity change factor between events, per ms, in %.
+ // Direction changes are excluded.
+ private static float MAX_EVENT_ACCELERATION;
+
+ // The rate of deceleration when the surface has overscrolled.
+ private static float OVERSCROLL_DECEL_RATE;
+ // The percentage of the surface which can be overscrolled before it must snap back.
+ private static float SNAP_LIMIT;
+
+ // The minimum amount of space that must be present for an axis to be considered scrollable,
+ // in pixels.
+ private static float MIN_SCROLLABLE_DISTANCE;
+
+ private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
+ Integer value = (prefs == null ? null : prefs.get(prefName));
+ return (float)(value == null || value < 0 ? defaultValue : value) / 1000f;
+ }
+
+ private static int getIntPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
+ Integer value = (prefs == null ? null : prefs.get(prefName));
+ return (value == null || value < 0 ? defaultValue : value);
+ }
+
+ static final float MS_PER_FRAME = 4.0f;
+ private static final float FRAMERATE_MULTIPLIER = (1000f/60f) / MS_PER_FRAME;
+
+ // The values we use for friction are based on a 16.6ms frame, adjust them to MS_PER_FRAME:
+ // FRICTION^1 = FRICTION_ADJUSTED^(16/MS_PER_FRAME)
+ // FRICTION_ADJUSTED = e ^ ((ln(FRICTION))/FRAMERATE_MULTIPLIER)
+ static float getFrameAdjustedFriction(float baseFriction) {
+ return (float)Math.pow(Math.E, (Math.log(baseFriction) / FRAMERATE_MULTIPLIER));
+ }
+
+ static void setPrefs(Map<String, Integer> prefs) {
+ FRICTION_SLOW = getFrameAdjustedFriction(getFloatPref(prefs, PREF_SCROLLING_FRICTION_SLOW, 850));
+ FRICTION_FAST = getFrameAdjustedFriction(getFloatPref(prefs, PREF_SCROLLING_FRICTION_FAST, 970));
+ VELOCITY_THRESHOLD = 10 / FRAMERATE_MULTIPLIER;
+ MAX_EVENT_ACCELERATION = getFloatPref(prefs, PREF_SCROLLING_MAX_EVENT_ACCELERATION, 12);
+ OVERSCROLL_DECEL_RATE = getFrameAdjustedFriction(getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_DECEL_RATE, 40));
+ SNAP_LIMIT = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT, 300);
+ MIN_SCROLLABLE_DISTANCE = getFloatPref(prefs, PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE, 500);
+ Log.i(LOGTAG, "Prefs: " + FRICTION_SLOW + "," + FRICTION_FAST + "," + VELOCITY_THRESHOLD + ","
+ + MAX_EVENT_ACCELERATION + "," + OVERSCROLL_DECEL_RATE + "," + SNAP_LIMIT + "," + MIN_SCROLLABLE_DISTANCE);
+ }
+
+ static {
+ // set the scrolling parameters to default values on startup
+ setPrefs(null);
+ }
+
+ private enum FlingStates {
+ STOPPED,
+ PANNING,
+ FLINGING,
+ }
+
+ private 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)
+ }
+
+ private final SubdocumentScrollHelper mSubscroller;
+
+ private int mOverscrollMode; /* Default to only overscrolling if we're allowed to scroll in a direction */
+ private float mFirstTouchPos; /* Position of the first touch event on the current drag. */
+ private float mTouchPos; /* Position of the most recent touch event on the current drag. */
+ private float mLastTouchPos; /* Position of the touch event before touchPos. */
+ private float mVelocity; /* Velocity in this direction; pixels per animation frame. */
+ private boolean mScrollingDisabled; /* Whether movement on this axis is locked. */
+ private boolean mDisableSnap; /* Whether overscroll snapping is disabled. */
+ private float mDisplacement;
+
+ private FlingStates mFlingState; /* The fling state we're in on this axis. */
+
+ protected abstract float getOrigin();
+ protected abstract float getViewportLength();
+ protected abstract float getPageStart();
+ protected abstract float getPageLength();
+
+ Axis(SubdocumentScrollHelper subscroller) {
+ mSubscroller = subscroller;
+ mOverscrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS;
+ }
+
+ public void setOverScrollMode(int overscrollMode) {
+ mOverscrollMode = overscrollMode;
+ }
+
+ public int getOverScrollMode() {
+ return mOverscrollMode;
+ }
+
+ private float getViewportEnd() {
+ return getOrigin() + getViewportLength();
+ }
+
+ private float getPageEnd() {
+ return getPageStart() + getPageLength();
+ }
+
+ void startTouch(float pos) {
+ mVelocity = 0.0f;
+ mScrollingDisabled = false;
+ mFirstTouchPos = mTouchPos = mLastTouchPos = pos;
+ }
+
+ float panDistance(float currentPos) {
+ return currentPos - mFirstTouchPos;
+ }
+
+ void setScrollingDisabled(boolean disabled) {
+ mScrollingDisabled = disabled;
+ }
+
+ void saveTouchPos() {
+ mLastTouchPos = mTouchPos;
+ }
+
+ void updateWithTouchAt(float pos, float timeDelta) {
+ float newVelocity = (mTouchPos - pos) / timeDelta * MS_PER_FRAME;
+
+ // If there's a direction change, or current velocity is very low,
+ // allow setting of the velocity outright. Otherwise, use the current
+ // velocity and a maximum change factor to set the new velocity.
+ boolean curVelocityIsLow = Math.abs(mVelocity) < 1.0f / FRAMERATE_MULTIPLIER;
+ boolean directionChange = (mVelocity > 0) != (newVelocity > 0);
+ if (curVelocityIsLow || (directionChange && !FloatUtils.fuzzyEquals(newVelocity, 0.0f))) {
+ mVelocity = newVelocity;
+ } else {
+ float maxChange = Math.abs(mVelocity * timeDelta * MAX_EVENT_ACCELERATION);
+ mVelocity = Math.min(mVelocity + maxChange, Math.max(mVelocity - maxChange, newVelocity));
+ }
+
+ mTouchPos = pos;
+ }
+
+ boolean overscrolled() {
+ return getOverscroll() != Overscroll.NONE;
+ }
+
+ private Overscroll getOverscroll() {
+ boolean minus = (getOrigin() < getPageStart());
+ boolean plus = (getViewportEnd() > getPageEnd());
+ 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 getPageStart() - getOrigin();
+ case PLUS: return getViewportEnd() - getPageEnd();
+ case BOTH: return (getViewportEnd() - getPageEnd()) + (getPageStart() - getOrigin());
+ default: return 0.0f;
+ }
+ }
+
+ /*
+ * Returns true if the page is zoomed in to some degree along this axis such that scrolling is
+ * possible and this axis has not been scroll locked while panning. Otherwise, returns false.
+ */
+ boolean scrollable() {
+ // If we're scrolling a subdocument, ignore the viewport length restrictions (since those
+ // apply to the top-level document) and only take into account axis locking.
+ if (mSubscroller.scrolling()) {
+ return !mScrollingDisabled;
+ }
+
+ // if we are axis locked, return false
+ if (mScrollingDisabled) {
+ return false;
+ }
+
+ // there is scrollable space, and we're not disabled, or the document fits the viewport
+ // but we always allow overscroll anyway
+ return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE ||
+ getOverScrollMode() == View.OVER_SCROLL_ALWAYS;
+ }
+
+ /*
+ * Returns the resistance, as a multiplier, that should be taken into account when
+ * tracking or pinching.
+ */
+ float getEdgeResistance(boolean forPinching) {
+ float excess = getExcess();
+ if (excess > 0.0f && (getOverscroll() == Overscroll.BOTH || !forPinching)) {
+ // excess can be greater than viewport length, but the resistance
+ // must never drop below 0.0
+ return Math.max(0.0f, SNAP_LIMIT - excess / getViewportLength());
+ }
+ return 1.0f;
+ }
+
+ /* Returns the velocity. If the axis is locked, returns 0. */
+ float getRealVelocity() {
+ return scrollable() ? mVelocity : 0f;
+ }
+
+ void startPan() {
+ mFlingState = FlingStates.PANNING;
+ }
+
+ void startFling(boolean stopped) {
+ mDisableSnap = mSubscroller.scrolling();
+
+ if (stopped) {
+ mFlingState = FlingStates.STOPPED;
+ } else {
+ mFlingState = FlingStates.FLINGING;
+ }
+ }
+
+ /* Advances a fling animation by one step. */
+ boolean advanceFling() {
+ if (mFlingState != FlingStates.FLINGING) {
+ return false;
+ }
+ if (mSubscroller.scrolling() && !mSubscroller.lastScrollSucceeded()) {
+ // if the subdocument stopped scrolling, it's because it reached the end
+ // of the subdocument. we don't do overscroll on subdocuments, so there's
+ // no point in continuing this fling.
+ return false;
+ }
+
+ float excess = getExcess();
+ Overscroll overscroll = getOverscroll();
+ boolean decreasingOverscroll = false;
+ if ((overscroll == Overscroll.MINUS && mVelocity > 0) ||
+ (overscroll == Overscroll.PLUS && mVelocity < 0))
+ {
+ decreasingOverscroll = true;
+ }
+
+ if (mDisableSnap || FloatUtils.fuzzyEquals(excess, 0.0f) || decreasingOverscroll) {
+ // If we aren't overscrolled, just apply friction.
+ if (Math.abs(mVelocity) >= VELOCITY_THRESHOLD) {
+ mVelocity *= FRICTION_FAST;
+ } else {
+ float t = mVelocity / VELOCITY_THRESHOLD;
+ mVelocity *= FloatUtils.interpolate(FRICTION_SLOW, FRICTION_FAST, t);
+ }
+ } else {
+ // Otherwise, decrease the velocity linearly.
+ float elasticity = 1.0f - excess / (getViewportLength() * SNAP_LIMIT);
+ if (overscroll == Overscroll.MINUS) {
+ mVelocity = Math.min((mVelocity + OVERSCROLL_DECEL_RATE) * elasticity, 0.0f);
+ } else { // must be Overscroll.PLUS
+ mVelocity = Math.max((mVelocity - OVERSCROLL_DECEL_RATE) * elasticity, 0.0f);
+ }
+ }
+
+ return true;
+ }
+
+ void stopFling() {
+ mVelocity = 0.0f;
+ mFlingState = FlingStates.STOPPED;
+ }
+
+ // Performs displacement of the viewport position according to the current velocity.
+ void displace() {
+ // if this isn't scrollable just return
+ if (!scrollable())
+ return;
+
+ if (mFlingState == FlingStates.PANNING)
+ mDisplacement += (mLastTouchPos - mTouchPos) * getEdgeResistance(false);
+ else
+ mDisplacement += mVelocity;
+
+ // if overscroll is disabled and we're trying to overscroll, reset the displacement
+ // to remove any excess. Using getExcess alone isn't enough here since it relies on
+ // getOverscroll which doesn't take into account any new displacement being applied
+ if (getOverScrollMode() == View.OVER_SCROLL_NEVER) {
+ if (mDisplacement + getOrigin() < getPageStart()) {
+ mDisplacement = getPageStart() - getOrigin();
+ stopFling();
+ } else if (mDisplacement + getViewportEnd() > getPageEnd()) {
+ mDisplacement = getPageEnd() - getViewportEnd();
+ stopFling();
+ }
+ }
+ }
+
+ float resetDisplacement() {
+ float d = mDisplacement;
+ mDisplacement = 0.0f;
+ return d;
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java b/android/source/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java
new file mode 100644
index 0000000000..a616fcc4da
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java
@@ -0,0 +1,83 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import org.libreoffice.kit.DirectBufferAllocator;
+
+import java.nio.ByteBuffer;
+
+/**
+ * A Cairo image that simply saves a buffer of pixel data.
+ */
+public class BufferedCairoImage extends CairoImage {
+ private static String LOGTAG = "GeckoBufferedCairoImage";
+ private ByteBuffer mBuffer;
+ private IntSize mSize;
+ private int mFormat;
+
+ /**
+ * Creates a buffered Cairo image from a byte buffer.
+ */
+ public BufferedCairoImage(ByteBuffer inBuffer, int inWidth, int inHeight, int inFormat) {
+ setBuffer(inBuffer, inWidth, inHeight, inFormat);
+ }
+
+ /**
+ * Creates a buffered Cairo image from an Android bitmap.
+ */
+ public BufferedCairoImage(Bitmap bitmap) {
+ setBitmap(bitmap);
+ }
+
+ private synchronized void freeBuffer() {
+ mBuffer = DirectBufferAllocator.free(mBuffer);
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ freeBuffer();
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error clearing buffer: ", ex);
+ }
+ }
+
+ @Override
+ public ByteBuffer getBuffer() {
+ return mBuffer;
+ }
+
+ @Override
+ public IntSize getSize() {
+ return mSize;
+ }
+
+ @Override
+ public int getFormat() {
+ return mFormat;
+ }
+
+
+ public void setBuffer(ByteBuffer buffer, int width, int height, int format) {
+ freeBuffer();
+ mBuffer = buffer;
+ mSize = new IntSize(width, height);
+ mFormat = format;
+ }
+
+ public void setBitmap(Bitmap bitmap) {
+ mFormat = CairoUtils.bitmapConfigToCairoFormat(bitmap.getConfig());
+ mSize = new IntSize(bitmap.getWidth(), bitmap.getHeight());
+
+ int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8;
+ mBuffer = DirectBufferAllocator.allocate(mSize.getArea() * bpp);
+ bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer());
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java b/android/source/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java
new file mode 100644
index 0000000000..078aa41bae
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java
@@ -0,0 +1,35 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import javax.microedition.khronos.opengles.GL10;
+
+/** Information needed to render Cairo bitmaps using OpenGL ES. */
+public class CairoGLInfo {
+ public final int internalFormat;
+ public final int format;
+ public final int type;
+
+ public CairoGLInfo(int cairoFormat) {
+ switch (cairoFormat) {
+ case CairoImage.FORMAT_ARGB32:
+ internalFormat = format = GL10.GL_RGBA; type = GL10.GL_UNSIGNED_BYTE;
+ break;
+ case CairoImage.FORMAT_RGB24:
+ internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_BYTE;
+ break;
+ case CairoImage.FORMAT_RGB16_565:
+ internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_SHORT_5_6_5;
+ break;
+ 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");
+ }
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/CairoImage.java b/android/source/src/java/org/mozilla/gecko/gfx/CairoImage.java
new file mode 100644
index 0000000000..5a18a4bb19
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/CairoImage.java
@@ -0,0 +1,28 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 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 getBuffer();
+
+ public abstract void destroy();
+
+ public abstract IntSize getSize();
+ 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/android/source/src/java/org/mozilla/gecko/gfx/CairoUtils.java b/android/source/src/java/org/mozilla/gecko/gfx/CairoUtils.java
new file mode 100644
index 0000000000..e0db6530d5
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/CairoUtils.java
@@ -0,0 +1,51 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Bitmap;
+
+/**
+ * Utility methods useful when displaying Cairo bitmaps using OpenGL ES.
+ */
+public class CairoUtils {
+ private CairoUtils() { /* Don't call me. */ }
+
+ 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");
+ }
+ }
+
+ public static Bitmap.Config cairoFormatTobitmapConfig(int format) {
+ switch (format) {
+ case CairoImage.FORMAT_A8: return Bitmap.Config.ALPHA_8;
+ case CairoImage.FORMAT_ARGB32: return Bitmap.Config.ARGB_8888;
+ case CairoImage.FORMAT_RGB16_565: return Bitmap.Config.RGB_565;
+ default:
+ throw new RuntimeException("Unknown CairoImage format");
+ }
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/ComposedTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/ComposedTileLayer.java
new file mode 100644
index 0000000000..bdef702218
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/ComposedTileLayer.java
@@ -0,0 +1,290 @@
+package org.mozilla.gecko.gfx;
+
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.util.Log;
+
+import org.libreoffice.LOKitShell;
+import org.libreoffice.TileIdentifier;
+import org.mozilla.gecko.util.FloatUtils;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+public abstract class ComposedTileLayer extends Layer implements ComponentCallbacks2 {
+ private static final String LOGTAG = ComposedTileLayer.class.getSimpleName();
+
+ protected final List<SubTile> tiles = new ArrayList<SubTile>();
+
+ protected final IntSize tileSize;
+ private final ReadWriteLock tilesReadWriteLock = new ReentrantReadWriteLock();
+ private final Lock tilesReadLock = tilesReadWriteLock.readLock();
+ private final Lock tilesWriteLock = tilesReadWriteLock.writeLock();
+
+ protected RectF currentViewport = new RectF();
+ protected float currentZoom = 1.0f;
+ protected RectF currentPageRect = new RectF();
+
+ private long reevaluationNanoTime = 0;
+
+ public ComposedTileLayer(Context context) {
+ context.registerComponentCallbacks(this);
+ this.tileSize = new IntSize(256, 256);
+ }
+
+ protected static RectF roundToTileSize(RectF input, IntSize tileSize) {
+ float minX = ((int) (input.left / tileSize.width)) * tileSize.width;
+ float minY = ((int) (input.top / tileSize.height)) * tileSize.height;
+ float maxX = ((int) (input.right / tileSize.width) + 1) * tileSize.width;
+ float maxY = ((int) (input.bottom / tileSize.height) + 1) * tileSize.height;
+ return new RectF(minX, minY, maxX, maxY);
+ }
+
+ protected static RectF inflate(RectF rect, IntSize inflateSize) {
+ RectF newRect = new RectF(rect);
+ newRect.left -= inflateSize.width;
+ newRect.left = newRect.left < 0.0f ? 0.0f : newRect.left;
+
+ newRect.top -= inflateSize.height;
+ newRect.top = newRect.top < 0.0f ? 0.0f : newRect.top;
+
+ newRect.right += inflateSize.width;
+ newRect.bottom += inflateSize.height;
+
+ return newRect;
+ }
+
+ protected static RectF normalizeRect(RectF rect, float sourceFactor, float targetFactor) {
+ return new RectF(
+ (rect.left / sourceFactor) * targetFactor,
+ (rect.top / sourceFactor) * targetFactor,
+ (rect.right / sourceFactor) * targetFactor,
+ (rect.bottom / sourceFactor) * targetFactor);
+ }
+
+ public void invalidate() {
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ tile.invalidate();
+ }
+ tilesReadLock.unlock();
+ }
+
+ @Override
+ public void beginTransaction() {
+ super.beginTransaction();
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ tile.beginTransaction();
+ }
+ tilesReadLock.unlock();
+ }
+
+ @Override
+ public void endTransaction() {
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ tile.endTransaction();
+ }
+ tilesReadLock.unlock();
+ super.endTransaction();
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ if (RectF.intersects(tile.getBounds(context), context.viewport)) {
+ tile.draw(context);
+ }
+ }
+ tilesReadLock.unlock();
+ }
+
+ @Override
+ protected void performUpdates(RenderContext context) {
+ super.performUpdates(context);
+
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ tile.beginTransaction();
+ tile.refreshTileMetrics();
+ tile.endTransaction();
+ tile.performUpdates(context);
+ }
+ tilesReadLock.unlock();
+ }
+
+ @Override
+ public Region getValidRegion(RenderContext context) {
+ Region validRegion = new Region();
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ validRegion.op(tile.getValidRegion(context), Region.Op.UNION);
+ }
+ tilesReadLock.unlock();
+ return validRegion;
+ }
+
+ @Override
+ public void setResolution(float newResolution) {
+ super.setResolution(newResolution);
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ tile.setResolution(newResolution);
+ }
+ tilesReadLock.unlock();
+ }
+
+ public void reevaluateTiles(ImmutableViewportMetrics viewportMetrics, DisplayPortMetrics mDisplayPort) {
+ RectF newViewPort = getViewPort(viewportMetrics);
+ float newZoom = getZoom(viewportMetrics);
+
+ // When
+ if (newZoom <= 0.0 || Float.isNaN(newZoom)) {
+ return;
+ }
+
+ if (currentViewport.equals(newViewPort) && FloatUtils.fuzzyEquals(currentZoom, newZoom)) {
+ return;
+ }
+
+ long currentReevaluationNanoTime = System.nanoTime();
+ if ((currentReevaluationNanoTime - reevaluationNanoTime) < 25 * 1000000) {
+ return;
+ }
+
+ reevaluationNanoTime = currentReevaluationNanoTime;
+
+ currentViewport = newViewPort;
+ currentZoom = newZoom;
+ currentPageRect = viewportMetrics.getPageRect();
+
+ LOKitShell.sendTileReevaluationRequest(this);
+ }
+
+ protected abstract RectF getViewPort(ImmutableViewportMetrics viewportMetrics);
+
+ protected abstract float getZoom(ImmutableViewportMetrics viewportMetrics);
+
+ protected abstract int getTilePriority();
+
+ private boolean containsTilesMatching(float x, float y, float currentZoom) {
+ tilesReadLock.lock();
+ try {
+ for (SubTile tile : tiles) {
+ if (tile.id.x == x && tile.id.y == y && tile.id.zoom == currentZoom) {
+ return true;
+ }
+ }
+ return false;
+ } finally {
+ tilesReadLock.unlock();
+ }
+ }
+
+ public void addNewTiles(List<SubTile> newTiles) {
+ for (float y = currentViewport.top; y < currentViewport.bottom; y += tileSize.height) {
+ if (y > currentPageRect.height()) {
+ continue;
+ }
+ for (float x = currentViewport.left; x < currentViewport.right; x += tileSize.width) {
+ if (x > currentPageRect.width()) {
+ continue;
+ }
+ if (!containsTilesMatching(x, y, currentZoom)) {
+ TileIdentifier tileId = new TileIdentifier((int) x, (int) y, currentZoom, tileSize);
+ SubTile tile = createNewTile(tileId);
+ newTiles.add(tile);
+ }
+ }
+ }
+ }
+
+ public void clearMarkedTiles() {
+ tilesWriteLock.lock();
+ Iterator<SubTile> iterator = tiles.iterator();
+ while (iterator.hasNext()) {
+ SubTile tile = iterator.next();
+ if (tile.markedForRemoval) {
+ tile.destroy();
+ iterator.remove();
+ }
+ }
+ tilesWriteLock.unlock();
+ }
+
+ public void markTiles() {
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ if (FloatUtils.fuzzyEquals(tile.id.zoom, currentZoom)) {
+ RectF tileRect = tile.id.getRectF();
+ if (!RectF.intersects(currentViewport, tileRect)) {
+ tile.markForRemoval();
+ }
+ } else {
+ tile.markForRemoval();
+ }
+ }
+ tilesReadLock.unlock();
+ }
+
+ public void clearAndReset() {
+ tilesWriteLock.lock();
+ tiles.clear();
+ tilesWriteLock.unlock();
+ currentViewport = new RectF();
+ }
+
+ private SubTile createNewTile(TileIdentifier tileId) {
+ SubTile tile = new SubTile(tileId);
+ tile.beginTransaction();
+ tilesWriteLock.lock();
+ tiles.add(tile);
+ tilesWriteLock.unlock();
+ return tile;
+ }
+
+ public boolean isStillValid(TileIdentifier tileId) {
+ return RectF.intersects(currentViewport, tileId.getRectF()) || currentViewport.contains(tileId.getRectF());
+ }
+
+ /**
+ * Invalidate tiles which intersect the input rect
+ */
+ public void invalidateTiles(List<SubTile> tilesToInvalidate, RectF cssRect) {
+ RectF zoomedRect = RectUtils.scale(cssRect, currentZoom);
+ tilesReadLock.lock();
+ for (SubTile tile : tiles) {
+ if (!tile.markedForRemoval && RectF.intersects(zoomedRect, tile.id.getRectF())) {
+ tilesToInvalidate.add(tile);
+ }
+ }
+ tilesReadLock.unlock();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ @Override
+ public void onLowMemory() {
+ Log.i(LOGTAG, "onLowMemory");
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ if (level >= 15 /*TRIM_MEMORY_RUNNING_CRITICAL*/) {
+ Log.i(LOGTAG, "Trimming memory - TRIM_MEMORY_RUNNING_CRITICAL");
+ } else if (level >= 10 /*TRIM_MEMORY_RUNNING_LOW*/) {
+ Log.i(LOGTAG, "Trimming memory - TRIM_MEMORY_RUNNING_LOW");
+ }
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java b/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
new file mode 100644
index 0000000000..d98efa2d50
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java
@@ -0,0 +1,760 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.mozilla.gecko.util.FloatUtils;
+
+import java.util.Map;
+
+final class DisplayPortCalculator {
+ private static final String LOGTAG = DisplayPortCalculator.class.getSimpleName();
+ private static final PointF ZERO_VELOCITY = new PointF(0, 0);
+
+ // Keep this in sync with the TILEDLAYERBUFFER_TILE_SIZE defined in gfx/layers/TiledLayerBuffer.h
+ private static final int TILE_SIZE = 256;
+
+ private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy";
+ private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier";
+ private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x";
+ private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y";
+ private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier";
+ private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold";
+ private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr";
+ private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr";
+ private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold";
+
+ private DisplayPortStrategy sStrategy;
+ private final LibreOfficeMainActivity mMainActivity;
+
+ DisplayPortCalculator(LibreOfficeMainActivity context) {
+ this.mMainActivity = context;
+ sStrategy = new VelocityBiasStrategy(mMainActivity, null);
+ }
+
+ DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
+ }
+
+ boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ if (displayPort == null) {
+ return true;
+ }
+ return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort);
+ }
+
+ boolean drawTimeUpdate(long millis, int pixels) {
+ return sStrategy.drawTimeUpdate(millis, pixels);
+ }
+
+ void resetPageState() {
+ sStrategy.resetPageState();
+ }
+
+ static void addPrefNames(JSONArray prefs) {
+ prefs.put(PREF_DISPLAYPORT_STRATEGY);
+ prefs.put(PREF_DISPLAYPORT_FM_MULTIPLIER);
+ prefs.put(PREF_DISPLAYPORT_FM_DANGER_X);
+ prefs.put(PREF_DISPLAYPORT_FM_DANGER_Y);
+ prefs.put(PREF_DISPLAYPORT_VB_MULTIPLIER);
+ prefs.put(PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD);
+ prefs.put(PREF_DISPLAYPORT_VB_REVERSE_BUFFER);
+ prefs.put(PREF_DISPLAYPORT_VB_DANGER_X_BASE);
+ prefs.put(PREF_DISPLAYPORT_VB_DANGER_Y_BASE);
+ prefs.put(PREF_DISPLAYPORT_VB_DANGER_X_INCR);
+ prefs.put(PREF_DISPLAYPORT_VB_DANGER_Y_INCR);
+ prefs.put(PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD);
+ }
+
+ /**
+ * Set the active strategy to use.
+ * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the
+ * mapping between ints and strategies.
+ */
+ boolean setStrategy(Map<String, Integer> prefs) {
+ Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY);
+ if (strategy == null) {
+ return false;
+ }
+
+ switch (strategy) {
+ case 0:
+ sStrategy = new FixedMarginStrategy(prefs);
+ break;
+ case 1:
+ sStrategy = new VelocityBiasStrategy(mMainActivity, prefs);
+ break;
+ case 2:
+ sStrategy = new DynamicResolutionStrategy(mMainActivity, prefs);
+ break;
+ case 3:
+ sStrategy = new NoMarginStrategy(prefs);
+ break;
+ case 4:
+ sStrategy = new PredictionBiasStrategy(mMainActivity, prefs);
+ break;
+ default:
+ Log.e(LOGTAG, "Invalid strategy index specified");
+ return false;
+ }
+ Log.i(LOGTAG, "Set strategy " + sStrategy.toString());
+ return true;
+ }
+
+ private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
+ Integer value = (prefs == null ? null : prefs.get(prefName));
+ return (float)(value == null || value < 0 ? defaultValue : value) / 1000f;
+ }
+
+ private static abstract class DisplayPortStrategy {
+ /** Calculates a displayport given a viewport and panning velocity. */
+ public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity);
+ /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */
+ public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort);
+ /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */
+ public boolean drawTimeUpdate(long millis, int pixels) { return false; }
+ /** Reset any page-specific state stored, as the page being displayed has changed. */
+ public void resetPageState() {}
+ }
+
+ /**
+ * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the
+ * given metrics object. The area in the returned FloatSize may be less than width*height if the page is
+ * small, but it will never be larger than width*height.
+ * Note that this process may change the relative aspect ratio of the given dimensions.
+ */
+ private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) {
+ // figure out how much of the desired buffer amount we can actually use on the horizontal axis
+ float usableWidth = Math.min(width, metrics.getPageWidth());
+ // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and
+ // use it on the vertical axis
+ float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth);
+ float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight());
+ if (usableHeight < height && usableWidth == width) {
+ // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal
+ float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight);
+ usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth());
+ }
+ return new FloatSize(usableWidth, usableHeight);
+ }
+
+ /**
+ * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis
+ * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then
+ * clamped to page bounds and returned.
+ */
+ private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) {
+ // calculate the danger zone amounts in pixels
+ float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
+ float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
+ rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
+ // clamp to page bounds
+ return clampToPageBounds(rect, metrics);
+ }
+
+ /**
+ * Expand the given margins such that when they are applied on the viewport, the resulting rect
+ * does not have any partial tiles, except when it is clipped by the page bounds. This assumes
+ * the tiles are TILE_SIZE by TILE_SIZE and start at the origin, such that there will always be
+ * a tile at (0,0)-(TILE_SIZE,TILE_SIZE)).
+ */
+ private static DisplayPortMetrics getTileAlignedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
+ float left = metrics.viewportRectLeft - margins.left;
+ float top = metrics.viewportRectTop - margins.top;
+ float right = metrics.viewportRectRight + margins.right;
+ float bottom = metrics.viewportRectBottom + margins.bottom;
+ left = (float) Math.max(metrics.pageRectLeft, TILE_SIZE * Math.floor(left / TILE_SIZE));
+ top = (float) Math.max(metrics.pageRectTop, TILE_SIZE * Math.floor(top / TILE_SIZE));
+ right = (float) Math.min(metrics.pageRectRight, TILE_SIZE * Math.ceil(right / TILE_SIZE));
+ bottom = (float) Math.min(metrics.pageRectBottom, TILE_SIZE * Math.ceil(bottom / TILE_SIZE));
+ return new DisplayPortMetrics(left, top, right, bottom, zoom);
+ }
+
+ /**
+ * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
+ * does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
+ * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
+ * metrics.getPageWidth(); and the same for the y axis.
+ */
+ private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) {
+ // check how much we're overflowing in each direction. note that at most one of leftOverflow
+ // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow
+ // can be greater than zero, because of the assumption described in the method javadoc.
+ float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left);
+ float rightOverflow = (metrics.viewportRectRight + margins.right) - metrics.pageRectRight;
+ float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top);
+ float bottomOverflow = (metrics.viewportRectBottom + margins.bottom) - metrics.pageRectBottom;
+
+ // if the margins overflow the page bounds, shift them to other side on the same axis
+ if (leftOverflow > 0) {
+ margins.left -= leftOverflow;
+ margins.right += leftOverflow;
+ } else if (rightOverflow > 0) {
+ margins.right -= rightOverflow;
+ margins.left += rightOverflow;
+ }
+ if (topOverflow > 0) {
+ margins.top -= topOverflow;
+ margins.bottom += topOverflow;
+ } else if (bottomOverflow > 0) {
+ margins.bottom -= bottomOverflow;
+ margins.top += bottomOverflow;
+ }
+ return margins;
+ }
+
+ /**
+ * Clamp the given rect to the page bounds and return it.
+ */
+ private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) {
+ if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop;
+ if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft;
+ if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight;
+ if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom;
+ return rect;
+ }
+
+ /**
+ * This class implements the variation where we basically don't bother with a display port.
+ */
+ private static class NoMarginStrategy extends DisplayPortStrategy {
+ NoMarginStrategy(Map<String, Integer> prefs) {
+ // no prefs in this strategy
+ }
+
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ return new DisplayPortMetrics(metrics.viewportRectLeft,
+ metrics.viewportRectTop,
+ metrics.viewportRectRight,
+ metrics.viewportRectBottom,
+ metrics.zoomFactor);
+ }
+
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "NoMarginStrategy";
+ }
+ }
+
+ /**
+ * This class implements the variation where we use a fixed-size margin on the display port.
+ * The margin is always 300 pixels in all directions, except when we are (a) approaching a page
+ * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain
+ * the area of the display port by (a) shifting the buffer to the other side on the same axis,
+ * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on
+ * one axis.
+ */
+ private static class FixedMarginStrategy extends DisplayPortStrategy {
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private final float SIZE_MULTIPLIER;
+
+ // If the visible rect is within the danger zone (measured as a fraction of the view size
+ // from the edge of the displayport) we start redrawing to minimize checkerboarding.
+ private final float DANGER_ZONE_X_MULTIPLIER;
+ private final float DANGER_ZONE_Y_MULTIPLIER;
+
+ FixedMarginStrategy(Map<String, Integer> prefs) {
+ SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000);
+ DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100);
+ DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200);
+ }
+
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169). we simultaneously need to make
+ // the display port as large as possible so that we redraw less. reshape the display
+ // port dimensions to accomplish this.
+ FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+ float horizontalBuffer = usableSize.width - metrics.getWidth();
+ float verticalBuffer = usableSize.height - metrics.getHeight();
+
+ // and now calculate the display port margins based on how much buffer we've decided to use and
+ // the page bounds, ensuring we use all of the available buffer amounts on one side or the other
+ // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is
+ // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer
+ // is split).
+ RectF margins = new RectF();
+ margins.left = horizontalBuffer / 2.0f;
+ margins.right = horizontalBuffer - margins.left;
+ margins.top = verticalBuffer / 2.0f;
+ margins.bottom = verticalBuffer - margins.top;
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // Increase the size of the viewport based on the danger zone multiplier (and clamp to page
+ // boundaries), and intersect it with the current displayport to determine whether we're
+ // close to checkerboarding.
+ RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
+ return !displayPort.contains(adjustedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER;
+ }
+ }
+
+ /**
+ * This class implements the variation with a small fixed-size margin with velocity bias.
+ * In this variation, the default margins are pretty small relative to the view size, but
+ * they are affected by the panning velocity. Specifically, if we are panning on one axis,
+ * we remove the margins on the other axis because we are likely axis-locked. Also once
+ * we are panning in one direction above a certain threshold velocity, we shift the buffer
+ * so that it is almost entirely in the direction of the pan, with a little bit in the
+ * reverse direction.
+ */
+ private static class VelocityBiasStrategy extends DisplayPortStrategy {
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private final float SIZE_MULTIPLIER;
+ // The velocity above which we apply the velocity bias
+ private final float VELOCITY_THRESHOLD;
+ // How much of the buffer to keep in the reverse direction of the velocity
+ private final float REVERSE_BUFFER;
+ // If the visible rect is within the danger zone we start redrawing to minimize
+ // checkerboarding. the danger zone amount is a linear function of the form:
+ // viewportsize * (base + velocity * incr)
+ // where base and incr are configurable values.
+ private final float DANGER_ZONE_BASE_X_MULTIPLIER;
+ private final float DANGER_ZONE_BASE_Y_MULTIPLIER;
+ private final float DANGER_ZONE_INCR_X_MULTIPLIER;
+ private final float DANGER_ZONE_INCR_Y_MULTIPLIER;
+
+ VelocityBiasStrategy(LibreOfficeMainActivity context, Map<String, Integer> prefs) {
+ SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000);
+ VELOCITY_THRESHOLD = LOKitShell.getDpi(context) * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32);
+ REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200);
+ DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000);
+ DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000);
+ DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0);
+ DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0);
+ }
+
+ /**
+ * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values.
+ * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER
+ * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the
+ * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction
+ * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the
+ * two margins on that axis.
+ */
+ private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) {
+ RectF margins = new RectF();
+
+ if (velocity.x > VELOCITY_THRESHOLD) {
+ margins.left = xAmount * REVERSE_BUFFER;
+ } else if (velocity.x < -VELOCITY_THRESHOLD) {
+ margins.left = xAmount * (1.0f - REVERSE_BUFFER);
+ } else {
+ margins.left = xAmount / 2.0f;
+ }
+ margins.right = xAmount - margins.left;
+
+ if (velocity.y > VELOCITY_THRESHOLD) {
+ margins.top = yAmount * REVERSE_BUFFER;
+ } else if (velocity.y < -VELOCITY_THRESHOLD) {
+ margins.top = yAmount * (1.0f - REVERSE_BUFFER);
+ } else {
+ margins.top = yAmount / 2.0f;
+ }
+ margins.bottom = yAmount - margins.top;
+
+ return margins;
+ }
+
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // but if we're panning on one axis, set the margins for the other axis to zero since we are likely
+ // axis locked and won't be displaying that extra area.
+ if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) {
+ displayPortHeight = metrics.getHeight();
+ } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) {
+ displayPortWidth = metrics.getWidth();
+ }
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169).
+ displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth());
+ displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight());
+ float horizontalBuffer = displayPortWidth - metrics.getWidth();
+ float verticalBuffer = displayPortHeight - metrics.getHeight();
+
+ // split the buffer amounts into margins based on velocity, and shift it to
+ // take into account the page bounds
+ RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity);
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // calculate the danger zone amounts based on the prefs
+ float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER));
+ float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER));
+ // clamp it such that when added to the viewport, they don't exceed page size.
+ // this is a prerequisite to calling shiftMarginsForPageBounds as we do below.
+ dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth());
+ dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight());
+
+ // split the danger zone into margins based on velocity, and ensure it doesn't exceed
+ // page bounds.
+ RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity);
+ dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics);
+
+ // we're about to checkerboard if the current viewport area + the danger zone margins
+ // fall out of the current displayport anywhere.
+ RectF adjustedViewport = new RectF(
+ metrics.viewportRectLeft - dangerMargins.left,
+ metrics.viewportRectTop - dangerMargins.top,
+ metrics.viewportRectRight + dangerMargins.right,
+ metrics.viewportRectBottom + dangerMargins.bottom);
+ return !displayPort.contains(adjustedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER
+ + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER
+ + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER;
+ }
+ }
+
+ /**
+ * This class implements the variation where we draw more of the page at low resolution while panning.
+ * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw
+ * resolution to compensate. This results in the same device-pixel area drawn; the compositor then
+ * scales this up to the viewport zoom level. This results in a large area of the page drawn but it
+ * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding,
+ * where we draw less but never even show it on the screen.
+ */
+ private static class DynamicResolutionStrategy extends DisplayPortStrategy {
+
+ // The velocity above which we start zooming out the display port to keep up
+ // with the panning.
+ private final float VELOCITY_EXPANSION_THRESHOLD;
+
+
+ DynamicResolutionStrategy(LibreOfficeMainActivity context, Map<String, Integer> prefs) {
+ // ignore prefs for now
+ VELOCITY_EXPANSION_THRESHOLD = LOKitShell.getDpi(context) / 16f;
+ VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f;
+ }
+
+ // The length of each axis of the display port will be the corresponding view length
+ // multiplied by this factor.
+ private static final float SIZE_MULTIPLIER = 1.5f;
+
+ // How much we increase the display port based on velocity. Assuming no friction and
+ // splitting (see below), this should be the number of frames (@60fps) between us
+ // calculating the display port and the draw of the *next* display port getting composited
+ // and displayed on the screen. This is because the timeline looks like this:
+ // Java: pan pan pan pan pan pan ! pan pan pan pan pan pan !
+ // Gecko: \-> draw -> composite / \-> draw -> composite /
+ // The display port calculated on the first "pan" gets composited to the screen at the
+ // first exclamation mark, and remains on the screen until the second exclamation mark.
+ // In order to avoid checkerboarding, that display port must be able to contain all of
+ // the panning until the second exclamation mark, which encompasses two entire draw/composite
+ // cycles.
+ // If we take into account friction, our velocity multiplier should be reduced as the
+ // amount of pan will decrease each time. If we take into account display port splitting,
+ // it should be increased as the splitting means some of the display port will be used to
+ // draw in the opposite direction of the velocity. For now I'm assuming these two cancel
+ // each other out.
+ private static final float VELOCITY_MULTIPLIER = 60.0f;
+
+ // The following constants adjust how biased the display port is in the direction of panning.
+ // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the
+ // display port "buffer" area, otherwise we use the slow split factor. This is based on the
+ // assumption that if the user is panning fast, they are less likely to reverse directions
+ // and go backwards, so we should spend more of our display port buffer in the direction of
+ // panning.
+ private final float VELOCITY_FAST_THRESHOLD;
+ private static final float FAST_SPLIT_FACTOR = 0.95f;
+ private static final float SLOW_SPLIT_FACTOR = 0.8f;
+
+ // The following constants are used for viewport prediction; we use them to estimate where
+ // the viewport will be soon and whether or not we should trigger a draw right now. "soon"
+ // in the previous sentence really refers to the amount of time it would take to draw and
+ // composite from the point at which we do the calculation, and that is not really a known
+ // quantity. The velocity multiplier is how much we multiply the velocity by; it has the
+ // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account
+ // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the
+ // viewport size that we use as an extra "danger zone" around the viewport; if this danger
+ // zone falls outside the display port then we are approaching the point at which we will
+ // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is
+ // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the
+ // danger zone, and thus will be constantly drawing.
+ private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f;
+ private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f)
+
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
+ float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
+
+ // for resolution calculation purposes, we need to know what the adjusted display port dimensions
+ // would be if we had zero velocity, so calculate that here before we increase the display port
+ // based on velocity.
+ FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+
+ // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their
+ // relative aspect ratio.
+ if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) {
+ float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth,
+ Math.abs(velocity.y) / displayPortHeight);
+ velocityFactor *= VELOCITY_MULTIPLIER;
+
+ displayPortWidth += (displayPortWidth * velocityFactor);
+ displayPortHeight += (displayPortHeight * velocityFactor);
+ }
+
+ // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels)
+ // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied
+ // by metrics.zoomFactor
+
+ // we need to avoid having a display port that is larger than the page, or we will end up
+ // painting things outside the page bounds (bug 729169). we simultaneously need to make
+ // the display port as large as possible so that we redraw less. reshape the display
+ // port dimensions to accomplish this. this may change the aspect ratio of the display port,
+ // but we are assuming that this is desirable because the advantages from pre-drawing will
+ // outweigh the disadvantages from any buffer reallocations that might occur.
+ FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
+ float horizontalBuffer = usableSize.width - metrics.getWidth();
+ float verticalBuffer = usableSize.height - metrics.getHeight();
+
+ // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have.
+ // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case
+ // the user scrolls there. we now need to split the buffer area on each axis so that we know
+ // what the exact margins on each side will be. first we split the buffer amount based on the direction
+ // we're moving, so that we have a larger buffer in the direction of travel.
+ RectF margins = new RectF();
+ margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x);
+ margins.right = horizontalBuffer - margins.left;
+ margins.top = splitBufferByVelocity(verticalBuffer, velocity.y);
+ margins.bottom = verticalBuffer - margins.top;
+
+ // then, we account for running into the page bounds - so that if we hit the top of the page, we need
+ // to drop the top margin and move that amount to the bottom margin.
+ margins = shiftMarginsForPageBounds(margins, metrics);
+
+ // finally, we calculate the resolution we want to render the display port area at. We do this
+ // so that as we expand the display port area (because of velocity), we reduce the resolution of
+ // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate
+ // the reduction in resolution by comparing the display port size with and without the velocity
+ // changes applied.
+ // this effectively means that as we pan faster and faster, the display port grows, but we paint
+ // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing
+ // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone.
+ // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we
+ // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor.
+ float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height);
+ float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor);
+
+ return new DisplayPortMetrics(
+ metrics.viewportRectLeft - margins.left,
+ metrics.viewportRectTop - margins.top,
+ metrics.viewportRectRight + margins.right,
+ metrics.viewportRectBottom + margins.bottom,
+ displayResolution);
+ }
+
+ /**
+ * Split the given buffer amount into two based on the velocity.
+ * Given an amount of total usable buffer on an axis, this will
+ * return the amount that should be used on the left/top side of
+ * the axis (the side which a negative velocity vector corresponds
+ * to).
+ */
+ private float splitBufferByVelocity(float amount, float velocity) {
+ // if no velocity, so split evenly
+ if (FloatUtils.fuzzyEquals(velocity, 0)) {
+ return amount / 2.0f;
+ }
+ // if we're moving quickly, assign more of the amount in that direction
+ // since is less likely that we will reverse direction immediately
+ if (velocity < -VELOCITY_FAST_THRESHOLD) {
+ return amount * FAST_SPLIT_FACTOR;
+ }
+ if (velocity > VELOCITY_FAST_THRESHOLD) {
+ return amount * (1.0f - FAST_SPLIT_FACTOR);
+ }
+ // if we're moving slowly, then assign less of the amount in that direction
+ if (velocity < 0) {
+ return amount * SLOW_SPLIT_FACTOR;
+ } else {
+ return amount * (1.0f - SLOW_SPLIT_FACTOR);
+ }
+ }
+
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // Expand the viewport based on our velocity (and clamp it to page boundaries).
+ // Then intersect it with the last-requested displayport to determine whether we're
+ // close to checkerboarding.
+
+ RectF predictedViewport = metrics.getViewport();
+
+ // first we expand the viewport in the direction we're moving based on some
+ // multiple of the current velocity.
+ if (velocity.length() > 0) {
+ if (velocity.x < 0) {
+ predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
+ } else if (velocity.x > 0) {
+ predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
+ }
+
+ if (velocity.y < 0) {
+ predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
+ } else if (velocity.y > 0) {
+ predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
+ }
+ }
+
+ // then we expand the viewport evenly in all directions just to have an extra
+ // safety zone. this also clamps it to page bounds.
+ predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics);
+ return !displayPort.contains(predictedViewport);
+ }
+
+ @Override
+ public String toString() {
+ return "DynamicResolutionStrategy";
+ }
+ }
+
+ /**
+ * This class implements the variation where we use the draw time to predict where we will be when
+ * a draw completes, and draw that instead of where we are now. In this variation, when our panning
+ * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can
+ * pan in any direction without encountering checkerboarding.
+ * Once the user is panning, we modify the displayport to encompass an area range of where we think
+ * the user will be when the draw completes. This heuristic relies on both the estimated draw time
+ * the panning velocity; unexpected changes in either of these values will cause the heuristic to
+ * fail and show checkerboard.
+ */
+ private static class PredictionBiasStrategy extends DisplayPortStrategy {
+ private static float VELOCITY_THRESHOLD;
+
+ private int mPixelArea; // area of the viewport, used in draw time calculations
+ private int mMinFramesToDraw; // minimum number of frames we take to draw
+ private int mMaxFramesToDraw; // maximum number of frames we take to draw
+
+ PredictionBiasStrategy(LibreOfficeMainActivity context, Map<String, Integer> prefs) {
+ VELOCITY_THRESHOLD = LOKitShell.getDpi(context) * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16);
+ resetPageState();
+ }
+
+ public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
+ float width = metrics.getWidth();
+ float height = metrics.getHeight();
+ mPixelArea = (int)(width * height);
+
+ if (velocity.length() < VELOCITY_THRESHOLD) {
+ // if we're going slow, expand the displayport to 9x viewport size
+ RectF margins = new RectF(width, height, width, height);
+ return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ // figure out how far we expect to be
+ float minDx = velocity.x * mMinFramesToDraw;
+ float minDy = velocity.y * mMinFramesToDraw;
+ float maxDx = velocity.x * mMaxFramesToDraw;
+ float maxDy = velocity.y * mMaxFramesToDraw;
+
+ // figure out how many pixels we will be drawing when we draw the above-calculated range.
+ // this will be larger than the viewport area.
+ float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy));
+ // adjust how far we will get because of the time spent drawing all these extra pixels. this
+ // will again increase the number of pixels drawn so really we could keep iterating this over
+ // and over, but once seems enough for now.
+ maxDx = maxDx * pixelsToDraw / mPixelArea;
+ maxDy = maxDy * pixelsToDraw / mPixelArea;
+
+ // and finally generate the displayport. the min/max stuff takes care of
+ // negative velocities as well as positive.
+ RectF margins = new RectF(
+ -Math.min(minDx, maxDx),
+ -Math.min(minDy, maxDy),
+ Math.max(minDx, maxDx),
+ Math.max(minDy, maxDy));
+ return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+ }
+
+ public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
+ // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs.
+ // refer to the comments in calculate() to understand what this is doing.
+ float minDx = velocity.x * mMinFramesToDraw;
+ float minDy = velocity.y * mMinFramesToDraw;
+ float maxDx = velocity.x * mMaxFramesToDraw;
+ float maxDy = velocity.y * mMaxFramesToDraw;
+ float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy));
+ maxDx = maxDx * pixelsToDraw / mPixelArea;
+ maxDy = maxDy * pixelsToDraw / mPixelArea;
+
+ // now that we have an idea of how far we will be when the draw completes, take the farthest
+ // end of that range and see if it falls outside the displayport bounds. if it does, allow
+ // the draw to go through
+ RectF predictedViewport = metrics.getViewport();
+ predictedViewport.left += maxDx;
+ predictedViewport.top += maxDy;
+ predictedViewport.right += maxDx;
+ predictedViewport.bottom += maxDy;
+
+ predictedViewport = clampToPageBounds(predictedViewport, metrics);
+ return !displayPort.contains(predictedViewport);
+ }
+
+ @Override
+ public boolean drawTimeUpdate(long millis, int pixels) {
+ // calculate the number of frames it took to draw a viewport-sized area
+ float normalizedTime = (float)mPixelArea * (float)millis / (float)pixels;
+ int normalizedFrames = (int)Math.ceil(normalizedTime * 60f / 1000f);
+ // broaden our range on how long it takes to draw if the draw falls outside
+ // the range. this allows it to grow gradually. this heuristic may need to
+ // be tweaked into more of a floating window average or something.
+ if (normalizedFrames <= mMinFramesToDraw) {
+ mMinFramesToDraw--;
+ } else if (normalizedFrames > mMaxFramesToDraw) {
+ mMaxFramesToDraw++;
+ } else {
+ return true;
+ }
+ Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]");
+ return true;
+ }
+
+ @Override
+ public void resetPageState() {
+ mMinFramesToDraw = 0;
+ mMaxFramesToDraw = 2;
+ }
+
+ @Override
+ public String toString() {
+ return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD;
+ }
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java b/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
new file mode 100644
index 0000000000..f622c44ff9
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java
@@ -0,0 +1,67 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.RectF;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+/*
+ * This class keeps track of the area we request Gecko to paint, as well
+ * as the resolution of the paint. The area may be different from the visible
+ * area of the page, and the resolution may be different from the resolution
+ * used in the compositor to render the page. This is so that we can ask Gecko
+ * to paint a much larger area without using extra memory, and then render some
+ * subsection of that with compositor scaling.
+ */
+public final class DisplayPortMetrics {
+ private final RectF mPosition;
+ private final float mResolution;
+
+ public RectF getPosition() {
+ return mPosition;
+ }
+
+ public float getResolution() {
+ return mResolution;
+ }
+
+ public DisplayPortMetrics() {
+ this(0, 0, 0, 0, 1);
+ }
+
+ public DisplayPortMetrics(float left, float top, float right, float bottom, float resolution) {
+ mPosition = new RectF(left, top, right, bottom);
+ mResolution = resolution;
+ }
+
+ public boolean contains(RectF rect) {
+ return mPosition.contains(rect);
+ }
+
+ public boolean fuzzyEquals(DisplayPortMetrics metrics) {
+ return RectUtils.fuzzyEquals(mPosition, metrics.mPosition)
+ && FloatUtils.fuzzyEquals(mResolution, metrics.mResolution);
+ }
+
+ public String toJSON() {
+ StringBuffer sb = new StringBuffer(256);
+ sb.append("{ \"left\": ").append(mPosition.left)
+ .append(", \"top\": ").append(mPosition.top)
+ .append(", \"right\": ").append(mPosition.right)
+ .append(", \"bottom\": ").append(mPosition.bottom)
+ .append(", \"resolution\": ").append(mResolution)
+ .append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ return "DisplayPortMetrics v=(" + mPosition.left + ","
+ + mPosition.top + "," + mPosition.right + ","
+ + mPosition.bottom + ") z=" + mResolution;
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java
new file mode 100644
index 0000000000..ea95c032e8
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java
@@ -0,0 +1,30 @@
+package org.mozilla.gecko.gfx;
+
+import android.content.Context;
+import android.graphics.RectF;
+
+public class DynamicTileLayer extends ComposedTileLayer {
+ public DynamicTileLayer(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected RectF getViewPort(ImmutableViewportMetrics viewportMetrics) {
+ RectF rect = viewportMetrics.getViewport();
+ return inflate(roundToTileSize(rect, tileSize), getInflateFactor());
+ }
+
+ @Override
+ protected float getZoom(ImmutableViewportMetrics viewportMetrics) {
+ return viewportMetrics.zoomFactor;
+ }
+
+ @Override
+ protected int getTilePriority() {
+ return 0;
+ }
+
+ private IntSize getInflateFactor() {
+ return new IntSize(tileSize.width*2, tileSize.height*4);
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/FixedZoomTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/FixedZoomTileLayer.java
new file mode 100644
index 0000000000..e86494c20b
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/FixedZoomTileLayer.java
@@ -0,0 +1,31 @@
+package org.mozilla.gecko.gfx;
+
+import android.content.Context;
+import android.graphics.RectF;
+
+public class FixedZoomTileLayer extends ComposedTileLayer {
+ public FixedZoomTileLayer(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected RectF getViewPort(ImmutableViewportMetrics viewportMetrics) {
+ float zoom = getZoom(viewportMetrics);
+ RectF rect = normalizeRect(viewportMetrics.getViewport(), viewportMetrics.zoomFactor, zoom);
+ return inflate(roundToTileSize(rect, tileSize), getInflateFactor());
+ }
+
+ @Override
+ protected float getZoom(ImmutableViewportMetrics viewportMetrics) {
+ return 1.0f / 16.0f;
+ }
+
+ @Override
+ protected int getTilePriority() {
+ return -1;
+ }
+
+ private IntSize getInflateFactor() {
+ return new IntSize(tileSize.width, tileSize.height*6);
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/FloatSize.java b/android/source/src/java/org/mozilla/gecko/gfx/FloatSize.java
new file mode 100644
index 0000000000..7b18373115
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/FloatSize.java
@@ -0,0 +1,53 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.util.FloatUtils;
+
+public class FloatSize {
+ public final float width, height;
+
+ public FloatSize(FloatSize size) { width = size.width; height = size.height; }
+ public FloatSize(IntSize size) { width = size.width; height = size.height; }
+ public FloatSize(float aWidth, float aHeight) { width = aWidth; height = aHeight; }
+
+ public FloatSize(JSONObject json) {
+ try {
+ width = (float)json.getDouble("width");
+ height = (float)json.getDouble("height");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String toString() { return "(" + width + "," + height + ")"; }
+
+ public boolean isPositive() {
+ return (width > 0 && height > 0);
+ }
+
+ public boolean fuzzyEquals(FloatSize size) {
+ return (FloatUtils.fuzzyEquals(size.width, width) &&
+ FloatUtils.fuzzyEquals(size.height, height));
+ }
+
+ public FloatSize scale(float factor) {
+ return new FloatSize(width * factor, height * factor);
+ }
+
+ /*
+ * Returns the size that represents a linear transition between this size and `to` at time `t`,
+ * which is on the scale [0, 1).
+ */
+ public FloatSize interpolate(FloatSize to, float t) {
+ return new FloatSize(FloatUtils.interpolate(width, to.width, t),
+ FloatUtils.interpolate(height, to.height, t));
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/GLController.java b/android/source/src/java/org/mozilla/gecko/gfx/GLController.java
new file mode 100644
index 0000000000..6a43dd6a87
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/GLController.java
@@ -0,0 +1,215 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGL11;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL;
+import javax.microedition.khronos.opengles.GL10;
+
+public class GLController {
+ private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+ private static final String LOGTAG = "GeckoGLController";
+
+ private LayerView mView;
+ private int mGLVersion;
+ private int mWidth, mHeight;
+
+ private EGL10 mEGL;
+ private EGLDisplay mEGLDisplay;
+ private EGLConfig mEGLConfig;
+ private EGLContext mEGLContext;
+ private EGLSurface mEGLSurface;
+
+ private static final int LOCAL_EGL_OPENGL_ES2_BIT = 4;
+
+ private static final int[] CONFIG_SPEC = {
+ EGL10.EGL_RED_SIZE, 5,
+ EGL10.EGL_GREEN_SIZE, 6,
+ EGL10.EGL_BLUE_SIZE, 5,
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
+ EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT,
+ EGL10.EGL_NONE
+ };
+
+ public GLController(LayerView view) {
+ mView = view;
+ mGLVersion = 2;
+ }
+
+ public void setGLVersion(int version) {
+ mGLVersion = version;
+ }
+
+ /** You must call this on the same thread you intend to use OpenGL on. */
+ public void initGLContext() {
+ initEGLContext();
+ createEGLSurface();
+ }
+
+ public void disposeGLContext() {
+ if (mEGL == null) {
+ return;
+ }
+
+ if (!mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_CONTEXT)) {
+ throw new GLControllerException("EGL context could not be released! " +
+ getEGLError());
+ }
+
+ if (mEGLSurface != null) {
+ if (!mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface)) {
+ throw new GLControllerException("EGL surface could not be destroyed! " +
+ getEGLError());
+ }
+
+ mEGLSurface = null;
+ }
+
+ if (mEGLContext != null) {
+ if (!mEGL.eglDestroyContext(mEGLDisplay, mEGLContext)) {
+ throw new GLControllerException("EGL context could not be destroyed! " +
+ getEGLError());
+ }
+
+ mEGLContext = null;
+ }
+ }
+
+ public GL10 getGL() { return (GL10) mEGLContext.getGL(); }
+ public EGLDisplay getEGLDisplay() { return mEGLDisplay; }
+ public EGLConfig getEGLConfig() { return mEGLConfig; }
+ public EGLContext getEGLContext() { return mEGLContext; }
+ public EGLSurface getEGLSurface() { return mEGLSurface; }
+ public LayerView getView() { return mView; }
+
+ public boolean hasSurface() {
+ return mEGLSurface != null;
+ }
+
+ public boolean swapBuffers() {
+ return mEGL.eglSwapBuffers(mEGLDisplay, mEGLSurface);
+ }
+
+ public synchronized int getWidth() {
+ return mWidth;
+ }
+
+ public synchronized int getHeight() {
+ return mHeight;
+ }
+
+ synchronized void surfaceDestroyed() {
+ notifyAll();
+ }
+
+ synchronized void surfaceChanged(int newWidth, int newHeight) {
+ mWidth = newWidth;
+ mHeight = newHeight;
+ notifyAll();
+ }
+
+ private void initEGL() {
+ mEGL = (EGL10)EGLContext.getEGL();
+
+ mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+ if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
+ throw new GLControllerException("eglGetDisplay() failed");
+ }
+
+ int[] version = new int[2];
+ if (!mEGL.eglInitialize(mEGLDisplay, version)) {
+ throw new GLControllerException("eglInitialize() failed " + getEGLError());
+ }
+
+ mEGLConfig = chooseConfig();
+ }
+
+ private void initEGLContext() {
+ initEGL();
+
+ int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, mGLVersion, EGL10.EGL_NONE };
+ mEGLContext = mEGL.eglCreateContext(mEGLDisplay, mEGLConfig, EGL10.EGL_NO_CONTEXT,
+ attribList);
+ if (mEGLContext == null || mEGLContext == EGL10.EGL_NO_CONTEXT) {
+ throw new GLControllerException("createContext() failed " +
+ getEGLError());
+ }
+
+ if (mView.getRenderer() != null) {
+ GL10 gl = (GL10) mEGLContext.getGL();
+ mView.getRenderer().onSurfaceCreated(gl, mEGLConfig);
+ mView.getRenderer().onSurfaceChanged(gl, mWidth, mHeight);
+ }
+ }
+
+ private EGLConfig chooseConfig() {
+ int[] numConfigs = new int[1];
+ if (!mEGL.eglChooseConfig(mEGLDisplay, CONFIG_SPEC, null, 0, numConfigs) ||
+ numConfigs[0] <= 0) {
+ throw new GLControllerException("No available EGL configurations " +
+ getEGLError());
+ }
+
+ EGLConfig[] configs = new EGLConfig[numConfigs[0]];
+ if (!mEGL.eglChooseConfig(mEGLDisplay, CONFIG_SPEC, configs, numConfigs[0], numConfigs)) {
+ throw new GLControllerException("No EGL configuration for that specification " +
+ getEGLError());
+ }
+
+ // Select the first 565 RGB configuration.
+ int[] red = new int[1], green = new int[1], blue = new int[1];
+ for (EGLConfig config : configs) {
+ mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_RED_SIZE, red);
+ mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_GREEN_SIZE, green);
+ mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_BLUE_SIZE, blue);
+ if (red[0] == 5 && green[0] == 6 && blue[0] == 5) {
+ return config;
+ }
+ }
+
+ // if there's no 565 RGB configuration, select another one that fulfils the specification
+ return configs[0];
+ }
+
+ private void createEGLSurface() {
+ Object window = mView.getNativeWindow();
+ mEGLSurface = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, window, null);
+ if (mEGLSurface == null || mEGLSurface == EGL10.EGL_NO_SURFACE) {
+ throw new GLControllerException("EGL window surface could not be created! " +
+ getEGLError());
+ }
+
+ if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
+ throw new GLControllerException("EGL surface could not be made into the current " +
+ "surface! " + getEGLError());
+ }
+
+ if (mView.getRenderer() != null) {
+ GL10 gl = (GL10) mEGLContext.getGL();
+ mView.getRenderer().onSurfaceCreated(gl, mEGLConfig);
+ mView.getRenderer().onSurfaceChanged(gl, mView.getWidth(), mView.getHeight());
+ }
+ }
+
+ private String getEGLError() {
+ return "Error " + mEGL.eglGetError();
+ }
+
+ public static class GLControllerException extends RuntimeException {
+ public static final long serialVersionUID = 1L;
+
+ GLControllerException(String e) {
+ super(e);
+ }
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
new file mode 100644
index 0000000000..72a96f0bb0
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -0,0 +1,356 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import org.libreoffice.LibreOfficeMainActivity;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+
+import org.libreoffice.LOKitShell;
+import org.mozilla.gecko.ZoomConstraints;
+
+import java.util.List;
+
+public class GeckoLayerClient implements PanZoomTarget {
+ private static final String LOGTAG = GeckoLayerClient.class.getSimpleName();
+
+ private LayerRenderer mLayerRenderer;
+
+ private LibreOfficeMainActivity mContext;
+ private IntSize mScreenSize;
+ private DisplayPortMetrics mDisplayPort;
+
+ private ComposedTileLayer mLowResLayer;
+ private ComposedTileLayer mRootLayer;
+
+ private boolean mForceRedraw;
+
+ /* The current viewport metrics.
+ * This is volatile so that we can read and write to it from different threads.
+ * We avoid synchronization to make getting the viewport metrics from
+ * the compositor as cheap as possible. The viewport is immutable so
+ * we don't need to worry about anyone mutating it while we're reading from it.
+ * Specifically:
+ * 1) reading mViewportMetrics from any thread is fine without synchronization
+ * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
+ * 3) whenever reading multiple fields from mViewportMetrics without synchronization (i.e. in
+ * case 1 above) you should always first grab a local copy of the reference, and then use
+ * that because mViewportMetrics might get reassigned in between reading the different
+ * fields. */
+ private volatile ImmutableViewportMetrics mViewportMetrics;
+
+ private ZoomConstraints mZoomConstraints;
+
+ private boolean mIsReady;
+
+ private PanZoomController mPanZoomController;
+ private LayerView mView;
+ private final DisplayPortCalculator mDisplayPortCalculator;
+
+ public GeckoLayerClient(LibreOfficeMainActivity context) {
+ // we can fill these in with dummy values because they are always written
+ // to before being read
+ mContext = context;
+ mScreenSize = new IntSize(0, 0);
+ mDisplayPort = new DisplayPortMetrics();
+ mDisplayPortCalculator = new DisplayPortCalculator(mContext);
+
+ mForceRedraw = true;
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ mViewportMetrics = new ImmutableViewportMetrics(displayMetrics);
+ }
+
+ public void setView(LayerView view) {
+ mView = view;
+ mPanZoomController = PanZoomController.Factory.create(mContext, this, view);
+ mView.connect(this);
+ }
+
+ public void notifyReady() {
+ mIsReady = true;
+
+ mRootLayer = new DynamicTileLayer(mContext);
+ mLowResLayer = new FixedZoomTileLayer(mContext);
+
+ mLayerRenderer = new LayerRenderer(mView);
+
+ mView.setLayerRenderer(mLayerRenderer);
+
+ sendResizeEventIfNecessary(false);
+ mView.requestRender();
+ }
+
+ public void destroy() {
+ mPanZoomController.destroy();
+ }
+
+ Layer getRoot() {
+ return mIsReady ? mRootLayer : null;
+ }
+
+ Layer getLowResLayer() {
+ return mIsReady ? mLowResLayer : null;
+ }
+
+ public LayerView getView() {
+ return mView;
+ }
+
+ /**
+ * 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.
+ */
+ private boolean getRedrawHint() {
+ if (mForceRedraw) {
+ mForceRedraw = false;
+ return true;
+ }
+
+ if (!mPanZoomController.getRedrawHint()) {
+ return false;
+ }
+ return mDisplayPortCalculator.aboutToCheckerboard(mViewportMetrics, mPanZoomController.getVelocityVector(), getDisplayPort());
+ }
+
+ /**
+ * The view calls this function to indicate that the viewport changed size. It must hold the
+ * monitor while calling it.
+ *
+ * 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.
+ */
+ void setViewportSize(FloatSize size, boolean forceResizeEvent) {
+ mViewportMetrics = mViewportMetrics.setViewportSize(size.width, size.height);
+ sendResizeEventIfNecessary(forceResizeEvent);
+ }
+
+ PanZoomController getPanZoomController() {
+ return mPanZoomController;
+ }
+
+ /* Informs Gecko that the screen size has changed.
+ * @param force: If true, a resize event will always be sent, otherwise
+ * it is only sent if size has changed. */
+ private void sendResizeEventIfNecessary(boolean force) {
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+ IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
+
+ if (!force && mScreenSize.equals(newScreenSize)) {
+ return;
+ }
+
+ mScreenSize = newScreenSize;
+
+ LOKitShell.sendSizeChangedEvent(mScreenSize.width, mScreenSize.height);
+ }
+
+ /**
+ * Sets the current page rect. You must hold the monitor while calling this.
+ */
+ private void setPageRect(RectF rect, RectF cssRect) {
+ // Since the "rect" is always just a multiple of "cssRect" we don't need to
+ // check both; this function assumes that both "rect" and "cssRect" are relative
+ // the zoom factor in mViewportMetrics.
+ if (mViewportMetrics.getCssPageRect().equals(cssRect))
+ return;
+
+ mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect);
+
+ // Page size is owned by the layer client, so no need to notify it of
+ // this change.
+
+ post(new Runnable() {
+ public void run() {
+ mPanZoomController.pageRectUpdated();
+ mView.requestRender();
+ }
+ });
+ }
+
+ private void adjustViewport(DisplayPortMetrics displayPort) {
+ ImmutableViewportMetrics metrics = getViewportMetrics();
+
+ ImmutableViewportMetrics clampedMetrics = metrics.clamp();
+
+ if (displayPort == null) {
+ displayPort = mDisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector());
+ }
+
+ mDisplayPort = displayPort;
+
+ reevaluateTiles();
+ }
+
+ /**
+ * Aborts any pan/zoom animation that is currently in progress.
+ */
+ public void abortPanZoomAnimation() {
+ if (mPanZoomController != null) {
+ mView.post(new Runnable() {
+ public void run() {
+ mPanZoomController.abortAnimation();
+ }
+ });
+ }
+ }
+
+ public void setZoomConstraints(ZoomConstraints constraints) {
+ mZoomConstraints = constraints;
+ }
+
+ /** The compositor invokes this function whenever it determines that the page rect
+ * has changed (based on the information it gets from layout). If setFirstPaintViewport
+ * is invoked on a frame, then this function will not be. For any given frame, this
+ * function will be invoked before syncViewportInfo.
+ */
+ public void setPageRect(float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
+ synchronized (getLock()) {
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ float ourZoom = getViewportMetrics().zoomFactor;
+ setPageRect(RectUtils.scale(cssPageRect, ourZoom), cssPageRect);
+ // Here the page size of the document has changed, but the document being displayed
+ // is still the same. Therefore, we don't need to send anything to browser.js; any
+ // changes we need to make to the display port will get sent the next time we call
+ // adjustViewport().
+ }
+ }
+
+ private DisplayPortMetrics getDisplayPort() {
+ return mDisplayPort;
+ }
+
+ public void beginDrawing() {
+ mLowResLayer.beginTransaction();
+ mRootLayer.beginTransaction();
+ }
+
+ public void endDrawing() {
+ mLowResLayer.endTransaction();
+ mRootLayer.endTransaction();
+ }
+
+ private void geometryChanged() {
+ sendResizeEventIfNecessary(false);
+ if (getRedrawHint()) {
+ adjustViewport(null);
+ }
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public ImmutableViewportMetrics getViewportMetrics() {
+ return mViewportMetrics;
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public ZoomConstraints getZoomConstraints() {
+ return mZoomConstraints;
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void setAnimationTarget(ImmutableViewportMetrics viewport) {
+ if (mIsReady) {
+ // We know what the final viewport of the animation is going to be, so
+ // immediately request a draw of that area by setting the display port
+ // accordingly. This way we should have the content pre-rendered by the
+ // time the animation is done.
+ DisplayPortMetrics displayPort = mDisplayPortCalculator.calculate(viewport, null);
+ adjustViewport(displayPort);
+ }
+ }
+
+ /** Implementation of PanZoomTarget
+ * You must hold the monitor while calling this.
+ */
+ @Override
+ public void setViewportMetrics(ImmutableViewportMetrics viewport) {
+ mViewportMetrics = viewport;
+ mView.requestRender();
+ if (mIsReady) {
+ geometryChanged();
+ }
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void forceRedraw() {
+ mForceRedraw = true;
+ if (mIsReady) {
+ geometryChanged();
+ }
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public boolean post(Runnable action) {
+ return mView.post(action);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public Object getLock() {
+ return this;
+ }
+
+ public PointF convertViewPointToLayerPoint(PointF viewPoint) {
+ ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+ PointF origin = viewportMetrics.getOrigin();
+ float zoom = viewportMetrics.zoomFactor;
+
+ return new PointF(
+ ((viewPoint.x + origin.x) / zoom),
+ ((viewPoint.y + origin.y) / zoom));
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public boolean isFullScreen() {
+ return false;
+ }
+
+ public void zoomTo(RectF rect) {
+ if (mPanZoomController instanceof JavaPanZoomController) {
+ ((JavaPanZoomController) mPanZoomController).animatedZoomTo(rect);
+ }
+ }
+
+ /**
+ * Move the viewport to the desired point, and change the zoom level.
+ */
+ public void moveTo(PointF point, Float zoom) {
+ if (mPanZoomController instanceof JavaPanZoomController) {
+ ((JavaPanZoomController) mPanZoomController).animatedMove(point, zoom);
+ }
+ }
+
+ public void zoomTo(float pageWidth, float pageHeight) {
+ zoomTo(new RectF(0, 0, pageWidth, pageHeight));
+ }
+
+ public void forceRender() {
+ mView.requestRender();
+ }
+
+ /* Root Layer Access */
+ private void reevaluateTiles() {
+ mLowResLayer.reevaluateTiles(mViewportMetrics, mDisplayPort);
+ mRootLayer.reevaluateTiles(mViewportMetrics, mDisplayPort);
+ }
+
+ public void clearAndResetlayers() {
+ mLowResLayer.clearAndReset();
+ mRootLayer.clearAndReset();
+ }
+
+ public void invalidateTiles(List<SubTile> tilesToInvalidate, RectF rect) {
+ mLowResLayer.invalidateTiles(tilesToInvalidate, rect);
+ mRootLayer.invalidateTiles(tilesToInvalidate, rect);
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java b/android/source/src/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
new file mode 100644
index 0000000000..f90580fbee
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
@@ -0,0 +1,241 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+/**
+ * ImmutableViewportMetrics are used to store the viewport metrics
+ * in way that we can access a version of them from multiple threads
+ * without having to take a lock
+ */
+public class ImmutableViewportMetrics {
+
+ // We need to flatten the RectF and FloatSize structures
+ // because Java doesn't have the concept of const classes
+ public final float pageRectLeft;
+ public final float pageRectTop;
+ public final float pageRectRight;
+ public final float pageRectBottom;
+ public final float cssPageRectLeft;
+ public final float cssPageRectTop;
+ public final float cssPageRectRight;
+ public final float cssPageRectBottom;
+ public final float viewportRectLeft;
+ public final float viewportRectTop;
+ public final float viewportRectRight;
+ public final float viewportRectBottom;
+ public final float zoomFactor;
+
+ public ImmutableViewportMetrics(DisplayMetrics metrics) {
+ viewportRectLeft = pageRectLeft = cssPageRectLeft = 0;
+ viewportRectTop = pageRectTop = cssPageRectTop = 0;
+ viewportRectRight = pageRectRight = cssPageRectRight = metrics.widthPixels;
+ viewportRectBottom = pageRectBottom = cssPageRectBottom = metrics.heightPixels;
+ zoomFactor = 1.0f;
+ }
+
+ private ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
+ float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
+ float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
+ float aViewportRectLeft, float aViewportRectTop, float aViewportRectRight,
+ float aViewportRectBottom, float aZoomFactor)
+ {
+ pageRectLeft = aPageRectLeft;
+ pageRectTop = aPageRectTop;
+ pageRectRight = aPageRectRight;
+ pageRectBottom = aPageRectBottom;
+ cssPageRectLeft = aCssPageRectLeft;
+ cssPageRectTop = aCssPageRectTop;
+ cssPageRectRight = aCssPageRectRight;
+ cssPageRectBottom = aCssPageRectBottom;
+ viewportRectLeft = aViewportRectLeft;
+ viewportRectTop = aViewportRectTop;
+ viewportRectRight = aViewportRectRight;
+ viewportRectBottom = aViewportRectBottom;
+ zoomFactor = aZoomFactor;
+ }
+
+ public float getWidth() {
+ return viewportRectRight - viewportRectLeft;
+ }
+
+ public float getHeight() {
+ return viewportRectBottom - viewportRectTop;
+ }
+
+ public PointF getOrigin() {
+ return new PointF(viewportRectLeft, viewportRectTop);
+ }
+
+ public FloatSize getSize() {
+ return new FloatSize(viewportRectRight - viewportRectLeft, viewportRectBottom - viewportRectTop);
+ }
+
+ public RectF getViewport() {
+ return new RectF(viewportRectLeft,
+ viewportRectTop,
+ viewportRectRight,
+ viewportRectBottom);
+ }
+
+ public RectF getCssViewport() {
+ return RectUtils.scale(getViewport(), 1/zoomFactor);
+ }
+
+ public RectF getPageRect() {
+ return new RectF(pageRectLeft, pageRectTop, pageRectRight, pageRectBottom);
+ }
+
+ public float getPageWidth() {
+ return pageRectRight - pageRectLeft;
+ }
+
+ public float getPageHeight() {
+ return pageRectBottom - pageRectTop;
+ }
+
+ public RectF getCssPageRect() {
+ return new RectF(cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom);
+ }
+
+ public float getZoomFactor() {
+ return zoomFactor;
+ }
+
+ /*
+ * Returns the viewport metrics that represent a linear transition between "this" and "to" at
+ * time "t", which is on the scale [0, 1). This function interpolates all values stored in
+ * the viewport metrics.
+ */
+ public ImmutableViewportMetrics interpolate(ImmutableViewportMetrics to, float t) {
+ return new ImmutableViewportMetrics(
+ FloatUtils.interpolate(pageRectLeft, to.pageRectLeft, t),
+ FloatUtils.interpolate(pageRectTop, to.pageRectTop, t),
+ FloatUtils.interpolate(pageRectRight, to.pageRectRight, t),
+ FloatUtils.interpolate(pageRectBottom, to.pageRectBottom, t),
+ FloatUtils.interpolate(cssPageRectLeft, to.cssPageRectLeft, t),
+ FloatUtils.interpolate(cssPageRectTop, to.cssPageRectTop, t),
+ FloatUtils.interpolate(cssPageRectRight, to.cssPageRectRight, t),
+ FloatUtils.interpolate(cssPageRectBottom, to.cssPageRectBottom, t),
+ FloatUtils.interpolate(viewportRectLeft, to.viewportRectLeft, t),
+ FloatUtils.interpolate(viewportRectTop, to.viewportRectTop, t),
+ FloatUtils.interpolate(viewportRectRight, to.viewportRectRight, t),
+ FloatUtils.interpolate(viewportRectBottom, to.viewportRectBottom, t),
+ FloatUtils.interpolate(zoomFactor, to.zoomFactor, t));
+ }
+
+ public ImmutableViewportMetrics setViewportSize(float width, float height) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectLeft + width, viewportRectTop + height,
+ zoomFactor);
+ }
+
+ public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newOriginX, newOriginY, newOriginX + getWidth(), newOriginY + getHeight(),
+ zoomFactor);
+ }
+
+ public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom,
+ newZoomFactor);
+ }
+
+ public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
+ return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
+ }
+
+ public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) {
+ return new ImmutableViewportMetrics(
+ pageRect.left, pageRect.top, pageRect.right, pageRect.bottom,
+ cssPageRect.left, cssPageRect.top, cssPageRect.right, cssPageRect.bottom,
+ viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom,
+ zoomFactor);
+ }
+
+ /* This will set the zoom factor and re-scale page-size and viewport offset
+ * accordingly. The given focus will remain at the same point on the screen
+ * after scaling.
+ */
+ public ImmutableViewportMetrics scaleTo(float newZoomFactor, PointF focus) {
+ // cssPageRect* is invariant, since we're setting the scale factor
+ // here. The page rect is based on the CSS page rect.
+ float newPageRectLeft = cssPageRectLeft * newZoomFactor;
+ float newPageRectTop = cssPageRectTop * newZoomFactor;
+ float newPageRectRight = cssPageRectLeft + ((cssPageRectRight - cssPageRectLeft) * newZoomFactor);
+ float newPageRectBottom = cssPageRectTop + ((cssPageRectBottom - cssPageRectTop) * newZoomFactor);
+
+ PointF origin = getOrigin();
+ origin.offset(focus.x, focus.y);
+ origin = PointUtils.scale(origin, newZoomFactor / zoomFactor);
+ origin.offset(-focus.x, -focus.y);
+
+ return new ImmutableViewportMetrics(
+ newPageRectLeft, newPageRectTop, newPageRectRight, newPageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ origin.x, origin.y, origin.x + getWidth(), origin.y + getHeight(),
+ newZoomFactor);
+ }
+
+ /** Clamps the viewport to remain within the page rect. */
+ public ImmutableViewportMetrics clamp() {
+ RectF newViewport = getViewport();
+
+ // The viewport bounds ought to never exceed the page bounds.
+ if (newViewport.right > pageRectRight)
+ newViewport.offset(pageRectRight - newViewport.right, 0);
+ if (newViewport.left < pageRectLeft)
+ newViewport.offset(pageRectLeft - newViewport.left, 0);
+
+ if (newViewport.bottom > pageRectBottom)
+ newViewport.offset(0, pageRectBottom - newViewport.bottom);
+ if (newViewport.top < pageRectTop)
+ newViewport.offset(0, pageRectTop - newViewport.top);
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newViewport.left, newViewport.top, newViewport.right, newViewport.bottom,
+ zoomFactor);
+ }
+
+ public boolean fuzzyEquals(ImmutableViewportMetrics other) {
+ return FloatUtils.fuzzyEquals(pageRectLeft, other.pageRectLeft)
+ && FloatUtils.fuzzyEquals(pageRectTop, other.pageRectTop)
+ && FloatUtils.fuzzyEquals(pageRectRight, other.pageRectRight)
+ && FloatUtils.fuzzyEquals(pageRectBottom, other.pageRectBottom)
+ && FloatUtils.fuzzyEquals(cssPageRectLeft, other.cssPageRectLeft)
+ && FloatUtils.fuzzyEquals(cssPageRectTop, other.cssPageRectTop)
+ && FloatUtils.fuzzyEquals(cssPageRectRight, other.cssPageRectRight)
+ && FloatUtils.fuzzyEquals(cssPageRectBottom, other.cssPageRectBottom)
+ && FloatUtils.fuzzyEquals(viewportRectLeft, other.viewportRectLeft)
+ && FloatUtils.fuzzyEquals(viewportRectTop, other.viewportRectTop)
+ && FloatUtils.fuzzyEquals(viewportRectRight, other.viewportRectRight)
+ && FloatUtils.fuzzyEquals(viewportRectBottom, other.viewportRectBottom)
+ && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
+ }
+
+ @Override
+ public String toString() {
+ return "ImmutableViewportMetrics v=(" + viewportRectLeft + "," + viewportRectTop + ","
+ + viewportRectRight + "," + viewportRectBottom + ") p=(" + pageRectLeft + ","
+ + pageRectTop + "," + pageRectRight + "," + pageRectBottom + ") c=("
+ + cssPageRectLeft + "," + cssPageRectTop + "," + cssPageRectRight + ","
+ + cssPageRectBottom + ") z=" + zoomFactor;
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java b/android/source/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java
new file mode 100644
index 0000000000..d460c19e1c
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java
@@ -0,0 +1,15 @@
+package org.mozilla.gecko.gfx;
+
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+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/android/source/src/java/org/mozilla/gecko/gfx/IntSize.java b/android/source/src/java/org/mozilla/gecko/gfx/IntSize.java
new file mode 100644
index 0000000000..b0741d2f68
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/IntSize.java
@@ -0,0 +1,73 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class IntSize {
+ public final int width, height;
+
+ public IntSize(IntSize size) { width = size.width; height = size.height; }
+ public IntSize(int inWidth, int inHeight) { width = inWidth; height = inHeight; }
+
+ public IntSize(FloatSize size) {
+ width = Math.round(size.width);
+ height = Math.round(size.height);
+ }
+
+ public IntSize(JSONObject json) {
+ try {
+ width = json.getInt("width");
+ height = json.getInt("height");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getArea() {
+ return width * height;
+ }
+
+ public boolean equals(IntSize size) {
+ return ((size.width == width) && (size.height == height));
+ }
+
+ public boolean isPositive() {
+ return (width > 0 && height > 0);
+ }
+
+ @Override
+ public String toString() { return "(" + width + "," + height + ")"; }
+
+ public IntSize scale(float factor) {
+ return new IntSize(Math.round(width * factor),
+ Math.round(height * factor));
+ }
+
+ /* Returns the power of two that is greater than or equal to value */
+ public static int nextPowerOfTwo(int value) {
+ // code taken from http://acius2.blogspot.com/2007/11/calculating-next-power-of-2.html
+ if (0 == value--) {
+ return 1;
+ }
+ value = (value >> 1) | value;
+ value = (value >> 2) | value;
+ value = (value >> 4) | value;
+ value = (value >> 8) | value;
+ value = (value >> 16) | value;
+ return value + 1;
+ }
+
+ public static int nextPowerOfTwo(float value) {
+ return nextPowerOfTwo((int) value);
+ }
+
+ public IntSize nextPowerOfTwo() {
+ return new IntSize(nextPowerOfTwo(width), nextPowerOfTwo(height));
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java b/android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
new file mode 100644
index 0000000000..b20d602a21
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
@@ -0,0 +1,1087 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.mozilla.gecko.ZoomConstraints;
+import org.mozilla.gecko.util.FloatUtils;
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.lang.StrictMath;
+
+/*
+ * Handles the kinetic scrolling and zooming physics for a layer controller.
+ *
+ * Many ideas are from Joe Hewitt's Scrollability:
+ * https://github.com/joehewitt/scrollability/
+ */
+class JavaPanZoomController
+ extends GestureDetector.SimpleOnGestureListener
+ implements PanZoomController, SimpleScaleGestureDetector.SimpleScaleGestureListener
+{
+ private static final String LOGTAG = "GeckoPanZoomController";
+
+ // Animation stops if the velocity is below this value when overscrolled or panning.
+ private static final float STOPPED_THRESHOLD = 4.0f;
+
+ // Animation stops is the velocity is below this threshold when flinging.
+ private static final float FLING_STOPPED_THRESHOLD = 0.1f;
+
+ // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
+ // between the touch-down and touch-up of a click). In units of density-independent pixels.
+ private final float PAN_THRESHOLD;
+
+ // Angle from axis within which we stay axis-locked
+ private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees
+
+ // The maximum amount we allow you to zoom into a page
+ private static final float MAX_ZOOM = 4.0f;
+
+ // The threshold zoom factor of whether a double tap triggers zoom-in or zoom-out
+ private static final float DOUBLE_TAP_THRESHOLD = 1.0f;
+
+ // The maximum amount we would like to scroll with the mouse
+ private final float MAX_SCROLL;
+
+ 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_LOCKED, /* touch-start followed by move (i.e. panning with axis lock) */
+ PANNING, /* panning without axis lock */
+ PANNING_HOLD, /* in panning, but not moving.
+ * similar to TOUCHING but after starting a pan */
+ PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */
+ PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
+ ANIMATED_ZOOM, /* animated zoom to a new rect */
+ BOUNCE, /* in a bounce animation */
+
+ WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has
+ put a finger down, but we don't yet know if a touch listener has
+ prevented the default actions yet. we still need to abort animations. */
+ }
+
+ private final PanZoomTarget mTarget;
+ private final SubdocumentScrollHelper mSubscroller;
+ private final Axis mX;
+ private final Axis mY;
+ private final TouchEventHandler mTouchEventHandler;
+ private Thread mMainThread;
+ private LibreOfficeMainActivity mContext;
+
+ /* The timer that handles flings or bounces. */
+ private Timer mAnimationTimer;
+ /* The runnable being scheduled by the animation timer. */
+ private AnimationRunnable mAnimationRunnable;
+ /* The zoom focus at the first zoom event (in page coordinates). */
+ private PointF mLastZoomFocus;
+ /* The time the last motion event took place. */
+ private long mLastEventTime;
+ /* Current state the pan/zoom UI is in. */
+ private PanZoomState mState;
+ /* Whether or not to wait for a double-tap before dispatching a single-tap */
+ private boolean mWaitForDoubleTap;
+
+ JavaPanZoomController(LibreOfficeMainActivity context, PanZoomTarget target, View view) {
+ mContext = context;
+ PAN_THRESHOLD = 1/16f * LOKitShell.getDpi(view.getContext());
+ MAX_SCROLL = 0.075f * LOKitShell.getDpi(view.getContext());
+ mTarget = target;
+ mSubscroller = new SubdocumentScrollHelper();
+ mX = new AxisX(mSubscroller);
+ mY = new AxisY(mSubscroller);
+ mTouchEventHandler = new TouchEventHandler(view.getContext(), view, this);
+
+ mMainThread = mContext.getMainLooper().getThread();
+ checkMainThread();
+
+ setState(PanZoomState.NOTHING);
+ }
+
+ public void destroy() {
+ mSubscroller.destroy();
+ mTouchEventHandler.destroy();
+ }
+
+ private static float easeOut(float t) {
+ // ease-out approx.
+ // -(t-1)^2+1
+ t = t-1;
+ return -t*t+1;
+ }
+
+ private void setState(PanZoomState state) {
+ if (state != mState) {
+ mState = state;
+ }
+ }
+
+ private ImmutableViewportMetrics getMetrics() {
+ return mTarget.getViewportMetrics();
+ }
+
+ // for debugging bug 713011; it can be taken out once that is resolved.
+ private void checkMainThread() {
+ if (mMainThread != Thread.currentThread()) {
+ // log with full stack trace
+ Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception());
+ }
+ }
+
+ /** This function MUST be called on the UI thread */
+ public boolean onMotionEvent(MotionEvent event) {
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_MASK) == InputDevice.SOURCE_CLASS_POINTER
+ && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_SCROLL) {
+ return handlePointerScroll(event);
+ }
+ return false;
+ }
+
+ /** This function MUST be called on the UI thread */
+ public boolean onTouchEvent(MotionEvent event) {
+ return mTouchEventHandler.handleEvent(event);
+ }
+
+ boolean handleEvent(MotionEvent event) {
+ switch (event.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: return handleTouchStart(event);
+ case MotionEvent.ACTION_MOVE: return handleTouchMove(event);
+ case MotionEvent.ACTION_UP: return handleTouchEnd(event);
+ case MotionEvent.ACTION_CANCEL: return handleTouchCancel(event);
+ }
+ return false;
+ }
+
+ /** This function MUST be called on the UI thread */
+ public void notifyDefaultActionPrevented(boolean prevented) {
+ mTouchEventHandler.handleEventListenerAction(!prevented);
+ }
+
+ /** This function must be called from the UI thread. */
+ public void abortAnimation() {
+ checkMainThread();
+ // this happens when gecko changes the viewport on us or if the device is rotated.
+ // if that's the case, abort any animation in progress and re-zoom so that the page
+ // snaps to edges. for other cases (where the user's finger(s) are down) don't do
+ // anything special.
+ switch (mState) {
+ case FLING:
+ mX.stopFling();
+ mY.stopFling();
+ // fall through
+ case BOUNCE:
+ case ANIMATED_ZOOM:
+ // the zoom that's in progress likely makes no sense any more (such as if
+ // the screen orientation changed) so abort it
+ setState(PanZoomState.NOTHING);
+ // fall through
+ case NOTHING:
+ // Don't do animations here; they're distracting and can cause flashes on page
+ // transitions.
+ synchronized (mTarget.getLock()) {
+ mTarget.setViewportMetrics(getValidViewportMetrics());
+ mTarget.forceRedraw();
+ }
+ break;
+ }
+ }
+
+ /** This function must be called on the UI thread. */
+ void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) {
+ checkMainThread();
+ mSubscroller.cancel();
+ if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+ // this is the first touch point going down, so we enter the pending state
+ // setting the state will kill any animations in progress, possibly leaving
+ // the page in overscroll
+ setState(PanZoomState.WAITING_LISTENERS);
+ }
+ }
+
+ /** This function must be called on the UI thread. */
+ void preventedTouchFinished() {
+ checkMainThread();
+ if (mState == PanZoomState.WAITING_LISTENERS) {
+ // if we enter here, we just finished a block of events whose default actions
+ // were prevented by touch listeners. Now there are no touch points left, so
+ // we need to reset our state and re-bounce because we might be in overscroll
+ bounce();
+ }
+ }
+
+ /** This must be called on the UI thread. */
+ public void pageRectUpdated() {
+ if (mState == PanZoomState.NOTHING) {
+ synchronized (mTarget.getLock()) {
+ ImmutableViewportMetrics validated = getValidViewportMetrics();
+ if (!getMetrics().fuzzyEquals(validated)) {
+ // page size changed such that we are now in overscroll. snap to
+ // the nearest valid viewport
+ mTarget.setViewportMetrics(validated);
+ }
+ }
+ }
+ }
+
+ /*
+ * Panning/scrolling
+ */
+
+ private boolean handleTouchStart(MotionEvent event) {
+ // user is taking control of movement, so stop
+ // any auto-movement we have going
+ stopAnimationTimer();
+
+ switch (mState) {
+ case ANIMATED_ZOOM:
+ // We just interrupted a double-tap animation, so force a redraw in
+ // case this touchstart is just a tap that doesn't end up triggering
+ // a redraw
+ mTarget.forceRedraw();
+ // fall through
+ case FLING:
+ case BOUNCE:
+ case NOTHING:
+ case WAITING_LISTENERS:
+ startTouch(event.getX(0), event.getY(0), event.getEventTime());
+ return false;
+ case TOUCHING:
+ case PANNING:
+ case PANNING_LOCKED:
+ case PANNING_HOLD:
+ case PANNING_HOLD_LOCKED:
+ case PINCHING:
+ Log.e(LOGTAG, "Received impossible touch down while in " + mState);
+ return false;
+ }
+ Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchStart");
+ return false;
+ }
+
+ private boolean handleTouchMove(MotionEvent event) {
+ if (mState == PanZoomState.PANNING_LOCKED || mState == PanZoomState.PANNING) {
+ if (getVelocity() > 18.0f) {
+ mContext.hideSoftKeyboard();
+ }
+ }
+
+ switch (mState) {
+ case FLING:
+ case BOUNCE:
+ case WAITING_LISTENERS:
+ // should never happen
+ Log.e(LOGTAG, "Received impossible touch move while in " + mState);
+ // fall through
+ case ANIMATED_ZOOM:
+ case NOTHING:
+ // may happen if user double-taps and drags without lifting after the
+ // second tap. ignore the move if this happens.
+ return false;
+
+ case TOUCHING:
+ // Don't allow panning if there is an element in full-screen mode. See bug 775511.
+ if (mTarget.isFullScreen() || panDistance(event) < PAN_THRESHOLD) {
+ return false;
+ }
+ cancelTouch();
+ startPanning(event.getX(0), event.getY(0), event.getEventTime());
+ track(event);
+ return true;
+
+ case PANNING_HOLD_LOCKED:
+ setState(PanZoomState.PANNING_LOCKED);
+ // fall through
+ case PANNING_LOCKED:
+ track(event);
+ return true;
+
+ case PANNING_HOLD:
+ setState(PanZoomState.PANNING);
+ // fall through
+ case PANNING:
+ track(event);
+ return true;
+
+ case PINCHING:
+ // scale gesture listener will handle this
+ return false;
+ }
+ Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchMove");
+ return false;
+ }
+
+ private boolean handleTouchEnd(MotionEvent event) {
+
+ switch (mState) {
+ case FLING:
+ case BOUNCE:
+ case WAITING_LISTENERS:
+ // should never happen
+ Log.e(LOGTAG, "Received impossible touch end while in " + mState);
+ // fall through
+ case ANIMATED_ZOOM:
+ case NOTHING:
+ // may happen if user double-taps and drags without lifting after the
+ // second tap. ignore if this happens.
+ return false;
+
+ case TOUCHING:
+ // the switch into TOUCHING might have happened while the page was
+ // snapping back after overscroll. we need to finish the snap if that
+ // was the case
+ bounce();
+ return false;
+
+ case PANNING:
+ case PANNING_LOCKED:
+ case PANNING_HOLD:
+ case PANNING_HOLD_LOCKED:
+ setState(PanZoomState.FLING);
+ fling();
+ return true;
+
+ case PINCHING:
+ setState(PanZoomState.NOTHING);
+ return true;
+ }
+ Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchEnd");
+ return false;
+ }
+
+ private boolean handleTouchCancel(MotionEvent event) {
+ cancelTouch();
+
+ if (mState == PanZoomState.WAITING_LISTENERS) {
+ // we might get a cancel event from the TouchEventHandler while in the
+ // WAITING_LISTENERS state if the touch listeners prevent-default the
+ // block of events. at this point being in WAITING_LISTENERS is equivalent
+ // to being in NOTHING with the exception of possibly being in overscroll.
+ // so here we don't want to do anything right now; the overscroll will be
+ // corrected in preventedTouchFinished().
+ return false;
+ }
+
+ // ensure we snap back if we're overscrolled
+ bounce();
+ return false;
+ }
+
+ private boolean handlePointerScroll(MotionEvent event) {
+ if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
+ float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+
+ scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
+ bounce();
+ return true;
+ }
+ return false;
+ }
+
+ private void startTouch(float x, float y, long time) {
+ mX.startTouch(x);
+ mY.startTouch(y);
+ setState(PanZoomState.TOUCHING);
+ mLastEventTime = time;
+ }
+
+ private void startPanning(float x, float y, long time) {
+ float dx = mX.panDistance(x);
+ float dy = mY.panDistance(y);
+ double angle = Math.atan2(dy, dx); // range [-pi, pi]
+ angle = Math.abs(angle); // range [0, pi]
+
+ // When the touch move breaks through the pan threshold, reposition the touch down origin
+ // so the page won't jump when we start panning.
+ mX.startTouch(x);
+ mY.startTouch(y);
+ mLastEventTime = time;
+
+ if (!mX.scrollable() || !mY.scrollable()) {
+ setState(PanZoomState.PANNING);
+ } else if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) {
+ mY.setScrollingDisabled(true);
+ setState(PanZoomState.PANNING_LOCKED);
+ } else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) {
+ mX.setScrollingDisabled(true);
+ setState(PanZoomState.PANNING_LOCKED);
+ } else {
+ setState(PanZoomState.PANNING);
+ }
+ }
+
+ private float panDistance(MotionEvent move) {
+ float dx = mX.panDistance(move.getX(0));
+ float dy = mY.panDistance(move.getY(0));
+ return (float) Math.hypot(dx , dy);
+ }
+
+ private void track(float x, float y, long time) {
+ float timeDelta = (float)(time - mLastEventTime);
+ if (FloatUtils.fuzzyEquals(timeDelta, 0)) {
+ // probably a duplicate event, ignore it. using a zero timeDelta will mess
+ // up our velocity
+ return;
+ }
+ mLastEventTime = time;
+
+ mX.updateWithTouchAt(x, timeDelta);
+ mY.updateWithTouchAt(y, timeDelta);
+ }
+
+ private void track(MotionEvent event) {
+ mX.saveTouchPos();
+ mY.saveTouchPos();
+
+ for (int i = 0; i < event.getHistorySize(); i++) {
+ track(event.getHistoricalX(0, i),
+ event.getHistoricalY(0, i),
+ event.getHistoricalEventTime(i));
+ }
+ track(event.getX(0), event.getY(0), event.getEventTime());
+
+ if (stopped()) {
+ if (mState == PanZoomState.PANNING) {
+ setState(PanZoomState.PANNING_HOLD);
+ } else if (mState == PanZoomState.PANNING_LOCKED) {
+ setState(PanZoomState.PANNING_HOLD_LOCKED);
+ } else {
+ // should never happen, but handle anyway for robustness
+ Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track");
+ setState(PanZoomState.PANNING_HOLD_LOCKED);
+ }
+ }
+
+ mX.startPan();
+ mY.startPan();
+ updatePosition();
+ }
+
+ private void scrollBy(float dx, float dy) {
+ ImmutableViewportMetrics scrolled = getMetrics().offsetViewportBy(dx, dy);
+ mTarget.setViewportMetrics(scrolled);
+ }
+
+ private void fling() {
+ updatePosition();
+
+ stopAnimationTimer();
+
+ boolean stopped = stopped();
+ mX.startFling(stopped);
+ mY.startFling(stopped);
+
+ startAnimationTimer(new FlingRunnable());
+ }
+
+ /* Performs a bounce-back animation to the given viewport metrics. */
+ private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) {
+ stopAnimationTimer();
+
+ ImmutableViewportMetrics bounceStartMetrics = getMetrics();
+ if (bounceStartMetrics.fuzzyEquals(metrics)) {
+ setState(PanZoomState.NOTHING);
+ finishAnimation();
+ return;
+ }
+
+ setState(state);
+
+ // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so
+ // getRedrawHint() is returning false. This means we can safely call
+ // setAnimationTarget to set the new final display port and not have it get
+ // clobbered by display ports from intermediate animation frames.
+ mTarget.setAnimationTarget(metrics);
+ startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics));
+ }
+
+ /* Performs a bounce-back animation to the nearest valid viewport metrics. */
+ private void bounce() {
+ bounce(getValidViewportMetrics(), PanZoomState.BOUNCE);
+ }
+
+ /* Starts the fling or bounce animation. */
+ private void startAnimationTimer(final AnimationRunnable runnable) {
+ if (mAnimationTimer != null) {
+ Log.e(LOGTAG, "Attempted to start a new fling without canceling the old one!");
+ stopAnimationTimer();
+ }
+
+ mAnimationTimer = new Timer("Animation Timer");
+ mAnimationRunnable = runnable;
+ mAnimationTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() { mTarget.post(runnable); }
+ }, 0, (int)Axis.MS_PER_FRAME);
+ }
+
+ /* Stops the fling or bounce animation. */
+ private void stopAnimationTimer() {
+ if (mAnimationTimer != null) {
+ mAnimationTimer.cancel();
+ mAnimationTimer = null;
+ }
+ if (mAnimationRunnable != null) {
+ mAnimationRunnable.terminate();
+ mAnimationRunnable = null;
+ }
+ }
+
+ private float getVelocity() {
+ float xvel = mX.getRealVelocity();
+ float yvel = mY.getRealVelocity();
+ return (float) StrictMath.hypot(xvel, yvel);
+ }
+
+ public PointF getVelocityVector() {
+ return new PointF(mX.getRealVelocity(), mY.getRealVelocity());
+ }
+
+ private boolean stopped() {
+ return getVelocity() < STOPPED_THRESHOLD;
+ }
+
+ private PointF resetDisplacement() {
+ return new PointF(mX.resetDisplacement(), mY.resetDisplacement());
+ }
+
+ private void updatePosition() {
+ mX.displace();
+ mY.displace();
+ PointF displacement = resetDisplacement();
+ if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) {
+ return;
+ }
+ if (! mSubscroller.scrollBy(displacement)) {
+ synchronized (mTarget.getLock()) {
+ scrollBy(displacement.x, displacement.y);
+ }
+ }
+ }
+
+ private abstract class AnimationRunnable implements Runnable {
+ private boolean mAnimationTerminated;
+
+ /* This should always run on the UI thread */
+ public final void run() {
+ /*
+ * Since the animation timer queues this runnable on the UI thread, it
+ * is possible that even when the animation timer is cancelled, there
+ * are multiple instances of this queued, so we need to have another
+ * mechanism to abort. This is done by using the mAnimationTerminated flag.
+ */
+ if (mAnimationTerminated) {
+ return;
+ }
+ animateFrame();
+ }
+
+ protected abstract void animateFrame();
+
+ /* This should always run on the UI thread */
+ final void terminate() {
+ mAnimationTerminated = true;
+ }
+ }
+
+ /* The callback that performs the bounce animation. */
+ private class BounceRunnable extends AnimationRunnable {
+ /* The current frame of the bounce-back animation */
+ private int mBounceFrame;
+ /*
+ * The viewport metrics that represent the start and end of the bounce-back animation,
+ * respectively.
+ */
+ private ImmutableViewportMetrics mBounceStartMetrics;
+ private ImmutableViewportMetrics mBounceEndMetrics;
+
+ BounceRunnable(ImmutableViewportMetrics startMetrics, ImmutableViewportMetrics endMetrics) {
+ mBounceStartMetrics = startMetrics;
+ mBounceEndMetrics = endMetrics;
+ }
+
+ protected void animateFrame() {
+ /*
+ * The pan/zoom controller might have signaled to us that it wants to abort the
+ * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
+ * out.
+ */
+ if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) {
+ finishAnimation();
+ return;
+ }
+
+ /* Perform the next frame of the bounce-back animation. */
+ if (mBounceFrame < (int)(256f/Axis.MS_PER_FRAME)) {
+ advanceBounce();
+ return;
+ }
+
+ /* Finally, if there's nothing else to do, complete the animation and go to sleep. */
+ finishBounce();
+ finishAnimation();
+ setState(PanZoomState.NOTHING);
+ }
+
+ /* Performs one frame of a bounce animation. */
+ private void advanceBounce() {
+ synchronized (mTarget.getLock()) {
+ float t = easeOut(mBounceFrame * Axis.MS_PER_FRAME / 256f);
+ ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t);
+ mTarget.setViewportMetrics(newMetrics);
+ mBounceFrame++;
+ }
+ }
+
+ /* Concludes a bounce animation and snaps the viewport into place. */
+ private void finishBounce() {
+ synchronized (mTarget.getLock()) {
+ mTarget.setViewportMetrics(mBounceEndMetrics);
+ mBounceFrame = -1;
+ }
+ }
+ }
+
+ // The callback that performs the fling animation.
+ private class FlingRunnable extends AnimationRunnable {
+ protected void animateFrame() {
+ /*
+ * The pan/zoom controller might have signaled to us that it wants to abort the
+ * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail
+ * out.
+ */
+ if (mState != PanZoomState.FLING) {
+ finishAnimation();
+ return;
+ }
+
+ /* Advance flings, if necessary. */
+ boolean flingingX = mX.advanceFling();
+ boolean flingingY = mY.advanceFling();
+
+ boolean overscrolled = (mX.overscrolled() || mY.overscrolled());
+
+ /* If we're still flinging in any direction, update the origin. */
+ if (flingingX || flingingY) {
+ updatePosition();
+
+ /*
+ * Check to see if we're still flinging with an appreciable velocity. The threshold is
+ * higher in the case of overscroll, so we bounce back eagerly when overscrolling but
+ * coast smoothly to a stop when not. In other words, require a greater velocity to
+ * maintain the fling once we enter overscroll.
+ */
+ float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD);
+ if (getVelocity() >= threshold) {
+ mContext.getDocumentOverlay().showPageNumberRect();
+ // we're still flinging
+ return;
+ }
+
+ mX.stopFling();
+ mY.stopFling();
+ }
+
+ /* Perform a bounce-back animation if overscrolled. */
+ if (overscrolled) {
+ bounce();
+ } else {
+ finishAnimation();
+ setState(PanZoomState.NOTHING);
+ }
+ }
+ }
+
+ private void finishAnimation() {
+ checkMainThread();
+
+ stopAnimationTimer();
+
+ mContext.getDocumentOverlay().hidePageNumberRect();
+
+ // Force a viewport synchronisation
+ mTarget.forceRedraw();
+ }
+
+ /* Returns the nearest viewport metrics with no overscroll visible. */
+ private ImmutableViewportMetrics getValidViewportMetrics() {
+ return getValidViewportMetrics(getMetrics());
+ }
+
+ private ImmutableViewportMetrics getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics) {
+ /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
+ float zoomFactor = viewportMetrics.zoomFactor;
+ RectF pageRect = viewportMetrics.getPageRect();
+ RectF viewport = viewportMetrics.getViewport();
+
+ float focusX = viewport.width() / 2.0f;
+ float focusY = viewport.height() / 2.0f;
+
+ float minZoomFactor = 0.0f;
+ float maxZoomFactor = MAX_ZOOM;
+
+ ZoomConstraints constraints = mTarget.getZoomConstraints();
+ if (null == constraints) {
+ Log.e(LOGTAG, "ZoomConstraints not available - too impatient?");
+ return viewportMetrics;
+
+ }
+ if (constraints.getMinZoom() > 0)
+ minZoomFactor = constraints.getMinZoom();
+ if (constraints.getMaxZoom() > 0)
+ maxZoomFactor = constraints.getMaxZoom();
+
+ maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);
+
+ if (zoomFactor < minZoomFactor) {
+ // if one (or both) of the page dimensions is smaller than the viewport,
+ // zoom using the top/left as the focus on that axis. this prevents the
+ // scenario where, if both dimensions are smaller than the viewport, but
+ // by different scale factors, we end up scrolled to the end on one axis
+ // after applying the scale
+ PointF center = new PointF(focusX, focusY);
+ viewportMetrics = viewportMetrics.scaleTo(minZoomFactor, center);
+ } else if (zoomFactor > maxZoomFactor) {
+ PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
+ viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center);
+ }
+
+ /* Now we pan to the right origin. */
+ viewportMetrics = viewportMetrics.clamp();
+
+ viewportMetrics = pushPageToCenterOfViewport(viewportMetrics);
+
+ return viewportMetrics;
+ }
+
+ private ImmutableViewportMetrics pushPageToCenterOfViewport(ImmutableViewportMetrics viewportMetrics) {
+ RectF pageRect = viewportMetrics.getPageRect();
+ RectF viewportRect = viewportMetrics.getViewport();
+
+ if (pageRect.width() < viewportRect.width()) {
+ float originX = (viewportRect.width() - pageRect.width()) / 2.0f;
+ viewportMetrics = viewportMetrics.setViewportOrigin(-originX, viewportMetrics.getOrigin().y);
+ }
+
+ if (pageRect.height() < viewportRect.height()) {
+ float originY = (viewportRect.height() - pageRect.height()) / 2.0f;
+ viewportMetrics = viewportMetrics.setViewportOrigin(viewportMetrics.getOrigin().x, -originY);
+ }
+
+ return viewportMetrics;
+ }
+
+ private class AxisX extends Axis {
+ AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); }
+ @Override
+ public float getOrigin() { return getMetrics().viewportRectLeft; }
+ @Override
+ protected float getViewportLength() { return getMetrics().getWidth(); }
+ @Override
+ protected float getPageStart() { return getMetrics().pageRectLeft; }
+ @Override
+ protected float getPageLength() { return getMetrics().getPageWidth(); }
+ }
+
+ private class AxisY extends Axis {
+ AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
+ @Override
+ public float getOrigin() { return getMetrics().viewportRectTop; }
+ @Override
+ protected float getViewportLength() { return getMetrics().getHeight(); }
+ @Override
+ protected float getPageStart() { return getMetrics().pageRectTop; }
+ @Override
+ protected float getPageLength() { return getMetrics().getPageHeight(); }
+ }
+
+ /*
+ * Zooming
+ */
+ @Override
+ public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
+ if (mState == PanZoomState.ANIMATED_ZOOM)
+ return false;
+
+ if (null == mTarget.getZoomConstraints())
+ return false;
+
+ setState(PanZoomState.PINCHING);
+ mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
+ cancelTouch();
+
+ return true;
+ }
+
+ @Override
+ public boolean onScale(SimpleScaleGestureDetector detector) {
+ if (mTarget.isFullScreen())
+ return false;
+
+ if (mState != PanZoomState.PINCHING)
+ return false;
+
+ float prevSpan = detector.getPreviousSpan();
+ if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) {
+ // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
+ return true;
+ }
+
+ float spanRatio = detector.getCurrentSpan() / prevSpan;
+
+ synchronized (mTarget.getLock()) {
+ float newZoomFactor = getMetrics().zoomFactor * spanRatio;
+ float minZoomFactor = 0.0f; // deliberately set to zero to allow big zoom out effect
+ float maxZoomFactor = MAX_ZOOM;
+
+ ZoomConstraints constraints = mTarget.getZoomConstraints();
+
+ if (constraints.getMaxZoom() > 0)
+ maxZoomFactor = constraints.getMaxZoom();
+
+ if (newZoomFactor < minZoomFactor) {
+ // apply resistance when zooming past minZoomFactor,
+ // such that it asymptotically reaches minZoomFactor / 2.0
+ // but never exceeds that
+ final float rate = 0.5f; // controls how quickly we approach the limit
+ float excessZoom = minZoomFactor - newZoomFactor;
+ excessZoom = 1.0f - (float)Math.exp(-excessZoom * rate);
+ newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f);
+ }
+
+ if (newZoomFactor > maxZoomFactor) {
+ // apply resistance when zooming past maxZoomFactor,
+ // such that it asymptotically reaches maxZoomFactor + 1.0
+ // but never exceeds that
+ float excessZoom = newZoomFactor - maxZoomFactor;
+ excessZoom = 1.0f - (float)Math.exp(-excessZoom);
+ newZoomFactor = maxZoomFactor + excessZoom;
+ }
+
+ scrollBy(mLastZoomFocus.x - detector.getFocusX(),
+ mLastZoomFocus.y - detector.getFocusY());
+ PointF focus = new PointF(detector.getFocusX(), detector.getFocusY());
+ scaleWithFocus(newZoomFactor, focus);
+ }
+
+ mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());
+
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(SimpleScaleGestureDetector detector) {
+ if (mState == PanZoomState.ANIMATED_ZOOM)
+ return;
+
+ // switch back to the touching state
+ startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());
+
+ // Force a viewport synchronisation
+ mTarget.forceRedraw();
+
+ }
+
+ /**
+ * Scales the viewport, keeping the given focus point in the same place before and after the
+ * scale operation. You must hold the monitor while calling this.
+ */
+ private void scaleWithFocus(float zoomFactor, PointF focus) {
+ ImmutableViewportMetrics viewportMetrics = getMetrics();
+ viewportMetrics = viewportMetrics.scaleTo(zoomFactor, focus);
+ mTarget.setViewportMetrics(viewportMetrics);
+ }
+
+ public boolean getRedrawHint() {
+ switch (mState) {
+ case PINCHING:
+ case ANIMATED_ZOOM:
+ case BOUNCE:
+ // don't redraw during these because the zoom is (or might be, in the case
+ // of BOUNCE) be changing rapidly and gecko will have to redraw the entire
+ // display port area. we trigger a force-redraw upon exiting these states.
+ return false;
+ default:
+ // allow redrawing in other states
+ return true;
+ }
+ }
+
+ @Override
+ public boolean onDown(MotionEvent motionEvent) {
+ mWaitForDoubleTap = mTarget.getZoomConstraints() != null;
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent motionEvent) {
+ // If we get this, it will be followed either by a call to
+ // onSingleTapUp (if the user lifts their finger before the
+ // long-press timeout) or a call to onLongPress (if the user
+ // does not). In the former case, we want to make sure it is
+ // treated as a click. (Note that if this is called, we will
+ // not get a call to onDoubleTap).
+ mWaitForDoubleTap = false;
+ }
+
+ private PointF getMotionInDocumentCoordinates(MotionEvent motionEvent) {
+ RectF viewport = getValidViewportMetrics().getViewport();
+ PointF viewPoint = new PointF(motionEvent.getX(0), motionEvent.getY(0));
+ return mTarget.convertViewPointToLayerPoint(viewPoint);
+ }
+
+ @Override
+ public void onLongPress(MotionEvent motionEvent) {
+ LOKitShell.sendTouchEvent("LongPress", getMotionInDocumentCoordinates(motionEvent));
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ mContext.getDocumentOverlay().showPageNumberRect();
+ return super.onScroll(e1, e2, distanceX, distanceY);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent motionEvent) {
+ // When double-tapping is allowed, we have to wait to see if this is
+ // going to be a double-tap.
+ if (!mWaitForDoubleTap) {
+ LOKitShell.sendTouchEvent("SingleTap", getMotionInDocumentCoordinates(motionEvent));
+ }
+ // return false because we still want to get the ACTION_UP event that triggers this
+ return false;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
+ // In cases where we don't wait for double-tap, we handle this in onSingleTapUp.
+ if (mWaitForDoubleTap) {
+ LOKitShell.sendTouchEvent("SingleTap", getMotionInDocumentCoordinates(motionEvent));
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent motionEvent) {
+ if (null == mTarget.getZoomConstraints()) {
+ return true;
+ }
+ // Double tap zooms in or out depending on the current zoom factor
+ PointF pointOfTap = getMotionInDocumentCoordinates(motionEvent);
+ ImmutableViewportMetrics metrics = getMetrics();
+ float newZoom = metrics.getZoomFactor() >=
+ DOUBLE_TAP_THRESHOLD ? mTarget.getZoomConstraints().getDefaultZoom() : DOUBLE_TAP_THRESHOLD;
+ // calculate new top_left point from the point of tap
+ float ratio = newZoom/metrics.getZoomFactor();
+ float newLeft = pointOfTap.x - 1/ratio * (pointOfTap.x - metrics.getOrigin().x / metrics.getZoomFactor());
+ float newTop = pointOfTap.y - 1/ratio * (pointOfTap.y - metrics.getOrigin().y / metrics.getZoomFactor());
+ // animate move to the new view
+ animatedMove(new PointF(newLeft, newTop), newZoom);
+
+ LOKitShell.sendTouchEvent("DoubleTap", pointOfTap);
+ return true;
+ }
+
+ private void cancelTouch() {
+ //GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", "");
+ //GeckoAppShell.sendEventToGecko(e);
+ }
+
+ /**
+ * Zoom to a specified rect IN CSS PIXELS.
+ *
+ * While we usually use device pixels, zoomToRect must be specified in CSS
+ * pixels.
+ */
+ boolean animatedZoomTo(RectF zoomToRect) {
+ final float startZoom = getMetrics().zoomFactor;
+
+ RectF viewport = getMetrics().getViewport();
+ // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
+ // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
+ // while enlarging make sure we enlarge equally on both sides to keep the target rect
+ // centered.
+ float targetRatio = viewport.width() / viewport.height();
+ float rectRatio = zoomToRect.width() / zoomToRect.height();
+ if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) {
+ // all good, do nothing
+ } else if (targetRatio < rectRatio) {
+ // need to increase zoomToRect height
+ float newHeight = zoomToRect.width() / targetRatio;
+ zoomToRect.top -= (newHeight - zoomToRect.height()) / 2;
+ zoomToRect.bottom = zoomToRect.top + newHeight;
+ } else { // targetRatio > rectRatio) {
+ // need to increase zoomToRect width
+ float newWidth = targetRatio * zoomToRect.height();
+ zoomToRect.left -= (newWidth - zoomToRect.width()) / 2;
+ zoomToRect.right = zoomToRect.left + newWidth;
+ }
+
+ float finalZoom = viewport.width() / zoomToRect.width();
+
+ ImmutableViewportMetrics finalMetrics = getMetrics();
+ finalMetrics = finalMetrics.setViewportOrigin(
+ zoomToRect.left * finalMetrics.zoomFactor,
+ zoomToRect.top * finalMetrics.zoomFactor);
+ finalMetrics = finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));
+
+ // 2. now run getValidViewportMetrics on it, so that the target viewport is
+ // clamped down to prevent overscroll, over-zoom, and other bad conditions.
+ finalMetrics = getValidViewportMetrics(finalMetrics);
+
+ bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
+ return true;
+ }
+
+ /**
+ * Move the viewport to the top-left point to and zoom to the desired
+ * zoom factor. Input zoom factor can be null, in this case leave the zoom unchanged.
+ */
+ boolean animatedMove(PointF topLeft, Float zoom) {
+ RectF moveToRect = getMetrics().getCssViewport();
+ moveToRect.offsetTo(topLeft.x, topLeft.y);
+
+ ImmutableViewportMetrics finalMetrics = getMetrics();
+
+ finalMetrics = finalMetrics.setViewportOrigin(
+ moveToRect.left * finalMetrics.zoomFactor,
+ moveToRect.top * finalMetrics.zoomFactor);
+
+ if (zoom != null) {
+ finalMetrics = finalMetrics.scaleTo(zoom, new PointF(0.0f, 0.0f));
+ }
+ finalMetrics = getValidViewportMetrics(finalMetrics);
+
+ bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
+ return true;
+ }
+
+ /** This function must be called from the UI thread. */
+ public void abortPanning() {
+ checkMainThread();
+ bounce();
+ }
+
+ public void setOverScrollMode(int overscrollMode) {
+ mX.setOverScrollMode(overscrollMode);
+ mY.setOverScrollMode(overscrollMode);
+ }
+
+ public int getOverScrollMode() {
+ return mX.getOverScrollMode();
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/Layer.java b/android/source/src/java/org/mozilla/gecko/gfx/Layer.java
new file mode 100644
index 0000000000..b7fee29fc9
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/Layer.java
@@ -0,0 +1,218 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import java.nio.FloatBuffer;
+import java.util.concurrent.locks.ReentrantLock;
+
+public abstract class Layer {
+ private final ReentrantLock mTransactionLock;
+ private boolean mInTransaction;
+ private Rect mNewPosition;
+ private float mNewResolution;
+
+ protected Rect mPosition;
+ protected float mResolution;
+ protected boolean mUsesDefaultProgram = true;
+
+ public Layer() {
+ this(null);
+ }
+
+ public Layer(IntSize size) {
+ mTransactionLock = new ReentrantLock();
+ if (size == null) {
+ mPosition = new Rect();
+ } else {
+ mPosition = new Rect(0, 0, size.width, size.height);
+ }
+ mResolution = 1.0f;
+ }
+
+ /**
+ * Updates the layer. This returns false if there is still work to be done
+ * after this update.
+ */
+ public final boolean update(RenderContext context) {
+ if (mTransactionLock.isHeldByCurrentThread()) {
+ throw new RuntimeException("draw() called while transaction lock held by this " +
+ "thread?!");
+ }
+
+ if (mTransactionLock.tryLock()) {
+ try {
+ performUpdates(context);
+ return true;
+ } finally {
+ mTransactionLock.unlock();
+ }
+ }
+
+ return false;
+ }
+
+ /** Subclasses override this function to draw the layer. */
+ public abstract void draw(RenderContext context);
+
+ /** Given the intrinsic size of the layer, returns the pixel boundaries of the layer rect. */
+ protected RectF getBounds(RenderContext context) {
+ return RectUtils.scale(new RectF(mPosition), context.zoomFactor / mResolution);
+ }
+
+ /**
+ * Returns the region of the layer that is considered valid. The default
+ * implementation of this will return the bounds of the layer, but this
+ * may be overridden.
+ */
+ public Region getValidRegion(RenderContext context) {
+ return new Region(RectUtils.round(getBounds(context)));
+ }
+
+ /**
+ * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer"
+ * includes altering the underlying CairoImage in any way. Thus you must call this function
+ * before modifying the byte buffer associated with this layer.
+ *
+ * This function may block, so you should never call this on the main UI thread.
+ */
+ public void beginTransaction() {
+ if (mTransactionLock.isHeldByCurrentThread())
+ throw new RuntimeException("Nested transactions are not supported");
+ mTransactionLock.lock();
+ mInTransaction = true;
+ mNewResolution = mResolution;
+ }
+
+ /** Call this when you're done modifying the layer. */
+ public void endTransaction() {
+ if (!mInTransaction)
+ throw new RuntimeException("endTransaction() called outside a transaction");
+ mInTransaction = false;
+ mTransactionLock.unlock();
+ }
+
+ /** Returns true if the layer is currently in a transaction and false otherwise. */
+ protected boolean inTransaction() {
+ return mInTransaction;
+ }
+
+ /** Returns the current layer position. */
+ public Rect getPosition() {
+ return mPosition;
+ }
+
+ /** Sets the position. Only valid inside a transaction. */
+ public void setPosition(Rect newPosition) {
+ if (!mInTransaction)
+ throw new RuntimeException("setPosition() is only valid inside a transaction");
+ mNewPosition = newPosition;
+ }
+
+ /** Returns the current layer's resolution. */
+ public float getResolution() {
+ return mResolution;
+ }
+
+ /**
+ * Sets the layer resolution. This value is used to determine how many pixels per
+ * device pixel this layer was rendered at. This will be reflected by scaling by
+ * the reciprocal of the resolution in the layer's transform() function.
+ * Only valid inside a transaction. */
+ public void setResolution(float newResolution) {
+ if (!mInTransaction)
+ throw new RuntimeException("setResolution() is only valid inside a transaction");
+ mNewResolution = newResolution;
+ }
+
+ public boolean usesDefaultProgram() {
+ return mUsesDefaultProgram;
+ }
+
+ /**
+ * Subclasses may override this method to perform custom layer updates. This will be called
+ * with the transaction lock held. Subclass implementations of this method must call the
+ * superclass implementation. Returns false if there is still work to be done after this
+ * update is complete.
+ */
+ protected void performUpdates(RenderContext context) {
+ if (mNewPosition != null) {
+ mPosition = mNewPosition;
+ mNewPosition = null;
+ }
+ if (mNewResolution != 0.0f) {
+ mResolution = mNewResolution;
+ mNewResolution = 0.0f;
+ }
+ }
+
+ /**
+ * This function fills in the provided <tt>dest</tt> array with values to render a texture.
+ * The array is filled with 4 sets of {x, y, z, texture_x, texture_y} values (so 20 values
+ * in total) corresponding to the corners of the rect.
+ */
+ protected final void fillRectCoordBuffer(float[] dest, RectF rect, float viewWidth, float viewHeight,
+ Rect cropRect, float texWidth, float texHeight) {
+ //x, y, z, texture_x, texture_y
+ dest[0] = rect.left / viewWidth;
+ dest[1] = rect.bottom / viewHeight;
+ dest[2] = 0;
+ dest[3] = cropRect.left / texWidth;
+ dest[4] = cropRect.top / texHeight;
+
+ dest[5] = rect.left / viewWidth;
+ dest[6] = rect.top / viewHeight;
+ dest[7] = 0;
+ dest[8] = cropRect.left / texWidth;
+ dest[9] = cropRect.bottom / texHeight;
+
+ dest[10] = rect.right / viewWidth;
+ dest[11] = rect.bottom / viewHeight;
+ dest[12] = 0;
+ dest[13] = cropRect.right / texWidth;
+ dest[14] = cropRect.top / texHeight;
+
+ dest[15] = rect.right / viewWidth;
+ dest[16] = rect.top / viewHeight;
+ dest[17] = 0;
+ dest[18] = cropRect.right / texWidth;
+ dest[19] = cropRect.bottom / texHeight;
+ }
+
+ public static class RenderContext {
+ public final RectF viewport;
+ public final RectF pageRect;
+ public final float zoomFactor;
+ public final int positionHandle;
+ public final int textureHandle;
+ public final FloatBuffer coordBuffer;
+
+ public RenderContext(RectF aViewport, RectF aPageRect, float aZoomFactor,
+ int aPositionHandle, int aTextureHandle, FloatBuffer aCoordBuffer) {
+ viewport = aViewport;
+ pageRect = aPageRect;
+ zoomFactor = aZoomFactor;
+ positionHandle = aPositionHandle;
+ textureHandle = aTextureHandle;
+ coordBuffer = aCoordBuffer;
+ }
+
+ public boolean fuzzyEquals(RenderContext other) {
+ if (other == null) {
+ return false;
+ }
+ return RectUtils.fuzzyEquals(viewport, other.viewport)
+ && RectUtils.fuzzyEquals(pageRect, other.pageRect)
+ && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
+ }
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java b/android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java
new file mode 100644
index 0000000000..6ea7dd0edc
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java
@@ -0,0 +1,453 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.libreoffice.kit.DirectBufferAllocator;
+import org.mozilla.gecko.gfx.Layer.RenderContext;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * The layer renderer implements the rendering logic for a layer view.
+ */
+public class LayerRenderer implements GLSurfaceView.Renderer {
+ private static final String LOGTAG = "GeckoLayerRenderer";
+
+ /*
+ * The amount of time a frame is allowed to take to render before we declare it a dropped
+ * frame.
+ */
+ private static final int MAX_FRAME_TIME = 16; /* 1000 ms / 60 FPS */
+
+ private final LayerView mView;
+ private final SingleTileLayer mBackgroundLayer;
+ private final NinePatchTileLayer mShadowLayer;
+ private final ScrollbarLayer mHorizScrollLayer;
+ private final ScrollbarLayer mVertScrollLayer;
+ private final FadeRunnable mFadeRunnable;
+ private ByteBuffer mCoordByteBuffer;
+ private FloatBuffer mCoordBuffer;
+ private RenderContext mLastPageContext;
+ private int mMaxTextureSize;
+
+ private CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>();
+
+ // Used by GLES 2.0
+ private int mProgram;
+ private int mPositionHandle;
+ private int mTextureHandle;
+ private int mSampleHandle;
+ private int mTMatrixHandle;
+
+ // column-major matrix applied to each vertex to shift the viewport from
+ // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by
+ // a factor of 2 to fill up the screen
+ public static final float[] DEFAULT_TEXTURE_MATRIX = {
+ 2.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 2.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 2.0f, 0.0f,
+ -1.0f, -1.0f, 0.0f, 1.0f
+ };
+
+ private static final int COORD_BUFFER_SIZE = 20;
+
+ // The shaders run on the GPU directly, the vertex shader is only applying the
+ // matrix transform detailed above
+
+ // Note we flip the y-coordinate in the vertex shader from a
+ // coordinate system with (0,0) in the top left to one with (0,0) in
+ // the bottom left.
+
+ public static final String DEFAULT_VERTEX_SHADER =
+ "uniform mat4 uTMatrix;\n" +
+ "attribute vec4 vPosition;\n" +
+ "attribute vec2 aTexCoord;\n" +
+ "varying vec2 vTexCoord;\n" +
+ "void main() {\n" +
+ " gl_Position = uTMatrix * vPosition;\n" +
+ " vTexCoord.x = aTexCoord.x;\n" +
+ " vTexCoord.y = 1.0 - aTexCoord.y;\n" +
+ "}\n";
+
+ // We use highp because the screenshot textures
+ // we use are large and we stretch them a lot
+ // so we need all the precision we can get.
+ // Unfortunately, highp is not required by ES 2.0
+ // so on GPU's like Mali we end up getting mediump
+ public static final String DEFAULT_FRAGMENT_SHADER =
+ "precision highp float;\n" +
+ "varying vec2 vTexCoord;\n" +
+ "uniform sampler2D sTexture;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(sTexture, vTexCoord);\n" +
+ "}\n";
+
+ public LayerRenderer(LayerView view) {
+ mView = view;
+
+ CairoImage backgroundImage = new BufferedCairoImage(view.getBackgroundPattern());
+ mBackgroundLayer = new SingleTileLayer(true, backgroundImage);
+
+ CairoImage shadowImage = new BufferedCairoImage(view.getShadowPattern());
+ mShadowLayer = new NinePatchTileLayer(shadowImage);
+
+ mHorizScrollLayer = ScrollbarLayer.create(this, false);
+ mVertScrollLayer = ScrollbarLayer.create(this, true);
+ mFadeRunnable = new FadeRunnable();
+
+ // Initialize the FloatBuffer that will be used to store all vertices and texture
+ // coordinates in draw() commands.
+ mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4);
+ mCoordByteBuffer.order(ByteOrder.nativeOrder());
+ mCoordBuffer = mCoordByteBuffer.asFloatBuffer();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ DirectBufferAllocator.free(mCoordByteBuffer);
+ mCoordByteBuffer = null;
+ mCoordBuffer = null;
+ } finally {
+ super.finalize();
+ }
+ }
+
+ public void destroy() {
+ DirectBufferAllocator.free(mCoordByteBuffer);
+ mCoordByteBuffer = null;
+ mCoordBuffer = null;
+ mBackgroundLayer.destroy();
+ mShadowLayer.destroy();
+ mHorizScrollLayer.destroy();
+ mVertScrollLayer.destroy();
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+ createDefaultProgram();
+ activateDefaultProgram();
+ }
+
+ public void createDefaultProgram() {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
+ int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER);
+
+ mProgram = GLES20.glCreateProgram();
+ GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
+ GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
+ GLES20.glLinkProgram(mProgram); // creates OpenGL program executables
+
+ // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members.
+ mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+ mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
+ mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
+ mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix");
+
+ int maxTextureSizeResult[] = new int[1];
+ GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
+ mMaxTextureSize = maxTextureSizeResult[0];
+ }
+
+ // Activates the shader program.
+ public void activateDefaultProgram() {
+ // Add the program to the OpenGL environment
+ GLES20.glUseProgram(mProgram);
+
+ // Set the transformation matrix
+ GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, DEFAULT_TEXTURE_MATRIX, 0);
+
+ // Enable the arrays from which we get the vertex and texture coordinates
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+ GLES20.glEnableVertexAttribArray(mTextureHandle);
+
+ GLES20.glUniform1i(mSampleHandle, 0);
+ }
+
+ // Deactivates the shader program. This must be done to avoid crashes after returning to the
+ // Gecko C++ compositor from Java.
+ public void deactivateDefaultProgram() {
+ GLES20.glDisableVertexAttribArray(mTextureHandle);
+ GLES20.glDisableVertexAttribArray(mPositionHandle);
+ GLES20.glUseProgram(0);
+ }
+
+ public int getMaxTextureSize() {
+ return mMaxTextureSize;
+ }
+
+ public void addLayer(Layer layer) {
+ synchronized (mExtraLayers) {
+ if (mExtraLayers.contains(layer)) {
+ mExtraLayers.remove(layer);
+ }
+
+ mExtraLayers.add(layer);
+ }
+ }
+
+ public void removeLayer(Layer layer) {
+ synchronized (mExtraLayers) {
+ mExtraLayers.remove(layer);
+ }
+ }
+
+ /**
+ * Called whenever a new frame is about to be drawn.
+ */
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ Frame frame = new Frame(mView.getLayerClient().getViewportMetrics());
+ synchronized (mView.getLayerClient()) {
+ frame.beginDrawing();
+ frame.drawBackground();
+ frame.drawRootLayer();
+ frame.drawForeground();
+ frame.endDrawing();
+ }
+ }
+
+ private RenderContext createScreenContext(ImmutableViewportMetrics metrics) {
+ RectF viewport = new RectF(0.0f, 0.0f, metrics.getWidth(), metrics.getHeight());
+ RectF pageRect = new RectF(metrics.getPageRect());
+ return createContext(viewport, pageRect, 1.0f);
+ }
+
+ private RenderContext createPageContext(ImmutableViewportMetrics metrics) {
+ Rect viewport = RectUtils.round(metrics.getViewport());
+ RectF pageRect = metrics.getPageRect();
+ float zoomFactor = metrics.zoomFactor;
+ return createContext(new RectF(viewport), pageRect, zoomFactor);
+ }
+
+ private RenderContext createContext(RectF viewport, RectF pageRect, float zoomFactor) {
+ return new RenderContext(viewport, pageRect, zoomFactor, mPositionHandle, mTextureHandle,
+ mCoordBuffer);
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl, final int width, final int height) {
+ GLES20.glViewport(0, 0, width, height);
+ }
+
+ /*
+ * create a vertex shader type (GLES20.GL_VERTEX_SHADER)
+ * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
+ */
+ public static int loadShader(int type, String shaderCode) {
+ int shader = GLES20.glCreateShader(type);
+ GLES20.glShaderSource(shader, shaderCode);
+ GLES20.glCompileShader(shader);
+ return shader;
+ }
+
+ class FadeRunnable implements Runnable {
+ private boolean mStarted;
+ private long mRunAt;
+
+ void scheduleStartFade(long delay) {
+ mRunAt = SystemClock.elapsedRealtime() + delay;
+ if (!mStarted) {
+ mView.postDelayed(this, delay);
+ mStarted = true;
+ }
+ }
+
+ void scheduleNextFadeFrame() {
+ if (mStarted) {
+ Log.e(LOGTAG, "scheduleNextFadeFrame() called while scheduled for starting fade");
+ }
+ mView.postDelayed(this, 1000L / 60L); // request another frame at 60fps
+ }
+
+ boolean timeToFade() {
+ return !mStarted;
+ }
+
+ public void run() {
+ long timeDelta = mRunAt - SystemClock.elapsedRealtime();
+ if (timeDelta > 0) {
+ // the run-at time was pushed back, so reschedule
+ mView.postDelayed(this, timeDelta);
+ } else {
+ // reached the run-at time, execute
+ mStarted = false;
+ mView.requestRender();
+ }
+ }
+ }
+
+ public class Frame {
+ // A fixed snapshot of the viewport metrics that this frame is using to render content.
+ private ImmutableViewportMetrics mFrameMetrics;
+ // A rendering context for page-positioned layers, and one for screen-positioned layers.
+ private RenderContext mPageContext, mScreenContext;
+ // Whether a layer was updated.
+ private boolean mUpdated;
+ private final Rect mPageRect;
+
+ public Frame(ImmutableViewportMetrics metrics) {
+ mFrameMetrics = metrics;
+ mPageContext = createPageContext(metrics);
+ mScreenContext = createScreenContext(metrics);
+ mPageRect = getPageRect();
+ }
+
+ private void setScissorRect() {
+ Rect scissorRect = transformToScissorRect(mPageRect);
+ GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+ GLES20.glScissor(scissorRect.left, scissorRect.top,
+ scissorRect.width(), scissorRect.height());
+ }
+
+ private Rect transformToScissorRect(Rect rect) {
+ IntSize screenSize = new IntSize(mFrameMetrics.getSize());
+
+ int left = Math.max(0, rect.left);
+ int top = Math.max(0, rect.top);
+ int right = Math.min(screenSize.width, rect.right);
+ int bottom = Math.min(screenSize.height, rect.bottom);
+
+ return new Rect(left, screenSize.height - bottom, right,
+ (screenSize.height - bottom) + (bottom - top));
+ }
+
+ private Rect getPageRect() {
+ Point origin = PointUtils.round(mFrameMetrics.getOrigin());
+ Rect pageRect = RectUtils.round(mFrameMetrics.getPageRect());
+ pageRect.offset(-origin.x, -origin.y);
+ return pageRect;
+ }
+
+ public void beginDrawing() {
+ TextureReaper.get().reap();
+ TextureGenerator.get().fill();
+
+ mUpdated = true;
+
+ Layer rootLayer = mView.getLayerClient().getRoot();
+ Layer lowResLayer = mView.getLayerClient().getLowResLayer();
+
+ if (!mPageContext.fuzzyEquals(mLastPageContext)) {
+ // the viewport or page changed, so show the scrollbars again
+ // as per UX decision
+ mVertScrollLayer.unfade();
+ mHorizScrollLayer.unfade();
+ mFadeRunnable.scheduleStartFade(ScrollbarLayer.FADE_DELAY);
+ } else if (mFadeRunnable.timeToFade()) {
+ boolean stillFading = mVertScrollLayer.fade() | mHorizScrollLayer.fade();
+ if (stillFading) {
+ mFadeRunnable.scheduleNextFadeFrame();
+ }
+ }
+ mLastPageContext = mPageContext;
+
+ /* Update layers. */
+ if (rootLayer != null) mUpdated &= rootLayer.update(mPageContext); // called on compositor thread
+ if (lowResLayer != null) mUpdated &= lowResLayer.update(mPageContext); // called on compositor thread
+ mUpdated &= mBackgroundLayer.update(mScreenContext); // called on compositor thread
+ mUpdated &= mShadowLayer.update(mPageContext); // called on compositor thread
+ mUpdated &= mVertScrollLayer.update(mPageContext); // called on compositor thread
+ mUpdated &= mHorizScrollLayer.update(mPageContext); // called on compositor thread
+
+ for (Layer layer : mExtraLayers)
+ mUpdated &= layer.update(mPageContext); // called on compositor thread
+ }
+
+ public void drawBackground() {
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+
+ /* Update background color. */
+ final int backgroundColor = Color.WHITE;
+
+ /* Clear to the page background colour. The bits set here need to
+ * match up with those used in gfx/layers/opengl/LayerManagerOGL.cpp.
+ */
+ GLES20.glClearColor(((backgroundColor >> 16) & 0xFF) / 255.0f,
+ ((backgroundColor >> 8) & 0xFF) / 255.0f,
+ (backgroundColor & 0xFF) / 255.0f,
+ 0.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
+ GLES20.GL_DEPTH_BUFFER_BIT);
+
+ /* Draw the background. */
+ mBackgroundLayer.setMask(mPageRect);
+ mBackgroundLayer.draw(mScreenContext);
+
+ /* Draw the drop shadow, if we need to. */
+ RectF untransformedPageRect = new RectF(0.0f, 0.0f, mPageRect.width(),
+ mPageRect.height());
+ if (!untransformedPageRect.contains(mFrameMetrics.getViewport()))
+ mShadowLayer.draw(mPageContext);
+
+ /* Scissor around the page-rect, in case the page has shrunk
+ * since the screenshot layer was last updated.
+ */
+ setScissorRect(); // Calls glEnable(GL_SCISSOR_TEST))
+ }
+
+ // Draws the layer the client added to us.
+ void drawRootLayer() {
+ Layer lowResLayer = mView.getLayerClient().getLowResLayer();
+ if (lowResLayer == null) {
+ return;
+ }
+ lowResLayer.draw(mPageContext);
+
+ Layer rootLayer = mView.getLayerClient().getRoot();
+ if (rootLayer == null) {
+ return;
+ }
+
+ rootLayer.draw(mPageContext);
+ }
+
+ public void drawForeground() {
+ /* Draw any extra layers that were added (likely plugins) */
+ if (mExtraLayers.size() > 0) {
+ for (Layer layer : mExtraLayers) {
+ if (!layer.usesDefaultProgram())
+ deactivateDefaultProgram();
+
+ layer.draw(mPageContext);
+
+ if (!layer.usesDefaultProgram())
+ activateDefaultProgram();
+ }
+ }
+
+ /* Draw the vertical scrollbar. */
+ if (mPageRect.height() > mFrameMetrics.getHeight())
+ mVertScrollLayer.draw(mPageContext);
+
+ /* Draw the horizontal scrollbar. */
+ if (mPageRect.width() > mFrameMetrics.getWidth())
+ mHorizScrollLayer.draw(mPageContext);
+ }
+
+ public void endDrawing() {
+ // If a layer update requires further work, schedule another redraw
+ if (!mUpdated)
+ mView.requestRender();
+ }
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/LayerView.java b/android/source/src/java/org/mozilla/gecko/gfx/LayerView.java
new file mode 100644
index 0000000000..29049f9291
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/LayerView.java
@@ -0,0 +1,337 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.PixelFormat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.FrameLayout;
+
+import org.libreoffice.LOEvent;
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.R;
+import org.mozilla.gecko.OnInterceptTouchListener;
+import org.mozilla.gecko.OnSlideSwipeListener;
+
+/**
+ * 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 FrameLayout {
+ private static String LOGTAG = LayerView.class.getName();
+
+ private GeckoLayerClient mLayerClient;
+ private PanZoomController mPanZoomController;
+ private GLController mGLController;
+ private InputConnectionHandler mInputConnectionHandler;
+ private LayerRenderer mRenderer;
+
+ private SurfaceView mSurfaceView;
+
+ private Listener mListener;
+ private OnInterceptTouchListener mTouchIntercepter;
+ private LibreOfficeMainActivity mContext;
+
+ public LayerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = (LibreOfficeMainActivity) context;
+
+ mSurfaceView = new SurfaceView(context);
+ addView(mSurfaceView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ SurfaceHolder holder = mSurfaceView.getHolder();
+ holder.addCallback(new SurfaceListener());
+ holder.setFormat(PixelFormat.RGB_565);
+
+ mGLController = new GLController(this);
+ }
+
+ void connect(GeckoLayerClient layerClient) {
+ mLayerClient = layerClient;
+ mPanZoomController = mLayerClient.getPanZoomController();
+ mRenderer = new LayerRenderer(this);
+ mInputConnectionHandler = null;
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+
+ createGLThread();
+ setOnTouchListener(new OnSlideSwipeListener(getContext(), mLayerClient));
+ }
+
+ public void show() {
+ // Fix this if TextureView support is turned back on above
+ mSurfaceView.setVisibility(View.VISIBLE);
+ }
+
+ public void hide() {
+ // Fix this if TextureView support is turned back on above
+ mSurfaceView.setVisibility(View.INVISIBLE);
+ }
+
+ public void destroy() {
+ if (mLayerClient != null) {
+ mLayerClient.destroy();
+ }
+ if (mRenderer != null) {
+ mRenderer.destroy();
+ }
+ }
+
+ public void setTouchIntercepter(final OnInterceptTouchListener touchIntercepter) {
+ // this gets run on the gecko thread, but for thread safety we want the assignment
+ // on the UI thread.
+ post(new Runnable() {
+ public void run() {
+ mTouchIntercepter = touchIntercepter;
+ }
+ });
+ }
+
+ public void setInputConnectionHandler(InputConnectionHandler inputConnectionHandler) {
+ mInputConnectionHandler = inputConnectionHandler;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ requestFocus();
+ }
+
+ if (mTouchIntercepter != null && mTouchIntercepter.onInterceptTouchEvent(this, event)) {
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) {
+ return true;
+ }
+ if (mTouchIntercepter != null && mTouchIntercepter.onTouch(this, event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ return mTouchIntercepter != null && mTouchIntercepter.onTouch(this, event);
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ return mPanZoomController != null && mPanZoomController.onMotionEvent(event);
+ }
+
+ public GeckoLayerClient getLayerClient() { return mLayerClient; }
+ public PanZoomController getPanZoomController() { return mPanZoomController; }
+
+ public ImmutableViewportMetrics getViewportMetrics() {
+ return mLayerClient.getViewportMetrics();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ if (mInputConnectionHandler != null)
+ return mInputConnectionHandler.onCreateInputConnection(outAttrs);
+ return null;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return mInputConnectionHandler != null && mInputConnectionHandler.onKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mInputConnectionHandler != null && mInputConnectionHandler.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return mInputConnectionHandler != null && mInputConnectionHandler.onKeyLongPress(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return mInputConnectionHandler != null && mInputConnectionHandler.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return mInputConnectionHandler != null && mInputConnectionHandler.onKeyUp(keyCode, event);
+ }
+
+ public void requestRender() {
+ if (mListener != null) {
+ mListener.renderRequested();
+ }
+ }
+
+ public void addLayer(Layer layer) {
+ mRenderer.addLayer(layer);
+ }
+
+ public void removeLayer(Layer layer) {
+ mRenderer.removeLayer(layer);
+ }
+
+ public int getMaxTextureSize() {
+ return mRenderer.getMaxTextureSize();
+ }
+
+ public void setLayerRenderer(LayerRenderer renderer) {
+ mRenderer = renderer;
+ }
+
+ public LayerRenderer getLayerRenderer() {
+ return mRenderer;
+ }
+
+ public LayerRenderer getRenderer() {
+ return mRenderer;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ Listener getListener() {
+ return mListener;
+ }
+
+ public GLController getGLController() {
+ return mGLController;
+ }
+
+ public Bitmap getDrawable(String name) {
+ Context context = getContext();
+ Resources resources = context.getResources();
+ String packageName = resources.getResourcePackageName(R.id.dummy_id_for_package_name_resolution);
+ int resourceID = resources.getIdentifier(name, "drawable", packageName);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inScaled = false;
+ return BitmapFactory.decodeResource(context.getResources(), resourceID, options);
+ }
+
+ Bitmap getBackgroundPattern() {
+ return getDrawable("background");
+ }
+
+ Bitmap getShadowPattern() {
+ return getDrawable("shadow");
+ }
+
+ private void onSizeChanged(int width, int height) {
+ mGLController.surfaceChanged(width, height);
+
+ mLayerClient.setViewportSize(new FloatSize(width, height), false);
+
+ if (mListener != null) {
+ mListener.surfaceChanged(width, height);
+ }
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_ZOOM_CONSTRAINTS));
+ }
+
+ private void onDestroyed() {
+ mGLController.surfaceDestroyed();
+
+ if (mListener != null) {
+ mListener.compositionPauseRequested();
+ }
+ }
+
+ public Object getNativeWindow() {
+ return mSurfaceView.getHolder();
+ }
+
+ public interface Listener {
+ void compositorCreated();
+ void renderRequested();
+ void compositionPauseRequested();
+ void surfaceChanged(int width, int height);
+ }
+
+ private class SurfaceListener implements SurfaceHolder.Callback {
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ onSizeChanged(width, height);
+ }
+
+ public void surfaceCreated(SurfaceHolder holder) {
+ if (mRenderControllerThread != null) {
+ mRenderControllerThread.surfaceCreated();
+ }
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ onDestroyed();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed) {
+ mLayerClient.setViewportSize(new FloatSize(right - left, bottom - top), true);
+ }
+ }
+
+ private RenderControllerThread mRenderControllerThread;
+
+ public synchronized void createGLThread() {
+ if (mRenderControllerThread != null) {
+ throw new LayerViewException ("createGLThread() called with a GL thread already in place!");
+ }
+
+ Log.e(LOGTAG, "### Creating GL thread!");
+ mRenderControllerThread = new RenderControllerThread(mGLController);
+ mRenderControllerThread.start();
+ setListener(mRenderControllerThread);
+ notifyAll();
+ }
+
+ public synchronized Thread destroyGLThread() {
+ // Wait for the GL thread to be started.
+ Log.e(LOGTAG, "### Waiting for GL thread to be created...");
+ while (mRenderControllerThread == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ Log.e(LOGTAG, "### Destroying GL thread!");
+ Thread thread = mRenderControllerThread;
+ mRenderControllerThread.shutdown();
+ setListener(null);
+ mRenderControllerThread = null;
+ return thread;
+ }
+
+ public static class LayerViewException extends RuntimeException {
+ public static final long serialVersionUID = 1L;
+
+ LayerViewException(String e) {
+ super(e);
+ }
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java
new file mode 100644
index 0000000000..99f203961a
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java
@@ -0,0 +1,131 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.RectF;
+import android.opengl.GLES20;
+
+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 static final int PATCH_SIZE = 16;
+ private static final int TEXTURE_SIZE = 64;
+
+ public NinePatchTileLayer(CairoImage image) {
+ super(image, PaintMode.NORMAL);
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ if (!initialized())
+ return;
+
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+ GLES20.glEnable(GLES20.GL_BLEND);
+
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID());
+ drawPatches(context);
+ }
+
+ private void drawPatches(RenderContext context) {
+ /*
+ * We divide the nine-patch bitmap up as follows:
+ *
+ * +---+---+---+
+ * | 0 | 1 | 2 |
+ * +---+---+---+
+ * | 3 | | 4 |
+ * +---+---+---+
+ * | 5 | 6 | 7 |
+ * +---+---+---+
+ */
+
+ // page is the rect of the "missing" center spot in the picture above
+ RectF page = context.pageRect;
+
+ drawPatch(context, 0, PATCH_SIZE * 3, /* 0 */
+ page.left - PATCH_SIZE, page.top - PATCH_SIZE, PATCH_SIZE, PATCH_SIZE);
+ drawPatch(context, PATCH_SIZE, PATCH_SIZE * 3, /* 1 */
+ page.left, page.top - PATCH_SIZE, page.width(), PATCH_SIZE);
+ drawPatch(context, PATCH_SIZE * 2, PATCH_SIZE * 3, /* 2 */
+ page.right, page.top - PATCH_SIZE, PATCH_SIZE, PATCH_SIZE);
+ drawPatch(context, 0, PATCH_SIZE * 2, /* 3 */
+ page.left - PATCH_SIZE, page.top, PATCH_SIZE, page.height());
+ drawPatch(context, PATCH_SIZE * 2, PATCH_SIZE * 2, /* 4 */
+ page.right, page.top, PATCH_SIZE, page.height());
+ drawPatch(context, 0, PATCH_SIZE, /* 5 */
+ page.left - PATCH_SIZE, page.bottom, PATCH_SIZE, PATCH_SIZE);
+ drawPatch(context, PATCH_SIZE, PATCH_SIZE, /* 6 */
+ page.left, page.bottom, page.width(), PATCH_SIZE);
+ drawPatch(context, PATCH_SIZE * 2, PATCH_SIZE, /* 7 */
+ page.right, page.bottom, PATCH_SIZE, PATCH_SIZE);
+ }
+
+ private void drawPatch(RenderContext context, int textureX, int textureY,
+ float tileX, float tileY, float tileWidth, float tileHeight) {
+ RectF viewport = context.viewport;
+ float viewportHeight = viewport.height();
+ float drawX = tileX - viewport.left;
+ float drawY = viewportHeight - (tileY + tileHeight - viewport.top);
+
+ float[] coords = {
+ //x, y, z, texture_x, texture_y
+ drawX/viewport.width(), drawY/viewport.height(), 0,
+ textureX/(float)TEXTURE_SIZE, textureY/(float)TEXTURE_SIZE,
+
+ drawX/viewport.width(), (drawY+tileHeight)/viewport.height(), 0,
+ textureX/(float)TEXTURE_SIZE, (textureY+PATCH_SIZE)/(float)TEXTURE_SIZE,
+
+ (drawX+tileWidth)/viewport.width(), drawY/viewport.height(), 0,
+ (textureX+PATCH_SIZE)/(float)TEXTURE_SIZE, textureY/(float)TEXTURE_SIZE,
+
+ (drawX+tileWidth)/viewport.width(), (drawY+tileHeight)/viewport.height(), 0,
+ (textureX+PATCH_SIZE)/(float)TEXTURE_SIZE, (textureY+PATCH_SIZE)/(float)TEXTURE_SIZE
+
+ };
+
+ // Get the buffer and handles from the context
+ FloatBuffer coordBuffer = context.coordBuffer;
+ int positionHandle = context.positionHandle;
+ int textureHandle = context.textureHandle;
+
+ // Make sure we are at position zero in the buffer in case other draw methods did not clean
+ // up after themselves
+ coordBuffer.position(0);
+ coordBuffer.put(coords);
+
+ // Unbind any the current array buffer so we can use client side buffers
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
+ GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
+ GLES20.GL_CLAMP_TO_EDGE);
+
+ // Use bilinear filtering for both magnification and minimization of the texture. This
+ // applies only to the shadow layer so we do not incur a high overhead.
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_LINEAR);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/PanZoomController.java b/android/source/src/java/org/mozilla/gecko/gfx/PanZoomController.java
new file mode 100644
index 0000000000..ebcd641f21
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -0,0 +1,36 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.View;
+import org.libreoffice.LibreOfficeMainActivity;
+
+interface PanZoomController {
+
+ class Factory {
+ static PanZoomController create(LibreOfficeMainActivity context, PanZoomTarget target, View view) {
+ return new JavaPanZoomController(context, target, view);
+ }
+ }
+
+ void destroy();
+
+ boolean onTouchEvent(MotionEvent event);
+ boolean onMotionEvent(MotionEvent event);
+ void notifyDefaultActionPrevented(boolean prevented);
+
+ boolean getRedrawHint();
+ PointF getVelocityVector();
+
+ void pageRectUpdated();
+ void abortPanning();
+ void abortAnimation();
+
+ void setOverScrollMode(int overscrollMode);
+ int getOverScrollMode();
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java b/android/source/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java
new file mode 100644
index 0000000000..88e1b216c6
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java
@@ -0,0 +1,26 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+
+import org.mozilla.gecko.ZoomConstraints;
+
+public interface PanZoomTarget {
+ public ImmutableViewportMetrics getViewportMetrics();
+ public ZoomConstraints getZoomConstraints();
+
+ public void setAnimationTarget(ImmutableViewportMetrics viewport);
+ public void setViewportMetrics(ImmutableViewportMetrics viewport);
+ /** This triggers an (asynchronous) viewport update/redraw. */
+ public void forceRedraw();
+
+ public boolean post(Runnable action);
+ public Object getLock();
+ public PointF convertViewPointToLayerPoint(PointF viewPoint);
+
+ boolean isFullScreen();
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/PointUtils.java b/android/source/src/java/org/mozilla/gecko/gfx/PointUtils.java
new file mode 100644
index 0000000000..4eff380527
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/PointUtils.java
@@ -0,0 +1,53 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.StrictMath;
+
+public final class PointUtils {
+ public static PointF add(PointF one, PointF two) {
+ return new PointF(one.x + two.x, one.y + two.y);
+ }
+
+ public static PointF subtract(PointF one, PointF two) {
+ return new PointF(one.x - two.x, one.y - two.y);
+ }
+
+ public static PointF scale(PointF point, float factor) {
+ return new PointF(point.x * factor, point.y * factor);
+ }
+
+ public static Point round(PointF point) {
+ return new Point(Math.round(point.x), Math.round(point.y));
+ }
+
+ /* Computes the magnitude of the given vector. */
+ public static float distance(PointF point) {
+ return (float)StrictMath.hypot(point.x, point.y);
+ }
+
+ /** Computes the scalar distance between two points. */
+ public static float distance(PointF one, PointF two) {
+ return PointF.length(one.x - two.x, one.y - two.y);
+ }
+
+ public static JSONObject toJSON(PointF point) throws JSONException {
+ // Ensure we put ints, not longs, because Gecko message handlers call getInt().
+ int x = Math.round(point.x);
+ int y = Math.round(point.y);
+ JSONObject json = new JSONObject();
+ json.put("x", x);
+ json.put("y", y);
+ return json;
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/RectUtils.java b/android/source/src/java/org/mozilla/gecko/gfx/RectUtils.java
new file mode 100644
index 0000000000..e7fa540a39
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/RectUtils.java
@@ -0,0 +1,110 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+public final class RectUtils {
+ private RectUtils() {}
+
+ public static RectF expand(RectF rect, float moreWidth, float moreHeight) {
+ float halfMoreWidth = moreWidth / 2;
+ float halfMoreHeight = moreHeight / 2;
+ return new RectF(rect.left - halfMoreWidth,
+ rect.top - halfMoreHeight,
+ rect.right + halfMoreWidth,
+ rect.bottom + halfMoreHeight);
+ }
+
+ public static RectF contract(RectF rect, float lessWidth, float lessHeight) {
+ float halfLessWidth = lessWidth / 2.0f;
+ float halfLessHeight = lessHeight / 2.0f;
+ return new RectF(rect.left + halfLessWidth,
+ rect.top + halfLessHeight,
+ rect.right - halfLessWidth,
+ rect.bottom - halfLessHeight);
+ }
+
+ public static RectF intersect(RectF one, RectF two) {
+ float left = Math.max(one.left, two.left);
+ float top = Math.max(one.top, two.top);
+ float right = Math.min(one.right, two.right);
+ float bottom = Math.min(one.bottom, two.bottom);
+ return new RectF(left, top, Math.max(right, left), Math.max(bottom, top));
+ }
+
+ public static RectF scale(RectF rect, float scale) {
+ float x = rect.left * scale;
+ float y = rect.top * scale;
+ return new RectF(x, y,
+ x + (rect.width() * scale),
+ y + (rect.height() * scale));
+ }
+
+ public static RectF inverseScale(RectF rect, float scale) {
+ float x = rect.left / scale;
+ float y = rect.top / scale;
+ return new RectF(x, y,
+ x + (rect.width() / scale),
+ y + (rect.height() / scale));
+ }
+
+ /** Returns the nearest integer rect of the given rect. */
+ public static Rect round(RectF rect) {
+ Rect r = new Rect();
+ round(rect, r);
+ return r;
+ }
+
+ public static void round(RectF rect, Rect dest) {
+ dest.set(Math.round(rect.left), Math.round(rect.top),
+ Math.round(rect.right), Math.round(rect.bottom));
+ }
+
+ public static Rect roundIn(RectF rect) {
+ return new Rect((int)Math.ceil(rect.left), (int)Math.ceil(rect.top),
+ (int)Math.floor(rect.right), (int)Math.floor(rect.bottom));
+ }
+
+ public static IntSize getSize(Rect rect) {
+ return new IntSize(rect.width(), rect.height());
+ }
+
+ public static Point getOrigin(Rect rect) {
+ return new Point(rect.left, rect.top);
+ }
+
+ public static PointF getOrigin(RectF rect) {
+ return new PointF(rect.left, rect.top);
+ }
+
+ public static boolean fuzzyEquals(RectF a, RectF b) {
+ if (a == null && b == null)
+ return true;
+ else
+ return a != null && b != null
+ && FloatUtils.fuzzyEquals(a.top, b.top)
+ && FloatUtils.fuzzyEquals(a.left, b.left)
+ && FloatUtils.fuzzyEquals(a.right, b.right)
+ && FloatUtils.fuzzyEquals(a.bottom, b.bottom);
+ }
+
+ /**
+ * Assign rectangle values from source to target.
+ */
+ public static void assign(final RectF target, final RectF source)
+ {
+ target.left = source.left;
+ target.top = source.top;
+ target.right = source.right;
+ target.bottom = source.bottom;
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java b/android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java
new file mode 100644
index 0000000000..5c74d56a00
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java
@@ -0,0 +1,143 @@
+package org.mozilla.gecko.gfx;
+
+import android.opengl.GLSurfaceView;
+
+import java.util.concurrent.LinkedBlockingQueue;
+
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * Thread which controls the rendering to OpenGL context. Render commands are queued and
+ * processed and delegated by this thread.
+ */
+public class RenderControllerThread extends Thread implements LayerView.Listener {
+ private LinkedBlockingQueue<RenderCommand> queue = new LinkedBlockingQueue<RenderCommand>();
+ private GLController controller;
+ private boolean renderQueued = false;
+ private int width;
+ private int height;
+
+ public RenderControllerThread(GLController controller) {
+ this.controller = controller;
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ RenderCommand command;
+ try {
+ command = queue.take();
+ execute(command);
+ if (command == RenderCommand.SHUTDOWN) {
+ return;
+ }
+ } catch (InterruptedException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+ }
+
+ void execute(RenderCommand command) {
+ switch (command) {
+ case SHUTDOWN:
+ doShutdown();
+ break;
+ case RENDER_FRAME:
+ doRenderFrame();
+ break;
+ case SIZE_CHANGED:
+ doSizeChanged();
+ break;
+ case SURFACE_CREATED:
+ doSurfaceCreated();
+ break;
+ case SURFACE_DESTROYED:
+ doSurfaceDestroyed();
+ break;
+ }
+ }
+
+ public void shutdown() {
+ queue.add(RenderCommand.SHUTDOWN);
+ }
+
+ @Override
+ public void compositorCreated() {
+
+ }
+
+ @Override
+ public void renderRequested() {
+ synchronized (this) {
+ if (!renderQueued) {
+ queue.add(RenderCommand.RENDER_FRAME);
+ renderQueued = true;
+ }
+ }
+ }
+
+ @Override
+ public void compositionPauseRequested() {
+ queue.add(RenderCommand.SURFACE_DESTROYED);
+ }
+
+ @Override
+ public void surfaceChanged(int width, int height) {
+ this.width = width;
+ this.height = height;
+ queue.add(RenderCommand.SIZE_CHANGED);
+ }
+
+ public void surfaceCreated() {
+ queue.add(RenderCommand.SURFACE_CREATED);
+ }
+
+ private GLSurfaceView.Renderer getRenderer() {
+ return controller.getView().getRenderer();
+ }
+
+ private void doShutdown() {
+ controller.disposeGLContext();
+ controller = null;
+ }
+
+ private void doRenderFrame() {
+ synchronized (this) {
+ renderQueued = false;
+ }
+ if (controller.getEGLSurface() == null) {
+ return;
+ }
+ GLSurfaceView.Renderer renderer = getRenderer();
+ if (renderer != null) {
+ renderer.onDrawFrame(controller.getGL());
+ }
+ controller.swapBuffers();
+ }
+
+ private void doSizeChanged() {
+ GLSurfaceView.Renderer renderer = getRenderer();
+ if (renderer != null) {
+ renderer.onSurfaceChanged(controller.getGL(), width, height);
+ }
+ }
+
+ private void doSurfaceCreated() {
+ if (!controller.hasSurface()) {
+ controller.initGLContext();
+ }
+ }
+
+ private void doSurfaceDestroyed() {
+ controller.disposeGLContext();
+ }
+
+ public enum RenderCommand {
+ SHUTDOWN,
+ RECREATE_SURFACE,
+ RENDER_FRAME,
+ SIZE_CHANGED,
+ SURFACE_CREATED,
+ SURFACE_DESTROYED,
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java
new file mode 100644
index 0000000000..7ef8ff0206
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java
@@ -0,0 +1,451 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+
+import org.libreoffice.kit.DirectBufferAllocator;
+import org.mozilla.gecko.util.FloatUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+
+/**
+ * Draws a small rect. This is scaled to become a scrollbar.
+ */
+public class ScrollbarLayer extends TileLayer {
+ private static String LOGTAG = LayerView.class.getName();
+ public static final long FADE_DELAY = 500; // milliseconds before fade-out starts
+ private static final float FADE_AMOUNT = 0.03f; // how much (as a percent) the scrollbar should fade per frame
+
+ private static final int PADDING = 1; // gap between scrollbar and edge of viewport
+ private static final int BAR_SIZE = 6;
+ private static final int CAP_RADIUS = (BAR_SIZE / 2);
+
+ private final boolean mVertical;
+ private final Bitmap mBitmap;
+ private final Canvas mCanvas;
+ private float mOpacity;
+
+ private LayerRenderer mRenderer;
+ private int mProgram;
+ private int mPositionHandle;
+ private int mTextureHandle;
+ private int mSampleHandle;
+ private int mTMatrixHandle;
+ private int mOpacityHandle;
+
+ // Fragment shader used to draw the scroll-bar with opacity
+ private static final String FRAGMENT_SHADER =
+ "precision mediump float;\n" +
+ "varying vec2 vTexCoord;\n" +
+ "uniform sampler2D sTexture;\n" +
+ "uniform float uOpacity;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(sTexture, vec2(vTexCoord.x, 1.0 - vTexCoord.y));\n" +
+ " gl_FragColor.a *= uOpacity;\n" +
+ "}\n";
+
+ // Dimensions of the texture image
+ private static final float TEX_HEIGHT = 8.0f;
+ private static final float TEX_WIDTH = 8.0f;
+
+ // Texture coordinates for the scrollbar's body
+ // We take a 1x1 pixel from the center of the image and scale it to become the bar
+ private static final float[] BODY_TEX_COORDS = {
+ // x, y
+ CAP_RADIUS/TEX_WIDTH, CAP_RADIUS/TEX_HEIGHT,
+ CAP_RADIUS/TEX_WIDTH, (CAP_RADIUS+1)/TEX_HEIGHT,
+ (CAP_RADIUS+1)/TEX_WIDTH, CAP_RADIUS/TEX_HEIGHT,
+ (CAP_RADIUS+1)/TEX_WIDTH, (CAP_RADIUS+1)/TEX_HEIGHT
+ };
+
+ // Texture coordinates for the top cap of the scrollbar
+ private static final float[] TOP_CAP_TEX_COORDS = {
+ // x, y
+ 0 , 1.0f - CAP_RADIUS/TEX_HEIGHT,
+ 0 , 1.0f,
+ BAR_SIZE/TEX_WIDTH, 1.0f - CAP_RADIUS/TEX_HEIGHT,
+ BAR_SIZE/TEX_WIDTH, 1.0f
+ };
+
+ // Texture coordinates for the bottom cap of the scrollbar
+ private static final float[] BOT_CAP_TEX_COORDS = {
+ // x, y
+ 0 , 1.0f - BAR_SIZE/TEX_HEIGHT,
+ 0 , 1.0f - CAP_RADIUS/TEX_HEIGHT,
+ BAR_SIZE/TEX_WIDTH, 1.0f - BAR_SIZE/TEX_HEIGHT,
+ BAR_SIZE/TEX_WIDTH, 1.0f - CAP_RADIUS/TEX_HEIGHT
+ };
+
+ // Texture coordinates for the left cap of the scrollbar
+ private static final float[] LEFT_CAP_TEX_COORDS = {
+ // x, y
+ 0 , 1.0f - BAR_SIZE/TEX_HEIGHT,
+ 0 , 1.0f,
+ CAP_RADIUS/TEX_WIDTH, 1.0f - BAR_SIZE/TEX_HEIGHT,
+ CAP_RADIUS/TEX_WIDTH, 1.0f
+ };
+
+ // Texture coordinates for the right cap of the scrollbar
+ private static final float[] RIGHT_CAP_TEX_COORDS = {
+ // x, y
+ CAP_RADIUS/TEX_WIDTH, 1.0f - BAR_SIZE/TEX_HEIGHT,
+ CAP_RADIUS/TEX_WIDTH, 1.0f,
+ BAR_SIZE/TEX_WIDTH , 1.0f - BAR_SIZE/TEX_HEIGHT,
+ BAR_SIZE/TEX_WIDTH , 1.0f
+ };
+
+ private ScrollbarLayer(LayerRenderer renderer, CairoImage image, boolean vertical, ByteBuffer buffer) {
+ super(image, TileLayer.PaintMode.NORMAL);
+ mVertical = vertical;
+ mRenderer = renderer;
+
+ IntSize size = image.getSize();
+ mBitmap = Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888);
+ mCanvas = new Canvas(mBitmap);
+
+ // Paint a spot to use as the scroll indicator
+ Paint foregroundPaint = new Paint();
+ foregroundPaint.setAntiAlias(true);
+ foregroundPaint.setStyle(Paint.Style.FILL);
+ foregroundPaint.setColor(Color.argb(127, 0, 0, 0));
+
+ mCanvas.drawColor(Color.argb(0, 0, 0, 0), PorterDuff.Mode.CLEAR);
+ mCanvas.drawCircle(CAP_RADIUS, CAP_RADIUS, CAP_RADIUS, foregroundPaint);
+
+ mBitmap.copyPixelsToBuffer(buffer.asIntBuffer());
+ }
+
+ public static ScrollbarLayer create(LayerRenderer renderer, boolean vertical) {
+ // just create an empty image for now, it will get drawn
+ // on demand anyway
+ int imageSize = IntSize.nextPowerOfTwo(BAR_SIZE);
+ ByteBuffer buffer = DirectBufferAllocator.allocate(imageSize * imageSize * 4);
+ CairoImage image = new BufferedCairoImage(buffer, imageSize, imageSize,
+ CairoImage.FORMAT_ARGB32);
+ return new ScrollbarLayer(renderer, image, vertical, buffer);
+ }
+
+ private void createProgram() {
+ int vertexShader = LayerRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
+ LayerRenderer.DEFAULT_VERTEX_SHADER);
+ int fragmentShader = LayerRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
+ FRAGMENT_SHADER);
+
+ mProgram = GLES20.glCreateProgram();
+ GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
+ GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
+ GLES20.glLinkProgram(mProgram); // creates OpenGL program executables
+
+ // Get handles to the shaders' vPosition, aTexCoord, sTexture, and uTMatrix members.
+ mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+ mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
+ mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
+ mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix");
+ mOpacityHandle = GLES20.glGetUniformLocation(mProgram, "uOpacity");
+ }
+
+ private void activateProgram() {
+ // Add the program to the OpenGL environment
+ GLES20.glUseProgram(mProgram);
+
+ // Set the transformation matrix
+ GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false,
+ LayerRenderer.DEFAULT_TEXTURE_MATRIX, 0);
+
+ // Enable the arrays from which we get the vertex and texture coordinates
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+ GLES20.glEnableVertexAttribArray(mTextureHandle);
+
+ GLES20.glUniform1i(mSampleHandle, 0);
+ GLES20.glUniform1f(mOpacityHandle, mOpacity);
+ }
+
+ private void deactivateProgram() {
+ GLES20.glDisableVertexAttribArray(mTextureHandle);
+ GLES20.glDisableVertexAttribArray(mPositionHandle);
+ GLES20.glUseProgram(0);
+ }
+
+ /**
+ * Decrease the opacity of the scrollbar by one frame's worth.
+ * Return true if the opacity was decreased, or false if the scrollbars
+ * are already fully faded out.
+ */
+ public boolean fade() {
+ if (FloatUtils.fuzzyEquals(mOpacity, 0.0f)) {
+ return false;
+ }
+ beginTransaction(); // called on compositor thread
+ mOpacity = Math.max(mOpacity - FADE_AMOUNT, 0.0f);
+ endTransaction();
+ return true;
+ }
+
+ /**
+ * Restore the opacity of the scrollbar to fully opaque.
+ * Return true if the opacity was changed, or false if the scrollbars
+ * are already fully opaque.
+ */
+ public boolean unfade() {
+ if (FloatUtils.fuzzyEquals(mOpacity, 1.0f)) {
+ return false;
+ }
+ beginTransaction(); // called on compositor thread
+ mOpacity = 1.0f;
+ endTransaction();
+
+ return true;
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ if (!initialized())
+ return;
+
+ // Create the shader program, if necessary
+ if (mProgram == 0) {
+ createProgram();
+ }
+
+ // Enable the shader program
+ mRenderer.deactivateDefaultProgram();
+ activateProgram();
+
+ GLES20.glEnable(GLES20.GL_BLEND);
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+
+ Rect rect = RectUtils.round(mVertical
+ ? getVerticalRect(context)
+ : getHorizontalRect(context));
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID());
+
+ float viewWidth = context.viewport.width();
+ float viewHeight = context.viewport.height();
+
+ float top = viewHeight - rect.top;
+ float bot = viewHeight - rect.bottom;
+
+ // Coordinates for the scrollbar's body combined with the texture coordinates
+ float[] bodyCoords = {
+ // x, y, z, texture_x, texture_y
+ rect.left/viewWidth, bot/viewHeight, 0,
+ BODY_TEX_COORDS[0], BODY_TEX_COORDS[1],
+
+ rect.left/viewWidth, (bot+rect.height())/viewHeight, 0,
+ BODY_TEX_COORDS[2], BODY_TEX_COORDS[3],
+
+ (rect.left+rect.width())/viewWidth, bot/viewHeight, 0,
+ BODY_TEX_COORDS[4], BODY_TEX_COORDS[5],
+
+ (rect.left+rect.width())/viewWidth, (bot+rect.height())/viewHeight, 0,
+ BODY_TEX_COORDS[6], BODY_TEX_COORDS[7]
+ };
+
+ // Get the buffer and handles from the context
+ FloatBuffer coordBuffer = context.coordBuffer;
+ int positionHandle = mPositionHandle;
+ int textureHandle = mTextureHandle;
+
+ // Make sure we are at position zero in the buffer in case other draw methods did not
+ // clean up after themselves
+ coordBuffer.position(0);
+ coordBuffer.put(bodyCoords);
+
+ // Unbind any the current array buffer so we can use client side buffers
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+
+ // Reset the position in the buffer for the next set of vertex and texture coordinates.
+ coordBuffer.position(0);
+
+ if (mVertical) {
+ // top endcap
+ float[] topCap = {
+ // x, y, z, texture_x, texture_y
+ rect.left/viewWidth, top/viewHeight, 0,
+ TOP_CAP_TEX_COORDS[0], TOP_CAP_TEX_COORDS[1],
+
+ rect.left/viewWidth, (top+CAP_RADIUS)/viewHeight, 0,
+ TOP_CAP_TEX_COORDS[2], TOP_CAP_TEX_COORDS[3],
+
+ (rect.left+BAR_SIZE)/viewWidth, top/viewHeight, 0,
+ TOP_CAP_TEX_COORDS[4], TOP_CAP_TEX_COORDS[5],
+
+ (rect.left+BAR_SIZE)/viewWidth, (top+CAP_RADIUS)/viewHeight, 0,
+ TOP_CAP_TEX_COORDS[6], TOP_CAP_TEX_COORDS[7]
+ };
+
+ coordBuffer.put(topCap);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the
+ // buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+
+ // Reset the position in the buffer for the next set of vertex and texture
+ // coordinates.
+ coordBuffer.position(0);
+
+ // bottom endcap
+ float[] botCap = {
+ // x, y, z, texture_x, texture_y
+ rect.left/viewWidth, (bot-CAP_RADIUS)/viewHeight, 0,
+ BOT_CAP_TEX_COORDS[0], BOT_CAP_TEX_COORDS[1],
+
+ rect.left/viewWidth, (bot)/viewHeight, 0,
+ BOT_CAP_TEX_COORDS[2], BOT_CAP_TEX_COORDS[3],
+
+ (rect.left+BAR_SIZE)/viewWidth, (bot-CAP_RADIUS)/viewHeight, 0,
+ BOT_CAP_TEX_COORDS[4], BOT_CAP_TEX_COORDS[5],
+
+ (rect.left+BAR_SIZE)/viewWidth, (bot)/viewHeight, 0,
+ BOT_CAP_TEX_COORDS[6], BOT_CAP_TEX_COORDS[7]
+ };
+
+ coordBuffer.put(botCap);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the
+ // buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+
+ // Reset the position in the buffer for the next set of vertex and texture
+ // coordinates.
+ coordBuffer.position(0);
+ } else {
+ // left endcap
+ float[] leftCap = {
+ // x, y, z, texture_x, texture_y
+ (rect.left-CAP_RADIUS)/viewWidth, bot/viewHeight, 0,
+ LEFT_CAP_TEX_COORDS[0], LEFT_CAP_TEX_COORDS[1],
+ (rect.left-CAP_RADIUS)/viewWidth, (bot+BAR_SIZE)/viewHeight, 0,
+ LEFT_CAP_TEX_COORDS[2], LEFT_CAP_TEX_COORDS[3],
+ (rect.left)/viewWidth, bot/viewHeight, 0, LEFT_CAP_TEX_COORDS[4],
+ LEFT_CAP_TEX_COORDS[5],
+ (rect.left)/viewWidth, (bot+BAR_SIZE)/viewHeight, 0,
+ LEFT_CAP_TEX_COORDS[6], LEFT_CAP_TEX_COORDS[7]
+ };
+
+ coordBuffer.put(leftCap);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the
+ // buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+
+ // Reset the position in the buffer for the next set of vertex and texture
+ // coordinates.
+ coordBuffer.position(0);
+
+ // right endcap
+ float[] rightCap = {
+ // x, y, z, texture_x, texture_y
+ rect.right/viewWidth, (bot)/viewHeight, 0,
+ RIGHT_CAP_TEX_COORDS[0], RIGHT_CAP_TEX_COORDS[1],
+
+ rect.right/viewWidth, (bot+BAR_SIZE)/viewHeight, 0,
+ RIGHT_CAP_TEX_COORDS[2], RIGHT_CAP_TEX_COORDS[3],
+
+ (rect.right+CAP_RADIUS)/viewWidth, (bot)/viewHeight, 0,
+ RIGHT_CAP_TEX_COORDS[4], RIGHT_CAP_TEX_COORDS[5],
+
+ (rect.right+CAP_RADIUS)/viewWidth, (bot+BAR_SIZE)/viewHeight, 0,
+ RIGHT_CAP_TEX_COORDS[6], RIGHT_CAP_TEX_COORDS[7]
+ };
+
+ coordBuffer.put(rightCap);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the
+ // buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20,
+ coordBuffer);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ }
+
+ // Enable the default shader program again
+ deactivateProgram();
+ mRenderer.activateDefaultProgram();
+ }
+
+ private RectF getVerticalRect(RenderContext context) {
+ RectF viewport = context.viewport;
+ RectF pageRect = context.pageRect;
+ float barStart = ((viewport.top - pageRect.top) * (viewport.height() / pageRect.height())) + CAP_RADIUS;
+ float barEnd = ((viewport.bottom - pageRect.top) * (viewport.height() / pageRect.height())) - CAP_RADIUS;
+ if (barStart > barEnd) {
+ float middle = (barStart + barEnd) / 2.0f;
+ barStart = barEnd = middle;
+ }
+ float right = viewport.width() - PADDING;
+ return new RectF(right - BAR_SIZE, barStart, right, barEnd);
+ }
+
+ private RectF getHorizontalRect(RenderContext context) {
+ RectF viewport = context.viewport;
+ RectF pageRect = context.pageRect;
+ float barStart = ((viewport.left - pageRect.left) * (viewport.width() / pageRect.width())) + CAP_RADIUS;
+ float barEnd = ((viewport.right - pageRect.left) * (viewport.width() / pageRect.width())) - CAP_RADIUS;
+ if (barStart > barEnd) {
+ float middle = (barStart + barEnd) / 2.0f;
+ barStart = barEnd = middle;
+ }
+ float bottom = viewport.height() - PADDING;
+ return new RectF(barStart, bottom - BAR_SIZE, barEnd, bottom);
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java b/android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java
new file mode 100644
index 0000000000..e89015b5ed
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java
@@ -0,0 +1,322 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import org.json.JSONException;
+
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.Stack;
+
+/**
+ * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector.
+ *
+ * This gesture detector is more reliable than the built-in ScaleGestureDetector because:
+ *
+ * - It doesn't assume that pointer IDs are numbered 0 and 1.
+ *
+ * - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some
+ * devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many
+ * pointers are down, with disastrous results (bug 706684).
+ *
+ * - Cancelling a zoom into a pan is handled correctly.
+ *
+ * - Starting with three or more fingers down, releasing fingers so that only two are down, and
+ * then performing a scale gesture is handled correctly.
+ *
+ * - It doesn't take pressure into account, which results in smoother scaling.
+ */
+public class SimpleScaleGestureDetector {
+ private static final String LOGTAG = "ScaleGestureDetector";
+
+ private SimpleScaleGestureListener mListener;
+ private long mLastEventTime;
+ private boolean mScaleResult;
+
+ /* Information about all pointers that are down. */
+ private LinkedList<PointerInfo> mPointerInfo;
+
+ /** Creates a new gesture detector with the given listener. */
+ public SimpleScaleGestureDetector(SimpleScaleGestureListener listener) {
+ mListener = listener;
+ mPointerInfo = new LinkedList<PointerInfo>();
+ }
+
+ /** Forward touch events to this function. */
+ public void onTouchEvent(MotionEvent event) {
+ switch (event.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ // If we get ACTION_DOWN while still tracking any pointers,
+ // something is wrong. Cancel the current gesture and start over.
+ if (getPointersDown() > 0)
+ onTouchEnd(event);
+ onTouchStart(event);
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ onTouchStart(event);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ onTouchMove(event);
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ onTouchEnd(event);
+ break;
+ }
+ }
+
+ private int getPointersDown() {
+ return mPointerInfo.size();
+ }
+
+ private int getActionIndex(MotionEvent event) {
+ return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ }
+
+ private void onTouchStart(MotionEvent event) {
+ mLastEventTime = event.getEventTime();
+ mPointerInfo.addFirst(PointerInfo.create(event, getActionIndex(event)));
+ if (getPointersDown() == 2) {
+ sendScaleGesture(EventType.BEGIN);
+ }
+ }
+
+ private void onTouchMove(MotionEvent event) {
+ mLastEventTime = event.getEventTime();
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ PointerInfo pointerInfo = pointerInfoForEventIndex(event, i);
+ if (pointerInfo != null) {
+ pointerInfo.populate(event, i);
+ }
+ }
+
+ if (getPointersDown() == 2) {
+ sendScaleGesture(EventType.CONTINUE);
+ }
+ }
+
+ private void onTouchEnd(MotionEvent event) {
+ mLastEventTime = event.getEventTime();
+
+ int action = event.getAction() & MotionEvent.ACTION_MASK;
+ boolean isCancel = (action == MotionEvent.ACTION_CANCEL ||
+ action == MotionEvent.ACTION_DOWN);
+
+ int id = event.getPointerId(getActionIndex(event));
+ ListIterator<PointerInfo> iterator = mPointerInfo.listIterator();
+ while (iterator.hasNext()) {
+ PointerInfo pointerInfo = iterator.next();
+ if (!(isCancel || pointerInfo.getId() == id)) {
+ continue;
+ }
+
+ // One of the pointers we were tracking was lifted. Remove its info object from the
+ // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this
+ // ended the gesture.
+ iterator.remove();
+ pointerInfo.recycle();
+ if (getPointersDown() == 1) {
+ sendScaleGesture(EventType.END);
+ }
+ }
+ }
+
+ /**
+ * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only
+ * one finger is down, returns the location of that finger.
+ */
+ public float getFocusX() {
+ switch (getPointersDown()) {
+ case 1:
+ return mPointerInfo.getFirst().getCurrent().x;
+ case 2:
+ PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
+ return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f;
+ }
+
+ Log.e(LOGTAG, "No gesture taking place in getFocusX()!");
+ return 0.0f;
+ }
+
+ /**
+ * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only
+ * one finger is down, returns the location of that finger.
+ */
+ public float getFocusY() {
+ switch (getPointersDown()) {
+ case 1:
+ return mPointerInfo.getFirst().getCurrent().y;
+ case 2:
+ PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
+ return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f;
+ }
+
+ Log.e(LOGTAG, "No gesture taking place in getFocusY()!");
+ return 0.0f;
+ }
+
+ /** Returns the most recent distance between the two pointers. */
+ public float getCurrentSpan() {
+ if (getPointersDown() != 2) {
+ Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!");
+ return 0.0f;
+ }
+
+ PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
+ return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent());
+ }
+
+ /** Returns the second most recent distance between the two pointers. */
+ public float getPreviousSpan() {
+ if (getPointersDown() != 2) {
+ Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!");
+ return 0.0f;
+ }
+
+ PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
+ PointF a = pointerA.getPrevious(), b = pointerB.getPrevious();
+ if (a == null || b == null) {
+ a = pointerA.getCurrent();
+ b = pointerB.getCurrent();
+ }
+
+ return PointUtils.distance(a, b);
+ }
+
+ /** Returns the time of the last event related to the gesture. */
+ public long getEventTime() {
+ return mLastEventTime;
+ }
+
+ /** Returns true if the scale gesture is in progress and false otherwise. */
+ public boolean isInProgress() {
+ return getPointersDown() == 2;
+ }
+
+ /* Sends the requested scale gesture notification to the listener. */
+ private void sendScaleGesture(EventType eventType) {
+ switch (eventType) {
+ case BEGIN:
+ mScaleResult = mListener.onScaleBegin(this);
+ break;
+ case CONTINUE:
+ if (mScaleResult) {
+ mListener.onScale(this);
+ }
+ break;
+ case END:
+ if (mScaleResult) {
+ mListener.onScaleEnd(this);
+ }
+ break;
+ }
+ }
+
+ /*
+ * Returns the pointer info corresponding to the given pointer index, or null if the pointer
+ * isn't one that's being tracked.
+ */
+ private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) {
+ int id = event.getPointerId(index);
+ for (PointerInfo pointerInfo : mPointerInfo) {
+ if (pointerInfo.getId() == id) {
+ return pointerInfo;
+ }
+ }
+ return null;
+ }
+
+ private enum EventType {
+ BEGIN,
+ CONTINUE,
+ END,
+ }
+
+ /* Encapsulates information about one of the two fingers involved in the gesture. */
+ private static class PointerInfo {
+ /* A free list that recycles pointer info objects, to reduce GC pauses. */
+ private static Stack<PointerInfo> sPointerInfoFreeList;
+
+ private int mId;
+ private PointF mCurrent, mPrevious;
+
+ private PointerInfo() {
+ // External users should use create() instead.
+ }
+
+ /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */
+ public static PointerInfo create(MotionEvent event, int index) {
+ if (sPointerInfoFreeList == null) {
+ sPointerInfoFreeList = new Stack<PointerInfo>();
+ }
+
+ PointerInfo pointerInfo;
+ if (sPointerInfoFreeList.empty()) {
+ pointerInfo = new PointerInfo();
+ } else {
+ pointerInfo = sPointerInfoFreeList.pop();
+ }
+
+ pointerInfo.populate(event, index);
+ return pointerInfo;
+ }
+
+ /*
+ * Fills in the fields of this instance from the given motion event and pointer index
+ * within that event.
+ */
+ public void populate(MotionEvent event, int index) {
+ mId = event.getPointerId(index);
+ mPrevious = mCurrent;
+ mCurrent = new PointF(event.getX(index), event.getY(index));
+ }
+
+ public void recycle() {
+ mId = -1;
+ mPrevious = mCurrent = null;
+ sPointerInfoFreeList.push(this);
+ }
+
+ public int getId() { return mId; }
+ public PointF getCurrent() { return mCurrent; }
+ public PointF getPrevious() { return mPrevious; }
+
+ @Override
+ public String toString() {
+ if (mId == -1) {
+ return "(up)";
+ }
+
+ try {
+ String prevString;
+ if (mPrevious == null) {
+ prevString = "n/a";
+ } else {
+ prevString = PointUtils.toJSON(mPrevious).toString();
+ }
+
+ // The current position should always be non-null.
+ String currentString = PointUtils.toJSON(mCurrent).toString();
+ return "id=" + mId + " cur=" + currentString + " prev=" + prevString;
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public static interface SimpleScaleGestureListener {
+ public boolean onScale(SimpleScaleGestureDetector detector);
+ public boolean onScaleBegin(SimpleScaleGestureDetector detector);
+ public void onScaleEnd(SimpleScaleGestureDetector detector);
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java
new file mode 100644
index 0000000000..0bc2716783
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java
@@ -0,0 +1,154 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
+import android.opengl.GLES20;
+
+import java.nio.FloatBuffer;
+
+/**
+ * Encapsulates the logic needed to draw a single textured tile.
+ *
+ * TODO: Repeating textures really should be their own type of layer.
+ */
+public class SingleTileLayer extends TileLayer {
+ private static final String LOGTAG = "GeckoSingleTileLayer";
+
+ private Rect mMask;
+
+ // To avoid excessive GC, declare some objects here that would otherwise
+ // be created and destroyed frequently during draw().
+ private final RectF mBounds;
+ private final RectF mTextureBounds;
+ private final RectF mViewport;
+ private final Rect mIntBounds;
+ private final Rect mSubRect;
+ private final RectF mSubRectF;
+ private final Region mMaskedBounds;
+ private final Rect mCropRect;
+ private final RectF mObjRectF;
+ private final float[] mCoords;
+
+ public SingleTileLayer(CairoImage image) {
+ this(false, image);
+ }
+
+ public SingleTileLayer(boolean repeat, CairoImage image) {
+ this(image, repeat ? TileLayer.PaintMode.REPEAT : TileLayer.PaintMode.NORMAL);
+ }
+
+ public SingleTileLayer(CairoImage image, TileLayer.PaintMode paintMode) {
+ super(image, paintMode);
+
+ mBounds = new RectF();
+ mTextureBounds = new RectF();
+ mViewport = new RectF();
+ mIntBounds = new Rect();
+ mSubRect = new Rect();
+ mSubRectF = new RectF();
+ mMaskedBounds = new Region();
+ mCropRect = new Rect();
+ mObjRectF = new RectF();
+ mCoords = new float[20];
+ }
+
+ /**
+ * Set an area to mask out when rendering.
+ */
+ public void setMask(Rect aMaskRect) {
+ mMask = aMaskRect;
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ // mTextureIDs may be null here during startup if Layer.java's draw method
+ // failed to acquire the transaction lock and call performUpdates.
+ if (!initialized())
+ return;
+
+ mViewport.set(context.viewport);
+
+ if (repeats()) {
+ // If we're repeating, we want to adjust the texture bounds so that
+ // the texture repeats the correct number of times when drawn at
+ // the size of the viewport.
+ mBounds.set(getBounds(context));
+ mTextureBounds.set(0.0f, 0.0f, mBounds.width(), mBounds.height());
+ mBounds.set(0.0f, 0.0f, mViewport.width(), mViewport.height());
+ } else if (stretches()) {
+ // If we're stretching, we just want the bounds and texture bounds
+ // to fit to the page.
+ mBounds.set(context.pageRect);
+ mTextureBounds.set(mBounds);
+ } else {
+ mBounds.set(getBounds(context));
+ mTextureBounds.set(mBounds);
+ }
+
+ mBounds.roundOut(mIntBounds);
+ mMaskedBounds.set(mIntBounds);
+ if (mMask != null) {
+ mMaskedBounds.op(mMask, Region.Op.DIFFERENCE);
+ if (mMaskedBounds.isEmpty())
+ return;
+ }
+
+ // XXX Possible optimisation here, form this array so we can draw it in
+ // a single call.
+ RegionIterator i = new RegionIterator(mMaskedBounds);
+ while (i.next(mSubRect)) {
+ // Compensate for rounding errors at the edge of the tile caused by
+ // the roundOut above
+ mSubRectF.set(Math.max(mBounds.left, (float)mSubRect.left),
+ Math.max(mBounds.top, (float)mSubRect.top),
+ Math.min(mBounds.right, (float)mSubRect.right),
+ Math.min(mBounds.bottom, (float)mSubRect.bottom));
+
+ // This is the left/top/right/bottom of the rect, relative to the
+ // bottom-left of the layer, to use for texture coordinates.
+ mCropRect.set(Math.round(mSubRectF.left - mBounds.left),
+ Math.round(mBounds.bottom - mSubRectF.top),
+ Math.round(mSubRectF.right - mBounds.left),
+ Math.round(mBounds.bottom - mSubRectF.bottom));
+
+ mObjRectF.set(mSubRectF.left - mViewport.left,
+ mViewport.bottom - mSubRectF.bottom,
+ mSubRectF.right - mViewport.left,
+ mViewport.bottom - mSubRectF.top);
+
+ fillRectCoordBuffer(mCoords, mObjRectF, mViewport.width(), mViewport.height(),
+ mCropRect, mTextureBounds.width(), mTextureBounds.height());
+
+ FloatBuffer coordBuffer = context.coordBuffer;
+ int positionHandle = context.positionHandle;
+ int textureHandle = context.textureHandle;
+
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID());
+
+ // Make sure we are at position zero in the buffer
+ coordBuffer.position(0);
+ coordBuffer.put(mCoords);
+
+ // Unbind any the current array buffer so we can use client side buffers
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer);
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ }
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/SubTile.java b/android/source/src/java/org/mozilla/gecko/gfx/SubTile.java
new file mode 100644
index 0000000000..bdad37195d
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/SubTile.java
@@ -0,0 +1,254 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import org.libreoffice.TileIdentifier;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+
+public class SubTile extends Layer {
+ private static String LOGTAG = SubTile.class.getSimpleName();
+ public final TileIdentifier id;
+
+ private final RectF mBounds;
+ private final RectF mTextureBounds;
+ private final RectF mViewport;
+ private final Rect mIntBounds;
+ private final Rect mSubRect;
+ private final RectF mSubRectF;
+ private final Region mMaskedBounds;
+ private final Rect mCropRect;
+ private final RectF mObjRectF;
+ private final float[] mCoords;
+
+ public boolean markedForRemoval = false;
+
+ private CairoImage mImage;
+ private IntSize mSize;
+ private int[] mTextureIDs;
+ private boolean mDirtyTile;
+
+ public SubTile(TileIdentifier id) {
+ super();
+ this.id = id;
+
+ mBounds = new RectF();
+ mTextureBounds = new RectF();
+ mViewport = new RectF();
+ mIntBounds = new Rect();
+ mSubRect = new Rect();
+ mSubRectF = new RectF();
+ mMaskedBounds = new Region();
+ mCropRect = new Rect();
+ mObjRectF = new RectF();
+ mCoords = new float[20];
+
+ mImage = null;
+ mTextureIDs = null;
+ mSize = new IntSize(0, 0);
+ mDirtyTile = false;
+ }
+
+ public void setImage(CairoImage image) {
+ if (image.getSize().isPositive()) {
+ this.mImage = image;
+ }
+ }
+
+ public void refreshTileMetrics() {
+ setPosition(id.getCSSRect());
+ }
+
+ public void markForRemoval() {
+ markedForRemoval = true;
+ }
+
+ protected int getTextureID() {
+ return mTextureIDs[0];
+ }
+
+ protected boolean initialized() {
+ return mTextureIDs != null;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroyImage();
+ cleanTexture();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void cleanTexture() {
+ if (mTextureIDs != null) {
+ TextureReaper.get().add(mTextureIDs);
+ mTextureIDs = null;
+ TextureReaper.get().reap();
+ }
+ }
+
+ public void destroy() {
+ try {
+ destroyImage();
+ cleanTexture();
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "Error clearing buffers: ", ex);
+ }
+ }
+
+ public void destroyImage() {
+ if (mImage != null) {
+ mImage.destroy();
+ mImage = null;
+ }
+ }
+
+ /**
+ * Invalidates the entire buffer so that it will be uploaded again. Only valid inside a
+ * transaction.
+ */
+ public void invalidate() {
+ if (!inTransaction()) {
+ throw new RuntimeException("invalidate() is only valid inside a transaction");
+ }
+ if (mImage == null) {
+ return;
+ }
+ mDirtyTile = true;
+ }
+
+ /**
+ * Remove the texture if the image is of different size than the current uploaded texture.
+ */
+ private void validateTexture() {
+ IntSize textureSize = mImage.getSize().nextPowerOfTwo();
+
+ if (!textureSize.equals(mSize)) {
+ mSize = textureSize;
+ cleanTexture();
+ }
+ }
+
+ @Override
+ protected void performUpdates(RenderContext context) {
+ super.performUpdates(context);
+ if (mImage == null && !mDirtyTile) {
+ return;
+ }
+ validateTexture();
+ uploadNewTexture();
+ mDirtyTile = false;
+ }
+
+ private void uploadNewTexture() {
+ ByteBuffer imageBuffer = mImage.getBuffer();
+ if (imageBuffer == null) {
+ return;
+ }
+
+ if (mTextureIDs == null) {
+ mTextureIDs = new int[1];
+ GLES20.glGenTextures(mTextureIDs.length, mTextureIDs, 0);
+ }
+
+ int cairoFormat = mImage.getFormat();
+ CairoGLInfo glInfo = new CairoGLInfo(cairoFormat);
+
+ bindAndSetGLParameters();
+
+ IntSize bufferSize = mImage.getSize();
+
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, glInfo.internalFormat,
+ mSize.width, mSize.height, 0, glInfo.format, glInfo.type, imageBuffer);
+
+ destroyImage();
+ }
+
+ private void bindAndSetGLParameters() {
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDs[0]);
+
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ }
+
+ @Override
+ public void draw(RenderContext context) {
+ // mTextureIDs may be null here during startup if Layer.java's draw method
+ // failed to acquire the transaction lock and call performUpdates.
+ if (!initialized())
+ return;
+
+ mViewport.set(context.viewport);
+
+ mBounds.set(getBounds(context));
+ mTextureBounds.set(mBounds);
+
+ mBounds.roundOut(mIntBounds);
+ mMaskedBounds.set(mIntBounds);
+
+ // XXX Possible optimisation here, form this array so we can draw it in
+ // a single call.
+ RegionIterator iterator = new RegionIterator(mMaskedBounds);
+ while (iterator.next(mSubRect)) {
+ // Compensate for rounding errors at the edge of the tile caused by
+ // the roundOut above
+ mSubRectF.set(Math.max(mBounds.left, (float) mSubRect.left),
+ Math.max(mBounds.top, (float) mSubRect.top),
+ Math.min(mBounds.right, (float) mSubRect.right),
+ Math.min(mBounds.bottom, (float) mSubRect.bottom));
+
+ // This is the left/top/right/bottom of the rect, relative to the
+ // bottom-left of the layer, to use for texture coordinates.
+ mCropRect.set(Math.round(mSubRectF.left - mBounds.left),
+ Math.round(mBounds.bottom - mSubRectF.top),
+ Math.round(mSubRectF.right - mBounds.left),
+ Math.round(mBounds.bottom - mSubRectF.bottom));
+
+ mObjRectF.set(mSubRectF.left - mViewport.left,
+ mViewport.bottom - mSubRectF.bottom,
+ mSubRectF.right - mViewport.left,
+ mViewport.bottom - mSubRectF.top);
+
+ fillRectCoordBuffer(mCoords, mObjRectF, mViewport.width(), mViewport.height(), mCropRect, mTextureBounds.width(), mTextureBounds.height());
+
+ FloatBuffer coordBuffer = context.coordBuffer;
+ int positionHandle = context.positionHandle;
+ int textureHandle = context.textureHandle;
+
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID());
+
+ // Make sure we are at position zero in the buffer
+ coordBuffer.position(0);
+ coordBuffer.put(mCoords);
+
+ // Unbind any the current array buffer so we can use client side buffers
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ // Vertex coordinates are x,y,z starting at position 0 into the buffer.
+ coordBuffer.position(0);
+ GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer);
+
+ // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer.
+ coordBuffer.position(3);
+ GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer);
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ }
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java b/android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
new file mode 100644
index 0000000000..5a752e3c71
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
@@ -0,0 +1,78 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+import android.os.Handler;
+
+class SubdocumentScrollHelper {
+ private static final String LOGTAG = "GeckoSubdocumentScrollHelper";
+
+ private final Handler mUiHandler;
+
+ /* This is the amount of displacement we have accepted but not yet sent to JS; this is
+ * only valid when mOverrideScrollPending is true. */
+ private final PointF mPendingDisplacement;
+
+ /* When this is true, we're sending scroll events to JS to scroll the active subdocument. */
+ private boolean mOverridePanning;
+
+ /* When this is true, we have received an ack for the last scroll event we sent to JS, and
+ * are ready to send the next scroll event. Note we only ever have one scroll event inflight
+ * at a time. */
+ private boolean mOverrideScrollAck;
+
+ /* When this is true, we have a pending scroll that we need to send to JS; we were unable
+ * to send it when it was initially requested because mOverrideScrollAck was not true. */
+ private boolean mOverrideScrollPending;
+
+ /* When this is true, the last scroll event we sent actually did some amount of scrolling on
+ * the subdocument; we use this to decide when we have reached the end of the subdocument. */
+ private boolean mScrollSucceeded;
+
+ SubdocumentScrollHelper() {
+ // mUiHandler will be bound to the UI thread since that's where this constructor runs
+ mUiHandler = new Handler();
+ mPendingDisplacement = new PointF();
+ }
+
+ void destroy() {
+ }
+
+ boolean scrollBy(PointF displacement) {
+ if (! mOverridePanning) {
+ return false;
+ }
+
+ if (! mOverrideScrollAck) {
+ mOverrideScrollPending = true;
+ mPendingDisplacement.x += displacement.x;
+ mPendingDisplacement.y += displacement.y;
+ return true;
+ }
+
+ mOverrideScrollAck = false;
+ mOverrideScrollPending = false;
+ // clear the |mPendingDisplacement| after serializing |displacement| to
+ // JSON because they might be the same object
+ mPendingDisplacement.x = 0;
+ mPendingDisplacement.y = 0;
+
+ return true;
+ }
+
+ void cancel() {
+ mOverridePanning = false;
+ }
+
+ boolean scrolling() {
+ return mOverridePanning;
+ }
+
+ boolean lastScrollSucceeded() {
+ return mScrollSucceeded;
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TextLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/TextLayer.java
new file mode 100644
index 0000000000..023433a888
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/TextLayer.java
@@ -0,0 +1,69 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+
+import org.libreoffice.kit.DirectBufferAllocator;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Draws text on a layer. This is used for the frame rate meter.
+ */
+public class TextLayer extends SingleTileLayer {
+ private final ByteBuffer mBuffer; // this buffer is owned by the BufferedCairoImage
+ private final IntSize mSize;
+
+ /*
+ * This awkward pattern is necessary due to Java's restrictions on when one can call superclass
+ * constructors.
+ */
+ private TextLayer(ByteBuffer buffer, BufferedCairoImage image, IntSize size, String text) {
+ super(false, image);
+ mBuffer = buffer;
+ mSize = size;
+ renderText(text);
+ }
+
+ public static TextLayer create(IntSize size, String text) {
+ ByteBuffer buffer = DirectBufferAllocator.allocate(size.width * size.height * 4);
+ BufferedCairoImage image = new BufferedCairoImage(buffer, size.width, size.height,
+ CairoImage.FORMAT_ARGB32);
+ return new TextLayer(buffer, image, size, text);
+ }
+
+ public void setText(String text) {
+ renderText(text);
+ invalidate();
+ }
+
+ private void renderText(String text) {
+ 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(text) + 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(text, 6.0f, 18.0f, textPaint);
+
+ bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer());
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TextureGenerator.java b/android/source/src/java/org/mozilla/gecko/gfx/TextureGenerator.java
new file mode 100644
index 0000000000..bccd8968c8
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/TextureGenerator.java
@@ -0,0 +1,77 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLContext;
+
+public class TextureGenerator {
+ private static final String LOGTAG = "TextureGenerator";
+ private static final int POOL_SIZE = 5;
+
+ private static TextureGenerator sSharedInstance;
+
+ private ArrayBlockingQueue<Integer> mTextureIds;
+ private EGLContext mContext;
+
+ private TextureGenerator() {
+ mTextureIds = new ArrayBlockingQueue<Integer>(POOL_SIZE);
+ }
+
+ public static TextureGenerator get() {
+ if (sSharedInstance == null)
+ sSharedInstance = new TextureGenerator();
+ return sSharedInstance;
+ }
+
+ public synchronized int take() {
+ try {
+ // Will block until one becomes available
+ return mTextureIds.take();
+ } catch (InterruptedException e) {
+ return 0;
+ }
+ }
+
+ public synchronized void fill() {
+ EGL10 egl = (EGL10) EGLContext.getEGL();
+ EGLContext context = egl.eglGetCurrentContext();
+
+ if (mContext != null && mContext != context) {
+ mTextureIds.clear();
+ }
+
+ mContext = context;
+
+ int numNeeded = mTextureIds.remainingCapacity();
+ if (numNeeded == 0)
+ return;
+
+ // Clear existing GL errors
+ int error;
+ while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ Log.w(LOGTAG, String.format("Clearing GL error: %#x", error));
+ }
+
+ int[] textures = new int[numNeeded];
+ GLES20.glGenTextures(numNeeded, textures, 0);
+
+ error = GLES20.glGetError();
+ if (error != GLES20.GL_NO_ERROR) {
+ Log.e(LOGTAG, String.format("Failed to generate textures: %#x", error), new Exception());
+ return;
+ }
+
+ for (int i = 0; i < numNeeded; i++) {
+ mTextureIds.offer(textures[i]);
+ }
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TextureReaper.java b/android/source/src/java/org/mozilla/gecko/gfx/TextureReaper.java
new file mode 100644
index 0000000000..1a8a504597
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/TextureReaper.java
@@ -0,0 +1,62 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.opengl.GLES20;
+import android.util.Log;
+
+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<Integer> mDeadTextureIDs = new ArrayList<Integer>();
+ private static final String LOGTAG = TextureReaper.class.getSimpleName();
+
+ private TextureReaper() {
+ }
+
+ public static TextureReaper get() {
+ if (sSharedInstance == null) {
+ sSharedInstance = new TextureReaper();
+ }
+ return sSharedInstance;
+ }
+
+ public void add(int[] textureIDs) {
+ for (int textureID : textureIDs) {
+ add(textureID);
+ }
+ }
+
+ public synchronized void add(int textureID) {
+ mDeadTextureIDs.add(textureID);
+ }
+
+ public synchronized void reap() {
+ int numTextures = mDeadTextureIDs.size();
+ // Adreno 200 will generate INVALID_VALUE if len == 0 is passed to glDeleteTextures,
+ // even though it's not supposed to.
+ if (numTextures == 0)
+ return;
+
+ int[] deadTextureIDs = new int[numTextures];
+ for (int i = 0; i < numTextures; i++) {
+ Integer id = mDeadTextureIDs.get(i);
+ if (id == null) {
+ deadTextureIDs[i] = 0;
+ Log.e(LOGTAG, "Dead texture id is null");
+ } else {
+ deadTextureIDs[i] = mDeadTextureIDs.get(i);
+ }
+ }
+ mDeadTextureIDs.clear();
+
+ GLES20.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0);
+ }
+} \ No newline at end of file
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/TileLayer.java
new file mode 100644
index 0000000000..3d0ff1fede
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/TileLayer.java
@@ -0,0 +1,176 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.Rect;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Base class for tile layers, which encapsulate the logic needed to draw textured tiles in OpenGL
+ * ES.
+ */
+public abstract class TileLayer extends Layer {
+ private static final String LOGTAG = "GeckoTileLayer";
+
+ private final Rect mDirtyRect;
+ private IntSize mSize;
+ private int[] mTextureIDs;
+
+ protected final CairoImage mImage;
+
+ public CairoImage getImage() {
+ return mImage;
+ }
+
+ public enum PaintMode { NORMAL, REPEAT, STRETCH };
+ private PaintMode mPaintMode;
+
+ public TileLayer(CairoImage image, PaintMode paintMode) {
+ super(image == null ? null : image.getSize());
+
+ mPaintMode = paintMode;
+ mImage = image;
+ mSize = new IntSize(0, 0);
+ mDirtyRect = new Rect();
+ }
+
+ protected boolean repeats() { return mPaintMode == PaintMode.REPEAT; }
+ protected boolean stretches() { return mPaintMode == PaintMode.STRETCH; }
+ protected int getTextureID() { return mTextureIDs[0]; }
+ protected boolean initialized() { return mImage != null && mTextureIDs != null; }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mTextureIDs != null)
+ TextureReaper.get().add(mTextureIDs);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ public void destroy() {
+ try {
+ if (mImage != null) {
+ mImage.destroy();
+ }
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error clearing buffers: ", ex);
+ }
+ }
+
+ public void setPaintMode(PaintMode mode) {
+ mPaintMode = mode;
+ }
+
+ /**
+ * Invalidates the entire buffer so that it will be uploaded again. Only valid inside a
+ * transaction.
+ */
+ public void invalidate() {
+ if (!inTransaction())
+ throw new RuntimeException("invalidate() is only valid inside a transaction");
+ IntSize bufferSize = mImage.getSize();
+ mDirtyRect.set(0, 0, bufferSize.width, bufferSize.height);
+ }
+
+ private void validateTexture() {
+ /* Calculate the ideal texture size. This must be a power of two if
+ * the texture is repeated or OpenGL ES 2.0 isn't supported, as
+ * OpenGL ES 2.0 is required for NPOT texture support (without
+ * extensions), but doesn't support repeating NPOT textures.
+ *
+ * XXX Currently, we don't pick a GLES 2.0 context, so always round.
+ */
+ IntSize textureSize = mImage.getSize().nextPowerOfTwo();
+
+ if (!textureSize.equals(mSize)) {
+ mSize = textureSize;
+
+ // Delete the old texture
+ if (mTextureIDs != null) {
+ TextureReaper.get().add(mTextureIDs);
+ mTextureIDs = null;
+
+ // Free the texture immediately, so we don't incur a
+ // temporarily increased memory usage.
+ TextureReaper.get().reap();
+ }
+ }
+ }
+
+ @Override
+ protected void performUpdates(RenderContext context) {
+ super.performUpdates(context);
+
+ // Reallocate the texture if the size has changed
+ validateTexture();
+
+ // Don't do any work if the image has an invalid size.
+ if (!mImage.getSize().isPositive())
+ return;
+
+ // If we haven't allocated a texture, assume the whole region is dirty
+ if (mTextureIDs == null) {
+ uploadFullTexture();
+ } else {
+ uploadDirtyRect(mDirtyRect);
+ }
+
+ mDirtyRect.setEmpty();
+ }
+
+ private void uploadFullTexture() {
+ IntSize bufferSize = mImage.getSize();
+ uploadDirtyRect(new Rect(0, 0, bufferSize.width, bufferSize.height));
+ }
+
+ private void uploadDirtyRect(Rect dirtyRect) {
+ // If we have nothing to upload, just return for now
+ if (dirtyRect.isEmpty())
+ return;
+
+ // It's possible that the buffer will be null, check for that and return
+ ByteBuffer imageBuffer = mImage.getBuffer();
+ if (imageBuffer == null)
+ return;
+
+ if (mTextureIDs == null) {
+ mTextureIDs = new int[1];
+ GLES20.glGenTextures(mTextureIDs.length, mTextureIDs, 0);
+ }
+
+ int cairoFormat = mImage.getFormat();
+ CairoGLInfo glInfo = new CairoGLInfo(cairoFormat);
+
+ bindAndSetGLParameters();
+
+ // XXX TexSubImage2D is too broken to rely on Adreno, and very slow
+ // on other chipsets, so we always upload the entire buffer.
+ IntSize bufferSize = mImage.getSize();
+
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width,
+ mSize.height, 0, glInfo.format, glInfo.type, imageBuffer);
+
+ }
+
+ private void bindAndSetGLParameters() {
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDs[0]);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_LINEAR);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR);
+
+ int repeatMode = repeats() ? GLES20.GL_REPEAT : GLES20.GL_CLAMP_TO_EDGE;
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, repeatMode);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, repeatMode);
+ }
+}
+
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java b/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
new file mode 100644
index 0000000000..1c227de20b
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
@@ -0,0 +1,306 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * This class handles incoming touch events from the user and sends them to
+ * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom
+ * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD.
+ *
+ * In the following code/comments, a "block" of events refers to a contiguous
+ * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to
+ * but not including the next DOWN or POINTER_DOWN event.
+ *
+ * "Dispatching" an event refers to performing the default actions for the event,
+ * which at our level of abstraction just means sending it off to the gesture
+ * detectors and the pan/zoom controller.
+ *
+ * If an event is "default-prevented" that means one or more listeners in Gecko
+ * has called preventDefault() on the event, which means that the default action
+ * for that event should not occur. Usually we care about a "block" of events being
+ * default-prevented, which means that the DOWN/POINTER_DOWN event that started
+ * the block, or the first MOVE event following that, were prevent-defaulted.
+ *
+ * A "default-prevented notification" is when we here in Java-land receive a notification
+ * from gecko as to whether or not a block of events was default-prevented. This happens
+ * at some point after the first or second event in the block is processed in Gecko.
+ * This code assumes we get EXACTLY ONE default-prevented notification for each block
+ * of events.
+ *
+ * Note that even if all events are default-prevented, we still send specific types
+ * of notifications to the pan/zoom controller. The notifications are needed
+ * to respond to user actions a timely manner regardless of default-prevention,
+ * and fix issues like bug 749384.
+ */
+public final class TouchEventHandler {
+ private static final String LOGTAG = "GeckoTouchEventHandler";
+
+ // The time limit for listeners to respond with preventDefault on touchevents
+ // before we begin panning the page
+ private final int EVENT_LISTENER_TIMEOUT = 200;
+
+ private final View mView;
+ private final GestureDetector mGestureDetector;
+ private final SimpleScaleGestureDetector mScaleGestureDetector;
+ private final JavaPanZoomController mPanZoomController;
+
+ // the queue of events that we are holding on to while waiting for a preventDefault
+ // notification
+ private final Queue<MotionEvent> mEventQueue;
+ private final ListenerTimeoutProcessor mListenerTimeoutProcessor;
+
+ // whether or not we should wait for touch listeners to respond (this state is
+ // per-tab and is updated when we switch tabs).
+ private boolean mWaitForTouchListeners;
+
+ // true if we should hold incoming events in our queue. this is re-set for every
+ // block of events, this is cleared once we find out if the block has been
+ // default-prevented or not (or we time out waiting for that).
+ private boolean mHoldInQueue;
+
+ // true if we should dispatch incoming events to the gesture detector and the pan/zoom
+ // controller. if this is false, then the current block of events has been
+ // default-prevented, and we should not dispatch these events (although we'll still send
+ // them to gecko listeners).
+ private boolean mDispatchEvents;
+
+ // this next variable requires some explanation. strap yourself in.
+ //
+ // for each block of events, we do two things: (1) send the events to gecko and expect
+ // exactly one default-prevented notification in return, and (2) kick off a delayed
+ // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in
+ // a timely fashion.
+ // since events are constantly coming in, we need to be able to handle more than one
+ // block of events in the queue.
+ //
+ // this means that there are ordering restrictions on these that we can take advantage of,
+ // and need to abide by. blocks of events in the queue will always be in the order that
+ // the user generated them. default-prevented notifications we get from gecko will be in
+ // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that
+ // have been posted will also fire in the same order as the blocks of events in the queue.
+ // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple
+ // ListenerTimeoutProcessor firings, and that interleaving is not predictable.
+ //
+ // therefore, we need to make sure that for each block of events, we process the queued
+ // events exactly once, either when we get the default-prevented notification, or when the
+ // timeout expires (whichever happens first). there is no way to associate the
+ // default-prevented notification with a particular block of events other than via ordering,
+ //
+ // so what we do to accomplish this is to track a "processing balance", which is the number
+ // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors
+ // that have fired. (think "balance" as in teeter-totter balance). this value is:
+ // - zero when we are in a state where the next default-prevented notification we expect
+ // to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to
+ // the next block of events in the queue.
+ // - positive when we are in a state where we have received more default-prevented notifications
+ // than ListenerTimeoutProcessors. This means that the next default-prevented notification
+ // does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors
+ // need to be ignored as they are for blocks we have already processed. (n is the absolute value
+ // of the balance.)
+ // - negative when we are in a state where we have received more ListenerTimeoutProcessors than
+ // default-prevented notifications. This means that the next ListenerTimeoutProcessor that
+ // we receive does correspond to the block at the head of the queue, but the next n
+ // default-prevented notifications need to be ignored as they are for blocks we have already
+ // processed. (n is the absolute value of the balance.)
+ private int mProcessingBalance;
+
+ TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) {
+ mView = view;
+
+ mEventQueue = new LinkedList<MotionEvent>();
+ mPanZoomController = panZoomController;
+ mGestureDetector = new GestureDetector(context, mPanZoomController);
+ mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
+ mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
+ mDispatchEvents = true;
+
+ mGestureDetector.setOnDoubleTapListener(mPanZoomController);
+ }
+
+ void destroy() {
+ }
+
+ /* This function MUST be called on the UI thread */
+ public boolean handleEvent(MotionEvent event) {
+ if (isDownEvent(event)) {
+ // this is the start of a new block of events! whee!
+ mHoldInQueue = mWaitForTouchListeners;
+
+ // Set mDispatchEvents to true so that we are guaranteed to either queue these
+ // events or dispatch them. The only time we should not do either is once we've
+ // heard back from content to preventDefault this block.
+ mDispatchEvents = true;
+ if (mHoldInQueue) {
+ // if the new block we are starting is the current block (i.e. there are no
+ // other blocks waiting in the queue, then we should let the pan/zoom controller
+ // know we are waiting for the touch listeners to run
+ if (mEventQueue.isEmpty()) {
+ mPanZoomController.startingNewEventBlock(event, true);
+ }
+ } else {
+ // we're not going to be holding this block of events in the queue, but we need
+ // a marker of some sort so that the processEventBlock loop deals with the blocks
+ // in the right order as notifications come in. we use a single null event in
+ // the queue as a placeholder for a block of events that has already been dispatched.
+ mEventQueue.add(null);
+ mPanZoomController.startingNewEventBlock(event, false);
+ }
+
+ // set the timeout so that we dispatch these events and update mProcessingBalance
+ // if we don't get a default-prevented notification
+ mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
+ }
+
+ // if we need to hold the events, add it to the queue. if we need to dispatch
+ // it directly, do that. it is possible that both mHoldInQueue and mDispatchEvents
+ // are false, in which case we are processing a block of events that we know
+ // has been default-prevented. in that case we don't keep the events as we don't
+ // need them (but we still pass them to the gecko listener).
+ if (mHoldInQueue) {
+ mEventQueue.add(MotionEvent.obtain(event));
+ } else if (mDispatchEvents) {
+ dispatchEvent(event);
+ } else if (touchFinished(event)) {
+ mPanZoomController.preventedTouchFinished();
+ }
+
+ return true;
+ }
+
+ /**
+ * This function is how gecko sends us a default-prevented notification. It is called
+ * once gecko knows definitively whether the block of events has had preventDefault
+ * called on it (either on the initial down event that starts the block, or on
+ * the first event following that down event).
+ *
+ * This function MUST be called on the UI thread.
+ */
+ public void handleEventListenerAction(boolean allowDefaultAction) {
+ if (mProcessingBalance > 0) {
+ // this event listener that triggered this took too long, and the corresponding
+ // ListenerTimeoutProcessor runnable already ran for the event in question. the
+ // block of events this is for has already been processed, so we don't need to
+ // do anything here.
+ } else {
+ processEventBlock(allowDefaultAction);
+ }
+ mProcessingBalance--;
+ }
+
+ /* This function MUST be called on the UI thread. */
+ public void setWaitForTouchListeners(boolean aValue) {
+ mWaitForTouchListeners = aValue;
+ }
+
+ private boolean isDownEvent(MotionEvent event) {
+ int action = (event.getAction() & MotionEvent.ACTION_MASK);
+ return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
+ }
+
+ private boolean touchFinished(MotionEvent event) {
+ int action = (event.getAction() & MotionEvent.ACTION_MASK);
+ return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
+ }
+
+ /**
+ * Dispatch the event to the gesture detectors and the pan/zoom controller.
+ */
+ private void dispatchEvent(MotionEvent event) {
+ if (mGestureDetector.onTouchEvent(event)) {
+ return;
+ }
+ mScaleGestureDetector.onTouchEvent(event);
+ if (mScaleGestureDetector.isInProgress()) {
+ return;
+ }
+ mPanZoomController.handleEvent(event);
+ }
+
+ /**
+ * Process the block of events at the head of the queue now that we know
+ * whether it has been default-prevented or not.
+ */
+ private void processEventBlock(boolean allowDefaultAction) {
+ if (!allowDefaultAction) {
+ // if the block has been default-prevented, cancel whatever stuff we had in
+ // progress in the gesture detector and pan zoom controller
+ long now = SystemClock.uptimeMillis();
+ dispatchEvent(MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0));
+ }
+
+ if (mEventQueue.isEmpty()) {
+ Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
+ return;
+ }
+
+ // the odd loop condition is because the first event in the queue will
+ // always be a DOWN or POINTER_DOWN event, and we want to process all
+ // the events in the queue starting at that one, up to but not including
+ // the next DOWN or POINTER_DOWN event.
+
+ MotionEvent event = mEventQueue.poll();
+ while (true) {
+ // event being null here is valid and represents a block of events
+ // that has already been dispatched.
+
+ if (event != null) {
+ // for each event we process, only dispatch it if the block hasn't been
+ // default-prevented.
+ if (allowDefaultAction) {
+ dispatchEvent(event);
+ } else if (touchFinished(event)) {
+ mPanZoomController.preventedTouchFinished();
+ }
+ }
+ if (mEventQueue.isEmpty()) {
+ // we have processed the backlog of events, and are all caught up.
+ // now we can set clear the hold flag and set the dispatch flag so
+ // that the handleEvent() function can do the right thing for all
+ // remaining events in this block (which is still ongoing) without
+ // having to put them in the queue.
+ mHoldInQueue = false;
+ mDispatchEvents = allowDefaultAction;
+ break;
+ }
+ event = mEventQueue.peek();
+ if (event == null || isDownEvent(event)) {
+ // we have finished processing the block we were interested in.
+ // now we wait for the next call to processEventBlock
+ if (event != null) {
+ mPanZoomController.startingNewEventBlock(event, true);
+ }
+ break;
+ }
+ // pop the event we peeked above, as it is still part of the block and
+ // we want to keep processing
+ mEventQueue.remove();
+ }
+ }
+
+ private class ListenerTimeoutProcessor implements Runnable {
+ /* This MUST be run on the UI thread */
+ public void run() {
+ if (mProcessingBalance < 0) {
+ // gecko already responded with default-prevented notification, and so
+ // the block of events this ListenerTimeoutProcessor corresponds to have
+ // already been removed from the queue.
+ } else {
+ processEventBlock(true);
+ }
+ mProcessingBalance++;
+ }
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java b/android/source/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java
new file mode 100644
index 0000000000..f8b5c2e055
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java
@@ -0,0 +1,173 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.gfx;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * ViewportMetrics manages state and contains some utility functions related to
+ * the page viewport for the Gecko layer client to use.
+ */
+public class ViewportMetrics {
+ private static final String LOGTAG = "GeckoViewportMetrics";
+
+ private RectF mPageRect;
+ private RectF mCssPageRect;
+ private RectF mViewportRect;
+ private float mZoomFactor;
+
+ public ViewportMetrics(DisplayMetrics metrics) {
+ mPageRect = new RectF(0, 0, metrics.widthPixels, metrics.heightPixels);
+ mCssPageRect = new RectF(0, 0, metrics.widthPixels, metrics.heightPixels);
+ mViewportRect = new RectF(0, 0, metrics.widthPixels, metrics.heightPixels);
+ mZoomFactor = 1.0f;
+ }
+
+ public ViewportMetrics(ViewportMetrics viewport) {
+ mPageRect = new RectF(viewport.getPageRect());
+ mCssPageRect = new RectF(viewport.getCssPageRect());
+ mViewportRect = new RectF(viewport.getViewport());
+ mZoomFactor = viewport.getZoomFactor();
+ }
+
+ public ViewportMetrics(ImmutableViewportMetrics viewport) {
+ mPageRect = new RectF(viewport.pageRectLeft,
+ viewport.pageRectTop,
+ viewport.pageRectRight,
+ viewport.pageRectBottom);
+ mCssPageRect = new RectF(viewport.cssPageRectLeft,
+ viewport.cssPageRectTop,
+ viewport.cssPageRectRight,
+ viewport.cssPageRectBottom);
+ mViewportRect = new RectF(viewport.viewportRectLeft,
+ viewport.viewportRectTop,
+ viewport.viewportRectRight,
+ viewport.viewportRectBottom);
+ mZoomFactor = viewport.zoomFactor;
+ }
+
+ public ViewportMetrics(JSONObject json) throws JSONException {
+ float x = (float)json.getDouble("x");
+ float y = (float)json.getDouble("y");
+ float width = (float)json.getDouble("width");
+ float height = (float)json.getDouble("height");
+ float pageLeft = (float)json.getDouble("pageLeft");
+ float pageTop = (float)json.getDouble("pageTop");
+ float pageRight = (float)json.getDouble("pageRight");
+ float pageBottom = (float)json.getDouble("pageBottom");
+ float cssPageLeft = (float)json.getDouble("cssPageLeft");
+ float cssPageTop = (float)json.getDouble("cssPageTop");
+ float cssPageRight = (float)json.getDouble("cssPageRight");
+ float cssPageBottom = (float)json.getDouble("cssPageBottom");
+ float zoom = (float)json.getDouble("zoom");
+
+ mPageRect = new RectF(pageLeft, pageTop, pageRight, pageBottom);
+ mCssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ mViewportRect = new RectF(x, y, x + width, y + height);
+ mZoomFactor = zoom;
+ }
+
+ public ViewportMetrics(float x, float y, float width, float height,
+ float pageLeft, float pageTop, float pageRight, float pageBottom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom,
+ float zoom) {
+ mPageRect = new RectF(pageLeft, pageTop, pageRight, pageBottom);
+ mCssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ mViewportRect = new RectF(x, y, x + width, y + height);
+ mZoomFactor = zoom;
+ }
+
+ public PointF getOrigin() {
+ return new PointF(mViewportRect.left, mViewportRect.top);
+ }
+
+ public FloatSize getSize() {
+ return new FloatSize(mViewportRect.width(), mViewportRect.height());
+ }
+
+ public RectF getViewport() {
+ return mViewportRect;
+ }
+
+ public RectF getCssViewport() {
+ return RectUtils.scale(mViewportRect, 1/mZoomFactor);
+ }
+
+ public RectF getPageRect() {
+ return mPageRect;
+ }
+
+ public RectF getCssPageRect() {
+ return mCssPageRect;
+ }
+
+ public float getZoomFactor() {
+ return mZoomFactor;
+ }
+
+ public void setPageRect(RectF pageRect, RectF cssPageRect) {
+ mPageRect = pageRect;
+ mCssPageRect = cssPageRect;
+ }
+
+ public void setViewport(RectF viewport) {
+ mViewportRect = viewport;
+ }
+
+ public void setOrigin(PointF origin) {
+ mViewportRect.set(origin.x, origin.y,
+ origin.x + mViewportRect.width(),
+ origin.y + mViewportRect.height());
+ }
+
+ public void setSize(FloatSize size) {
+ mViewportRect.right = mViewportRect.left + size.width;
+ mViewportRect.bottom = mViewportRect.top + size.height;
+ }
+
+ public void setZoomFactor(float zoomFactor) {
+ mZoomFactor = zoomFactor;
+ }
+
+ public String toJSON() {
+ // Round off height and width. Since the height and width are the size of the screen, it
+ // makes no sense to send non-integer coordinates to Gecko.
+ int height = Math.round(mViewportRect.height());
+ int width = Math.round(mViewportRect.width());
+
+ StringBuffer sb = new StringBuffer(512);
+ sb.append("{ \"x\" : ").append(mViewportRect.left)
+ .append(", \"y\" : ").append(mViewportRect.top)
+ .append(", \"width\" : ").append(width)
+ .append(", \"height\" : ").append(height)
+ .append(", \"pageLeft\" : ").append(mPageRect.left)
+ .append(", \"pageTop\" : ").append(mPageRect.top)
+ .append(", \"pageRight\" : ").append(mPageRect.right)
+ .append(", \"pageBottom\" : ").append(mPageRect.bottom)
+ .append(", \"cssPageLeft\" : ").append(mCssPageRect.left)
+ .append(", \"cssPageTop\" : ").append(mCssPageRect.top)
+ .append(", \"cssPageRight\" : ").append(mCssPageRect.right)
+ .append(", \"cssPageBottom\" : ").append(mCssPageRect.bottom)
+ .append(", \"zoom\" : ").append(mZoomFactor)
+ .append(" }");
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer buff = new StringBuffer(256);
+ buff.append("v=").append(mViewportRect.toString())
+ .append(" p=").append(mPageRect.toString())
+ .append(" c=").append(mCssPageRect.toString())
+ .append(" z=").append(mZoomFactor);
+ return buff.toString();
+ }
+}
diff --git a/android/source/src/java/org/mozilla/gecko/util/FloatUtils.java b/android/source/src/java/org/mozilla/gecko/util/FloatUtils.java
new file mode 100644
index 0000000000..a48266c573
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/util/FloatUtils.java
@@ -0,0 +1,41 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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 org.mozilla.gecko.util;
+
+import android.graphics.PointF;
+
+public final class FloatUtils {
+ private FloatUtils() {}
+
+ public static boolean fuzzyEquals(float a, float b) {
+ return (Math.abs(a - b) < 1e-6);
+ }
+
+ public static boolean fuzzyEquals(PointF a, PointF b) {
+ return fuzzyEquals(a.x, b.x) && fuzzyEquals(a.y, b.y);
+ }
+
+ /*
+ * Returns the value that represents a linear transition between `from` and `to` at time `t`,
+ * which is on the scale [0, 1). Thus with t = 0.0f, this returns `from`; with t = 1.0f, this
+ * returns `to`; with t = 0.5f, this returns the value halfway from `from` to `to`.
+ */
+ public static float interpolate(float from, float to, float t) {
+ return from + (to - from) * t;
+ }
+
+ /**
+ * Returns 'value', clamped so that it isn't any lower than 'low', and it
+ * isn't any higher than 'high'.
+ */
+ public static float clamp(float value, float low, float high) {
+ if (high < low) {
+ throw new IllegalArgumentException(
+ "clamp called with invalid parameters (" + high + " < " + low + ")" );
+ }
+ return Math.max(low, Math.min(high, value));
+ }
+}