From 8d977511e3ab755da65d34a0bd618ef3c9db90c7 Mon Sep 17 00:00:00 2001 From: Ximeng Zu Date: Mon, 14 Aug 2017 11:41:30 -0500 Subject: [PATCH] tdf#106370 Android: add ability to insert pictures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added ability to insert pictures to Android Viewer. You can take photo or select photo from device or the cloud (Google photos, Dropbox). You can also compress the picture before inserting it with multiple compress grades. So far, inserting doesn't work for Writer due LO native library issues (I think). Change-Id: If6841ba04fe18585703c8b85909cf39747dbbc2f Reviewed-on: https://gerrit.libreoffice.org/41150 Reviewed-by: Tomaž Vajngerl Tested-by: Tomaž Vajngerl --- android/source/AndroidManifest.xml | 11 + android/source/res/layout/toolbar_bottom.xml | 10 + android/source/res/values/strings.xml | 11 + android/source/res/xml/file_paths.xml | 4 + .../org/libreoffice/FormattingController.java | 208 +++++++++++++++++- .../src/java/org/libreoffice/LOEvent.java | 1 + .../src/java/org/libreoffice/LOKitThread.java | 4 +- .../org/libreoffice/LOKitTileProvider.java | 8 + .../libreoffice/LibreOfficeMainActivity.java | 7 + 9 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 android/source/res/xml/file_paths.xml diff --git a/android/source/AndroidManifest.xml b/android/source/AndroidManifest.xml index c2a3656f8e92..fb51eb4b0e43 100644 --- a/android/source/AndroidManifest.xml +++ b/android/source/AndroidManifest.xml @@ -7,6 +7,7 @@ + @@ -133,6 +134,16 @@ android:value=".LibreOfficeMainActivity" /> + + + + diff --git a/android/source/res/layout/toolbar_bottom.xml b/android/source/res/layout/toolbar_bottom.xml index 1b4730d89adf..a537a52d32b9 100644 --- a/android/source/res/layout/toolbar_bottom.xml +++ b/android/source/res/layout/toolbar_bottom.xml @@ -328,6 +328,16 @@ android:paddingBottom="12dp" android:paddingTop="12dp" app:srcCompat="@drawable/ic_rect" /> + + diff --git a/android/source/res/values/strings.xml b/android/source/res/values/strings.xml index e84c496db3c5..3f6955cd7a7b 100644 --- a/android/source/res/values/strings.xml +++ b/android/source/res/values/strings.xml @@ -157,4 +157,15 @@ Cancel Please enter password + + Take Photo + Select Photo + Select Picture + No Camera Found + Smallest Size + Medium Size + Max Quality + Don\'t Compress + Do you want to compress the photo? + diff --git a/android/source/res/xml/file_paths.xml b/android/source/res/xml/file_paths.xml new file mode 100644 index 000000000000..2bbe2aef17ea --- /dev/null +++ b/android/source/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/source/src/java/org/libreoffice/FormattingController.java b/android/source/src/java/org/libreoffice/FormattingController.java index 4d36249dc6b9..0ba72cf50875 100644 --- a/android/source/src/java/org/libreoffice/FormattingController.java +++ b/android/source/src/java/org/libreoffice/FormattingController.java @@ -1,15 +1,45 @@ package org.libreoffice; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.support.design.widget.Snackbar; +import android.support.v4.content.FileProvider; import android.util.Log; import android.view.View; import android.widget.ImageButton; +import org.json.JSONException; +import org.json.JSONObject; import org.libreoffice.kit.Document; - class FormattingController implements View.OnClickListener { +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import static org.libreoffice.SearchController.addProperty; + +class FormattingController implements View.OnClickListener { private static final String LOGTAG = ToolbarController.class.getSimpleName(); + private static final int TAKE_PHOTO = 1; + private static final int SELECT_PHOTO = 2; + private static final int IMAGE_BUFFER_SIZE = 4 * 1024; private LibreOfficeMainActivity mContext; + private String mCurrentPhotoPath; FormattingController(LibreOfficeMainActivity context) { mContext = context; @@ -29,6 +59,7 @@ import org.libreoffice.kit.Document; 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_font_shrink).setOnClickListener(this); mContext.findViewById(R.id.button_font_grow).setOnClickListener(this); @@ -99,6 +130,8 @@ import org.libreoffice.kit.Document; case R.id.button_superscript: LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SuperScript")); break; + case R.id.button_insert_picture: + insertPicture(); } } @@ -152,4 +185,177 @@ import org.libreoffice.kit.Document; } }); } + + 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 sendImagePickingIntent() { + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_PICK); + 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 resInfoList = mContext.getPackageManager() + .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + mContext.grantUriPermission(packageName, photoURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + mContext.startActivityForResult(takePictureIntent, TAKE_PHOTO); + } + } + } + + void handleActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == TAKE_PHOTO && resultCode == Activity.RESULT_OK) { + mContext.pendingInsertGraphic = true; + } else if (requestCode == SELECT_PHOTO && resultCode == Activity.RESULT_OK) { + getFileFromURI(data.getData()); + mContext.pendingInsertGraphic = true; + } + } + + // Called by LOKitTileProvider when activity is resumed from photo/gallery/camera/cloud apps + void popCompressImageGradeSelection() { + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + String[] options = {mContext.getResources().getString(R.string.compress_photo_smallest_size), + mContext.getResources().getString(R.string.compress_photo_medium_size), + mContext.getResources().getString(R.string.compress_photo_max_quality), + mContext.getResources().getString(R.string.compress_photo_no_compress)}; + builder.setTitle(mContext.getResources().getString(R.string.compress_photo_title)); + builder.setItems(options, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + int compressGrade; + switch (which) { + case 0: + compressGrade = 0; + break; + case 1: + compressGrade = 50; + break; + case 2: + compressGrade = 100; + break; + case 3: + compressGrade = -1; + break; + default: + compressGrade = -1; + } + compressImage(compressGrade); + sendInsertGraphic(); + } + }); + builder.show(); + } + + private void getFileFromURI(Uri uri) { + try { + InputStream input = mContext.getContentResolver().openInputStream(uri); + mCurrentPhotoPath = createImageFile().getAbsolutePath(); + FileOutputStream output = new FileOutputStream(mCurrentPhotoPath); + if (input != null) { + byte[] buffer = new byte[IMAGE_BUFFER_SIZE]; + int read; + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + input.close(); + } + output.flush(); + output.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void sendInsertGraphic() { + JSONObject rootJson = new JSONObject(); + try { + addProperty(rootJson, "FileName", "string", "file://" + mCurrentPhotoPath); + } catch (JSONException ex) { + ex.printStackTrace(); + } + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:InsertGraphic", rootJson.toString())); + LOKitShell.sendEvent(new LOEvent(LOEvent.REFRESH)); + mContext.setDocumentChanged(true); + mContext.pendingInsertGraphic = false; + } + + private void compressImage(int grade) { + if (grade < 0 || grade > 100) { + return; + } + mContext.showProgressSpinner(); + Bitmap bmp = BitmapFactory.decodeFile(mCurrentPhotoPath); + try { + mCurrentPhotoPath = createImageFile().getAbsolutePath(); + FileOutputStream out = new FileOutputStream(mCurrentPhotoPath); + bmp.compress(Bitmap.CompressFormat.JPEG, grade, out); + } catch (Exception e) { + e.printStackTrace(); + } + mContext.hideProgressSpinner(); + } + + private File createImageFile() throws IOException { + // Create an image file name + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + File storageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + File image = File.createTempFile( + imageFileName, /* prefix */ + ".jpg", /* suffix */ + storageDir /* directory */ + ); + // Save a file: path for use with ACTION_VIEW intents + mCurrentPhotoPath = image.getAbsolutePath(); + return image; + } } diff --git a/android/source/src/java/org/libreoffice/LOEvent.java b/android/source/src/java/org/libreoffice/LOEvent.java index 7846f7331bbd..4d081e61c0f2 100644 --- a/android/source/src/java/org/libreoffice/LOEvent.java +++ b/android/source/src/java/org/libreoffice/LOEvent.java @@ -39,6 +39,7 @@ public class LOEvent implements Comparable { 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 final int mType; public int mPriority = 0; diff --git a/android/source/src/java/org/libreoffice/LOKitThread.java b/android/source/src/java/org/libreoffice/LOKitThread.java index 721853d08a99..99b53397b9d0 100644 --- a/android/source/src/java/org/libreoffice/LOKitThread.java +++ b/android/source/src/java/org/libreoffice/LOKitThread.java @@ -354,7 +354,9 @@ class LOKitThread extends Thread { case LOEvent.UPDATE_CALC_HEADERS: updateCalcHeaders(); break; - + case LOEvent.REFRESH: + refresh(); + break; } } diff --git a/android/source/src/java/org/libreoffice/LOKitTileProvider.java b/android/source/src/java/org/libreoffice/LOKitTileProvider.java index 7be5ac31f60c..a68f65221795 100644 --- a/android/source/src/java/org/libreoffice/LOKitTileProvider.java +++ b/android/source/src/java/org/libreoffice/LOKitTileProvider.java @@ -150,6 +150,14 @@ class LOKitTileProvider implements TileProvider { mContext.getDocumentPartViewListAdapter().notifyDataSetChanged(); } }); + mContext.runOnUiThread(new Runnable() { + @Override + public void run() { + if (mContext.pendingInsertGraphic) { + mContext.getFormattingController().popCompressImageGradeSelection(); + } + } + }); } @Override diff --git a/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java b/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java index defd0d18476e..534eaf44de59 100644 --- a/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java +++ b/android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java @@ -98,6 +98,7 @@ public class LibreOfficeMainActivity extends AppCompatActivity implements Settin private LOKitTileProvider mTileProvider; private String mPassword; private boolean mPasswordProtected; + public boolean pendingInsertGraphic; // boolean indicating a pending insert graphic action, used in LOKitTileProvider.postLoad() public GeckoLayerClient getLayerClient() { return mLayerClient; @@ -863,6 +864,12 @@ public class LibreOfficeMainActivity extends AppCompatActivity implements Settin .setPositiveButton(R.string.alert_copy_svg_slide_show_to_clipboard_dismiss, null).show(); } } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mFormattingController.handleActivityResult(requestCode, resultCode, data); + hideBottomToolbar(); + } } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */