1 package org
.libreoffice
;
3 import android
.app
.Activity
;
4 import android
.app
.AlertDialog
;
5 import android
.content
.DialogInterface
;
6 import android
.content
.Intent
;
7 import android
.content
.pm
.PackageManager
;
8 import android
.content
.pm
.ResolveInfo
;
9 import android
.graphics
.Bitmap
;
10 import android
.graphics
.BitmapFactory
;
11 import android
.net
.Uri
;
12 import android
.os
.Environment
;
13 import android
.provider
.MediaStore
;
14 import com
.google
.android
.material
.snackbar
.Snackbar
;
15 import androidx
.core
.content
.FileProvider
;
16 import android
.util
.Log
;
17 import android
.view
.LayoutInflater
;
18 import android
.view
.View
;
19 import android
.widget
.ImageButton
;
20 import android
.widget
.TextView
;
22 import org
.json
.JSONException
;
23 import org
.json
.JSONObject
;
24 import org
.libreoffice
.kit
.Document
;
27 import java
.io
.FileOutputStream
;
28 import java
.io
.IOException
;
29 import java
.io
.InputStream
;
30 import java
.text
.SimpleDateFormat
;
31 import java
.util
.Date
;
32 import java
.util
.List
;
33 import java
.util
.Locale
;
35 import static org
.libreoffice
.SearchController
.addProperty
;
37 class FormattingController
implements View
.OnClickListener
{
38 private static final String LOGTAG
= ToolbarController
.class.getSimpleName();
39 private static final int TAKE_PHOTO
= 1;
40 private static final int SELECT_PHOTO
= 2;
41 private static final int IMAGE_BUFFER_SIZE
= 4 * 1024;
43 private final LibreOfficeMainActivity mContext
;
44 private String mCurrentPhotoPath
;
46 FormattingController(LibreOfficeMainActivity context
) {
49 mContext
.findViewById(R
.id
.button_insertFormatListBullets
).setOnClickListener(this);
50 mContext
.findViewById(R
.id
.button_insertFormatListNumbering
).setOnClickListener(this);
51 mContext
.findViewById(R
.id
.button_increaseIndent
).setOnClickListener(this);
52 mContext
.findViewById(R
.id
.button_decreaseIndent
).setOnClickListener(this);
54 mContext
.findViewById(R
.id
.button_bold
).setOnClickListener(this);
55 mContext
.findViewById(R
.id
.button_italic
).setOnClickListener(this);
56 mContext
.findViewById(R
.id
.button_strikethrough
).setOnClickListener(this);
57 mContext
.findViewById(R
.id
.button_underlined
).setOnClickListener(this);
58 mContext
.findViewById(R
.id
.button_clearformatting
).setOnClickListener(this);
60 mContext
.findViewById(R
.id
.button_align_left
).setOnClickListener(this);
61 mContext
.findViewById(R
.id
.button_align_center
).setOnClickListener(this);
62 mContext
.findViewById(R
.id
.button_align_right
).setOnClickListener(this);
63 mContext
.findViewById(R
.id
.button_align_justify
).setOnClickListener(this);
65 mContext
.findViewById(R
.id
.button_insert_line
).setOnClickListener(this);
66 mContext
.findViewById(R
.id
.button_insert_rect
).setOnClickListener(this);
67 mContext
.findViewById(R
.id
.button_insert_picture
).setOnClickListener(this);
69 mContext
.findViewById(R
.id
.button_insert_table
).setOnClickListener(this);
70 mContext
.findViewById(R
.id
.button_delete_table
).setOnClickListener(this);
72 mContext
.findViewById(R
.id
.button_font_shrink
).setOnClickListener(this);
73 mContext
.findViewById(R
.id
.button_font_grow
).setOnClickListener(this);
75 mContext
.findViewById(R
.id
.button_subscript
).setOnClickListener(this);
76 mContext
.findViewById(R
.id
.button_superscript
).setOnClickListener(this);
80 public void onClick(View view
) {
81 ImageButton button
= (ImageButton
) view
;
83 if (button
.isSelected()) {
84 button
.getBackground().setState(new int[]{-android
.R
.attr
.state_selected
});
86 button
.getBackground().setState(new int[]{android
.R
.attr
.state_selected
});
89 final int buttonId
= button
.getId();
90 if (buttonId
== R
.id
.button_insertFormatListBullets
) {
91 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:DefaultBullet"));
92 } else if (buttonId
== R
.id
.button_insertFormatListNumbering
) {
93 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:DefaultNumbering"));
94 } else if (buttonId
== R
.id
.button_increaseIndent
) {
95 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:IncrementIndent"));
96 } else if (buttonId
== R
.id
.button_decreaseIndent
) {
97 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:DecrementIndent"));
98 } else if (buttonId
== R
.id
.button_bold
) {
99 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:Bold"));
100 } else if (buttonId
== R
.id
.button_italic
) {
101 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:Italic"));
102 } else if (buttonId
== R
.id
.button_strikethrough
) {
103 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:Strikeout"));
104 } else if (buttonId
== R
.id
.button_clearformatting
) {
105 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:ResetAttributes"));
106 } else if (buttonId
== R
.id
.button_underlined
) {
107 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:UnderlineDouble"));
108 } else if (buttonId
== R
.id
.button_align_left
) {
109 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:LeftPara"));
110 } else if (buttonId
== R
.id
.button_align_center
) {
111 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:CenterPara"));
112 } else if (buttonId
== R
.id
.button_align_right
) {
113 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:RightPara"));
114 } else if (buttonId
== R
.id
.button_align_justify
) {
115 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:JustifyPara"));
116 } else if (buttonId
== R
.id
.button_insert_line
) {
117 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:Line"));
118 } else if (buttonId
== R
.id
.button_insert_rect
) {
119 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:Rect"));
120 } else if (buttonId
== R
.id
.button_font_shrink
) {
121 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:Shrink"));
122 } else if (buttonId
== R
.id
.button_font_grow
) {
123 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:Grow"));
124 } else if (buttonId
== R
.id
.button_subscript
) {
125 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:SubScript"));
126 }else if (buttonId
== R
.id
.button_superscript
) {
127 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:SuperScript"));
128 } else if (buttonId
== R
.id
.button_insert_picture
) {
130 } else if (buttonId
== R
.id
.button_insert_table
) {
132 } else if (buttonId
== R
.id
.button_delete_table
) {
137 void onToggleStateChanged(final int type
, final boolean selected
) {
138 LOKitShell
.getMainHandler().post(new Runnable() {
143 buttonId
= R
.id
.button_bold
;
145 case Document
.ITALIC
:
146 buttonId
= R
.id
.button_italic
;
148 case Document
.UNDERLINE
:
149 buttonId
= R
.id
.button_underlined
;
151 case Document
.STRIKEOUT
:
152 buttonId
= R
.id
.button_strikethrough
;
154 case Document
.ALIGN_LEFT
:
155 buttonId
= R
.id
.button_align_left
;
157 case Document
.ALIGN_CENTER
:
158 buttonId
= R
.id
.button_align_center
;
160 case Document
.ALIGN_RIGHT
:
161 buttonId
= R
.id
.button_align_right
;
163 case Document
.ALIGN_JUSTIFY
:
164 buttonId
= R
.id
.button_align_justify
;
166 case Document
.BULLET_LIST
:
167 buttonId
= R
.id
.button_insertFormatListBullets
;
169 case Document
.NUMBERED_LIST
:
170 buttonId
= R
.id
.button_insertFormatListNumbering
;
173 Log
.e(LOGTAG
, "Uncaptured state change type: " + type
);
177 ImageButton button
= mContext
.findViewById(buttonId
);
178 button
.setSelected(selected
);
180 button
.getBackground().setState(new int[]{android
.R
.attr
.state_selected
});
182 button
.getBackground().setState(new int[]{-android
.R
.attr
.state_selected
});
188 private void insertPicture() {
189 AlertDialog
.Builder builder
= new AlertDialog
.Builder(mContext
);
190 String
[] options
= {mContext
.getResources().getString(R
.string
.take_photo
),
191 mContext
.getResources().getString(R
.string
.select_photo
)};
192 builder
.setItems(options
, new DialogInterface
.OnClickListener() {
194 public void onClick(DialogInterface dialog
, int which
) {
197 dispatchTakePictureIntent();
200 sendImagePickingIntent();
203 sendImagePickingIntent();
210 private void insertTable() {
211 final AlertDialog
.Builder insertTableBuilder
= new AlertDialog
.Builder(mContext
);
212 insertTableBuilder
.setTitle(R
.string
.insert_table
);
213 LayoutInflater layoutInflater
= mContext
.getLayoutInflater();
214 View numberPicker
= layoutInflater
.inflate(R
.layout
.number_picker
, null);
215 final int minValue
= 1;
216 final int maxValue
= 20;
217 TextView npRowPositive
= numberPicker
.findViewById(R
.id
.number_picker_rows_positive
);
218 TextView npRowNegative
= numberPicker
.findViewById(R
.id
.number_picker_rows_negative
);
219 TextView npColPositive
= numberPicker
.findViewById(R
.id
.number_picker_cols_positive
);
220 TextView npColNegative
= numberPicker
.findViewById(R
.id
.number_picker_cols_negative
);
221 final TextView npRowCount
= numberPicker
.findViewById(R
.id
.number_picker_row_count
);
222 final TextView npColCount
= numberPicker
.findViewById(R
.id
.number_picker_col_count
);
224 View
.OnClickListener positiveButtonClickListener
= new View
.OnClickListener() {
226 public void onClick(View v
) {
227 int rowCount
= Integer
.parseInt(npRowCount
.getText().toString());
228 int colCount
= Integer
.parseInt(npColCount
.getText().toString());
229 final int id
= v
.getId();
230 if (id
== R
.id
.number_picker_rows_positive
&& rowCount
< maxValue
) {
231 npRowCount
.setText(String
.valueOf(++rowCount
));
232 } else if (id
== R
.id
.number_picker_cols_positive
&& colCount
< maxValue
) {
233 npColCount
.setText(String
.valueOf(++colCount
));
238 View
.OnClickListener negativeButtonClickListener
= new View
.OnClickListener() {
240 public void onClick(View v
) {
241 int rowCount
= Integer
.parseInt(npRowCount
.getText().toString());
242 int colCount
= Integer
.parseInt(npColCount
.getText().toString());
243 final int id
= v
.getId();
244 if (id
== R
.id
.number_picker_rows_negative
&& rowCount
> minValue
) {
245 npRowCount
.setText(String
.valueOf(--rowCount
));
246 } else if (id
== R
.id
.number_picker_cols_negative
&& colCount
> minValue
) {
247 npColCount
.setText(String
.valueOf(--colCount
));
252 npRowPositive
.setOnClickListener(positiveButtonClickListener
);
253 npColPositive
.setOnClickListener(positiveButtonClickListener
);
254 npRowNegative
.setOnClickListener(negativeButtonClickListener
);
255 npColNegative
.setOnClickListener(negativeButtonClickListener
);
257 insertTableBuilder
.setView(numberPicker
);
258 insertTableBuilder
.setNeutralButton(R
.string
.alert_cancel
, null);
259 insertTableBuilder
.setPositiveButton(R
.string
.alert_ok
, new DialogInterface
.OnClickListener() {
261 public void onClick(DialogInterface dialog
, int which
) {
264 JSONObject cols
= new JSONObject();
265 cols
.put("type", "long");
266 cols
.put("value", Integer
.valueOf(npColCount
.getText().toString()));
267 JSONObject rows
= new JSONObject();
268 rows
.put("type","long");
269 rows
.put("value",Integer
.valueOf(npRowCount
.getText().toString()));
270 JSONObject params
= new JSONObject();
271 params
.put("Columns", cols
);
272 params
.put("Rows", rows
);
273 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:InsertTable",params
.toString()));
274 LibreOfficeMainActivity
.setDocumentChanged(true);
275 } catch (JSONException e
) {
282 AlertDialog
.Builder insertBuilder
= new AlertDialog
.Builder(mContext
);
283 insertBuilder
.setTitle(R
.string
.select_insert_options
);
284 insertBuilder
.setNeutralButton(R
.string
.alert_cancel
, null);
285 final int[] selectedItem
= new int[1];
286 insertBuilder
.setSingleChoiceItems(mContext
.getResources().getStringArray(R
.array
.insertrowscolumns
), -1, new DialogInterface
.OnClickListener() {
288 public void onClick(DialogInterface dialog
, int which
) {
289 selectedItem
[0] = which
;
292 insertBuilder
.setPositiveButton(R
.string
.alert_ok
, new DialogInterface
.OnClickListener() {
294 public void onClick(DialogInterface dialog
, int which
) {
295 switch (selectedItem
[0]){
297 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:InsertRowsBefore"));
298 LibreOfficeMainActivity
.setDocumentChanged(true);
301 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:InsertRowsAfter"));
302 LibreOfficeMainActivity
.setDocumentChanged(true);
305 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:InsertColumnsBefore"));
306 LibreOfficeMainActivity
.setDocumentChanged(true);
309 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:InsertColumnsAfter"));
310 LibreOfficeMainActivity
.setDocumentChanged(true);
313 insertTableBuilder
.show();
319 insertBuilder
.show();
322 private void deleteTable() {
323 AlertDialog
.Builder deleteBuilder
= new AlertDialog
.Builder(mContext
);
324 deleteBuilder
.setTitle(R
.string
.select_delete_options
);
325 deleteBuilder
.setNeutralButton(R
.string
.alert_cancel
,null);
326 final int[] selectedItem
= new int[1];
327 deleteBuilder
.setSingleChoiceItems(mContext
.getResources().getStringArray(R
.array
.deleterowcolumns
), -1, new DialogInterface
.OnClickListener() {
329 public void onClick(DialogInterface dialog
, int which
) {
330 selectedItem
[0] = which
;
333 deleteBuilder
.setPositiveButton(R
.string
.alert_ok
, new DialogInterface
.OnClickListener() {
335 public void onClick(DialogInterface dialog
, int which
) {
336 switch (selectedItem
[0]){
338 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:DeleteRows"));
339 LibreOfficeMainActivity
.setDocumentChanged(true);
342 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:DeleteColumns"));
343 LibreOfficeMainActivity
.setDocumentChanged(true);
346 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:DeleteTable"));
347 LibreOfficeMainActivity
.setDocumentChanged(true);
352 deleteBuilder
.show();
355 private void sendImagePickingIntent() {
356 Intent intent
= new Intent(Intent
.ACTION_PICK
);
357 intent
.setType("image/*");
358 mContext
.startActivityForResult(Intent
.createChooser(intent
,
359 mContext
.getResources().getString(R
.string
.select_photo_title
)), SELECT_PHOTO
);
362 private void dispatchTakePictureIntent() {
363 if (!mContext
.getPackageManager().hasSystemFeature(PackageManager
.FEATURE_CAMERA
)) {
364 Snackbar
.make(mContext
.findViewById(R
.id
.button_insert_picture
),
365 mContext
.getResources().getString(R
.string
.no_camera_found
), Snackbar
.LENGTH_SHORT
).show();
368 Intent takePictureIntent
= new Intent(MediaStore
.ACTION_IMAGE_CAPTURE
);
369 // Ensure that there's a camera activity to handle the intent
370 if (takePictureIntent
.resolveActivity(mContext
.getPackageManager()) != null) {
371 // Create the File where the photo should go
372 File photoFile
= null;
374 photoFile
= createImageFile();
375 } catch (IOException ex
) {
376 ex
.printStackTrace();
378 // Continue only if the File was successfully created
379 if (photoFile
!= null) {
380 Uri photoURI
= FileProvider
.getUriForFile(mContext
,
381 mContext
.getPackageName() + ".fileprovider",
383 takePictureIntent
.putExtra(MediaStore
.EXTRA_OUTPUT
, photoURI
);
384 // Grant permissions to potential photo/camera apps (for some Android versions)
385 List
<ResolveInfo
> resInfoList
= mContext
.getPackageManager()
386 .queryIntentActivities(takePictureIntent
, PackageManager
.MATCH_DEFAULT_ONLY
);
387 for (ResolveInfo resolveInfo
: resInfoList
) {
388 String packageName
= resolveInfo
.activityInfo
.packageName
;
389 mContext
.grantUriPermission(packageName
, photoURI
, Intent
.FLAG_GRANT_WRITE_URI_PERMISSION
390 | Intent
.FLAG_GRANT_READ_URI_PERMISSION
);
392 mContext
.startActivityForResult(takePictureIntent
, TAKE_PHOTO
);
397 void handleActivityResult(int requestCode
, int resultCode
, Intent data
) {
398 if (requestCode
== TAKE_PHOTO
&& resultCode
== Activity
.RESULT_OK
) {
399 compressAndInsertImage();
400 } else if (requestCode
== SELECT_PHOTO
&& resultCode
== Activity
.RESULT_OK
) {
401 getFileFromURI(data
.getData());
402 compressAndInsertImage();
406 void compressAndInsertImage() {
407 AlertDialog
.Builder builder
= new AlertDialog
.Builder(mContext
);
408 String
[] options
= {mContext
.getResources().getString(R
.string
.compress_photo_smallest_size
),
409 mContext
.getResources().getString(R
.string
.compress_photo_medium_size
),
410 mContext
.getResources().getString(R
.string
.compress_photo_max_quality
),
411 mContext
.getResources().getString(R
.string
.compress_photo_no_compress
)};
412 builder
.setTitle(mContext
.getResources().getString(R
.string
.compress_photo_title
));
413 builder
.setItems(options
, new DialogInterface
.OnClickListener() {
415 public void onClick(DialogInterface dialog
, int which
) {
433 compressImage(compressGrade
);
440 private void getFileFromURI(Uri uri
) {
442 InputStream input
= mContext
.getContentResolver().openInputStream(uri
);
443 mCurrentPhotoPath
= createImageFile().getAbsolutePath();
444 FileOutputStream output
= new FileOutputStream(mCurrentPhotoPath
);
446 byte[] buffer
= new byte[IMAGE_BUFFER_SIZE
];
448 while ((read
= input
.read(buffer
)) != -1) {
449 output
.write(buffer
, 0, read
);
455 } catch (Exception e
) {
460 private void sendInsertGraphic() {
461 JSONObject rootJson
= new JSONObject();
463 addProperty(rootJson
, "FileName", "string", "file://" + mCurrentPhotoPath
);
464 } catch (JSONException ex
) {
465 ex
.printStackTrace();
467 LOKitShell
.sendEvent(new LOEvent(LOEvent
.UNO_COMMAND
, ".uno:InsertGraphic", rootJson
.toString()));
468 LOKitShell
.sendEvent(new LOEvent(LOEvent
.REFRESH
));
469 mContext
.setDocumentChanged(true);
472 private void compressImage(int grade
) {
473 if (grade
< 0 || grade
> 100) {
476 mContext
.showProgressSpinner();
477 Bitmap bmp
= BitmapFactory
.decodeFile(mCurrentPhotoPath
);
479 mCurrentPhotoPath
= createImageFile().getAbsolutePath();
480 FileOutputStream out
= new FileOutputStream(mCurrentPhotoPath
);
481 bmp
.compress(Bitmap
.CompressFormat
.JPEG
, grade
, out
);
482 } catch (Exception e
) {
485 mContext
.hideProgressSpinner();
488 private File
createImageFile() throws IOException
{
489 // Create an image file name
490 String timeStamp
= new SimpleDateFormat("yyyyMMdd_HHmmss", Locale
.US
).format(new Date());
491 String imageFileName
= "JPEG_" + timeStamp
+ "_";
492 File storageDir
= mContext
.getExternalFilesDir(Environment
.DIRECTORY_PICTURES
);
493 File image
= File
.createTempFile(
494 imageFileName
, /* prefix */
496 storageDir
/* directory */
498 // Save a file: path for use with ACTION_VIEW intents
499 mCurrentPhotoPath
= image
.getAbsolutePath();