Skip to content

Commit

Permalink
feat(ios,android): support portable web urls for content. Closes #325
Browse files Browse the repository at this point in the history
  • Loading branch information
mlynch committed Mar 14, 2018
1 parent c51e727 commit 7b41141
Show file tree
Hide file tree
Showing 16 changed files with 311 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import android.util.Log;
import android.util.TypedValue;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
Expand All @@ -24,8 +26,18 @@ public AndroidProtocolHandler(Context context) {
this.context = context;
}

public InputStream openAsset(String path) throws IOException {
return context.getAssets().open(path, AssetManager.ACCESS_STREAMING);
public InputStream openAsset(String path, String assetPath) throws IOException {
if (path.startsWith(assetPath + "/_capacitor_")) {
if (path.contains("content://")) {
String contentPath = path.replace(assetPath + "/_capacitor_/", "content://");
return context.getContentResolver().openInputStream(Uri.parse(contentPath));
} else {
String filePath = path.replace(assetPath + "/_capacitor_/", "");
return new FileInputStream(new File(filePath));
}
} else {
return context.getAssets().open(path, AssetManager.ACCESS_STREAMING);
}
}

public InputStream openResource(Uri uri) {
Expand Down
220 changes: 220 additions & 0 deletions android/capacitor/src/main/java/com/getcapacitor/FileUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* Portions adopted from react-native-image-crop-picker
*
* MIT License
* Copyright (c) 2017 Ivan Pusic
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.getcapacitor;

import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Log;

import java.io.File;

/**
* Common File utilities, such as resolve content URIs and
* creating portable web paths from low-level files
*/
public class FileUtils {

public enum Type {
IMAGE("image");
private String type;
Type(String type) {
this.type = type;
}
}

public static String getPortablePath(Context c, Uri u) {
String path = getFileUrlForUri(c, u);
path = path.replace("file://", "");
return "_capacitor_" + path;
}

static String getFileUrlForUri(final Context context, final Uri uri) {

// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];

if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
} else {
final int splitIndex = docId.indexOf(':', 1);
final String tag = docId.substring(0, splitIndex);
final String path = docId.substring(splitIndex + 1);

String nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag);
if (nonPrimaryVolume != null) {
String result = nonPrimaryVolume + "/" + path;
File file = new File(result);
if (file.exists() && file.canRead()) {
return result;
}
return null;
}
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {

final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];

Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};

return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {

// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();

return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}

return null;
}

/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
private static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {

Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};

try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}


/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}

/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}

/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}

/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}

private static String getPathToNonPrimaryVolume(Context context, String tag) {
File[] volumes = context.getExternalCacheDirs();
if (volumes != null) {
for (File volume : volumes) {
if (volume != null) {
String path = volume.getAbsolutePath();
if (path != null) {
int index = path.indexOf(tag);
if (index != -1) {
return path.substring(0, index) + tag;
}
}
}
}
}
return null;
}

}
23 changes: 0 additions & 23 deletions android/capacitor/src/main/java/com/getcapacitor/ImageUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,6 @@ public static Bitmap transform(Bitmap bitmap, Matrix matrix) {
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}

private static String getFileUrlForContentImage(Context c, Uri uri) {
String wholeID = DocumentsContract.getDocumentId(uri);

// Split at colon, use second item in the array
String id = wholeID.split(":")[1];
String[] column = { MediaStore.Images.Media.DATA };

// where id is equal to
String sel = MediaStore.Images.Media._ID + "=?";
Cursor cursor = c.getContentResolver().
query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
column, sel, new String[]{ id }, null);

String filePath = "";
int columnIndex = cursor.getColumnIndex(column[0]);
if (cursor.moveToFirst()) {
filePath = cursor.getString(columnIndex);
}

cursor.close();
return filePath;
}

/**
* Correct the orientation of an image by reading its exif information and rotating
* the appropriate amount for portrait mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ public InputStream handle(Uri url) {
InputStream stream;
String path = url.getPath().replaceFirst(virtualAssetPath, assetPath);
try {
stream = protocolHandler.openAsset(path);
stream = protocolHandler.openAsset(path, assetPath);
} catch (IOException e) {
Log.e(TAG, "Unable to open asset URL: " + url);
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import android.util.Base64;

import com.getcapacitor.Dialogs;
import com.getcapacitor.FileUtils;
import com.getcapacitor.ImageUtils;
import com.getcapacitor.JSObject;
import com.getcapacitor.NativePlugin;
Expand Down Expand Up @@ -274,6 +275,7 @@ public void processCameraImage(PluginCall call, Intent data) {
} else if (resultType == CameraResultType.URI) {
JSObject ret = new JSObject();
ret.put("path", contentUri.toString());
ret.put("webPath", FileUtils.getPortablePath(getContext(), contentUri));
call.success(ret);
}
}
Expand Down Expand Up @@ -301,6 +303,7 @@ public void processPickedImage(PluginCall call, Intent data) {
} else if (settings.resultType == CameraResultType.URI) {
JSObject ret = new JSObject();
ret.put("path", u.toString());
ret.put("webPath", FileUtils.getPortablePath(getContext(), u));
call.success(ret);
}
} catch (FileNotFoundException ex) {
Expand Down Expand Up @@ -339,7 +342,7 @@ private void returnBase64(PluginCall call, ByteArrayOutputStream bitmapOutputStr
String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);

JSObject data = new JSObject();
data.put("base64_data", "data:image/jpeg;base64," + encoded);
data.put("base64Data", "data:image/jpeg;base64," + encoded);
call.success(data);
}

Expand Down
17 changes: 16 additions & 1 deletion core/src/core-plugin-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,23 @@ export enum CameraSource {
}

export interface CameraPhoto {
base64_data?: string;
/**
* The base64 encoded data of the image, if using CameraResultType.Base64.
*/
base64Data?: string;
/**
* If using CameraResultType.Uri, the path will contain a full,
* platform-specific file URL that can be read later using the Filsystem API.
*/
path?: string;
/**
* webPath returns a path that can be used to set the src attribute of an image for efficient
* loading and rendering.
*/
webPath?: string;
/**
* The format of the image. Currently, only "jpeg" is supported.
*/
format: string;
}

Expand Down
4 changes: 2 additions & 2 deletions core/src/web/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ export class CameraPluginWeb extends WebPlugin implements CameraPlugin {
private _getCameraPhoto(photo: Blob) {
return new Promise<CameraPhoto>((resolve, reject) => {
var reader = new FileReader();
reader.readAsDataURL(photo);
reader.readAsDataURL(photo);
reader.onloadend = () => {
resolve({
base64_data: reader.result,
base64Data: reader.result,
format: 'jpeg'
});
};
Expand Down
11 changes: 11 additions & 0 deletions example/ios/IonicRunner/capacitor.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"appId": "com.example.app",
"appName": "App",
"bundledWebRuntime": true,
"webDir": "www",
"plugins": {
"SplashScreen": {
"launchShowDuration": 12345
}
}
}
2 changes: 1 addition & 1 deletion example/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Plugins } from '@capacitor/core';
export class MyApp {
@ViewChild(Nav) nav: Nav;

rootPage = 'FilesystemPage';
rootPage = 'CameraPage';

PLUGINS = [
{ name: 'App', page: 'AppPage' },
Expand Down
Loading

0 comments on commit 7b41141

Please sign in to comment.