Avoid potential negative array index access to cached text.
[LibreOffice.git] / android / source / src / java / org / libreoffice / FormattingController.java
blob49e81eb697849c15c4f56751a101861adab27e46
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;
26 import java.io.File;
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) {
47 mContext = 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);
79 @Override
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});
85 } else {
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) {
129 insertPicture();
130 } else if (buttonId == R.id.button_insert_table) {
131 insertTable();
132 } else if (buttonId == R.id.button_delete_table) {
133 deleteTable();
137 void onToggleStateChanged(final int type, final boolean selected) {
138 LOKitShell.getMainHandler().post(new Runnable() {
139 public void run() {
140 Integer buttonId;
141 switch (type) {
142 case Document.BOLD:
143 buttonId = R.id.button_bold;
144 break;
145 case Document.ITALIC:
146 buttonId = R.id.button_italic;
147 break;
148 case Document.UNDERLINE:
149 buttonId = R.id.button_underlined;
150 break;
151 case Document.STRIKEOUT:
152 buttonId = R.id.button_strikethrough;
153 break;
154 case Document.ALIGN_LEFT:
155 buttonId = R.id.button_align_left;
156 break;
157 case Document.ALIGN_CENTER:
158 buttonId = R.id.button_align_center;
159 break;
160 case Document.ALIGN_RIGHT:
161 buttonId = R.id.button_align_right;
162 break;
163 case Document.ALIGN_JUSTIFY:
164 buttonId = R.id.button_align_justify;
165 break;
166 case Document.BULLET_LIST:
167 buttonId = R.id.button_insertFormatListBullets;
168 break;
169 case Document.NUMBERED_LIST:
170 buttonId = R.id.button_insertFormatListNumbering;
171 break;
172 default:
173 Log.e(LOGTAG, "Uncaptured state change type: " + type);
174 return;
177 ImageButton button = mContext.findViewById(buttonId);
178 button.setSelected(selected);
179 if (selected) {
180 button.getBackground().setState(new int[]{android.R.attr.state_selected});
181 } else {
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() {
193 @Override
194 public void onClick(DialogInterface dialog, int which) {
195 switch (which) {
196 case 0:
197 dispatchTakePictureIntent();
198 break;
199 case 1:
200 sendImagePickingIntent();
201 break;
202 default:
203 sendImagePickingIntent();
207 builder.show();
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() {
225 @Override
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() {
239 @Override
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() {
260 @Override
261 public void onClick(DialogInterface dialog, int which) {
263 try {
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) {
276 e.printStackTrace();
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() {
287 @Override
288 public void onClick(DialogInterface dialog, int which) {
289 selectedItem[0] = which;
292 insertBuilder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
293 @Override
294 public void onClick(DialogInterface dialog, int which) {
295 switch (selectedItem[0]){
296 case 0:
297 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertRowsBefore"));
298 LibreOfficeMainActivity.setDocumentChanged(true);
299 break;
300 case 1:
301 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertRowsAfter"));
302 LibreOfficeMainActivity.setDocumentChanged(true);
303 break;
304 case 2:
305 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertColumnsBefore"));
306 LibreOfficeMainActivity.setDocumentChanged(true);
307 break;
308 case 3:
309 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertColumnsAfter"));
310 LibreOfficeMainActivity.setDocumentChanged(true);
311 break;
312 case 4:
313 insertTableBuilder.show();
314 break;
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() {
328 @Override
329 public void onClick(DialogInterface dialog, int which) {
330 selectedItem[0] = which;
333 deleteBuilder.setPositiveButton(R.string.alert_ok, new DialogInterface.OnClickListener() {
334 @Override
335 public void onClick(DialogInterface dialog, int which) {
336 switch (selectedItem[0]){
337 case 0:
338 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteRows"));
339 LibreOfficeMainActivity.setDocumentChanged(true);
340 break;
341 case 1:
342 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteColumns"));
343 LibreOfficeMainActivity.setDocumentChanged(true);
344 break;
345 case 2:
346 LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:DeleteTable"));
347 LibreOfficeMainActivity.setDocumentChanged(true);
348 break;
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();
366 return;
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;
373 try {
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",
382 photoFile);
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() {
414 @Override
415 public void onClick(DialogInterface dialog, int which) {
416 int compressGrade;
417 switch (which) {
418 case 0:
419 compressGrade = 0;
420 break;
421 case 1:
422 compressGrade = 50;
423 break;
424 case 2:
425 compressGrade = 100;
426 break;
427 case 3:
428 compressGrade = -1;
429 break;
430 default:
431 compressGrade = -1;
433 compressImage(compressGrade);
434 sendInsertGraphic();
437 builder.show();
440 private void getFileFromURI(Uri uri) {
441 try {
442 InputStream input = mContext.getContentResolver().openInputStream(uri);
443 mCurrentPhotoPath = createImageFile().getAbsolutePath();
444 FileOutputStream output = new FileOutputStream(mCurrentPhotoPath);
445 if (input != null) {
446 byte[] buffer = new byte[IMAGE_BUFFER_SIZE];
447 int read;
448 while ((read = input.read(buffer)) != -1) {
449 output.write(buffer, 0, read);
451 input.close();
453 output.flush();
454 output.close();
455 } catch (Exception e) {
456 e.printStackTrace();
460 private void sendInsertGraphic() {
461 JSONObject rootJson = new JSONObject();
462 try {
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) {
474 return;
476 mContext.showProgressSpinner();
477 Bitmap bmp = BitmapFactory.decodeFile(mCurrentPhotoPath);
478 try {
479 mCurrentPhotoPath = createImageFile().getAbsolutePath();
480 FileOutputStream out = new FileOutputStream(mCurrentPhotoPath);
481 bmp.compress(Bitmap.CompressFormat.JPEG, grade, out);
482 } catch (Exception e) {
483 e.printStackTrace();
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 */
495 ".jpg", /* suffix */
496 storageDir /* directory */
498 // Save a file: path for use with ACTION_VIEW intents
499 mCurrentPhotoPath = image.getAbsolutePath();
500 return image;