summaryrefslogtreecommitdiffstats
path: root/android/source/src/java/org/libreoffice
diff options
context:
space:
mode:
Diffstat (limited to 'android/source/src/java/org/libreoffice')
-rw-r--r--android/source/src/java/org/libreoffice/AboutDialogFragment.java108
-rw-r--r--android/source/src/java/org/libreoffice/ColorPaletteAdapter.java135
-rw-r--r--android/source/src/java/org/libreoffice/ColorPaletteListener.java6
-rw-r--r--android/source/src/java/org/libreoffice/ColorPickerAdapter.java162
-rw-r--r--android/source/src/java/org/libreoffice/DocumentPartView.java21
-rw-r--r--android/source/src/java/org/libreoffice/DocumentPartViewListAdapter.java50
-rw-r--r--android/source/src/java/org/libreoffice/FontController.java449
-rw-r--r--android/source/src/java/org/libreoffice/FormattingController.java496
-rw-r--r--android/source/src/java/org/libreoffice/InvalidationHandler.java768
-rw-r--r--android/source/src/java/org/libreoffice/LOEvent.java177
-rw-r--r--android/source/src/java/org/libreoffice/LOKitInputConnectionHandler.java74
-rw-r--r--android/source/src/java/org/libreoffice/LOKitShell.java169
-rw-r--r--android/source/src/java/org/libreoffice/LOKitThread.java447
-rw-r--r--android/source/src/java/org/libreoffice/LOKitTileProvider.java820
-rw-r--r--android/source/src/java/org/libreoffice/LibreOfficeApplication.java33
-rw-r--r--android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java1121
-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.java177
-rw-r--r--android/source/src/java/org/libreoffice/SearchController.java82
-rw-r--r--android/source/src/java/org/libreoffice/SettingsActivity.java63
-rw-r--r--android/source/src/java/org/libreoffice/SettingsListenerModel.java56
-rw-r--r--android/source/src/java/org/libreoffice/ThumbnailCreator.java121
-rw-r--r--android/source/src/java/org/libreoffice/TileIdentifier.java92
-rw-r--r--android/source/src/java/org/libreoffice/TileProvider.java205
-rw-r--r--android/source/src/java/org/libreoffice/TileProviderFactory.java31
-rw-r--r--android/source/src/java/org/libreoffice/ToolbarController.java274
-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.java278
-rw-r--r--android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java271
-rw-r--r--android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java552
-rw-r--r--android/source/src/java/org/libreoffice/ui/FileUtilities.java153
-rw-r--r--android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java473
-rw-r--r--android/source/src/java/org/libreoffice/ui/PageView.java69
-rw-r--r--android/source/src/java/org/libreoffice/ui/RecentFile.java25
-rw-r--r--android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java93
55 files changed, 9833 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..9695d1e9d
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/AboutDialogFragment.java
@@ -0,0 +1,108 @@
+/*
+ *
+ * * This file is part of the LibreOffice project.
+ * * This Source Code Form is subject to the terms of the Mozilla Public
+ * * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ */
+
+package org.libreoffice;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.view.View;
+import android.widget.TextView;
+
+public class AboutDialogFragment extends DialogFragment {
+
+ @NonNull @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+
+ @SuppressLint("InflateParams") //suppressed because the view will be placed in a dialog
+ View messageView = getActivity().getLayoutInflater().inflate(R.layout.about, null, false);
+
+ // When linking text, force to always use default color. This works
+ // around a pressed color state bug.
+ TextView textView = messageView.findViewById(R.id.about_credits);
+ int defaultColor = textView.getTextColors().getDefaultColor();
+ textView.setTextColor(defaultColor);
+
+ // Take care of placeholders 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(R.raw.license);
+ dialog.dismiss();
+ }
+ })
+ .setPositiveButton(R.string.about_notice, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ loadFromAbout(R.raw.notice);
+ dialog.dismiss();
+ }
+ })
+ .setNeutralButton(R.string.about_moreinfo, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ loadFromAbout(R.raw.example);
+ dialog.dismiss();
+ }
+ });
+
+ return builder.create();
+ }
+
+ private void loadFromAbout(int resourceId) {
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("android.resource://" + BuildConfig.APPLICATION_ID + "/" + resourceId));
+ String packageName = getActivity().getApplicationContext().getPackageName();
+ ComponentName componentName = new ComponentName(packageName, LibreOfficeMainActivity.class.getName());
+ i.setComponent(componentName);
+ getActivity().startActivity(i);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/ColorPaletteAdapter.java b/android/source/src/java/org/libreoffice/ColorPaletteAdapter.java
new file mode 100644
index 000000000..16d8a9778
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ColorPaletteAdapter.java
@@ -0,0 +1,135 @@
+package org.libreoffice;
+
+import android.content.Context;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageButton;
+
+
+public class ColorPaletteAdapter extends RecyclerView.Adapter<ColorPaletteAdapter.ColorPaletteViewHolder> {
+
+ private int[][] color_palette;
+ private final Context mContext;
+ private int upperSelectedBox = -1;
+ private int selectedBox = 0;
+ private boolean animate;
+ private final ColorPaletteListener colorPaletteListener;
+
+ public ColorPaletteAdapter(Context mContext, ColorPaletteListener colorPaletteListener) {
+ this.mContext = mContext;
+ this.color_palette = new int[11][8];
+ this.colorPaletteListener = colorPaletteListener;
+ }
+
+ @Override
+ public ColorPaletteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View item = LayoutInflater.from(mContext).inflate(R.layout.colorbox, parent, false);
+ return new ColorPaletteViewHolder(item);
+ }
+
+
+ public int getSelectedBox() {
+ return selectedBox;
+ }
+
+ public int getUpperSelectedBox() {
+ return upperSelectedBox;
+ }
+
+ @Override
+ public void onBindViewHolder(final ColorPaletteViewHolder holder, int position) {
+
+ holder.colorBox.setBackgroundColor(color_palette[upperSelectedBox][position]);
+ if (selectedBox == position) {
+ holder.colorBox.setImageResource(R.drawable.ic_done_all_white_12dp);
+ } else {
+ holder.colorBox.setImageDrawable(null);
+ }
+
+ holder.colorBox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ setPosition(holder.getAdapterPosition());
+ }
+ });
+ if (animate) //it will only animate when the upper color box is selected
+ setAnimation(holder.colorBox);
+
+ }
+
+ private void setAnimation(View viewToAnimate) {
+ Animation animation = AnimationUtils.loadAnimation(mContext, android.R.anim.fade_in);
+ viewToAnimate.startAnimation(animation);
+ }
+
+ @Override
+ public int getItemCount() {
+ return color_palette[0].length;
+ }
+
+ private void setPosition(int position) {
+ this.selectedBox = position;
+ colorPaletteListener.applyColor(color_palette[upperSelectedBox][position]);
+ animate = false;
+ updateAdapter();
+ }
+
+ public void setPosition(int upperSelectedBox, int position) {
+ if (this.upperSelectedBox != upperSelectedBox) {
+ this.upperSelectedBox = upperSelectedBox;
+ this.selectedBox = position;
+ colorPaletteListener.applyColor(color_palette[upperSelectedBox][position]);
+ animate = true;
+ updateAdapter();
+ }
+ }
+
+ /*
+ this is for InvalidationHandler when .uno:FontColor is captured
+ */
+ public void changePosition(int upperSelectedBox, int position) {
+ if(this.upperSelectedBox != upperSelectedBox){
+ this.upperSelectedBox = upperSelectedBox;
+ animate=true;
+ }
+
+ this.selectedBox = position;
+
+ updateAdapter();
+
+ }
+
+ public void setColorPalette(int[][] color_palette) {
+ this.color_palette = color_palette;
+ this.upperSelectedBox = 0;
+ this.selectedBox = 0;
+ }
+
+ private void updateAdapter(){
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ ColorPaletteAdapter.this.notifyDataSetChanged();
+ }
+ });
+ }
+
+
+ class ColorPaletteViewHolder extends RecyclerView.ViewHolder {
+
+ ImageButton colorBox;
+
+ public ColorPaletteViewHolder(View itemView) {
+ super(itemView);
+ colorBox = itemView.findViewById(R.id.fontColorBox);
+ }
+ }
+
+
+}
diff --git a/android/source/src/java/org/libreoffice/ColorPaletteListener.java b/android/source/src/java/org/libreoffice/ColorPaletteListener.java
new file mode 100644
index 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..a17dd264f
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ColorPickerAdapter.java
@@ -0,0 +1,162 @@
+package org.libreoffice;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+
+public class ColorPickerAdapter extends RecyclerView.Adapter<ColorPickerAdapter.ColorPickerViewHolder> {
+
+ private final Context mContext;
+ private final ColorPaletteAdapter colorPaletteAdapter;
+ private final ColorPaletteListener colorPaletteListener;
+ private final int[] colorList;
+ private final int[][] colorPalette = new int[11][8];
+
+ public ColorPickerAdapter(Context mContext, final ColorPaletteAdapter colorPaletteAdapter, ColorPaletteListener colorPaletteListener) {
+ this.mContext = mContext;
+ this.colorPaletteAdapter = colorPaletteAdapter;
+ this.colorPaletteListener = colorPaletteListener;
+ Resources r = mContext.getResources();
+ this.colorList = r.getIntArray(R.array.fontcolors);
+ initializeColorPalette();
+
+
+ }
+
+ @Override
+ public ColorPickerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View item = LayoutInflater.from(mContext).inflate(R.layout.colorbox, parent, false);
+ return new ColorPickerViewHolder(item);
+
+ }
+
+ @Override
+ public void onBindViewHolder(final ColorPickerViewHolder holder, int position) {
+ holder.colorBox.setBackgroundColor(colorList[position]);
+
+ if (colorPaletteAdapter.getUpperSelectedBox() == position
+ && colorPaletteAdapter.getSelectedBox() >= 0) {
+ holder.colorBox.setImageResource(R.drawable.ic_done_white_12dp);
+ } else {
+ holder.colorBox.setImageDrawable(null);
+ }
+
+ holder.colorBox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ setPosition(holder.getAdapterPosition());
+ colorPaletteListener.applyColor(colorList[holder.getAdapterPosition()]);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return colorList.length;
+ }
+
+
+ private void setPosition(int position) {
+ selectSubColor(position, position==0?0:3);
+ colorPaletteListener.applyColor(colorList[position]);
+ updateAdapter();
+ }
+
+ /**
+ * Switches to first palette, but doesn't mark any color as selected.
+ * Use this if no color in the palette matches the actual one.
+ */
+ public void unselectColors() {
+ colorPaletteAdapter.changePosition(0, -1);
+ updateAdapter();
+ }
+
+ private void selectSubColor(int position1, int position2) {
+ colorPaletteAdapter.setPosition(position1, position2);
+ }
+
+ private void initializeColorPalette() {
+
+ for (int i = 0; i < 11; i++) {
+ int red = Color.red(colorList[i]);
+ int green = Color.green(colorList[i]);
+ int blue = Color.blue(colorList[i]);
+
+ int red_tint = red;
+ int green_tint = green;
+ int blue_tint = blue;
+
+ int red_shade = red;
+ int green_shade = green;
+ int blue_shade = blue;
+ if (i == 0) {
+ colorPalette[0][0] = colorList[i];
+ for (int k = 1; k < 7; k++) {
+ red_tint = (int) (red_tint + (255 - red_tint) * 0.25);
+ green_tint = (int) (green_tint + (255 - green_tint) * 0.25);
+ blue_tint = (int) (blue_tint + (255 - blue_tint) * 0.25);
+ colorPalette[i][k] = (Color.rgb(red_tint, green_tint, blue_tint));
+ }
+ } else {
+ colorPalette[i][3] = colorList[i];
+ for (int k = 2; k >= 0; k--) {
+ red_shade = (int) (red_shade * 0.75);
+ green_shade = (int) (green_shade * 0.75);
+ blue_shade = (int) (blue_shade * 0.75);
+ colorPalette[i][k] = (Color.rgb(red_shade, green_shade, blue_shade));
+ }
+ for (int k = 4; k < 7; k++) {
+ red_tint = (int) (red_tint + (255 - red_tint) * 0.45);
+ green_tint = (int) (green_tint + (255 - green_tint) * 0.45);
+ blue_tint = (int) (blue_tint + (255 - blue_tint) * 0.45);
+ colorPalette[i][k] = (Color.rgb(red_tint, green_tint, blue_tint));
+ }
+ }
+ colorPalette[i][7] = Color.WHITE; // last one is always white
+ }
+ colorPaletteAdapter.setColorPalette(colorPalette);
+ }
+
+ public void findSelectedTextColor(int color) {
+ // try to find and highlight the color in the existing palettes
+ for (int i = 0; i < 11; i++) {
+ for (int k = 0; k < 8; k++) {
+ if (colorPalette[i][k] == color) {
+ colorPaletteAdapter.changePosition(i, k);
+ updateAdapter();
+ return;
+ }
+ }
+ }
+
+ // no color in the palettes matched
+ unselectColors();
+ }
+
+ private void updateAdapter(){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ ColorPickerAdapter.this.notifyDataSetChanged();
+ }
+ });
+
+ }
+
+ class ColorPickerViewHolder extends RecyclerView.ViewHolder {
+
+ ImageButton colorBox;
+
+ public ColorPickerViewHolder(View itemView) {
+ super(itemView);
+ this.colorBox = itemView.findViewById(R.id.fontColorBox);
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/DocumentPartView.java b/android/source/src/java/org/libreoffice/DocumentPartView.java
new file mode 100644
index 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..a0ed871a4
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/DocumentPartViewListAdapter.java
@@ -0,0 +1,50 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+import android.app.Activity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.List;
+
+public class DocumentPartViewListAdapter extends ArrayAdapter<DocumentPartView> {
+
+ private final Activity activity;
+ private final ThumbnailCreator thumbnailCollector;
+
+ public DocumentPartViewListAdapter(Activity activity, int resource, List<DocumentPartView> objects) {
+ super(activity, resource, objects);
+ this.activity = activity;
+ this.thumbnailCollector = new ThumbnailCreator();
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ if (view == null) {
+ LayoutInflater layoutInflater = activity.getLayoutInflater();
+ view = layoutInflater.inflate(R.layout.document_part_list_layout, null);
+ }
+
+ DocumentPartView documentPartView = getItem(position);
+ TextView textView = view.findViewById(R.id.text);
+ textView.setText(documentPartView.partName);
+
+ ImageView imageView = view.findViewById(R.id.image);
+ thumbnailCollector.createThumbnail(position, imageView);
+
+ return view;
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/FontController.java b/android/source/src/java/org/libreoffice/FontController.java
new file mode 100644
index 000000000..e0161076a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/FontController.java
@@ -0,0 +1,449 @@
+package org.libreoffice;
+
+import android.graphics.Color;
+import android.graphics.Rect;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.Spinner;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+public class FontController implements AdapterView.OnItemSelectedListener {
+
+ /** -1 as value in ".uno:Color" et al. means "automatic color"/no color set. */
+ private static final int COLOR_AUTO = -1;
+
+ private boolean mFontNameSpinnerSet = false;
+ private boolean mFontSizeSpinnerSet = false;
+ private final LibreOfficeMainActivity mActivity;
+ private final ArrayList<String> mFontList = new ArrayList<>();
+ private final ArrayList<String> mFontSizes = new ArrayList<>();
+ private final HashMap<String, ArrayList<String>> mAllFontSizes = new HashMap<>();
+
+ private String mCurrentFontSelected = null;
+ private String mCurrentFontSizeSelected = null;
+
+ public FontController(LibreOfficeMainActivity activity) {
+ mActivity = activity;
+ }
+ private BottomSheetBehavior colorPickerBehavior;
+ private BottomSheetBehavior backColorPickerBehavior;
+ private BottomSheetBehavior toolBarBottomBehavior;
+ private ColorPickerAdapter colorPickerAdapter;
+ private ColorPickerAdapter backColorPickerAdapter;
+
+ final ColorPaletteListener colorPaletteListener = new ColorPaletteListener() {
+ @Override
+ public void applyColor(int color) {
+ sendFontColorChange(color, false);
+ }
+
+ @Override
+ public void updateColorPickerPosition(int color) {
+ if (colorPickerAdapter == null) {
+ return;
+ }
+ if (color == COLOR_AUTO) {
+ colorPickerAdapter.unselectColors();
+ changeFontColorBoxColor(Color.TRANSPARENT);
+ return;
+ }
+ final int colorWithAlpha = color | 0xFF000000;
+ colorPickerAdapter.findSelectedTextColor(colorWithAlpha);
+ changeFontColorBoxColor(colorWithAlpha);
+ }
+ };
+
+ final ColorPaletteListener backColorPaletteListener = new ColorPaletteListener() {
+ @Override
+ public void applyColor(int color) {
+ sendFontBackColorChange(color, false);
+ }
+
+ @Override
+ public void updateColorPickerPosition(int color) {
+ if (backColorPickerAdapter == null) {
+ return;
+ }
+ if (color == COLOR_AUTO) {
+ backColorPickerAdapter.unselectColors();
+ changeFontBackColorBoxColor(Color.TRANSPARENT);
+ return;
+ }
+ final int colorWithAlpha = color | 0xFF000000;
+ backColorPickerAdapter.findSelectedTextColor(colorWithAlpha);
+ changeFontBackColorBoxColor(colorWithAlpha);
+ }
+ };
+
+ private void changeFontColorBoxColor(final int color){
+ final ImageButton fontColorPickerButton = mActivity.findViewById(R.id.font_color_picker_button);
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ fontColorPickerButton.setBackgroundColor(color);
+ }
+ });
+ }
+
+ private void changeFontBackColorBoxColor(final int color){
+ final ImageButton fontBackColorPickerButton = mActivity.findViewById(R.id.font_back_color_picker_button);
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ fontBackColorPickerButton.setBackgroundColor(color);
+ }
+ });
+ }
+
+ private void sendFontChange(String fontName) {
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "string");
+ valueJson.put("value", fontName);
+ json.put("CharFontName.FamilyName", valueJson);
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:CharFontName", json.toString()));
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendFontSizeChange(String fontSize) {
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "float");
+ valueJson.put("value", fontSize);
+ json.put("FontHeight.Height", valueJson);
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:FontHeight", json.toString()));
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendFontColorChange(int color, boolean keepAlpha){
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "long");
+ valueJson.put("value", keepAlpha ? color : 0x00FFFFFF & color);
+ json.put("Color", valueJson);
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Color", json.toString()));
+ changeFontColorBoxColor(color);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /*
+ * 0x00FFFFFF & color operation removes the alpha which is FF,
+ * if we don't remove it, the color value becomes negative which is not recognized by LOK
+ */
+ private void sendFontBackColorChange(int color, boolean keepAlpha) {
+ try {
+ JSONObject json = new JSONObject();
+ JSONObject valueJson = new JSONObject();
+ valueJson.put("type", "long");
+ valueJson.put("value", keepAlpha ? color : 0x00FFFFFF & color);
+ if(mActivity.getTileProvider().isSpreadsheet()){
+ json.put("BackgroundColor", valueJson);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:BackgroundColor", json.toString()));
+ }else 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<>();
+ for (int i = 0; i < array.length(); i++) {
+ fontSizes.add(array.getString(i));
+ }
+ mAllFontSizes.put(key, fontSizes);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void setupFontViews() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ setupFontNameSpinner();
+ setupFontSizeSpinner();
+ setupColorPicker();
+ setupBackColorPicker();
+ }
+ });
+ }
+
+ private void setupFontNameSpinner() {
+ Spinner fontSpinner = mActivity.findViewById(R.id.font_name_spinner);
+ ArrayAdapter<String> dataAdapter = new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mFontList);
+ dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ fontSpinner.setAdapter(dataAdapter);
+ }
+
+ private void setupFontSizeSpinner() {
+ Spinner fontSizeSpinner = mActivity.findViewById(R.id.font_size_spinner);
+ ArrayAdapter<String> dataAdapter = new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mFontSizes);
+ dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ fontSizeSpinner.setAdapter(dataAdapter);
+ }
+
+ private void setupColorPicker(){
+ LinearLayout colorPickerLayout = mActivity.findViewById(R.id.toolbar_color_picker);
+
+ RecyclerView recyclerView = colorPickerLayout.findViewById(R.id.fontColorView);
+ GridLayoutManager gridLayoutManager = new GridLayoutManager(mActivity, 11, GridLayoutManager.VERTICAL, true);
+ recyclerView.setHasFixedSize(true);
+ recyclerView.setLayoutManager(gridLayoutManager);
+
+
+
+ RecyclerView recyclerView2 = colorPickerLayout.findViewById(R.id.fontColorViewSub);
+ GridLayoutManager gridLayoutManager2 = new GridLayoutManager(mActivity,4);
+ recyclerView2.setHasFixedSize(true);
+ recyclerView2.addItemDecoration(new RecyclerView.ItemDecoration() {
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.bottom = 3;
+ outRect.top = 3;
+ outRect.left = 3;
+ outRect.right = 3;
+ }
+ });
+ recyclerView2.setLayoutManager(gridLayoutManager2);
+
+ ColorPaletteAdapter colorPaletteAdapter = new ColorPaletteAdapter(mActivity, colorPaletteListener);
+ recyclerView2.setAdapter(colorPaletteAdapter);
+
+ this.colorPickerAdapter = new ColorPickerAdapter(mActivity, colorPaletteAdapter, colorPaletteListener);
+ recyclerView.setAdapter(colorPickerAdapter);
+ RelativeLayout fontColorPicker = mActivity.findViewById(R.id.font_color_picker);
+ ImageButton fontColorPickerButton = mActivity.findViewById(R.id.font_color_picker_button);
+ View.OnClickListener clickListener = new View.OnClickListener(){
+ @Override
+ public void onClick(View view) {
+ toolBarBottomBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ colorPickerBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ mActivity.findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ }
+ };
+ LinearLayout toolbarBottomLayout = mActivity.findViewById(R.id.toolbar_bottom);
+ colorPickerBehavior = BottomSheetBehavior.from(colorPickerLayout);
+ toolBarBottomBehavior = BottomSheetBehavior.from(toolbarBottomLayout);
+
+ ImageButton pickerGoBackButton = colorPickerLayout.findViewById(R.id.button_go_back_color_picker);
+ pickerGoBackButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toolBarBottomBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ colorPickerBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ }
+ });
+
+
+ fontColorPicker.setOnClickListener(clickListener);
+ fontColorPickerButton.setOnClickListener(clickListener);
+
+ final Button autoColorButton = colorPickerLayout.findViewById(R.id.button_auto_color);
+ autoColorButton.setOnClickListener(view -> {
+ sendFontColorChange(COLOR_AUTO, true);
+ });
+ }
+
+ private void setupBackColorPicker(){
+ LinearLayout backColorPickerLayout = mActivity.findViewById(R.id.toolbar_back_color_picker);
+
+ RecyclerView recyclerView = backColorPickerLayout.findViewById(R.id.fontColorView);
+ GridLayoutManager gridLayoutManager = new GridLayoutManager(mActivity, 11, GridLayoutManager.VERTICAL, true);
+ recyclerView.setHasFixedSize(true);
+ recyclerView.setLayoutManager(gridLayoutManager);
+
+
+
+ RecyclerView recyclerView2 = backColorPickerLayout.findViewById(R.id.fontColorViewSub);
+ GridLayoutManager gridLayoutManager2 = new GridLayoutManager(mActivity,4);
+ recyclerView2.setHasFixedSize(true);
+ recyclerView2.addItemDecoration(new RecyclerView.ItemDecoration() {
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.bottom = 3;
+ outRect.top = 3;
+ outRect.left = 3;
+ outRect.right = 3;
+ }
+ });
+ recyclerView2.setLayoutManager(gridLayoutManager2);
+
+ ColorPaletteAdapter colorPaletteAdapter = new ColorPaletteAdapter(mActivity, backColorPaletteListener);
+ recyclerView2.setAdapter(colorPaletteAdapter);
+
+ this.backColorPickerAdapter = new ColorPickerAdapter(mActivity, colorPaletteAdapter, backColorPaletteListener);
+ recyclerView.setAdapter(backColorPickerAdapter);
+ RelativeLayout fontColorPicker = mActivity.findViewById(R.id.font_back_color_picker);
+ ImageButton fontColorPickerButton = mActivity.findViewById(R.id.font_back_color_picker_button);
+ View.OnClickListener clickListener = new View.OnClickListener(){
+ @Override
+ public void onClick(View view) {
+ toolBarBottomBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ backColorPickerBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ mActivity.findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ }
+ };
+ LinearLayout toolbarBottomLayout = mActivity.findViewById(R.id.toolbar_bottom);
+ backColorPickerBehavior = BottomSheetBehavior.from(backColorPickerLayout);
+ toolBarBottomBehavior = BottomSheetBehavior.from(toolbarBottomLayout);
+
+ ImageButton pickerGoBackButton = backColorPickerLayout.findViewById(R.id.button_go_back_color_picker);
+ pickerGoBackButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toolBarBottomBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ backColorPickerBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ }
+ });
+
+
+ fontColorPicker.setOnClickListener(clickListener);
+ fontColorPickerButton.setOnClickListener(clickListener);
+
+ final Button autoColorButton = backColorPickerLayout.findViewById(R.id.button_auto_color);
+ autoColorButton.setOnClickListener(view -> {
+ sendFontBackColorChange(COLOR_AUTO, true);
+ });
+ }
+
+ public void selectFont(final String fontName) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ selectFontCurrentThread(fontName);
+ }
+ });
+ }
+
+ private void selectFontCurrentThread(String fontName) {
+ Spinner spinner = mActivity.findViewById(R.id.font_name_spinner);
+ if (!mFontNameSpinnerSet) {
+ spinner.setOnItemSelectedListener(this);
+ mFontNameSpinnerSet = true;
+ }
+
+ if (fontName.equals(mCurrentFontSelected))
+ return;
+
+ int position = mFontList.indexOf(fontName);
+ if (position != -1) {
+ mCurrentFontSelected = fontName;
+ spinner.setSelection(position,false);
+ }
+
+ resetFontSizes(fontName);
+ }
+
+ private void resetFontSizes(String fontName) {
+ if (mAllFontSizes.get(fontName) != null) {
+ mFontSizes.clear();
+ mFontSizes.addAll(mAllFontSizes.get(fontName));
+ Spinner spinner = mActivity.findViewById(R.id.font_size_spinner);
+ ArrayAdapter<?> arrayAdapter = (ArrayAdapter<?>)spinner.getAdapter();
+ arrayAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void selectFontSize(final String fontSize) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ selectFontSizeCurrentThread(fontSize);
+ }
+ });
+ }
+
+ private void selectFontSizeCurrentThread(String fontSize) {
+ Spinner spinner = mActivity.findViewById(R.id.font_size_spinner);
+ if (!mFontSizeSpinnerSet) {
+ spinner.setOnItemSelectedListener(this);
+ mFontSizeSpinnerSet = true;
+ }
+
+ if (fontSize.equals(mCurrentFontSizeSelected))
+ return;
+
+ int position = mFontSizes.indexOf(fontSize);
+ if (position != -1) {
+ mCurrentFontSizeSelected = fontSize;
+ spinner.setSelection(position, false);
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/FormattingController.java b/android/source/src/java/org/libreoffice/FormattingController.java
new file mode 100644
index 000000000..49cffabf7
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/FormattingController.java
@@ -0,0 +1,496 @@
+package org.libreoffice;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import com.google.android.material.snackbar.Snackbar;
+import androidx.core.content.FileProvider;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.kit.Document;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import static org.libreoffice.SearchController.addProperty;
+
+class FormattingController implements View.OnClickListener {
+ private static final String LOGTAG = ToolbarController.class.getSimpleName();
+ private static final int TAKE_PHOTO = 1;
+ private static final int SELECT_PHOTO = 2;
+ private static final int IMAGE_BUFFER_SIZE = 4 * 1024;
+
+ private final LibreOfficeMainActivity mContext;
+ private String mCurrentPhotoPath;
+
+ FormattingController(LibreOfficeMainActivity context) {
+ mContext = context;
+
+ mContext.findViewById(R.id.button_insertFormatListBullets).setOnClickListener(this);
+ mContext.findViewById(R.id.button_insertFormatListNumbering).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_bold).setOnClickListener(this);
+ mContext.findViewById(R.id.button_italic).setOnClickListener(this);
+ mContext.findViewById(R.id.button_strikethrough).setOnClickListener(this);
+ mContext.findViewById(R.id.button_underlined).setOnClickListener(this);
+ mContext.findViewById(R.id.button_clearformatting).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_align_left).setOnClickListener(this);
+ mContext.findViewById(R.id.button_align_center).setOnClickListener(this);
+ mContext.findViewById(R.id.button_align_right).setOnClickListener(this);
+ mContext.findViewById(R.id.button_align_justify).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_insert_line).setOnClickListener(this);
+ mContext.findViewById(R.id.button_insert_rect).setOnClickListener(this);
+ mContext.findViewById(R.id.button_insert_picture).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_insert_table).setOnClickListener(this);
+ mContext.findViewById(R.id.button_delete_table).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_font_shrink).setOnClickListener(this);
+ mContext.findViewById(R.id.button_font_grow).setOnClickListener(this);
+
+ mContext.findViewById(R.id.button_subscript).setOnClickListener(this);
+ mContext.findViewById(R.id.button_superscript).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ ImageButton button = (ImageButton) view;
+
+ if (button.isSelected()) {
+ button.getBackground().setState(new int[]{-android.R.attr.state_selected});
+ } else {
+ button.getBackground().setState(new int[]{android.R.attr.state_selected});
+ }
+
+ final int buttonId = button.getId();
+ if (buttonId == R.id.button_insertFormatListBullets) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DefaultBullet"));
+ } else if (buttonId == R.id.button_insertFormatListNumbering) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DefaultNumbering"));
+ } else if (buttonId == R.id.button_bold) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Bold"));
+ } else if (buttonId == R.id.button_italic) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Italic"));
+ } else if (buttonId == R.id.button_strikethrough) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Strikeout"));
+ } else if (buttonId == R.id.button_clearformatting) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:ResetAttributes"));
+ } else if (buttonId == R.id.button_underlined) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:UnderlineDouble"));
+ } else if (buttonId == R.id.button_align_left) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:LeftPara"));
+ } else if (buttonId == R.id.button_align_center) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:CenterPara"));
+ } else if (buttonId == R.id.button_align_right) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:RightPara"));
+ } else if (buttonId == R.id.button_align_justify) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:JustifyPara"));
+ } else if (buttonId == R.id.button_insert_line) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Line"));
+ } else if (buttonId == R.id.button_insert_rect) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Rect"));
+ } else if (buttonId == R.id.button_font_shrink) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Shrink"));
+ } else if (buttonId == R.id.button_font_grow) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Grow"));
+ } else if (buttonId == R.id.button_subscript) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SubScript"));
+ }else if (buttonId == R.id.button_superscript) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SuperScript"));
+ } else if (buttonId == R.id.button_insert_picture) {
+ insertPicture();
+ } else if (buttonId == R.id.button_insert_table) {
+ insertTable();
+ } else if (buttonId == R.id.button_delete_table) {
+ deleteTable();
+ }
+ }
+
+ void onToggleStateChanged(final int type, final boolean selected) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ Integer buttonId;
+ switch (type) {
+ case Document.BOLD:
+ buttonId = R.id.button_bold;
+ break;
+ case Document.ITALIC:
+ buttonId = R.id.button_italic;
+ break;
+ case Document.UNDERLINE:
+ buttonId = R.id.button_underlined;
+ break;
+ case Document.STRIKEOUT:
+ buttonId = R.id.button_strikethrough;
+ break;
+ case Document.ALIGN_LEFT:
+ buttonId = R.id.button_align_left;
+ break;
+ case Document.ALIGN_CENTER:
+ buttonId = R.id.button_align_center;
+ break;
+ case Document.ALIGN_RIGHT:
+ buttonId = R.id.button_align_right;
+ break;
+ case Document.ALIGN_JUSTIFY:
+ buttonId = R.id.button_align_justify;
+ break;
+ case Document.BULLET_LIST:
+ buttonId = R.id.button_insertFormatListBullets;
+ break;
+ case Document.NUMBERED_LIST:
+ buttonId = R.id.button_insertFormatListNumbering;
+ break;
+ default:
+ Log.e(LOGTAG, "Uncaptured state change type: " + type);
+ return;
+ }
+
+ ImageButton button = mContext.findViewById(buttonId);
+ button.setSelected(selected);
+ if (selected) {
+ button.getBackground().setState(new int[]{android.R.attr.state_selected});
+ } else {
+ button.getBackground().setState(new int[]{-android.R.attr.state_selected});
+ }
+ }
+ });
+ }
+
+ private void insertPicture() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ String[] options = {mContext.getResources().getString(R.string.take_photo),
+ mContext.getResources().getString(R.string.select_photo)};
+ builder.setItems(options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case 0:
+ dispatchTakePictureIntent();
+ break;
+ case 1:
+ sendImagePickingIntent();
+ break;
+ default:
+ sendImagePickingIntent();
+ }
+ }
+ });
+ builder.show();
+ }
+
+ private void insertTable() {
+ final AlertDialog.Builder insertTableBuilder = new AlertDialog.Builder(mContext);
+ insertTableBuilder.setTitle(R.string.insert_table);
+ LayoutInflater layoutInflater = mContext.getLayoutInflater();
+ View numberPicker = layoutInflater.inflate(R.layout.number_picker, null);
+ final int minValue = 1;
+ final int maxValue = 20;
+ TextView npRowPositive = numberPicker.findViewById(R.id.number_picker_rows_positive);
+ TextView npRowNegative = numberPicker.findViewById(R.id.number_picker_rows_negative);
+ TextView npColPositive = numberPicker.findViewById(R.id.number_picker_cols_positive);
+ TextView npColNegative = numberPicker.findViewById(R.id.number_picker_cols_negative);
+ final TextView npRowCount = numberPicker.findViewById(R.id.number_picker_row_count);
+ final TextView npColCount = numberPicker.findViewById(R.id.number_picker_col_count);
+
+ View.OnClickListener positiveButtonClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int rowCount = Integer.parseInt(npRowCount.getText().toString());
+ int colCount = Integer.parseInt(npColCount.getText().toString());
+ final int id = v.getId();
+ if (id == R.id.number_picker_rows_positive && rowCount < maxValue) {
+ npRowCount.setText(String.valueOf(++rowCount));
+ } else if (id == R.id.number_picker_cols_positive && colCount < maxValue) {
+ npColCount.setText(String.valueOf(++colCount));
+ }
+ }
+ };
+
+ View.OnClickListener negativeButtonClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int rowCount = Integer.parseInt(npRowCount.getText().toString());
+ int colCount = Integer.parseInt(npColCount.getText().toString());
+ final int id = v.getId();
+ if (id == R.id.number_picker_rows_negative && rowCount > minValue) {
+ npRowCount.setText(String.valueOf(--rowCount));
+ } else if (id == R.id.number_picker_cols_negative && colCount > minValue) {
+ npColCount.setText(String.valueOf(--colCount));
+ }
+ }
+ };
+
+ npRowPositive.setOnClickListener(positiveButtonClickListener);
+ npColPositive.setOnClickListener(positiveButtonClickListener);
+ npRowNegative.setOnClickListener(negativeButtonClickListener);
+ npColNegative.setOnClickListener(negativeButtonClickListener);
+
+ insertTableBuilder.setView(numberPicker);
+ insertTableBuilder.setNeutralButton(R.string.alert_cancel, null);
+ insertTableBuilder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ try {
+ JSONObject cols = new JSONObject();
+ cols.put("type", "long");
+ cols.put("value", Integer.valueOf(npColCount.getText().toString()));
+ JSONObject rows = new JSONObject();
+ rows.put("type","long");
+ rows.put("value",Integer.valueOf(npRowCount.getText().toString()));
+ JSONObject params = new JSONObject();
+ params.put("Columns", cols);
+ params.put("Rows", rows);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertTable",params.toString()));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ }
+ });
+
+ AlertDialog.Builder insertBuilder = new AlertDialog.Builder(mContext);
+ insertBuilder.setTitle(R.string.select_insert_options);
+ insertBuilder.setNeutralButton(R.string.alert_cancel, null);
+ final int[] selectedItem = new int[1];
+ insertBuilder.setSingleChoiceItems(mContext.getResources().getStringArray(R.array.insertrowscolumns), -1, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ selectedItem[0] = which;
+ }
+ });
+ insertBuilder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (selectedItem[0]){
+ case 0:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertRowsBefore"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 1:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertRowsAfter"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 2:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertColumnsBefore"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 3:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertColumnsAfter"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 4:
+ insertTableBuilder.show();
+ break;
+
+ }
+ }
+ });
+ insertBuilder.show();
+ }
+
+ private void deleteTable() {
+ AlertDialog.Builder deleteBuilder = new AlertDialog.Builder(mContext);
+ deleteBuilder.setTitle(R.string.select_delete_options);
+ deleteBuilder.setNeutralButton(R.string.alert_cancel,null);
+ final int[] selectedItem = new int[1];
+ deleteBuilder.setSingleChoiceItems(mContext.getResources().getStringArray(R.array.deleterowcolumns), -1, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ selectedItem[0] = which;
+ }
+ });
+ deleteBuilder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (selectedItem[0]){
+ case 0:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteRows"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 1:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteColumns"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ case 2:
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteTable"));
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ break;
+ }
+ }
+ });
+ deleteBuilder.show();
+ }
+
+ private void sendImagePickingIntent() {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType("image/*");
+ mContext.startActivityForResult(Intent.createChooser(intent,
+ mContext.getResources().getString(R.string.select_photo_title)), SELECT_PHOTO);
+ }
+
+ private void dispatchTakePictureIntent() {
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
+ Snackbar.make(mContext.findViewById(R.id.button_insert_picture),
+ mContext.getResources().getString(R.string.no_camera_found), Snackbar.LENGTH_SHORT).show();
+ return;
+ }
+ Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ // Ensure that there's a camera activity to handle the intent
+ if (takePictureIntent.resolveActivity(mContext.getPackageManager()) != null) {
+ // Create the File where the photo should go
+ File photoFile = null;
+ try {
+ photoFile = createImageFile();
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ // Continue only if the File was successfully created
+ if (photoFile != null) {
+ Uri photoURI = FileProvider.getUriForFile(mContext,
+ mContext.getPackageName() + ".fileprovider",
+ photoFile);
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
+ // Grant permissions to potential photo/camera apps (for some Android versions)
+ List<ResolveInfo> resInfoList = mContext.getPackageManager()
+ .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ for (ResolveInfo resolveInfo : resInfoList) {
+ String packageName = resolveInfo.activityInfo.packageName;
+ mContext.grantUriPermission(packageName, photoURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ mContext.startActivityForResult(takePictureIntent, TAKE_PHOTO);
+ }
+ }
+ }
+
+ void handleActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == TAKE_PHOTO && resultCode == Activity.RESULT_OK) {
+ compressAndInsertImage();
+ } else if (requestCode == SELECT_PHOTO && resultCode == Activity.RESULT_OK) {
+ getFileFromURI(data.getData());
+ compressAndInsertImage();
+ }
+ }
+
+ void compressAndInsertImage() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ String[] options = {mContext.getResources().getString(R.string.compress_photo_smallest_size),
+ mContext.getResources().getString(R.string.compress_photo_medium_size),
+ mContext.getResources().getString(R.string.compress_photo_max_quality),
+ mContext.getResources().getString(R.string.compress_photo_no_compress)};
+ builder.setTitle(mContext.getResources().getString(R.string.compress_photo_title));
+ builder.setItems(options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int compressGrade;
+ switch (which) {
+ case 0:
+ compressGrade = 0;
+ break;
+ case 1:
+ compressGrade = 50;
+ break;
+ case 2:
+ compressGrade = 100;
+ break;
+ case 3:
+ compressGrade = -1;
+ break;
+ default:
+ compressGrade = -1;
+ }
+ compressImage(compressGrade);
+ sendInsertGraphic();
+ }
+ });
+ builder.show();
+ }
+
+ private void getFileFromURI(Uri uri) {
+ try {
+ InputStream input = mContext.getContentResolver().openInputStream(uri);
+ mCurrentPhotoPath = createImageFile().getAbsolutePath();
+ FileOutputStream output = new FileOutputStream(mCurrentPhotoPath);
+ if (input != null) {
+ byte[] buffer = new byte[IMAGE_BUFFER_SIZE];
+ int read;
+ while ((read = input.read(buffer)) != -1) {
+ output.write(buffer, 0, read);
+ }
+ input.close();
+ }
+ output.flush();
+ output.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendInsertGraphic() {
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "FileName", "string", "file://" + mCurrentPhotoPath);
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertGraphic", rootJson.toString()));
+ LOKitShell.sendEvent(new LOEvent(LOEvent.REFRESH));
+ mContext.setDocumentChanged(true);
+ }
+
+ private void compressImage(int grade) {
+ if (grade < 0 || grade > 100) {
+ return;
+ }
+ mContext.showProgressSpinner();
+ Bitmap bmp = BitmapFactory.decodeFile(mCurrentPhotoPath);
+ try {
+ mCurrentPhotoPath = createImageFile().getAbsolutePath();
+ FileOutputStream out = new FileOutputStream(mCurrentPhotoPath);
+ bmp.compress(Bitmap.CompressFormat.JPEG, grade, out);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ mContext.hideProgressSpinner();
+ }
+
+ private File createImageFile() throws IOException {
+ // Create an image file name
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
+ String imageFileName = "JPEG_" + timeStamp + "_";
+ File storageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+ File image = File.createTempFile(
+ imageFileName, /* prefix */
+ ".jpg", /* suffix */
+ storageDir /* directory */
+ );
+ // Save a file: path for use with ACTION_VIEW intents
+ mCurrentPhotoPath = image.getAbsolutePath();
+ return image;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/InvalidationHandler.java b/android/source/src/java/org/libreoffice/InvalidationHandler.java
new file mode 100644
index 000000000..0f3f1dd7b
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/InvalidationHandler.java
@@ -0,0 +1,768 @@
+package org.libreoffice;
+
+import android.content.Intent;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.canvas.SelectionHandle;
+import org.libreoffice.kit.Document;
+import org.libreoffice.kit.Office;
+import org.libreoffice.overlay.DocumentOverlay;
+import org.mozilla.gecko.gfx.GeckoLayerClient;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses (interprets) and handles invalidation messages from LibreOffice.
+ */
+public class InvalidationHandler implements Document.MessageCallback, Office.MessageCallback {
+ private static final String LOGTAG = InvalidationHandler.class.getSimpleName();
+ private final DocumentOverlay mDocumentOverlay;
+ private final GeckoLayerClient mLayerClient;
+ private OverlayState mState;
+ private boolean mKeyEvent = false;
+ private final LibreOfficeMainActivity mContext;
+
+ private int currentTotalPageNumber = 0; // total page number of the current document
+
+ public InvalidationHandler(LibreOfficeMainActivity context) {
+ mContext = context;
+ mDocumentOverlay = mContext.getDocumentOverlay();
+ mLayerClient = mContext.getLayerClient();
+ mState = OverlayState.NONE;
+ }
+
+ /**
+ * Processes callback message
+ *
+ * @param messageID - ID of the message
+ * @param payload - additional invalidation message payload
+ */
+ @Override
+ public void messageRetrieved(int messageID, String payload) {
+ if (!LOKitShell.isEditingEnabled()) {
+ // enable handling of hyperlinks and search result even in the Viewer
+ if (messageID != Document.CALLBACK_INVALIDATE_TILES
+ && messageID != Document.CALLBACK_DOCUMENT_PASSWORD
+ && messageID != Document.CALLBACK_HYPERLINK_CLICKED
+ && messageID != Document.CALLBACK_SEARCH_RESULT_SELECTION
+ && messageID != Document.CALLBACK_SC_FOLLOW_JUMP
+ && messageID != Document.CALLBACK_TEXT_SELECTION
+ && messageID != Document.CALLBACK_TEXT_SELECTION_START
+ && messageID != Document.CALLBACK_TEXT_SELECTION_END)
+ return;
+ }
+ switch (messageID) {
+ case Document.CALLBACK_INVALIDATE_TILES:
+ invalidateTiles(payload);
+ break;
+ case Document.CALLBACK_UNO_COMMAND_RESULT:
+ unoCommandResult(payload);
+ break;
+ case Document.CALLBACK_INVALIDATE_VISIBLE_CURSOR:
+ invalidateCursor(payload);
+ break;
+ case Document.CALLBACK_TEXT_SELECTION:
+ textSelection(payload);
+ break;
+ case Document.CALLBACK_TEXT_SELECTION_START:
+ textSelectionStart(payload);
+ break;
+ case Document.CALLBACK_TEXT_SELECTION_END:
+ textSelectionEnd(payload);
+ break;
+ case Document.CALLBACK_CURSOR_VISIBLE:
+ cursorVisibility(payload);
+ break;
+ case Document.CALLBACK_GRAPHIC_SELECTION:
+ graphicSelection(payload);
+ break;
+ case Document.CALLBACK_HYPERLINK_CLICKED:
+ if (!payload.startsWith("http://") && !payload.startsWith("https://")) {
+ payload = "http://" + payload;
+ }
+ Intent urlIntent = new Intent(Intent.ACTION_VIEW);
+ urlIntent.setData(Uri.parse(payload));
+ mContext.startActivity(urlIntent);
+ break;
+ case Document.CALLBACK_STATE_CHANGED:
+ stateChanged(payload);
+ break;
+ case Document.CALLBACK_SEARCH_RESULT_SELECTION:
+ searchResultSelection(payload);
+ // when doing a search, CALLBACK_SEARCH_RESULT_SELECTION is called in addition
+ // to the CALLBACK_TEXT_SELECTION{,_START,_END} callbacks and the handling of
+ // the previous 3 makes the cursor shown in addition to the selection rectangle,
+ // so hide the cursor again to just show the selection rectangle for the search result
+ mDocumentOverlay.hideCursor();
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.MIDDLE);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.START);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.END);
+ break;
+ case Document.CALLBACK_SEARCH_NOT_FOUND:
+ Log.d(LOGTAG, "LOK_CALLBACK: Search not found.");
+ // this callback is never caught. Hope someone fix this.
+ break;
+ case Document.CALLBACK_CELL_CURSOR:
+ invalidateCellCursor(payload);
+ break;
+ case Document.CALLBACK_SC_FOLLOW_JUMP:
+ jumpToCell(payload);
+ break;
+ case Document.CALLBACK_INVALIDATE_HEADER:
+ invalidateHeader();
+ break;
+ case Document.CALLBACK_CELL_ADDRESS:
+ cellAddress(payload);
+ break;
+ case Document.CALLBACK_CELL_FORMULA:
+ cellFormula(payload);
+ break;
+ case Document.CALLBACK_DOCUMENT_PASSWORD:
+ documentPassword();
+ break;
+ case Document.CALLBACK_DOCUMENT_SIZE_CHANGED:
+ pageSizeChanged(payload);
+ default:
+
+ Log.d(LOGTAG, "LOK_CALLBACK uncaught: " + messageID + " : " + payload);
+ }
+ }
+
+ private void unoCommandResult(String payload) {
+ try {
+ JSONObject payloadObject = new JSONObject(payload);
+ if (payloadObject.getString("commandName").equals(".uno:Save")) {
+ if (payloadObject.getString("success").equals("true")) {
+ mContext.saveFileToOriginalSource();
+ }
+ }else if(payloadObject.getString("commandName").equals(".uno:Name") ||
+ payloadObject.getString("commandName").equals(".uno:RenamePage")){
+ //success returns false even though its true for some reason,
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.getTileProvider().resetParts();
+ mContext.getDocumentPartViewListAdapter().notifyDataSetChanged();
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ Toast.makeText(mContext, mContext.getString(R.string.part_name_changed), Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else if(payloadObject.getString("commandName").equals(".uno:Remove") ||
+ payloadObject.getString("commandName").equals(".uno:DeletePage") ) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.getTileProvider().resetParts();
+ mContext.getDocumentPartViewListAdapter().notifyDataSetChanged();
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ Toast.makeText(mContext, mContext.getString(R.string.part_deleted), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }catch(JSONException e){
+ e.printStackTrace();
+ }
+ }
+
+ private void cellFormula(final String payload) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ ((EditText)mContext.findViewById(R.id.calc_formula)).setText(payload);
+ }
+ });
+ }
+
+ private void cellAddress(final String payload) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ ((EditText)mContext.findViewById(R.id.calc_address)).setText(payload);
+ }
+ });
+ }
+
+ private void invalidateHeader() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_CALC_HEADERS));
+ }
+
+ private void documentPassword() {
+ mContext.setPasswordProtected(true);
+ mContext.promptForPassword();
+ synchronized (this) {
+ try {
+ this.wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ mContext.setPassword();
+ }
+
+ private void invalidateCellCursor(String payload) {
+ RectF cellCursorRect = convertPayloadToRectangle(payload);
+
+ if (cellCursorRect != null) {
+ mDocumentOverlay.showCellSelection(cellCursorRect);
+ moveViewportToMakeSelectionVisible(cellCursorRect);
+ }
+ }
+
+ private void jumpToCell(String payload) {
+ RectF cellCursorRect = convertPayloadCellToRectangle(payload);
+
+ if (cellCursorRect != null) {
+ moveViewportToMakeSelectionVisible(cellCursorRect);
+ }
+ }
+
+ /**
+ * Handles the search result selection message, which is a JSONObject
+ *
+ * @param payload
+ */
+ private void searchResultSelection(String payload) {
+ RectF selectionRectangle = null;
+ try {
+ JSONObject collectiveResult = new JSONObject(payload);
+ JSONArray searchResult = collectiveResult.getJSONArray("searchResultSelection");
+ if (searchResult.length() == 1) {
+ String rectangle = searchResult.getJSONObject(0).getString("rectangles");
+ selectionRectangle = convertPayloadToRectangle(rectangle);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ if (selectionRectangle != null) {
+ moveViewportToMakeSelectionVisible(selectionRectangle);
+ }
+ }
+
+ /**
+ * Move the viewport to show the selection. The selection will appear at the
+ * viewport position depending on where the selection is relative to the
+ * viewport (either selection is above, below, on left or right). The difference
+ * between this method and moveViewportToMakeCursorVisible() is that this method
+ * takes into account the width and height of the selection and zooms out
+ * accordingly.
+ *
+ * @param selectionRectangle - selection position on the document
+ */
+ public void moveViewportToMakeSelectionVisible(RectF selectionRectangle) {
+ RectF moveToRect = mLayerClient.getViewportMetrics().getCssViewport();
+ if (moveToRect.contains(selectionRectangle)) {
+ return;
+ }
+
+ float newLeft = moveToRect.left;
+ float newTop = moveToRect.top;
+
+ // if selection rectangle is wider or taller than current viewport, we need to zoom out
+ float oldZoom = mLayerClient.getViewportMetrics().getZoomFactor();
+ float widthRatio = 1f;
+ float heightRatio = 1f;
+ if (moveToRect.width() < selectionRectangle.width()) {
+ widthRatio = selectionRectangle.width() / moveToRect.width() / 0.85f; // 0.85f gives some margin (must < 0.9)
+ }
+ if (moveToRect.height() < selectionRectangle.height()) {
+ heightRatio = selectionRectangle.height() / moveToRect.height() / 0.45f; // 0.45f gives some margin (must < 0.5)
+ }
+ float newZoom = widthRatio > heightRatio ? oldZoom/widthRatio : oldZoom/heightRatio;
+
+ // if selection is out of viewport we need to adjust accordingly
+ if (selectionRectangle.right < moveToRect.left || selectionRectangle.left < moveToRect.left) {
+ newLeft = selectionRectangle.left - (moveToRect.width() * 0.1f) * oldZoom / newZoom; // 0.1f gives left margin
+ } else if (selectionRectangle.right > moveToRect.right || selectionRectangle.left > moveToRect.right) {
+ newLeft = selectionRectangle.right - (moveToRect.width() * 0.9f) * oldZoom / newZoom; // 0.9f gives right margin
+ }
+
+ if (selectionRectangle.top < moveToRect.top || selectionRectangle.bottom < moveToRect.top) {
+ newTop = selectionRectangle.top - (moveToRect.height() * 0.1f) * oldZoom / newZoom; // 0.1f gives top margin
+ } else if (selectionRectangle.bottom > moveToRect.bottom || selectionRectangle.top > moveToRect.bottom){
+ newTop = selectionRectangle.bottom - (moveToRect.height() * 0.5f) * oldZoom / newZoom; // 0.5 f gives bottom margin
+ }
+
+ LOKitShell.moveViewportTo(mContext, new PointF(newLeft, newTop), newZoom);
+ }
+
+ private void pageSizeChanged(String payload){
+ if(mContext.getTileProvider().isTextDocument()){
+ String[] bounds = payload.split(",");
+ int pageWidth = Integer.parseInt(bounds[0]);
+ int pageHeight = Integer.parseInt(bounds[1].trim());
+ LOKitShell.sendEvent(new LOEvent(LOEvent.PAGE_SIZE_CHANGED, pageWidth, pageHeight));
+ }
+ }
+
+ private void stateChanged(String payload) {
+ String[] parts = payload.split("=");
+ if (parts.length < 2) {
+ Log.e(LOGTAG, "LOK_CALLBACK_STATE_CHANGED unexpected payload: " + payload);
+ return;
+ }
+ final String value = parts[1];
+ boolean pressed = Boolean.parseBoolean(value);
+ if (!mContext.getTileProvider().isReady()) {
+ Log.w(LOGTAG, "tile provider not ready, ignoring payload "+payload);
+ return;
+ }
+ if (parts[0].equals(".uno:Bold")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.BOLD, pressed);
+ } else if (parts[0].equals(".uno:Italic")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ITALIC, pressed);
+ } else if (parts[0].equals(".uno:Underline")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.UNDERLINE, pressed);
+ } else if (parts[0].equals(".uno:Strikeout")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.STRIKEOUT, pressed);
+ } else if (parts[0].equals(".uno:CharFontName")) {
+ mContext.getFontController().selectFont(value);
+ } else if (parts[0].equals(".uno:FontHeight")) {
+ mContext.getFontController().selectFontSize(value);
+ } else if (parts[0].equals(".uno:LeftPara")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ALIGN_LEFT, pressed);
+ } else if (parts[0].equals(".uno:CenterPara")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ALIGN_CENTER, pressed);
+ } else if (parts[0].equals(".uno:RightPara")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ALIGN_RIGHT, pressed);
+ } else if (parts[0].equals(".uno:JustifyPara")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.ALIGN_JUSTIFY, pressed);
+ } else if (parts[0].equals(".uno:DefaultBullet")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.BULLET_LIST, pressed);
+ } else if (parts[0].equals(".uno:DefaultNumbering")) {
+ mContext.getFormattingController().onToggleStateChanged(Document.NUMBERED_LIST, pressed);
+ } else if (parts[0].equals(".uno:Color")) {
+ mContext.getFontController().colorPaletteListener.updateColorPickerPosition(Integer.parseInt(value));
+ } else if (mContext.getTileProvider().isTextDocument() && parts[0].equals(".uno:BackColor")) {
+ mContext.getFontController().backColorPaletteListener.updateColorPickerPosition(Integer.parseInt(value));
+ } else if (mContext.getTileProvider().isPresentation() && parts[0].equals(".uno:CharBackColor")) {
+ mContext.getFontController().backColorPaletteListener.updateColorPickerPosition(Integer.parseInt(value));
+ } else if (mContext.getTileProvider().isSpreadsheet() && parts[0].equals(".uno:BackgroundColor")) {
+ mContext.getFontController().backColorPaletteListener.updateColorPickerPosition(Integer.parseInt(value));
+ } else if (parts[0].equals(".uno:StatePageNumber")) {
+ // get the total page number and compare to the current value and update accordingly
+ String[] splitStrings = parts[1].split(" ");
+ int totalPageNumber = Integer.valueOf(splitStrings[splitStrings.length - 1]);
+ if (totalPageNumber != currentTotalPageNumber) {
+ currentTotalPageNumber = totalPageNumber;
+ // update part page rectangles stored in DocumentOverlayView object
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_PART_PAGE_RECT));
+ }
+ } else {
+ Log.d(LOGTAG, "LOK_CALLBACK_STATE_CHANGED type uncatched: " + payload);
+ }
+ }
+
+ /**
+ * Parses the payload text with rectangle coordinates and converts to rectangle in pixel coordinates
+ *
+ * @param payload - invalidation message payload text
+ * @return rectangle in pixel coordinates
+ */
+ public RectF convertPayloadToRectangle(String payload) {
+ String payloadWithoutWhitespace = payload.replaceAll("\\s", ""); // remove all whitespace from the string
+
+ if (payloadWithoutWhitespace.isEmpty() || payloadWithoutWhitespace.equals("EMPTY")) {
+ return null;
+ }
+
+ String[] coordinates = payloadWithoutWhitespace.split(",");
+
+ if (coordinates.length != 4) {
+ return null;
+ }
+ return convertPayloadToRectangle(coordinates);
+ }
+
+ /**
+ * Parses the payload text with rectangle coordinates and converts to rectangle in pixel coordinates
+ *
+ * @param payload - invalidation message payload text
+ * @return rectangle in pixel coordinates
+ */
+ public RectF convertPayloadCellToRectangle(String payload) {
+ String payloadWithoutWhitespace = payload.replaceAll("\\s", ""); // remove all whitespace from the string
+
+ if (payloadWithoutWhitespace.isEmpty() || payloadWithoutWhitespace.equals("EMPTY")) {
+ return null;
+ }
+
+ String[] coordinates = payloadWithoutWhitespace.split(",");
+
+ if (coordinates.length != 6 ) {
+ return null;
+ }
+ return convertPayloadToRectangle(coordinates);
+ }
+
+ /**
+ * Converts rectangle coordinates to rectangle in pixel coordinates
+ *
+ * @param coordinates - the first four items defines the rectangle
+ * @return rectangle in pixel coordinates
+ */
+ public RectF convertPayloadToRectangle(String[] coordinates) {
+ if (coordinates.length < 4 ) {
+ return null;
+ }
+
+ int x = Integer.decode(coordinates[0]);
+ int y = Integer.decode(coordinates[1]);
+ int width = Integer.decode(coordinates[2]);
+ int height = Integer.decode(coordinates[3]);
+
+ float dpi = LOKitShell.getDpi(mContext);
+
+ return new RectF(
+ LOKitTileProvider.twipToPixel(x, dpi),
+ LOKitTileProvider.twipToPixel(y, dpi),
+ LOKitTileProvider.twipToPixel(x + width, dpi),
+ LOKitTileProvider.twipToPixel(y + height, dpi)
+ );
+ }
+
+ /**
+ * Parses the payload text with more rectangles (separated by ';') and converts to a list of rectangles.
+ *
+ * @param payload - invalidation message payload text
+ * @return list of rectangles
+ */
+ public List<RectF> convertPayloadToRectangles(String payload) {
+ List<RectF> rectangles = new ArrayList<RectF>();
+ String[] rectangleArray = payload.split(";");
+
+ for (String coordinates : rectangleArray) {
+ RectF rectangle = convertPayloadToRectangle(coordinates);
+ if (rectangle != null) {
+ rectangles.add(rectangle);
+ }
+
+ }
+
+ return rectangles;
+ }
+
+ /**
+ * Handles the tile invalidation message
+ *
+ * @param payload
+ */
+ private void invalidateTiles(String payload) {
+ RectF rectangle = convertPayloadToRectangle(payload);
+ if (rectangle != null) {
+ LOKitShell.sendTileInvalidationRequest(rectangle);
+ }
+ }
+
+ /**
+ * Handles the cursor invalidation message
+ *
+ * @param payload
+ */
+ private synchronized void invalidateCursor(String payload) {
+ RectF cursorRectangle = convertPayloadToRectangle(payload);
+ if (cursorRectangle != null) {
+ mDocumentOverlay.positionCursor(cursorRectangle);
+ mDocumentOverlay.positionHandle(SelectionHandle.HandleType.MIDDLE, cursorRectangle);
+
+ if (mState == OverlayState.TRANSITION || mState == OverlayState.CURSOR) {
+ changeStateTo(OverlayState.CURSOR);
+ }
+
+ if (mKeyEvent) {
+ moveViewportToMakeCursorVisible(cursorRectangle);
+ mKeyEvent = false;
+ }
+ }
+ }
+
+ /**
+ * Move the viewport to show the cursor. The cursor will appear at the
+ * viewport position depending on where the cursor is relative to the
+ * viewport (either cursor is above, below, on left or right).
+ *
+ * @param cursorRectangle - cursor position on the document
+ */
+ public void moveViewportToMakeCursorVisible(RectF cursorRectangle) {
+ RectF moveToRect = mLayerClient.getViewportMetrics().getCssViewport();
+ if (moveToRect.contains(cursorRectangle)) {
+ return;
+ }
+
+ float newLeft = moveToRect.left;
+ float newTop = moveToRect.top;
+
+ if (cursorRectangle.right < moveToRect.left || cursorRectangle.left < moveToRect.left) {
+ newLeft = cursorRectangle.left - (moveToRect.width() * 0.1f);
+ } else if (cursorRectangle.right > moveToRect.right || cursorRectangle.left > moveToRect.right) {
+ newLeft = cursorRectangle.right - (moveToRect.width() * 0.9f);
+ }
+
+ if (cursorRectangle.top < moveToRect.top || cursorRectangle.bottom < moveToRect.top) {
+ newTop = cursorRectangle.top - (moveToRect.height() * 0.1f);
+ } else if (cursorRectangle.bottom > moveToRect.bottom || cursorRectangle.top > moveToRect.bottom) {
+ newTop = cursorRectangle.bottom - (moveToRect.height() / 2.0f);
+ }
+
+ LOKitShell.moveViewportTo(mContext, new PointF(newLeft, newTop), null);
+ }
+
+ /**
+ * Handles the text selection start message
+ *
+ * @param payload
+ */
+ private synchronized void textSelectionStart(String payload) {
+ RectF selectionRect = convertPayloadToRectangle(payload);
+ if (selectionRect != null) {
+ mDocumentOverlay.positionHandle(SelectionHandle.HandleType.START, selectionRect);
+ }
+ }
+
+ /**
+ * Handles the text selection end message
+ *
+ * @param payload
+ */
+ private synchronized void textSelectionEnd(String payload) {
+ RectF selectionRect = convertPayloadToRectangle(payload);
+ if (selectionRect != null) {
+ mDocumentOverlay.positionHandle(SelectionHandle.HandleType.END, selectionRect);
+ }
+ }
+
+ /**
+ * Handles the text selection message
+ *
+ * @param payload
+ */
+ private synchronized void textSelection(String payload) {
+ if (payload.isEmpty() || payload.equals("EMPTY")) {
+ if (mState == OverlayState.SELECTION) {
+ changeStateTo(OverlayState.TRANSITION);
+ }
+ mDocumentOverlay.changeSelections(Collections.<RectF>emptyList());
+ if (mContext.getTileProvider().isSpreadsheet()) {
+ mDocumentOverlay.showHeaderSelection(null);
+ }
+ mContext.getToolbarController().showHideClipboardCutAndCopy(false);
+ } else {
+ List<RectF> rectangles = convertPayloadToRectangles(payload);
+ if (mState != OverlayState.SELECTION) {
+ changeStateTo(OverlayState.TRANSITION);
+ }
+ changeStateTo(OverlayState.SELECTION);
+ mDocumentOverlay.changeSelections(rectangles);
+ if (mContext.getTileProvider().isSpreadsheet()) {
+ mDocumentOverlay.showHeaderSelection(rectangles.get(0));
+ }
+ String selectedText = mContext.getTileProvider().getTextSelection("");
+ mContext.getToolbarController().showClipboardActions(selectedText);
+ }
+ }
+
+ /**
+ * Handles the cursor visibility message
+ *
+ * @param payload
+ */
+ private synchronized void cursorVisibility(String payload) {
+ if (payload.equals("true")) {
+ mDocumentOverlay.showCursor();
+ if (mState != OverlayState.SELECTION) {
+ mDocumentOverlay.showHandle(SelectionHandle.HandleType.MIDDLE);
+ }
+ } else if (payload.equals("false")) {
+ mDocumentOverlay.hideCursor();
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.MIDDLE);
+ }
+ }
+
+ /**
+ * Handles the graphic selection change message
+ *
+ * @param payload
+ */
+ private void graphicSelection(String payload) {
+ if (payload.isEmpty() || payload.equals("EMPTY")) {
+ if (mState == OverlayState.GRAPHIC_SELECTION) {
+ changeStateTo(OverlayState.TRANSITION);
+ }
+ } else {
+ RectF rectangle = convertPayloadToRectangle(payload);
+ mDocumentOverlay.changeGraphicSelection(rectangle);
+ if (mState != OverlayState.GRAPHIC_SELECTION) {
+ changeStateTo(OverlayState.TRANSITION);
+ }
+ changeStateTo(OverlayState.GRAPHIC_SELECTION);
+ }
+ }
+
+ /**
+ * Trigger a transition to a new overlay state.
+ *
+ * @param next - new state to transition to
+ */
+ public synchronized void changeStateTo(OverlayState next) {
+ changeState(mState, next);
+ }
+
+ /**
+ * Executes a transition from old overlay state to a new overlay state.
+ *
+ * @param previous - old state
+ * @param next - new state
+ */
+ private synchronized void changeState(OverlayState previous, OverlayState next) {
+ mState = next;
+ handleGeneralChangeState(previous, next);
+ switch (next) {
+ case CURSOR:
+ handleCursorState(previous);
+ break;
+ case SELECTION:
+ handleSelectionState(previous);
+ break;
+ case GRAPHIC_SELECTION:
+ handleGraphicSelectionState(previous);
+ break;
+ case TRANSITION:
+ handleTransitionState(previous);
+ break;
+ case NONE:
+ handleNoneState(previous);
+ break;
+ }
+ }
+
+ /**
+ * Handle a general transition - executed for all transitions.
+ */
+ private void handleGeneralChangeState(OverlayState previous, OverlayState next) {
+ if (previous == OverlayState.NONE &&
+ !mContext.getToolbarController().getEditModeStatus()) {
+ mContext.getToolbarController().switchToEditMode();
+ } else if (next == OverlayState.NONE &&
+ mContext.getToolbarController().getEditModeStatus()) {
+ mContext.getToolbarController().switchToViewMode();
+ }
+ }
+
+ /**
+ * Handle a transition to OverlayState.NONE state.
+ */
+ private void handleNoneState(OverlayState previous) {
+ if (previous == OverlayState.NONE) {
+ return;
+ }
+
+ // Just hide everything
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.START);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.END);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.MIDDLE);
+ mDocumentOverlay.hideSelections();
+ mDocumentOverlay.hideCursor();
+ mDocumentOverlay.hideGraphicSelection();
+ mContext.hideSoftKeyboard();
+ }
+
+ /**
+ * Handle a transition to OverlayState.SELECTION state.
+ */
+ private void handleSelectionState(OverlayState previous) {
+ mDocumentOverlay.showHandle(SelectionHandle.HandleType.START);
+ mDocumentOverlay.showHandle(SelectionHandle.HandleType.END);
+ mDocumentOverlay.showSelections();
+ }
+
+ /**
+ * Handle a transition to OverlayState.CURSOR state.
+ */
+ private void handleCursorState(OverlayState previous) {
+ mContext.showSoftKeyboardOrFormattingToolbar();
+ if (previous == OverlayState.TRANSITION) {
+ mDocumentOverlay.showHandle(SelectionHandle.HandleType.MIDDLE);
+ mDocumentOverlay.showCursor();
+ }
+ }
+
+ /**
+ * Handle a transition to OverlayState.TRANSITION state.
+ */
+ private void handleTransitionState(OverlayState previous) {
+ switch (previous) {
+ case SELECTION:
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.START);
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.END);
+ mDocumentOverlay.hideSelections();
+ break;
+ case CURSOR:
+ mDocumentOverlay.hideHandle(SelectionHandle.HandleType.MIDDLE);
+ break;
+ case GRAPHIC_SELECTION:
+ mDocumentOverlay.hideGraphicSelection();
+ break;
+ }
+ }
+
+ /**
+ * Handle a transition to OverlayState.GRAPHIC_SELECTION state.
+ */
+ private void handleGraphicSelectionState(OverlayState previous) {
+ mDocumentOverlay.showGraphicSelection();
+ mContext.hideSoftKeyboard();
+ }
+
+ /**
+ * The current state the overlay is in.
+ */
+ public OverlayState getCurrentState() {
+ return mState;
+ }
+
+ /**
+ * A key event happened (i.e. user started typing).
+ */
+ public void keyEvent() {
+ mKeyEvent = true;
+ }
+
+ /**
+ * The states the overlay.
+ */
+ public enum OverlayState {
+ /**
+ * State where the overlay is empty
+ */
+ NONE,
+ /**
+ * In-between state where we need to transition to a new overlay state.
+ * In this state we properly disable the older state and wait to transition
+ * to a new state triggered by an invalidation.
+ */
+ TRANSITION,
+ /**
+ * State where we operate with the cursor.
+ */
+ CURSOR,
+ /**
+ * State where we operate the graphic selection.
+ */
+ GRAPHIC_SELECTION,
+ /**
+ * State where we operate the text selection.
+ */
+ SELECTION
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOEvent.java b/android/source/src/java/org/libreoffice/LOEvent.java
new file mode 100644
index 000000000..d1170eee1
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOEvent.java
@@ -0,0 +1,177 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.view.KeyEvent;
+
+import org.libreoffice.canvas.SelectionHandle;
+import org.mozilla.gecko.gfx.ComposedTileLayer;
+
+/**
+ * Events and data that is queued and processed by LOKitThread.
+ */
+public class LOEvent implements Comparable<LOEvent> {
+ public static final int SIZE_CHANGED = 1;
+ public static final int CHANGE_PART = 2;
+ public static final int LOAD = 3;
+ public static final int CLOSE = 4;
+ public static final int TILE_REEVALUATION_REQUEST = 5;
+ public static final int THUMBNAIL = 6;
+ public static final int TILE_INVALIDATION = 7;
+ public static final int TOUCH = 8;
+ public static final int KEY_EVENT = 9;
+ public static final int CHANGE_HANDLE_POSITION = 10;
+ public static final int SWIPE_RIGHT = 11;
+ public static final int SWIPE_LEFT = 12;
+ public static final int NAVIGATION_CLICK = 13;
+ public static final int UNO_COMMAND = 14;
+ public static final int LOAD_NEW = 16;
+ public static final int SAVE_AS = 17;
+ public static final int UPDATE_PART_PAGE_RECT = 18;
+ public static final int UPDATE_ZOOM_CONSTRAINTS = 19;
+ public static final int UPDATE_CALC_HEADERS = 20;
+ public static final int REFRESH = 21;
+ public static final int PAGE_SIZE_CHANGED = 22;
+ public static final int UNO_COMMAND_NOTIFY = 23;
+ public static final int SAVE_COPY_AS = 24;
+
+
+ public final int mType;
+ public int mPriority = 0;
+ private String mTypeString;
+
+ public ThumbnailCreator.ThumbnailCreationTask mTask;
+ public int mPartIndex;
+ public String mString;
+ public String filePath;
+ public String fileType;
+ public ComposedTileLayer mComposedTileLayer;
+ public String mTouchType;
+ public PointF mDocumentCoordinate;
+ public KeyEvent mKeyEvent;
+ public RectF mInvalidationRect;
+ public SelectionHandle.HandleType mHandleType;
+ public String mValue;
+ public int mPageWidth;
+ public int mPageHeight;
+ public boolean mNotify;
+
+ public LOEvent(int type) {
+ mType = type;
+ }
+
+ public LOEvent(int type, ComposedTileLayer composedTileLayer) {
+ mType = type;
+ mTypeString = "Tile Reevaluation";
+ mComposedTileLayer = composedTileLayer;
+ }
+
+ public LOEvent(int type, String someString) {
+ mType = type;
+ mTypeString = "String";
+ mString = someString;
+ mValue = null;
+ }
+
+ public LOEvent(int type, String someString, boolean notify) {
+ mType = type;
+ mTypeString = "String";
+ mString = someString;
+ mValue = null;
+ mNotify = notify;
+ }
+
+ public LOEvent(int type, String someString, String value, boolean notify) {
+ mType = type;
+ mTypeString = "String";
+ mString = someString;
+ mValue = value;
+ mNotify = notify;
+ }
+
+ public LOEvent(int type, String key, String value) {
+ mType = type;
+ mTypeString = "key / value";
+ mString = key;
+ mValue = value;
+ }
+
+ public LOEvent(String filePath, int type) {
+ mType = type;
+ mTypeString = "Load";
+ this.filePath = filePath;
+ }
+
+ public LOEvent(String filePath, String fileType, int type) {
+ mType = type;
+ mTypeString = "Load New/Save As";
+ this.filePath = filePath;
+ this.fileType = fileType;
+ }
+
+ public LOEvent(int type, int partIndex) {
+ mType = type;
+ mPartIndex = partIndex;
+ mTypeString = "Change part";
+ }
+
+ public LOEvent(int type, ThumbnailCreator.ThumbnailCreationTask task) {
+ mType = type;
+ mTask = task;
+ mTypeString = "Thumbnail";
+ }
+
+ public LOEvent(int type, String touchType, PointF documentTouchCoordinate) {
+ mType = type;
+ mTypeString = "Touch";
+ mTouchType = touchType;
+ mDocumentCoordinate = documentTouchCoordinate;
+ }
+
+ public LOEvent(int type, KeyEvent keyEvent) {
+ mType = type;
+ mTypeString = "Key Event";
+ mKeyEvent = keyEvent;
+ }
+
+ public LOEvent(int type, RectF rect) {
+ mType = type;
+ mTypeString = "Tile Invalidation";
+ mInvalidationRect = rect;
+ }
+
+ public LOEvent(int type, SelectionHandle.HandleType handleType, PointF documentCoordinate) {
+ mType = type;
+ mHandleType = handleType;
+ mDocumentCoordinate = documentCoordinate;
+ }
+
+ public LOEvent(int type, int pageWidth, int pageHeight){
+ mType = type;
+ mPageWidth = pageWidth;
+ mPageHeight = pageHeight;
+ }
+
+ public String getTypeString() {
+ if (mTypeString == null) {
+ return "Event type: " + mType;
+ }
+ return mTypeString;
+ }
+
+ @Override
+ public int compareTo(LOEvent another) {
+ return mPriority - another.mPriority;
+ }
+
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOKitInputConnectionHandler.java b/android/source/src/java/org/libreoffice/LOKitInputConnectionHandler.java
new file mode 100644
index 000000000..7b50ef5ff
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitInputConnectionHandler.java
@@ -0,0 +1,74 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import org.mozilla.gecko.gfx.InputConnectionHandler;
+
+/**
+ * Implementation of InputConnectionHandler. When a key event happens it is
+ * directed to this class which is then directed further to LOKitThread.
+ */
+public class LOKitInputConnectionHandler implements InputConnectionHandler {
+ private static final String LOGTAG = LOKitInputConnectionHandler.class.getSimpleName();
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return null;
+ }
+
+ /**
+ * When key pre-Ime happens.
+ */
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * When key down event happens.
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ LOKitShell.sendKeyEvent(event);
+ return false;
+ }
+
+ /**
+ * When key long press event happens.
+ */
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * When key multiple event happens. Key multiple event is triggered when
+ * non-ascii characters are entered on soft keyboard.
+ */
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ LOKitShell.sendKeyEvent(event);
+ return false;
+ }
+
+ /**
+ * When key up event happens.
+ */
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ LOKitShell.sendKeyEvent(event);
+ return false;
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOKitShell.java b/android/source/src/java/org/libreoffice/LOKitShell.java
new file mode 100644
index 000000000..f6a76228e
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitShell.java
@@ -0,0 +1,169 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.view.KeyEvent;
+
+import org.libreoffice.canvas.SelectionHandle;
+import org.mozilla.gecko.gfx.ComposedTileLayer;
+
+/**
+ * Common static LOKit functions, functions to send events.
+ */
+public class LOKitShell {
+ public static float getDpi(Context context) {
+ LOKitTileProvider tileProvider = ((LibreOfficeMainActivity)context).getTileProvider();
+ if (tileProvider != null && tileProvider.isSpreadsheet())
+ return 96f;
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ return metrics.density * 160;
+ }
+
+ // Get a Handler for the main java thread
+ public static Handler getMainHandler() {
+ return LibreOfficeApplication.getMainHandler();
+ }
+
+ public static void showProgressSpinner(final LibreOfficeMainActivity context) {
+ getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ context.showProgressSpinner();
+ }
+ });
+ }
+
+ public static void hideProgressSpinner(final LibreOfficeMainActivity context) {
+ getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ context.hideProgressSpinner();
+ }
+ });
+ }
+
+ public static int getMemoryClass(Context context) {
+ ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ return activityManager.getMemoryClass() * 1024 * 1024;
+ }
+
+ public static boolean isEditingEnabled() {
+ return !LibreOfficeMainActivity.isReadOnlyMode();
+ }
+
+ // EVENTS
+
+ /**
+ * Make sure LOKitThread is running and send event to it.
+ */
+ public static void sendEvent(LOEvent event) {
+ LibreOfficeMainActivity.loKitThread.queueEvent(event);
+ }
+
+ public static void sendThumbnailEvent(ThumbnailCreator.ThumbnailCreationTask task) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.THUMBNAIL, task));
+ }
+
+ /**
+ * Send touch event to LOKitThread.
+ */
+ public static void sendTouchEvent(String touchType, PointF documentTouchCoordinate) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.TOUCH, touchType, documentTouchCoordinate));
+ }
+
+ /**
+ * Send key event to LOKitThread.
+ */
+ public static void sendKeyEvent(KeyEvent event) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.KEY_EVENT, event));
+ }
+
+ public static void sendSizeChangedEvent(int width, int height) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.SIZE_CHANGED));
+ }
+
+ public static void sendSwipeRightEvent() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.SWIPE_RIGHT));
+ }
+
+ public static void sendSwipeLeftEvent() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.SWIPE_LEFT));
+ }
+
+ public static void sendChangePartEvent(int part) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.CHANGE_PART, part));
+ }
+
+ public static void sendLoadEvent(String inputFilePath) {
+ LOKitShell.sendEvent(new LOEvent(inputFilePath, LOEvent.LOAD));
+ }
+
+ public static void sendNewDocumentLoadEvent(String newDocumentPath, String newDocumentType) {
+ LOKitShell.sendEvent(new LOEvent(newDocumentPath, newDocumentType, LOEvent.LOAD_NEW));
+ }
+
+ public static void sendSaveAsEvent(String filePath, String fileFormat) {
+ LOKitShell.sendEvent(new LOEvent(filePath, fileFormat, LOEvent.SAVE_AS));
+ }
+
+ public static void sendSaveCopyAsEvent(String filePath, String fileFormat) {
+ LOKitShell.sendEvent(new LOEvent(filePath, fileFormat, LOEvent.SAVE_COPY_AS));
+ }
+
+ public static void sendCloseEvent() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.CLOSE));
+ }
+
+ /**
+ * Send tile reevaluation to LOKitThread.
+ */
+ public static void sendTileReevaluationRequest(ComposedTileLayer composedTileLayer) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.TILE_REEVALUATION_REQUEST, composedTileLayer));
+ }
+
+ /**
+ * Send tile invalidation to LOKitThread.
+ */
+ public static void sendTileInvalidationRequest(RectF rect) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.TILE_INVALIDATION, rect));
+ }
+
+ /**
+ * Send change handle position event to LOKitThread.
+ */
+ public static void sendChangeHandlePositionEvent(SelectionHandle.HandleType handleType, PointF documentCoordinate) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.CHANGE_HANDLE_POSITION, handleType, documentCoordinate));
+ }
+
+ public static void sendNavigationClickEvent() {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.NAVIGATION_CLICK));
+ }
+
+ /**
+ * Move the viewport to the desired point (top-left), and change the zoom level.
+ * Ensure this runs on the UI thread.
+ */
+ public static void moveViewportTo(final LibreOfficeMainActivity context, final PointF position, final Float zoom) {
+ context.getLayerClient().post(new Runnable() {
+ @Override
+ public void run() {
+ context.getLayerClient().moveTo(position, zoom);
+ }
+ });
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOKitThread.java b/android/source/src/java/org/libreoffice/LOKitThread.java
new file mode 100644
index 000000000..c29f98461
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitThread.java
@@ -0,0 +1,447 @@
+package org.libreoffice;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import org.libreoffice.canvas.SelectionHandle;
+import org.mozilla.gecko.ZoomConstraints;
+import org.mozilla.gecko.gfx.CairoImage;
+import org.mozilla.gecko.gfx.ComposedTileLayer;
+import org.mozilla.gecko.gfx.GeckoLayerClient;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+import org.mozilla.gecko.gfx.SubTile;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/*
+ * Thread that communicates with LibreOffice through LibreOfficeKit JNI interface. The thread
+ * consumes events from other threads (mainly the UI thread) and acts accordingly.
+ */
+class LOKitThread extends Thread {
+ private static final String LOGTAG = LOKitThread.class.getSimpleName();
+
+ private final LinkedBlockingQueue<LOEvent> mEventQueue = new LinkedBlockingQueue<LOEvent>();
+
+ private TileProvider mTileProvider;
+ private InvalidationHandler mInvalidationHandler;
+ private ImmutableViewportMetrics mViewportMetrics;
+ private GeckoLayerClient mLayerClient;
+ private final LibreOfficeMainActivity mContext;
+
+ LOKitThread(LibreOfficeMainActivity context) {
+ mContext = context;
+ mInvalidationHandler = null;
+ TileProviderFactory.initialize();
+ }
+
+ /**
+ * Starting point of the thread. Processes events that gather in the queue.
+ */
+ @Override
+ public void run() {
+ while (true) {
+ LOEvent event;
+ try {
+ event = mEventQueue.take();
+ processEvent(event);
+ } catch (InterruptedException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+ }
+
+ /**
+ * Viewport changed, Recheck if tiles need to be added / removed.
+ */
+ private void tileReevaluationRequest(ComposedTileLayer composedTileLayer) {
+ if (mTileProvider == null) {
+ return;
+ }
+ List<SubTile> tiles = new ArrayList<SubTile>();
+
+ mLayerClient.beginDrawing();
+ composedTileLayer.addNewTiles(tiles);
+ mLayerClient.endDrawing();
+
+ for (SubTile tile : tiles) {
+ TileIdentifier tileId = tile.id;
+ CairoImage image = mTileProvider.createTile(tileId.x, tileId.y, tileId.size, tileId.zoom);
+ mLayerClient.beginDrawing();
+ if (image != null) {
+ tile.setImage(image);
+ }
+ mLayerClient.endDrawing();
+ mLayerClient.forceRender();
+ }
+
+ mLayerClient.beginDrawing();
+ composedTileLayer.markTiles();
+ composedTileLayer.clearMarkedTiles();
+ mLayerClient.endDrawing();
+ mLayerClient.forceRender();
+ }
+
+ /**
+ * Invalidate tiles that intersect the input rect.
+ */
+ private void tileInvalidation(RectF rect) {
+ if (mLayerClient == null || mTileProvider == null) {
+ return;
+ }
+
+ mLayerClient.beginDrawing();
+
+ List<SubTile> tiles = new ArrayList<SubTile>();
+ mLayerClient.invalidateTiles(tiles, rect);
+
+ for (SubTile tile : tiles) {
+ CairoImage image = mTileProvider.createTile(tile.id.x, tile.id.y, tile.id.size, tile.id.zoom);
+ tile.setImage(image);
+ tile.invalidate();
+ }
+ mLayerClient.endDrawing();
+ mLayerClient.forceRender();
+ }
+
+ /**
+ * Handle the geometry change + draw.
+ */
+ private void redraw(boolean resetZoomAndPosition) {
+ if (mLayerClient == null || mTileProvider == null) {
+ // called too early...
+ return;
+ }
+
+ mLayerClient.setPageRect(0, 0, mTileProvider.getPageWidth(), mTileProvider.getPageHeight());
+ mViewportMetrics = mLayerClient.getViewportMetrics();
+ mLayerClient.setViewportMetrics(mViewportMetrics);
+
+ if (resetZoomAndPosition) {
+ zoomAndRepositionTheDocument();
+ }
+
+ mLayerClient.forceRedraw();
+ mLayerClient.forceRender();
+ }
+
+ /**
+ * Reposition the view (zoom and position) when the document is firstly shown. This is document type dependent.
+ */
+ private void zoomAndRepositionTheDocument() {
+ if (mTileProvider.isSpreadsheet()) {
+ // Don't do anything for spreadsheets - show at 100%
+ } else if (mTileProvider.isTextDocument()) {
+ // Always zoom text document to the beginning of the document and centered by width
+ float centerY = mViewportMetrics.getCssViewport().centerY();
+ mLayerClient.zoomTo(new RectF(0, centerY, mTileProvider.getPageWidth(), centerY));
+ } else {
+ // Other documents - always show the whole document on the screen,
+ // regardless of document shape and orientation.
+ if (mViewportMetrics.getViewport().width() < mViewportMetrics.getViewport().height()) {
+ mLayerClient.zoomTo(mTileProvider.getPageWidth(), 0);
+ } else {
+ mLayerClient.zoomTo(0, mTileProvider.getPageHeight());
+ }
+ }
+ }
+
+ /**
+ * Invalidate everything + handle the geometry change
+ */
+ private void refresh(boolean resetZoomAndPosition) {
+ mLayerClient.clearAndResetlayers();
+ redraw(resetZoomAndPosition);
+ updatePartPageRectangles();
+ if (mTileProvider != null && mTileProvider.isSpreadsheet()) {
+ updateCalcHeaders();
+ }
+ }
+
+ /**
+ * Update part page rectangles which hold positions of each document page.
+ * Result is stored in DocumentOverlayView class.
+ */
+ private void updatePartPageRectangles() {
+ if (mTileProvider == null) {
+ Log.d(LOGTAG, "mTileProvider==null when calling updatePartPageRectangles");
+ return;
+ }
+ String partPageRectString = ((LOKitTileProvider) mTileProvider).getPartPageRectangles();
+ List<RectF> partPageRectangles = mInvalidationHandler.convertPayloadToRectangles(partPageRectString);
+ mContext.getDocumentOverlay().setPartPageRectangles(partPageRectangles);
+ }
+
+ private void updatePageSize(int pageWidth, int pageHeight){
+ mTileProvider.setDocumentSize(pageWidth, pageHeight);
+ redraw(true);
+ }
+
+ private void updateZoomConstraints() {
+ if (mTileProvider == null) return;
+ mLayerClient = mContext.getLayerClient();
+ // Set 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));
+ }
+
+ /**
+ * Change part of the document.
+ */
+ private void changePart(int partIndex) {
+ LOKitShell.showProgressSpinner(mContext);
+ mTileProvider.changePart(partIndex);
+ mViewportMetrics = mLayerClient.getViewportMetrics();
+ // mLayerClient.setViewportMetrics(mViewportMetrics.scaleTo(0.9f, new PointF()));
+ refresh(true);
+ LOKitShell.hideProgressSpinner(mContext);
+ }
+
+ /**
+ * Handle load document event.
+ * @param filePath - filePath to where the document is located
+ * @return Whether the document has been loaded successfully.
+ */
+ private boolean loadDocument(String filePath) {
+ mLayerClient = mContext.getLayerClient();
+
+ mInvalidationHandler = new InvalidationHandler(mContext);
+ mTileProvider = TileProviderFactory.create(mContext, mInvalidationHandler, filePath);
+
+ if (mTileProvider.isReady()) {
+ LOKitShell.showProgressSpinner(mContext);
+ updateZoomConstraints();
+ refresh(true);
+ LOKitShell.hideProgressSpinner(mContext);
+ return true;
+ } else {
+ closeDocument();
+ return false;
+ }
+ }
+
+ /**
+ * Handle load new document event.
+ * @param filePath - filePath to where new document is to be created
+ * @param fileType - fileType what type of new document is to be loaded
+ */
+ private void loadNewDocument(String filePath, String fileType) {
+ boolean ok = loadDocument(fileType);
+ if (ok) {
+ mTileProvider.saveDocumentAs(filePath, true);
+ }
+ }
+
+ /**
+ * Save the currently loaded document.
+ */
+ private void saveDocumentAs(String filePath, String fileType, boolean bTakeOwnership) {
+ if (mTileProvider == null) {
+ Log.e(LOGTAG, "Error in saving, Tile Provider instance is null");
+ } else {
+ mTileProvider.saveDocumentAs(filePath, fileType, bTakeOwnership);
+ }
+ }
+
+ /**
+ * Close the currently loaded document.
+ */
+ private void closeDocument() {
+ if (mTileProvider != null) {
+ mTileProvider.close();
+ mTileProvider = null;
+ }
+ }
+
+ /**
+ * Process the input event.
+ */
+ private void processEvent(LOEvent event) {
+ switch (event.mType) {
+ case LOEvent.LOAD:
+ loadDocument(event.filePath);
+ break;
+ case LOEvent.LOAD_NEW:
+ loadNewDocument(event.filePath, event.fileType);
+ break;
+ case LOEvent.SAVE_AS:
+ saveDocumentAs(event.filePath, event.fileType, true);
+ break;
+ case LOEvent.SAVE_COPY_AS:
+ saveDocumentAs(event.filePath, event.fileType, false);
+ break;
+ case LOEvent.CLOSE:
+ closeDocument();
+ break;
+ case LOEvent.SIZE_CHANGED:
+ redraw(false);
+ break;
+ case LOEvent.CHANGE_PART:
+ changePart(event.mPartIndex);
+ break;
+ case LOEvent.TILE_INVALIDATION:
+ tileInvalidation(event.mInvalidationRect);
+ break;
+ case LOEvent.THUMBNAIL:
+ createThumbnail(event.mTask);
+ break;
+ case LOEvent.TOUCH:
+ touch(event.mTouchType, event.mDocumentCoordinate);
+ break;
+ case LOEvent.KEY_EVENT:
+ keyEvent(event.mKeyEvent);
+ break;
+ case LOEvent.TILE_REEVALUATION_REQUEST:
+ tileReevaluationRequest(event.mComposedTileLayer);
+ break;
+ case LOEvent.CHANGE_HANDLE_POSITION:
+ changeHandlePosition(event.mHandleType, event.mDocumentCoordinate);
+ break;
+ case LOEvent.SWIPE_LEFT:
+ if (null != mTileProvider) onSwipeLeft();
+ break;
+ case LOEvent.SWIPE_RIGHT:
+ if (null != mTileProvider) onSwipeRight();
+ break;
+ case LOEvent.NAVIGATION_CLICK:
+ mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.NONE);
+ break;
+ case LOEvent.UNO_COMMAND:
+ if (null == mTileProvider)
+ Log.e(LOGTAG, "no mTileProvider when trying to process "+event.mValue+" from UNO_COMMAND "+event.mString);
+ else
+ mTileProvider.postUnoCommand(event.mString, event.mValue);
+ break;
+ case LOEvent.UPDATE_PART_PAGE_RECT:
+ updatePartPageRectangles();
+ break;
+ case LOEvent.UPDATE_ZOOM_CONSTRAINTS:
+ updateZoomConstraints();
+ break;
+ case LOEvent.UPDATE_CALC_HEADERS:
+ updateCalcHeaders();
+ break;
+ case LOEvent.UNO_COMMAND_NOTIFY:
+ if (null == mTileProvider)
+ Log.e(LOGTAG, "no mTileProvider when trying to process "+event.mValue+" from UNO_COMMAND "+event.mString);
+ else
+ mTileProvider.postUnoCommand(event.mString, event.mValue, event.mNotify);
+ break;
+ case LOEvent.REFRESH:
+ refresh(false);
+ break;
+ case LOEvent.PAGE_SIZE_CHANGED:
+ updatePageSize(event.mPageWidth, event.mPageHeight);
+ break;
+ }
+ }
+
+ private void updateCalcHeaders() {
+ if (null == mTileProvider) return;
+ LOKitTileProvider tileProvider = (LOKitTileProvider)mTileProvider;
+ String values = tileProvider.getCalcHeaders();
+ mContext.getCalcHeadersController().setHeaders(values);
+ }
+
+ /**
+ * Request a change of the handle position.
+ */
+ private void changeHandlePosition(SelectionHandle.HandleType handleType, PointF documentCoordinate) {
+ switch (handleType) {
+ case MIDDLE:
+ mTileProvider.setTextSelectionReset(documentCoordinate);
+ break;
+ case START:
+ mTileProvider.setTextSelectionStart(documentCoordinate);
+ break;
+ case END:
+ mTileProvider.setTextSelectionEnd(documentCoordinate);
+ break;
+ }
+ }
+
+ /**
+ * Processes key events.
+ */
+ private void keyEvent(KeyEvent keyEvent) {
+ if (!LOKitShell.isEditingEnabled()) {
+ return;
+ }
+ if (mTileProvider == null) {
+ return;
+ }
+ mInvalidationHandler.keyEvent();
+ mTileProvider.sendKeyEvent(keyEvent);
+ }
+
+ /**
+ * Process swipe left event.
+ */
+ private void onSwipeLeft() {
+ mTileProvider.onSwipeLeft();
+ }
+
+ /**
+ * Process swipe right event.
+ */
+ private void onSwipeRight() {
+ mTileProvider.onSwipeRight();
+ }
+
+ /**
+ * Processes touch events.
+ */
+ private void touch(String touchType, PointF documentCoordinate) {
+ if (mTileProvider == null || mViewportMetrics == null) {
+ return;
+ }
+
+ // to handle hyperlinks, enable single tap even in the Viewer
+ boolean editing = LOKitShell.isEditingEnabled();
+ float zoomFactor = mViewportMetrics.getZoomFactor();
+
+ if (touchType.equals("LongPress")) {
+ mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.TRANSITION);
+ mTileProvider.mouseButtonDown(documentCoordinate, 1, zoomFactor);
+ mTileProvider.mouseButtonUp(documentCoordinate, 1, zoomFactor);
+ mTileProvider.mouseButtonDown(documentCoordinate, 2, zoomFactor);
+ mTileProvider.mouseButtonUp(documentCoordinate, 2, zoomFactor);
+ } else if (touchType.equals("SingleTap")) {
+ mInvalidationHandler.changeStateTo(InvalidationHandler.OverlayState.TRANSITION);
+ mTileProvider.mouseButtonDown(documentCoordinate, 1, zoomFactor);
+ mTileProvider.mouseButtonUp(documentCoordinate, 1, zoomFactor);
+ } else if (touchType.equals("GraphicSelectionStart") && editing) {
+ mTileProvider.setGraphicSelectionStart(documentCoordinate);
+ } else if (touchType.equals("GraphicSelectionEnd") && editing) {
+ mTileProvider.setGraphicSelectionEnd(documentCoordinate);
+ }
+ }
+
+ /**
+ * Create thumbnail for the requested document task.
+ */
+ private void createThumbnail(final ThumbnailCreator.ThumbnailCreationTask task) {
+ final Bitmap bitmap = task.getThumbnail(mTileProvider);
+ task.applyBitmap(bitmap);
+ }
+
+ /**
+ * Queue an event.
+ */
+ public void queueEvent(LOEvent event) {
+ mEventQueue.add(event);
+ }
+
+ /**
+ * Clear all events in the queue (used when document is closed).
+ */
+ public void clearQueue() {
+ mEventQueue.clear();
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LOKitTileProvider.java b/android/source/src/java/org/libreoffice/LOKitTileProvider.java
new file mode 100644
index 000000000..0c7931763
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LOKitTileProvider.java
@@ -0,0 +1,820 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.os.Build;
+import android.print.PrintAttributes;
+import android.print.PrintDocumentAdapter;
+import android.print.PrintManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.widget.Toast;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.kit.DirectBufferAllocator;
+import org.libreoffice.kit.Document;
+import org.libreoffice.kit.LibreOfficeKit;
+import org.libreoffice.kit.Office;
+import org.mozilla.gecko.gfx.BufferedCairoImage;
+import org.mozilla.gecko.gfx.CairoImage;
+import org.mozilla.gecko.gfx.IntSize;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+
+/**
+ * LOKit implementation of TileProvider.
+ */
+class LOKitTileProvider implements TileProvider {
+ private static final String LOGTAG = LOKitTileProvider.class.getSimpleName();
+ private static final int TILE_SIZE = 256;
+ private final float mTileWidth;
+ private final float mTileHeight;
+ private String mInputFile;
+ private Office mOffice;
+ private Document mDocument;
+ private final boolean mIsReady;
+ private final LibreOfficeMainActivity mContext;
+
+ private final float mDPI;
+ private float mWidthTwip;
+ private float mHeightTwip;
+
+ private final Document.MessageCallback mMessageCallback;
+
+ private final long objectCreationTime = System.currentTimeMillis();
+
+ /**
+ * Initialize LOKit and load the document.
+ * @param messageCallback - callback for messages retrieved from LOKit
+ * @param input - input path of the document
+ */
+ LOKitTileProvider(LibreOfficeMainActivity context, InvalidationHandler messageCallback, String input) {
+ mContext = context;
+ mMessageCallback = messageCallback;
+
+ LibreOfficeKit.putenv("SAL_LOG=+WARN+INFO");
+ LibreOfficeKit.init(mContext);
+
+ mOffice = new Office(LibreOfficeKit.getLibreOfficeKitHandle());
+ mOffice.setMessageCallback(messageCallback);
+ mOffice.setOptionalFeatures(Document.LOK_FEATURE_DOCUMENT_PASSWORD);
+ mContext.setTileProvider(this);
+ mInputFile = input;
+
+ Log.i(LOGTAG, "====> Loading file '" + input + "'");
+
+ File fileToBeEncoded = new File(input);
+ String encodedFileName = android.net.Uri.encode(fileToBeEncoded.getName());
+
+ mDocument = mOffice.documentLoad(
+ (new File(fileToBeEncoded.getParent(),encodedFileName)).getPath()
+ );
+
+ if (mDocument == null && !mContext.isPasswordProtected()) {
+ Log.i(LOGTAG, "====> mOffice.documentLoad() returned null, trying to restart 'Office' and loading again");
+ mOffice.destroy();
+ Log.i(LOGTAG, "====> mOffice.destroy() done");
+ ByteBuffer handle = LibreOfficeKit.getLibreOfficeKitHandle();
+ Log.i(LOGTAG, "====> getLibreOfficeKitHandle() = " + handle);
+ mOffice = new Office(handle);
+ Log.i(LOGTAG, "====> new Office created");
+ mOffice.setMessageCallback(messageCallback);
+ mOffice.setOptionalFeatures(Document.LOK_FEATURE_DOCUMENT_PASSWORD);
+ Log.i(LOGTAG, "====> setup Lokit callback and optional features (password support)");
+ mDocument = mOffice.documentLoad(
+ (new File(fileToBeEncoded.getParent(),encodedFileName)).getPath()
+ );
+ }
+
+ Log.i(LOGTAG, "====> mDocument = " + mDocument);
+
+ mDPI = LOKitShell.getDpi(mContext);
+ mTileWidth = pixelToTwip(TILE_SIZE, mDPI);
+ mTileHeight = pixelToTwip(TILE_SIZE, mDPI);
+
+ if (mDocument != null)
+ mDocument.initializeForRendering();
+
+ if (checkDocument()) {
+ postLoad();
+ mIsReady = true;
+ } else {
+ mIsReady = false;
+ }
+ }
+
+ /**
+ * Triggered after the document is loaded.
+ */
+ private void postLoad() {
+ mDocument.setMessageCallback(mMessageCallback);
+
+ resetParts();
+ // Writer documents always have one part, so hide the navigation drawer.
+ if (mDocument.getDocumentType() == Document.DOCTYPE_TEXT) {
+ mContext.disableNavigationDrawer();
+ mContext.getToolbarController().hideItem(R.id.action_parts);
+ }
+
+ // Enable headers for Calc documents
+ if (mDocument.getDocumentType() == Document.DOCTYPE_SPREADSHEET) {
+ mContext.initializeCalcHeaders();
+ }
+
+ mDocument.setPart(0);
+
+ setupDocumentFonts();
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.getDocumentPartViewListAdapter().notifyDataSetChanged();
+ }
+ });
+ }
+
+ public void addPart(){
+ int parts = mDocument.getParts();
+ if(mDocument.getDocumentType() == Document.DOCTYPE_SPREADSHEET){
+ try{
+ JSONObject jsonObject = new JSONObject();
+ JSONObject values = new JSONObject();
+ JSONObject values2 = new JSONObject();
+ values.put("type", "long");
+ values.put("value", 0); //add to the last
+ values2.put("type", "string");
+ values2.put("value", "");
+ jsonObject.put("Name", values2);
+ jsonObject.put("Index", values);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Insert", jsonObject.toString()));
+ }catch (JSONException e) {
+ e.printStackTrace();
+ }
+ } else if (mDocument.getDocumentType() == Document.DOCTYPE_PRESENTATION){
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertPage"));
+ }
+
+ String partName = mDocument.getPartName(parts);
+ if (partName.isEmpty()) {
+ partName = getGenericPartName(parts);
+ }
+ mDocument.setPart(parts);
+ resetDocumentSize();
+ final DocumentPartView partView = new DocumentPartView(parts, partName);
+ mContext.getDocumentPartView().add(partView);
+ }
+
+ public void resetParts(){
+ mContext.getDocumentPartView().clear();
+ if (mDocument.getDocumentType() != Document.DOCTYPE_TEXT) {
+ int parts = mDocument.getParts();
+ for (int i = 0; i < parts; i++) {
+ String partName = mDocument.getPartName(i);
+
+ if (partName.isEmpty()) {
+ partName = getGenericPartName(i);
+ }
+ Log.i(LOGTAG, "resetParts: " + partName);
+ mDocument.setPart(i);
+ resetDocumentSize();
+ final DocumentPartView partView = new DocumentPartView(i, partName);
+ mContext.getDocumentPartView().add(partView);
+ }
+ }
+ }
+
+ public void renamePart(String partName) {
+ try{
+ for(int i=0; i<mDocument.getParts(); i++){
+ if(mContext.getDocumentPartView().get(i).partName.equals(partName)){
+ //part name must be unique
+ Toast.makeText(mContext, mContext.getString(R.string.name_already_used), Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ JSONObject parameter = new JSONObject();
+ JSONObject name = new JSONObject();
+ name.put("type", "string");
+ name.put("value", partName);
+ parameter.put("Name", name);
+ if(isPresentation()){
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:RenamePage", parameter.toString(),true));
+ }else {
+ JSONObject index = new JSONObject();
+ index.put("type","long");
+ index.put("value", getCurrentPartNumber()+1);
+ parameter.put("Index", index);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Name", parameter.toString(),true));
+ }
+ }catch (JSONException e){
+ e.printStackTrace();
+ }
+ }
+
+ public void removePart() {
+ try{
+ if (!isSpreadsheet() && !isPresentation()) {
+ //document must be spreadsheet or presentation
+ return;
+ }
+
+ if(isPresentation()){
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:DeletePage", true));
+ return;
+ }
+
+ if(getPartsCount() < 2){
+ return;
+ }
+
+ JSONObject parameter = new JSONObject();
+ JSONObject index = new JSONObject();
+ index.put("type","long");
+ index.put("value", getCurrentPartNumber()+1);
+ parameter.put("Index", index);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Remove", parameter.toString(),true));
+ }catch (JSONException e){
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public boolean saveDocumentAs(final String filePath, String format, boolean takeOwnership) {
+ String options = "";
+ if (takeOwnership) {
+ options = "TakeOwnership";
+ }
+
+ final String newFilePath = "file://" + filePath;
+ Log.d("saveFilePathURL", newFilePath);
+ LOKitShell.showProgressSpinner(mContext);
+ mDocument.saveAs(newFilePath, format, options);
+ final boolean ok;
+ if (!mOffice.getError().isEmpty()){
+ ok = true;
+ Log.e("Save Error", mOffice.getError());
+ if (format.equals("svg")) {
+ // error in creating temp slideshow svg file
+ Log.d(LOGTAG, "Error in creating temp slideshow svg file");
+ } else if(format.equals("pdf")){
+ Log.d(LOGTAG, "Error in creating pdf file");
+ } else {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ // There was some error
+ mContext.showCustomStatusMessage(mContext.getString(R.string.unable_to_save));
+ }
+ });
+ }
+ } else {
+ ok = false;
+ if (format.equals("svg")) {
+ // successfully created temp slideshow svg file
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.startPresentation(newFilePath);
+ }
+ });
+ } else if (takeOwnership) {
+ mInputFile = filePath;
+ }
+ }
+ LOKitShell.hideProgressSpinner(mContext);
+ return ok;
+ }
+
+ @Override
+ public boolean saveDocumentAs(final String filePath, boolean takeOwnership) {
+ final int docType = mDocument.getDocumentType();
+ if (docType == Document.DOCTYPE_TEXT)
+ return saveDocumentAs(filePath, "odt", takeOwnership);
+ else if (docType == Document.DOCTYPE_SPREADSHEET)
+ return saveDocumentAs(filePath, "ods", takeOwnership);
+ else if (docType == Document.DOCTYPE_PRESENTATION)
+ return saveDocumentAs(filePath, "odp", takeOwnership);
+ else if (docType == Document.DOCTYPE_DRAWING)
+ return saveDocumentAs(filePath, "odg", takeOwnership);
+
+ Log.w(LOGTAG, "Cannot determine file format from document. Not saving.");
+ return false;
+ }
+
+ public void printDocument() {
+ if (Build.VERSION.SDK_INT < 19) {
+ mContext.showCustomStatusMessage(mContext.getString(R.string.printing_not_supported));
+ return;
+ }
+
+ String mInputFileName = (new File(mInputFile)).getName();
+ String file = mInputFileName.substring(0,(mInputFileName.length()-3))+"pdf";
+ String cacheFile = mContext.getExternalCacheDir().getAbsolutePath() + "/" + file;
+ mDocument.saveAs("file://"+cacheFile,"pdf","");
+ try {
+ PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
+ PrintDocumentAdapter printAdapter = new PDFDocumentAdapter(mContext, cacheFile);
+ printManager.print("Document", printAdapter, new PrintAttributes.Builder().build());
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void saveDocument(){
+ mContext.saveDocument();
+ }
+
+ private void setupDocumentFonts() {
+ String values = mDocument.getCommandValues(".uno:CharFontName");
+ if (values == null || values.isEmpty())
+ return;
+
+ mContext.getFontController().parseJson(values);
+ mContext.getFontController().setupFontViews();
+ }
+
+ private String getGenericPartName(int i) {
+ if (mDocument == null) {
+ return "";
+ }
+ switch (mDocument.getDocumentType()) {
+ case Document.DOCTYPE_DRAWING:
+ case Document.DOCTYPE_TEXT:
+ return mContext.getString(R.string.page) + " " + (i + 1);
+ case Document.DOCTYPE_SPREADSHEET:
+ return mContext.getString(R.string.sheet) + " " + (i + 1);
+ case Document.DOCTYPE_PRESENTATION:
+ return mContext.getString(R.string.slide) + " " + (i + 1);
+ case Document.DOCTYPE_OTHER:
+ default:
+ return mContext.getString(R.string.part) + " " + (i + 1);
+ }
+ }
+
+ static float twipToPixel(float input, float dpi) {
+ return input / 1440.0f * dpi;
+ }
+
+ private static float pixelToTwip(float input, float dpi) {
+ return (input / dpi) * 1440.0f;
+ }
+
+
+ /**
+ * @see TileProvider#getPartsCount()
+ */
+ @Override
+ public int getPartsCount() {
+ return mDocument.getParts();
+ }
+
+ /**
+ * Wrapper for getPartPageRectangles() JNI function.
+ */
+ public String getPartPageRectangles() {
+ return mDocument.getPartPageRectangles();
+ }
+
+ /**
+ * Fetch Calc header information.
+ */
+ public String getCalcHeaders() {
+ long nX = 0;
+ long nY = 0;
+ long nWidth = mDocument.getDocumentWidth();
+ long nHeight = mDocument.getDocumentHeight();
+ return mDocument.getCommandValues(".uno:ViewRowColumnHeaders?x=" + nX + "&y=" + nY
+ + "&width=" + nWidth + "&height=" + nHeight);
+ }
+
+ /**
+ * @see TileProvider#onSwipeLeft()
+ */
+ @Override
+ public void onSwipeLeft() {
+ if (mDocument.getDocumentType() == Document.DOCTYPE_PRESENTATION &&
+ getCurrentPartNumber() < getPartsCount()-1) {
+ LOKitShell.sendChangePartEvent(getCurrentPartNumber()+1);
+ }
+ }
+
+ /**
+ * @see TileProvider#onSwipeRight()
+ */
+ @Override
+ public void onSwipeRight() {
+ if (mDocument.getDocumentType() == Document.DOCTYPE_PRESENTATION &&
+ getCurrentPartNumber() > 0) {
+ LOKitShell.sendChangePartEvent(getCurrentPartNumber()-1);
+ }
+ }
+
+ private boolean checkDocument() {
+ String error = null;
+ boolean ret;
+
+ if (mDocument == null || !mOffice.getError().isEmpty()) {
+ error = "Cannot open " + mInputFile + ": " + mOffice.getError();
+ ret = false;
+ } else {
+ ret = resetDocumentSize();
+ if (!ret) {
+ error = "Document returned an invalid size or the document is empty.";
+ }
+ }
+
+ if (!ret && !mContext.isPasswordProtected()) {
+ final String message = error;
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mContext.showAlertDialog(message);
+ }
+ });
+ } else if (!ret && mContext.isPasswordProtected()) {
+ mContext.finish();
+ }
+
+ return ret;
+ }
+
+ private boolean resetDocumentSize() {
+ mWidthTwip = mDocument.getDocumentWidth();
+ mHeightTwip = mDocument.getDocumentHeight();
+
+ if (mWidthTwip == 0 || mHeightTwip == 0) {
+ Log.e(LOGTAG, "Document size zero - last error: " + mOffice.getError());
+ return false;
+ } else {
+ Log.i(LOGTAG, "Reset document size: " + mDocument.getDocumentWidth() + " x " + mDocument.getDocumentHeight());
+ }
+
+ return true;
+ }
+
+ @Override
+ public void setDocumentSize(int pageWidth, int pageHeight){
+ mWidthTwip = pageWidth;
+ mHeightTwip = pageHeight;
+ }
+
+ /**
+ * @see TileProvider#getPageWidth()
+ */
+ @Override
+ public int getPageWidth() {
+ return (int) twipToPixel(mWidthTwip, mDPI);
+ }
+
+ /**
+ * @see TileProvider#getPageHeight()
+ */
+ @Override
+ public int getPageHeight() {
+ return (int) twipToPixel(mHeightTwip, mDPI);
+ }
+
+ /**
+ * @see TileProvider#isReady()
+ */
+ @Override
+ public boolean isReady() {
+ return mIsReady;
+ }
+
+ /**
+ * @see TileProvider#createTile(float, float, org.mozilla.gecko.gfx.IntSize, float)
+ */
+ @Override
+ public CairoImage createTile(float x, float y, IntSize tileSize, float zoom) {
+ ByteBuffer buffer = DirectBufferAllocator.guardedAllocate(tileSize.width * tileSize.height * 4);
+ if (buffer == null)
+ return null;
+
+ CairoImage image = new BufferedCairoImage(buffer, tileSize.width, tileSize.height, CairoImage.FORMAT_ARGB32);
+ rerenderTile(image, x, y, tileSize, zoom);
+ return image;
+ }
+
+ /**
+ * @see TileProvider#rerenderTile(org.mozilla.gecko.gfx.CairoImage, float, float, org.mozilla.gecko.gfx.IntSize, float)
+ */
+ @Override
+ public void rerenderTile(CairoImage image, float x, float y, IntSize tileSize, float zoom) {
+ if (mDocument != null && image.getBuffer() != null) {
+ float twipX = pixelToTwip(x, mDPI) / zoom;
+ float twipY = pixelToTwip(y, mDPI) / zoom;
+ float twipWidth = mTileWidth / zoom;
+ float twipHeight = mTileHeight / zoom;
+ long start = System.currentTimeMillis() - objectCreationTime;
+
+ //Log.i(LOGTAG, "paintTile >> @" + start + " (" + tileSize.width + " " + tileSize.height + " " + (int) twipX + " " + (int) twipY + " " + (int) twipWidth + " " + (int) twipHeight + ")");
+ mDocument.paintTile(image.getBuffer(), tileSize.width, tileSize.height, (int) twipX, (int) twipY, (int) twipWidth, (int) twipHeight);
+
+ long stop = System.currentTimeMillis() - objectCreationTime;
+ //Log.i(LOGTAG, "paintTile << @" + stop + " elapsed: " + (stop - start));
+ } else {
+ if (mDocument == null) {
+ Log.e(LOGTAG, "Document is null!!");
+ }
+ }
+ }
+
+ /**
+ * @see TileProvider#thumbnail(int)
+ */
+ @Override
+ public Bitmap thumbnail(int size) {
+ int widthPixel = getPageWidth();
+ int heightPixel = getPageHeight();
+
+ if (widthPixel > heightPixel) {
+ double ratio = heightPixel / (double) widthPixel;
+ widthPixel = size;
+ heightPixel = (int) (widthPixel * ratio);
+ } else {
+ double ratio = widthPixel / (double) heightPixel;
+ heightPixel = size;
+ widthPixel = (int) (heightPixel * ratio);
+ }
+
+ Log.w(LOGTAG, "Thumbnail size: " + getPageWidth() + " " + getPageHeight() + " " + widthPixel + " " + heightPixel);
+
+ ByteBuffer buffer = ByteBuffer.allocateDirect(widthPixel * heightPixel * 4);
+ if (mDocument != null)
+ mDocument.paintTile(buffer, widthPixel, heightPixel, 0, 0, (int) mWidthTwip, (int) mHeightTwip);
+
+ Bitmap bitmap = null;
+ try {
+ bitmap = Bitmap.createBitmap(widthPixel, heightPixel, Bitmap.Config.ARGB_8888);
+ bitmap.copyPixelsFromBuffer(buffer);
+ } catch (IllegalArgumentException e) {
+ Log.e(LOGTAG, "width (" + widthPixel + ") and height (" + heightPixel + ") must not be 0! (ToDo: likely timing issue)");
+ }
+ if (bitmap == null) {
+ Log.w(LOGTAG, "Thumbnail not created!");
+ }
+ return bitmap;
+ }
+
+ /**
+ * @see TileProvider#close()
+ */
+ @Override
+ public void close() {
+ Log.i(LOGTAG, "Document destroyed: " + mInputFile);
+ if (mDocument != null) {
+ mDocument.destroy();
+ mDocument = null;
+ }
+ }
+
+ /**
+ * @see TileProvider#isDrawing()
+ */
+ @Override
+ public boolean isDrawing() {
+ return mDocument != null && mDocument.getDocumentType() == Document.DOCTYPE_DRAWING;
+ }
+
+ /**
+ * @see TileProvider#isTextDocument()
+ */
+ @Override
+ public boolean isTextDocument() {
+ return mDocument != null && mDocument.getDocumentType() == Document.DOCTYPE_TEXT;
+ }
+
+ /**
+ * @see TileProvider#isSpreadsheet()
+ */
+ @Override
+ public boolean isSpreadsheet() {
+ return mDocument != null && mDocument.getDocumentType() == Document.DOCTYPE_SPREADSHEET;
+ }
+
+ /**
+ * @see TileProvider#isPresentation()
+ */
+ @Override
+ public boolean isPresentation(){
+ return mDocument != null && mDocument.getDocumentType() == Document.DOCTYPE_PRESENTATION;
+ }
+
+ /**
+ * Returns the Unicode character generated by this event or 0.
+ */
+ private int getCharCode(KeyEvent keyEvent) {
+ switch (keyEvent.getKeyCode())
+ {
+ case KeyEvent.KEYCODE_DEL:
+ case KeyEvent.KEYCODE_ENTER:
+ return 0;
+ }
+ return keyEvent.getUnicodeChar();
+ }
+
+ /**
+ * Returns the integer code representing the key of the event (non-zero for
+ * control keys).
+ */
+ private int getKeyCode(KeyEvent keyEvent) {
+ switch (keyEvent.getKeyCode()) {
+ case KeyEvent.KEYCODE_DEL:
+ return com.sun.star.awt.Key.BACKSPACE;
+ case KeyEvent.KEYCODE_ENTER:
+ return com.sun.star.awt.Key.RETURN;
+ }
+ return 0;
+ }
+
+ /**
+ * @see TileProvider#sendKeyEvent(android.view.KeyEvent)
+ */
+ @Override
+ public void sendKeyEvent(KeyEvent keyEvent) {
+ switch (keyEvent.getAction()) {
+ case KeyEvent.ACTION_MULTIPLE:
+ String keyString = keyEvent.getCharacters();
+ for (int i = 0; i < keyString.length(); i++) {
+ int codePoint = keyString.codePointAt(i);
+ mDocument.postKeyEvent(Document.KEY_EVENT_PRESS, codePoint, getKeyCode(keyEvent));
+ }
+ break;
+ case KeyEvent.ACTION_DOWN:
+ mDocument.postKeyEvent(Document.KEY_EVENT_PRESS, getCharCode(keyEvent), getKeyCode(keyEvent));
+ break;
+ case KeyEvent.ACTION_UP:
+ mDocument.postKeyEvent(Document.KEY_EVENT_RELEASE, getCharCode(keyEvent), getKeyCode(keyEvent));
+ break;
+ }
+ }
+
+ private void mouseButton(int type, PointF inDocument, int numberOfClicks, float zoomFactor) {
+ int x = (int) pixelToTwip(inDocument.x, mDPI);
+ int y = (int) pixelToTwip(inDocument.y, mDPI);
+
+ mDocument.setClientZoom(TILE_SIZE, TILE_SIZE, (int) (mTileWidth / zoomFactor), (int) (mTileHeight / zoomFactor));
+ mDocument.postMouseEvent(type, x, y, numberOfClicks, Document.MOUSE_BUTTON_LEFT, Document.KEYBOARD_MODIFIER_NONE);
+ }
+
+ /**
+ * @see TileProvider#mouseButtonDown(android.graphics.PointF, int, float)
+ */
+ @Override
+ public void mouseButtonDown(PointF documentCoordinate, int numberOfClicks, float zoomFactor) {
+ mouseButton(Document.MOUSE_EVENT_BUTTON_DOWN, documentCoordinate, numberOfClicks, zoomFactor);
+ }
+
+ /**
+ * @see TileProvider#mouseButtonUp(android.graphics.PointF, int, float)
+ */
+ @Override
+ public void mouseButtonUp(PointF documentCoordinate, int numberOfClicks, float zoomFactor) {
+ mouseButton(Document.MOUSE_EVENT_BUTTON_UP, documentCoordinate, numberOfClicks, zoomFactor);
+ }
+
+ /**
+ * @param command UNO command string
+ * @param arguments Arguments to UNO command
+ */
+ @Override
+ public void postUnoCommand(String command, String arguments) {
+ postUnoCommand(command, arguments, false);
+ }
+
+ /**
+ * @param command
+ * @param arguments
+ * @param notifyWhenFinished
+ */
+ @Override
+ public void postUnoCommand(String command, String arguments, boolean notifyWhenFinished) {
+ mDocument.postUnoCommand(command, arguments, notifyWhenFinished);
+ }
+
+ private void setTextSelection(int type, PointF documentCoordinate) {
+ int x = (int) pixelToTwip(documentCoordinate.x, mDPI);
+ int y = (int) pixelToTwip(documentCoordinate.y, mDPI);
+ mDocument.setTextSelection(type, x, y);
+ }
+
+ /**
+ * @see TileProvider#setTextSelectionStart(android.graphics.PointF)
+ */
+ @Override
+ public void setTextSelectionStart(PointF documentCoordinate) {
+ setTextSelection(Document.SET_TEXT_SELECTION_START, documentCoordinate);
+ }
+
+ /**
+ * @see TileProvider#setTextSelectionEnd(android.graphics.PointF)
+ */
+ @Override
+ public void setTextSelectionEnd(PointF documentCoordinate) {
+ setTextSelection(Document.SET_TEXT_SELECTION_END, documentCoordinate);
+ }
+
+ /**
+ * @see TileProvider#setTextSelectionReset(android.graphics.PointF)
+ */
+ @Override
+ public void setTextSelectionReset(PointF documentCoordinate) {
+ setTextSelection(Document.SET_TEXT_SELECTION_RESET, documentCoordinate);
+ }
+
+ /**
+ * @param mimeType
+ * @return
+ */
+ @Override
+ public String getTextSelection(String mimeType) {
+ return mDocument.getTextSelection(mimeType);
+ }
+
+ /**
+ * paste
+ * @param mimeType
+ * @param data
+ * @return
+ */
+ @Override
+ public boolean paste(String mimeType, String data) {
+ return mDocument.paste(mimeType, data);
+ }
+
+
+ /**
+ * @see org.libreoffice.TileProvider#setGraphicSelectionStart(android.graphics.PointF)
+ */
+ @Override
+ public void setGraphicSelectionStart(PointF documentCoordinate) {
+ setGraphicSelection(Document.SET_GRAPHIC_SELECTION_START, documentCoordinate);
+ }
+
+ /**
+ * @see org.libreoffice.TileProvider#setGraphicSelectionEnd(android.graphics.PointF)
+ */
+ @Override
+ public void setGraphicSelectionEnd(PointF documentCoordinate) {
+ setGraphicSelection(Document.SET_GRAPHIC_SELECTION_END, documentCoordinate);
+ }
+
+ private void setGraphicSelection(int type, PointF documentCoordinate) {
+ int x = (int) pixelToTwip(documentCoordinate.x, mDPI);
+ int y = (int) pixelToTwip(documentCoordinate.y, mDPI);
+ LibreOfficeMainActivity.setDocumentChanged(true);
+ mDocument.setGraphicSelection(type, x, y);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
+ }
+
+ /**
+ * @see TileProvider#changePart(int)
+ */
+ @Override
+ public void changePart(int partIndex) {
+ if (mDocument == null)
+ return;
+
+ mDocument.setPart(partIndex);
+ resetDocumentSize();
+ }
+
+ /**
+ * @see TileProvider#getCurrentPartNumber()
+ */
+ @Override
+ public int getCurrentPartNumber() {
+ if (mDocument == null)
+ return 0;
+
+ return mDocument.getPart();
+ }
+
+ public void setDocumentPassword(String url, String password) {
+ mOffice.setDocumentPassword(url, password);
+ }
+
+ public Document.MessageCallback getMessageCallback() {
+ return mMessageCallback;
+ }
+}
+
+// vim:set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/android/source/src/java/org/libreoffice/LibreOfficeApplication.java b/android/source/src/java/org/libreoffice/LibreOfficeApplication.java
new file mode 100644
index 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..a44ec56e2
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java
@@ -0,0 +1,1121 @@
+package org.libreoffice;
+
+import android.app.AlertDialog;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.AssetManager;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.DocumentsContract;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.snackbar.Snackbar;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import android.text.InputType;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TabHost;
+import android.widget.Toast;
+
+import org.libreoffice.overlay.CalcHeadersController;
+import org.libreoffice.overlay.DocumentOverlay;
+import org.libreoffice.ui.FileUtilities;
+import org.libreoffice.ui.LibreOfficeUIActivity;
+import org.mozilla.gecko.gfx.GeckoLayerClient;
+import org.mozilla.gecko.gfx.LayerView;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Main activity of the LibreOffice App. It is started in the UI thread.
+ */
+public class LibreOfficeMainActivity extends AppCompatActivity implements SettingsListenerModel.OnSettingsPreferenceChangedListener {
+
+ private static final String LOGTAG = "LibreOfficeMainActivity";
+ 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";
+ private static final int REQUEST_CODE_SAVEAS = 12345;
+ private static final int REQUEST_CODE_EXPORT_TO_PDF = 12346;
+
+ //TODO "public static" is a temporary workaround
+ public static LOKitThread loKitThread;
+
+ private GeckoLayerClient mLayerClient;
+
+ private static boolean mIsExperimentalMode;
+ private static boolean mIsDeveloperMode;
+ private static boolean mbISReadOnlyMode;
+
+ private DrawerLayout mDrawerLayout;
+ Toolbar toolbarTop;
+
+ private ListView mDrawerList;
+ private final List<DocumentPartView> mDocumentPartView = new ArrayList<DocumentPartView>();
+ private DocumentPartViewListAdapter mDocumentPartViewListAdapter;
+ private DocumentOverlay mDocumentOverlay;
+ /** URI to save the document to. */
+ private Uri mDocumentUri;
+ /** Temporary local copy of the document. */
+ private File mTempFile = null;
+ private File mTempSlideShowFile = null;
+
+ BottomSheetBehavior bottomToolbarSheetBehavior;
+ BottomSheetBehavior toolbarColorPickerBottomSheetBehavior;
+ BottomSheetBehavior toolbarBackColorPickerBottomSheetBehavior;
+ private FormattingController mFormattingController;
+ private ToolbarController mToolbarController;
+ private FontController mFontController;
+ private SearchController mSearchController;
+ private UNOCommandsController mUNOCommandsController;
+ private CalcHeadersController mCalcHeadersController;
+ private LOKitTileProvider mTileProvider;
+ private String mPassword;
+ private boolean mPasswordProtected;
+ private boolean mbSkipNextRefresh;
+
+ public GeckoLayerClient getLayerClient() {
+ return mLayerClient;
+ }
+
+ public static boolean isExperimentalMode() {
+ return mIsExperimentalMode;
+ }
+
+ public static boolean isDeveloperMode() {
+ return mIsDeveloperMode;
+ }
+
+ private boolean isKeyboardOpen = false;
+ private boolean isFormattingToolbarOpen = false;
+ private boolean isSearchToolbarOpen = false;
+ private static boolean isDocumentChanged = false;
+ private boolean isUNOCommandsToolbarOpen = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.w(LOGTAG, "onCreate..");
+ super.onCreate(savedInstanceState);
+
+ SettingsListenerModel.getInstance().setListener(this);
+ updatePreferences();
+
+ setContentView(R.layout.activity_main);
+
+ toolbarTop = findViewById(R.id.toolbar);
+ hideBottomToolbar();
+
+ mToolbarController = new ToolbarController(this, toolbarTop);
+ mFormattingController = new FormattingController(this);
+ toolbarTop.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LOKitShell.sendNavigationClickEvent();
+ }
+ });
+
+ mFontController = new FontController(this);
+ mSearchController = new SearchController(this);
+ mUNOCommandsController = new UNOCommandsController(this);
+
+ loKitThread = new LOKitThread(this);
+ loKitThread.start();
+
+ mLayerClient = new GeckoLayerClient(this);
+ LayerView layerView = findViewById(R.id.layer_view);
+ mLayerClient.setView(layerView);
+ layerView.setInputConnectionHandler(new LOKitInputConnectionHandler());
+ mLayerClient.notifyReady();
+
+ layerView.setOnKeyListener(new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View view, int i, KeyEvent keyEvent) {
+ if(!isReadOnlyMode() && keyEvent.getKeyCode() != KeyEvent.KEYCODE_BACK){
+ setDocumentChanged(true);
+ }
+ return false;
+ }
+ });
+
+ // create TextCursorLayer
+ mDocumentOverlay = new DocumentOverlay(this, layerView);
+
+ mbISReadOnlyMode = !isExperimentalMode();
+
+ final Uri docUri = getIntent().getData();
+ if (docUri != null) {
+ if (docUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
+ || docUri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
+ final boolean isReadOnlyDoc = (getIntent().getFlags() & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == 0;
+ mbISReadOnlyMode = !isExperimentalMode() || isReadOnlyDoc;
+ Log.d(LOGTAG, "SCHEME_CONTENT: getPath(): " + docUri.getPath());
+
+ String displayName = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), docUri);
+ toolbarTop.setTitle(displayName);
+
+ } else if (docUri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
+ mbISReadOnlyMode = true;
+ Log.d(LOGTAG, "SCHEME_FILE: getPath(): " + docUri.getPath());
+ toolbarTop.setTitle(docUri.getLastPathSegment());
+ }
+ // create a temporary local copy to work with
+ boolean copyOK = copyFileToTemp(docUri) && mTempFile != null;
+ if (!copyOK) {
+ // TODO: can't open the file
+ Log.e(LOGTAG, "couldn't create temporary file from " + docUri);
+ return;
+ }
+
+ // if input doc is a template, a new doc is created and a proper URI to save to
+ // will only be available after a "Save As"
+ if (isTemplate(docUri)) {
+ toolbarTop.setTitle(R.string.default_document_name);
+ } else {
+ mDocumentUri = docUri;
+ }
+
+ LOKitShell.sendLoadEvent(mTempFile.getPath());
+ } else if (getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY) != null) {
+ // New document type string is not null, meaning we want to open a new document
+ String newDocumentType = getIntent().getStringExtra(LibreOfficeUIActivity.NEW_DOC_TYPE_KEY);
+ // create a temporary local file, will be copied to the actual URI when saving
+ loadNewDocument(newDocumentType);
+ toolbarTop.setTitle(getString(R.string.default_document_name));
+ } else {
+ Log.e(LOGTAG, "No document specified. This should never happen.");
+ return;
+ }
+ // the loadDocument/loadNewDocument event already triggers a refresh as well,
+ // so there's no need to do another refresh in 'onStart'
+ mbSkipNextRefresh = true;
+
+ mDrawerLayout = findViewById(R.id.drawer_layout);
+
+ if (mDocumentPartViewListAdapter == null) {
+ mDrawerList = findViewById(R.id.left_drawer);
+
+ mDocumentPartViewListAdapter = new DocumentPartViewListAdapter(this, R.layout.document_part_list_layout, mDocumentPartView);
+ mDrawerList.setAdapter(mDocumentPartViewListAdapter);
+ mDrawerList.setOnItemClickListener(new DocumentPartClickListener());
+ }
+
+ mToolbarController.setupToolbars();
+
+ TabHost host = findViewById(R.id.toolbarTabHost);
+ host.setup();
+
+ TabHost.TabSpec spec = host.newTabSpec(getString(R.string.tabhost_character));
+ spec.setContent(R.id.tab_character);
+ spec.setIndicator(getString(R.string.tabhost_character));
+ host.addTab(spec);
+
+ spec = host.newTabSpec(getString(R.string.tabhost_paragraph));
+ spec.setContent(R.id.tab_paragraph);
+ spec.setIndicator(getString(R.string.tabhost_paragraph));
+ host.addTab(spec);
+
+ spec = host.newTabSpec(getString(R.string.tabhost_insert));
+ spec.setContent(R.id.tab_insert);
+ spec.setIndicator(getString(R.string.tabhost_insert));
+ host.addTab(spec);
+
+ spec = host.newTabSpec(getString(R.string.tabhost_style));
+ spec.setContent(R.id.tab_style);
+ spec.setIndicator(getString(R.string.tabhost_style));
+ host.addTab(spec);
+
+ LinearLayout bottomToolbarLayout = findViewById(R.id.toolbar_bottom);
+ LinearLayout toolbarColorPickerLayout = findViewById(R.id.toolbar_color_picker);
+ LinearLayout toolbarBackColorPickerLayout = findViewById(R.id.toolbar_back_color_picker);
+ bottomToolbarSheetBehavior = BottomSheetBehavior.from(bottomToolbarLayout);
+ toolbarColorPickerBottomSheetBehavior = BottomSheetBehavior.from(toolbarColorPickerLayout);
+ toolbarBackColorPickerBottomSheetBehavior = BottomSheetBehavior.from(toolbarBackColorPickerLayout);
+ bottomToolbarSheetBehavior.setHideable(true);
+ toolbarColorPickerBottomSheetBehavior.setHideable(true);
+ toolbarBackColorPickerBottomSheetBehavior.setHideable(true);
+ }
+
+ private void updatePreferences() {
+ SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ mIsExperimentalMode = BuildConfig.ALLOW_EDITING
+ && sPrefs.getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY, false);
+ mIsDeveloperMode = mIsExperimentalMode
+ && sPrefs.getBoolean(ENABLE_DEVELOPER_PREFS_KEY, false);
+ if (sPrefs.getInt(ASSETS_EXTRACTED_PREFS_KEY, 0) != BuildConfig.VERSION_CODE) {
+ if(copyFromAssets(getAssets(), "unpack", getApplicationInfo().dataDir)) {
+ sPrefs.edit().putInt(ASSETS_EXTRACTED_PREFS_KEY, BuildConfig.VERSION_CODE).apply();
+ }
+ }
+ }
+
+ // Loads a new Document and saves it to a temporary file
+ private void loadNewDocument(String newDocumentType) {
+ String tempFileName = "LibreOffice_" + UUID.randomUUID().toString();
+ mTempFile = new File(this.getCacheDir(), tempFileName);
+ LOKitShell.sendNewDocumentLoadEvent(mTempFile.getPath(), newDocumentType);
+ }
+
+ public RectF getCurrentCursorPosition() {
+ return mDocumentOverlay.getCurrentCursorPosition();
+ }
+
+ private boolean copyFileToTemp(Uri documentUri) {
+ // CSV files need a .csv suffix to be opened in Calc.
+ String suffix = null;
+ String intentType = getIntent().getType();
+ // K-9 mail uses the first, GMail uses the second variant.
+ if ("text/comma-separated-values".equals(intentType) || "text/csv".equals(intentType))
+ suffix = ".csv";
+
+ try {
+ mTempFile = File.createTempFile("LibreOffice", suffix, this.getCacheDir());
+ final FileOutputStream outputStream = new FileOutputStream(mTempFile);
+ return copyUriToStream(documentUri, outputStream);
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Save the document.
+ */
+ public void saveDocument() {
+ Toast.makeText(this, R.string.message_saving, Toast.LENGTH_SHORT).show();
+ // local save
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND_NOTIFY, ".uno:Save", true));
+ }
+
+ /**
+ * Open file chooser and save the document to the URI
+ * selected there.
+ */
+ public void saveDocumentAs() {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ String mimeType = getODFMimeTypeForDocument();
+ intent.setType(mimeType);
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mDocumentUri);
+
+ startActivityForResult(intent, REQUEST_CODE_SAVEAS);
+ }
+
+ /**
+ * Saves the document under the given URI using ODF format
+ * and uses that URI from now on for all operations.
+ * @param newUri URI to save the document and use from now on.
+ */
+ private void saveDocumentAs(Uri newUri) {
+ mDocumentUri = newUri;
+ // save in ODF format
+ mTileProvider.saveDocumentAs(mTempFile.getPath(), true);
+ saveFileToOriginalSource();
+
+ String displayName = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), mDocumentUri);
+ toolbarTop.setTitle(displayName);
+ mbISReadOnlyMode = !isExperimentalMode();
+ getToolbarController().setupToolbars();
+ }
+
+ public void exportToPDF() {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType(FileUtilities.MIMETYPE_PDF);
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mDocumentUri);
+
+ startActivityForResult(intent, REQUEST_CODE_EXPORT_TO_PDF);
+ }
+
+ private void exportToPDF(final Uri uri) {
+ boolean exportOK = false;
+ File tempFile = null;
+ try {
+ tempFile = File.createTempFile("LibreOffice_", ".pdf");
+ mTileProvider.saveDocumentAs(tempFile.getAbsolutePath(),"pdf", false);
+
+ try {
+ FileInputStream inputStream = new FileInputStream(tempFile);
+ exportOK = copyStreamToUri(inputStream, uri);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (tempFile != null && tempFile.exists()) {
+ tempFile.delete();
+ }
+ }
+
+ final int msgId = exportOK ? R.string.pdf_export_finished : R.string.unable_to_export_pdf;
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ showCustomStatusMessage(getString(msgId));
+ }
+ });
+ }
+
+ /**
+ * Returns the ODF MIME type that can be used for the current document,
+ * regardless of whether the document is an ODF Document or not
+ * (e.g. returns FileUtilities.MIMETYPE_OPENDOCUMENT_TEXT for a DOCX file).
+ * @return MIME type, or empty string, if no appropriate MIME type could be found.
+ */
+ private String getODFMimeTypeForDocument() {
+ if (mTileProvider.isTextDocument())
+ return FileUtilities.MIMETYPE_OPENDOCUMENT_TEXT;
+ else if (mTileProvider.isSpreadsheet())
+ return FileUtilities.MIMETYPE_OPENDOCUMENT_SPREADSHEET;
+ else if (mTileProvider.isPresentation())
+ return FileUtilities.MIMETYPE_OPENDOCUMENT_PRESENTATION;
+ else if (mTileProvider.isDrawing())
+ return FileUtilities.MIMETYPE_OPENDOCUMENT_GRAPHICS;
+ else {
+ Log.w(LOGTAG, "Cannot determine MIME type to use.");
+ return "";
+ }
+ }
+
+ /**
+ * Returns whether the MIME type for the URI is considered one for a document template.
+ */
+ private boolean isTemplate(final Uri documentUri) {
+ final String mimeType = getContentResolver().getType(documentUri);
+ return FileUtilities.isTemplateMimeType(mimeType);
+ }
+
+ public void saveFileToOriginalSource() {
+ if (isReadOnlyMode() || mTempFile == null || mDocumentUri == null || !mDocumentUri.getScheme().equals(ContentResolver.SCHEME_CONTENT))
+ return;
+
+ boolean copyOK = false;
+ try {
+ final FileInputStream inputStream = new FileInputStream(mTempFile);
+ copyOK = copyStreamToUri(inputStream, mDocumentUri);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ if (copyOK) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(LibreOfficeMainActivity.this, R.string.message_saved,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ setDocumentChanged(false);
+ } else {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(LibreOfficeMainActivity.this, R.string.message_saving_failed,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.i(LOGTAG, "onResume..");
+ // check for config change
+ updatePreferences();
+ if (mToolbarController.getEditModeStatus() && isExperimentalMode()) {
+ mToolbarController.switchToEditMode();
+ } else {
+ mToolbarController.switchToViewMode();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ Log.i(LOGTAG, "onPause..");
+ super.onPause();
+ }
+
+ @Override
+ protected void onStart() {
+ Log.i(LOGTAG, "onStart..");
+ super.onStart();
+ if (!mbSkipNextRefresh) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.REFRESH));
+ }
+ mbSkipNextRefresh = false;
+ }
+
+ @Override
+ protected void onStop() {
+ Log.i(LOGTAG, "onStop..");
+ hideSoftKeyboardDirect();
+ super.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(LOGTAG, "onDestroy..");
+ LOKitShell.sendCloseEvent();
+ mLayerClient.destroy();
+ super.onDestroy();
+
+ if (isFinishing()) { // Not an orientation change
+ if (mTempFile != null) {
+ // noinspection ResultOfMethodCallIgnored
+ mTempFile.delete();
+ }
+ if (mTempSlideShowFile != null && mTempSlideShowFile.exists()) {
+ // noinspection ResultOfMethodCallIgnored
+ mTempSlideShowFile.delete();
+ }
+ }
+ }
+ @Override
+ public void onBackPressed() {
+ if (!isDocumentChanged) {
+ super.onBackPressed();
+ return;
+ }
+
+
+ DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which){
+ case DialogInterface.BUTTON_POSITIVE:
+ mTileProvider.saveDocument();
+ isDocumentChanged=false;
+ onBackPressed();
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ //CANCEL
+ break;
+ case DialogInterface.BUTTON_NEUTRAL:
+ //NO
+ isDocumentChanged=false;
+ onBackPressed();
+ break;
+ }
+ }
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.save_alert_dialog_title)
+ .setPositiveButton(R.string.save_document, dialogClickListener)
+ .setNegativeButton(R.string.action_cancel, dialogClickListener)
+ .setNeutralButton(R.string.no_save_document, dialogClickListener)
+ .show();
+
+ }
+
+ public List<DocumentPartView> getDocumentPartView() {
+ return mDocumentPartView;
+ }
+
+ public void disableNavigationDrawer() {
+ // Only the original thread that created mDrawerLayout should touch its views.
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, mDrawerList);
+ }
+ });
+ }
+
+ public DocumentPartViewListAdapter getDocumentPartViewListAdapter() {
+ return mDocumentPartViewListAdapter;
+ }
+
+ /**
+ * Show software keyboard.
+ * Force the request on main thread.
+ */
+ public void showSoftKeyboard() {
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if(!isKeyboardOpen) showSoftKeyboardDirect();
+ else hideSoftKeyboardDirect();
+ }
+ });
+
+ }
+
+ private void showSoftKeyboardDirect() {
+ LayerView layerView = findViewById(R.id.layer_view);
+
+ if (layerView.requestFocus()) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.showSoftInput(layerView, InputMethodManager.SHOW_FORCED);
+ }
+ isKeyboardOpen=true;
+ isSearchToolbarOpen=false;
+ isFormattingToolbarOpen=false;
+ isUNOCommandsToolbarOpen=false;
+ hideBottomToolbar();
+ }
+
+ public void showSoftKeyboardOrFormattingToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (findViewById(R.id.toolbar_bottom).getVisibility() != View.VISIBLE
+ && findViewById(R.id.toolbar_color_picker).getVisibility() != View.VISIBLE) {
+ showSoftKeyboardDirect();
+ }
+ }
+ });
+ }
+
+ /**
+ * Hides software keyboard on UI thread.
+ */
+ public void hideSoftKeyboard() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ hideSoftKeyboardDirect();
+ }
+ });
+ }
+
+ /**
+ * Hides software keyboard.
+ */
+ private void hideSoftKeyboardDirect() {
+ if (getCurrentFocus() != null) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
+ isKeyboardOpen=false;
+ }
+ }
+
+ public void showBottomToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ bottomToolbarSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ }
+ });
+ }
+
+ public void hideBottomToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ bottomToolbarSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ toolbarColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ toolbarBackColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
+ isFormattingToolbarOpen=false;
+ isSearchToolbarOpen=false;
+ isUNOCommandsToolbarOpen=false;
+ }
+ });
+ }
+
+ public void showFormattingToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (isFormattingToolbarOpen) {
+ hideFormattingToolbar();
+ } else {
+ showBottomToolbar();
+ findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.formatting_toolbar).setVisibility(View.VISIBLE);
+ findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
+ hideSoftKeyboardDirect();
+ isSearchToolbarOpen=false;
+ isFormattingToolbarOpen=true;
+ isUNOCommandsToolbarOpen=false;
+ }
+
+ }
+ });
+ }
+
+ public void hideFormattingToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ hideBottomToolbar();
+ }
+ });
+ }
+
+ public void showSearchToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (isSearchToolbarOpen) {
+ hideSearchToolbar();
+ } else {
+ showBottomToolbar();
+ findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
+ toolbarColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ toolbarBackColorPickerBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ findViewById(R.id.search_toolbar).setVisibility(View.VISIBLE);
+ findViewById(R.id.UNO_commands_toolbar).setVisibility(View.GONE);
+ hideSoftKeyboardDirect();
+ isFormattingToolbarOpen=false;
+ isSearchToolbarOpen=true;
+ isUNOCommandsToolbarOpen=false;
+ }
+ }
+ });
+ }
+
+ public void hideSearchToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ hideBottomToolbar();
+ }
+ });
+ }
+
+ public void showUNOCommandsToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if(isUNOCommandsToolbarOpen){
+ hideUNOCommandsToolbar();
+ }else{
+ showBottomToolbar();
+ findViewById(R.id.formatting_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.search_toolbar).setVisibility(View.GONE);
+ findViewById(R.id.UNO_commands_toolbar).setVisibility(View.VISIBLE);
+ hideSoftKeyboardDirect();
+ isFormattingToolbarOpen=false;
+ isSearchToolbarOpen=false;
+ isUNOCommandsToolbarOpen=true;
+ }
+ }
+ });
+ }
+
+ public void hideUNOCommandsToolbar() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ hideBottomToolbar();
+ }
+ });
+ }
+
+ public void showProgressSpinner() {
+ findViewById(R.id.loadingPanel).setVisibility(View.VISIBLE);
+ }
+
+ public void hideProgressSpinner() {
+ findViewById(R.id.loadingPanel).setVisibility(View.GONE);
+ }
+
+ public void showAlertDialog(String message) {
+
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(LibreOfficeMainActivity.this);
+
+ alertDialogBuilder.setTitle(R.string.error);
+ alertDialogBuilder.setMessage(message);
+ alertDialogBuilder.setNeutralButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ finish();
+ }
+ });
+
+ AlertDialog alertDialog = alertDialogBuilder.create();
+ alertDialog.show();
+ }
+
+ public DocumentOverlay getDocumentOverlay() {
+ return mDocumentOverlay;
+ }
+
+ public CalcHeadersController getCalcHeadersController() {
+ return mCalcHeadersController;
+ }
+
+ public ToolbarController getToolbarController() {
+ return mToolbarController;
+ }
+
+ public FontController getFontController() {
+ return mFontController;
+ }
+
+ public FormattingController getFormattingController() {
+ return mFormattingController;
+ }
+
+ public void openDrawer() {
+ mDrawerLayout.openDrawer(mDrawerList);
+ hideBottomToolbar();
+ }
+
+ public void showAbout() {
+ AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
+ aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
+ }
+
+ public void addPart(){
+ mTileProvider.addPart();
+ mDocumentPartViewListAdapter.notifyDataSetChanged();
+ setDocumentChanged(true);
+ }
+
+ public void renamePart(){
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.enter_part_name);
+ final EditText input = new EditText(this);
+ input.setInputType(InputType.TYPE_CLASS_TEXT);
+ builder.setView(input);
+
+ builder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mTileProvider.renamePart( input.getText().toString());
+ }
+ });
+ builder.setNegativeButton(R.string.alert_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+
+ builder.show();
+ }
+
+ public void deletePart() {
+ mTileProvider.removePart();
+ }
+
+ public void showSettings() {
+ startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
+ }
+
+ public boolean isDrawerEnabled() {
+ boolean isDrawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
+ boolean isDrawerLocked = mDrawerLayout.getDrawerLockMode(mDrawerList) != DrawerLayout.LOCK_MODE_UNLOCKED;
+ return !isDrawerOpen && !isDrawerLocked;
+ }
+
+ @Override
+ public void settingsPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.matches(ENABLE_EXPERIMENTAL_PREFS_KEY)) {
+ Log.d(LOGTAG, "Editing Preference Changed");
+ mIsExperimentalMode = sharedPreferences.getBoolean(ENABLE_EXPERIMENTAL_PREFS_KEY, false);
+ }
+ }
+
+ public void promptForPassword() {
+ PasswordDialogFragment passwordDialogFragment = new PasswordDialogFragment();
+ passwordDialogFragment.setLOMainActivity(this);
+ passwordDialogFragment.show(getSupportFragmentManager(), "PasswordDialogFragment");
+ }
+
+ // this function can only be called in InvalidationHandler.java
+ public void setPassword() {
+ mTileProvider.setDocumentPassword("file://" + mTempFile.getPath(), mPassword);
+ }
+
+ // setTileProvider is meant to let main activity have a handle of LOKit when dealing with password
+ public void setTileProvider(LOKitTileProvider loKitTileProvider) {
+ mTileProvider = loKitTileProvider;
+ }
+
+ public LOKitTileProvider getTileProvider() {
+ return mTileProvider;
+ }
+
+ public void savePassword(String pwd) {
+ mPassword = pwd;
+ synchronized (mTileProvider.getMessageCallback()) {
+ mTileProvider.getMessageCallback().notifyAll();
+ }
+ }
+
+ public void setPasswordProtected(boolean b) {
+ mPasswordProtected = b;
+ }
+
+ public boolean isPasswordProtected() {
+ return mPasswordProtected;
+ }
+
+ public void initializeCalcHeaders() {
+ mCalcHeadersController = new CalcHeadersController(this, mLayerClient.getView());
+ mCalcHeadersController.setupHeaderPopupView();
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ findViewById(R.id.calc_header_top_left).setVisibility(View.VISIBLE);
+ findViewById(R.id.calc_header_row).setVisibility(View.VISIBLE);
+ findViewById(R.id.calc_header_column).setVisibility(View.VISIBLE);
+ findViewById(R.id.calc_address).setVisibility(View.VISIBLE);
+ findViewById(R.id.calc_formula).setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ public static boolean isReadOnlyMode() {
+ return mbISReadOnlyMode;
+ }
+
+ public boolean hasLocationForSave() {
+ return mDocumentUri != null;
+ }
+
+ public static void setDocumentChanged (boolean changed) {
+ isDocumentChanged = changed;
+ }
+
+ private class DocumentPartClickListener implements android.widget.AdapterView.OnItemClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ DocumentPartView partView = mDocumentPartViewListAdapter.getItem(position);
+ LOKitShell.sendChangePartEvent(partView.partIndex);
+ mDrawerLayout.closeDrawer(mDrawerList);
+ }
+ }
+
+ private static boolean copyFromAssets(AssetManager assetManager,
+ String fromAssetPath, String targetDir) {
+ try {
+ String[] files = assetManager.list(fromAssetPath);
+
+ boolean res = true;
+ for (String file : files) {
+ String[] dirOrFile = assetManager.list(fromAssetPath + "/" + file);
+ if ( dirOrFile.length == 0) {
+ // noinspection ResultOfMethodCallIgnored
+ new File(targetDir).mkdirs();
+ res &= copyAsset(assetManager,
+ fromAssetPath + "/" + file,
+ targetDir + "/" + file);
+ } else
+ res &= copyFromAssets(assetManager,
+ fromAssetPath + "/" + file,
+ targetDir + "/" + file);
+ }
+ return res;
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(LOGTAG, "copyFromAssets failed: " + e.getMessage());
+ return false;
+ }
+ }
+
+ private static boolean copyAsset(AssetManager assetManager, String fromAssetPath, String toPath) {
+ ReadableByteChannel source = null;
+ FileChannel dest = null;
+ try {
+ try {
+ source = Channels.newChannel(assetManager.open(fromAssetPath));
+ dest = new FileOutputStream(toPath).getChannel();
+ long bytesTransferred = 0;
+ // might not copy all at once, so make sure everything gets copied...
+ ByteBuffer buffer = ByteBuffer.allocate(4096);
+ while (source.read(buffer) > 0) {
+ buffer.flip();
+ bytesTransferred += dest.write(buffer);
+ buffer.clear();
+ }
+ Log.v(LOGTAG, "Success copying " + fromAssetPath + " to " + toPath + " bytes: " + bytesTransferred);
+ return true;
+ } finally {
+ if (dest != null) dest.close();
+ if (source != null) source.close();
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(LOGTAG, "file " + fromAssetPath + " not found! " + e.getMessage());
+ return false;
+ } catch (IOException e) {
+ Log.e(LOGTAG, "failed to copy file " + fromAssetPath + " from assets to " + toPath + " - " + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Copies everything from the given input stream to the given output stream
+ * and closes both streams in the end.
+ * @return Whether copy operation was successful.
+ */
+ private boolean copyStream(InputStream inputStream, OutputStream outputStream) {
+ try {
+ byte[] buffer = new byte[4096];
+ int readBytes = inputStream.read(buffer);
+ while (readBytes != -1) {
+ outputStream.write(buffer, 0, readBytes);
+ readBytes = inputStream.read(buffer);
+ }
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ inputStream.close();
+ outputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Copies everything from the given Uri to the given OutputStream
+ * and closes the OutputStream in the end.
+ * The copy operation runs in a separate thread, but the method only returns
+ * after the thread has finished its execution.
+ * This can be used to copy in a blocking way when network access is involved,
+ * which is not allowed from the main thread, but that may happen when an underlying
+ * DocumentsProvider (like the NextCloud one) does network access.
+ */
+ private boolean copyUriToStream(final Uri inputUri, final OutputStream outputStream) {
+ class CopyThread extends Thread {
+ /** Whether copy operation was successful. */
+ private boolean result = false;
+
+ @Override
+ public void run() {
+ final ContentResolver contentResolver = getContentResolver();
+ try {
+ InputStream inputStream = contentResolver.openInputStream(inputUri);
+ result = copyStream(inputStream, outputStream);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ CopyThread copyThread = new CopyThread();
+ copyThread.start();
+ try {
+ // wait for copy operation to finish
+ // NOTE: might be useful to add some indicator in UI for long copy operations involving network...
+ copyThread.join();
+ } catch(InterruptedException e) {
+ e.printStackTrace();
+ }
+ return copyThread.result;
+ }
+
+ /**
+ * Copies everything from the given InputStream to the given URI and closes the
+ * InputStream in the end.
+ * @see LibreOfficeMainActivity#copyUriToStream(Uri, OutputStream)
+ * which does the same thing the other way around.
+ */
+ private boolean copyStreamToUri(final InputStream inputStream, final Uri outputUri) {
+ class CopyThread extends Thread {
+ /** Whether copy operation was successful. */
+ private boolean result = false;
+
+ @Override
+ public void run() {
+ final ContentResolver contentResolver = getContentResolver();
+ try {
+ OutputStream outputStream = contentResolver.openOutputStream(outputUri);
+ result = copyStream(inputStream, outputStream);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ CopyThread copyThread = new CopyThread();
+ copyThread.start();
+ try {
+ // wait for copy operation to finish
+ // NOTE: might be useful to add some indicator in UI for long copy operations involving network...
+ copyThread.join();
+ } catch(InterruptedException e) {
+ e.printStackTrace();
+ }
+ return copyThread.result;
+ }
+
+ public void showCustomStatusMessage(String message){
+ Snackbar.make(mDrawerLayout, message, Snackbar.LENGTH_LONG).show();
+ }
+
+ public void preparePresentation() {
+ if (getExternalCacheDir() != null) {
+ String tempPath = getExternalCacheDir().getPath() + "/" + mTempFile.getName() + ".svg";
+ mTempSlideShowFile = new File(tempPath);
+ if (mTempSlideShowFile.exists() && !isDocumentChanged) {
+ startPresentation("file://" + tempPath);
+ } else {
+ LOKitShell.sendSaveCopyAsEvent(tempPath, "svg");
+ }
+ }
+ }
+
+ public void startPresentation(String tempPath) {
+ // 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) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_CODE_SAVEAS && resultCode == RESULT_OK) {
+ final Uri fileUri = data.getData();
+ saveDocumentAs(fileUri);
+ } else if (requestCode == REQUEST_CODE_EXPORT_TO_PDF && resultCode == RESULT_OK) {
+ final Uri fileUri = data.getData();
+ exportToPDF(fileUri);
+ } else {
+ mFormattingController.handleActivityResult(requestCode, resultCode, data);
+ hideBottomToolbar();
+ }
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/LocaleHelper.java b/android/source/src/java/org/libreoffice/LocaleHelper.java
new file mode 100644
index 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..08bc7f596
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/PasswordDialogFragment.java
@@ -0,0 +1,56 @@
+package org.libreoffice;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+public class PasswordDialogFragment extends DialogFragment {
+
+ private LibreOfficeMainActivity mContext;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+
+ final View dialogView = inflater.inflate(R.layout.password_dialog, null);
+
+ builder.setView(dialogView)
+ .setPositiveButton(R.string.action_pwd_dialog_OK, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String pwd = ((EditText)dialogView.findViewById(R.id.password)).getText().toString();
+ mContext.savePassword(pwd);
+ }
+ })
+ .setNegativeButton(R.string.action_pwd_dialog_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mContext.savePassword(null);
+ }
+ }).setTitle(R.string.action_pwd_dialog_title);
+
+ return builder.create();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ getDialog().setCanceledOnTouchOutside(false);
+ setCancelable(false);
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ public void setLOMainActivity(LibreOfficeMainActivity context) {
+ mContext = context;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/PresentationActivity.java b/android/source/src/java/org/libreoffice/PresentationActivity.java
new file mode 100644
index 000000000..ede7c0c40
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/PresentationActivity.java
@@ -0,0 +1,177 @@
+package org.libreoffice;
+
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import androidx.core.view.GestureDetectorCompat;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.webkit.WebView;
+import android.widget.Button;
+import android.widget.ImageButton;
+
+public class PresentationActivity extends AppCompatActivity {
+
+ private static final String LOGTAG = PresentationActivity.class.getSimpleName();
+ WebView mWebView;
+ View mGestureView;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ View decorView = getWindow().getDecorView();
+ // Hide the status bar.
+ int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
+ decorView.setSystemUiVisibility(uiOptions);
+
+ setContentView(R.layout.presentation_mode);
+
+ // get intent and url
+ Intent intent = getIntent();
+ String filePath = intent.getDataString();
+
+ // set up WebView
+ mWebView = findViewById(R.id.presentation_view);
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return true;
+ }
+ });
+
+ // set up buttons within presentation_gesture_view
+ ImageButton prevButton = findViewById(R.id.slide_show_nav_prev);
+ ImageButton nextButton = findViewById(R.id.slide_show_nav_next);
+ Button backButton = findViewById(R.id.slide_show_nav_back);
+
+ prevButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ pageLeft();
+ }
+ });
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ pageRight();
+ }
+ });
+ backButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onBackPressed();
+ }
+ });
+
+ // set up presentation_gesture_view
+ mGestureView = findViewById(R.id.presentation_gesture_view);
+ final GestureDetectorCompat gestureDetector =
+ new GestureDetectorCompat(this, new PresentationGestureViewListener());
+ mGestureView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return gestureDetector.onTouchEvent(event);
+ }
+ });
+
+ // load url
+ mWebView.loadUrl(filePath);
+ }
+
+ private class PresentationGestureViewListener extends GestureDetector.SimpleOnGestureListener {
+ private static final int SWIPE_VELOCITY_THRESHOLD = 100;
+ private static final int SCROLL_THRESHOLD = 10; // if scrollCounter is larger than this, a page switch is triggered
+ private int scrollCounter = 0; // a counter for measuring scrolling distance
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ boolean result = false;
+ try {
+ float diffY = e2.getY() - e1.getY();
+ float diffX = e2.getX() - e1.getX();
+ if (Math.abs(diffX) > Math.abs(diffY)) {
+ if (Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
+ if (diffX > 0) {
+ pageRight();
+ } else {
+ pageLeft();
+ }
+ result = true;
+ }
+ }
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ }
+ return result;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ boolean result = false;
+ try {
+ float diffY = e2.getY() - e1.getY();
+ float diffX = e2.getX() - e1.getX();
+ if (Math.abs(diffX) < Math.abs(diffY)) {
+ if (distanceY > 0) {
+ scrollCounter++;
+ if (scrollCounter >= SCROLL_THRESHOLD) {
+ pageRight();
+ scrollCounter = 0;
+ }
+ } else {
+ scrollCounter--;
+ if (scrollCounter <= -SCROLL_THRESHOLD) {
+ pageLeft();
+ scrollCounter = 0;
+ }
+ }
+ result = true;
+ }
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ }
+ return result;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (e.getX() < mGestureView.getWidth()/3) {
+ pageLeft();
+ } else if (e.getX() < mGestureView.getWidth()*2/3) {
+ hideControlButtons();
+ } else {
+ pageRight();
+ }
+ return true;
+ }
+ }
+
+ private void hideControlButtons() {
+ View[] views= {findViewById(R.id.slide_show_nav_prev),findViewById(R.id.slide_show_nav_next),findViewById(R.id.slide_show_nav_back)} ;
+ for (View view : views) {
+ if (view.getVisibility() == View.GONE) {
+ view.setVisibility(View.VISIBLE);
+ } else if (view.getVisibility() == View.VISIBLE) {
+ view.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void pageLeft() {
+ mWebView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT));
+ }
+
+ private void pageRight() {
+ mWebView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT));
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/SearchController.java b/android/source/src/java/org/libreoffice/SearchController.java
new file mode 100644
index 000000000..6095e1fd2
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/SearchController.java
@@ -0,0 +1,82 @@
+package org.libreoffice;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class SearchController implements View.OnClickListener {
+ private final LibreOfficeMainActivity mActivity;
+
+ private enum SearchDirection {
+ UP, DOWN
+ }
+
+ SearchController(LibreOfficeMainActivity activity) {
+ mActivity = activity;
+
+ activity.findViewById(R.id.button_search_up).setOnClickListener(this);
+ activity.findViewById(R.id.button_search_down).setOnClickListener(this);
+
+ ((EditText) mActivity.findViewById(R.id.search_string)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ // search downward when the "search button" on keyboard is pressed,
+ SearchDirection direction = SearchDirection.DOWN;
+ String searchText = ((EditText) mActivity.findViewById(R.id.search_string)).getText().toString();
+ float x = mActivity.getCurrentCursorPosition().centerX();
+ float y = mActivity.getCurrentCursorPosition().centerY();
+ search(searchText, direction, x, y);
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ private void search(String searchString, SearchDirection direction, float x, float y) {
+ try {
+ JSONObject rootJson = new JSONObject();
+
+ addProperty(rootJson, "SearchItem.SearchString", "string", searchString);
+ addProperty(rootJson, "SearchItem.Backward", "boolean", direction == SearchDirection.UP ? "true" : "false");
+ addProperty(rootJson, "SearchItem.SearchStartPointX", "long", String.valueOf((long) UnitConverter.pixelToTwip(x, LOKitShell.getDpi(mActivity))));
+ addProperty(rootJson, "SearchItem.SearchStartPointY", "long", String.valueOf((long) UnitConverter.pixelToTwip(y, LOKitShell.getDpi(mActivity))));
+ addProperty(rootJson, "SearchItem.Command", "long", String.valueOf(0)); // search all == 1
+
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:ExecuteSearch", rootJson.toString()));
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void addProperty(JSONObject json, String parentValue, String type, String value) throws JSONException {
+ JSONObject child = new JSONObject();
+ child.put("type", type);
+ child.put("value", value);
+ json.put(parentValue, child);
+ }
+
+ @Override
+ public void onClick(View view) {
+ ImageButton button = (ImageButton) view;
+
+ SearchDirection direction = SearchDirection.DOWN;
+ if (button.getId() == R.id.button_search_up) {
+ direction = SearchDirection.UP;
+ }
+
+ String searchText = ((EditText) mActivity.findViewById(R.id.search_string)).getText().toString();
+
+ float x = mActivity.getCurrentCursorPosition().centerX();
+ float y = mActivity.getCurrentCursorPosition().centerY();
+ search(searchText, direction, x, y);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/SettingsActivity.java b/android/source/src/java/org/libreoffice/SettingsActivity.java
new file mode 100644
index 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..c0c097747
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ThumbnailCreator.java
@@ -0,0 +1,121 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.widget.ImageView;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Create thumbnails for the parts of the document.
+ */
+public class ThumbnailCreator {
+ private static final String LOG_TAG = ThumbnailCreator.class.getSimpleName();
+ private static final int THUMBNAIL_SIZE = 256;
+
+ private static boolean needsThumbnailCreation(int partNumber, ImageView imageView) {
+ ThumbnailCreationTask thumbnailCreationTask = currentThumbnailCreationTask(imageView);
+
+ if (thumbnailCreationTask == null) {
+ return true;
+ }
+
+ if (thumbnailCreationTask.partNumber != partNumber) {
+ thumbnailCreationTask.cancel();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private static ThumbnailCreationTask currentThumbnailCreationTask(ImageView imageView) {
+ if (imageView == null) {
+ return null;
+ }
+ Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof ThumbnailDrawable) {
+ return ((ThumbnailDrawable) drawable).thumbnailCreationTask.get();
+ } else {
+ return null;
+ }
+ }
+
+ public void createThumbnail(int partNumber, ImageView imageView) {
+ if (needsThumbnailCreation(partNumber, imageView)) {
+ ThumbnailCreationTask task = new ThumbnailCreationTask(imageView, partNumber);
+ ThumbnailDrawable thumbnailDrawable = new ThumbnailDrawable(task);
+ imageView.setImageDrawable(thumbnailDrawable);
+ imageView.setMinimumHeight(THUMBNAIL_SIZE);
+ LOKitShell.sendThumbnailEvent(task);
+ }
+ }
+
+ static class ThumbnailDrawable extends ColorDrawable {
+ public final WeakReference<ThumbnailCreationTask> thumbnailCreationTask;
+
+ public ThumbnailDrawable(ThumbnailCreationTask thumbnailCreationTask) {
+ super(Color.WHITE);
+ this.thumbnailCreationTask = new WeakReference<ThumbnailCreationTask>(thumbnailCreationTask);
+ }
+ }
+
+ class ThumbnailCreationTask{
+ private final WeakReference<ImageView> imageViewReference;
+ private final int partNumber;
+ private boolean cancelled = false;
+
+ public ThumbnailCreationTask(ImageView imageView, int partNumber) {
+ imageViewReference = new WeakReference<ImageView>(imageView);
+ this.partNumber = partNumber;
+ }
+
+ public void cancel() {
+ cancelled = true;
+ }
+
+ public Bitmap getThumbnail(TileProvider tileProvider) {
+ int currentPart = tileProvider.getCurrentPartNumber();
+ tileProvider.changePart(partNumber);
+ final Bitmap bitmap = tileProvider.thumbnail(THUMBNAIL_SIZE);
+ tileProvider.changePart(currentPart);
+ return bitmap;
+ }
+
+ private void changeBitmap(Bitmap bitmap) {
+ if (cancelled) {
+ bitmap = null;
+ }
+
+ if (imageViewReference == null) {
+ return;
+ }
+ ImageView imageView = imageViewReference.get();
+ ThumbnailCreationTask thumbnailCreationTask = currentThumbnailCreationTask(imageView);
+ if (this == thumbnailCreationTask) {
+ imageView.setImageBitmap(bitmap);
+ }
+ }
+
+ public void applyBitmap(final Bitmap bitmap) {
+ // run on UI thread
+ LibreOfficeApplication.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ changeBitmap(bitmap);
+ }
+ });
+ }
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/TileIdentifier.java b/android/source/src/java/org/libreoffice/TileIdentifier.java
new file mode 100644
index 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..c979a9883
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/TileProvider.java
@@ -0,0 +1,205 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.view.KeyEvent;
+
+import org.mozilla.gecko.gfx.CairoImage;
+import org.mozilla.gecko.gfx.IntSize;
+
+/**
+ * Provides the tiles and other document information.
+ */
+public interface TileProvider {
+
+ /**
+ * Save the current document under the given path.
+ * @param takeOwnership Whether to take ownership of the new file,
+ * i.e. whether the current document is changed to the
+ * newly saved document (takeOwnership = true),
+ * as compared to just saving a copy of the current document
+ * or exporting to a different file format.
+ * Must be 'false' when using this method for export to e.g. PNG or PDF.
+ * @return Whether saving was successful.
+ */
+ boolean saveDocumentAs(String filePath, String format, boolean takeOwnership);
+
+ /**
+ * Saves the current document under the given path,
+ * using the default file format.
+ * @param takeOwnership (s. documentation for
+ * 'saveDocumentAs(String filePath, String format, boolean takeOwnership)')
+ */
+ boolean saveDocumentAs(String filePath, boolean takeOwnership);
+
+ /**
+ * Returns the page width in pixels.
+ */
+ int getPageWidth();
+
+ /**
+ * Returns the page height in pixels.
+ */
+ int getPageHeight();
+
+ boolean isReady();
+
+ CairoImage createTile(float x, float y, IntSize tileSize, float zoom);
+
+ /**
+ * Rerender and overwrite tile's image buffer directly
+ */
+ void rerenderTile(CairoImage image, float x, float y, IntSize tileSize, float zoom);
+
+ /**
+ * Change the document part to the one specified by the partIndex input parameter.
+ *
+ * @param partIndex - part index to change to
+ */
+ void changePart(int partIndex);
+
+ /**
+ * Get the current document part number.
+ *
+ * @return
+ */
+ int getCurrentPartNumber();
+
+ /**
+ * Get the total number of parts.
+ */
+ int getPartsCount();
+
+ Bitmap thumbnail(int size);
+
+ /**
+ * Closes the document.
+ */
+ void close();
+
+ /**
+ * Returns true if the current open document is a drawing.
+ */
+ boolean isDrawing();
+
+ /**
+ * Returns true if the current open document is a text document.
+ */
+ boolean isTextDocument();
+
+ /**
+ * Returns true if the current open document is a spreadsheet.
+ */
+ boolean isSpreadsheet();
+
+ /**
+ * Returns true if the current open document is a presentation
+ */
+ boolean isPresentation();
+
+ /**
+ * Trigger a key event.
+ *
+ * @param keyEvent - contains information about key event
+ */
+ void sendKeyEvent(KeyEvent keyEvent);
+
+ /**
+ * Trigger a mouse button down event.
+ *
+ * @param documentCoordinate - coordinate relative to the document where the mouse button should be triggered
+ * @param numberOfClicks - number of clicks (1 - single click, 2 - double click)
+ */
+ void mouseButtonDown(PointF documentCoordinate, int numberOfClicks, float zoomFactor);
+
+
+ /**
+ * Trigger a swipe left event.
+ */
+ void onSwipeLeft();
+
+ /**
+ * Trigger a swipe left event.
+ */
+ void onSwipeRight();
+
+ /**
+ * Trigger a mouse button up event.
+ *
+ * @param documentCoordinate - coordinate relative to the document where the mouse button should be triggered
+ * @param numberOfClicks - number of clicks (1 - single click, 2 - double click)
+ */
+ void mouseButtonUp(PointF documentCoordinate, int numberOfClicks, float zoomFactor);
+
+ /**
+ * Post a UNO command to LOK.
+ *
+ * @param command - the .uno: command, like ".uno:Bold"
+ */
+ void postUnoCommand(String command, String arguments);
+
+ /**
+ * This is the actual reference to the function in LOK, used for getting notified when uno:save event finishes
+ * @param command
+ * @param arguments
+ * @param notifyWhenFinished
+ */
+ void postUnoCommand(String command, String arguments, boolean notifyWhenFinished);
+
+ /**
+ * Send text selection start coordinate.
+ * @param documentCoordinate
+ */
+ void setTextSelectionStart(PointF documentCoordinate);
+
+ /**
+ * Send text selection end coordinate.
+ * @param documentCoordinate
+ */
+ void setTextSelectionEnd(PointF documentCoordinate);
+
+ /**
+ * get selected text
+ * @param mimeType
+ */
+ String getTextSelection(String mimeType);
+
+ /**
+ * copy
+ * @param mimeType
+ * @param data
+ * @return
+ */
+ boolean paste(String mimeType, String data);
+ /**
+ * Send text selection reset coordinate.
+ * @param documentCoordinate
+ */
+ void setTextSelectionReset(PointF documentCoordinate);
+
+ /**
+ * Send a request to change start the change of graphic selection.
+ */
+ void setGraphicSelectionStart(PointF documentCoordinate);
+
+ /**
+ * Send a request to change end the change of graphic selection...
+ */
+ void setGraphicSelectionEnd(PointF documentCoordinate);
+
+ /**
+ * Set the new page size of the document when changed
+ */
+ void setDocumentSize(int pageWidth, int pageHeight);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/TileProviderFactory.java b/android/source/src/java/org/libreoffice/TileProviderFactory.java
new file mode 100644
index 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..9f5c5309a
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ToolbarController.java
@@ -0,0 +1,274 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import androidx.appcompat.widget.Toolbar;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+/**
+ * Controls the changes to the toolbar.
+ */
+public class ToolbarController implements Toolbar.OnMenuItemClickListener {
+ private static final String LOGTAG = ToolbarController.class.getSimpleName();
+ private final Toolbar mToolbarTop;
+
+ private final LibreOfficeMainActivity mContext;
+ private final Menu mMainMenu;
+
+ private boolean isEditModeOn = false;
+ private String clipboardText = null;
+ ClipboardManager clipboardManager;
+ ClipData clipData;
+
+ public ToolbarController(LibreOfficeMainActivity context, Toolbar toolbarTop) {
+ mToolbarTop = toolbarTop;
+ mContext = context;
+
+ mToolbarTop.inflateMenu(R.menu.main);
+ mToolbarTop.setOnMenuItemClickListener(this);
+ switchToViewMode();
+
+ mMainMenu = mToolbarTop.getMenu();
+ clipboardManager = (ClipboardManager)mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ }
+
+ private void enableMenuItem(final int menuItemId, final boolean enabled) {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ public void run() {
+ MenuItem menuItem = mMainMenu.findItem(menuItemId);
+ if (menuItem != null) {
+ menuItem.setEnabled(enabled);
+ } else {
+ Log.e(LOGTAG, "MenuItem not found.");
+ }
+ }
+ });
+ }
+
+ public void setEditModeOn(boolean enabled) {
+ isEditModeOn = enabled;
+ }
+
+ public boolean getEditModeStatus() {
+ return isEditModeOn;
+ }
+
+ /**
+ * Change the toolbar to edit mode.
+ */
+ void switchToEditMode() {
+ if (!LOKitShell.isEditingEnabled())
+ return;
+
+ setEditModeOn(true);
+ // Ensure the change is done on UI thread
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.setGroupVisible(R.id.group_edit_actions, true);
+ if (!LibreOfficeMainActivity.isDeveloperMode() && mMainMenu.findItem(R.id.action_UNO_commands) != null) {
+ mMainMenu.findItem(R.id.action_UNO_commands).setVisible(false);
+ } else {
+ mMainMenu.findItem(R.id.action_UNO_commands).setVisible(true);
+ }
+ if(mContext.getTileProvider() != null && mContext.getTileProvider().isSpreadsheet()){
+ mMainMenu.setGroupVisible(R.id.group_spreadsheet_options, true);
+ } else if(mContext.getTileProvider() != null && mContext.getTileProvider().isPresentation()){
+ mMainMenu.setGroupVisible(R.id.group_presentation_options, true);
+ }
+ mToolbarTop.setNavigationIcon(R.drawable.ic_check);
+ mToolbarTop.setLogo(null);
+ }
+ });
+ }
+
+ /**
+ * Show clipboard Actions on the toolbar
+ * */
+ void showClipboardActions(final String value){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if(value != null){
+ mMainMenu.setGroupVisible(R.id.group_edit_actions, false);
+ mMainMenu.setGroupVisible(R.id.group_edit_clipboard, true);
+ if(getEditModeStatus()){
+ showHideClipboardCutAndCopy(true);
+ } else {
+ mMainMenu.findItem(R.id.action_cut).setVisible(false);
+ mMainMenu.findItem(R.id.action_paste).setVisible(false);
+ }
+ clipboardText = value;
+ }
+ }
+ });
+ }
+
+ void hideClipboardActions(){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.setGroupVisible(R.id.group_edit_actions, getEditModeStatus());
+ mMainMenu.setGroupVisible(R.id.group_edit_clipboard, false);
+ }
+ });
+ }
+
+ void showHideClipboardCutAndCopy(final boolean option){
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.findItem(R.id.action_copy).setVisible(option);
+ mMainMenu.findItem(R.id.action_cut).setVisible(option);
+ }
+ });
+ }
+
+ /**
+ * Change the toolbar to view mode.
+ */
+ void switchToViewMode() {
+ // Ensure the change is done on UI thread
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mMainMenu.setGroupVisible(R.id.group_edit_actions, false);
+ mToolbarTop.setNavigationIcon(R.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) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.action_keyboard) {
+ mContext.showSoftKeyboard();
+ } else if (itemId == R.id.action_format) {
+ mContext.showFormattingToolbar();
+ } else if (itemId == R.id.action_about) {
+ mContext.showAbout();
+ return true;
+ } else if (itemId == R.id.action_save) {
+ mContext.getTileProvider().saveDocument();
+ return true;
+ } else if (itemId == R.id.action_save_as) {
+ mContext.saveDocumentAs();
+ return true;
+ } else if (itemId == R.id.action_parts) {
+ mContext.openDrawer();
+ return true;
+ } else if (itemId == R.id.action_exportToPDF) {
+ mContext.exportToPDF();
+ return true;
+ } else if (itemId == R.id.action_print) {
+ mContext.getTileProvider().printDocument();
+ return true;
+ } else if (itemId == R.id.action_settings) {
+ mContext.showSettings();
+ return true;
+ } else if (itemId == R.id.action_search) {
+ mContext.showSearchToolbar();
+ return true;
+ } else if (itemId == R.id.action_undo) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Undo"));
+ return true;
+ } else if (itemId == R.id.action_redo) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Redo"));
+ return true;
+ } else if (itemId == R.id.action_presentation) {
+ mContext.preparePresentation();
+ return true;
+ } else if (itemId == R.id.action_add_slide || itemId == R.id.action_add_worksheet) {
+ mContext.addPart();
+ return true;
+ } else if (itemId == R.id.action_rename_worksheet || itemId == R.id.action_rename_slide) {
+ mContext.renamePart();
+ return true;
+ } else if (itemId == R.id.action_delete_worksheet || itemId == R.id.action_delete_slide) {
+ mContext.deletePart();
+ return true;
+ } else if (itemId == R.id.action_back) {
+ hideClipboardActions();
+ return true;
+ } else if (itemId == R.id.action_copy) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Copy"));
+ clipData = ClipData.newPlainText("clipboard data", clipboardText);
+ clipboardManager.setPrimaryClip(clipData);
+ Toast.makeText(mContext, mContext.getResources().getString(R.string.action_text_copied), Toast.LENGTH_SHORT).show();
+ return true;
+ } else if (itemId == R.id.action_paste) {
+ clipData = clipboardManager.getPrimaryClip();
+ ClipData.Item clipItem = clipData.getItemAt(0);
+ mContext.setDocumentChanged(true);
+ return mContext.getTileProvider().paste("text/plain;charset=utf-16", clipItem.getText().toString());
+ } else if (itemId == R.id.action_cut) {
+ clipData = ClipData.newPlainText("clipboard data", clipboardText);
+ clipboardManager.setPrimaryClip(clipData);
+ LOKitShell.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
+ mContext.setDocumentChanged(true);
+ return true;
+ } else if (itemId == R.id.action_UNO_commands) {
+ mContext.showUNOCommandsToolbar();
+ return true;
+ }
+ return false;
+ }
+
+ void setupToolbars() {
+ if (LibreOfficeMainActivity.isExperimentalMode()) {
+ boolean enableSaveEntry = !LibreOfficeMainActivity.isReadOnlyMode() && mContext.hasLocationForSave();
+ enableMenuItem(R.id.action_save, enableSaveEntry);
+ if (LibreOfficeMainActivity.isReadOnlyMode()) {
+ // show message in case experimental mode is enabled (i.e. editing is supported in general),
+ // but current document is readonly
+ Toast.makeText(mContext, mContext.getString(R.string.readonly_file), Toast.LENGTH_LONG).show();
+ }
+ }
+ 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..cba67732c
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/UNOCommandsController.java
@@ -0,0 +1,85 @@
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice;
+
+import android.content.DialogInterface;
+import androidx.appcompat.app.AlertDialog;
+import android.text.method.ScrollingMovementMethod;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Scroller;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import static org.libreoffice.SearchController.addProperty;
+
+class UNOCommandsController implements View.OnClickListener {
+ private final LibreOfficeMainActivity mActivity;
+ private JSONObject mRootJSON = new JSONObject();
+
+
+ UNOCommandsController(LibreOfficeMainActivity activity) {
+ mActivity = activity;
+
+ activity.findViewById(R.id.button_send_UNO_commands).setOnClickListener(this);
+ activity.findViewById(R.id.button_send_UNO_commands_clear).setOnClickListener(this);
+ activity.findViewById(R.id.button_send_UNO_commands_show).setOnClickListener(this);
+ activity.findViewById(R.id.button_add_property).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view.getId() == R.id.button_send_UNO_commands) {
+ String cmdText = ((EditText) mActivity.findViewById(R.id.UNO_commands_string)).getText().toString();
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:" + cmdText, mRootJSON.toString()));
+ } else if (view.getId() == R.id.button_add_property) {
+ String parentValue = ((EditText) mActivity.findViewById(R.id.UNO_commands_string_parent_value)).getText().toString();
+ String type = ((EditText) mActivity.findViewById(R.id.UNO_commands_string_type)).getText().toString();
+ String value = ((EditText) mActivity.findViewById(R.id.UNO_commands_string_value)).getText().toString();
+ try {
+ addProperty(mRootJSON, parentValue, type, value);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ showCommandDialog();
+ } else if (view.getId() == R.id.button_send_UNO_commands_clear) {
+ mRootJSON = new JSONObject();
+ ((EditText) mActivity.findViewById(R.id.UNO_commands_string_parent_value)).setText("");
+ ((EditText) mActivity.findViewById(R.id.UNO_commands_string_type)).setText("");
+ ((EditText) mActivity.findViewById(R.id.UNO_commands_string_value)).setText("");
+ showCommandDialog();
+ } else if (view.getId() == R.id.button_send_UNO_commands_show) {
+ showCommandDialog();
+ }
+ }
+
+ private void showCommandDialog() {
+ try {
+ AlertDialog dialog = new AlertDialog.Builder(mActivity)
+ .setTitle(R.string.current_uno_command)
+ .setMessage(mRootJSON.toString(2))
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .show();
+ TextView textView = dialog.findViewById(android.R.id.message);
+ if (textView != null) {
+ textView.setScroller(new Scroller(mActivity));
+ textView.setVerticalScrollBarEnabled(true);
+ textView.setMovementMethod(new ScrollingMovementMethod());
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/UnitConverter.java b/android/source/src/java/org/libreoffice/UnitConverter.java
new file mode 100644
index 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..51f6f7cf8
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/canvas/BitmapHandle.java
@@ -0,0 +1,63 @@
+package org.libreoffice.canvas;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import androidx.core.content.ContextCompat;
+
+/**
+ * Bitmap handle canvas element is used to show a handle on the screen.
+ * The handle visual comes from the bitmap, which must be provided in time
+ * of construction.
+ */
+public abstract class BitmapHandle extends CommonCanvasElement {
+ public final RectF mDocumentPosition;
+ private final Bitmap mBitmap;
+ final RectF mScreenPosition;
+
+ BitmapHandle(Bitmap bitmap) {
+ mBitmap = bitmap;
+ mScreenPosition = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+ mDocumentPosition = new RectF();
+ }
+
+ /**
+ * Return a bitmap for a drawable id.
+ */
+ static Bitmap getBitmapForDrawable(Context context, int drawableId) {
+ Drawable drawable = ContextCompat.getDrawable(context, drawableId);
+
+ return ImageUtils.getBitmapForDrawable(drawable);
+ }
+
+ /**
+ * Draw the bitmap handle to the canvas.
+ * @param canvas - the canvas
+ */
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawBitmap(mBitmap, mScreenPosition.left, mScreenPosition.top, null);
+ }
+
+ /**
+ * Test if the bitmap has been hit.
+ * @param x - x coordinate
+ * @param y - y coordinate
+ * @return true if the bitmap has been hit
+ */
+ @Override
+ public boolean onHitTest(float x, float y) {
+ return mScreenPosition.contains(x, y);
+ }
+
+ /**
+ * Change the position of the handle.
+ * @param x - x coordinate
+ * @param y - y coordinate
+ */
+ public void reposition(float x, float y) {
+ mScreenPosition.offsetTo(x, y);
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/canvas/CalcHeaderCell.java b/android/source/src/java/org/libreoffice/canvas/CalcHeaderCell.java
new file mode 100644
index 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..8b99c292c
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/CalcHeadersController.java
@@ -0,0 +1,281 @@
+package org.libreoffice.overlay;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import com.google.android.material.snackbar.Snackbar;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.Button;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.LOEvent;
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.R;
+import org.mozilla.gecko.gfx.LayerView;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+
+import static org.libreoffice.SearchController.addProperty;
+
+public class CalcHeadersController {
+ private static final String LOGTAG = CalcHeadersController.class.getSimpleName();
+
+ private final CalcHeadersView mCalcRowHeadersView;
+ private final CalcHeadersView mCalcColumnHeadersView;
+
+ private LibreOfficeMainActivity mContext;
+
+ public CalcHeadersController(LibreOfficeMainActivity context, final LayerView layerView) {
+ mContext = context;
+ mContext.getDocumentOverlay().setCalcHeadersController(this);
+ mCalcRowHeadersView = context.findViewById(R.id.calc_header_row);
+ mCalcColumnHeadersView = context.findViewById(R.id.calc_header_column);
+ if (mCalcColumnHeadersView == null || mCalcRowHeadersView == null) {
+ Log.e(LOGTAG, "Failed to initialize Calc headers - View is null");
+ } else {
+ mCalcRowHeadersView.initialize(layerView, true);
+ mCalcColumnHeadersView.initialize(layerView, false);
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_CALC_HEADERS));
+ context.findViewById(R.id.calc_header_top_left).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectAll"));
+ if (mCalcColumnHeadersView == null) return;
+ mCalcColumnHeadersView.showHeaderPopup(new PointF());
+ }
+ });
+ ((EditText)context.findViewById(R.id.calc_address)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO) {
+ String text = v.getText().toString();
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "ToPoint", "string", text);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:GoToCell", rootJson.toString()));
+ mContext.hideSoftKeyboard();
+ layerView.requestFocus();
+ }
+ return true;
+ }
+ });
+ ((EditText)context.findViewById(R.id.calc_formula)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO) {
+ String text = v.getText().toString();
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "StringName", "string", text);
+ addProperty(rootJson, "DontCommit", "boolean", String.valueOf(false));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:EnterString", rootJson.toString()));
+ mContext.hideSoftKeyboard();
+ layerView.requestFocus();
+ mContext.setDocumentChanged(true);
+ }
+ return true;
+ }
+ });
+ // manually select A1 for address bar and formula bar to update when calc first opens
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "ToPoint", "string", "A1");
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:GoToCell", rootJson.toString()));
+ }
+
+ public void setupHeaderPopupView() {
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ String[] rowOrColumn = {"Row","Column"};
+ CalcHeadersView[] headersViews= {mCalcRowHeadersView, mCalcColumnHeadersView};
+ for (int i = 0; i < rowOrColumn.length; i++) {
+ // create popup window
+ final String tempName = rowOrColumn[i];
+ final CalcHeadersView tempView = headersViews[i];
+ final View headerPopupView = inflater.inflate(R.layout.calc_header_popup, null);
+ final PopupWindow popupWindow = new PopupWindow(headerPopupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
+ @Override
+ public void onDismiss() {
+ headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_dialog).setVisibility(View.GONE);
+ popupWindow.setFocusable(false);
+ }
+ });
+ popupWindow.setOutsideTouchable(true);
+ popupWindow.setBackgroundDrawable(new ColorDrawable());
+ popupWindow.setAnimationStyle(android.R.style.Animation_Dialog);
+ tempView.setHeaderPopupWindow(popupWindow);
+ // set up child views in the popup window
+ headerPopupView.findViewById(R.id.calc_header_popup_insert).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Insert"+tempName+"s"));
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_delete).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Delete"+tempName+"s"));
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_hide).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Hide"+tempName));
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_show).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Show"+tempName));
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_optimal_length).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ View view = headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_dialog);
+ if (view.getVisibility() == View.VISIBLE) {
+ view.setVisibility(View.GONE);
+ popupWindow.setFocusable(false);
+ popupWindow.update();
+ } else {
+ popupWindow.dismiss();
+ view.setVisibility(View.VISIBLE);
+ popupWindow.setFocusable(true);
+ popupWindow.showAtLocation(tempView, Gravity.CENTER, 0, 0);
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ Snackbar.make(tempView, R.string.calc_alert_double_click_optimal_length, Snackbar.LENGTH_LONG).show();
+ }
+ });
+ }
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String text = ((EditText)headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_text)).getText().toString();
+ tempView.sendOptimalLengthRequest(text);
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ headerPopupView.findViewById(R.id.calc_header_popup_adjust_length).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mContext.getDocumentOverlay().showAdjustLengthLine(tempView == mCalcRowHeadersView, tempView);
+ tempView.dismissPopupWindow();
+ mContext.setDocumentChanged(true);
+ }
+ });
+ ((Button)headerPopupView.findViewById(R.id.calc_header_popup_adjust_length))
+ .setText(tempView == mCalcRowHeadersView ? R.string.calc_adjust_height : R.string.calc_adjust_width);
+ ((Button)headerPopupView.findViewById(R.id.calc_header_popup_optimal_length))
+ .setText(tempView == mCalcRowHeadersView ? R.string.calc_optimal_height : R.string.calc_optimal_width);
+
+ }
+ }
+
+ public void setHeaders(String headers) {
+ HeaderInfo parsedHeaders = parseHeaderInfo(headers);
+ if (parsedHeaders != null) {
+ mCalcRowHeadersView.setHeaders(parsedHeaders.rowLabels, parsedHeaders.rowDimens);
+ mCalcColumnHeadersView.setHeaders(parsedHeaders.columnLabels, parsedHeaders.columnDimens);
+ showHeaders();
+ } else {
+ Log.e(LOGTAG, "Parse header info JSON failed.");
+ }
+ }
+
+ public void showHeaders() {
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mCalcColumnHeadersView.invalidate();
+ mCalcRowHeadersView.invalidate();
+ }
+ });
+ }
+
+ private HeaderInfo parseHeaderInfo(String headers) {
+ HeaderInfo headerInfo = new HeaderInfo();
+ try {
+ JSONObject collectiveResult = new JSONObject(headers);
+ JSONArray rowResult = collectiveResult.getJSONArray("rows");
+ for (int i = 0; i < rowResult.length(); i++) {
+ headerInfo.rowLabels.add(rowResult.getJSONObject(i).getString("text"));
+ headerInfo.rowDimens.add(BigDecimal.valueOf(rowResult.getJSONObject(i).getLong("size")).floatValue());
+ }
+ JSONArray columnResult = collectiveResult.getJSONArray("columns");
+ for (int i = 0; i < columnResult.length(); i++) {
+ headerInfo.columnLabels.add(columnResult.getJSONObject(i).getString("text"));
+ headerInfo.columnDimens.add(BigDecimal.valueOf(columnResult.getJSONObject(i).getLong("size")).floatValue());
+ }
+ return headerInfo;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public void showHeaderSelection(RectF cellCursorRect) {
+ mCalcRowHeadersView.setHeaderSelection(cellCursorRect);
+ mCalcColumnHeadersView.setHeaderSelection(cellCursorRect);
+ showHeaders();
+ }
+
+ public void setPendingRowOrColumnSelectionToShowUp(boolean b) {
+ mCalcRowHeadersView.setPendingRowOrColumnSelectionToShowUp(b);
+ mCalcColumnHeadersView.setPendingRowOrColumnSelectionToShowUp(b);
+ }
+
+ public boolean pendingRowOrColumnSelectionToShowUp() {
+ return mCalcColumnHeadersView.pendingRowOrColumnSelectionToShowUp()
+ || mCalcRowHeadersView.pendingRowOrColumnSelectionToShowUp();
+ }
+
+ private class HeaderInfo {
+ ArrayList<String> rowLabels;
+ ArrayList<Float> rowDimens;
+ ArrayList<String> columnLabels;
+ ArrayList<Float> columnDimens;
+ private HeaderInfo() {
+ rowLabels = new ArrayList<String>();
+ rowDimens = new ArrayList<Float>();
+ columnDimens = new ArrayList<Float>();
+ columnLabels = new ArrayList<String>();
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java b/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java
new file mode 100644
index 000000000..98af7a955
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java
@@ -0,0 +1,278 @@
+package org.libreoffice.overlay;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import androidx.core.view.GestureDetectorCompat;
+import android.util.AttributeSet;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.PopupWindow;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.libreoffice.LOEvent;
+import org.libreoffice.LOKitShell;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.canvas.CalcHeaderCell;
+import org.libreoffice.kit.Document;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+import org.mozilla.gecko.gfx.LayerView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import static org.libreoffice.SearchController.addProperty;
+
+public class CalcHeadersView extends View {
+ private static final String LOGTAG = CalcHeadersView.class.getSimpleName();
+
+ private boolean mInitialized;
+ private LayerView mLayerView;
+ private boolean mIsRow; // true if this is for row headers, false for column
+ private ArrayList<String> mLabels;
+ private ArrayList<Float> mDimens;
+ private RectF mCellCursorRect;
+ private boolean mPendingRowOrColumnSelectionToShowUp;
+ private GestureDetectorCompat mDetector;
+ private PopupWindow mPopupWindow;
+ private int mPrevScrollIndex = -1;
+
+ public CalcHeadersView(Context context) {
+ super(context);
+ }
+
+ public CalcHeadersView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CalcHeadersView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void initialize(LayerView layerView, boolean isRow) {
+ if (!mInitialized) {
+ mLayerView = layerView;
+ mIsRow = isRow;
+
+ LOKitShell.getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ mDetector = new GestureDetectorCompat(getContext(), new HeaderGestureListener());
+ }
+ });
+
+ setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mPrevScrollIndex = -1; // clear mPrevScrollIndex to default
+ }
+ return mDetector.onTouchEvent(event);
+ }
+ });
+
+ mInitialized = true;
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mInitialized && mDimens != null && mLabels != null) {
+ updateHeaders(canvas);
+ }
+ }
+
+ private void updateHeaders(Canvas canvas) {
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ float zoom = metrics.getZoomFactor();
+ PointF origin = metrics.getOrigin();
+
+ // Draw headers
+ boolean inRangeOfVisibleHeaders = false; // a helper variable for skipping unnecessary onDraw()'s
+ float top,bottom,left,right;
+ for (int i = 1; i < mLabels.size(); i++) {
+ if (mDimens.get(i).equals(mDimens.get(i-1))) continue;
+ if (mIsRow) {
+ top = -origin.y + zoom*mDimens.get(i-1);
+ bottom = -origin.y + zoom*mDimens.get(i);
+ if (top <= getHeight() && bottom >= 0) {
+ inRangeOfVisibleHeaders = true;
+ boolean isSelected = mCellCursorRect != null && bottom > mCellCursorRect.top - origin.y && top < mCellCursorRect.bottom - origin.y;
+ new CalcHeaderCell(0f, top, getWidth(), bottom - top, mLabels.get(i), isSelected).onDraw(canvas);
+ } else {
+ if (inRangeOfVisibleHeaders) {
+ break;
+ }
+ }
+ } else {
+ left = -origin.x + zoom*mDimens.get(i-1);
+ right = -origin.x + zoom*mDimens.get(i);
+ if (left <= getWidth() && right >= 0) {
+ boolean isSelected = mCellCursorRect != null && right > mCellCursorRect.left - origin.x && left < mCellCursorRect.right - origin.x;
+ new CalcHeaderCell(left, 0f, right - left, getHeight(), mLabels.get(i), isSelected).onDraw(canvas);
+ } else {
+ if (inRangeOfVisibleHeaders) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle a single tap event on a header cell.
+ * Selects whole row/column.
+ */
+ private void highlightRowOrColumn(PointF point, boolean shift) {
+ int index = getIndexFromPointOfTouch(point);
+ try {
+ JSONObject rootJson = new JSONObject();
+ if (shift) {
+ addProperty(rootJson, "Modifier", "unsigned short",
+ String.valueOf(Document.KEYBOARD_MODIFIER_SHIFT));
+ } else {
+ addProperty(rootJson, "Modifier", "unsigned short", "0");
+ }
+ if (mIsRow) {
+ addProperty(rootJson, "Row", "unsigned short", String.valueOf(index));
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectRow", rootJson.toString()));
+ } else {
+ addProperty(rootJson, "Col", "unsigned short", String.valueOf(index));
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectColumn", rootJson.toString()));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ // At this point, InvalidationHandler.java will have received two callbacks.
+ // One is for text selection (first) and the other for cell selection (second).
+ // The second will override the first on headers which is not wanted.
+ // setPendingRowOrColumnSelectionToShowUp(true) will skip the second call.
+ setPendingRowOrColumnSelectionToShowUp(true);
+ }
+
+ public int getIndexFromPointOfTouch(PointF point) {
+ int searchedIndex, index;
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ float zoom = metrics.getZoomFactor();
+ PointF origin = metrics.getOrigin();
+ if (mIsRow) {
+ searchedIndex = Collections.binarySearch(mDimens, (point.y+origin.y)/zoom);
+ } else {
+ searchedIndex = Collections.binarySearch(mDimens, (point.x+origin.x)/zoom);
+ }
+ // converting searched index to real index on headers
+ if (searchedIndex < 0) {
+ index = - searchedIndex - 2;
+ } else {
+ index = searchedIndex;
+ }
+ return index;
+ }
+
+ public void setPendingRowOrColumnSelectionToShowUp(boolean b) {
+ mPendingRowOrColumnSelectionToShowUp = b;
+ }
+
+ public boolean pendingRowOrColumnSelectionToShowUp() {
+ return mPendingRowOrColumnSelectionToShowUp;
+ }
+
+ public void setHeaders(ArrayList<String> labels, ArrayList<Float> dimens) {
+ mLabels = labels;
+ mDimens = dimens;
+ }
+
+ public void setHeaderSelection(RectF cellCursorRect) {
+ mCellCursorRect = cellCursorRect;
+ }
+
+ public void showHeaderPopup(PointF point) {
+ if (mPopupWindow == null ||
+ !LibreOfficeMainActivity.isExperimentalMode()) return;
+ if (mIsRow) {
+ mPopupWindow.showAsDropDown(this, getWidth()*3/2, -getHeight()+(int)point.y);
+ } else {
+ mPopupWindow.showAsDropDown(this, (int)point.x, getHeight()/2);
+ }
+ }
+
+ public void dismissPopupWindow() {
+ if (mPopupWindow == null) return;
+ mPopupWindow.dismiss();
+ }
+
+ public void setHeaderPopupWindow(PopupWindow popupWindow) {
+ if (mPopupWindow != null) return;
+ mPopupWindow = popupWindow;
+ }
+
+ public void sendOptimalLengthRequest(String text) {
+ JSONObject rootJson = new JSONObject();
+ if (mIsRow) {
+ try {
+ addProperty(rootJson, "aExtraHeight", "unsigned short", text);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SetOptimalRowHeight", rootJson.toString()));
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ }
+ } else {
+ try {
+ addProperty(rootJson, "aExtraWidth", "unsigned short", text);
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SetOptimalColumnWidth", rootJson.toString()));
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+
+ private class HeaderGestureListener extends SimpleOnGestureListener {
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ PointF pointOfTouch = new PointF(e.getX(), e.getY());
+ highlightRowOrColumn(pointOfTouch, false);
+ showHeaderPopup(pointOfTouch);
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ PointF point2 = new PointF(e2.getX(), e2.getY());
+ if (mPrevScrollIndex != getIndexFromPointOfTouch(point2)) {
+ mPrevScrollIndex = getIndexFromPointOfTouch(point2);
+ highlightRowOrColumn(point2, true);
+ dismissPopupWindow();
+ showHeaderPopup(point2);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ PointF pointOfTouch = new PointF(e.getX(), e.getY());
+ highlightRowOrColumn(pointOfTouch, false);
+ if (mIsRow) {
+ JSONObject rootJson = new JSONObject();
+ try {
+ addProperty(rootJson, "aExtraHeight", "unsigned short", String.valueOf(0));
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ }
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND,".uno:SetOptimalRowHeight", rootJson.toString()));
+ } else {
+ LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND,".uno:SetOptimalColumnWidthDirect"));
+ }
+ showHeaderPopup(pointOfTouch);
+ return true;
+ }
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java b/android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java
new file mode 100644
index 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..086108cd9
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java
@@ -0,0 +1,552 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice.overlay;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.R;
+import org.libreoffice.canvas.AdjustLengthLine;
+import org.libreoffice.canvas.CalcSelectionBox;
+import org.libreoffice.canvas.Cursor;
+import org.libreoffice.canvas.GraphicSelection;
+import org.libreoffice.canvas.PageNumberRect;
+import org.libreoffice.canvas.SelectionHandle;
+import org.libreoffice.canvas.SelectionHandleEnd;
+import org.libreoffice.canvas.SelectionHandleMiddle;
+import org.libreoffice.canvas.SelectionHandleStart;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.RectUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Document overlay view is responsible for showing the client drawn overlay
+ * elements like cursor, selection and graphic selection, and manipulate them.
+ */
+public class DocumentOverlayView extends View implements View.OnTouchListener {
+ private static final String LOGTAG = DocumentOverlayView.class.getSimpleName();
+
+ private static final int CURSOR_BLINK_TIME = 500;
+
+ private boolean mInitialized = false;
+
+ private List<RectF> mSelections = new ArrayList<RectF>();
+ private List<RectF> mScaledSelections = new ArrayList<RectF>();
+ private Paint mSelectionPaint = new Paint();
+ private boolean mSelectionsVisible;
+
+ private GraphicSelection mGraphicSelection;
+
+ private boolean mGraphicSelectionMove = false;
+
+ private LayerView mLayerView;
+
+ private SelectionHandle mHandleMiddle;
+ private SelectionHandle mHandleStart;
+ private SelectionHandle mHandleEnd;
+
+ private Cursor mCursor;
+
+ private SelectionHandle mDragHandle = null;
+
+ private List<RectF> mPartPageRectangles;
+ private PageNumberRect mPageNumberRect;
+ private boolean mPageNumberAvailable = false;
+ private int previousIndex = 0; // previous page number, used to compare with the current
+ private CalcHeadersController mCalcHeadersController;
+
+ private CalcSelectionBox mCalcSelectionBox;
+ private boolean mCalcSelectionBoxDragging;
+ private AdjustLengthLine mAdjustLengthLine;
+ private boolean mAdjustLengthLineDragging;
+
+ public DocumentOverlayView(Context context) {
+ super(context);
+ }
+
+ public DocumentOverlayView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DocumentOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Initialize the selection and cursor view.
+ */
+ public void initialize(LayerView layerView) {
+ if (!mInitialized) {
+ setOnTouchListener(this);
+ mLayerView = layerView;
+
+ mCursor = new Cursor();
+ mCursor.setVisible(false);
+
+ mSelectionPaint.setColor(Color.BLUE);
+ mSelectionPaint.setAlpha(50);
+ mSelectionsVisible = false;
+
+ mGraphicSelection = new GraphicSelection((LibreOfficeMainActivity) getContext());
+ mGraphicSelection.setVisible(false);
+
+ postDelayed(cursorAnimation, CURSOR_BLINK_TIME);
+
+ mHandleMiddle = new SelectionHandleMiddle((LibreOfficeMainActivity) getContext());
+ mHandleStart = new SelectionHandleStart((LibreOfficeMainActivity) getContext());
+ mHandleEnd = new SelectionHandleEnd((LibreOfficeMainActivity) getContext());
+
+ mInitialized = true;
+ }
+ }
+
+ /**
+ * Change the cursor position.
+ * @param position - new position of the cursor
+ */
+ public void changeCursorPosition(RectF position) {
+ if (RectUtils.fuzzyEquals(mCursor.mPosition, position)) {
+ return;
+ }
+ mCursor.mPosition = position;
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ }
+
+ /**
+ * Change the text selection rectangles.
+ * @param selectionRects - list of text selection rectangles
+ */
+ public void changeSelections(List<RectF> selectionRects) {
+ mSelections = selectionRects;
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ }
+
+ /**
+ * Change the graphic selection rectangle.
+ * @param rectangle - new graphic selection rectangle
+ */
+ public void changeGraphicSelection(RectF rectangle) {
+ if (RectUtils.fuzzyEquals(mGraphicSelection.mRectangle, rectangle)) {
+ return;
+ }
+
+ mGraphicSelection.mRectangle = rectangle;
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ }
+
+ public void repositionWithViewport(float x, float y, float zoom) {
+ RectF rect = convertToScreen(mCursor.mPosition, x, y, zoom);
+ mCursor.reposition(rect);
+
+ rect = convertToScreen(mHandleMiddle.mDocumentPosition, x, y, zoom);
+ mHandleMiddle.reposition(rect.left, rect.bottom);
+
+ rect = convertToScreen(mHandleStart.mDocumentPosition, x, y, zoom);
+ mHandleStart.reposition(rect.left, rect.bottom);
+
+ rect = convertToScreen(mHandleEnd.mDocumentPosition, x, y, zoom);
+ mHandleEnd.reposition(rect.left, rect.bottom);
+
+ mScaledSelections.clear();
+ for (RectF selection : mSelections) {
+ RectF scaledSelection = convertToScreen(selection, x, y, zoom);
+ mScaledSelections.add(scaledSelection);
+ }
+
+ if (mCalcSelectionBox != null) {
+ rect = convertToScreen(mCalcSelectionBox.mDocumentPosition, x, y, zoom);
+ mCalcSelectionBox.reposition(rect);
+ }
+
+ if (mGraphicSelection != null && mGraphicSelection.mRectangle != null) {
+ RectF scaledGraphicSelection = convertToScreen(mGraphicSelection.mRectangle, x, y, zoom);
+ mGraphicSelection.reposition(scaledGraphicSelection);
+ }
+
+ invalidate();
+ }
+
+ /**
+ * Convert the input rectangle from document to screen coordinates
+ * according to current viewport data (x, y, zoom).
+ */
+ private static RectF convertToScreen(RectF inputRect, float x, float y, float zoom) {
+ RectF rect = RectUtils.scale(inputRect, zoom);
+ rect.offset(-x, -y);
+ return rect;
+ }
+
+ /**
+ * Set part page rectangles and initialize a page number rectangle object
+ * (canvas element).
+ */
+ public void setPartPageRectangles (List<RectF> rectangles) {
+ mPartPageRectangles = rectangles;
+ mPageNumberRect = new PageNumberRect();
+ mPageNumberAvailable = true;
+ }
+
+ /**
+ * Drawing on canvas.
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ mCursor.draw(canvas);
+
+ if (mPageNumberAvailable) {
+ mPageNumberRect.draw(canvas);
+ }
+
+ mHandleMiddle.draw(canvas);
+ mHandleStart.draw(canvas);
+ mHandleEnd.draw(canvas);
+
+ if (mSelectionsVisible) {
+ for (RectF selection : mScaledSelections) {
+ canvas.drawRect(selection, mSelectionPaint);
+ }
+ }
+
+ if (mCalcSelectionBox != null) {
+ mCalcSelectionBox.draw(canvas);
+ }
+
+ mGraphicSelection.draw(canvas);
+
+ if (mCalcHeadersController != null) {
+ mCalcHeadersController.showHeaders();
+ }
+
+ if (mAdjustLengthLine != null) {
+ mAdjustLengthLine.draw(canvas);
+ }
+ }
+
+ /**
+ * Cursor animation function. Switch the alpha between opaque and fully transparent.
+ */
+ private Runnable cursorAnimation = new Runnable() {
+ public void run() {
+ if (mCursor.isVisible()) {
+ mCursor.cycleAlpha();
+ invalidate();
+ }
+ postDelayed(cursorAnimation, CURSOR_BLINK_TIME);
+ }
+ };
+
+ /**
+ * Show the cursor on the view.
+ */
+ public void showCursor() {
+ if (!mCursor.isVisible()) {
+ mCursor.setVisible(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Hide the cursor.
+ */
+ public void hideCursor() {
+ if (mCursor.isVisible()) {
+ mCursor.setVisible(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Calculate and show page number according to current viewport position.
+ * In particular, this function compares the middle point of the
+ * view port with page rectangles and finds out which page the user
+ * is currently on. It does not update the associated canvas element
+ * unless there is a change of page number.
+ */
+ public void showPageNumberRect() {
+ if (null == mPartPageRectangles) return;
+ PointF midPoint = mLayerView.getLayerClient().convertViewPointToLayerPoint(new PointF(getWidth()/2f, getHeight()/2f));
+ int index = previousIndex;
+ // search which page the user in currently on. can enhance the search algorithm to binary search if necessary
+ for (RectF page : mPartPageRectangles) {
+ if (page.top < midPoint.y && midPoint.y < page.bottom) {
+ index = mPartPageRectangles.indexOf(page) + 1;
+ break;
+ }
+ }
+ // index == 0 applies to non-text document, i.e. don't show page info on non-text docs
+ if (index == 0) {
+ return;
+ }
+ // if page rectangle canvas element is not visible or the page number is changed, show
+ if (!mPageNumberRect.isVisible() || index != previousIndex) {
+ previousIndex = index;
+ String pageNumberString = getContext().getString(R.string.page) + " " + index + "/" + mPartPageRectangles.size();
+ mPageNumberRect.setPageNumberString(pageNumberString);
+ mPageNumberRect.setVisible(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Hide page number rectangle canvas element.
+ */
+ public void hidePageNumberRect() {
+ if (null == mPageNumberRect) return;
+ if (mPageNumberRect.isVisible()) {
+ mPageNumberRect.setVisible(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Show text selection rectangles.
+ */
+ public void showSelections() {
+ if (!mSelectionsVisible) {
+ mSelectionsVisible = true;
+ invalidate();
+ }
+ }
+
+ /**
+ * Hide text selection rectangles.
+ */
+ public void hideSelections() {
+ if (mSelectionsVisible) {
+ mSelectionsVisible = false;
+ invalidate();
+ }
+ }
+
+ /**
+ * Show the graphic selection on the view.
+ */
+ public void showGraphicSelection() {
+ if (!mGraphicSelection.isVisible()) {
+ mGraphicSelectionMove = false;
+ mGraphicSelection.reset();
+ mGraphicSelection.setVisible(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Hide the graphic selection.
+ */
+ public void hideGraphicSelection() {
+ if (mGraphicSelection.isVisible()) {
+ mGraphicSelection.setVisible(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Handle the triggered touch event.
+ */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ PointF point = new PointF(event.getX(), event.getY());
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ if (mAdjustLengthLine != null && !mAdjustLengthLine.contains(point.x, point.y)) {
+ mAdjustLengthLine.setVisible(false);
+ invalidate();
+ }
+ if (mGraphicSelection.isVisible()) {
+ // Check if inside graphic selection was hit
+ if (mGraphicSelection.contains(point.x, point.y)) {
+ mGraphicSelectionMove = true;
+ mGraphicSelection.dragStart(point);
+ invalidate();
+ return true;
+ }
+ } else {
+ if (mHandleStart.contains(point.x, point.y)) {
+ mHandleStart.dragStart(point);
+ mDragHandle = mHandleStart;
+ return true;
+ } else if (mHandleEnd.contains(point.x, point.y)) {
+ mHandleEnd.dragStart(point);
+ mDragHandle = mHandleEnd;
+ return true;
+ } else if (mHandleMiddle.contains(point.x, point.y)) {
+ mHandleMiddle.dragStart(point);
+ mDragHandle = mHandleMiddle;
+ return true;
+ } else if (mCalcSelectionBox != null &&
+ mCalcSelectionBox.contains(point.x, point.y) &&
+ !mHandleStart.isVisible()) {
+ mCalcSelectionBox.dragStart(point);
+ mCalcSelectionBoxDragging = true;
+ return true;
+ } else if (mAdjustLengthLine != null &&
+ mAdjustLengthLine.contains(point.x, point.y)) {
+ mAdjustLengthLine.dragStart(point);
+ mAdjustLengthLineDragging = true;
+ return true;
+ }
+ }
+ }
+ case MotionEvent.ACTION_UP: {
+ if (mGraphicSelection.isVisible() && mGraphicSelectionMove) {
+ mGraphicSelection.dragEnd(point);
+ mGraphicSelectionMove = false;
+ invalidate();
+ return true;
+ } else if (mDragHandle != null) {
+ mDragHandle.dragEnd(point);
+ mDragHandle = null;
+ } else if (mCalcSelectionBoxDragging) {
+ mCalcSelectionBox.dragEnd(point);
+ mCalcSelectionBoxDragging = false;
+ } else if (mAdjustLengthLineDragging) {
+ mAdjustLengthLine.dragEnd(point);
+ mAdjustLengthLineDragging = false;
+ invalidate();
+ }
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (mGraphicSelection.isVisible() && mGraphicSelectionMove) {
+ mGraphicSelection.dragging(point);
+ invalidate();
+ return true;
+ } else if (mDragHandle != null) {
+ mDragHandle.dragging(point);
+ } else if (mCalcSelectionBoxDragging) {
+ mCalcSelectionBox.dragging(point);
+ } else if (mAdjustLengthLineDragging) {
+ mAdjustLengthLine.dragging(point);
+ invalidate();
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Change the handle document position.
+ * @param type - the type of the handle
+ * @param position - the new document position
+ */
+ public void positionHandle(SelectionHandle.HandleType type, RectF position) {
+ SelectionHandle handle = getHandleForType(type);
+ if (RectUtils.fuzzyEquals(handle.mDocumentPosition, position)) {
+ return;
+ }
+
+ RectUtils.assign(handle.mDocumentPosition, position);
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ }
+
+ /**
+ * Hide the handle.
+ * @param type - type of the handle
+ */
+ public void hideHandle(SelectionHandle.HandleType type) {
+ SelectionHandle handle = getHandleForType(type);
+ if (handle.isVisible()) {
+ handle.setVisible(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Show the handle.
+ * @param type - type of the handle
+ */
+ public void showHandle(SelectionHandle.HandleType type) {
+ SelectionHandle handle = getHandleForType(type);
+ if (!handle.isVisible()) {
+ handle.setVisible(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns the handle instance for the input type.
+ */
+ private SelectionHandle getHandleForType(SelectionHandle.HandleType type) {
+ switch(type) {
+ case START:
+ return mHandleStart;
+ case END:
+ return mHandleEnd;
+ case MIDDLE:
+ return mHandleMiddle;
+ }
+ return null;
+ }
+
+ public RectF getCurrentCursorPosition() {
+ return mCursor.mPosition;
+ }
+
+ public void setCalcHeadersController(CalcHeadersController calcHeadersController) {
+ mCalcHeadersController = calcHeadersController;
+ mCalcSelectionBox = new CalcSelectionBox((LibreOfficeMainActivity) getContext());
+ }
+
+ public void showCellSelection(RectF cellCursorRect) {
+ if (mCalcHeadersController == null || mCalcSelectionBox == null) return;
+ if (RectUtils.fuzzyEquals(mCalcSelectionBox.mDocumentPosition, cellCursorRect)) {
+ return;
+ }
+
+ // show selection on main GL view (i.e. in the document)
+ RectUtils.assign(mCalcSelectionBox.mDocumentPosition, cellCursorRect);
+ mCalcSelectionBox.setVisible(true);
+
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+
+ // show selection on headers
+ if (!mCalcHeadersController.pendingRowOrColumnSelectionToShowUp()) {
+ showHeaderSelection(cellCursorRect);
+ } else {
+ mCalcHeadersController.setPendingRowOrColumnSelectionToShowUp(false);
+ }
+ }
+
+ public void showHeaderSelection(RectF rect) {
+ if (mCalcHeadersController == null) return;
+ mCalcHeadersController.showHeaderSelection(rect);
+ }
+
+ public void showAdjustLengthLine(boolean isRow, final CalcHeadersView view) {
+ mAdjustLengthLine = new AdjustLengthLine((LibreOfficeMainActivity) getContext(), view, isRow, getWidth(), getHeight());
+ ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
+ RectF position = convertToScreen(mCalcSelectionBox.mDocumentPosition, metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
+ mAdjustLengthLine.setScreenRect(position);
+ mAdjustLengthLine.setVisible(true);
+ invalidate();
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/FileUtilities.java b/android/source/src/java/org/libreoffice/ui/FileUtilities.java
new file mode 100644
index 000000000..902b30ed7
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/FileUtilities.java
@@ -0,0 +1,153 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.libreoffice.ui;
+
+import java.util.Map;
+import java.util.HashMap;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+import android.util.Log;
+
+public class FileUtilities {
+
+ private static final String LOGTAG = FileUtilities.class.getSimpleName();
+
+ // These have to be in sync with the file_view_modes resource.
+ static final int DOC = 0;
+ static final int CALC = 1;
+ static final int IMPRESS = 2;
+ static final int DRAWING = 3;
+
+ static final int UNKNOWN = 10;
+
+ public static final String MIMETYPE_OPENDOCUMENT_TEXT = "application/vnd.oasis.opendocument.text";
+ public static final String MIMETYPE_OPENDOCUMENT_SPREADSHEET = "application/vnd.oasis.opendocument.spreadsheet";
+ public static final String MIMETYPE_OPENDOCUMENT_PRESENTATION = "application/vnd.oasis.opendocument.presentation";
+ public static final String MIMETYPE_OPENDOCUMENT_GRAPHICS = "application/vnd.oasis.opendocument.graphics";
+ public static final String MIMETYPE_PDF = "application/pdf";
+
+ private static final Map<String, Integer> mExtnMap = new HashMap<String, Integer>();
+ static {
+ // Please keep this in sync with AndroidManifest.xml
+ // and 'SUPPORTED_MIME_TYPES' in LibreOfficeUIActivity.java
+
+ // ODF
+ mExtnMap.put(".odt", DOC);
+ mExtnMap.put(".odg", DRAWING);
+ mExtnMap.put(".odp", IMPRESS);
+ mExtnMap.put(".ods", CALC);
+ mExtnMap.put(".fodt", DOC);
+ mExtnMap.put(".fodg", DRAWING);
+ mExtnMap.put(".fodp", IMPRESS);
+ mExtnMap.put(".fods", CALC);
+
+ // ODF templates
+ mExtnMap.put(".ott", DOC);
+ mExtnMap.put(".otg", DRAWING);
+ mExtnMap.put(".otp", IMPRESS);
+ mExtnMap.put(".ots", CALC);
+
+ // MS
+ mExtnMap.put(".rtf", DOC);
+ mExtnMap.put(".doc", DOC);
+ mExtnMap.put(".vsd", DRAWING);
+ mExtnMap.put(".vsdx", DRAWING);
+ mExtnMap.put(".pub", DRAWING);
+ mExtnMap.put(".ppt", IMPRESS);
+ mExtnMap.put(".pps", IMPRESS);
+ mExtnMap.put(".xls", CALC);
+
+ // MS templates
+ mExtnMap.put(".dot", DOC);
+ mExtnMap.put(".pot", IMPRESS);
+ mExtnMap.put(".xlt", CALC);
+
+ // OOXML
+ mExtnMap.put(".docx", DOC);
+ mExtnMap.put(".pptx", IMPRESS);
+ mExtnMap.put(".ppsx", IMPRESS);
+ mExtnMap.put(".xlsx", CALC);
+
+ // OOXML templates
+ mExtnMap.put(".dotx", DOC);
+ mExtnMap.put(".potx", IMPRESS);
+ mExtnMap.put(".xltx", CALC);
+
+ // Other
+ mExtnMap.put(".csv", CALC);
+ mExtnMap.put(".wps", DOC);
+ mExtnMap.put(".key", IMPRESS);
+ mExtnMap.put(".abw", DOC);
+ mExtnMap.put(".pmd", DRAWING);
+ mExtnMap.put(".emf", DRAWING);
+ mExtnMap.put(".svm", DRAWING);
+ mExtnMap.put(".wmf", DRAWING);
+ mExtnMap.put(".svg", DRAWING);
+ }
+
+ public static String getExtension(String filename) {
+ if (filename == null)
+ return "";
+ int nExt = filename.lastIndexOf('.');
+ if (nExt < 0)
+ return "";
+ return filename.substring(nExt);
+ }
+
+ private static int lookupExtension(String filename) {
+ String extn = getExtension(filename);
+ if (!mExtnMap.containsKey(extn))
+ return UNKNOWN;
+ return mExtnMap.get(extn);
+ }
+
+ static int getType(String filename) {
+ int type = lookupExtension (filename);
+ Log.d(LOGTAG, "extn : " + filename + " -> " + type);
+ return type;
+ }
+
+ /**
+ * Returns whether the passed MIME type is one for a document template.
+ */
+ public static boolean isTemplateMimeType(final String mimeType) {
+ // this works for ODF and OOXML template MIME types
+ return mimeType != null && mimeType.endsWith("template");
+ }
+
+ /**
+ * Tries to retrieve the display (which should be the document name)
+ * for the given URI using the given resolver.
+ */
+ public static String retrieveDisplayNameForDocumentUri(ContentResolver resolver, Uri docUri) {
+ String displayName = "";
+ // try to retrieve original file name
+ Cursor cursor = null;
+ try {
+ String[] columns = {OpenableColumns.DISPLAY_NAME};
+ cursor = resolver.query(docUri, columns, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ displayName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
+ }
+ } catch (SecurityException e) {
+ // thrown e.g. when Uri has become invalid, e.g. corresponding file has been deleted
+ Log.i(LOGTAG, "SecurityException when trying to receive display name for Uri " + docUri);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return displayName;
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java b/android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java
new file mode 100644
index 000000000..a5fa78f22
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/LibreOfficeUIActivity.java
@@ -0,0 +1,473 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.libreoffice.ui;
+
+import android.Manifest;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.view.ViewCompat;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.appcompat.widget.Toolbar;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.OvershootInterpolator;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.libreoffice.AboutDialogFragment;
+import org.libreoffice.LibreOfficeMainActivity;
+import org.libreoffice.LocaleHelper;
+import org.libreoffice.R;
+import org.libreoffice.SettingsActivity;
+import org.libreoffice.SettingsListenerModel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class LibreOfficeUIActivity extends AppCompatActivity implements SettingsListenerModel.OnSettingsPreferenceChangedListener, View.OnClickListener{
+ public enum DocumentType {
+ WRITER,
+ CALC,
+ IMPRESS,
+ DRAW,
+ INVALID
+ }
+
+ private static final String LOGTAG = LibreOfficeUIActivity.class.getSimpleName();
+
+ public static final String EXPLORER_PREFS_KEY = "EXPLORER_PREFS";
+ private static final String RECENT_DOCUMENTS_KEY = "RECENT_DOCUMENT_URIS";
+ // delimiter used for storing multiple URIs in a string
+ private static final String RECENT_DOCUMENTS_DELIMITER = " ";
+ private static final String DISPLAY_LANGUAGE = "DISPLAY_LANGUAGE";
+
+ public static final String NEW_DOC_TYPE_KEY = "NEW_DOC_TYPE_KEY";
+ public static final String NEW_WRITER_STRING_KEY = "private:factory/swriter";
+ public static final String NEW_IMPRESS_STRING_KEY = "private:factory/simpress";
+ public static final String NEW_CALC_STRING_KEY = "private:factory/scalc";
+ public static final String NEW_DRAW_STRING_KEY = "private:factory/sdraw";
+
+ // keep this in sync with 'AndroidManifext.xml'
+ private static final String[] SUPPORTED_MIME_TYPES = {
+ "application/vnd.oasis.opendocument.text",
+ "application/vnd.oasis.opendocument.graphics",
+ "application/vnd.oasis.opendocument.presentation",
+ "application/vnd.oasis.opendocument.spreadsheet",
+ "application/vnd.oasis.opendocument.text-flat-xml",
+ "application/vnd.oasis.opendocument.graphics-flat-xml",
+ "application/vnd.oasis.opendocument.presentation-flat-xml",
+ "application/vnd.oasis.opendocument.spreadsheet-flat-xml",
+ "application/vnd.oasis.opendocument.text-template",
+ "application/vnd.oasis.opendocument.spreadsheet-template",
+ "application/vnd.oasis.opendocument.graphics-template",
+ "application/vnd.oasis.opendocument.presentation-template",
+ "application/rtf",
+ "text/rtf",
+ "application/msword",
+ "application/vnd.ms-powerpoint",
+ "application/vnd.ms-excel",
+ "application/vnd.visio",
+ "application/vnd.visio.xml",
+ "application/x-mspublisher",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+ "application/vnd.openxmlformats-officedocument.presentationml.template",
+ "text/csv",
+ "text/comma-separated-values",
+ "application/vnd.ms-works",
+ "application/vnd.apple.keynote",
+ "application/x-abiword",
+ "application/x-pagemaker",
+ "image/x-emf",
+ "image/x-svm",
+ "image/x-wmf",
+ "image/svg+xml",
+ };
+
+ private static final int REQUEST_CODE_OPEN_FILECHOOSER = 12345;
+
+ private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 0;
+
+ private Animation fabOpenAnimation;
+ private Animation fabCloseAnimation;
+ private boolean isFabMenuOpen = false;
+ private FloatingActionButton editFAB;
+ private FloatingActionButton writerFAB;
+ private FloatingActionButton drawFAB;
+ private FloatingActionButton impressFAB;
+ private FloatingActionButton calcFAB;
+ private LinearLayout drawLayout;
+ private LinearLayout writerLayout;
+ private LinearLayout impressLayout;
+ private LinearLayout calcLayout;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ readPreferences();
+ SettingsListenerModel.getInstance().setListener(this);
+
+ // init UI
+ createUI();
+ fabOpenAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_open);
+ fabCloseAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_close);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ Log.i(LOGTAG, "no permission to read external storage - asking for permission");
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ PERMISSION_WRITE_EXTERNAL_STORAGE);
+ }
+ }
+
+ @Override
+ protected void attachBaseContext(Context newBase) {
+ super.attachBaseContext(LocaleHelper.onAttach(newBase));
+ }
+
+ public void createUI() {
+ setContentView(R.layout.activity_document_browser);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+
+ if (actionBar != null) {
+ actionBar.setIcon(R.drawable.lo_icon);
+ }
+
+ 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);
+ TextView openFileView = findViewById(R.id.open_file_view);
+ openFileView.setOnClickListener(this);
+
+
+ RecyclerView recentRecyclerView = findViewById(R.id.list_recent);
+
+ SharedPreferences prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
+ String recentPref = prefs.getString(RECENT_DOCUMENTS_KEY, "");
+ String[] recentFileStrings = recentPref.split(RECENT_DOCUMENTS_DELIMITER);
+
+ final List<RecentFile> recentFiles = new ArrayList<>();
+ for (String recentFileString : recentFileStrings) {
+ Uri uri = Uri.parse(recentFileString);
+ String filename = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), uri);
+ if (!filename.isEmpty()) {
+ recentFiles.add(new RecentFile(uri, filename));
+ }
+ }
+
+ recentRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
+ recentRecyclerView.setAdapter(new RecentFilesAdapter(this, recentFiles));
+ }
+
+ private void expandFabMenu() {
+ ViewCompat.animate(editFAB).rotation(45.0F).withLayer().setDuration(300).setInterpolator(new OvershootInterpolator(10.0F)).start();
+ drawLayout.startAnimation(fabOpenAnimation);
+ impressLayout.startAnimation(fabOpenAnimation);
+ writerLayout.startAnimation(fabOpenAnimation);
+ calcLayout.startAnimation(fabOpenAnimation);
+ writerFAB.setClickable(true);
+ impressFAB.setClickable(true);
+ drawFAB.setClickable(true);
+ calcFAB.setClickable(true);
+ isFabMenuOpen = true;
+ }
+
+ private void collapseFabMenu() {
+ ViewCompat.animate(editFAB).rotation(0.0F).withLayer().setDuration(300).setInterpolator(new OvershootInterpolator(10.0F)).start();
+ writerLayout.startAnimation(fabCloseAnimation);
+ impressLayout.startAnimation(fabCloseAnimation);
+ drawLayout.startAnimation(fabCloseAnimation);
+ calcLayout.startAnimation(fabCloseAnimation);
+ writerFAB.setClickable(false);
+ impressFAB.setClickable(false);
+ drawFAB.setClickable(false);
+ calcFAB.setClickable(false);
+ isFabMenuOpen = false;
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (isFabMenuOpen) {
+ collapseFabMenu();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_CODE_OPEN_FILECHOOSER && resultCode == RESULT_OK) {
+ final Uri fileUri = data.getData();
+ openDocument(fileUri);
+ }
+ }
+
+ private void showSystemFilePickerAndOpenFile() {
+ Intent intent = new Intent();
+ if (Build.VERSION.SDK_INT >= 19) {
+ intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
+ }
+ else {
+ // Intent.ACTION_OPEN_DOCUMENT added in API level 19, but minSdkVersion is currently 16
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ }
+
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, SUPPORTED_MIME_TYPES);
+
+ try {
+ startActivityForResult(intent, REQUEST_CODE_OPEN_FILECHOOSER);
+ } catch (ActivityNotFoundException e) {
+ Log.w(LOGTAG, "No activity available that can handle the intent to open a document.");
+ }
+ }
+
+ public void openDocument(final Uri documentUri) {
+ // "forward" to LibreOfficeMainActivity to open the file
+ Intent intent = new Intent(Intent.ACTION_VIEW, documentUri);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+ addDocumentToRecents(documentUri);
+
+ String packageName = getApplicationContext().getPackageName();
+ ComponentName componentName = new ComponentName(packageName,
+ LibreOfficeMainActivity.class.getName());
+ intent.setComponent(componentName);
+ startActivity(intent);
+ }
+
+ private void loadNewDocument(DocumentType docType) {
+ final String newDocumentType;
+ if (docType == DocumentType.WRITER) {
+ newDocumentType = NEW_WRITER_STRING_KEY;
+ } else if (docType == DocumentType.CALC) {
+ newDocumentType = NEW_CALC_STRING_KEY;
+ } else if (docType == DocumentType.IMPRESS) {
+ newDocumentType = NEW_IMPRESS_STRING_KEY;
+ } else if (docType == DocumentType.DRAW) {
+ newDocumentType = NEW_DRAW_STRING_KEY;
+ } else {
+ Log.w(LOGTAG, "invalid document type passed to loadNewDocument method. Ignoring request");
+ return;
+ }
+
+ Intent intent = new Intent(LibreOfficeUIActivity.this, LibreOfficeMainActivity.class);
+ intent.putExtra(NEW_DOC_TYPE_KEY, newDocumentType);
+ startActivity(intent);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.view_menu, menu);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.action_about) {
+ AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
+ aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
+ return true;
+ }
+ if (itemId == R.id.action_settings) {
+ startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ public void readPreferences(){
+ SharedPreferences defaultPrefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
+ final String displayLanguage = defaultPrefs.getString(DISPLAY_LANGUAGE, LocaleHelper.SYSTEM_DEFAULT_LANGUAGE);
+ LocaleHelper.setLocale(this, displayLanguage);
+ }
+
+ @Override
+ public void settingsPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ readPreferences();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(LOGTAG, "onResume");
+ createUI();
+ }
+
+ private void addDocumentToRecents(Uri fileUri) {
+ SharedPreferences prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
+ if (Build.VERSION.SDK_INT < 19) {
+ // ContentResolver#takePersistableUriPermission only available from SDK level 19 on
+ Log.i(LOGTAG, "Recently used files not supported, requires SDK version >= 19.");
+ // drop potential entries
+ prefs.edit().putString(RECENT_DOCUMENTS_KEY, "").apply();
+ return;
+ }
+
+ // preserve permissions across device reboots,
+ // s. https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions
+ getContentResolver().takePersistableUriPermission(fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+ String newRecent = fileUri.toString();
+ List<String> recentsList = new ArrayList<>(Arrays.asList(prefs.getString(RECENT_DOCUMENTS_KEY, "").split(RECENT_DOCUMENTS_DELIMITER)));
+
+ // remove string if present, so that it doesn't appear multiple times
+ recentsList.remove(newRecent);
+
+ // put the new value in the first place
+ recentsList.add(0, newRecent);
+
+ /*
+ * 4 because the number of recommended items in App Shortcuts is 4, and also
+ * because it's a good number of recent items in general
+ */
+ final int RECENTS_SIZE = 4;
+
+ while (recentsList.size() > RECENTS_SIZE) {
+ recentsList.remove(RECENTS_SIZE);
+ }
+
+ // serialize to String that can be set for pref
+ String value = TextUtils.join(RECENT_DOCUMENTS_DELIMITER, recentsList);
+ prefs.edit().putString(RECENT_DOCUMENTS_KEY, value).apply();
+
+ //update app shortcuts (7.0 and above)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
+ ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
+
+ //Remove all shortcuts, and apply new ones.
+ shortcutManager.removeAllDynamicShortcuts();
+
+ ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
+ for (String recentDoc : recentsList) {
+ Uri docUri = Uri.parse(recentDoc);
+ String filename = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), docUri);
+ if (filename.isEmpty()) {
+ continue;
+ }
+
+ //find the appropriate drawable
+ int drawable = 0;
+ switch (FileUtilities.getType(filename)) {
+ case FileUtilities.DOC:
+ drawable = R.drawable.writer;
+ break;
+ case FileUtilities.CALC:
+ drawable = R.drawable.calc;
+ break;
+ case FileUtilities.DRAWING:
+ drawable = R.drawable.draw;
+ break;
+ case FileUtilities.IMPRESS:
+ drawable = R.drawable.impress;
+ break;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, docUri);
+ String packageName = this.getApplicationContext().getPackageName();
+ ComponentName componentName = new ComponentName(packageName, LibreOfficeMainActivity.class.getName());
+ intent.setComponent(componentName);
+
+ ShortcutInfo shortcut = new ShortcutInfo.Builder(this, filename)
+ .setShortLabel(filename)
+ .setLongLabel(filename)
+ .setIcon(Icon.createWithResource(this, drawable))
+ .setIntent(intent)
+ .build();
+
+ shortcuts.add(shortcut);
+ }
+ shortcutManager.setDynamicShortcuts(shortcuts);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == R.id.editFAB) {
+ // Intent.ACTION_CREATE_DOCUMENT, used in 'createNewFileDialog' requires SDK version 19
+ if (Build.VERSION.SDK_INT < 19) {
+ Toast.makeText(this,
+ getString(R.string.creating_new_files_not_supported), Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (isFabMenuOpen) {
+ collapseFabMenu();
+ } else {
+ expandFabMenu();
+ }
+ } else if (id == R.id.open_file_view) {
+ showSystemFilePickerAndOpenFile();
+ } else if (id == R.id.newWriterFAB) {
+ loadNewDocument(DocumentType.WRITER);
+ } else if (id == R.id.newImpressFAB) {
+ loadNewDocument(DocumentType.IMPRESS);
+ } else if (id == R.id.newCalcFAB) {
+ loadNewDocument(DocumentType.CALC);
+ } else if (id == R.id.newDrawFAB) {
+ loadNewDocument(DocumentType.DRAW);
+ }
+ }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/PageView.java b/android/source/src/java/org/libreoffice/ui/PageView.java
new file mode 100644
index 000000000..4c3f69562
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/PageView.java
@@ -0,0 +1,69 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+
+package org.libreoffice.ui;
+
+import org.libreoffice.R;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+public class PageView extends View{
+ private Bitmap bmp;
+ private Paint mPaintBlack;
+ private static final String LOGTAG = "PageView";
+
+ public PageView(Context context ) {
+ super(context);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);
+ initialise();
+ }
+ public PageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);
+ Log.d(LOGTAG, bmp.toString());
+ initialise();
+ }
+ public PageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ bmp = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_page);//load a "page"
+ initialise();
+ }
+
+ private void initialise(){
+ mPaintBlack = new Paint();
+ mPaintBlack.setARGB(255, 0, 0, 0);
+ Log.d(LOGTAG, " Doing some set-up");
+ }
+
+ public void setBitmap(Bitmap bmp){
+ this.bmp = bmp;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Log.d(LOGTAG, "Draw");
+ Log.d(LOGTAG, Integer.toString(bmp.getHeight()));
+ if( bmp != null ){
+ int horizontalMargin = (int) (canvas.getWidth()*0.1);
+ //int verticalMargin = (int) (canvas.getHeight()*0.1);
+ int verticalMargin = horizontalMargin;
+ canvas.drawBitmap(bmp, new Rect(0, 0, bmp.getWidth(), bmp.getHeight()),
+ new Rect(horizontalMargin,verticalMargin,canvas.getWidth()-horizontalMargin,
+ canvas.getHeight()-verticalMargin),
+ mPaintBlack);//
+ }
+ if( bmp == null)
+ canvas.drawText(getContext().getString(R.string.bmp_null), 100, 100, new Paint());
+ }
+
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/android/source/src/java/org/libreoffice/ui/RecentFile.java b/android/source/src/java/org/libreoffice/ui/RecentFile.java
new file mode 100644
index 000000000..fdcc688aa
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/RecentFile.java
@@ -0,0 +1,25 @@
+package org.libreoffice.ui;
+
+import android.net.Uri;
+
+/**
+ * An entry for a recently used file in the RecentFilesAdapter.
+ */
+public class RecentFile {
+
+ private final Uri uri;
+ private final String displayName;
+
+ public RecentFile(Uri docUri, String name) {
+ uri = docUri;
+ displayName = name;
+ }
+
+ public Uri getUri() {
+ return uri;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+}
diff --git a/android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java b/android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java
new file mode 100644
index 000000000..ef00b9fb6
--- /dev/null
+++ b/android/source/src/java/org/libreoffice/ui/RecentFilesAdapter.java
@@ -0,0 +1,93 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.libreoffice.ui;
+
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.libreoffice.R;
+
+import java.util.List;
+
+class RecentFilesAdapter extends RecyclerView.Adapter<RecentFilesAdapter.ViewHolder> {
+
+ private final LibreOfficeUIActivity mActivity;
+ private final List<RecentFile> recentFiles;
+
+ RecentFilesAdapter(LibreOfficeUIActivity activity, List<RecentFile> recentFiles) {
+ this.mActivity = activity;
+ this.recentFiles = recentFiles;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View item = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_recent_files, parent, false);
+ return new ViewHolder(item);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ final RecentFile entry = recentFiles.get(position);
+
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mActivity.openDocument(entry.getUri());
+ }
+ });
+
+ final String filename = entry.getDisplayName();
+ holder.textView.setText(filename);
+
+ int compoundDrawableInt = 0;
+
+ switch (FileUtilities.getType(filename)) {
+ case FileUtilities.DOC:
+ compoundDrawableInt = R.drawable.writer;
+ break;
+ case FileUtilities.CALC:
+ compoundDrawableInt = R.drawable.calc;
+ break;
+ case FileUtilities.DRAWING:
+ compoundDrawableInt = R.drawable.draw;
+ break;
+ case FileUtilities.IMPRESS:
+ compoundDrawableInt = R.drawable.impress;
+ break;
+ }
+
+ // set icon if known filetype was detected
+ if (compoundDrawableInt != 0)
+ holder.imageView.setImageDrawable(ContextCompat.getDrawable(mActivity, compoundDrawableInt));
+ }
+
+ @Override
+ public int getItemCount() {
+ return recentFiles.size();
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+
+ TextView textView;
+ ImageView imageView;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ this.textView = itemView.findViewById(R.id.textView);
+ this.imageView = itemView.findViewById(R.id.imageView);
+ }
+ }
+}