summaryrefslogtreecommitdiffstats
path: root/android/source/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:51:28 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:51:28 +0000
commit940b4d1848e8c70ab7642901a68594e8016caffc (patch)
treeeb72f344ee6c3d9b80a7ecc079ea79e9fba8676d /android/source/src
parentInitial commit. (diff)
downloadlibreoffice-940b4d1848e8c70ab7642901a68594e8016caffc.tar.xz
libreoffice-940b4d1848e8c70ab7642901a68594e8016caffc.zip
Adding upstream version 1:7.0.4.upstream/1%7.0.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'android/source/src')
-rw-r--r--android/source/src/java/org/libreoffice/AboutDialogFragment.java113
-rw-r--r--android/source/src/java/org/libreoffice/ColorPaletteAdapter.java131
-rw-r--r--android/source/src/java/org/libreoffice/ColorPaletteListener.java6
-rw-r--r--android/source/src/java/org/libreoffice/ColorPickerAdapter.java166
-rw-r--r--android/source/src/java/org/libreoffice/DocumentPartView.java21
-rw-r--r--android/source/src/java/org/libreoffice/DocumentPartViewListAdapter.java51
-rw-r--r--android/source/src/java/org/libreoffice/FontController.java431
-rw-r--r--android/source/src/java/org/libreoffice/FormattingController.java529
-rw-r--r--android/source/src/java/org/libreoffice/InvalidationHandler.java722
-rw-r--r--android/source/src/java/org/libreoffice/LOEvent.java184
-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.java491
-rw-r--r--android/source/src/java/org/libreoffice/LOKitTileProvider.java904
-rw-r--r--android/source/src/java/org/libreoffice/LibreOfficeApplication.java33
-rw-r--r--android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java980
-rw-r--r--android/source/src/java/org/libreoffice/LocaleHelper.java58
-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.java188
-rw-r--r--android/source/src/java/org/libreoffice/SearchController.java89
-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.java185
-rw-r--r--android/source/src/java/org/libreoffice/TileProviderFactory.java31
-rw-r--r--android/source/src/java/org/libreoffice/ToolbarController.java278
-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.java54
-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.java286
-rw-r--r--android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java271
-rw-r--r--android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java549
-rw-r--r--android/source/src/java/org/libreoffice/storage/DocumentProviderFactory.java128
-rw-r--r--android/source/src/java/org/libreoffice/storage/DocumentProviderSettingsActivity.java102
-rw-r--r--android/source/src/java/org/libreoffice/storage/IDocumentProvider.java70
-rw-r--r--android/source/src/java/org/libreoffice/storage/IFile.java116
-rw-r--r--android/source/src/java/org/libreoffice/storage/IOUtils.java56
-rw-r--r--android/source/src/java/org/libreoffice/storage/external/BrowserSelectorActivity.java153
-rw-r--r--android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserActivity.java42
-rw-r--r--android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserFragment.java199
-rw-r--r--android/source/src/java/org/libreoffice/storage/external/ExternalFile.java163
-rw-r--r--android/source/src/java/org/libreoffice/storage/external/ExtsdDocumentsProvider.java175
-rw-r--r--android/source/src/java/org/libreoffice/storage/external/IExternalDocumentProvider.java22
-rw-r--r--android/source/src/java/org/libreoffice/storage/external/OTGDocumentsProvider.java90
-rw-r--r--android/source/src/java/org/libreoffice/storage/local/LocalDocumentsDirectoryProvider.java73
-rw-r--r--android/source/src/java/org/libreoffice/storage/local/LocalDocumentsProvider.java60
-rw-r--r--android/source/src/java/org/libreoffice/storage/local/LocalFile.java103
-rw-r--r--android/source/src/java/org/libreoffice/storage/owncloud/OwnCloudFile.java178
-rw-r--r--android/source/src/java/org/libreoffice/storage/owncloud/OwnCloudProvider.java192
-rw-r--r--android/source/src/java/org/libreoffice/ui/FileUtilities.java278
-rw-r--r--android/source/src/java/org/libreoffice/ui/FolderIconView.java204
-rw-r--r--android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java1224
-rw-r--r--android/source/src/java/org/libreoffice/ui/PageView.java69
-rw-r--r--android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java95
-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.java42
-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.java275
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java354
-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.java1101
-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.java520
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/LayerView.java449
-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.java51
-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.java148
-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.java256
-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
113 files changed, 20616 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 000000000..6c944bae7
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/AboutDialogFragment.java
@@ -0,0 +1,113 @@
+/*
+ *
+ * * 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 android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.view.View;
+import android.widget.TextView;
+
+import java.io.File;
+
+public class AboutDialogFragment extends DialogFragment {
+
+ private static final String DEFAULT_DOC_PATH = "/assets/example.odt";
+
+
+ @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 in the version and vendor text views.
+ TextView versionView = messageView.findViewById(R.id.about_version);
+ TextView vendorView = messageView.findViewById(R.id.about_vendor);
+ try
+ {
+ String versionName = getActivity().getPackageManager()
+ .getPackageInfo(getActivity().getPackageName(), 0).versionName;
+ String[] tokens = versionName.split("/");
+ if (tokens.length == 3)
+ {
+ String version = String.format(versionView.getText().toString().replace("\n", "<br/>"),
+ tokens[0], "<a href=\"https://hub.libreoffice.org/git-core/" + tokens[1] + "\">" + tokens[1] + "</a>");
+ @SuppressWarnings("deprecation") // since 24 with additional option parameter
+ Spanned versionString = Html.fromHtml(version);
+ versionView.setText(versionString);
+ versionView.setMovementMethod(LinkMovementMethod.getInstance());
+ String vendor = vendorView.getText().toString();
+ vendor = vendor.replace("$VENDOR", tokens[2]);
+ vendorView.setText(vendor);
+ }
+ else
+ throw new PackageManager.NameNotFoundException();
+ }
+ catch (PackageManager.NameNotFoundException e)
+ {
+ versionView.setText("");
+ vendorView.setText("");
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder .setIcon(R.drawable.lo_icon)
+ .setTitle(R.string.app_name)
+ .setView(messageView)
+ .setNegativeButton(R.string.about_license, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ loadFromAbout("/assets/license.txt");
+ dialog.dismiss();
+ }
+ })
+ .setPositiveButton(R.string.about_notice, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ loadFromAbout("/assets/notice.txt");
+ dialog.dismiss();
+ }
+ })
+ .setNeutralButton(R.string.about_moreinfo, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ loadFromAbout(DEFAULT_DOC_PATH);
+ dialog.dismiss();
+ }
+ });
+
+ return builder.create();
+ }
+
+ private void loadFromAbout(String input) {
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.fromFile(new File(input)));
+ 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 000000000..6ec6aa138
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ColorPaletteAdapter.java
@@ -0,0 +1,131 @@
+package org.libreoffice;
+
+import android.content.Context;
+import android.support.v7.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> {
+
+ int[][] color_palette;
+ Context mContext;
+ int upperSelectedBox = -1;
+ int selectedBox = 0;
+ boolean animate;
+ 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;
+ }
+
+ @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);
+ }
+ }
+
+
+} \ No newline at end of file
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 000000000..a79a19e5c
--- /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 000000000..c93d5a01b
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ColorPickerAdapter.java
@@ -0,0 +1,166 @@
+package org.libreoffice;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.support.v7.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> {
+
+ Context mContext;
+ ColorPaletteAdapter colorPaletteAdapter;
+ ColorPaletteListener colorPaletteListener;
+ int[] colorList;
+ int[][] colorPalette = new int[11][8];
+ int selectedBox = 0;
+
+ 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 (selectedBox != position)
+ holder.colorBox.setImageDrawable(null);
+ else {
+ holder.colorBox.setImageResource(R.drawable.ic_done_white_12dp);
+ }
+
+ 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) {
+ this.selectedBox = position;
+ selectSubColor(position, position==0?0:3);
+ colorPaletteListener.applyColor(colorList[position]);
+ 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[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));
+ }
+ } else {
+ 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));
+ }
+ }
+ }
+ for (int i = 0; i < 11; i++){
+ this.colorPalette[i][7] = (Color.rgb(255, 255, 255)); // last one is always white
+ }
+ colorPaletteAdapter.setColorPalette(colorPalette);
+ }
+
+ public void findSelectedTextColor(int color) {
+ /*
+ Libreoffice recognizes -1 as Black
+ */
+ if (color == -1) {
+ colorPaletteAdapter.changePosition(0, 0);
+ selectedBox = 0;
+ updateAdapter();
+ return;
+ }
+ /*
+ Find the color if the palette points another color
+ */
+ if (colorPalette[selectedBox][colorPaletteAdapter.getSelectedBox()] != color) {
+ for (int i = 0; i < 11; i++) {
+ for (int k = 0; k < 8; k++) {
+ if (colorPalette[i][k] == color) {
+ colorPaletteAdapter.changePosition(i, k);
+ selectedBox = i;
+ updateAdapter();
+ return;
+ }
+ }
+ }
+ }
+ }
+ 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);
+ }
+ }
+} \ No newline at end of file
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 000000000..f1ce71900
--- /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 000000000..a576fc67d
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/DocumentPartViewListAdapter.java
@@ -0,0 +1,51 @@
+/* -*- 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 static final String LOGTAG = DocumentPartViewListAdapter.class.getSimpleName();
+
+ 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 000000000..a00e13e14
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/FontController.java
@@ -0,0 +1,431 @@
+package org.libreoffice;
+
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.support.design.widget.BottomSheetBehavior;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+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 {
+
+ private boolean mFontNameSpinnerSet = false;
+ private boolean mFontSizeSpinnerSet = false;
+ private final LibreOfficeMainActivity mActivity;
+ private final ArrayList<String> mFontList = new ArrayList<String>();
+ private final ArrayList<String> mFontSizes = new ArrayList<String>();
+ private final HashMap<String, ArrayList<String>> mAllFontSizes = new HashMap<String, ArrayList<String>>();
+
+ 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);
+ }
+
+ @Override
+ public void updateColorPickerPosition(int color) {
+ if (null == colorPickerAdapter) return;
+ colorPickerAdapter.findSelectedTextColor(color + 0xFF000000);
+ changeFontColorBoxColor(color + 0xFF000000);
+ }
+ };
+
+ final ColorPaletteListener backColorPaletteListener = new ColorPaletteListener() {
+ @Override
+ public void applyColor(int color) {
+ sendFontBackColorChange(color);
+ }
+
+ @Override
+ public void updateColorPickerPosition(int color) {
+ if(backColorPickerAdapter != null)
+ backColorPickerAdapter.findSelectedTextColor(color + 0xFF000000);
+ changeFontBackColorBoxColor(color + 0xFF000000);
+
+ }
+ };
+
+ 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() {
+ if(color == -1){ //Libreoffice recognizes -1 as black
+ fontColorPickerButton.setBackgroundColor(Color.BLACK);
+ }else{
+ 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() {
+ if(color == -1){ //Libreoffice recognizes -1 as black
+ fontBackColorPickerButton.setBackgroundColor(Color.BLACK);
+ }else{
+ 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){
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "long");
+ valueJson.put("value", 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){
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "long");
+ valueJson.put("value", 0x00FFFFFF & color);
+ if(mActivity.isSpreadsheet()){
+ json.put("BackgroundColor", valueJson);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:BackgroundColor", json.toString()));
+ }else if(mActivity.getTileProvider().isPresentation()){
+ json.put("CharBackColor", valueJson);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:CharBackColor", json.toString()));
+ }else {
+ json.put("BackColor", valueJson);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:BackColor", 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<String>();
+ 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<String>(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<String>(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);
+
+ }
+
+ 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);
+
+ }
+
+ 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 000000000..a34c4c41e
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/FormattingController.java
@@ -0,0 +1,529 @@
+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 android.support.design.widget.Snackbar;
+import android.support.v4.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 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_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});
+ }
+
+ switch(button.getId()) {
+
+ case R.id.button_insertFormatListBullets:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DefaultBullet"));
+ break;
+
+ case R.id.button_insertFormatListNumbering:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DefaultNumbering"));
+ break;
+
+ case R.id.button_bold:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Bold"));
+ break;
+ case R.id.button_italic:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Italic"));
+ break;
+ case R.id.button_strikethrough:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Strikeout"));
+ break;
+ case R.id.button_clearformatting:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:ResetAttributes"));
+ break;
+ case R.id.button_underlined:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:UnderlineDouble"));
+ break;
+ case R.id.button_align_left:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:LeftPara"));
+ break;
+ case R.id.button_align_center:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:CenterPara"));
+ break;
+ case R.id.button_align_right:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:RightPara"));
+ break;
+ case R.id.button_align_justify:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:JustifyPara"));
+ break;
+ case R.id.button_insert_line:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Line"));
+ break;
+ case R.id.button_insert_rect:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Rect"));
+ break;
+ case R.id.button_font_shrink:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Shrink"));
+ break;
+ case R.id.button_font_grow:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Grow"));
+ break;
+ case R.id.button_subscript:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SubScript"));
+ break;
+ case R.id.button_superscript:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SuperScript"));
+ break;
+ case R.id.button_insert_picture:
+ insertPicture();
+ break;
+ case R.id.button_insert_table:
+ insertTable();
+ break;
+ case R.id.button_delete_table:
+ deleteTable();
+ break;
+ }
+ }
+
+ 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());
+ switch (v.getId()){
+ case R.id.number_picker_rows_positive:
+ if(rowCount < maxValue)
+ npRowCount.setText(String.valueOf(++rowCount));
+ break;
+ case R.id.number_picker_cols_positive:
+ if(colCount < maxValue)
+ npColCount.setText(String.valueOf(++colCount));
+ break;
+ }
+ }
+ };
+
+ 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());
+ switch (v.getId()){
+ case R.id.number_picker_rows_negative:
+ if(rowCount > minValue)
+ npRowCount.setText(String.valueOf(--rowCount));
+ break;
+ case R.id.number_picker_cols_negative:
+ if(colCount > minValue)
+ npColCount.setText(String.valueOf(--colCount));
+ break;
+ }
+ }
+ };
+
+ 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) {
+ mContext.pendingInsertGraphic = true;
+ } else if (requestCode == SELECT_PHOTO && resultCode == Activity.RESULT_OK) {
+ getFileFromURI(data.getData());
+ mContext.pendingInsertGraphic = true;
+ }
+ }
+
+ // Called by LOKitTileProvider when activity is resumed from photo/gallery/camera/cloud apps
+ void popCompressImageGradeSelection() {
+ 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);
+ mContext.pendingInsertGraphic = false;
+ }
+
+ 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 000000000..32e9b5665
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/InvalidationHandler.java
@@ -0,0 +1,722 @@
+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 String LOGTAG = InvalidationHandler.class.getSimpleName();
+ private final DocumentOverlay mDocumentOverlay;
+ private final GeckoLayerClient mLayerClient;
+ private OverlayState mState;
+ private boolean mKeyEvent = false;
+ private 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_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_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.saveFilesToCloud();
+ }
+ }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);
+ }
+ }
+
+ /**
+ * 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")) {
+ 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;
+ }
+
+ 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.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.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 000000000..4db48a5bb
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOEvent.java
@@ -0,0 +1,184 @@
+/* -*- 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 RESUME = 15;
+ 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 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(int type, String key, int value) {
+ mType = type;
+ mTypeString = "Resume partIndex";
+ mString = key;
+ mPartIndex = 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 000000000..bbef709af
--- /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 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 000000000..c69e02669
--- /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 {
+ private static final String LOGTAG = LOKitShell.class.getSimpleName();
+
+ public static float getDpi(Context context) {
+ if (((LibreOfficeMainActivity)context).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.isExperimentalMode();
+ }
+
+ // 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 sendResumeEvent(String inputFile, int partIndex) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.RESUME, inputFile, partIndex));
+ }
+
+ 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 000000000..e554f0800
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitThread.java
@@ -0,0 +1,491 @@
+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.libreoffice.ui.LibreOfficeUIActivity;
+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 LinkedBlockingQueue<LOEvent> mEventQueue = new LinkedBlockingQueue<LOEvent>();
+
+ private TileProvider mTileProvider;
+ private InvalidationHandler mInvalidationHandler;
+ private ImmutableViewportMetrics mViewportMetrics;
+ private GeckoLayerClient mLayerClient;
+ private 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() {
+ if (mLayerClient == null || mTileProvider == null) {
+ // called too early...
+ return;
+ }
+
+ mLayerClient.setPageRect(0, 0, mTileProvider.getPageWidth(), mTileProvider.getPageHeight());
+ mViewportMetrics = mLayerClient.getViewportMetrics();
+ mLayerClient.setViewportMetrics(mViewportMetrics);
+
+ 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() {
+ mLayerClient.clearAndResetlayers();
+ redraw();
+ 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();
+ }
+
+ private void updateZoomConstraints() {
+ if (mTileProvider == null) return;
+ mLayerClient = mContext.getLayerClient();
+ // Set min zoom to the page width so that you cannot zoom below page width
+ final float minZoom = mLayerClient.getViewportMetrics().getWidth()/mTileProvider.getPageWidth();
+ mLayerClient.setZoomConstraints(new ZoomConstraints(true, 1f, minZoom, 0f));
+ }
+
+
+ /**
+ * Resume the document with the current part
+ */
+
+ private void resumeDocument(String filename, int partIndex){
+
+ mLayerClient = mContext.getLayerClient();
+
+ mInvalidationHandler = new InvalidationHandler(mContext);
+ mTileProvider = TileProviderFactory.create(mContext, mInvalidationHandler, filename);
+
+ if (mTileProvider.isReady()) {
+ updateZoomConstraints();
+ changePart(partIndex);
+ } else {
+ closeDocument();
+ }
+ }
+
+ /**
+ * 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();
+ LOKitShell.hideProgressSpinner(mContext);
+ }
+
+ /**
+ * Handle load document event.
+ * @param filePath - filePath to where the document is located
+ */
+ private void loadDocument(String filePath) {
+ mLayerClient = mContext.getLayerClient();
+
+ mInvalidationHandler = new InvalidationHandler(mContext);
+ mTileProvider = TileProviderFactory.create(mContext, mInvalidationHandler, filePath);
+
+ if (mTileProvider.isReady()) {
+ LOKitShell.showProgressSpinner(mContext);
+ updateZoomConstraints();
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ // synchronize to avoid deletion while loading
+ synchronized (LOKitThread.this) {
+ refresh();
+ }
+ }
+ });
+ LOKitShell.hideProgressSpinner(mContext);
+ } else {
+ closeDocument();
+ }
+ }
+
+ /**
+ * 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) {
+ mLayerClient = mContext.getLayerClient();
+
+ mInvalidationHandler = new InvalidationHandler(mContext);
+ mTileProvider = TileProviderFactory.create(mContext, mInvalidationHandler, fileType);
+
+ if (mTileProvider.isReady()) {
+ LOKitShell.showProgressSpinner(mContext);
+ updateZoomConstraints();
+ refresh();
+ LOKitShell.hideProgressSpinner(mContext);
+
+ if (fileType.matches(LibreOfficeUIActivity.NEW_WRITER_STRING_KEY))
+ mTileProvider.saveDocumentAs(filePath, "odt");
+ else if (fileType.matches(LibreOfficeUIActivity.NEW_CALC_STRING_KEY))
+ mTileProvider.saveDocumentAs(filePath, "ods");
+ else if (fileType.matches(LibreOfficeUIActivity.NEW_IMPRESS_STRING_KEY))
+ mTileProvider.saveDocumentAs(filePath, "odp");
+ else
+ mTileProvider.saveDocumentAs(filePath, "odg");
+
+ } else {
+ closeDocument();
+ }
+ }
+
+ /**
+ * Save the currently loaded document.
+ */
+ private void saveDocumentAs(String filePath, String fileType) {
+ if (mTileProvider == null) {
+ Log.e(LOGTAG, "Error in saving, Tile Provider instance is null");
+ } else {
+ mTileProvider.saveDocumentAs(filePath, fileType);
+ }
+ }
+
+ /**
+ * Close the currently loaded document.
+ */
+ // needs to be synchronized to not destroy doc while it's loaded
+ private synchronized 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);
+ break;
+ case LOEvent.RESUME:
+ resumeDocument(event.mString, event.mPartIndex);
+ break;
+ case LOEvent.CLOSE:
+ closeDocument();
+ break;
+ case LOEvent.SIZE_CHANGED:
+ redraw();
+ 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();
+ 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") && editing) {
+ 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 000000000..0e2649337
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitTileProvider.java
@@ -0,0 +1,904 @@
+/* -*- 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.os.Environment;
+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.libreoffice.ui.FileUtilities;
+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 int TILE_SIZE = 256;
+ private final float mTileWidth;
+ private final float mTileHeight;
+ private final String mInputFile;
+ private Office mOffice;
+ private Document mDocument;
+ private boolean mIsReady = false;
+ private LibreOfficeMainActivity mContext;
+
+ private float mDPI;
+ private float mWidthTwip;
+ private float mHeightTwip;
+
+ private Document.MessageCallback mMessageCallback;
+
+ private 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;
+ File f = new File(mInputFile);
+ final String cacheFile = mContext.getExternalCacheDir().getAbsolutePath() + "/lo_cached_" + f.getName();
+
+ if(mContext.firstStart){
+ File cacheFileObj = new File(cacheFile);
+ if(cacheFileObj.exists()) {
+ cacheFileObj.delete();
+ }
+ mContext.firstStart=false;
+ }
+
+ Log.i(LOGTAG, "====> Loading file '" + input + "'");
+ File fileToBeEncoded;
+ if(isDocumentCached()){
+ fileToBeEncoded = new File(cacheFile);
+ }else{
+ 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);
+
+ if(isSpreadsheet()) {
+ mContext.setIsSpreadsheet(true); // Calc is treated differently e.g. DPI = 96f
+ }
+
+ 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);
+
+ int parts = mDocument.getParts();
+ Log.i(LOGTAG, "Document parts: " + parts);
+ mContext.getDocumentPartView().clear();
+
+ // Writer documents always have one part, so hide the navigation drawer.
+ if (mDocument.getDocumentType() != Document.DOCTYPE_TEXT) {
+ for (int i = 0; i < parts; i++) {
+ String partName = mDocument.getPartName(i);
+ if (partName.isEmpty()) {
+ partName = getGenericPartName(i);
+ }else if (partName.startsWith("Slide") || partName.startsWith("Sheet") || partName.startsWith("Part")) {
+ partName = getGenericPartName(i);
+ }
+ Log.i(LOGTAG, "Document part " + i + " name:'" + partName + "'");
+
+ mDocument.setPart(i);
+ resetDocumentSize();
+ final DocumentPartView partView = new DocumentPartView(i, partName);
+ mContext.getDocumentPartView().add(partView);
+ }
+ } else {
+ 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();
+ }
+ });
+ mContext.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mContext.pendingInsertGraphic) {
+ mContext.getFormattingController().popCompressImageGradeSelection();
+ }
+ }
+ });
+ }
+
+ 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(){
+ int parts = mDocument.getParts();
+ mContext.getDocumentPartView().clear();
+ if (mDocument.getDocumentType() != Document.DOCTYPE_TEXT) {
+ 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() == false && isPresentation() == false) {
+ //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 void saveDocumentAs(final String filePath, String format) {
+ final String newFilePath = "file://" + filePath;
+ Log.d("saveFilePathURL", newFilePath);
+ LOKitShell.showProgressSpinner(mContext);
+ mDocument.saveAs(newFilePath, format, "");
+ if (!mOffice.getError().isEmpty()){
+ 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");
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ // There was some error
+ mContext.showCustomStatusMessage(mContext.getString(R.string.unable_to_export_pdf));
+ }
+ });
+ }else {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ // There was some error
+ mContext.showSaveStatusMessage(true);
+ }
+ });
+ }
+ } else {
+ if (format.equals("svg")) {
+ // successfully created temp slideshow svg file
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.startPresentation(newFilePath);
+ }
+ });
+ }else if(format.equals("pdf")){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ // There was no error
+ mContext.showCustomStatusMessage(mContext.getString(R.string.pdf_exported_at)+filePath);
+ }
+ });
+ } else {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ // There was no error
+ mContext.showSaveStatusMessage(false);
+ }
+ });
+ }
+ }
+ LOKitShell.hideProgressSpinner(mContext);
+ }
+
+ public void exportToPDF(boolean print){
+ String dir = Environment.getExternalStorageDirectory().getAbsolutePath()+"/Documents";
+ File docDir = new File(dir);
+ if(!docDir.exists()){
+ docDir.mkdir();
+ }
+ String mInputFileName = (new File(mInputFile)).getName();
+ String file = mInputFileName.substring(0,(mInputFileName.length()-3))+"pdf";
+ if(print){
+ String cacheFile = mContext.getExternalCacheDir().getAbsolutePath()
+ + "/" + file;
+ mDocument.saveAs("file://"+cacheFile,"pdf","");
+ printDocument(cacheFile);
+ }else{
+ saveDocumentAs(dir+"/"+file,"pdf");
+ }
+ }
+
+ private void printDocument(String cacheFile) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ 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();
+ }
+ } else {
+ mContext.showCustomStatusMessage(mContext.getString(R.string.printing_not_supported));
+ }
+ }
+
+ public boolean isDocumentCached(){
+ File input = new File(mInputFile);
+ final String cacheFile = mContext.getExternalCacheDir().getAbsolutePath() + "/lo_cached_" + input.getName();
+ File cacheFileObj = new File(cacheFile);
+ if(cacheFileObj.exists())
+ return true;
+
+ return false;
+ }
+
+ public void cacheDocument() {
+ String cacheDir = mContext.getExternalCacheDir().getAbsolutePath();
+ File input = new File(mInputFile);
+ final String cacheFile = cacheDir + "/lo_cached_" + input.getName();
+ Log.i(LOGTAG, "cacheDocument: " + cacheFile);
+ if(isDocumentCached()){
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Save"));
+ }else if(mDocument != null){
+ mDocument.saveAs("file://"+cacheFile, FileUtilities.getExtension(input.getPath()).substring(1),"");
+ }else{
+ Log.w(LOGTAG, "mDocument was null when trying to save cacheDocument: " + cacheFile);
+ }
+ }
+
+ public void saveDocument(){
+ if(isDocumentCached()){
+ String format = FileUtilities.getExtension(mInputFile).substring(1);
+ String cacheDir = mContext.getExternalCacheDir().getAbsolutePath();
+ File input = new File(mInputFile);
+ final String cacheFile = cacheDir + "/lo_cached_" + input.getName();
+ String path = input.getAbsolutePath();
+ saveDocumentAs(path, format);
+ (new File(cacheFile)).delete();
+ }else{
+ 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#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 000000000..cb79219fc
--- /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.app.Application;
+import android.content.Context;
+import android.os.Handler;
+
+public class LibreOfficeApplication extends Application {
+
+ 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 000000000..cbc628e94
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java
@@ -0,0 +1,980 @@
+package org.libreoffice;
+
+import android.app.Activity;
+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.AssetFileDescriptor;
+import android.content.res.AssetManager;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.design.widget.BottomSheetBehavior;
+import android.support.design.widget.Snackbar;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.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.storage.DocumentProviderFactory;
+import org.libreoffice.storage.IFile;
+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.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URI;
+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;
+
+/**
+ * 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";
+ private static final String DEFAULT_DOC_PATH = "/assets/example.odt";
+ private 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";
+
+ //TODO "public static" is a temporary workaround
+ public static LOKitThread loKitThread;
+
+ private GeckoLayerClient mLayerClient;
+
+ private static boolean mIsExperimentalMode;
+ private static boolean mIsDeveloperMode;
+
+ private int providerId;
+ private URI documentUri;
+
+ private DrawerLayout mDrawerLayout;
+ Toolbar toolbarTop;
+
+ private ListView mDrawerList;
+ private List<DocumentPartView> mDocumentPartView = new ArrayList<DocumentPartView>();
+ private DocumentPartViewListAdapter mDocumentPartViewListAdapter;
+ private int partIndex=-1;
+ private File mInputFile;
+ private DocumentOverlay mDocumentOverlay;
+ private File mTempFile = null;
+ private File mTempSlideShowFile = null;
+ private String newDocumentType = null;
+ public boolean firstStart = true;
+
+ 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 boolean mIsSpreadsheet;
+ private LOKitTileProvider mTileProvider;
+ private String mPassword;
+ private boolean mPasswordProtected;
+ public boolean pendingInsertGraphic; // boolean indicating a pending insert graphic action, used in LOKitTileProvider.postLoad()
+
+ public GeckoLayerClient getLayerClient() {
+ return mLayerClient;
+ }
+
+ public static boolean isExperimentalMode() {
+ return mIsExperimentalMode;
+ }
+
+ public static boolean isDeveloperMode() {
+ return mIsDeveloperMode;
+ }
+
+ public boolean usesTemporaryFile() {
+ return mTempFile != null;
+ }
+
+ private boolean isKeyboardOpen = false;
+ private boolean isFormattingToolbarOpen = false;
+ private boolean isSearchToolbarOpen = false;
+ private static boolean isDocumentChanged = false;
+ private boolean isUNOCommandsToolbarOpen = false;
+ public boolean isNewDocument = false;
+ private long lastModified = 0;
+
+ @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(keyEvent.getKeyCode() != KeyEvent.KEYCODE_BACK){
+ isDocumentChanged=true;
+ }
+ return false;
+ }
+ });
+
+ // create TextCursorLayer
+ mDocumentOverlay = new DocumentOverlay(this, layerView);
+
+ // New document type string is not null, meaning we want to open a new document
+ if (getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY) != null) {
+ String newDocumentType = getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY);
+ String newFilePath = getIntent().getStringExtra(LibreOfficeUIActivity.NEW_FILE_PATH_KEY);
+
+ // Load the new document
+ loadNewDocument(newFilePath, newDocumentType);
+ }
+
+ if (getIntent().getData() != null) {
+ if (getIntent().getData().getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
+ if (copyFileToTemp() && mTempFile != null) {
+ mInputFile = mTempFile;
+ Log.d(LOGTAG, "SCHEME_CONTENT: getPath(): " + getIntent().getData().getPath());
+ toolbarTop.setTitle(mInputFile.getName());
+ } else {
+ // TODO: can't open the file
+ Log.e(LOGTAG, "couldn't create temporary file from " + getIntent().getData());
+ }
+ } else if (getIntent().getData().getScheme().equals(ContentResolver.SCHEME_FILE)) {
+ mInputFile = new File(getIntent().getData().getPath());
+ Log.d(LOGTAG, "SCHEME_FILE: getPath(): " + getIntent().getData().getPath());
+ toolbarTop.setTitle(mInputFile.getName());
+ // Gather data to rebuild IFile object later
+ providerId = getIntent().getIntExtra(
+ "org.libreoffice.document_provider_id", 0);
+ documentUri = (URI) getIntent().getSerializableExtra(
+ "org.libreoffice.document_uri");
+ }
+ } else {
+ if (!isNewDocument) {
+ mInputFile = new File(DEFAULT_DOC_PATH);
+ }
+ }
+
+ 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());
+ }
+
+ lastModified = mInputFile.lastModified();
+
+ 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
+ private void loadNewDocument(String newFilePath, String newDocumentType) {
+ mInputFile = new File(newFilePath);
+ LOKitShell.sendNewDocumentLoadEvent(newFilePath, newDocumentType);
+ isNewDocument = true;
+ toolbarTop.setTitle(mInputFile.getName());
+ }
+
+ public RectF getCurrentCursorPosition() {
+ return mDocumentOverlay.getCurrentCursorPosition();
+ }
+
+ private boolean copyFileToTemp() {
+ ContentResolver contentResolver = getContentResolver();
+ FileChannel inputChannel = null;
+ FileChannel outputChannel = null;
+ // 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 {
+ try {
+ AssetFileDescriptor assetFD = contentResolver.openAssetFileDescriptor(getIntent().getData(), "r");
+ if (assetFD == null) {
+ Log.e(LOGTAG, "couldn't create assetfiledescriptor from " + getIntent().getDataString());
+ return false;
+ }
+ inputChannel = assetFD.createInputStream().getChannel();
+ mTempFile = File.createTempFile("LibreOffice", suffix, this.getCacheDir());
+
+ outputChannel = new FileOutputStream(mTempFile).getChannel();
+ long bytesTransferred = 0;
+ // might not copy all at once, so make sure everything gets copied...
+ while (bytesTransferred < inputChannel.size()) {
+ bytesTransferred += outputChannel.transferFrom(inputChannel, bytesTransferred, inputChannel.size());
+ }
+ Log.e(LOGTAG, "Success copying " + bytesTransferred + " bytes");
+ return true;
+ } finally {
+ if (inputChannel != null) inputChannel.close();
+ if (outputChannel != null) outputChannel.close();
+ }
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Save a new document
+ * */
+ public void saveAs(){
+ LOKitShell.sendSaveAsEvent(mInputFile.getPath(), FileUtilities.getExtension(mInputFile.getPath()).substring(1));
+ }
+
+ /**
+ * Save the document and invoke save on document provider to upload the file
+ * to the cloud if necessary.
+ */
+ public void saveDocument() {
+ if (!mInputFile.exists()) {
+ // Needed for handling null in case new document is not created.
+ mInputFile = new File(DEFAULT_DOC_PATH);
+ lastModified = mInputFile.lastModified();
+ }
+ Toast.makeText(this, R.string.message_saving, Toast.LENGTH_SHORT).show();
+ // local save
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Save", true));
+ }
+
+ public void saveFilesToCloud(){
+ final Activity activity = LibreOfficeMainActivity.this;
+ final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ // rebuild the IFile object from the data passed in the Intent
+ IFile mStorageFile = DocumentProviderFactory.getInstance()
+ .getProvider(providerId).createFromUri(LibreOfficeMainActivity.this, documentUri);
+ // call document provider save operation
+ mStorageFile.saveDocument(mInputFile);
+ }
+ catch (final RuntimeException e) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity, e.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ Log.e(LOGTAG, e.getMessage(), e.getCause());
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void param) {
+ Toast.makeText(activity, R.string.message_saved,
+ Toast.LENGTH_SHORT).show();
+ setDocumentChanged(false);
+ }
+ };
+
+ if (lastModified < mInputFile.lastModified()) {
+ task.execute();
+ lastModified = mInputFile.lastModified();
+ } else {
+ Toast.makeText(activity, R.string.message_save_incomplete, Toast.LENGTH_LONG).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 (!isNewDocument){
+ if (partIndex == -1)
+ LOKitShell.sendLoadEvent(mInputFile.getPath());
+ else
+ LOKitShell.sendResumeEvent(mInputFile.getPath(), partIndex);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ Log.i(LOGTAG, "onStop..");
+ //save document to cache
+ if (mTileProvider != null)
+ mTileProvider.cacheDocument();
+ hideSoftKeyboardDirect();
+ LOKitShell.sendCloseEvent();
+ super.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(LOGTAG, "onDestroy..");
+ 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:
+ if (isNewDocument) {
+ saveAs();
+ } else {
+ 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://"+mInputFile.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 void setIsSpreadsheet(boolean b) {
+ mIsSpreadsheet = b;
+ }
+
+ public boolean isSpreadsheet() {
+ return mIsSpreadsheet;
+ }
+
+ 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);
+ partIndex = partView.partIndex;
+ 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;
+ }
+ }
+
+ // This method is used in LOKitTileProvider.java to show status of new file creation.
+ public void showSaveStatusMessage(boolean error) {
+ if (!error)
+ Snackbar.make(mDrawerLayout, getString(R.string.create_new_file_success) + mInputFile.getName(), Snackbar.LENGTH_LONG).show();
+ else
+ Snackbar.make(mDrawerLayout, getString(R.string.create_new_file_error) + mInputFile.getName(), Snackbar.LENGTH_LONG).show(); }
+
+ public void showCustomStatusMessage(String message){
+ Snackbar.make(mDrawerLayout, message, Snackbar.LENGTH_LONG).show();
+ }
+
+ public void preparePresentation() {
+ if (getExternalCacheDir() != null) {
+ String tempPath = getExternalCacheDir().getPath() + "/" + mInputFile.getName() + ".svg";
+ mTempSlideShowFile = new File(tempPath);
+ if (mTempSlideShowFile.exists() && !isDocumentChanged) {
+ startPresentation("file://" + tempPath);
+ } else {
+ LOKitShell.sendSaveAsEvent(tempPath, "svg");
+ }
+ }
+ }
+
+ public void startPresentation(String tempPath) {
+ // pre-KitKat android doesn't have chrome-based WebView, which is needed to show svg slideshow
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ Intent intent = new Intent(this, PresentationActivity.class);
+ intent.setData(Uri.parse(tempPath));
+ startActivity(intent);
+ } else {
+ // copy the svg file path to clipboard for the user to paste in a browser
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("temp svg file path", tempPath);
+ clipboard.setPrimaryClip(clip);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.alert_copy_svg_slide_show_to_clipboard)
+ .setPositiveButton(R.string.alert_copy_svg_slide_show_to_clipboard_dismiss, null).show();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mFormattingController.handleActivityResult(requestCode, resultCode, data);
+ hideBottomToolbar();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ }
+
+}
+
+/* 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 000000000..8c0e9b3fb
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LocaleHelper.java
@@ -0,0 +1,58 @@
+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;
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ 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 000000000..2ce167ce3
--- /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 000000000..112e35c4b
--- /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 android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.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 000000000..3308e6884
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/PresentationActivity.java
@@ -0,0 +1,188 @@
+package org.libreoffice;
+
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.view.GestureDetectorCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+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);
+
+ // First we hide the status bar
+ if (Build.VERSION.SDK_INT < 16) {
+ // If the Android version is lower than Jellybean, use this call to hide
+ // the status bar.
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ } else {
+ // If higher than Jellybean
+ 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));
+ }
+} \ No newline at end of file
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 000000000..a9414f7f7
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/SearchController.java
@@ -0,0 +1,89 @@
+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 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;
+ switch(button.getId()) {
+ case R.id.button_search_down:
+ direction = SearchDirection.DOWN;
+ break;
+ case R.id.button_search_up:
+ direction = SearchDirection.UP;
+ break;
+ default:
+ break;
+ }
+
+ 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 000000000..5623abc2e
--- /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 000000000..1b5a909e1
--- /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 000000000..52870b67a
--- /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 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 000000000..9f6fc5605
--- /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 000000000..dabf30b83
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/TileProvider.java
@@ -0,0 +1,185 @@
+/* -*- 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.
+ */
+ void saveDocumentAs(String filePath, String format);
+
+ /**
+ * 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 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 000000000..3219ce2b4
--- /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 000000000..d21396cf4
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ToolbarController.java
@@ -0,0 +1,278 @@
+/* -*- 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 android.support.v7.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);
+ }
+
+ public void disableMenuItem(final int menuItemId, final boolean disabled) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ MenuItem menuItem = mMainMenu.findItem(menuItemId);
+ if (menuItem != null) {
+ menuItem.setEnabled(!disabled);
+ } 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;
+ // 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);
+ setEditModeOn(true);
+ }
+ });
+ }
+
+ /**
+ * 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.drawable.lo_icon);
+ 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) {
+ switch (item.getItemId()) {
+ case R.id.action_keyboard:
+ mContext.showSoftKeyboard();
+ break;
+ case R.id.action_format:
+ mContext.showFormattingToolbar();
+ break;
+ case R.id.action_about:
+ mContext.showAbout();
+ return true;
+ case R.id.action_save:
+ if (mContext.isNewDocument) {
+ mContext.saveAs();
+ } else {
+ mContext.getTileProvider().saveDocument();
+ }
+ return true;
+ case R.id.action_parts:
+ mContext.openDrawer();
+ return true;
+ case R.id.action_exportToPDF:
+ mContext.getTileProvider().exportToPDF(false);
+ return true;
+ case R.id.action_print:
+ mContext.getTileProvider().exportToPDF(true);
+ return true;
+ case R.id.action_settings:
+ mContext.showSettings();
+ return true;
+ case R.id.action_search:
+ mContext.showSearchToolbar();
+ return true;
+ case R.id.action_undo:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Undo"));
+ return true;
+ case R.id.action_redo:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Redo"));
+ return true;
+ case R.id.action_presentation:
+ mContext.preparePresentation();
+ return true;
+ case R.id.action_add_slide:
+ mContext.addPart();
+ return true;
+ case R.id.action_add_worksheet:
+ mContext.addPart();
+ return true;
+ case R.id.action_rename_worksheet:
+ case R.id.action_rename_slide:
+ mContext.renamePart();
+ return true;
+ case R.id.action_delete_worksheet:
+ mContext.deletePart();
+ return true;
+ case R.id.action_delete_slide:
+ mContext.deletePart();
+ return true;
+ case R.id.action_back:
+ hideClipboardActions();
+ return true;
+ case 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;
+ case 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());
+ case 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;
+ case R.id.action_UNO_commands:
+ mContext.showUNOCommandsToolbar();
+ return true;
+ }
+ return false;
+ }
+
+ void setupToolbars() {
+ if (mContext.usesTemporaryFile()) {
+ disableMenuItem(R.id.action_save, true);
+ Toast.makeText(mContext, mContext.getString(R.string.temp_file_saving_disabled), Toast.LENGTH_LONG).show();
+ }
+ 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 000000000..9453b3bd0
--- /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 android.support.v7.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 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 = (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();
+ }
+ }
+} \ No newline at end of file
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 000000000..f668021b0
--- /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 000000000..a6f8cb17c
--- /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 000000000..e46173db5
--- /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 android.support.v4.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 000000000..c1f8e74e7
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/CalcHeaderCell.java
@@ -0,0 +1,54 @@
+package org.libreoffice.canvas;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.text.TextPaint;
+
+public class CalcHeaderCell extends CommonCanvasElement {
+ private TextPaint mTextPaint = new TextPaint();
+ private Paint mBgPaint = new Paint();
+ private RectF mBounds;
+ private 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);
+ if (selected) {
+ // if the cell is selected, display filled
+ mBgPaint.setStyle(Style.FILL_AND_STROKE);
+ } else {
+ // if not, display only the frame
+ mBgPaint.setStyle(Style.STROKE);
+ }
+ mBgPaint.setColor(Color.GRAY);
+ mBgPaint.setAlpha(100); // hard coded for now
+ mTextPaint.setColor(Color.GRAY);
+ mTextPaint.setTextSize(24f); // hard coded for now
+ mText = text;
+ }
+
+ /**
+ * 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.drawText(mText, mBounds.left, mBounds.bottom, mTextPaint);
+ }
+} \ No newline at end of file
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 000000000..af31d708d
--- /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 000000000..51e8801f6
--- /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 000000000..26789e8d8
--- /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 000000000..6b40ae4ba
--- /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 000000000..1cd30edb7
--- /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 000000000..8d773b2ea
--- /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 000000000..68b445af6
--- /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 000000000..ecda9b77c
--- /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 000000000..62de88ea5
--- /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 000000000..ddd16fe5e
--- /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 000000000..b85b80fc9
--- /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 000000000..76bdf9110
--- /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 000000000..ad28826f6
--- /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 000000000..40c9ddcd8
--- /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 android.support.design.widget.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 000000000..a8b2d2048
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java
@@ -0,0 +1,286 @@
+package org.libreoffice.overlay;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.support.v4.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;
+ if (mCellCursorRect != null && bottom > mCellCursorRect.top - origin.y && top < mCellCursorRect.bottom - origin.y) {
+ // if cell is within current selected portion
+ new CalcHeaderCell(0f, top, getWidth(), bottom - top, mLabels.get(i), true).onDraw(canvas);
+ } else {
+ new CalcHeaderCell(0f, top, getWidth(), bottom - top, mLabels.get(i), false).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) {
+ if (mCellCursorRect != null && right > mCellCursorRect.left - origin.x && left < mCellCursorRect.right - origin.x) {
+ // if cell is within current selected portion
+ new CalcHeaderCell(left, 0f, right - left, getHeight(), mLabels.get(i), true).onDraw(canvas);
+ } else {
+ new CalcHeaderCell(left, 0f, right - left, getHeight(), mLabels.get(i), false).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 000000000..f977866a2
--- /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 000000000..b425fbb59
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java
@@ -0,0 +1,549 @@
+/* -*- 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);
+ }
+
+ 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/storage/DocumentProviderFactory.java b/android/source/src/java/org/libreoffice/storage/DocumentProviderFactory.java
new file mode 100644
index 000000000..acf5aebcd
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/DocumentProviderFactory.java
@@ -0,0 +1,128 @@
+/* -*- 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.storage;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.libreoffice.storage.external.ExtsdDocumentsProvider;
+import org.libreoffice.storage.external.OTGDocumentsProvider;
+import org.libreoffice.storage.local.LocalDocumentsDirectoryProvider;
+import org.libreoffice.storage.local.LocalDocumentsProvider;
+import org.libreoffice.storage.owncloud.OwnCloudProvider;
+
+import android.content.Context;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+
+/**
+ * Keeps the instances of the available IDocumentProviders in the system.
+ * Instances are maintained in a sorted list and providers have to be
+ * accessed from their position.
+ *
+ * The factory follows the Singleton pattern, there is only one instance of it
+ * in the application and it must be retrieved with
+ * DocumentProviderFactory.getInstance().
+ */
+public final class DocumentProviderFactory {
+ public static int EXTSD_PROVIDER_INDEX = 2;
+ public static int OTG_PROVIDER_INDEX = 3;
+
+ /**
+ * Private factory instance for the Singleton pattern.
+ */
+ private static DocumentProviderFactory instance = null;
+
+ private IDocumentProvider[] providers;
+
+ private String[] providerNames;
+
+ private DocumentProviderFactory() {
+ // private to prevent external instances of the factory
+ }
+
+ /**
+ * Initializes the factory with some context. If this method is called for
+ * twice or more times those calls will have no effect.
+ *
+ * @param context
+ * Application context for the factory.
+ */
+ public static void initialize(Context context) {
+ if (instance == null) {
+ // initialize instance
+ instance = new DocumentProviderFactory();
+
+ // initialize document providers list
+ instance.providers = new IDocumentProvider[5];
+ instance.providers[0] = new LocalDocumentsDirectoryProvider(0);
+ instance.providers[1] = new LocalDocumentsProvider(1);
+ instance.providers[OTG_PROVIDER_INDEX] = new OTGDocumentsProvider(OTG_PROVIDER_INDEX, context);
+ instance.providers[4] = new OwnCloudProvider(4, context);
+
+ instance.providers[EXTSD_PROVIDER_INDEX] = new ExtsdDocumentsProvider(EXTSD_PROVIDER_INDEX, context);
+
+ // initialize document provider names list
+ instance.providerNames = new String[instance.providers.length];
+ for (int i = 0; i < instance.providers.length; i++) {
+ instance.providerNames[i] = context.getString(instance
+ .getProvider(i).getNameResource());
+ }
+ }
+ }
+
+ /**
+ * Retrieve the unique instance of the factory.
+ *
+ * @return the unique factory object or null if it is not yet initialized.
+ */
+ public static DocumentProviderFactory getInstance() {
+ return instance;
+ }
+
+ /**
+ * Retrieve the provider associated to a certain id.
+ *
+ * @param id
+ * @return document provider with that id.
+ */
+ public IDocumentProvider getProvider(int id) {
+ // as for now, id == position in providers array
+ return providers[id];
+ }
+
+ /**
+ * Returns a sorted list of the names of the providers. Order is meaningful
+ * to retrieve the actual provider object with getProvider().
+ *
+ * @return Array with the names of the available providers.
+ */
+ public String[] getNames() {
+ return providerNames;
+ }
+
+ /**
+ * Returns the default provider.
+ *
+ * @return default provider.
+ */
+ public IDocumentProvider getDefaultProvider() {
+ return providers[0];
+ }
+
+ public Set<OnSharedPreferenceChangeListener> getChangeListeners() {
+ Set<OnSharedPreferenceChangeListener> listeners =
+ new HashSet<OnSharedPreferenceChangeListener>();
+ for (IDocumentProvider provider : providers) {
+ if (provider instanceof OnSharedPreferenceChangeListener)
+ listeners.add((OnSharedPreferenceChangeListener) provider);
+ }
+ return listeners;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/DocumentProviderSettingsActivity.java b/android/source/src/java/org/libreoffice/storage/DocumentProviderSettingsActivity.java
new file mode 100644
index 000000000..b842e79fa
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/DocumentProviderSettingsActivity.java
@@ -0,0 +1,102 @@
+/* -*- 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.storage;
+
+import java.util.Set;
+
+import org.libreoffice.R;
+import org.libreoffice.storage.external.BrowserSelectorActivity;
+
+import android.content.Intent;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.support.v7.app.AppCompatActivity;
+
+public class DocumentProviderSettingsActivity extends AppCompatActivity {
+
+ public static final String KEY_PREF_OWNCLOUD_SERVER = "pref_server_url";
+ public static final String KEY_PREF_OWNCLOUD_USER_NAME = "pref_user_name";
+ public static final String KEY_PREF_OWNCLOUD_PASSWORD = "pref_password";
+ public static final String KEY_PREF_EXTERNAL_SD_PATH_URI = "pref_extsd_path_uri";
+ public static final String KEY_PREF_OTG_PATH_URI = "pref_otg_path_uri";
+
+ private Set<OnSharedPreferenceChangeListener> listeners;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Display the fragment as the main content.
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new SettingsFragment()).commit();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ listeners = DocumentProviderFactory.getInstance().getChangeListeners();
+ for (OnSharedPreferenceChangeListener listener : listeners) {
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .registerOnSharedPreferenceChangeListener(listener);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ for (OnSharedPreferenceChangeListener listener : listeners) {
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .unregisterOnSharedPreferenceChangeListener(listener);
+ }
+ }
+
+ public static class SettingsFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.documentprovider_preferences);
+
+ PreferenceScreen extSDPreference =
+ (PreferenceScreen)findPreference(KEY_PREF_EXTERNAL_SD_PATH_URI);
+ PreferenceScreen otgPreference =
+ (PreferenceScreen)findPreference(KEY_PREF_OTG_PATH_URI);
+
+ extSDPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ startBrowserSelectorActivity(KEY_PREF_EXTERNAL_SD_PATH_URI,
+ BrowserSelectorActivity.MODE_EXT_SD);
+ return true;
+ }
+ });
+ otgPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ startBrowserSelectorActivity(KEY_PREF_OTG_PATH_URI,
+ BrowserSelectorActivity.MODE_OTG);
+ return true;
+ }
+ });
+
+ }
+
+ private void startBrowserSelectorActivity(String prefKey, String mode) {
+ Intent i = new Intent(getActivity(), BrowserSelectorActivity.class);
+ i.putExtra(BrowserSelectorActivity.PREFERENCE_KEY_EXTRA, prefKey);
+ i.putExtra(BrowserSelectorActivity.MODE_EXTRA, mode);
+ startActivity(i);
+ }
+
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/IDocumentProvider.java b/android/source/src/java/org/libreoffice/storage/IDocumentProvider.java
new file mode 100644
index 000000000..044d7ddb4
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/IDocumentProvider.java
@@ -0,0 +1,70 @@
+/* -*- 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.storage;
+
+import android.content.Context;
+
+import java.net.URI;
+
+/**
+ * Represents a Document Provider, an object able to provide documents from a
+ * certain source (e.g. local documents, DropBox, Google Docs).
+ */
+public interface IDocumentProvider {
+
+ /**
+ * Provides the content root element for the Document Provider.
+ *
+ * @return Content root element.
+ * @throws RuntimeException in case of error.
+ * @param context
+ */
+ IFile getRootDirectory(Context context);
+
+ /**
+ * Transforms some URI into the IFile object that represents that content.
+ *
+ *
+ * @param context
+ * @param uri
+ * URI pointing to some content object that has been previously
+ * retrieved with IFile.getUri().
+ * @return IFile object pointing to the content represented by uri.
+ * @throws RuntimeException in case of error.
+ */
+ IFile createFromUri(Context context, URI uri);
+
+ /**
+ * Get internationalized name for this provider. This name is intended to be
+ * shown in the UI.
+ *
+ * @return string resource pointing to the provider name.
+ */
+ int getNameResource();
+
+ /**
+ * Provides the unique ID for a document provider instance in a program.
+ *
+ * This ID should be set when the instance is built. It could be used to
+ * tell two instances of the same document provider apart, e. g. two
+ * instances of OwnCloudProvider pointing to different servers.
+ *
+ * @return Unique ID for a document provider instance.
+ */
+ int getId();
+
+ /**
+ * Checks if the Document Provider is available or not.
+ *
+ * @return A boolean value based on provider availability.
+ * @param context
+ */
+ boolean checkProviderAvailability(Context context);
+}
diff --git a/android/source/src/java/org/libreoffice/storage/IFile.java b/android/source/src/java/org/libreoffice/storage/IFile.java
new file mode 100644
index 000000000..c9cfa7f11
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/IFile.java
@@ -0,0 +1,116 @@
+/* -*- 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.storage;
+
+import android.content.Context;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.net.URI;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * An abstraction of the File class, intended to be implemented by different
+ * Document Providers.
+ *
+ * It represents a file or a directory in the context of a certain Document
+ * Provider. It wraps the file-related operations and provides access to the
+ * final document as a local File, downloading it if necessary.
+ */
+public interface IFile {
+
+ /**
+ * Provides a URI that represents this IFile object.
+ *
+ * @return URI that represents this IFile object in the context of the
+ * Document Provider that created it. The URI can be transformed
+ * back into an IFile object with IDocumentProvider.createFromUri().
+ */
+ URI getUri();
+
+ /**
+ * Returns the name of the file or directory represented by this file.
+ *
+ * @return This file's name.
+ */
+ String getName();
+
+ /**
+ * Indicates if this file represents a directory in the context of the
+ * Document Provider which originated it.
+ *
+ * @return true if this file is a directory, false otherwise.
+ */
+ boolean isDirectory();
+
+ /**
+ * Returns the file size in bytes.
+ *
+ * @return file size in bytes, 0 if the file does not exist.
+ */
+ long getSize();
+
+ /**
+ * Returns the time when this file was last modified, measured in
+ * milliseconds since January 1st, 1970, midnight.
+ *
+ * @return time when this file was last modified, or 0 if the file does not
+ * exist.
+ */
+ Date getLastModified();
+
+ /**
+ * Returns a list containing the files in the directory represented by this
+ * file.
+ *
+ * @return list of files contained by this directory, or an empty list if
+ * this is not a directory.
+ * @throws RuntimeException in case of error.
+ */
+ List<IFile> listFiles();
+
+ /**
+ * Gets the list of files contained in the directory represented by this
+ * file, and filters it through some FilenameFilter.
+ *
+ * @param filter
+ * the filter to match names against.
+ * @return filtered list of files contained by this directory, or an empty
+ * list if this is not a directory.
+ * @throws RuntimeException in case of error.
+ */
+ List<IFile> listFiles(FileFilter filter);
+
+ /**
+ * Returns the pparent of this file.
+ *
+ * @return this file's parent or null if it does not have it.
+ * @param context
+ */
+ IFile getParent(Context context);
+
+ /**
+ * Returns the document wrapped by this IFile as a local file. The result
+ * for a directory is not defined.
+ *
+ * @return local file containing the document wrapped by this object.
+ * @throws RuntimeException in case of error.
+ */
+ File getDocument();
+
+ /**
+ * Replaces the wrapped document with a new version of it.
+ *
+ * @param file
+ * A local file pointing to the new version of the document.
+ */
+ void saveDocument(File file);
+}
diff --git a/android/source/src/java/org/libreoffice/storage/IOUtils.java b/android/source/src/java/org/libreoffice/storage/IOUtils.java
new file mode 100644
index 000000000..f345f5cbe
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/IOUtils.java
@@ -0,0 +1,56 @@
+package org.libreoffice.storage;
+
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * File IO related methods.
+ */
+public class IOUtils {
+ private static final int BUFFER_SIZE = 1024 * 8;
+ private static final String LOGTAG = IOUtils.class.getSimpleName();
+
+ public static File getFileFromURIString(String URIpath) throws IllegalArgumentException{
+ try{
+ return new File(new URI(URIpath));
+ } catch (URISyntaxException e) {
+ //should not happen as all URIs are system generated
+ Log.wtf(LOGTAG, e.getReason());
+ return null;
+ }
+ }
+
+ public static boolean isInvalidFile(File f) {
+ return f == null || !f.exists() || f.getTotalSpace() == 0
+ || !f.canRead() || !f.canWrite();
+ }
+
+ public static int copy(InputStream input, OutputStream output) throws Exception {
+ byte[] buffer = new byte[BUFFER_SIZE];
+
+ BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE);
+ BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE);
+
+ int count = 0, n = 0;
+ try {
+ while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
+ out.write(buffer, 0, n);
+ count += n;
+ }
+ out.flush();
+ } finally {
+ if (out != null) out.close();
+ if (in != null) in.close();
+ }
+
+ return count;
+ }
+
+}
diff --git a/android/source/src/java/org/libreoffice/storage/external/BrowserSelectorActivity.java b/android/source/src/java/org/libreoffice/storage/external/BrowserSelectorActivity.java
new file mode 100644
index 000000000..07b64623b
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/external/BrowserSelectorActivity.java
@@ -0,0 +1,153 @@
+package org.libreoffice.storage.external;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.UriPermission;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+
+import org.libreoffice.R;
+import org.libreoffice.storage.DocumentProviderFactory;
+
+import java.util.Set;
+
+/**
+ * Activity to select which directory browser to use.
+ * Android 5+ will use the DocumentTree intent to locate a browser.
+ * Android 4+ & OTG will use the internal directory browser.
+ */
+public class BrowserSelectorActivity extends AppCompatActivity {
+ public static final String PREFERENCE_KEY_EXTRA = "org.libreoffice.pref_key_extra";
+ public static final String MODE_EXTRA = "org.libreoffice.mode_extra";
+ public static final String MODE_OTG = "OTG";
+ public static final String MODE_EXT_SD = "EXT_SD";
+
+ private static final String LOGTAG = BrowserSelectorActivity.class.getSimpleName();
+ private static final int REQUEST_DOCUMENT_TREE = 1;
+ private static final int REQUEST_INTERNAL_BROWSER = 2;
+ private Set<SharedPreferences.OnSharedPreferenceChangeListener> listeners;
+ private String preferenceKey;
+ private SharedPreferences preferences;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ preferenceKey = getIntent().getStringExtra(PREFERENCE_KEY_EXTRA);
+ preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ String mode = getIntent().getStringExtra(MODE_EXTRA);
+
+ if(mode.equals(MODE_EXT_SD)) {
+ findSDCard();
+ } else if (mode.equals(MODE_OTG)) {
+ findOTGDevice();
+ }
+ }
+
+ private void findOTGDevice() {
+ useInternalBrowser(DocumentProviderFactory.OTG_PROVIDER_INDEX);
+ }
+
+ private void findSDCard() {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ useDocumentTreeBrowser();
+ } else {
+ useInternalBrowser(DocumentProviderFactory.EXTSD_PROVIDER_INDEX);
+ }
+ }
+
+ private void useInternalBrowser(int providerIndex) {
+ IExternalDocumentProvider provider =
+ (IExternalDocumentProvider) DocumentProviderFactory.getInstance()
+ .getProvider(providerIndex);
+ String previousDirectoryPath = preferences.getString(preferenceKey, provider.guessRootURI(this));
+ Intent i = new Intent(this, DirectoryBrowserActivity.class);
+ i.putExtra(DirectoryBrowserActivity.DIRECTORY_PATH_EXTRA, previousDirectoryPath);
+ startActivityForResult(i, REQUEST_INTERNAL_BROWSER);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void useDocumentTreeBrowser() {
+ Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ startActivityForResult(i, REQUEST_DOCUMENT_TREE);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ //listeners are registered here as onActivityResult is called before onResume
+ super.onActivityResult(requestCode, resultCode, data);
+
+ registerListeners();
+ if(resultCode == RESULT_OK) {
+ switch(requestCode) {
+ case REQUEST_DOCUMENT_TREE:
+ Uri treeUri = data.getData();
+ preferences.edit()
+ .putString(preferenceKey, treeUri.toString())
+ .apply();
+
+ updatePersistedUriPermission(treeUri);
+ getContentResolver().takePersistableUriPermission(treeUri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION |
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ break;
+
+ case REQUEST_INTERNAL_BROWSER:
+ Uri fileUri = data.getData();
+ preferences.edit()
+ .putString(preferenceKey, fileUri.toString())
+ .apply();
+ break;
+ default:
+ }
+ }
+ unregisterListeners();
+ Log.d(LOGTAG, "Preference saved: " +
+ preferences.getString(preferenceKey, getString(R.string.directory_not_saved)));
+ finish();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void updatePersistedUriPermission(Uri treeUri) {
+ freePreviousUriPermissions();
+
+ //TODO: Use non-emulator Android 5+ device to check if needed
+ /*this.grantUriPermission(this.getPackageName(),
+ treeUri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION |
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION); */
+
+ getContentResolver().takePersistableUriPermission(treeUri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION |
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void freePreviousUriPermissions() {
+ ContentResolver cr = getContentResolver();
+ for (UriPermission uriPermission : cr.getPersistedUriPermissions()) {
+ cr.releasePersistableUriPermission(uriPermission.getUri(), 0);
+ }
+ }
+
+ private void registerListeners() {
+ listeners = DocumentProviderFactory.getInstance().getChangeListeners();
+ for (SharedPreferences.OnSharedPreferenceChangeListener listener : listeners) {
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .registerOnSharedPreferenceChangeListener(listener);
+ }
+ }
+
+ private void unregisterListeners() {
+ for (SharedPreferences.OnSharedPreferenceChangeListener listener : listeners) {
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .unregisterOnSharedPreferenceChangeListener(listener);
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserActivity.java b/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserActivity.java
new file mode 100644
index 000000000..1cf9f52fa
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserActivity.java
@@ -0,0 +1,42 @@
+package org.libreoffice.storage.external;
+
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+
+import org.libreoffice.R;
+
+/**
+ * Container for DirectoryBrowserFragment
+ */
+public class DirectoryBrowserActivity extends AppCompatActivity {
+ public static final String DIRECTORY_PATH_EXTRA = "org.libreoffice.directory_path_extra";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent data = getIntent();
+ String initialPath = data.getStringExtra(DIRECTORY_PATH_EXTRA);
+
+ setContentView(R.layout.activity_directory_browser);
+ FragmentManager fm = getFragmentManager();
+ Fragment fragment = DirectoryBrowserFragment.newInstance(initialPath);
+ fm.beginTransaction()
+ .add(R.id.fragment_container, fragment)
+ .commit();
+ }
+
+ @Override
+ public void onBackPressed() {
+ FragmentManager fm = getFragmentManager();
+ if(fm.getBackStackEntryCount() > 0) {
+ fm.popBackStack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserFragment.java b/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserFragment.java
new file mode 100644
index 000000000..18165650a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserFragment.java
@@ -0,0 +1,199 @@
+package org.libreoffice.storage.external;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.libreoffice.R;
+import org.libreoffice.storage.IOUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * A simple directory browser.
+ */
+public class DirectoryBrowserFragment extends Fragment {
+ private static final String LOGTAG = DirectoryBrowserFragment.class.getSimpleName();
+ private static final String INITIAL_PATH_URI_KEY = "initial_path";
+ private File currentDirectory;
+ private FileArrayAdapter directoryAdapter;
+
+ public static DirectoryBrowserFragment newInstance(String initialPathURI) {
+ Bundle args = new Bundle();
+ args.putString(INITIAL_PATH_URI_KEY, initialPathURI);
+ DirectoryBrowserFragment fragment = new DirectoryBrowserFragment();
+ fragment.setArguments(args);
+ Log.d(LOGTAG, "Saved path: " + initialPathURI);
+
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ String initialPathURI = getArguments().getString(INITIAL_PATH_URI_KEY);
+ setupCurrentDirectory(initialPathURI);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_directory_browser, container, false);
+
+ final EditText directoryHeader = v.findViewById(R.id.directory_header);
+ Button directorySearchButton = v.findViewById(R.id.directory_search_button);
+ Button positiveButton = v.findViewById(R.id.confirm_button);
+ Button negativeButton = v.findViewById(R.id.cancel_button);
+ ImageView upImage = v.findViewById(R.id.up_image);
+ ListView directoryListView = v.findViewById(R.id.directory_list);
+
+ directoryHeader.setText(currentDirectory.getPath());
+ directorySearchButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String currentPath = currentDirectory.getAbsolutePath();
+ String enteredPath = directoryHeader.getText().toString();
+ File testDirectory = new File(enteredPath);
+ if(enteredPath.equals(currentPath)) ;
+ else if (isInvalidFileDirectory(testDirectory)) {
+ Toast.makeText(getActivity(), R.string.bad_directory, Toast.LENGTH_SHORT)
+ .show();
+ }
+ else {
+ changeDirectory(testDirectory);
+ }
+ }
+ });
+
+ positiveButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent data = new Intent();
+ data.setData(Uri.fromFile(currentDirectory));
+ getActivity().setResult(Activity.RESULT_OK, data);
+ getActivity().finish();
+ }
+ });
+
+ negativeButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getActivity().setResult(Activity.RESULT_CANCELED, null);
+ getActivity().finish();
+ }
+ });
+
+ upImage.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ changeDirectory(currentDirectory.getParentFile());
+ }
+ });
+
+ directoryAdapter = new FileArrayAdapter(getActivity(), new ArrayList<File>());
+ directoryAdapter.populateFileList(currentDirectory);
+ directoryListView.setAdapter(directoryAdapter);
+ directoryListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ changeDirectory(directoryAdapter.getItem(position));
+ }
+ });
+
+ return v;
+ }
+
+ private void changeDirectory(File destination) {
+ if(destination == null) {
+ Toast.makeText(getActivity(), R.string.unable_to_go_further, Toast.LENGTH_SHORT)
+ .show();
+ } else {
+ Fragment fragment = DirectoryBrowserFragment.newInstance(destination.toURI().toString());
+ getActivity().getFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, fragment)
+ .addToBackStack(null)
+ .commit();
+ }
+ }
+
+ private void setupCurrentDirectory(String initialPathURI) {
+ File initialDirectory = null;
+ if(initialPathURI != null && !initialPathURI.isEmpty()) {
+ initialDirectory = IOUtils.getFileFromURIString(initialPathURI);
+ }
+
+ if(isInvalidFileDirectory(initialDirectory)) {
+ initialDirectory = Environment.getExternalStorageDirectory();
+ }
+ currentDirectory = initialDirectory;
+ }
+
+ private boolean isInvalidFileDirectory(File f) {
+ return f == null || !f.exists() || !f.isDirectory() ||!f.canRead();
+ }
+
+ private class FileArrayAdapter extends ArrayAdapter<File> {
+ private Comparator<File> caseInsensitiveNaturalOrderComparator;
+
+ public FileArrayAdapter(Context context, ArrayList<File> files) {
+ super(context, 0, files);
+ caseInsensitiveNaturalOrderComparator = new AlphabeticalFileComparator();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = getActivity().getLayoutInflater()
+ .inflate(android.R.layout.simple_list_item_1, null);
+ }
+
+ File f = this.getItem(position);
+ TextView tv = convertView.findViewById(android.R.id.text1);
+ tv.setText(f.getName());
+
+ return convertView;
+ }
+
+ public void sortAlphabetically() {
+ this.sort(caseInsensitiveNaturalOrderComparator);
+ }
+
+ public void populateFileList(File directory) {
+ for(File f : directory.listFiles()){
+ if(f.isDirectory()){
+ directoryAdapter.add(f);
+ }
+ }
+ directoryAdapter.sortAlphabetically();
+ }
+ }
+
+ private class AlphabeticalFileComparator implements Comparator<File> {
+ @Override
+ public int compare(File lhs, File rhs) {
+ String lhsName = lhs.getName();
+ String rhsName = rhs.getName();
+
+ return lhsName.compareToIgnoreCase(rhsName);
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/external/ExternalFile.java b/android/source/src/java/org/libreoffice/storage/external/ExternalFile.java
new file mode 100644
index 000000000..aff33e441
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/external/ExternalFile.java
@@ -0,0 +1,163 @@
+package org.libreoffice.storage.external;
+
+import android.content.Context;
+import android.support.v4.provider.DocumentFile;
+import android.util.Log;
+
+import org.libreoffice.storage.IFile;
+import org.libreoffice.storage.IOUtils;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Implementation of IFile for the external file system, for Android 4.4+
+ *
+ * Uses the DocumentFile class.
+ *
+ * The DocumentFile class obfuscates the path of the files it wraps,
+ * preventing usage of LOK's documentLoad method. A copy of the DocumentFile's contents
+ * will be created in the cache when files are opened, allowing use of documentLoad.
+ */
+public class ExternalFile implements IFile{
+ private final static String LOGTAG = "ExternalFile";
+
+ private ExtsdDocumentsProvider provider;
+ private DocumentFile docFile;
+ private File duplicateFile;
+ private Context context;
+
+ public ExternalFile(ExtsdDocumentsProvider provider, DocumentFile docFile, Context context) {
+ this.provider = provider;
+ this.context = context;
+ this.docFile = docFile;
+ }
+
+ @Override
+ public URI getUri() {
+ try{
+ return new URI(docFile.toString());
+ } catch (URISyntaxException e) {
+ Log.e(LOGTAG, e.getMessage(), e.getCause());
+ return null;
+ }
+ }
+
+ @Override
+ public String getName() {
+ return docFile.getName();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return docFile.isDirectory();
+ }
+
+ @Override
+ public long getSize() {
+ return docFile.length();
+ }
+
+ @Override
+ public Date getLastModified() {
+ return new Date(docFile.lastModified());
+ }
+
+ @Override
+ public List<IFile> listFiles() {
+ List<IFile> children = new ArrayList<IFile>();
+ for (DocumentFile child : docFile.listFiles()) {
+ children.add(new ExternalFile(provider, child, context));
+ }
+ return children;
+ }
+
+ @Override
+ public List<IFile> listFiles(FileFilter filter) {
+ File file;
+ try{
+ List<IFile> children = new ArrayList<IFile>();
+ for (DocumentFile child : docFile.listFiles()) {
+ file = new File(new URI(child.getUri().toString()));
+ if(filter.accept(file))
+ children.add(new ExternalFile(provider, child, context));
+ }
+ return children;
+
+ }catch (Exception e){
+ e.printStackTrace();
+ }
+ /* if something goes wrong */
+ return listFiles();
+
+ }
+
+ @Override
+ public IFile getParent(Context context) {
+ // this is the root node
+ if(docFile.getParentFile() == null) return null;
+
+ return new ExternalFile(provider, docFile.getParentFile(), this.context);
+ }
+
+ @Override
+ public File getDocument() {
+ if(isDirectory()) {
+ return null;
+ } else {
+ duplicateFile = duplicateInCache();
+ return duplicateFile;
+ }
+ }
+
+ private File duplicateInCache() {
+ try{
+ InputStream istream = context.getContentResolver().
+ openInputStream(docFile.getUri());
+
+ File storageFolder = provider.getCacheDir();
+ File fileCopy = new File(storageFolder, docFile.getName());
+ OutputStream ostream = new FileOutputStream(fileCopy);
+
+ IOUtils.copy(istream, ostream);
+ return fileCopy;
+ } catch (Exception e) {
+ Log.e(LOGTAG, e.getMessage(), e.getCause());
+ return null;
+ }
+ }
+
+ @Override
+ public void saveDocument(File file) {
+ try{
+ OutputStream ostream = context.getContentResolver().
+ openOutputStream(docFile.getUri());
+ InputStream istream = new FileInputStream(file);
+
+ IOUtils.copy(istream, ostream);
+
+ } catch (Exception e) {
+ Log.e(LOGTAG, e.getMessage(), e.getCause());
+ }
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object)
+ return true;
+ if (!(object instanceof ExternalFile))
+ return false;
+ ExternalFile file = (ExternalFile) object;
+ return file.getUri().equals(getUri());
+ }
+
+}
diff --git a/android/source/src/java/org/libreoffice/storage/external/ExtsdDocumentsProvider.java b/android/source/src/java/org/libreoffice/storage/external/ExtsdDocumentsProvider.java
new file mode 100644
index 000000000..e45929374
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/external/ExtsdDocumentsProvider.java
@@ -0,0 +1,175 @@
+package org.libreoffice.storage.external;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.provider.DocumentFile;
+import android.util.Log;
+
+import org.libreoffice.R;
+import org.libreoffice.storage.DocumentProviderSettingsActivity;
+import org.libreoffice.storage.IFile;
+
+import java.io.File;
+import java.net.URI;
+
+/**
+ * Implementation of IDocumentProvider for the external file system, for android 4.4+
+ *
+ * The DocumentFile class is required when accessing files in external storage
+ * for Android 4.4+. The ExternalFile class is used to handle this.
+ *
+ * Android 4.4 & 5+ use different types of root directory paths,
+ * 5 using a DirectoryTree Uri and 4.4 using a normal File path.
+ * As such, different methods are required to obtain the rootDirectory IFile.
+ * 4.4 has to guess the location of the rootDirectory as well.
+ */
+public class ExtsdDocumentsProvider implements IExternalDocumentProvider,
+ OnSharedPreferenceChangeListener{
+ private static final String LOGTAG = ExtsdDocumentsProvider.class.getSimpleName();
+
+ private int id;
+ private File cacheDir;
+ private String rootPathURI;
+
+ public ExtsdDocumentsProvider(int id, Context context) {
+ this.id = id;
+ setupRootPathUri(context);
+ setupCache(context);
+ }
+
+ private void setupRootPathUri(Context context) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ rootPathURI = preferences.getString(
+ DocumentProviderSettingsActivity.KEY_PREF_EXTERNAL_SD_PATH_URI, guessRootURI(context));
+ }
+
+ public String guessRootURI(Context context) {
+ // TODO: unfortunately the getExternalFilesDirs function relies on devices to actually
+ // follow guidelines re external storage. Of course device manufacturers don't and as such
+ // you cannot rely on it returning the actual paths (neither the compat, nor the native variant)
+ File[] possibleRemovables = ContextCompat.getExternalFilesDirs(context,null);
+ // the primary dir that is already covered by the "LocalDocumentsProvider"
+ // might be emulated/part of internal memory or actual SD card
+ // TODO: change to not confuse android's "external storage" with "expandable storage"
+ String primaryExternal = Environment.getExternalStorageDirectory().getAbsolutePath();
+
+ for (File option: possibleRemovables) {
+ // Returned paths may be null if a storage device is unavailable.
+ if (null == option) {
+ Log.w(LOGTAG,"path was a null option :-/"); continue; }
+ String optionPath = option.getAbsolutePath();
+ if(optionPath.contains(primaryExternal)) {
+ Log.v(LOGTAG, "did get file path - but is same as primary storage ("+ primaryExternal +")");
+ continue;
+ }
+
+ return option.toURI().toString();
+ }
+
+ // TODO: do some manual probing of possible directories (/storage/sdcard1 and similar)
+ Log.i(LOGTAG, "no secondary storage reported");
+ return null;
+ }
+
+ private void setupCache(Context context) {
+ // TODO: probably we should do smarter cache management
+ cacheDir = new File(context.getExternalCacheDir(), "externalFiles");
+ if (cacheDir.exists()) {
+ deleteRecursive(cacheDir);
+ }
+ cacheDir.mkdirs();
+ }
+
+ private static void deleteRecursive(File file) {
+ if (file.isDirectory()) {
+ for (File child : file.listFiles())
+ deleteRecursive(child);
+ }
+ file.delete();
+ }
+
+ public File getCacheDir() {
+ return cacheDir;
+ }
+
+ @Override
+ public IFile getRootDirectory(Context context) {
+ if(android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
+ return android4RootDirectory(context);
+ } else {
+ return android5RootDirectory(context);
+ }
+ }
+
+ private ExternalFile android4RootDirectory(Context context) {
+ try{
+ File f = new File(new URI(rootPathURI));
+ return new ExternalFile(this, DocumentFile.fromFile(f), context);
+ } catch (Exception e) {
+ //invalid rootPathURI
+ throw buildRuntimeExceptionForInvalidFileURI(context);
+ }
+ }
+
+ private ExternalFile android5RootDirectory(Context context) {
+ try {
+ return new ExternalFile(this,
+ DocumentFile.fromTreeUri(context, Uri.parse(rootPathURI)),
+ context);
+ } catch (Exception e) {
+ //invalid rootPathURI
+ throw buildRuntimeExceptionForInvalidFileURI(context);
+ }
+ }
+
+ private RuntimeException buildRuntimeExceptionForInvalidFileURI(Context context) {
+ // ToDo: discarding the original exception / catch-all handling is bad style
+ return new RuntimeException(context.getString(R.string.ext_document_provider_error));
+ }
+
+ @Override
+ public IFile createFromUri(Context context, URI javaURI) {
+ //TODO: refactor when new DocumentFile API exist
+ //uri must be of a DocumentFile file, not directory.
+ Uri androidUri = Uri.parse(javaURI.toString());
+ return new ExternalFile(this,
+ DocumentFile.fromSingleUri(context, androidUri),
+ context);
+ }
+
+ @Override
+ public int getNameResource() {
+ return R.string.external_sd_file_system;
+ }
+
+ @Override
+ public int getId() {
+ return id;
+ }
+
+ @Override
+ public boolean checkProviderAvailability(Context context) {
+ // too many devices (or I am just unlucky) don't report the mounted state properly, and other
+ // devices also consider dedicated part of internal storage to be "mounted" so cannot use
+ // getExternalStorageState().equals(Environment.MEDIA_MOUNTED) && isExternalStorageRemovable()
+ // but they refer to the primary external storage anyway, so what currently is covered by the
+ // "LocalDocumentsProvider"
+ return rootPathURI!=null && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
+ if (key.equals(DocumentProviderSettingsActivity.KEY_PREF_EXTERNAL_SD_PATH_URI)) {
+ rootPathURI = preferences.getString(key, "");
+ }
+ }
+
+}
diff --git a/android/source/src/java/org/libreoffice/storage/external/IExternalDocumentProvider.java b/android/source/src/java/org/libreoffice/storage/external/IExternalDocumentProvider.java
new file mode 100644
index 000000000..a439417b6
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/external/IExternalDocumentProvider.java
@@ -0,0 +1,22 @@
+package org.libreoffice.storage.external;
+
+import android.content.Context;
+
+import org.libreoffice.storage.IDocumentProvider;
+
+
+/**
+ * Interface for external document providers.
+ */
+public interface IExternalDocumentProvider extends IDocumentProvider {
+
+ /**
+ * Used to obtain the default directory to display when
+ * browsing using the internal DirectoryBrowser.
+ *
+ * @return a guess of the root file's URI.
+ * @param context
+ */
+ String guessRootURI(Context context);
+
+}
diff --git a/android/source/src/java/org/libreoffice/storage/external/OTGDocumentsProvider.java b/android/source/src/java/org/libreoffice/storage/external/OTGDocumentsProvider.java
new file mode 100644
index 000000000..4341bc354
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/external/OTGDocumentsProvider.java
@@ -0,0 +1,90 @@
+package org.libreoffice.storage.external;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import org.libreoffice.R;
+import org.libreoffice.storage.DocumentProviderSettingsActivity;
+import org.libreoffice.storage.IFile;
+import org.libreoffice.storage.IOUtils;
+import org.libreoffice.storage.local.LocalFile;
+
+import java.io.File;
+import java.net.URI;
+
+/**
+ * TODO: OTG currently uses LocalFile. Change to an IFile that handles abrupt OTG unmounting
+ */
+public class OTGDocumentsProvider implements IExternalDocumentProvider,
+ SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private static final String LOGTAG = OTGDocumentsProvider.class.getSimpleName();
+
+ private String rootPathURI;
+ private int id;
+
+ public OTGDocumentsProvider(int id, Context context) {
+ this.id = id;
+ setupRootPath(context);
+ }
+
+ private void setupRootPath(Context context) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ rootPathURI = preferences.getString(
+ DocumentProviderSettingsActivity.KEY_PREF_OTG_PATH_URI, "");
+ }
+
+ @Override
+ public IFile createFromUri(Context context, URI uri) {
+ return new LocalFile(uri);
+ }
+
+ @Override
+ public int getNameResource() {
+ return R.string.otg_file_system;
+ }
+
+ @Override
+ public int getId() {
+ return id;
+ }
+
+ @Override
+ public IFile getRootDirectory(Context context) {
+ // TODO: handle this with more fine-grained exceptions
+ if(rootPathURI.equals("")) {
+ Log.e(LOGTAG, "rootPathURI is empty");
+ throw new RuntimeException(context.getString(R.string.ext_document_provider_error));
+ }
+
+ File f = IOUtils.getFileFromURIString(rootPathURI);
+ if(IOUtils.isInvalidFile(f)) {
+ Log.e(LOGTAG, "rootPathURI is invalid - missing device?");
+ throw new RuntimeException(context.getString(R.string.otg_missing_error));
+ }
+
+ return new LocalFile(f);
+ }
+
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.equals(DocumentProviderSettingsActivity.KEY_PREF_OTG_PATH_URI)) {
+ rootPathURI = sharedPreferences.getString(key, "");
+ }
+ }
+
+ @Override
+ public String guessRootURI(Context context) {
+ return "";
+ }
+
+ @Override
+ public boolean checkProviderAvailability(Context context) {
+ // check if system supports USB Host
+ return rootPathURI.length()>0 && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_HOST);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/local/LocalDocumentsDirectoryProvider.java b/android/source/src/java/org/libreoffice/storage/local/LocalDocumentsDirectoryProvider.java
new file mode 100644
index 000000000..15522e93a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/local/LocalDocumentsDirectoryProvider.java
@@ -0,0 +1,73 @@
+/* -*- 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.storage.local;
+
+import java.io.File;
+
+import org.libreoffice.storage.IFile;
+import org.libreoffice.R;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Environment;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+
+/**
+ * A convenience IDocumentProvider to browse the /sdcard/Documents directory.
+ *
+ * Extends LocalDocumentsProvider to overwrite getRootDirectory and set it to
+ * /sdcard/Documents. Most documents will probably be stored there so there is
+ * no need for the user to browse the filesystem from the root every time.
+ */
+public class LocalDocumentsDirectoryProvider extends LocalDocumentsProvider {
+
+ public LocalDocumentsDirectoryProvider(int id) {
+ super(id);
+ }
+
+ private static File getDocumentsDir() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ // DIRECTORY_DOCUMENTS is 19 or later only
+ return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
+ } else {
+ return new File(Environment.getExternalStorageDirectory() + "/Documents");
+ }
+ }
+
+ @Override
+ public IFile getRootDirectory(Context context) {
+ File documentsDirectory = getDocumentsDir();
+ if (!documentsDirectory.exists()) {
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+ if(!documentsDirectory.mkdirs()) {
+ // fallback to the toplevel dir - might be due to the dir not mounted/used as USB-Mass-Storage or similar
+ // TODO: handle unavailability of the storage/failure of the mkdir properly
+ Log.e("LocalDocumentsProvider", "not sure how we ended up here - if we have read permissions to use it in the first place, we also should have the write-permissions..");
+ documentsDirectory = Environment.getExternalStorageDirectory();
+ }
+ }
+ }
+ return new LocalFile(documentsDirectory);
+ }
+
+ @Override
+ public int getNameResource() {
+ return R.string.local_documents;
+ }
+
+ @Override
+ public boolean checkProviderAvailability(Context context) {
+ File documentsDirectory = getDocumentsDir();
+ return documentsDirectory.exists() && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/local/LocalDocumentsProvider.java b/android/source/src/java/org/libreoffice/storage/local/LocalDocumentsProvider.java
new file mode 100644
index 000000000..1a10fad42
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/local/LocalDocumentsProvider.java
@@ -0,0 +1,60 @@
+/* -*- 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.storage.local;
+
+import java.net.URI;
+
+import org.libreoffice.storage.IDocumentProvider;
+import org.libreoffice.storage.IFile;
+
+import org.libreoffice.R;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.support.v4.content.ContextCompat;
+
+/**
+ * Implementation of IDocumentProvider for the local file system.
+ */
+public class LocalDocumentsProvider implements IDocumentProvider {
+
+ private int id;
+
+ public LocalDocumentsProvider(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public IFile getRootDirectory(Context context) {
+ return new LocalFile(Environment.getExternalStorageDirectory());
+ }
+
+ @Override
+ public IFile createFromUri(Context context, URI uri) {
+ return new LocalFile(uri);
+ }
+
+ @Override
+ public int getNameResource() {
+ return R.string.local_file_system;
+ }
+
+ @Override
+ public int getId() {
+ return id;
+ }
+
+ @Override
+ public boolean checkProviderAvailability(Context context) {
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/local/LocalFile.java b/android/source/src/java/org/libreoffice/storage/local/LocalFile.java
new file mode 100644
index 000000000..4ff5bbf11
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/local/LocalFile.java
@@ -0,0 +1,103 @@
+/* -*- 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.storage.local;
+
+import android.content.Context;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.libreoffice.storage.IFile;
+
+/**
+ * Implementation of IFile for the local file system.
+ */
+public class LocalFile implements IFile {
+
+ private File file;
+
+ public LocalFile(File file) {
+ this.file = file;
+ }
+
+ public LocalFile(URI uri) {
+ this.file = new File(uri);
+ }
+
+ public URI getUri() {
+ return file.toURI();
+ }
+
+ public String getName() {
+ return file.getName();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return file.isDirectory();
+ }
+
+ @Override
+ public long getSize() {
+ return file.length();
+ }
+
+ @Override
+ public Date getLastModified() {
+ return new Date(file.lastModified());
+ }
+
+ @Override
+ public List<IFile> listFiles() {
+ List<IFile> children = new ArrayList<IFile>();
+ for (File child : file.listFiles()) {
+ children.add(new LocalFile(child));
+ }
+ return children;
+ }
+
+ @Override
+ public List<IFile> listFiles(FileFilter filter) {
+ List<IFile> children = new ArrayList<IFile>();
+ for (File child : file.listFiles(filter)) {
+ children.add(new LocalFile(child));
+ }
+ return children;
+ }
+
+ @Override
+ public IFile getParent(Context context) {
+ return new LocalFile(file.getParentFile());
+ }
+
+ @Override
+ public File getDocument() {
+ return file;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object)
+ return true;
+ if (!(object instanceof LocalFile))
+ return false;
+ LocalFile file = (LocalFile) object;
+ return file.getUri().equals(getUri());
+ }
+
+ @Override
+ public void saveDocument(File file) {
+ // do nothing; file is local
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/owncloud/OwnCloudFile.java b/android/source/src/java/org/libreoffice/storage/owncloud/OwnCloudFile.java
new file mode 100644
index 000000000..fa74a54b0
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/owncloud/OwnCloudFile.java
@@ -0,0 +1,178 @@
+package org.libreoffice.storage.owncloud;
+
+import android.content.Context;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.libreoffice.storage.IFile;
+
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.resources.files.ChunkedUploadRemoteFileOperation;
+import com.owncloud.android.lib.resources.files.DownloadRemoteFileOperation;
+import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
+import com.owncloud.android.lib.resources.files.RemoteFile;
+import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation;
+
+/**
+ * Implementation of IFile for ownCloud servers.
+ */
+public class OwnCloudFile implements IFile {
+
+ private OwnCloudProvider provider;
+ private RemoteFile file;
+
+ private String name;
+ private String parentPath;
+
+ protected OwnCloudFile(OwnCloudProvider provider, RemoteFile file) {
+ this.provider = provider;
+ this.file = file;
+
+ // get name and parent from path
+ File localFile = new File(file.getRemotePath());
+ this.name = localFile.getName();
+ this.parentPath = localFile.getParent();
+ }
+
+ @Override
+ public URI getUri(){
+
+ try{
+ return URI.create(URLEncoder.encode(file.getRemotePath(),"UTF-8"));
+ }catch(UnsupportedEncodingException e){
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return file.getMimeType().equals("DIR");
+ }
+
+ @Override
+ public long getSize() {
+ return file.getLength();
+ }
+
+ @Override
+ public Date getLastModified() {
+ return new Date(file.getModifiedTimestamp());
+ }
+
+ @Override
+ public List<IFile> listFiles() {
+ List<IFile> children = new ArrayList<IFile>();
+ if (isDirectory()) {
+ ReadRemoteFolderOperation refreshOperation = new ReadRemoteFolderOperation(
+ file.getRemotePath());
+ RemoteOperationResult result = refreshOperation.execute(provider
+ .getClient());
+ if (!result.isSuccess()) {
+ throw provider.buildRuntimeExceptionForResultCode(result.getCode());
+ }
+ for (Object obj : result.getData()) {
+ RemoteFile child = (RemoteFile) obj;
+ if (!child.getRemotePath().equals(file.getRemotePath()))
+ children.add(new OwnCloudFile(provider, child));
+ }
+ }
+ return children;
+ }
+
+ @Override
+ public List<IFile> listFiles(FileFilter filter) {
+ List<IFile> children = new ArrayList<IFile>();
+ if (isDirectory()) {
+ ReadRemoteFolderOperation refreshOperation = new ReadRemoteFolderOperation(
+ file.getRemotePath());
+ RemoteOperationResult result = refreshOperation.execute(provider
+ .getClient());
+ if (!result.isSuccess()) {
+ throw provider.buildRuntimeExceptionForResultCode(result.getCode());
+ }
+
+ for (Object obj : result.getData()) {
+ RemoteFile child = (RemoteFile) obj;
+ if (!child.getRemotePath().equals(file.getRemotePath())){
+ OwnCloudFile ownCloudFile = new OwnCloudFile(provider, child);
+ if(!ownCloudFile.isDirectory()){
+ File f = new File(provider.getCacheDir().getAbsolutePath(),
+ ownCloudFile.getName());
+ if(filter.accept(f))
+ children.add(ownCloudFile);
+ f.delete();
+ }else{
+ children.add(ownCloudFile);
+ }
+ }
+ }
+ }
+ return children;
+ }
+
+ @Override
+ public IFile getParent(Context context) {
+ if (parentPath == null)
+ // this is the root node
+ return null;
+
+ return provider.createFromUri(context, URI.create(parentPath));
+ }
+
+ @Override
+ public File getDocument() {
+ if (isDirectory()) {
+ return null;
+ }
+ File downFolder = provider.getCacheDir();
+ DownloadRemoteFileOperation operation = new DownloadRemoteFileOperation(
+ file.getRemotePath(), downFolder.getAbsolutePath());
+ RemoteOperationResult result = operation.execute(provider.getClient());
+ if (!result.isSuccess()) {
+ throw provider.buildRuntimeExceptionForResultCode(result.getCode());
+ }
+ return new File(downFolder.getAbsolutePath() + file.getRemotePath());
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object)
+ return true;
+ if (!(object instanceof OwnCloudFile))
+ return false;
+ OwnCloudFile file = (OwnCloudFile) object;
+ return file.getUri().equals(getUri());
+ }
+
+ @Override
+ public void saveDocument(File newFile) {
+ UploadRemoteFileOperation uploadOperation;
+ if (newFile.length() > ChunkedUploadRemoteFileOperation.CHUNK_SIZE) {
+ uploadOperation = new ChunkedUploadRemoteFileOperation(
+ newFile.getPath(), file.getRemotePath(), file.getMimeType());
+ } else {
+ uploadOperation = new UploadRemoteFileOperation(newFile.getPath(),
+ file.getRemotePath(), file.getMimeType());
+ }
+
+ RemoteOperationResult result = uploadOperation.execute(provider
+ .getClient());
+ if (!result.isSuccess()) {
+ throw provider.buildRuntimeExceptionForResultCode(result.getCode());
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/storage/owncloud/OwnCloudProvider.java b/android/source/src/java/org/libreoffice/storage/owncloud/OwnCloudProvider.java
new file mode 100644
index 000000000..0852ab617
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/storage/owncloud/OwnCloudProvider.java
@@ -0,0 +1,192 @@
+package org.libreoffice.storage.owncloud;
+
+import java.io.File;
+import java.net.URI;
+
+import org.libreoffice.R;
+import org.libreoffice.storage.DocumentProviderSettingsActivity;
+import org.libreoffice.storage.IDocumentProvider;
+import org.libreoffice.storage.IFile;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientFactory;
+import com.owncloud.android.lib.common.OwnCloudCredentialsFactory;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
+import com.owncloud.android.lib.resources.files.FileUtils;
+import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
+import com.owncloud.android.lib.resources.files.RemoteFile;
+
+
+/**
+ * Implementation of IDocumentProvider for ownCloud servers.
+ */
+public class OwnCloudProvider implements IDocumentProvider,
+ OnSharedPreferenceChangeListener {
+
+ private int id;
+
+ private Context context;
+ private OwnCloudClient client;
+ private File cacheDir;
+
+ private String serverUrl;
+ private String userName;
+ private String password;
+ private RemoteOperationResult result;
+
+ public OwnCloudProvider(int id, Context context) {
+ this.id = id;
+ this.context = context;
+
+ // read preferences
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ serverUrl = preferences.getString(
+ DocumentProviderSettingsActivity.KEY_PREF_OWNCLOUD_SERVER, "");
+ userName = preferences.getString(
+ DocumentProviderSettingsActivity.KEY_PREF_OWNCLOUD_USER_NAME, "");
+ password = preferences.getString(
+ DocumentProviderSettingsActivity.KEY_PREF_OWNCLOUD_PASSWORD, "");
+
+ setupClient();
+
+ // make sure cache directory exists, and clear it
+ // TODO: probably we should do smarter cache management
+ cacheDir = new File(context.getCacheDir(), "ownCloud");
+ if (cacheDir.exists()) {
+ deleteRecursive(cacheDir);
+ }
+ cacheDir.mkdirs();
+ }
+
+ private void setupClient() {
+ Uri serverUri = Uri.parse(serverUrl);
+ client = OwnCloudClientFactory.createOwnCloudClient(serverUri, context,
+ true);
+ client.setCredentials(OwnCloudCredentialsFactory.newBasicCredentials(
+ userName, password));
+ }
+
+ @Override
+ public IFile getRootDirectory(Context context) {
+ return createFromUri(context, URI.create(FileUtils.PATH_SEPARATOR));
+ }
+
+ @Override
+ public IFile createFromUri(Context context, URI uri) {
+ if(serverUrl != "" || userName != "" || password != ""){
+ ReadRemoteFileOperation refreshOperation = new ReadRemoteFileOperation(
+ uri.getPath());
+ this.result = refreshOperation.execute(client);
+ if (!result.isSuccess()) {
+ throw buildRuntimeExceptionForResultCode(result.getCode());
+ }
+ if (result.getData().size() > 0) {
+ return new OwnCloudFile(this, (RemoteFile) result.getData().get(0));
+ }
+ } else {
+ throw buildRuntimeExceptionForResultCode(ResultCode.WRONG_CONNECTION);
+ }
+
+ return null;
+ }
+
+ @Override
+ public int getNameResource() {
+ return R.string.owncloud;
+ }
+
+ /**
+ * Used by OwnCloudFiles to get a configured client to run their own
+ * operations.
+ *
+ * @return configured OwnCloudClient.
+ */
+ protected OwnCloudClient getClient() {
+ return client;
+ }
+
+ /**
+ * Used by OwnCloudFiles to get the cache directory they should download
+ * files to.
+ *
+ * @return cache directory.
+ */
+ protected File getCacheDir() {
+ return cacheDir;
+ }
+
+ /**
+ * Build the proper RuntimeException for some error result.
+ *
+ * @param code Result code got from some RemoteOperationResult.
+ * @return exception with the proper internationalized error message.
+ */
+ protected RuntimeException buildRuntimeExceptionForResultCode(ResultCode code) {
+ int errorMessage;
+ switch (code) {
+ case WRONG_CONNECTION: // SocketException
+ case FILE_NOT_FOUND: // HTTP 404
+ errorMessage = R.string.owncloud_wrong_connection;
+ break;
+ case UNAUTHORIZED: // wrong user/pass
+ errorMessage = R.string.owncloud_unauthorized;
+ break;
+ default:
+ errorMessage = R.string.owncloud_unspecified_error;
+ break;
+ }
+ return new RuntimeException(context.getString(errorMessage));
+ }
+
+ /**
+ * Deletes files and recursively deletes directories.
+ *
+ * @param file
+ * File or directory to be deleted.
+ */
+ private static void deleteRecursive(File file) {
+ if (file.isDirectory()) {
+ for (File child : file.listFiles())
+ deleteRecursive(child);
+ }
+ file.delete();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences preferences,
+ String key) {
+ boolean changed = false;
+ if (key.equals(DocumentProviderSettingsActivity.KEY_PREF_OWNCLOUD_SERVER)) {
+ serverUrl = preferences.getString(key, "");
+ changed = true;
+ }
+ else if (key.equals(DocumentProviderSettingsActivity.KEY_PREF_OWNCLOUD_USER_NAME)) {
+ userName = preferences.getString(key, "");
+ changed = true;
+ }
+ else if (key.equals(DocumentProviderSettingsActivity.KEY_PREF_OWNCLOUD_PASSWORD)) {
+ password = preferences.getString(key, "");
+ changed = true;
+ }
+
+ if (changed)
+ setupClient();
+ }
+
+ @Override
+ public int getId() {
+ return id;
+ }
+
+ @Override
+ public boolean checkProviderAvailability(Context context) {
+ return client != null;
+ }
+}
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 000000000..7a5848600
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/FileUtilities.java
@@ -0,0 +1,278 @@
+/* -*- 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 org.libreoffice.storage.IFile;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FilenameFilter;
+import java.text.Collator;
+import java.util.Map;
+import java.util.Collections;
+import java.util.List;
+import java.util.HashMap;
+import java.util.Comparator;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+public class FileUtilities {
+
+ private static String LOGTAG = FileUtilities.class.getSimpleName();
+
+ static final int ALL = -1;
+
+ // 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;
+
+ static final int SORT_AZ = 0;
+ static final int SORT_ZA = 1;
+ /** Oldest Files First*/
+ static final int SORT_OLDEST = 2;
+ /** Newest Files First*/
+ static final int SORT_NEWEST = 3;
+ /** Largest Files First */
+ static final int SORT_LARGEST = 4;
+ /** Smallest Files First */
+ static final int SORT_SMALLEST = 5;
+
+ public static final String DEFAULT_WRITER_EXTENSION = ".odt";
+ public static final String DEFAULT_IMPRESS_EXTENSION = ".odp";
+ public static final String DEFAULT_SPREADSHEET_EXTENSION = ".ods";
+ public static final String DEFAULT_DRAWING_EXTENSION = ".odg";
+
+ private static final Map<String, Integer> mExtnMap = new HashMap<String, Integer>();
+ private static final Map<String, String> extensionToMimeTypeMap = new HashMap<String, String>();
+ static {
+ // Please keep this in sync with AndroidManifest.xml
+
+ // 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);
+
+ // Some basic MIME types
+ // Android's MimeTypeMap lacks some types that we need
+ extensionToMimeTypeMap.put("odb", "application/vnd.oasis.opendocument.database");
+ extensionToMimeTypeMap.put("odf", "application/vnd.oasis.opendocument.formula");
+ extensionToMimeTypeMap.put("odg", "application/vnd.oasis.opendocument.graphics");
+ extensionToMimeTypeMap.put("otg", "application/vnd.oasis.opendocument.graphics-template");
+ extensionToMimeTypeMap.put("odi", "application/vnd.oasis.opendocument.image");
+ extensionToMimeTypeMap.put("odp", "application/vnd.oasis.opendocument.presentation");
+ extensionToMimeTypeMap.put("otp", "application/vnd.oasis.opendocument.presentation-template");
+ extensionToMimeTypeMap.put("ods", "application/vnd.oasis.opendocument.spreadsheet");
+ extensionToMimeTypeMap.put("ots", "application/vnd.oasis.opendocument.spreadsheet-template");
+ extensionToMimeTypeMap.put("odt", "application/vnd.oasis.opendocument.text");
+ extensionToMimeTypeMap.put("odm", "application/vnd.oasis.opendocument.text-master");
+ extensionToMimeTypeMap.put("ott", "application/vnd.oasis.opendocument.text-template");
+ extensionToMimeTypeMap.put("oth", "application/vnd.oasis.opendocument.text-web");
+ }
+
+ 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;
+ }
+
+ static String getMimeType(String filename) {
+ String extension = MimeTypeMap.getFileExtensionFromUrl(filename);
+ String mime = extensionToMimeTypeMap.get(extension);
+ if (mime == null) {
+ //fallback to Android's MimeTypeMap
+ mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+ extension);
+ }
+ return mime;
+ }
+
+ // Filter by mode, and/or in future by filename/wildcard
+ private static boolean doAccept(String filename, int byMode, String byFilename) {
+ Log.d(LOGTAG, "doAccept : " + filename + " mode " + byMode + " byFilename " + byFilename);
+ if (filename == null)
+ return false;
+
+ // check extension
+ if (byMode != ALL) {
+ if (mExtnMap.get (getExtension (filename)) != byMode)
+ return false;
+ }
+ if (!byFilename.equals("")) {
+ // FIXME return false on a non-match
+ }
+ return true;
+ }
+
+ static FileFilter getFileFilter(final int mode) {
+ return new FileFilter() {
+ public boolean accept(File pathname) {
+ if (pathname.isDirectory())
+ return true;
+ if (lookupExtension(pathname.getName()) == UNKNOWN)
+ return false;
+ return doAccept(pathname.getName(), mode, "");
+ }
+ };
+ }
+
+ static FilenameFilter getFilenameFilter(final int mode) {
+ return new FilenameFilter() {
+ public boolean accept(File dir, String filename) {
+ if (new File(dir , filename).isDirectory())
+ return true;
+ return doAccept(filename, mode, "");
+ }
+ };
+ }
+
+ static void sortFiles(List<IFile> files, int sortMode) {
+ if (files == null)
+ return;
+ // Compare filenames in the default locale
+ final Collator mCollator = Collator.getInstance();
+ switch (sortMode) {
+ case SORT_AZ:
+ Collections.sort(files , new Comparator<IFile>() {
+ public int compare(IFile lhs, IFile rhs) {
+ return mCollator.compare(lhs.getName(), rhs.getName());
+ }
+ });
+ break;
+ case SORT_ZA:
+ Collections.sort(files , new Comparator<IFile>() {
+ public int compare(IFile lhs, IFile rhs) {
+ return mCollator.compare(rhs.getName(), lhs.getName());
+ }
+ });
+ break;
+ case SORT_OLDEST:
+ Collections.sort(files , new Comparator<IFile>() {
+ public int compare(IFile lhs, IFile rhs) {
+ return lhs.getLastModified().compareTo(rhs.getLastModified());
+ }
+ });
+ break;
+ case SORT_NEWEST:
+ Collections.sort(files , new Comparator<IFile>() {
+ public int compare(IFile lhs, IFile rhs) {
+ return rhs.getLastModified().compareTo(lhs.getLastModified());
+ }
+ });
+ break;
+ case SORT_LARGEST:
+ Collections.sort(files , new Comparator<IFile>() {
+ public int compare(IFile lhs, IFile rhs) {
+ return Long.valueOf(rhs.getSize()).compareTo(lhs.getSize());
+ }
+ });
+ break;
+ case SORT_SMALLEST:
+ Collections.sort(files , new Comparator<IFile>() {
+ public int compare(IFile lhs, IFile rhs) {
+ return Long.valueOf(lhs.getSize()).compareTo(rhs.getSize());
+ }
+ });
+ break;
+ default:
+ Log.e(LOGTAG, "uncatched sortMode: " + sortMode);
+ }
+ }
+
+ static boolean isHidden(File file) {
+ return file.getName().startsWith(".");
+ }
+
+ static boolean isThumbnail(File file) {
+ return isHidden(file) && file.getName().endsWith(".png");
+ }
+
+ static boolean hasThumbnail(File file) {
+ String filename = file.getName();
+ if (lookupExtension(filename) == DOC) // only do this for docs for now
+ {
+ // Will need another method to check if Thumb is up-to-date - or extend this one?
+ return new File(file.getParent(), getThumbnailName(file)).isFile();
+ }
+ return true;
+ }
+
+ static String getThumbnailName(File file) {
+ return "." + file.getName().split("[.]")[0] + ".png" ;
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/FolderIconView.java b/android/source/src/java/org/libreoffice/ui/FolderIconView.java
new file mode 100644
index 000000000..cde6cd27a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/FolderIconView.java
@@ -0,0 +1,204 @@
+/* -*- 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.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import java.io.File;
+import java.util.Stack;
+
+public class FolderIconView extends View{
+ private String LOGTAG = "FolderIconView";
+
+ private Paint mPaintBlack;
+ private Paint mPaintGray;
+ private Paint mPaintShadow;
+
+ private File dir;
+
+ public FolderIconView(Context context) {
+ super(context);
+ initialisePaints();
+ }
+ public FolderIconView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialisePaints();
+ }
+ public FolderIconView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initialisePaints();
+ }
+
+ private void initialisePaints() {
+ mPaintBlack = new Paint();
+ mPaintBlack.setColor(Color.DKGRAY);//Can also use parseColor(String "#aarrggbb")
+ mPaintBlack.setAntiAlias(true);
+
+ mPaintGray = new Paint();
+ mPaintGray.setColor(Color.GRAY);//Can also use parseColor(String "#aarrggbb")
+ mPaintGray.setAntiAlias(true);
+
+ mPaintShadow = new Paint();
+ mPaintShadow.setColor(Color.parseColor("#88888888"));
+ mPaintShadow.setAntiAlias(true);
+ }
+
+ public void setDir(File dir) {
+ this.dir = dir;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Log.d(LOGTAG, "onDraw");
+ //float width = (float)canvas.getWidth();
+ //float height = (float)canvas.getHeight();
+ float width = (float) this.getWidth();
+ float height = (float) this.getHeight();
+ float centerX = width*0.5f;// centered on horz axis
+ float centerY = height*0.5f;
+ float outerRadius = 0.8f*0.5f*width;
+ float innerRadius = 0.7f*0.5f*width;
+ float thumbHeight = outerRadius*1.25f;
+ float thumbWidth = thumbHeight*(float)(1/Math.sqrt(2));
+ float DZx = 0.2f*outerRadius;
+ float DZy = 0.2f*outerRadius;
+ //Bitmap blankPage = BitmapFactory.decodeResource(getResources(), R.drawable.page);
+ Log.i(LOGTAG, Float.toString(width) + "x" + Float.toString(height));
+ canvas.drawCircle(centerX, centerY, outerRadius, mPaintGray);
+ canvas.drawCircle(centerX, centerY, innerRadius, mPaintBlack);
+ //Either get thumbs from directory or use generic page images
+ //For now just get the first 4 thumbs -> add some checks later
+ if (dir == null)
+ return;//TODO
+ File[] contents = dir.listFiles();//TODO consider filtering thumbs to match grid.
+ if (contents == null)
+ // dir is not a directory,
+ // or user does not have permissions to read it
+ return;
+ Stack<Bitmap> thumbs = new Stack<Bitmap>();
+ BitmapFactory factory = new BitmapFactory();
+ for (File file : contents) {
+ if (!FileUtilities.isThumbnail(file))
+ continue;
+ thumbs.push(BitmapFactory.decodeFile(file.getAbsolutePath()));//TODO switch to push for semantics
+ if (thumbs.size() > 3)
+ break;
+ }
+ /*while(thumbs.size() < 4) {// padd out with blanks?
+ thumbs.push(blankPage);
+ }*/
+ Log.i(LOGTAG, Integer.toString(thumbs.size()));
+ //should handle empty folders better
+ // options:
+ // don't show?
+ // show generic LO icons for writer etc
+ // Show a generic blank page icon
+ if (thumbs.isEmpty())
+ return;
+ /*float left = centerX ;//+ 0.25f*outerRadius;
+ float top = centerY - 0.5f*outerRadius;
+ float right = left + thumbs.get(0).getWidth()*0.4f;
+ float bottom = top + thumbs.get(0).getHeight()*0.4f;
+ RectF dest = new RectF(left, top, right, bottom);
+ RectF shadowBox = new RectF(dest);
+ shadowBox.inset(-1, -1);
+ int size = thumbs.size();
+ for (int i = 1; i <= size; i++) {
+ canvas.drawRect(shadowBox, mPaintShadow);
+ canvas.drawBitmap(thumbs.pop(), null, dest, null);
+ dest.offset(-outerRadius*0.2f, outerRadius*0.1f);
+ shadowBox.offset(-outerRadius*0.2f, outerRadius*0.1f);
+ }*/
+ float left;
+ float top;
+ float right;
+ float bottom;
+ RectF dest;
+ RectF shadowBox;
+ int size;
+ switch(thumbs.size()) {
+ case 0:
+ break;
+ case 1:
+ left = centerX - 0.5f*thumbWidth;
+ top = centerY - 0.5f*thumbHeight;
+ right = left + thumbWidth;
+ bottom = top + thumbHeight;
+ dest = new RectF(left, top, right, bottom);
+ shadowBox = new RectF(dest);
+ shadowBox.inset(-1, -1);
+ canvas.drawRect(shadowBox, mPaintShadow);
+ canvas.drawBitmap(thumbs.pop(), null, dest, null);
+ break;
+ case 2:
+ left = centerX - 0.5f*thumbWidth + 0.5f*DZx;
+ top = centerY - 0.5f*thumbHeight - 0.5f*DZy;
+ right = left + thumbWidth;
+ bottom = top + thumbHeight;
+ dest = new RectF(left, top, right, bottom);
+ shadowBox = new RectF(dest);
+ shadowBox.inset(-1, -1);
+ size = thumbs.size();
+ for (int i = 1; i <= size; i++) {
+ canvas.drawRect(shadowBox, mPaintShadow);
+ canvas.drawBitmap(thumbs.pop(), null, dest, null);
+ dest.offset(-DZx, DZy);
+ shadowBox.offset(-DZx, DZy);
+ }
+ break;
+ case 3:
+ left = centerX - 0.5f*thumbWidth + DZx;
+ top = centerY - 0.5f*thumbHeight - DZy;
+ right = left + thumbWidth;
+ bottom = top + thumbHeight;
+ dest = new RectF(left, top, right, bottom);
+ shadowBox = new RectF(dest);
+ shadowBox.inset(-1, -1);
+ size = thumbs.size();
+ for (int i = 1; i <= size; i++) {
+ canvas.drawRect(shadowBox, mPaintShadow);
+ canvas.drawBitmap(thumbs.pop(), null, dest, null);
+ dest.offset(-DZx, DZy);
+ shadowBox.offset(-DZx, DZy);
+ }
+ break;
+ case 4:
+ left = centerX - 0.5f*thumbWidth + 1.5f*DZx;
+ top = centerY - 0.5f*thumbHeight - 1.5f*DZy;
+ right = left + thumbWidth;
+ bottom = top + thumbHeight;
+ dest = new RectF(left, top, right, bottom);
+ shadowBox = new RectF(dest);
+ shadowBox.inset(-1, -1);
+ size = thumbs.size();
+ for (int i = 1; i <= size; i++) {
+ canvas.drawRect(shadowBox, mPaintShadow);
+ canvas.drawBitmap(thumbs.pop(), null, dest, null);
+ dest.offset(-DZx, DZy);
+ shadowBox.offset(-DZx, DZy);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+}
+
+/* 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 000000000..a9d797c4b
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java
@@ -0,0 +1,1224 @@
+/* -*- 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.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+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.hardware.usb.UsbManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.design.widget.FloatingActionButton;
+import android.support.design.widget.NavigationView;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.text.InputType;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.OvershootInterpolator;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.libreoffice.AboutDialogFragment;
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.LocaleHelper;
+import org.libreoffice.R;
+import org.libreoffice.SettingsActivity;
+import org.libreoffice.SettingsListenerModel;
+import org.libreoffice.storage.DocumentProviderFactory;
+import org.libreoffice.storage.DocumentProviderSettingsActivity;
+import org.libreoffice.storage.IDocumentProvider;
+import org.libreoffice.storage.IFile;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FilenameFilter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class LibreOfficeUIActivity extends AppCompatActivity implements SettingsListenerModel.OnSettingsPreferenceChangedListener, View.OnClickListener{
+ private String LOGTAG = LibreOfficeUIActivity.class.getSimpleName();
+ private SharedPreferences prefs;
+ private int filterMode = FileUtilities.ALL;
+ private int viewMode;
+ private int sortMode;
+ private boolean showHiddenFiles;
+ private String displayLanguage;
+
+ // dynamic permissions IDs
+ private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 0;
+
+ FileFilter fileFilter;
+ FilenameFilter filenameFilter;
+ private List<IFile> filePaths = new ArrayList<IFile>();
+ private DocumentProviderFactory documentProviderFactory;
+ private IDocumentProvider documentProvider;
+ private IFile homeDirectory;
+ private IFile currentDirectory;
+ private int currentlySelectedFile;
+
+ private static final String CURRENT_DIRECTORY_KEY = "CURRENT_DIRECTORY";
+ private static final String DOC_PROVIDER_KEY = "CURRENT_DOCUMENT_PROVIDER";
+ private static final String FILTER_MODE_KEY = "FILTER_MODE";
+ public static final String EXPLORER_VIEW_TYPE_KEY = "EXPLORER_VIEW_TYPE";
+ public static final String EXPLORER_PREFS_KEY = "EXPLORER_PREFS";
+ public static final String SORT_MODE_KEY = "SORT_MODE";
+ private static final String RECENT_DOCUMENTS_KEY = "RECENT_DOCUMENTS";
+ private static final String ENABLE_SHOW_HIDDEN_FILES_KEY = "ENABLE_SHOW_HIDDEN_FILES";
+ private static final String DISPLAY_LANGUAGE = "DISPLAY_LANGUAGE";
+
+ public static final String NEW_FILE_PATH_KEY = "NEW_FILE_PATH_KEY";
+ 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";
+
+ public static final int GRID_VIEW = 0;
+ public static final int LIST_VIEW = 1;
+
+ private DrawerLayout drawerLayout;
+ private NavigationView navigationDrawer;
+ private ActionBar actionBar;
+ private ActionBarDrawerToggle drawerToggle;
+ private RecyclerView fileRecyclerView;
+ private RecyclerView recentRecyclerView;
+
+ private boolean canQuit = false;
+
+ 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);
+
+ // initialize document provider factory
+ DocumentProviderFactory.initialize(this);
+ documentProviderFactory = DocumentProviderFactory.getInstance();
+
+ PreferenceManager.setDefaultValues(this, R.xml.documentprovider_preferences, false);
+ readPreferences();
+ SettingsListenerModel.getInstance().setListener(this);
+ // Registering the USB detect broadcast receiver
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+ registerReceiver(mUSBReceiver, filter);
+ // init UI and populate with contents from the provider
+
+
+ createUI();
+ fabOpenAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_open);
+ fabCloseAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_close);
+ }
+
+ @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 = getSupportActionBar();
+
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ editFAB = findViewById(R.id.editFAB);
+ editFAB.setOnClickListener(this);
+ 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);
+
+ recentRecyclerView = findViewById(R.id.list_recent);
+
+ Set<String> recentFileStrings = prefs.getStringSet(RECENT_DOCUMENTS_KEY, new HashSet<String>());
+
+ final ArrayList<IFile> recentFiles = new ArrayList<IFile>();
+ for (String recentFileString : recentFileStrings) {
+ try {
+ if(documentProvider != null)
+ recentFiles.add(documentProvider.createFromUri(this, new URI(recentFileString)));
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ } catch (RuntimeException e){
+ e.printStackTrace();
+ }
+ }
+
+ recentRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
+ recentRecyclerView.setAdapter(new RecentFilesAdapter(this, recentFiles));
+
+ fileRecyclerView = findViewById(R.id.file_recycler_view);
+ //This should be tested because it possibly disables view recycling
+ fileRecyclerView.setNestedScrollingEnabled(false);
+ openDirectory(currentDirectory);
+ registerForContextMenu(fileRecyclerView);
+
+ //Setting up navigation drawer
+ drawerLayout = findViewById(R.id.drawer_layout);
+ navigationDrawer = findViewById(R.id.navigation_drawer);
+
+ final ArrayList<CharSequence> providerNames = new ArrayList<CharSequence>(
+ Arrays.asList(documentProviderFactory.getNames())
+ );
+
+ // Loop through the document providers menu items and check if they are available or not
+ for (int index = 0; index < providerNames.size(); index++) {
+ MenuItem item = navigationDrawer.getMenu().getItem(index);
+ item.setEnabled(documentProviderFactory.getProvider(index).checkProviderAvailability(this));
+ }
+
+ navigationDrawer.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+
+ switch (item.getItemId()) {
+ case R.id.menu_storage_preferences: {
+ startActivity(new Intent(LibreOfficeUIActivity.this, DocumentProviderSettingsActivity.class));
+ return true;
+ }
+
+ case R.id.menu_provider_documents: {
+ switchToDocumentProvider(documentProviderFactory.getProvider(0));
+ return true;
+ }
+
+ case R.id.menu_provider_filesystem: {
+ switchToDocumentProvider(documentProviderFactory.getProvider(1));
+ return true;
+ }
+
+ case R.id.menu_provider_extsd: {
+ switchToDocumentProvider(documentProviderFactory.getProvider(2));
+ return true;
+ }
+
+ case R.id.menu_provider_otg: {
+ switchToDocumentProvider(documentProviderFactory.getProvider(3));
+ return true;
+ }
+
+ case R.id.menu_provider_owncloud: {
+ switchToDocumentProvider(documentProviderFactory.getProvider(4));
+ return true;
+ }
+
+ default:
+ return false;
+ }
+
+
+ }
+ });
+ drawerToggle = new ActionBarDrawerToggle(this, drawerLayout,
+ R.string.document_locations, R.string.close_document_locations) {
+
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ super.onDrawerOpened(drawerView);
+ supportInvalidateOptionsMenu();
+ navigationDrawer.requestFocus(); // Make keypad navigation easier
+ if (isFabMenuOpen) {
+ collapseFabMenu(); //Collapse FAB Menu when drawer is opened
+ }
+ }
+
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ super.onDrawerClosed(drawerView);
+ supportInvalidateOptionsMenu();
+ }
+ };
+ drawerToggle.setDrawerIndicatorEnabled(true);
+ drawerLayout.addDrawerListener(drawerToggle);
+ drawerToggle.syncState();
+ }
+
+ 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
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+
+ drawerToggle.syncState();
+ }
+
+ private void refreshView() {
+ // enable home icon as "up" if required
+ if (currentDirectory != null && homeDirectory != null && !currentDirectory.equals(homeDirectory)) {
+ drawerToggle.setDrawerIndicatorEnabled(false);
+ } else {
+ drawerToggle.setDrawerIndicatorEnabled(true);
+ }
+
+ FileUtilities.sortFiles(filePaths, sortMode);
+ // refresh view
+ fileRecyclerView.setLayoutManager(isViewModeList() ? new LinearLayoutManager(this) : new GridLayoutManager(this, 3));
+ fileRecyclerView.setAdapter(new ExplorerItemAdapter(this, filePaths));
+ // close drawer if it was open
+ drawerLayout.closeDrawer(navigationDrawer);
+ if (isFabMenuOpen) {
+ collapseFabMenu();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (drawerLayout.isDrawerOpen(navigationDrawer)) {
+ drawerLayout.closeDrawer(navigationDrawer);
+ if (isFabMenuOpen) {
+ collapseFabMenu();
+ }
+ } else if (currentDirectory != null && homeDirectory != null && !currentDirectory.equals(homeDirectory)) {
+ // navigate upwards in directory hierarchy
+ openParentDirectory();
+ } else if (isFabMenuOpen) {
+ collapseFabMenu();
+ } else {
+ // only exit if warning has been shown
+ if (canQuit) {
+ super.onBackPressed();
+ return;
+ }
+
+ // show warning about leaving the app and set a timer
+ Toast.makeText(this, R.string.back_again_to_quit,
+ Toast.LENGTH_SHORT).show();
+ canQuit = true;
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ canQuit = false;
+ }
+ }, 3000);
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.context_menu, menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.context_menu_open:
+ open(currentlySelectedFile);
+ return true;
+ case R.id.context_menu_share:
+ share(currentlySelectedFile);
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ private boolean isViewModeList(){
+ return viewMode == LIST_VIEW;
+ }
+
+ private void switchToDocumentProvider(IDocumentProvider provider) {
+
+ new AsyncTask<IDocumentProvider, Void, Void>() {
+ @Override
+ protected Void doInBackground(IDocumentProvider... provider) {
+ // switch document provider:
+ // these operations may imply network access and must be run in
+ // a different thread
+ try {
+ homeDirectory = provider[0].getRootDirectory(LibreOfficeUIActivity.this);
+ List<IFile> paths = homeDirectory.listFiles(FileUtilities
+ .getFileFilter(filterMode));
+ filePaths = new ArrayList<IFile>();
+ for(IFile file: paths) {
+ if(showHiddenFiles){
+ filePaths.add(file);
+ } else {
+ if(!file.getName().startsWith(".")){
+ filePaths.add(file);
+ }
+ }
+ }
+ }
+ catch (final RuntimeException e) {
+ final Activity activity = LibreOfficeUIActivity.this;
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity, e.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ startActivity(new Intent(activity, DocumentProviderSettingsActivity.class));
+ Log.e(LOGTAG, "failed to switch document provider "+ e.getMessage(), e.getCause());
+ return null;
+ }
+ //no exception
+ documentProvider = provider[0];
+ currentDirectory = homeDirectory;
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ refreshView();
+ }
+ }.execute(provider);
+ }
+
+ public void openDirectory(IFile dir) {
+ if (dir == null)
+ return;
+
+ //show recent files if in home directory
+ if (dir.equals(homeDirectory)) {
+ recentRecyclerView.setVisibility(View.VISIBLE);
+ findViewById(R.id.header_browser).setVisibility((View.VISIBLE));
+ findViewById(R.id.header_recents).setVisibility((View.VISIBLE));
+ actionBar.setTitle(R.string.app_name);
+ findViewById(R.id.text_directory_path).setVisibility(View.GONE);
+ } else {
+ recentRecyclerView.setVisibility(View.GONE);
+ findViewById(R.id.header_browser).setVisibility((View.GONE));
+ findViewById(R.id.header_recents).setVisibility((View.GONE));
+ actionBar.setTitle(dir.getName());
+ findViewById(R.id.text_directory_path).setVisibility(View.VISIBLE);
+ ((TextView)findViewById(R.id.text_directory_path)).setText(getString(R.string.current_dir,
+ dir.getUri().getPath()));
+ }
+
+ new AsyncTask<IFile, Void, Void>() {
+ @Override
+ protected Void doInBackground(IFile... dir) {
+ // get list of files:
+ // this operation may imply network access and must be run in
+ // a different thread
+ currentDirectory = dir[0];
+ try {
+ List<IFile> paths = currentDirectory.listFiles(FileUtilities
+ .getFileFilter(filterMode));
+ filePaths = new ArrayList<IFile>();
+ for(IFile file: paths) {
+ if(showHiddenFiles){
+ filePaths.add(file);
+ } else {
+ if(!file.getName().startsWith(".")){
+ filePaths.add(file);
+ }
+ }
+ }
+ }
+ catch (final RuntimeException e) {
+ final Activity activity = LibreOfficeUIActivity.this;
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity, e.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ Log.e(LOGTAG, e.getMessage(), e.getCause());
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ refreshView();
+ }
+ }.execute(dir);
+ }
+
+ public void open(final IFile document) {
+ addDocumentToRecents(document);
+ new AsyncTask<IFile, Void, File>() {
+ @Override
+ protected File doInBackground(IFile... document) {
+ // this operation may imply network access and must be run in
+ // a different thread
+ try {
+ return document[0].getDocument();
+ }
+ catch (final RuntimeException e) {
+ final Activity activity = LibreOfficeUIActivity.this;
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity, e.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ Log.e(LOGTAG, e.getMessage(), e.getCause());
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(File file) {
+ if (file != null) {
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.fromFile(file));
+ String packageName = getApplicationContext().getPackageName();
+ ComponentName componentName = new ComponentName(packageName,
+ LibreOfficeMainActivity.class.getName());
+ i.setComponent(componentName);
+
+ // these extras allow to rebuild the IFile object in LOMainActivity
+ i.putExtra("org.libreoffice.document_provider_id",
+ documentProvider.getId());
+ i.putExtra("org.libreoffice.document_uri",
+ document.getUri());
+
+ startActivity(i);
+ }
+ }
+ }.execute(document);
+ }
+
+ // Opens an Input dialog to get the name of new file
+ private void createNewFileInputDialog(final String defaultFileName, final String newDocumentType) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.create_new_document_title);
+ final EditText input = new EditText(this);
+ input.setInputType(InputType.TYPE_CLASS_TEXT);
+ input.setText(defaultFileName);
+ builder.setView(input);
+
+ builder.setPositiveButton(R.string.action_create, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final String newFilePath = currentDirectory.getUri().getPath() + input.getText().toString();
+ loadNewDocument(newDocumentType, newFilePath);
+ }
+ });
+
+ builder.setNegativeButton(R.string.action_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+
+ builder.show();
+ }
+
+ private void loadNewDocument(String newDocumentType, String newFilePath) {
+ Intent intent = new Intent(LibreOfficeUIActivity.this, LibreOfficeMainActivity.class);
+ intent.putExtra(NEW_DOC_TYPE_KEY, newDocumentType);
+ intent.putExtra(NEW_FILE_PATH_KEY, newFilePath);
+ startActivity(intent);
+ }
+
+ private void open(int position) {
+ IFile file = filePaths.get(position);
+ if (!file.isDirectory()) {
+ open(file);
+ } else {
+ openDirectory(file);
+ }
+ }
+
+ private void openParentDirectory() {
+ new AsyncTask<Void, Void, IFile>() {
+ @Override
+ protected IFile doInBackground(Void... dir) {
+ // this operation may imply network access and must be run in
+ // a different thread
+ return currentDirectory.getParent(LibreOfficeUIActivity.this);
+ }
+
+ @Override
+ protected void onPostExecute(IFile result) {
+ openDirectory(result);
+ }
+ }.execute();
+ }
+
+ private void share(int position) {
+
+ new AsyncTask<IFile, Void, File>() {
+ @Override
+ protected File doInBackground(IFile... document) {
+ // this operation may imply network access and must be run in
+ // a different thread
+ try {
+ return document[0].getDocument();
+ } catch (final RuntimeException e) {
+ final Activity activity = LibreOfficeUIActivity.this;
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity, e.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ Log.e(LOGTAG, e.getMessage(), e.getCause());
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(File file) {
+ if (file != null) {
+ Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
+ Uri uri = Uri.fromFile(file);
+ sharingIntent.setType(FileUtilities.getMimeType(file.getName()));
+ sharingIntent.putExtra(android.content.Intent.EXTRA_STREAM, uri);
+ sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
+ file.getName());
+ startActivity(Intent.createChooser(sharingIntent,
+ getString(R.string.share_via)));
+ }
+ }
+ }.execute(filePaths.get(position));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.view_menu, menu);
+
+ switch (sortMode) {
+ case FileUtilities.SORT_SMALLEST: {
+ menu.findItem(R.id.menu_sort_size_asc).setChecked(true);
+ }
+ break;
+
+ case FileUtilities.SORT_LARGEST: {
+ menu.findItem(R.id.menu_sort_size_desc).setChecked(true);
+ }
+ break;
+
+ case FileUtilities.SORT_AZ: {
+ menu.findItem(R.id.menu_sort_az).setChecked(true);
+ }
+ break;
+
+ case FileUtilities.SORT_ZA: {
+ menu.findItem(R.id.menu_sort_za).setChecked(true);
+ }
+ break;
+
+ case FileUtilities.SORT_NEWEST: {
+ menu.findItem(R.id.menu_sort_modified_newest).setChecked(true);
+ }
+ break;
+
+ case FileUtilities.SORT_OLDEST: {
+ menu.findItem(R.id.menu_sort_modified_oldest).setChecked(true);
+ }
+ break;
+ }
+
+ switch (filterMode) {
+ case FileUtilities.ALL:
+ menu.findItem(R.id.menu_filter_everything).setChecked(true);
+ break;
+
+ case FileUtilities.DOC:
+ menu.findItem(R.id.menu_filter_documents).setChecked(true);
+ break;
+
+ case FileUtilities.CALC:
+ menu.findItem(R.id.menu_filter_presentations).setChecked(true);
+ break;
+
+ case FileUtilities.IMPRESS:
+ menu.findItem(R.id.menu_filter_presentations).setChecked(true);
+ break;
+
+ case FileUtilities.DRAWING:
+ menu.findItem(R.id.menu_filter_drawings).setChecked(true);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Will close the drawer if the home button is pressed
+ if (drawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (!currentDirectory.equals(homeDirectory)){
+ openParentDirectory();
+ }
+ break;
+
+ case R.id.menu_filter_everything:
+ item.setChecked(true);
+ filterMode = FileUtilities.ALL;
+ openDirectory(currentDirectory);
+ break;
+
+ case R.id.menu_filter_documents:
+ item.setChecked(true);
+ filterMode = FileUtilities.DOC;
+ openDirectory(currentDirectory);
+ break;
+
+ case R.id.menu_filter_spreadsheets:
+ item.setChecked(true);
+ filterMode = FileUtilities.CALC;
+ openDirectory(currentDirectory);
+ break;
+
+ case R.id.menu_filter_presentations:
+ item.setChecked(true);
+ filterMode = FileUtilities.IMPRESS;
+ openDirectory(currentDirectory);
+ break;
+
+ case R.id.menu_filter_drawings:
+ item.setChecked(true);
+ filterMode = FileUtilities.DRAWING;
+ openDirectory(currentDirectory);
+ break;
+
+ case R.id.menu_sort_size_asc: {
+ sortMode = FileUtilities.SORT_SMALLEST;
+ this.onResume();
+ }
+ break;
+
+ case R.id.menu_sort_size_desc: {
+ sortMode = FileUtilities.SORT_LARGEST;
+ this.onResume();
+ }
+ break;
+
+ case R.id.menu_sort_az: {
+ sortMode = FileUtilities.SORT_AZ;
+ this.onResume();
+ }
+ break;
+
+ case R.id.menu_sort_za: {
+ sortMode = FileUtilities.SORT_ZA;
+ this.onResume();
+ }
+ break;
+
+ case R.id.menu_sort_modified_newest: {
+ sortMode = FileUtilities.SORT_NEWEST;
+ this.onResume();
+ }
+ break;
+
+ case R.id.menu_sort_modified_oldest: {
+ sortMode = FileUtilities.SORT_OLDEST;
+ this.onResume();
+ }
+ break;
+
+ case R.id.action_about: {
+ AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
+ aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
+ }
+ return true;
+ case R.id.action_settings:
+ startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+ public void readPreferences(){
+ prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
+ sortMode = prefs.getInt(SORT_MODE_KEY, FileUtilities.SORT_AZ);
+ SharedPreferences defaultPrefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
+ viewMode = Integer.valueOf(defaultPrefs.getString(EXPLORER_VIEW_TYPE_KEY, ""+ GRID_VIEW));
+ filterMode = Integer.valueOf(defaultPrefs.getString(FILTER_MODE_KEY , "-1"));
+ showHiddenFiles = defaultPrefs.getBoolean(ENABLE_SHOW_HIDDEN_FILES_KEY, false);
+ displayLanguage = defaultPrefs.getString(DISPLAY_LANGUAGE, LocaleHelper.SYSTEM_DEFAULT_LANGUAGE);
+
+ Intent i = this.getIntent();
+ if (i.hasExtra(CURRENT_DIRECTORY_KEY)) {
+ try {
+ currentDirectory = documentProvider.createFromUri(this, new URI(
+ i.getStringExtra(CURRENT_DIRECTORY_KEY)));
+ } catch (URISyntaxException e) {
+ currentDirectory = documentProvider.getRootDirectory(this);
+ }
+ Log.d(LOGTAG, CURRENT_DIRECTORY_KEY);
+ }
+
+ if (i.hasExtra(FILTER_MODE_KEY)) {
+ filterMode = i.getIntExtra( FILTER_MODE_KEY, FileUtilities.ALL);
+ Log.d(LOGTAG, FILTER_MODE_KEY);
+ }
+
+ if (i.hasExtra(EXPLORER_VIEW_TYPE_KEY)) {
+ viewMode = i.getIntExtra( EXPLORER_VIEW_TYPE_KEY, GRID_VIEW);
+ Log.d(LOGTAG, EXPLORER_VIEW_TYPE_KEY);
+ }
+
+ LocaleHelper.setLocale(this, displayLanguage);
+ }
+
+ @Override
+ public void settingsPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ readPreferences();
+ refreshView();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ // TODO Auto-generated method stub
+ super.onSaveInstanceState(outState);
+
+ if(currentDirectory != null) {
+ outState.putString(CURRENT_DIRECTORY_KEY, currentDirectory.getUri().toString());
+ Log.d(LOGTAG, currentDirectory.toString() + Integer.toString(filterMode) + Integer.toString(viewMode));
+ }
+ outState.putInt(FILTER_MODE_KEY, filterMode);
+ outState.putInt(EXPLORER_VIEW_TYPE_KEY , viewMode);
+ if(documentProvider != null)
+ outState.putInt(DOC_PROVIDER_KEY, documentProvider.getId());
+
+ outState.putBoolean(ENABLE_SHOW_HIDDEN_FILES_KEY , showHiddenFiles);
+
+ //prefs.edit().putInt(EXPLORER_VIEW_TYPE, viewType).commit();
+ Log.d(LOGTAG, "savedInstanceState");
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ // TODO Auto-generated method stub
+ super.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState.isEmpty()){
+ return;
+ }
+ if (documentProvider == null) {
+ Log.d(LOGTAG, "onRestoreInstanceState - documentProvider is null");
+ documentProvider = DocumentProviderFactory.getInstance()
+ .getProvider(savedInstanceState.getInt(DOC_PROVIDER_KEY));
+ }
+ try {
+ currentDirectory = documentProvider.createFromUri(this, new URI(
+ savedInstanceState.getString(CURRENT_DIRECTORY_KEY)));
+ } catch (URISyntaxException e) {
+ currentDirectory = documentProvider.getRootDirectory(this);
+ }
+ filterMode = savedInstanceState.getInt(FILTER_MODE_KEY, FileUtilities.ALL);
+ viewMode = savedInstanceState.getInt(EXPLORER_VIEW_TYPE_KEY, GRID_VIEW);
+ showHiddenFiles = savedInstanceState.getBoolean(ENABLE_SHOW_HIDDEN_FILES_KEY, false);
+ //openDirectory(currentDirectory);
+ Log.d(LOGTAG, "onRestoreInstanceState");
+ Log.d(LOGTAG, currentDirectory.toString() + Integer.toString(filterMode) + Integer.toString(viewMode));
+ }
+
+ private final BroadcastReceiver mUSBReceiver = new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
+ Toast.makeText(context, R.string.usb_connected_configure, Toast.LENGTH_SHORT).show();
+ startActivity(new Intent(context, DocumentProviderSettingsActivity.class));
+ Log.d(LOGTAG, "USB device attached");
+ } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
+ Log.d(LOGTAG, "USB device detached");
+ }
+ }
+ };
+ @Override
+ protected void onPause() {
+ super.onPause();
+ Log.d(LOGTAG, "onPause");
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(LOGTAG, "onResume");
+ Log.d(LOGTAG, "sortMode="+ sortMode + " filterMode=" + filterMode);
+ createUI();
+ }
+
+ @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);
+ } else {
+ switchToDocumentProvider(documentProviderFactory.getDefaultProvider());
+ setEditFABVisibility(View.VISIBLE);
+ }
+ Log.d(LOGTAG, "onStart");
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.d(LOGTAG, "onStop");
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(mUSBReceiver);
+ Log.d(LOGTAG, "onDestroy");
+ }
+
+ private int dpToPx(int dp){
+ final float scale = getApplicationContext().getResources().getDisplayMetrics().density;
+ return (int) (dp * scale + 0.5f);
+ }
+
+ private void addDocumentToRecents(IFile iFile) {
+ String newRecent = iFile.getUri().toString();
+ Set<String> recentsSet = prefs.getStringSet(RECENT_DOCUMENTS_KEY, new HashSet<String>());
+
+ //create array to work with
+ ArrayList<String> recentsArrayList = new ArrayList<String>(recentsSet);
+
+ //remove string if present, so that it doesn't appear multiple times
+ recentsSet.remove(newRecent);
+
+ //put the new value in the first place
+ recentsArrayList.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 (recentsArrayList.size() > RECENTS_SIZE) {
+ recentsArrayList.remove(RECENTS_SIZE);
+ }
+
+ //switch to Set, so that it could be inserted into prefs
+ recentsSet = new HashSet<String>(recentsArrayList);
+
+ prefs.edit().putStringSet(RECENT_DOCUMENTS_KEY, recentsSet).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<ShortcutInfo>();
+ for (String pathString : recentsArrayList) {
+
+ //find the appropriate drawable
+ int drawable = 0;
+ switch (FileUtilities.getType(pathString)) {
+ 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;
+ }
+
+ File file = new File(pathString);
+
+ //for some reason, getName uses %20 instead of space
+ String filename = file.getName().replace("%20", " ");
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.fromFile(file));
+ 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();
+ switch (id){
+ case R.id.editFAB:
+ if (isFabMenuOpen) {
+ collapseFabMenu();
+ } else {
+ expandFabMenu();
+ }
+ break;
+ case R.id.newWriterFAB:
+ createNewFileInputDialog(getString(R.string.default_document_name) + FileUtilities.DEFAULT_WRITER_EXTENSION, NEW_WRITER_STRING_KEY);
+ break;
+ case R.id.newImpressFAB:
+ createNewFileInputDialog(getString(R.string.default_document_name) + FileUtilities.DEFAULT_IMPRESS_EXTENSION, NEW_IMPRESS_STRING_KEY);
+ break;
+ case R.id.newCalcFAB:
+ createNewFileInputDialog(getString(R.string.default_document_name) + FileUtilities.DEFAULT_SPREADSHEET_EXTENSION, NEW_CALC_STRING_KEY);
+ break;
+ case R.id.newDrawFAB:
+ createNewFileInputDialog(getString(R.string.default_document_name) + FileUtilities.DEFAULT_DRAWING_EXTENSION, NEW_DRAW_STRING_KEY);
+ break;
+ }
+ }
+
+
+ class ExplorerItemAdapter extends RecyclerView.Adapter<ExplorerItemAdapter.ViewHolder> {
+
+ private Activity mActivity;
+ private List<IFile> filePaths;
+ private final long KB = 1024;
+ private final long MB = 1048576;
+
+ ExplorerItemAdapter(Activity activity, List<IFile> filePaths) {
+ this.mActivity = activity;
+ this.filePaths = filePaths;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View item = LayoutInflater.from(parent.getContext())
+ .inflate(isViewModeList() ? R.layout.file_list_item : R.layout.file_explorer_grid_item, parent, false);
+ return new ViewHolder(item);
+ }
+
+ @Override
+ public void onBindViewHolder(final ViewHolder holder, final int position) {
+ final IFile file = filePaths.get(position);
+
+ holder.itemView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ open(holder.getAdapterPosition());
+ }
+ });
+ holder.itemView.setOnLongClickListener(new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View view) {
+ //to be picked out by floating context menu (workaround-ish)
+ currentlySelectedFile = holder.getAdapterPosition();
+ //must return false so the click is not consumed
+ return false;
+ }
+ });
+
+ holder.filenameView.setText(file.getName());
+ switch (FileUtilities.getType(file.getName())) {
+ case FileUtilities.DOC:
+ holder.iconView.setImageResource(R.drawable.writer);
+ break;
+ case FileUtilities.CALC:
+ holder.iconView.setImageResource(R.drawable.calc);
+ break;
+ case FileUtilities.DRAWING:
+ holder.iconView.setImageResource(R.drawable.draw);
+ break;
+ case FileUtilities.IMPRESS:
+ holder.iconView.setImageResource(R.drawable.impress);
+ break;
+ }
+
+ if (file.isDirectory()) {
+ //Eventually have thumbnails of each sub file on a black circle
+ //For now just a folder icon
+ holder.iconView.setImageResource(R.drawable.ic_folder_black_24dp);
+ holder.iconView.setColorFilter(ContextCompat.getColor(mActivity, R.color.text_color_secondary));
+ }
+
+ // Date and Size field only exist when we are displaying items in a list.
+ if(isViewModeList()) {
+ if (!file.isDirectory()) {
+ String size;
+ long length = filePaths.get(position).getSize();
+ if (length < KB) {
+ size = Long.toString(length) + "B";
+ } else if (length < MB) {
+ size = Long.toString(length / KB) + "KB";
+ } else {
+ size = Long.toString(length / MB) + "MB";
+ }
+ holder.fileSizeView.setText(size);
+ }
+ SimpleDateFormat df = new SimpleDateFormat("dd MMM yyyy hh:ss");
+ Date date = file.getLastModified();
+ //TODO format date
+ holder.fileDateView.setText(df.format(date));
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return filePaths.size();
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+
+ View itemView;
+ TextView filenameView, fileSizeView, fileDateView;
+ ImageView iconView;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ this.itemView = itemView;
+ filenameView = itemView.findViewById(R.id.file_item_name);
+ iconView = itemView.findViewById(R.id.file_item_icon);
+ // Check if view mode is List, only then initialise Size and Date field
+ if (isViewModeList()) {
+ fileSizeView = itemView.findViewById(R.id.file_item_size);
+ fileDateView = itemView.findViewById(R.id.file_item_date);
+ }
+ }
+ }
+ }
+
+ private void setEditFABVisibility(final int visibility){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ editFAB.setVisibility(visibility);
+ }
+ });
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch(requestCode){
+ case PERMISSION_WRITE_EXTERNAL_STORAGE:
+ if(permissions.length>0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
+ switchToDocumentProvider(documentProviderFactory.getDefaultProvider());
+ setEditFABVisibility(View.VISIBLE);
+ } else {
+ setEditFABVisibility(View.INVISIBLE);
+ }
+ break;
+ default:
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+}
+
+/* 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 000000000..1d32a7de7
--- /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 String tag = "PageView";
+
+ public PageView(Context context ) {
+ super(context);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);
+ intialise();
+ }
+ public PageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);
+ Log.d( tag , bmp.toString());
+ intialise();
+ }
+ public PageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);//load a "page"
+ intialise();
+ }
+
+ private void intialise(){
+ mPaintBlack = new Paint();
+ mPaintBlack.setARGB(255, 0, 0, 0);
+ Log.d(tag, " Doing some set-up");
+ }
+
+ public void setBitmap(Bitmap bmp){
+ this.bmp = bmp;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Log.d(tag, "Draw");
+ Log.d(tag, 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/RecentFilesAdapter.java b/android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java
new file mode 100644
index 000000000..fc16d06a4
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java
@@ -0,0 +1,95 @@
+/* -*- 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.support.v4.content.ContextCompat;
+import android.support.v7.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 org.libreoffice.storage.IFile;
+
+import java.util.List;
+
+class RecentFilesAdapter extends RecyclerView.Adapter<RecentFilesAdapter.ViewHolder> {
+
+ private LibreOfficeUIActivity mActivity;
+ private List<IFile> recentFiles;
+
+ RecentFilesAdapter(LibreOfficeUIActivity activity, List<IFile> 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 IFile iFile = recentFiles.get(position);
+
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mActivity.open(iFile);
+ }
+ });
+
+ String filename = iFile.getName();
+
+ 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 000000000..d0cd3d48a
--- /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 000000000..29f50ebf4
--- /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 000000000..f1672ba3d
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/ZoomConstraints.java
@@ -0,0 +1,42 @@
+/* -*- 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 boolean mAllowZoom;
+ private final boolean mAllowDoubleTapZoom;
+ private final float mDefaultZoom;
+ private final float mMinZoom;
+ private final float mMaxZoom;
+
+ public ZoomConstraints(boolean allowZoom, float defaultZoom, float minZoom, float maxZoom) {
+ mAllowZoom = allowZoom;
+ mAllowDoubleTapZoom = allowZoom;
+ mDefaultZoom = defaultZoom;
+ mMinZoom = minZoom;
+ mMaxZoom = maxZoom;
+ }
+
+ public final boolean getAllowZoom() {
+ return mAllowZoom;
+ }
+
+ public final boolean getAllowDoubleTapZoom() {
+ return mAllowDoubleTapZoom;
+ }
+
+ 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 000000000..d4a7ac2ce
--- /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 000000000..a616fcc4d
--- /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 000000000..078aa41ba
--- /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 000000000..5a18a4bb1
--- /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 000000000..e0db6530d
--- /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 000000000..bdef70221
--- /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 000000000..d98efa2d5
--- /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 000000000..f622c44ff
--- /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 000000000..ea95c032e
--- /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 000000000..e86494c20
--- /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 000000000..7b1837311
--- /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 000000000..e296f4760
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/GLController.java
@@ -0,0 +1,275 @@
+/* -*- 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 boolean mSurfaceValid;
+ private int mWidth, mHeight;
+
+ private EGL10 mEGL;
+ private EGLDisplay mEGLDisplay;
+ private EGLConfig mEGLConfig;
+ private EGLContext mEGLContext;
+ private EGLSurface mEGLSurface;
+
+ private GL mGL;
+
+ 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;
+ mSurfaceValid = false;
+ }
+
+ 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());
+ }
+
+ mGL = null;
+ mEGLContext = null;
+ }
+ }
+
+ public GL getGL() { return 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 boolean checkForLostContext() {
+ if (mEGL.eglGetError() != EGL11.EGL_CONTEXT_LOST) {
+ return false;
+ }
+
+ mEGLDisplay = null;
+ mEGLConfig = null;
+ mEGLContext = null;
+ mEGLSurface = null;
+ mGL = null;
+ return true;
+ }
+
+ // This function is invoked by JNI
+ public synchronized void resumeCompositorIfValid() {
+ if (mSurfaceValid) {
+ mView.getListener().compositionResumeRequested(mWidth, mHeight);
+ }
+ }
+
+ // Wait until we are allowed to use EGL functions on the Surface backing
+ // this window. This function is invoked by JNI
+ public synchronized void waitForValidSurface() {
+ while (!mSurfaceValid) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public synchronized int getWidth() {
+ return mWidth;
+ }
+
+ public synchronized int getHeight() {
+ return mHeight;
+ }
+
+ synchronized void surfaceDestroyed() {
+ mSurfaceValid = false;
+ notifyAll();
+ }
+
+ synchronized void surfaceChanged(int newWidth, int newHeight) {
+ mWidth = newWidth;
+ mHeight = newHeight;
+ mSurfaceValid = true;
+ 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());
+ }
+
+ mGL = mEGLContext.getGL();
+
+ if (mView.getRenderer() != null) {
+ mView.getRenderer().onSurfaceCreated((GL10)mGL, mEGLConfig);
+ mView.getRenderer().onSurfaceChanged((GL10)mGL, 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;
+ }
+ }
+
+ throw new GLControllerException("No suitable EGL configuration found");
+ }
+
+ 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());
+ }
+
+ mGL = mEGLContext.getGL();
+
+ if (mView.getRenderer() != null) {
+ mView.getRenderer().onSurfaceCreated((GL10)mGL, mEGLConfig);
+ mView.getRenderer().onSurfaceChanged((GL10)mGL, mView.getWidth(), mView.getHeight());
+ }
+ }
+
+ /**
+ * Provides an EGLSurface without assuming ownership of this surface.
+ * This class does not keep a reference to the provided EGL surface; the
+ * caller assumes ownership of the surface once it is returned.
+ */
+ private EGLSurface provideEGLSurface() {
+ if (mEGL == null) {
+ initEGL();
+ }
+
+ Object window = mView.getNativeWindow();
+ EGLSurface surface = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, window, null);
+ if (surface == null || surface == EGL10.EGL_NO_SURFACE) {
+ throw new GLControllerException("EGL window surface could not be created! " +
+ getEGLError());
+ }
+
+ return surface;
+ }
+
+ 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 000000000..681fb6fd6
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -0,0 +1,354 @@
+/* -*- 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();
+ 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) {
+ mViewportMetrics = mViewportMetrics.setViewportSize(size.width, size.height);
+ sendResizeEventIfNecessary();
+ }
+
+ PanZoomController getPanZoomController() {
+ return mPanZoomController;
+ }
+
+ /* Informs Gecko that the screen size has changed. */
+ private void sendResizeEventIfNecessary() {
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+ IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
+
+ if (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();
+ 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 000000000..f90580fbe
--- /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 000000000..d460c19e1
--- /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 000000000..b0741d2f6
--- /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 000000000..db2fcc03c
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
@@ -0,0 +1,1101 @@
+/* -*- 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;
+
+/*
+ * 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 (Build.VERSION.SDK_INT <= 11) {
+ return false;
+ }
+
+ switch (event.getSource() & InputDevice.SOURCE_CLASS_MASK) {
+ case InputDevice.SOURCE_CLASS_POINTER:
+ switch (event.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_SCROLL: return handlePointerScroll(event);
+ }
+ break;
+ }
+ 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.sqrt(dx * dx + dy * 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) Math.sqrt(xvel * xvel + yvel * 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();
+
+ if (!constraints.getAllowZoom()) {
+ // If allowZoom is false, clamp to the default zoom level.
+ maxZoomFactor = minZoomFactor = constraints.getDefaultZoom();
+ }
+
+ 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() || !mTarget.getZoomConstraints().getAllowZoom())
+ 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) {
+ if (mTarget.getZoomConstraints() != null)
+ mWaitForDoubleTap = mTarget.getZoomConstraints().getAllowDoubleTapZoom();
+ else
+ mWaitForDoubleTap = false;
+ 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() || !mTarget.getZoomConstraints().getAllowDoubleTapZoom()) {
+ 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().getMinZoom() : 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 000000000..b7fee29fc
--- /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 000000000..b1aea3616
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java
@@ -0,0 +1,520 @@
+/* -*- 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 int mBackgroundColor;
+
+ private CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>();
+
+ /* Used by robocop for testing purposes */
+ private IntBuffer mPixelBuffer;
+
+ // 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();
+ }
+
+ 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);
+
+ // TODO: Move these calls into a separate deactivate() call that is called after the
+ // underlay and overlay are rendered.
+ }
+
+ // 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.
+ */
+ public void onDrawFrame(GL10 gl) {
+ Frame frame = createFrame(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);
+ }
+
+ 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;
+ }
+
+ public Frame createFrame(ImmutableViewportMetrics metrics) {
+ return new Frame(metrics);
+ }
+
+ 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 {
+ // The timestamp recording the start of this frame.
+ private long mFrameStartTime;
+ // 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;
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ public void beginDrawing() {
+ mFrameStartTime = SystemClock.uptimeMillis();
+
+ 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
+ }
+
+ /** Retrieves the bounds for the layer, rounded in such a way that it
+ * can be used as a mask for something that will render underneath it.
+ * This will round the bounds inwards, but stretch the mask towards any
+ * near page edge, where near is considered to be 'within 2 pixels'.
+ * Returns null if the given layer is null.
+ */
+ private Rect getMaskForLayer(Layer layer) {
+ if (layer == null) {
+ return null;
+ }
+
+ RectF bounds = RectUtils.contract(layer.getBounds(mPageContext), 1.0f, 1.0f);
+ Rect mask = RectUtils.roundIn(bounds);
+
+ // If the mask is within two pixels of any page edge, stretch it over
+ // that edge. This is to avoid drawing thin slivers when masking
+ // layers.
+ if (mask.top <= 2) {
+ mask.top = -1;
+ }
+ if (mask.left <= 2) {
+ mask.left = -1;
+ }
+
+ // Because we're drawing relative to the page-rect, we only need to
+ // take into account its width and height (and not its origin)
+ int pageRight = mPageRect.width();
+ int pageBottom = mPageRect.height();
+
+ if (mask.right >= pageRight - 2) {
+ mask.right = pageRight + 1;
+ }
+ if (mask.bottom >= pageBottom - 2) {
+ mask.bottom = pageBottom + 1;
+ }
+
+ return mask;
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ public void drawBackground() {
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+
+ /* Update background color. */
+ mBackgroundColor = 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(((mBackgroundColor>>16)&0xFF) / 255.0f,
+ ((mBackgroundColor>>8)&0xFF) / 255.0f,
+ (mBackgroundColor&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);
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ 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);
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ public void endDrawing() {
+ // If a layer update requires further work, schedule another redraw
+ if (!mUpdated)
+ mView.requestRender();
+
+ /* Used by robocop for testing purposes */
+ IntBuffer pixelBuffer = mPixelBuffer;
+ if (mUpdated && pixelBuffer != null) {
+ synchronized (pixelBuffer) {
+ pixelBuffer.position(0);
+ GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(),
+ (int)mScreenContext.viewport.height(), GLES20.GL_RGBA,
+ GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
+ pixelBuffer.notify();
+ }
+ }
+ }
+ }
+}
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 000000000..05f211811
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/LayerView.java
@@ -0,0 +1,449 @@
+/* -*- 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.graphics.SurfaceTexture;
+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.TextureView;
+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.
+ *
+ * Note that LayerView is accessed by Robocop via reflection.
+ */
+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;
+
+ /* Must be a PAINT_xxx constant */
+ private int mPaintState = PAINT_NONE;
+ private boolean mFullScreen = false;
+
+ private SurfaceView mSurfaceView;
+ private TextureView mTextureView;
+
+ private Listener mListener;
+ private OnInterceptTouchListener mTouchIntercepter;
+ //TODO static because of registerCxxCompositor() function, should be fixed in the future
+ private static LibreOfficeMainActivity mContext;
+
+ /* Flags used to determine when to show the painted surface. The integer
+ * order must correspond to the order in which these states occur. */
+ public static final int PAINT_NONE = 0;
+ public static final int PAINT_BEFORE_FIRST = 1;
+ public static final int PAINT_AFTER_FIRST = 2;
+
+ boolean shouldUseTextureView() {
+ // we can only use TextureView on ICS or higher
+ /*if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ Log.i(LOGTAG, "Not using TextureView: not on ICS+");
+ return false;
+ }
+
+ try {
+ // and then we can only use it if we have a hardware accelerated window
+ Method m = View.class.getMethod("isHardwareAccelerated", new Class[0]);
+ return (Boolean) m.invoke(this);
+ } catch (Exception e) {
+ Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString());
+ return false;
+ }*/
+ return false;
+ }
+
+ public LayerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = (LibreOfficeMainActivity) context;
+
+ if (shouldUseTextureView()) {
+ mTextureView = new TextureView(context);
+ mTextureView.setSurfaceTextureListener(new SurfaceTextureListener());
+
+ addView(mTextureView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ } else {
+ 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 void setViewportSize(IntSize size) {
+ mLayerClient.setViewportSize(new FloatSize(size));
+ }
+
+ 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 boolean isIMEEnabled() {
+ /*if (mInputConnectionHandler != null) {
+ return mInputConnectionHandler.isIMEEnabled();
+ }*/
+ return false;
+ }
+
+ 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;
+ }
+
+ /* paintState must be a PAINT_xxx constant. The state will only be changed
+ * if paintState represents a state that occurs after the current state. */
+ public void setPaintState(int paintState) {
+ if (paintState > mPaintState) {
+ Log.d(LOGTAG, "LayerView paint state set to " + paintState);
+ mPaintState = paintState;
+ }
+ }
+
+ public int getPaintState() {
+ return mPaintState;
+ }
+
+ 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));
+
+ 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() {
+ if (mSurfaceView != null)
+ return mSurfaceView.getHolder();
+
+ return mTextureView.getSurfaceTexture();
+ }
+
+ /** This function is invoked by Gecko (compositor thread) via JNI; be careful when modifying signature. */
+ public static GLController registerCxxCompositor() {
+ try {
+ LayerView layerView = mContext.getLayerClient().getView();
+ layerView.mListener.compositorCreated();
+ return layerView.getGLController();
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Error registering compositor!", e);
+ return null;
+ }
+ }
+
+ public interface Listener {
+ void compositorCreated();
+ void renderRequested();
+ void compositionPauseRequested();
+ void compositionResumeRequested(int width, int height);
+ 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) {
+ setViewportSize(new IntSize(right - left, bottom - top));
+ }
+ }
+
+ private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ // We don't do this for surfaceCreated above because it is always followed by a surfaceChanged,
+ // but that is not the case here.
+ if (mRenderControllerThread != null) {
+ mRenderControllerThread.surfaceCreated();
+ }
+ onSizeChanged(width, height);
+ }
+
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ onDestroyed();
+ return true; // allow Android to call release() on the SurfaceTexture, we are done drawing to it
+ }
+
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ onSizeChanged(width, height);
+ }
+
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ }
+ }
+
+ 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);
+ }
+ }
+
+ public void setFullScreen(boolean fullScreen) {
+ mFullScreen = fullScreen;
+ }
+
+ public boolean isFullScreen() {
+ return mFullScreen;
+ }
+}
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 000000000..99f203961
--- /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 000000000..ebcd641f2
--- /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 000000000..88e1b216c
--- /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 000000000..4eb07a31f
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/PointUtils.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.Point;
+import android.graphics.PointF;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+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)Math.sqrt(point.x * point.x + point.y * 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 000000000..e7fa540a3
--- /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 000000000..06f82f158
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java
@@ -0,0 +1,148 @@
+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 compositionResumeRequested(int width, int height) {
+
+ }
+
+ @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((GL10) controller.getGL());
+ }
+ controller.swapBuffers();
+ }
+
+ private void doSizeChanged() {
+ GLSurfaceView.Renderer renderer = getRenderer();
+ if (renderer != null) {
+ renderer.onSurfaceChanged((GL10) 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 000000000..7ef8ff020
--- /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 000000000..1d901a02a
--- /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 = "GeckoSimpleScaleGestureDetector";
+
+ 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 000000000..0bc271678
--- /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 000000000..42750df62
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/SubTile.java
@@ -0,0 +1,256 @@
+/* -*- 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(false);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void cleanTexture(boolean immediately) {
+ if (mTextureIDs != null) {
+ TextureReaper.get().add(mTextureIDs);
+ mTextureIDs = null;
+ if (immediately) {
+ TextureReaper.get().reap();
+ }
+ }
+ }
+
+ public void destroy() {
+ try {
+ destroyImage();
+ cleanTexture(false);
+ } 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(true);
+ }
+ }
+
+ @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);
+ }
+ }
+} \ No newline at end of file
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 000000000..5a752e3c7
--- /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 000000000..023433a88
--- /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 000000000..bccd8968c
--- /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 000000000..1a8a50459
--- /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 000000000..3d0ff1fed
--- /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 000000000..1c227de20
--- /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 000000000..f8b5c2e05
--- /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 000000000..a48266c57
--- /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));
+ }
+}