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>
This commit is contained in:
parent
4e2555b7f3
commit
8d977511e3
9 changed files with 262 additions and 2 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
4
android/source/res/xml/file_paths.xml
Normal file
4
android/source/res/xml/file_paths.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-path name="LO_files_universal" path="Android/" />
|
||||
</paths>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -354,7 +354,9 @@ class LOKitThread extends Thread {
|
|||
case LOEvent.UPDATE_CALC_HEADERS:
|
||||
updateCalcHeaders();
|
||||
break;
|
||||
|
||||
case LOEvent.REFRESH:
|
||||
refresh();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: */
|
||||
|
|
Loading…
Reference in a new issue