diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /android/source/src/java/org/libreoffice | |
parent | Initial commit. (diff) | |
download | libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.tar.xz libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
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); + } + } +} |