summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXimeng Zu <uznomis@yahoo.com>2017-08-14 11:41:30 -0500
committerTomaž Vajngerl <quikee@gmail.com>2017-11-16 06:31:48 +0100
commit8d977511e3ab755da65d34a0bd618ef3c9db90c7 (patch)
tree50ac7f7a84c4560e42375e1df409d46ecb20ebb0
parent4e2555b7f37172ab28d43d5aecfa52a38c0cdd65 (diff)
tdf#106370 Android: add ability to insert pictures
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 <quikee@gmail.com> Tested-by: Tomaž Vajngerl <quikee@gmail.com>
-rw-r--r--android/source/AndroidManifest.xml11
-rw-r--r--android/source/res/layout/toolbar_bottom.xml10
-rw-r--r--android/source/res/values/strings.xml11
-rw-r--r--android/source/res/xml/file_paths.xml4
-rw-r--r--android/source/src/java/org/libreoffice/FormattingController.java208
-rw-r--r--android/source/src/java/org/libreoffice/LOEvent.java1
-rw-r--r--android/source/src/java/org/libreoffice/LOKitThread.java4
-rw-r--r--android/source/src/java/org/libreoffice/LOKitTileProvider.java8
-rw-r--r--android/source/src/java/org/libreoffice/LibreOfficeMainActivity.java7
9 files changed, 262 insertions, 2 deletions
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 @@
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<!-- App wants to know if device supports USB host capability(not mandatory) -->
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
+ <uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET" />
@@ -133,6 +134,16 @@
android:value=".LibreOfficeMainActivity" />
</activity>
+ <provider
+ android:name="android.support.v4.content.FileProvider"
+ android:authorities="${applicationId}.fileprovider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/file_paths" />
+ </provider>
+
</application>
</manifest>
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" />
+
+ <ImageButton
+ android:id="@+id/button_insert_picture"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="0.25"
+ android:background="@drawable/image_button_background"
+ android:paddingBottom="12dp"
+ android:paddingTop="12dp"
+ app:srcCompat="@drawable/ic_folder_black_24dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
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 @@
<string name="action_pwd_dialog_cancel">Cancel</string>
<string name="action_pwd_dialog_title">Please enter password</string>
+ <!-- Insert Image Strings -->
+ <string name="take_photo">Take Photo</string>
+ <string name="select_photo">Select Photo</string>
+ <string name="select_photo_title">Select Picture</string>
+ <string name="no_camera_found">No Camera Found</string>
+ <string name="compress_photo_smallest_size">Smallest Size</string>
+ <string name="compress_photo_medium_size">Medium Size</string>
+ <string name="compress_photo_max_quality">Max Quality</string>
+ <string name="compress_photo_no_compress">Don\'t Compress</string>
+ <string name="compress_photo_title">Do you want to compress the photo?</string>
+
</resources>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+ <external-path name="LO_files_universal" path="Android/" />
+</paths> \ 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<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) {
+ 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<LOEvent> {
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: */