拍照,图册选取,截图的心路历程

背景

考研过程中使用了Anki,不能不感叹这款神器的牛皮,尽管自己尚未开发出这款App的全部功能,但它已经将我的英语词汇和写作往前面推了一些。感叹和由衷地感激这款遍布全球的项目的伟大,我也想为这个项目做些什么。首先就是想完成考研中咒骂了无数次的坑,怎么不自带一个截图功能。后哦来!我总算懂的这个坑因何无人来填。TMD,适配太难受了。尤其是小米和华为这两个行业巨头(中指),系统定制化太过严重,为了提升用户体验,篡改了很多原生Intent设置,不得不写出很多冗余代码只为适配这些独特机型,呵呵,MMP, 在墙内,原生的无法使用必须定制化这可以理解,定制化的系统更加方便国内用户的使用,这毫无疑问,在全球怕是再没有那个国家像中国的Android这样友好的用户体验,一切以用户为核心,从利益出发。这也导致在国内碎片话严重的令人发指,以及各种千奇百怪的保活策略和有意思的白名单。哎呀,现在强盗收保护费都收的那么理直气壮吗? 我记得一次在面试招XX银行时的时候我遇到这么一个问题,请问你怎么实现保活?emmmm,哈哈哈哈。

顺利完成裁剪任务

历时两天我完成了裁剪功能的添加。想想也还是有点小激动的,马上成为34个国家app的贡献者之一啦!代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
public class BasicImageFieldController extends FieldControllerBase implements IFieldController {

private static final int ACTIVITY_SELECT_IMAGE = 1;
private static final int ACTIVITY_TAKE_PICTURE = 2;
private static final int ACTIVITY_CROP_PICTURE = 3;
private static final int IMAGE_SAVE_MAX_WIDTH = 1920;

private ImageView mImagePreview;

private int showCropButton = 0;


@Override
public void createUI(Context context, LinearLayout layout) {
mImagePreview = new ImageView(mActivity);
mLinearLayout = layout;
DisplayMetrics metrics = getDisplayMetrics();

int height = metrics.heightPixels;
int width = metrics.widthPixels;

LinearLayout.LayoutParams p = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
setPreviewImage(mField.getImagePath(), getMaxImageSize());
mImagePreview.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
mImagePreview.setAdjustViewBounds(true);

mImagePreview.setMaxHeight((int) Math.round(height * 0.4));
mImagePreview.setMaxWidth((int) Math.round(width * 0.6));

Button mBtnGallery = new Button(mActivity);
mBtnGallery.setText(gtxt(R.string.multimedia_editor_image_field_editing_galery));
mBtnGallery.setOnClickListener(v -> {
Intent i = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
mActivity.startActivityForResultWithoutAnimation(i, ACTIVITY_SELECT_IMAGE);
});

Button mBtnCamera = new Button(mActivity);
mBtnCamera.setText(gtxt(R.string.multimedia_editor_image_field_editing_photo));
mBtnCamera.setOnClickListener(v -> {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File image;
File storageDir;
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US).format(new Date());
try {
storageDir = mActivity.getCacheDir();
image = File.createTempFile("img_" + timeStamp, ".jpg", storageDir);
mTempImagePath = image.getPath();
Uri uriSavedImage = FileProvider.getUriForFile(mActivity,
mActivity.getApplicationContext().getPackageName() + ".apkgfileprovider",
image);

cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uriSavedImage);
if (cameraIntent.resolveActivity(context.getPackageManager()) != null) {
mActivity.startActivityForResultWithoutAnimation(cameraIntent, ACTIVITY_TAKE_PICTURE);
}
else {
Timber.w("Device has a camera, but no app to handle ACTION_IMAGE_CAPTURE Intent");
}
} catch (IOException e) {
e.printStackTrace();
}
});

if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
mBtnCamera.setVisibility(View.INVISIBLE);
}

// Some hardware has no camera or reports yes but has zero (e.g., cheap devices, and Chromebook emulator)
if ((!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) &&
!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)) ||
(CompatHelper.getCompat().getCameraCount() < 1)) {
mBtnCamera.setVisibility(View.INVISIBLE);
}

layout.addView(mImagePreview, ViewGroup.LayoutParams.MATCH_PARENT, p);
layout.addView(mBtnGallery, ViewGroup.LayoutParams.MATCH_PARENT);
layout.addView(mBtnCamera, ViewGroup.LayoutParams.MATCH_PARENT);
}


private String gtxt(int id) {
return mActivity.getText(id).toString();
}


private DisplayMetrics getDisplayMetrics() {
if (mMetrics == null) {
mMetrics = new DisplayMetrics();
mActivity.getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}
return mMetrics;
}


@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// ignore RESULT_CANCELED but handle image select and take
if (resultCode == Activity.RESULT_CANCELED) {
return;
}
if (requestCode == ACTIVITY_SELECT_IMAGE) {
handleSelectImageResult(data);
} else if (requestCode == ACTIVITY_TAKE_PICTURE) {
handleTakePictureResult();
}else if (requestCode == ACTIVITY_CROP_PICTURE) {
handleCropResult(data);
}

showCropButton++;

if (showCropButton == 1) {
Button cropBtn = new Button(mActivity);
cropBtn.setText(gtxt(R.string.crop_button));
cropBtn.setOnClickListener(v -> {
File file = new File(mTempImagePath);
Uri uri = FileProvider.getUriForFile(mActivity, mActivity.getApplicationContext().getPackageName() + ".apkgfileprovider", file);
photoCrop(uri);
});
mLinearLayout.addView(cropBtn);
}
}


private String rotateAndCompress(String inPath) {
// Set the rotation of the camera image and save as png
File f = new File(inPath);
// use same filename but with png extension for output file
String outPath = inPath.substring(0, inPath.lastIndexOf(".")) + ".png";
// Load into a bitmap with max size of 1920 pixels and rotate if necessary
Bitmap b = BitmapUtil.decodeFile(f, IMAGE_SAVE_MAX_WIDTH);
FileOutputStream out = null;
try {
out = new FileOutputStream(outPath);
b = ExifUtil.rotateFromCamera(f, b);
b.compress(Bitmap.CompressFormat.PNG, 90, out);
f.delete();
return outPath;
} catch (FileNotFoundException e) {
Timber.e(e, "Error in BasicImageFieldController.rotateAndCompress()");
return inPath;
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}


private void setPreviewImage(String imagePath, int maxsize) {
if (imagePath != null && !imagePath.equals("")) {
File f = new File(imagePath);
Bitmap b = BitmapUtil.decodeFile(f, maxsize);
b = ExifUtil.rotateFromCamera(f, b);
mImagePreview.setImageBitmap(b);
}
}


@Override
public void onDestroy() {
ImageView imageView = mImagePreview;
BitmapUtil.freeImageView(imageView);
showCropButton = 0;
}


/**
* Standard system crop intent
* @param uri
*/

public void photoCrop(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra("return-data", true);
mActivity.startActivityForResultWithoutAnimation(intent, ACTIVITY_CROP_PICTURE);
}

private void showCropDialog(Uri uri) {
new MaterialDialog.Builder(mActivity)
.content(R.string.crop_image)
.positiveText(R.string.dialog_ok)
.negativeText(R.string.dialog_cancel)
.onPositive((dialog, which) -> {
photoCrop(uri);
})
.build().show();
}

private void writeCropImage(Bitmap cropImage) {

if (cropImage != null) {

FileOutputStream out = null;
File storageDir = mActivity.getCacheDir();

String fileName = mTempImagePath.substring(mTempImagePath.lastIndexOf("/") + 1, mTempImagePath.lastIndexOf("."));

if (!fileName.contains("crop")) {
fileName += "_crop";
}

String outPath = storageDir.getAbsolutePath() + "/" + fileName + ".png";

try {
out = new FileOutputStream(outPath);
cropImage.compress(Bitmap.CompressFormat.PNG, 90, out);
mField.setImagePath(outPath);
mField.setHasTemporaryMedia(true);
mTempImagePath = outPath;
} catch (FileNotFoundException e) {
Timber.e(e, "Error in BasicImageFieldController.rotateAndCompress()");
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

private void handleSelectImageResult(Intent data) {
Uri selectedImage = data.getData();
// Timber.d(selectedImage.toString());
String[] filePathColumn = { MediaColumns.DATA };

Cursor cursor = mActivity.getContentResolver().query(selectedImage, filePathColumn, null, null, null);
cursor.moveToFirst();

int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
String filePath = cursor.getString(columnIndex);
cursor.close();

mField.setImagePath(filePath);
setPreviewImage(filePath, getMaxImageSize());
mTempImagePath = filePath;
showCropDialog(selectedImage);
}

private void handleTakePictureResult() {
String imagePath = rotateAndCompress(mTempImagePath);
mField.setImagePath(imagePath);
mField.setHasTemporaryMedia(true);
setPreviewImage(imagePath, getMaxImageSize());
mTempImagePath = imagePath;
File file = new File(imagePath);
Uri uri = FileProvider.getUriForFile(mActivity, mActivity.getApplicationContext().getPackageName() + ".apkgfileprovider", file);
showCropDialog(uri);
}


private void handleCropResult(Intent data) {
Bundle bundle = data.getExtras();

if (bundle != null) {
Bitmap b = bundle.getParcelable("data");
mImagePreview.setImageBitmap(b);
writeCropImage(b);
}
}
}

FilePath文件
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path path="export/" name="export-directory" />
<cache-path path="/" name="cache-directory" />
<external-path path="." name="photos" />
</paths>

然而适配的并不顺利,小米和华为直接闪退,网上查了下,小米和华为不支持裁剪直接返回Bitmap,想想也是哈,毕竟Binder支持1MB的大小交互,而且闪退时的Error也有出现Binder的影子。想办法解决,那没返回值,像拍照一样直接传入写入的uri让系统裁剪程序向该Uri进行写入,再进行读取岂不妙哉。

失望

我又一次失败了,TMD,咋一直报安全异常呢

1
2
3
4
5
6
7
8
Writing exception to parcel
java.lang.SecurityException: Permission Denial: writing androidx.core.content.FileProvider uri content://com.ichi2.anki.apkgfileprovider/cache-directory/img_201904190726581247673844.jpg from pid=3207, uid=10008 requires the provider be exported, or grantUriPermission()
at android.content.ContentProvider.enforceWritePermissionInner(ContentProvider.java:682)
at android.content.ContentProvider$Transport.enforceWritePermission(ContentProvider.java:497)
at android.content.ContentProvider$Transport.enforceFilePermission(ContentProvider.java:469)
at android.content.ContentProvider$Transport.openAssetFile(ContentProvider.java:384)
at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:262)
at android.os.Binder.execTransact(Binder.java:565)

代码我写的不错啊!有条有理,章法清晰的复制粘贴。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
public class BasicImageFieldController extends FieldControllerBase implements IFieldController {

...
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// ignore RESULT_CANCELED but handle image select and take
if (resultCode == Activity.RESULT_CANCELED) {
return;
}
if (requestCode == ACTIVITY_SELECT_IMAGE) {
handleSelectImageResult(data);
} else if (requestCode == ACTIVITY_TAKE_PICTURE) {
handleTakePictureResult();
}else if (requestCode == ACTIVITY_CROP_PICTURE) {
File cropFile=new File(mTempImagePath);
Uri cropUri = FileProvider.getUriForFile(mActivity,
mActivity.getApplicationContext().getPackageName() + ".apkgfileprovider",
cropFile);
handleCropResult(cropUri);
}

showCropButton++;

if (showCropButton == 1) {
Button cropBtn = new Button(mActivity);
cropBtn.setText(gtxt(R.string.crop_button));
cropBtn.setOnClickListener(v -> {
File file = new File(mTempImagePath);
Uri uri = FileProvider.getUriForFile(mActivity, mActivity.getApplicationContext().getPackageName() + ".apkgfileprovider", file);
photoCrop(uri);
});
mLinearLayout.addView(cropBtn);
}
}



public void photoCrop(Uri uri) {
File image;
File storageDir;
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US).format(new Date());
storageDir = mActivity.getCacheDir();
try {
image = File.createTempFile("img_" + timeStamp, ".jpg", storageDir);
mTempImagePath = image.getPath();
Uri cropUri = Uri.fromFile(image);
mField.setImagePath(mTempImagePath);
mField.setHasTemporaryMedia(true);

Intent intent = new Intent("com.android.camera.action.CROP");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
intent.setDataAndType(uri, "image/*");
intent.putExtra("aspectX", 1); // 裁剪框比例
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 300); // 输出图片大小
intent.putExtra("outputY", 300);
intent.putExtra("scale", true);
intent.putExtra("return-data", false);


intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
mActivity.startActivityForResultWithoutAnimation(intent, ACTIVITY_CROP_PICTURE);

} catch (IOException e) {
e.printStackTrace();
}
}

private void showCropDialog(Uri uri) {
new MaterialDialog.Builder(mActivity)
.content(R.string.crop_image)
.positiveText(R.string.dialog_ok)
.negativeText(R.string.dialog_cancel)
.onPositive((dialog, which) -> {
photoCrop(uri);
})
.build().show();
}

private void writeCropImage(Bitmap cropImage) {

if (cropImage != null) {

FileOutputStream out = null;
File storageDir = mActivity.getCacheDir();

String fileName = mTempImagePath.substring(mTempImagePath.lastIndexOf("/") + 1, mTempImagePath.lastIndexOf("."));

if (!fileName.contains("crop")) {
fileName += "_crop";
}

String outPath = storageDir.getAbsolutePath() + "/" + fileName + ".png";

try {
out = new FileOutputStream(outPath);
cropImage.compress(Bitmap.CompressFormat.PNG, 90, out);
mField.setImagePath(outPath);
mField.setHasTemporaryMedia(true);
mTempImagePath = outPath;
} catch (FileNotFoundException e) {
Timber.e(e, "Error in BasicImageFieldController.rotateAndCompress()");
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

private void handleSelectImageResult(Intent data) {
Uri selectedImage = getImageUri(mActivity, data);
setPreviewImage(mTempImagePath, getMaxImageSize());
showCropDialog(selectedImage);
}

private void handleTakePictureResult() {
String imagePath = rotateAndCompress(mTempImagePath);
mField.setImagePath(imagePath);
mField.setHasTemporaryMedia(true);
setPreviewImage(imagePath, getMaxImageSize());
mTempImagePath = imagePath;
File file = new File(imagePath);
Uri uri = FileProvider.getUriForFile(mActivity, mActivity.getApplicationContext().getPackageName() + ".apkgfileprovider", file);
showCropDialog(uri);
}


private void handleCropResult(Uri uri) {
if (uri != null) {
Bitmap photo = null;
try {
photo = BitmapFactory.decodeStream(mActivity.getContentResolver().openInputStream(uri));
//mImagePreview.setImageBitmap(photo);
writeCropImage(photo);
setPreviewImage(mTempImagePath, getMaxImageSize());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Timber.i("");
}
}

private Uri getImageUri(Context context,Intent data){
String imagePath = null;
Uri uri = data.getData();
if(Build.VERSION.SDK_INT >= 19){
if(DocumentsContract.isDocumentUri(context,uri)){
String docId = DocumentsContract.getDocumentId(uri);
if("com.android.providers.media.documents".equals(uri.getAuthority())){
String id = docId.split(":")[1];
String selection = MediaStore.Images.Media._ID+"="+id;
imagePath = getImagePath(context,MediaStore.Images.Media.EXTERNAL_CONTENT_URI,selection);
}else if("com.android.providers.downloads.documents".equals(uri.getAuthority())){
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"),Long.valueOf(docId));
imagePath = getImagePath(context,contentUri,null);
}
}else if("content".equalsIgnoreCase(uri.getScheme())){
imagePath = getImagePath(context,uri,null);
}else if("file".equalsIgnoreCase(uri.getScheme())){
imagePath = uri.getPath();
}
}else{
uri= data.getData();
imagePath = getImagePath(context,uri,null);
}
File file = new File(imagePath);
mTempImagePath = imagePath;
mField.setImagePath(imagePath);
mField.setHasTemporaryMedia(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context,
mActivity.getApplicationContext().getPackageName() + ".apkgfileprovider",
file);
} else {
uri = Uri.fromFile(file);
}

return uri;
}

private String getImagePath(Context context,Uri uri, String selection) {
String path = null;
Cursor cursor = context.getContentResolver().query(uri,null,selection,null,null);
if(cursor != null){
if(cursor.moveToFirst()){
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}
}

对比了下参考代码,唯一的区别的就是我是选择当前的app的缓存目录作为输出uri,而参考代码则是选择sdcard的外存目录的指定文件夹作为目录,于是我死马当作活马医狠下心选择外存作为写入uri. 还别说,真香

扒开云雾见月明

为什么同样是将缓存目录作为写入uri,而照相机可以成功写入,而裁剪功能却不行,会报安全异常,权限非法。分析异常可以看出是FileProvider没有授权,然而哈,我已经在FilePath进行了注册,而且拍照功能是可以正常运行的。至今仍是懵逼中。为毛同样的输出Uri,拍照Intent可以写入,而Crop Intent却不行。神奇诶。而且这个并非是定制化的锅,我在官方模拟器测过同样会报这个error,由此可见是系统rom的Crop Intent处理与Take Photo Intent不同。算是一个遗留下的坑以后看系统源码来填。

文章目录
  1. 1. 背景
  2. 2. 顺利完成裁剪任务
  3. 3. 失望
  4. 4. 扒开云雾见月明
|