();
58 | private PathStrategy mStrategy;
59 | /**
60 | * The default FileProvider implementation does not need to be initialized. If you want to
61 | * override this method, you must provide your own subclass of FileProvider.
62 | */
63 | @Override
64 | public boolean onCreate() {
65 | return true;
66 | }
67 | /**
68 | * After the FileProvider is instantiated, this method is called to provide the system with
69 | * information about the provider.
70 | *
71 | * @param context A {@link Context} for the current component.
72 | * @param info A {@link ProviderInfo} for the new provider.
73 | */
74 | @Override
75 | public void attachInfo(Context context, ProviderInfo info) {
76 | super.attachInfo(context, info);
77 | // Sanity check our security
78 | if (info.exported) {
79 | throw new SecurityException("Provider must not be exported");
80 | }
81 | if (!info.grantUriPermissions) {
82 | throw new SecurityException("Provider must grant uri permissions");
83 | }
84 | mStrategy = getPathStrategy(context, info.authority);
85 | }
86 |
87 | public static Uri getUriForFile(Context context, String authority, File file) {
88 | final PathStrategy strategy = getPathStrategy(context, authority);
89 | return strategy.getUriForFile(file);
90 | }
91 | /**
92 | * Use a content URI returned by
93 | * {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
94 | * managed by the FileProvider.
95 | * FileProvider reports the column names defined in {@link OpenableColumns}:
96 | *
97 | * - {@link OpenableColumns#DISPLAY_NAME}
98 | * - {@link OpenableColumns#SIZE}
99 | *
100 | * For more information, see
101 | * {@link ContentProvider#query(Uri, String[], String, String[], String)
102 | * ContentProvider.query()}.
103 | *
104 | * @param uri A content URI returned by {@link #getUriForFile}.
105 | * @param projection The list of columns to put into the {@link Cursor}. If null all columns are
106 | * included.
107 | * @param selection Selection criteria to apply. If null then all data that matches the content
108 | * URI is returned.
109 | * @param selectionArgs An array of {@link String}, containing arguments to bind to
110 | * the selection parameter. The query method scans selection from left to
111 | * right and iterates through selectionArgs, replacing the current "?" character in
112 | * selection with the value at the current position in selectionArgs. The
113 | * values are bound to selection as {@link String} values.
114 | * @param sortOrder A {@link String} containing the column name(s) on which to sort
115 | * the resulting {@link Cursor}.
116 | * @return A {@link Cursor} containing the results of the query.
117 | *
118 | */
119 | @Override
120 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
121 | String sortOrder) {
122 | // ContentProvider has already checked granted permissions
123 | final File file = mStrategy.getFileForUri(uri);
124 | if (projection == null) {
125 | projection = COLUMNS;
126 | }
127 | String[] cols = new String[projection.length];
128 | Object[] values = new Object[projection.length];
129 | int i = 0;
130 | for (String col : projection) {
131 | if (OpenableColumns.DISPLAY_NAME.equals(col)) {
132 | cols[i] = OpenableColumns.DISPLAY_NAME;
133 | values[i++] = file.getName();
134 | } else if (OpenableColumns.SIZE.equals(col)) {
135 | cols[i] = OpenableColumns.SIZE;
136 | values[i++] = file.length();
137 | }
138 | }
139 | cols = copyOf(cols, i);
140 | values = copyOf(values, i);
141 | final MatrixCursor cursor = new MatrixCursor(cols, 1);
142 | cursor.addRow(values);
143 | return cursor;
144 | }
145 | /**
146 | * Returns the MIME type of a content URI returned by
147 | * {@link #getUriForFile(Context, String, File) getUriForFile()}.
148 | *
149 | * @param uri A content URI returned by
150 | * {@link #getUriForFile(Context, String, File) getUriForFile()}.
151 | * @return If the associated file has an extension, the MIME type associated with that
152 | * extension; otherwise application/octet-stream
.
153 | */
154 | @Override
155 | public String getType(Uri uri) {
156 | // ContentProvider has already checked granted permissions
157 | final File file = mStrategy.getFileForUri(uri);
158 | final int lastDot = file.getName().lastIndexOf('.');
159 | if (lastDot >= 0) {
160 | final String extension = file.getName().substring(lastDot + 1);
161 | final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
162 | if (mime != null) {
163 | return mime;
164 | }
165 | }
166 | return "application/octet-stream";
167 | }
168 | /**
169 | * By default, this method throws an {@link UnsupportedOperationException}. You must
170 | * subclass FileProvider if you want to provide different functionality.
171 | */
172 | @Override
173 | public Uri insert(Uri uri, ContentValues values) {
174 | throw new UnsupportedOperationException("No external inserts");
175 | }
176 | /**
177 | * By default, this method throws an {@link UnsupportedOperationException}. You must
178 | * subclass FileProvider if you want to provide different functionality.
179 | */
180 | @Override
181 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
182 | throw new UnsupportedOperationException("No external updates");
183 | }
184 | /**
185 | * Deletes the file associated with the specified content URI, as
186 | * returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
187 | * method does not throw an {@link IOException}; you must check its return value.
188 | *
189 | * @param uri A content URI for a file, as returned by
190 | * {@link #getUriForFile(Context, String, File) getUriForFile()}.
191 | * @param selection Ignored. Set to {@code null}.
192 | * @param selectionArgs Ignored. Set to {@code null}.
193 | * @return 1 if the delete succeeds; otherwise, 0.
194 | */
195 | @Override
196 | public int delete(Uri uri, String selection, String[] selectionArgs) {
197 | // ContentProvider has already checked granted permissions
198 | final File file = mStrategy.getFileForUri(uri);
199 | return file.delete() ? 1 : 0;
200 | }
201 | /**
202 | * By default, FileProvider automatically returns the
203 | * {@link ParcelFileDescriptor} for a file associated with a content://
204 | * {@link Uri}. To get the {@link ParcelFileDescriptor}, call
205 | * {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
206 | * ContentResolver.openFileDescriptor}.
207 | *
208 | * To override this method, you must provide your own subclass of FileProvider.
209 | *
210 | * @param uri A content URI associated with a file, as returned by
211 | * {@link #getUriForFile(Context, String, File) getUriForFile()}.
212 | * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
213 | * write access, or "rwt" for read and write access that truncates any existing file.
214 | * @return A new {@link ParcelFileDescriptor} with which you can access the file.
215 | */
216 | @Override
217 | public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
218 | // ContentProvider has already checked granted permissions
219 | final File file = mStrategy.getFileForUri(uri);
220 | final int fileMode = modeToMode(mode);
221 | return ParcelFileDescriptor.open(file, fileMode);
222 | }
223 | /**
224 | * Return {@link PathStrategy} for given authority, either by parsing or
225 | * returning from cache.
226 | */
227 | private static PathStrategy getPathStrategy(Context context, String authority) {
228 | PathStrategy strat;
229 | synchronized (sCache) {
230 | strat = sCache.get(authority);
231 | if (strat == null) {
232 | try {
233 | strat = parsePathStrategy(context, authority);
234 | } catch (IOException e) {
235 | throw new IllegalArgumentException(
236 | "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
237 | } catch (XmlPullParserException e) {
238 | throw new IllegalArgumentException(
239 | "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
240 | }
241 | sCache.put(authority, strat);
242 | }
243 | }
244 | return strat;
245 | }
246 | /**
247 | * Parse and return {@link PathStrategy} for given authority as defined in
248 | * {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
249 | *
250 | * @see #getPathStrategy(Context, String)
251 | */
252 | private static PathStrategy parsePathStrategy(Context context, String authority)
253 | throws IOException, XmlPullParserException {
254 | final SimplePathStrategy strat = new SimplePathStrategy(authority);
255 | final ProviderInfo info = context.getPackageManager()
256 | .resolveContentProvider(authority, PackageManager.GET_META_DATA);
257 | final XmlResourceParser in = info.loadXmlMetaData(
258 | context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
259 | //if (in == null) {
260 | // throw new IllegalArgumentException(
261 | // "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
262 | //}
263 |
264 | File target = null;
265 | target = buildPath(DEVICE_ROOT, ".");
266 | if (target != null) {
267 | strat.addRoot("devroot", target);
268 | }
269 |
270 | return strat;
271 | }
272 | /**
273 | * Strategy for mapping between {@link File} and {@link Uri}.
274 | *
275 | * Strategies must be symmetric so that mapping a {@link File} to a
276 | * {@link Uri} and then back to a {@link File} points at the original
277 | * target.
278 | *
279 | * Strategies must remain consistent across app launches, and not rely on
280 | * dynamic state. This ensures that any generated {@link Uri} can still be
281 | * resolved if your process is killed and later restarted.
282 | *
283 | * @see SimplePathStrategy
284 | */
285 | interface PathStrategy {
286 | /**
287 | * Return a {@link Uri} that represents the given {@link File}.
288 | */
289 | public Uri getUriForFile( File file );
290 | /**
291 | * Return a {@link File} that represents the given {@link Uri}.
292 | */
293 | public File getFileForUri( Uri uri );
294 | }
295 | /**
296 | * Strategy that provides access to files living under a narrow whitelist of
297 | * filesystem roots. It will throw {@link SecurityException} if callers try
298 | * accessing files outside the configured roots.
299 | *
300 | * For example, if configured with
301 | * {@code addRoot("myfiles", context.getFilesDir())}, then
302 | * {@code context.getFileStreamPath("foo.txt")} would map to
303 | * {@code content://myauthority/myfiles/foo.txt}.
304 | */
305 | static class SimplePathStrategy implements PathStrategy {
306 | private final String mAuthority;
307 | private final HashMap mRoots = new HashMap();
308 | public SimplePathStrategy(String authority) {
309 | mAuthority = authority;
310 | }
311 | /**
312 | * Add a mapping from a name to a filesystem root. The provider only offers
313 | * access to files that live under configured roots.
314 | */
315 | public void addRoot(String name, File root) {
316 | if (TextUtils.isEmpty(name)) {
317 | throw new IllegalArgumentException("Name must not be empty");
318 | }
319 | try {
320 | // Resolve to canonical path to keep path checking fast
321 | root = root.getCanonicalFile();
322 | } catch (IOException e) {
323 | throw new IllegalArgumentException(
324 | "Failed to resolve canonical path for " + root, e);
325 | }
326 | mRoots.put(name, root);
327 | }
328 | @Override
329 | public Uri getUriForFile(File file) {
330 | String path;
331 | try {
332 | path = file.getCanonicalPath();
333 | } catch (IOException e) {
334 | throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
335 | }
336 | // Find the most-specific root path
337 | Map.Entry mostSpecific = null;
338 | for (Map.Entry root : mRoots.entrySet()) {
339 | final String rootPath = root.getValue().getPath();
340 | if (path.startsWith(rootPath) && (mostSpecific == null
341 | || rootPath.length() > mostSpecific.getValue().getPath().length())) {
342 | mostSpecific = root;
343 | }
344 | }
345 | if (mostSpecific == null) {
346 | throw new IllegalArgumentException(
347 | "Failed to find configured root that contains " + path);
348 | }
349 | // Start at first char of path under root
350 | final String rootPath = mostSpecific.getValue().getPath();
351 | if (rootPath.endsWith("/")) {
352 | path = path.substring(rootPath.length());
353 | } else {
354 | path = path.substring(rootPath.length() + 1);
355 | }
356 | // Encode the tag and path separately
357 | path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
358 | return new Uri.Builder().scheme("content")
359 | .authority(mAuthority).encodedPath(path).build();
360 | }
361 | @Override
362 | public File getFileForUri(Uri uri) {
363 | String path = uri.getEncodedPath();
364 | final int splitIndex = path.indexOf('/', 1);
365 | final String tag = Uri.decode(path.substring(1, splitIndex));
366 | path = Uri.decode(path.substring(splitIndex + 1));
367 | final File root = mRoots.get(tag);
368 | if (root == null) {
369 | throw new IllegalArgumentException("Unable to find configured root for " + uri );
370 | }
371 | File file = new File(root, path);
372 | try {
373 | file = file.getCanonicalFile();
374 | } catch (IOException e) {
375 | throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
376 | }
377 | if (!file.getPath().startsWith(root.getPath())) {
378 | throw new SecurityException("Resolved path jumped beyond configured root");
379 | }
380 | return file;
381 | }
382 | }
383 | /**
384 | * Copied from ContentResolver.java
385 | */
386 | private static int modeToMode(String mode) {
387 | int modeBits;
388 | if ("r".equals(mode)) {
389 | modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
390 | } else if ("w".equals(mode) || "wt".equals(mode)) {
391 | modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
392 | | ParcelFileDescriptor.MODE_CREATE
393 | | ParcelFileDescriptor.MODE_TRUNCATE;
394 | } else if ("wa".equals(mode)) {
395 | modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
396 | | ParcelFileDescriptor.MODE_CREATE
397 | | ParcelFileDescriptor.MODE_APPEND;
398 | } else if ("rw".equals(mode)) {
399 | modeBits = ParcelFileDescriptor.MODE_READ_WRITE
400 | | ParcelFileDescriptor.MODE_CREATE;
401 | } else if ("rwt".equals(mode)) {
402 | modeBits = ParcelFileDescriptor.MODE_READ_WRITE
403 | | ParcelFileDescriptor.MODE_CREATE
404 | | ParcelFileDescriptor.MODE_TRUNCATE;
405 | } else {
406 | throw new IllegalArgumentException("Invalid mode: " + mode);
407 | }
408 | return modeBits;
409 | }
410 | private static File buildPath(File base, String... segments) {
411 | File cur = base;
412 | for (String segment : segments) {
413 | if (segment != null) {
414 | cur = new File(cur, segment);
415 | }
416 | }
417 | return cur;
418 | }
419 | private static String[] copyOf(String[] original, int newLength) {
420 | final String[] result = new String[newLength];
421 | System.arraycopy(original, 0, result, 0, newLength);
422 | return result;
423 | }
424 | private static Object[] copyOf(Object[] original, int newLength) {
425 | final Object[] result = new Object[newLength];
426 | System.arraycopy(original, 0, result, 0, newLength);
427 | return result;
428 | }
429 | }
--------------------------------------------------------------------------------
/.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraMediaReceiver.java:
--------------------------------------------------------------------------------
1 | package com.yasirkula.unity;
2 |
3 | /**
4 | * Created by yasirkula on 22.04.2018.
5 | */
6 |
7 | public interface NativeCameraMediaReceiver
8 | {
9 | void OnMediaReceived( String path );
10 | }
11 |
--------------------------------------------------------------------------------
/.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraPermissionFragment.java:
--------------------------------------------------------------------------------
1 | package com.yasirkula.unity;
2 |
3 | // Original work Copyright (c) 2017 Yury Habets
4 | // Modified work Copyright 2018 yasirkula
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | // SOFTWARE.
23 |
24 | import android.Manifest;
25 | import android.annotation.TargetApi;
26 | import android.app.Fragment;
27 | import android.content.Intent;
28 | import android.content.pm.PackageManager;
29 | import android.os.Build;
30 | import android.os.Bundle;
31 | import android.util.Log;
32 |
33 | import java.util.ArrayList;
34 |
35 | @TargetApi( Build.VERSION_CODES.M )
36 | public class NativeCameraPermissionFragment extends Fragment
37 | {
38 | public static final String PICTURE_PERMISSION_ID = "NC_PictureMode";
39 | private static final int PERMISSIONS_REQUEST_CODE = 120645;
40 |
41 | private final NativeCameraPermissionReceiver permissionReceiver;
42 |
43 | public NativeCameraPermissionFragment()
44 | {
45 | permissionReceiver = null;
46 | }
47 |
48 | public NativeCameraPermissionFragment( final NativeCameraPermissionReceiver permissionReceiver )
49 | {
50 | this.permissionReceiver = permissionReceiver;
51 | }
52 |
53 | @Override
54 | public void onCreate( Bundle savedInstanceState )
55 | {
56 | super.onCreate( savedInstanceState );
57 | if( permissionReceiver == null )
58 | onRequestPermissionsResult( PERMISSIONS_REQUEST_CODE, new String[0], new int[0] );
59 | else
60 | {
61 | ArrayList permissions = new ArrayList( 3 );
62 | if( NativeCameraUtils.IsPermissionDefinedInManifest( getActivity(), Manifest.permission.CAMERA ) )
63 | permissions.add( Manifest.permission.CAMERA );
64 |
65 | if( Build.VERSION.SDK_INT < 30 )
66 | permissions.add( Manifest.permission.WRITE_EXTERNAL_STORAGE );
67 |
68 | if( Build.VERSION.SDK_INT < 33 || getActivity().getApplicationInfo().targetSdkVersion < 33 )
69 | permissions.add( Manifest.permission.READ_EXTERNAL_STORAGE );
70 |
71 | if( permissions.size() > 0 )
72 | {
73 | String[] permissionsArray = new String[permissions.size()];
74 | permissions.toArray( permissionsArray );
75 |
76 | requestPermissions( permissionsArray, PERMISSIONS_REQUEST_CODE );
77 | }
78 | else
79 | onRequestPermissionsResult( PERMISSIONS_REQUEST_CODE, new String[0], new int[0] );
80 | }
81 | }
82 |
83 | @Override
84 | public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults )
85 | {
86 | if( requestCode != PERMISSIONS_REQUEST_CODE )
87 | return;
88 |
89 | if( permissionReceiver == null )
90 | {
91 | Log.e( "Unity", "Fragment data got reset while asking permissions!" );
92 |
93 | getFragmentManager().beginTransaction().remove( this ).commitAllowingStateLoss();
94 | return;
95 | }
96 |
97 | // 0 -> denied, must go to settings
98 | // 1 -> granted
99 | // 2 -> denied, can ask again
100 | int result = 1;
101 | if( permissions.length == 0 || grantResults.length == 0 )
102 | result = 2;
103 | else
104 | {
105 | for( int i = 0; i < permissions.length && i < grantResults.length; ++i )
106 | {
107 | if( grantResults[i] == PackageManager.PERMISSION_DENIED )
108 | {
109 | if( !shouldShowRequestPermissionRationale( permissions[i] ) )
110 | {
111 | result = 0;
112 | break;
113 | }
114 |
115 | result = 2;
116 | }
117 | }
118 | }
119 |
120 | permissionReceiver.OnPermissionResult( result );
121 | getFragmentManager().beginTransaction().remove( this ).commitAllowingStateLoss();
122 |
123 | // Resolves a bug in Unity 2019 where the calling activity
124 | // doesn't resume automatically after the fragment finishes
125 | // Credit: https://stackoverflow.com/a/12409215/2373034
126 | try
127 | {
128 | Intent resumeUnityActivity = new Intent( getActivity(), getActivity().getClass() );
129 | resumeUnityActivity.setFlags( Intent.FLAG_ACTIVITY_REORDER_TO_FRONT );
130 | getActivity().startActivityIfNeeded( resumeUnityActivity, 0 );
131 | }
132 | catch( Exception e )
133 | {
134 | Log.e( "Unity", "Exception (resume):", e );
135 | }
136 | }
137 | }
--------------------------------------------------------------------------------
/.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraPermissionReceiver.java:
--------------------------------------------------------------------------------
1 | package com.yasirkula.unity;
2 |
3 | /**
4 | * Created by yasirkula on 5.03.2018.
5 | */
6 |
7 | public interface NativeCameraPermissionReceiver
8 | {
9 | void OnPermissionResult( int result );
10 | }
11 |
--------------------------------------------------------------------------------
/.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraPictureFragment.java:
--------------------------------------------------------------------------------
1 | package com.yasirkula.unity;
2 |
3 | import android.app.Activity;
4 | import android.app.Fragment;
5 | import android.content.ActivityNotFoundException;
6 | import android.content.Intent;
7 | import android.content.pm.PackageManager;
8 | import android.database.Cursor;
9 | import android.net.Uri;
10 | import android.os.Build;
11 | import android.os.Bundle;
12 | import android.provider.MediaStore;
13 | import android.util.Log;
14 | import android.widget.Toast;
15 |
16 | import java.io.File;
17 |
18 | /**
19 | * Created by yasirkula on 22.04.2018.
20 | */
21 |
22 | public class NativeCameraPictureFragment extends Fragment
23 | {
24 | private static final int CAMERA_PICTURE_CODE = 554776;
25 |
26 | private static final String IMAGE_NAME = "IMG_camera.jpg";
27 | public static final String DEFAULT_CAMERA_ID = "UNCP_DEF_CAMERA";
28 | public static final String AUTHORITY_ID = "UNCP_AUTHORITY";
29 |
30 | private final NativeCameraMediaReceiver mediaReceiver;
31 | private String fileTargetPath;
32 | private int lastImageId = Integer.MAX_VALUE;
33 |
34 | public NativeCameraPictureFragment()
35 | {
36 | mediaReceiver = null;
37 | }
38 |
39 | public NativeCameraPictureFragment( final NativeCameraMediaReceiver mediaReceiver )
40 | {
41 | this.mediaReceiver = mediaReceiver;
42 | }
43 |
44 | @Override
45 | public void onCreate( Bundle savedInstanceState )
46 | {
47 | super.onCreate( savedInstanceState );
48 | if( mediaReceiver == null )
49 | {
50 | Log.e( "Unity", "NativeCameraPictureFragment.mediaReceiver became null in onCreate!" );
51 | onActivityResult( CAMERA_PICTURE_CODE, Activity.RESULT_CANCELED, null );
52 | }
53 | else
54 | {
55 | int defaultCamera = getArguments().getInt( DEFAULT_CAMERA_ID );
56 | String authority = getArguments().getString( AUTHORITY_ID );
57 |
58 | File photoFile = new File( getActivity().getCacheDir(), IMAGE_NAME );
59 | try
60 | {
61 | if( photoFile.exists() )
62 | NativeCameraUtils.ClearFileContents( photoFile );
63 | else
64 | photoFile.createNewFile();
65 | }
66 | catch( Exception e )
67 | {
68 | Log.e( "Unity", "Exception:", e );
69 | onActivityResult( CAMERA_PICTURE_CODE, Activity.RESULT_CANCELED, null );
70 | return;
71 | }
72 |
73 | fileTargetPath = photoFile.getAbsolutePath();
74 |
75 | // Credit: https://stackoverflow.com/a/8555925/2373034
76 | // Get the id of the newest image in the Gallery
77 | Cursor imageCursor = null;
78 | try
79 | {
80 | imageCursor = getActivity().getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
81 | new String[] { MediaStore.Images.Media._ID }, null, null, MediaStore.Images.Media._ID + " DESC" );
82 | if( imageCursor != null )
83 | {
84 | if( imageCursor.moveToFirst() )
85 | lastImageId = imageCursor.getInt( imageCursor.getColumnIndex( MediaStore.Images.Media._ID ) );
86 | else if( imageCursor.getCount() <= 0 )
87 | {
88 | // If there are currently no images in the Gallery, after the image is captured, querying the Gallery with
89 | // "_ID > lastImageId" will return the newly captured image since its ID will always be greater than Integer.MIN_VALUE
90 | lastImageId = Integer.MIN_VALUE;
91 | }
92 | }
93 | }
94 | catch( Exception e )
95 | {
96 | Log.e( "Unity", "Exception:", e );
97 | }
98 | finally
99 | {
100 | if( imageCursor != null )
101 | imageCursor.close();
102 | }
103 |
104 | Intent intent = new Intent( MediaStore.ACTION_IMAGE_CAPTURE );
105 | NativeCameraUtils.SetOutputUri( getActivity(), intent, authority, photoFile );
106 |
107 | if( defaultCamera == 0 )
108 | NativeCameraUtils.SetDefaultCamera( intent, true );
109 | else if( defaultCamera == 1 )
110 | NativeCameraUtils.SetDefaultCamera( intent, false );
111 |
112 | if( NativeCamera.QuickCapture )
113 | intent.putExtra( "android.intent.extra.quickCapture", true );
114 |
115 | try
116 | {
117 | // MIUI devices have issues with Intent.createChooser on at least Android 11 (https://stackoverflow.com/questions/67785661/taking-and-picking-photos-on-poco-x3-with-android-11-does-not-work)
118 | if( NativeCamera.UseDefaultCameraApp || ( Build.VERSION.SDK_INT == 30 && NativeCameraUtils.IsXiaomiOrMIUI() ) )
119 | startActivityForResult( intent, CAMERA_PICTURE_CODE );
120 | else
121 | startActivityForResult( Intent.createChooser( intent, "" ), CAMERA_PICTURE_CODE );
122 | }
123 | catch( ActivityNotFoundException e )
124 | {
125 | Toast.makeText( getActivity(), "No apps can perform this action.", Toast.LENGTH_LONG ).show();
126 | onActivityResult( CAMERA_PICTURE_CODE, Activity.RESULT_CANCELED, null );
127 | }
128 | }
129 | }
130 |
131 | @Override
132 | public void onActivityResult( int requestCode, int resultCode, Intent data )
133 | {
134 | if( requestCode != CAMERA_PICTURE_CODE )
135 | return;
136 |
137 | File result = null;
138 | if( resultCode == Activity.RESULT_OK )
139 | {
140 | result = new File( fileTargetPath );
141 |
142 | if( lastImageId != 0L ) // it got reset somehow?
143 | {
144 | // Credit: https://stackoverflow.com/a/8555925/2373034
145 | // Check if the image is saved to the Gallery instead of the specified path
146 | Cursor imageCursor = null;
147 | try
148 | {
149 | final String[] imageColumns = { MediaStore.Images.Media.DATA, MediaStore.Images.Media.SIZE, MediaStore.Images.Media._ID };
150 | imageCursor = getActivity().getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageColumns,
151 | MediaStore.Images.Media._ID + ">?", new String[] { "" + lastImageId }, MediaStore.Images.Media._ID + " DESC" );
152 | if( imageCursor != null && imageCursor.moveToNext() )
153 | {
154 | String path = imageCursor.getString( imageCursor.getColumnIndex( MediaStore.Images.Media.DATA ) );
155 | if( path != null && path.length() > 0 )
156 | {
157 | boolean shouldDeleteImage = false;
158 |
159 | String id = "" + imageCursor.getInt( imageCursor.getColumnIndex( MediaStore.Images.Media._ID ) );
160 | long size = imageCursor.getLong( imageCursor.getColumnIndex( MediaStore.Images.Media.SIZE ) );
161 | if( size > result.length() )
162 | {
163 | shouldDeleteImage = true;
164 |
165 | Uri contentUri;
166 | try
167 | {
168 | contentUri = Uri.withAppendedPath( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id );
169 | }
170 | catch( Exception e )
171 | {
172 | Log.e( "Unity", "Exception:", e );
173 | contentUri = null;
174 | }
175 |
176 | NativeCameraUtils.CopyFile( getActivity(), new File( path ), result, contentUri );
177 | }
178 | else
179 | {
180 | try
181 | {
182 | if( !new File( path ).getCanonicalPath().equals( result.getCanonicalPath() ) )
183 | shouldDeleteImage = true;
184 | }
185 | catch( Exception e )
186 | {
187 | Log.e( "Unity", "Exception:", e );
188 | }
189 | }
190 |
191 | if( shouldDeleteImage && !NativeCamera.KeepGalleryReferences )
192 | {
193 | Log.d( "Unity", "Trying to delete duplicate gallery item: " + path );
194 |
195 | getActivity().getContentResolver().delete( MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
196 | MediaStore.Images.Media._ID + "=?", new String[] { id } );
197 | }
198 | }
199 | }
200 | }
201 | catch( Exception e )
202 | {
203 | Log.e( "Unity", "Exception:", e );
204 | }
205 | finally
206 | {
207 | if( imageCursor != null )
208 | imageCursor.close();
209 | }
210 | }
211 | }
212 |
213 | Log.d( "Unity", "NativeCameraPictureFragment.onActivityResult: " + ( ( result == null ) ? "null" : ( ( result.exists() ? result.length() : -1 ) + " " + result.getAbsolutePath() ) ) );
214 | if( mediaReceiver != null )
215 | mediaReceiver.OnMediaReceived( ( result != null && result.exists() && result.length() > 1L ) ? result.getAbsolutePath() : "" );
216 | else
217 | Log.e( "Unity", "NativeCameraPictureFragment.mediaReceiver became null in onActivityResult!" );
218 |
219 | getFragmentManager().beginTransaction().remove( this ).commitAllowingStateLoss();
220 | }
221 | }
--------------------------------------------------------------------------------
/.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraUtils.java:
--------------------------------------------------------------------------------
1 | package com.yasirkula.unity;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.ClipData;
5 | import android.content.ContentResolver;
6 | import android.content.ContentUris;
7 | import android.content.Context;
8 | import android.content.Intent;
9 | import android.content.pm.PackageInfo;
10 | import android.content.pm.PackageManager;
11 | import android.content.pm.ProviderInfo;
12 | import android.database.Cursor;
13 | import android.graphics.Bitmap;
14 | import android.graphics.BitmapFactory;
15 | import android.graphics.Matrix;
16 | import android.media.ExifInterface;
17 | import android.media.MediaMetadataRetriever;
18 | import android.media.ThumbnailUtils;
19 | import android.net.Uri;
20 | import android.os.Build;
21 | import android.os.Environment;
22 | import android.provider.DocumentsContract;
23 | import android.provider.MediaStore;
24 | import android.provider.OpenableColumns;
25 | import android.util.Log;
26 | import android.util.Size;
27 | import android.webkit.MimeTypeMap;
28 |
29 | import java.io.BufferedReader;
30 | import java.io.File;
31 | import java.io.FileInputStream;
32 | import java.io.FileOutputStream;
33 | import java.io.IOException;
34 | import java.io.InputStream;
35 | import java.io.InputStreamReader;
36 | import java.io.OutputStream;
37 | import java.io.RandomAccessFile;
38 | import java.util.Locale;
39 |
40 | /**
41 | * Created by yasirkula on 30.04.2018.
42 | */
43 |
44 | public class NativeCameraUtils
45 | {
46 | private static String authority = null;
47 | private static String secondaryStoragePath = null;
48 | private static int isXiaomiOrMIUI = 0; // 1: true, -1: false
49 |
50 | public static String GetAuthority( Context context )
51 | {
52 | if( authority == null )
53 | {
54 | // Find the authority of ContentProvider first
55 | // Credit: https://stackoverflow.com/a/2001769/2373034
56 | try
57 | {
58 | PackageInfo packageInfo = context.getPackageManager().getPackageInfo( context.getPackageName(), PackageManager.GET_PROVIDERS );
59 | ProviderInfo[] providers = packageInfo.providers;
60 | if( providers != null )
61 | {
62 | for( ProviderInfo provider : providers )
63 | {
64 | if( provider.name != null && provider.packageName != null && provider.authority != null &&
65 | provider.name.equals( NativeCameraContentProvider.class.getName() ) && provider.packageName.equals( context.getPackageName() )
66 | && provider.authority.length() > 0 )
67 | {
68 | authority = provider.authority;
69 | break;
70 | }
71 | }
72 | }
73 | }
74 | catch( Exception e )
75 | {
76 | Log.e( "Unity", "Exception:", e );
77 | }
78 | }
79 |
80 | return authority;
81 | }
82 |
83 | public static boolean IsXiaomiOrMIUI()
84 | {
85 | if( isXiaomiOrMIUI > 0 )
86 | return true;
87 | else if( isXiaomiOrMIUI < 0 )
88 | return false;
89 |
90 | if( "xiaomi".equalsIgnoreCase( android.os.Build.MANUFACTURER ) )
91 | {
92 | isXiaomiOrMIUI = 1;
93 | return true;
94 | }
95 |
96 | // Check if device is using MIUI
97 | // Credit: https://gist.github.com/Muyangmin/e8ec1002c930d8df3df46b306d03315d
98 | String line;
99 | BufferedReader inputStream = null;
100 | try
101 | {
102 | java.lang.Process process = Runtime.getRuntime().exec( "getprop ro.miui.ui.version.name" );
103 | inputStream = new BufferedReader( new InputStreamReader( process.getInputStream() ), 1024 );
104 | line = inputStream.readLine();
105 |
106 | if( line != null && line.length() > 0 )
107 | {
108 | isXiaomiOrMIUI = 1;
109 | return true;
110 | }
111 | else
112 | {
113 | isXiaomiOrMIUI = -1;
114 | return false;
115 | }
116 | }
117 | catch( Exception e )
118 | {
119 | isXiaomiOrMIUI = -1;
120 | return false;
121 | }
122 | finally
123 | {
124 | if( inputStream != null )
125 | {
126 | try
127 | {
128 | inputStream.close();
129 | }
130 | catch( Exception e )
131 | {
132 | }
133 | }
134 | }
135 | }
136 |
137 | // Credit: https://github.com/jamesmontemagno/MediaPlugin/issues/307#issuecomment-356199135
138 | // Credit: https://stackoverflow.com/a/67288087/2373034
139 | public static void SetDefaultCamera( Intent intent, boolean useRearCamera )
140 | {
141 | if( useRearCamera )
142 | {
143 | intent.putExtra( "android.intent.extras.LENS_FACING_BACK", 1 );
144 | intent.putExtra( "android.intent.extras.CAMERA_FACING", 0 );
145 | intent.putExtra( "android.intent.extra.USE_FRONT_CAMERA", false );
146 | intent.putExtra( "com.google.assistant.extra.USE_FRONT_CAMERA", false );
147 | }
148 | else
149 | {
150 | intent.putExtra( "android.intent.extras.LENS_FACING_FRONT", 1 );
151 | intent.putExtra( "android.intent.extras.CAMERA_FACING", 1 );
152 | intent.putExtra( "android.intent.extra.USE_FRONT_CAMERA", true );
153 | intent.putExtra( "com.google.assistant.extra.USE_FRONT_CAMERA", true );
154 |
155 | intent.putExtra( "camerafacing", "front" );
156 | intent.putExtra( "default_camera", "1" );
157 | }
158 | }
159 |
160 | @TargetApi( Build.VERSION_CODES.JELLY_BEAN )
161 | public static void SetOutputUri( Context context, Intent intent, String authority, File output )
162 | {
163 | Uri uri;
164 | if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN )
165 | uri = NativeCameraContentProvider.getUriForFile( context, authority, output );
166 | else
167 | uri = Uri.fromFile( output );
168 |
169 | intent.putExtra( MediaStore.EXTRA_OUTPUT, uri );
170 |
171 | // Credit: https://medium.com/@quiro91/sharing-files-through-intents-part-2-fixing-the-permissions-before-lollipop-ceb9bb0eec3a
172 | if( Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN )
173 | {
174 | intent.setClipData( ClipData.newRawUri( "", uri ) );
175 | intent.setFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION );
176 | }
177 | }
178 |
179 | // Credit: https://stackoverflow.com/a/6994668/2373034
180 | public static void ClearFileContents( File file ) throws IOException
181 | {
182 | RandomAccessFile stream = null;
183 | try
184 | {
185 | stream = new RandomAccessFile( file, "rw" );
186 | stream.setLength( 0 );
187 | }
188 | finally
189 | {
190 | if( stream != null )
191 | {
192 | try
193 | {
194 | stream.close();
195 | }
196 | catch( Exception e )
197 | {
198 | }
199 | }
200 | }
201 | }
202 |
203 | // Credit: https://stackoverflow.com/a/9293885/2373034
204 | public static void CopyFile( Context context, File src, File dst, Uri rawUri )
205 | {
206 | try
207 | {
208 | if( !src.exists() )
209 | return;
210 |
211 | if( dst.exists() )
212 | ClearFileContents( dst );
213 |
214 | InputStream in = new FileInputStream( src );
215 | try
216 | {
217 | OutputStream out = new FileOutputStream( dst );
218 | try
219 | {
220 | byte[] buf = new byte[1024];
221 | int len;
222 | while( ( len = in.read( buf ) ) > 0 )
223 | out.write( buf, 0, len );
224 | }
225 | finally
226 | {
227 | out.close();
228 | }
229 | }
230 | finally
231 | {
232 | in.close();
233 | }
234 | }
235 | catch( Exception e )
236 | {
237 | if( rawUri == null )
238 | Log.e( "Unity", "Exception:", e );
239 | else
240 | {
241 | // Try to save the file via contentResolver (can happen e.g. on Android 10 where raw file system access is restricted)
242 | try
243 | {
244 | InputStream input = context.getContentResolver().openInputStream( rawUri );
245 | if( input == null )
246 | return;
247 |
248 | OutputStream output = null;
249 | try
250 | {
251 | output = new FileOutputStream( dst, false );
252 |
253 | byte[] buf = new byte[4096];
254 | int len;
255 | while( ( len = input.read( buf ) ) > 0 )
256 | output.write( buf, 0, len );
257 | }
258 | finally
259 | {
260 | if( output != null )
261 | output.close();
262 |
263 | input.close();
264 | }
265 | }
266 | catch( Exception e2 )
267 | {
268 | Log.e( "Unity", "Exception:", e );
269 | Log.e( "Unity", "Exception2:", e2 );
270 | }
271 | }
272 | }
273 | }
274 |
275 | public static boolean IsPermissionDefinedInManifest( Context context, String permission )
276 | {
277 | try
278 | {
279 | String[] requestedPermissions = context.getPackageManager().getPackageInfo( context.getPackageName(), PackageManager.GET_PERMISSIONS ).requestedPermissions;
280 | if( requestedPermissions != null )
281 | {
282 | for( String requestedPermission : requestedPermissions )
283 | {
284 | if( permission.equals( requestedPermission ) )
285 | return true;
286 | }
287 | }
288 | }
289 | catch( PackageManager.NameNotFoundException e )
290 | {
291 | }
292 |
293 | return false;
294 | }
295 |
296 | public static String GetPathFromURIOrCopyFile( Context context, Uri uri, String defaultPath )
297 | {
298 | if( uri == null )
299 | return null;
300 |
301 | String path = GetPathFromURI( context, uri );
302 | if( path == null || path.length() == 0 )
303 | {
304 | Log.d( "Unity", "Filepath of '" + uri + "' couldn't be determined. Falling back to: " + defaultPath );
305 | path = defaultPath;
306 | }
307 |
308 | if( path != null && path.length() > 0 )
309 | {
310 | // Check if file is accessible
311 | FileInputStream inputStream = null;
312 | try
313 | {
314 | inputStream = new FileInputStream( new File( path ) );
315 | inputStream.read();
316 |
317 | return path;
318 | }
319 | catch( Exception e )
320 | {
321 | Log.e( "Unity", "Media uri isn't accessible via File API: " + uri, e );
322 | }
323 | finally
324 | {
325 | if( inputStream != null )
326 | {
327 | try
328 | {
329 | inputStream.close();
330 | }
331 | catch( Exception e )
332 | {
333 | }
334 | }
335 | }
336 | }
337 |
338 | // File path couldn't be determined, copy the file to an accessible temporary location
339 | // Credit: https://developer.android.com/training/secure-file-sharing/retrieve-info.html#RetrieveFileInfo
340 | ContentResolver resolver = context.getContentResolver();
341 | Cursor returnCursor = null;
342 | String filename = null;
343 |
344 | try
345 | {
346 | returnCursor = resolver.query( uri, null, null, null, null );
347 | if( returnCursor != null && returnCursor.moveToFirst() )
348 | filename = returnCursor.getString( returnCursor.getColumnIndex( OpenableColumns.DISPLAY_NAME ) );
349 | }
350 | catch( Exception e )
351 | {
352 | Log.e( "Unity", "Exception:", e );
353 | }
354 | finally
355 | {
356 | if( returnCursor != null )
357 | returnCursor.close();
358 | }
359 |
360 | String extension = null;
361 | int filenameExtensionIndex = ( filename != null ) ? filename.lastIndexOf( '.' ) : -1;
362 | if( filenameExtensionIndex > 0 && filenameExtensionIndex < filename.length() - 1 )
363 | extension = filename.substring( filenameExtensionIndex ).toLowerCase( Locale.US );
364 | else
365 | {
366 | String mime = resolver.getType( uri );
367 | if( mime != null )
368 | {
369 | String mimeExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType( mime );
370 | if( mimeExtension != null && mimeExtension.length() > 0 )
371 | extension = "." + mimeExtension;
372 | }
373 | }
374 |
375 | if( extension == null )
376 | extension = ".mp4";
377 |
378 | try
379 | {
380 | InputStream input = resolver.openInputStream( uri );
381 | if( input == null )
382 | {
383 | Log.w( "Unity", "Couldn't open input stream: " + uri );
384 | return null;
385 | }
386 |
387 | File tempFile = new File( context.getCacheDir().getAbsolutePath(), "VID_copy" + extension );
388 | OutputStream output = null;
389 | try
390 | {
391 | output = new FileOutputStream( tempFile, false );
392 |
393 | byte[] buf = new byte[4096];
394 | int len;
395 | while( ( len = input.read( buf ) ) > 0 )
396 | {
397 | output.write( buf, 0, len );
398 | }
399 |
400 | Log.d( "Unity", "Copied media from " + uri + " to: " + tempFile.getAbsolutePath() );
401 | return tempFile.getAbsolutePath();
402 | }
403 | finally
404 | {
405 | if( output != null )
406 | output.close();
407 |
408 | input.close();
409 | }
410 | }
411 | catch( Exception e )
412 | {
413 | Log.e( "Unity", "Exception:", e );
414 | }
415 |
416 | return null;
417 | }
418 |
419 | // Credit: https://stackoverflow.com/a/36714242/2373034
420 | public static String GetPathFromURI( Context context, Uri uri )
421 | {
422 | if( uri == null )
423 | return null;
424 |
425 | Log.d( "Unity", "Media uri: " + uri.toString() );
426 |
427 | String selection = null;
428 | String[] selectionArgs = null;
429 |
430 | try
431 | {
432 | if( Build.VERSION.SDK_INT >= 19 && DocumentsContract.isDocumentUri( context.getApplicationContext(), uri ) )
433 | {
434 | if( "com.android.externalstorage.documents".equals( uri.getAuthority() ) )
435 | {
436 | final String docId = DocumentsContract.getDocumentId( uri );
437 | final String[] split = docId.split( ":" );
438 |
439 | if( "primary".equalsIgnoreCase( split[0] ) )
440 | return Environment.getExternalStorageDirectory() + File.separator + split[1];
441 | else if( "raw".equalsIgnoreCase( split[0] ) ) // https://stackoverflow.com/a/51874578/2373034
442 | return split[1];
443 |
444 | return getSecondaryStoragePathFor( split[1] );
445 | }
446 | else if( "com.android.providers.downloads.documents".equals( uri.getAuthority() ) )
447 | {
448 | final String id = DocumentsContract.getDocumentId( uri );
449 | if( id.startsWith( "raw:" ) ) // https://stackoverflow.com/a/51874578/2373034
450 | return id.substring( 4 );
451 | else if( id.indexOf( ':' ) < 0 ) // Don't attempt to parse stuff like "msf:NUMBER" (newer Android versions)
452 | uri = ContentUris.withAppendedId( Uri.parse( "content://downloads/public_downloads" ), Long.parseLong( id ) );
453 | else
454 | return null;
455 | }
456 | else if( "com.android.providers.media.documents".equals( uri.getAuthority() ) )
457 | {
458 | final String docId = DocumentsContract.getDocumentId( uri );
459 | final String[] split = docId.split( ":" );
460 | final String type = split[0];
461 | if( "image".equals( type ) )
462 | uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
463 | else if( "video".equals( type ) )
464 | uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
465 | else if( "audio".equals( type ) )
466 | uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
467 | else if( "raw".equals( type ) ) // https://stackoverflow.com/a/51874578/2373034
468 | return split[1];
469 |
470 | selection = "_id=?";
471 | selectionArgs = new String[] { split[1] };
472 | }
473 | }
474 |
475 | if( "content".equalsIgnoreCase( uri.getScheme() ) )
476 | {
477 | String[] projection = { MediaStore.Images.Media.DATA };
478 | Cursor cursor = null;
479 |
480 | try
481 | {
482 | cursor = context.getContentResolver().query( uri, projection, selection, selectionArgs, null );
483 | if( cursor != null )
484 | {
485 | int column_index = cursor.getColumnIndexOrThrow( MediaStore.Images.Media.DATA );
486 | if( cursor.moveToFirst() )
487 | {
488 | String columnValue = cursor.getString( column_index );
489 | if( columnValue != null && columnValue.length() > 0 )
490 | return columnValue;
491 | }
492 | }
493 | }
494 | catch( Exception e )
495 | {
496 | }
497 | finally
498 | {
499 | if( cursor != null )
500 | cursor.close();
501 | }
502 | }
503 | else if( "file".equalsIgnoreCase( uri.getScheme() ) )
504 | return uri.getPath();
505 |
506 | // File path couldn't be determined
507 | return null;
508 | }
509 | catch( Exception e )
510 | {
511 | Log.e( "Unity", "Exception:", e );
512 | return null;
513 | }
514 | }
515 |
516 | private static String getSecondaryStoragePathFor( String localPath )
517 | {
518 | if( secondaryStoragePath == null )
519 | {
520 | String primaryPath = Environment.getExternalStorageDirectory().getAbsolutePath();
521 |
522 | // Try paths saved at system environments
523 | // Credit: https://stackoverflow.com/a/32088396/2373034
524 | String strSDCardPath = System.getenv( "SECONDARY_STORAGE" );
525 | if( strSDCardPath == null || strSDCardPath.length() == 0 )
526 | strSDCardPath = System.getenv( "EXTERNAL_SDCARD_STORAGE" );
527 |
528 | if( strSDCardPath != null && strSDCardPath.length() > 0 )
529 | {
530 | if( !strSDCardPath.contains( ":" ) )
531 | strSDCardPath += ":";
532 |
533 | String[] externalPaths = strSDCardPath.split( ":" );
534 | for( int i = 0; i < externalPaths.length; i++ )
535 | {
536 | String path = externalPaths[i];
537 | if( path != null && path.length() > 0 )
538 | {
539 | File file = new File( path );
540 | if( file.exists() && file.isDirectory() && file.canRead() && !file.getAbsolutePath().equalsIgnoreCase( primaryPath ) )
541 | {
542 | String absolutePath = file.getAbsolutePath() + File.separator + localPath;
543 | if( new File( absolutePath ).exists() )
544 | {
545 | secondaryStoragePath = file.getAbsolutePath();
546 | return absolutePath;
547 | }
548 | }
549 | }
550 | }
551 | }
552 |
553 | // Try most common possible paths
554 | // Credit: https://gist.github.com/PauloLuan/4bcecc086095bce28e22
555 | String[] possibleRoots = new String[] { "/storage", "/mnt", "/storage/removable",
556 | "/removable", "/data", "/mnt/media_rw", "/mnt/sdcard0" };
557 | for( String root : possibleRoots )
558 | {
559 | try
560 | {
561 | File fileList[] = new File( root ).listFiles();
562 | for( File file : fileList )
563 | {
564 | if( file.exists() && file.isDirectory() && file.canRead() && !file.getAbsolutePath().equalsIgnoreCase( primaryPath ) )
565 | {
566 | String absolutePath = file.getAbsolutePath() + File.separator + localPath;
567 | if( new File( absolutePath ).exists() )
568 | {
569 | secondaryStoragePath = file.getAbsolutePath();
570 | return absolutePath;
571 | }
572 | }
573 | }
574 | }
575 | catch( Exception e )
576 | {
577 | }
578 | }
579 |
580 | secondaryStoragePath = "_NulL_";
581 | }
582 | else if( !secondaryStoragePath.equals( "_NulL_" ) )
583 | return secondaryStoragePath + File.separator + localPath;
584 |
585 | return null;
586 | }
587 |
588 | private static BitmapFactory.Options GetImageMetadata( final String path )
589 | {
590 | try
591 | {
592 | BitmapFactory.Options result = new BitmapFactory.Options();
593 | result.inJustDecodeBounds = true;
594 | BitmapFactory.decodeFile( path, result );
595 |
596 | return result;
597 | }
598 | catch( Exception e )
599 | {
600 | Log.e( "Unity", "Exception:", e );
601 | return null;
602 | }
603 | }
604 |
605 | // Credit: https://stackoverflow.com/a/30572852/2373034
606 | private static int GetImageOrientation( Context context, final String path )
607 | {
608 | try
609 | {
610 | ExifInterface exif = new ExifInterface( path );
611 | int orientationEXIF = exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED );
612 | if( orientationEXIF != ExifInterface.ORIENTATION_UNDEFINED )
613 | return orientationEXIF;
614 | }
615 | catch( Exception e )
616 | {
617 | }
618 |
619 | Cursor cursor = null;
620 | try
621 | {
622 | cursor = context.getContentResolver().query( Uri.fromFile( new File( path ) ), new String[] { MediaStore.Images.Media.ORIENTATION }, null, null, null );
623 | if( cursor != null && cursor.moveToFirst() )
624 | {
625 | int orientation = cursor.getInt( cursor.getColumnIndex( MediaStore.Images.Media.ORIENTATION ) );
626 | if( orientation == 90 )
627 | return ExifInterface.ORIENTATION_ROTATE_90;
628 | if( orientation == 180 )
629 | return ExifInterface.ORIENTATION_ROTATE_180;
630 | if( orientation == 270 )
631 | return ExifInterface.ORIENTATION_ROTATE_270;
632 |
633 | return ExifInterface.ORIENTATION_NORMAL;
634 | }
635 | }
636 | catch( Exception e )
637 | {
638 | }
639 | finally
640 | {
641 | if( cursor != null )
642 | cursor.close();
643 | }
644 |
645 | return ExifInterface.ORIENTATION_UNDEFINED;
646 | }
647 |
648 | // Credit: https://gist.github.com/aviadmini/4be34097dfdb842ae066fae48501ed41
649 | private static Matrix GetImageOrientationCorrectionMatrix( final int orientation, final float scale )
650 | {
651 | Matrix matrix = new Matrix();
652 |
653 | switch( orientation )
654 | {
655 | case ExifInterface.ORIENTATION_ROTATE_270:
656 | {
657 | matrix.postRotate( 270 );
658 | matrix.postScale( scale, scale );
659 |
660 | break;
661 | }
662 | case ExifInterface.ORIENTATION_ROTATE_180:
663 | {
664 | matrix.postRotate( 180 );
665 | matrix.postScale( scale, scale );
666 |
667 | break;
668 | }
669 | case ExifInterface.ORIENTATION_ROTATE_90:
670 | {
671 | matrix.postRotate( 90 );
672 | matrix.postScale( scale, scale );
673 |
674 | break;
675 | }
676 | case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
677 | {
678 | matrix.postScale( -scale, scale );
679 | break;
680 | }
681 | case ExifInterface.ORIENTATION_FLIP_VERTICAL:
682 | {
683 | matrix.postScale( scale, -scale );
684 | break;
685 | }
686 | case ExifInterface.ORIENTATION_TRANSPOSE:
687 | {
688 | matrix.postRotate( 90 );
689 | matrix.postScale( -scale, scale );
690 |
691 | break;
692 | }
693 | case ExifInterface.ORIENTATION_TRANSVERSE:
694 | {
695 | matrix.postRotate( 270 );
696 | matrix.postScale( -scale, scale );
697 |
698 | break;
699 | }
700 | default:
701 | {
702 | matrix.postScale( scale, scale );
703 | break;
704 | }
705 | }
706 |
707 | return matrix;
708 | }
709 |
710 | public static String LoadImageAtPath( Context context, String path, final String temporaryFilePath, final int maxSize )
711 | {
712 | BitmapFactory.Options metadata = GetImageMetadata( path );
713 | if( metadata == null )
714 | return path;
715 |
716 | boolean shouldCreateNewBitmap = false;
717 | if( metadata.outWidth > maxSize || metadata.outHeight > maxSize )
718 | shouldCreateNewBitmap = true;
719 |
720 | if( metadata.outMimeType != null && !metadata.outMimeType.equals( "image/jpeg" ) && !metadata.outMimeType.equals( "image/png" ) )
721 | shouldCreateNewBitmap = true;
722 |
723 | int orientation = GetImageOrientation( context, path );
724 | if( orientation != ExifInterface.ORIENTATION_NORMAL && orientation != ExifInterface.ORIENTATION_UNDEFINED )
725 | shouldCreateNewBitmap = true;
726 |
727 | if( shouldCreateNewBitmap )
728 | {
729 | Bitmap bitmap = null;
730 | FileOutputStream out = null;
731 |
732 | try
733 | {
734 | // Credit: https://developer.android.com/topic/performance/graphics/load-bitmap.html
735 | int sampleSize = 1;
736 | int halfHeight = metadata.outHeight / 2;
737 | int halfWidth = metadata.outWidth / 2;
738 | while( ( halfHeight / sampleSize ) >= maxSize || ( halfWidth / sampleSize ) >= maxSize )
739 | sampleSize *= 2;
740 |
741 | BitmapFactory.Options options = new BitmapFactory.Options();
742 | options.inSampleSize = sampleSize;
743 | options.inJustDecodeBounds = false;
744 | bitmap = BitmapFactory.decodeFile( path, options );
745 |
746 | float scaleX = 1f, scaleY = 1f;
747 | if( bitmap.getWidth() > maxSize )
748 | scaleX = maxSize / (float) bitmap.getWidth();
749 | if( bitmap.getHeight() > maxSize )
750 | scaleY = maxSize / (float) bitmap.getHeight();
751 |
752 | // Create a new bitmap if it should be scaled down or if its orientation is wrong
753 | float scale = scaleX < scaleY ? scaleX : scaleY;
754 | if( scale < 1f || ( orientation != ExifInterface.ORIENTATION_NORMAL && orientation != ExifInterface.ORIENTATION_UNDEFINED ) )
755 | {
756 | Matrix transformationMatrix = GetImageOrientationCorrectionMatrix( orientation, scale );
757 | Bitmap transformedBitmap = Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), transformationMatrix, true );
758 | if( transformedBitmap != bitmap )
759 | {
760 | bitmap.recycle();
761 | bitmap = transformedBitmap;
762 | }
763 | }
764 |
765 | out = new FileOutputStream( temporaryFilePath );
766 | if( metadata.outMimeType == null || !metadata.outMimeType.equals( "image/jpeg" ) )
767 | bitmap.compress( Bitmap.CompressFormat.PNG, 100, out );
768 | else
769 | bitmap.compress( Bitmap.CompressFormat.JPEG, 100, out );
770 |
771 | path = temporaryFilePath;
772 | }
773 | catch( Exception e )
774 | {
775 | Log.e( "Unity", "Exception:", e );
776 |
777 | try
778 | {
779 | File temporaryFile = new File( temporaryFilePath );
780 | if( temporaryFile.exists() )
781 | temporaryFile.delete();
782 | }
783 | catch( Exception e2 )
784 | {
785 | }
786 | }
787 | finally
788 | {
789 | if( bitmap != null )
790 | bitmap.recycle();
791 |
792 | try
793 | {
794 | if( out != null )
795 | out.close();
796 | }
797 | catch( Exception e )
798 | {
799 | }
800 | }
801 | }
802 |
803 | return path;
804 | }
805 |
806 | public static String GetImageProperties( Context context, final String path )
807 | {
808 | BitmapFactory.Options metadata = GetImageMetadata( path );
809 | if( metadata == null )
810 | return "";
811 |
812 | int width = metadata.outWidth;
813 | int height = metadata.outHeight;
814 |
815 | String mimeType = metadata.outMimeType;
816 | if( mimeType == null )
817 | mimeType = "";
818 |
819 | int orientationUnity;
820 | int orientation = GetImageOrientation( context, path );
821 | if( orientation == ExifInterface.ORIENTATION_UNDEFINED )
822 | orientationUnity = -1;
823 | else if( orientation == ExifInterface.ORIENTATION_NORMAL )
824 | orientationUnity = 0;
825 | else if( orientation == ExifInterface.ORIENTATION_ROTATE_90 )
826 | orientationUnity = 1;
827 | else if( orientation == ExifInterface.ORIENTATION_ROTATE_180 )
828 | orientationUnity = 2;
829 | else if( orientation == ExifInterface.ORIENTATION_ROTATE_270 )
830 | orientationUnity = 3;
831 | else if( orientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL )
832 | orientationUnity = 4;
833 | else if( orientation == ExifInterface.ORIENTATION_TRANSPOSE )
834 | orientationUnity = 5;
835 | else if( orientation == ExifInterface.ORIENTATION_FLIP_VERTICAL )
836 | orientationUnity = 6;
837 | else if( orientation == ExifInterface.ORIENTATION_TRANSVERSE )
838 | orientationUnity = 7;
839 | else
840 | orientationUnity = -1;
841 |
842 | if( orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270 ||
843 | orientation == ExifInterface.ORIENTATION_TRANSPOSE || orientation == ExifInterface.ORIENTATION_TRANSVERSE )
844 | {
845 | int temp = width;
846 | width = height;
847 | height = temp;
848 | }
849 |
850 | return width + ">" + height + ">" + mimeType + ">" + orientationUnity;
851 | }
852 |
853 | @TargetApi( Build.VERSION_CODES.JELLY_BEAN_MR1 )
854 | public static String GetVideoProperties( Context context, final String path )
855 | {
856 | MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
857 | try
858 | {
859 | metadataRetriever.setDataSource( path );
860 |
861 | String width = metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH );
862 | String height = metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT );
863 | String duration = metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_DURATION );
864 | String rotation = "0";
865 | if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 )
866 | rotation = metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION );
867 |
868 | if( width == null )
869 | width = "0";
870 | if( height == null )
871 | height = "0";
872 | if( duration == null )
873 | duration = "0";
874 | if( rotation == null )
875 | rotation = "0";
876 |
877 | return width + ">" + height + ">" + duration + ">" + rotation;
878 | }
879 | catch( Exception e )
880 | {
881 | Log.e( "Unity", "Exception:", e );
882 | return "";
883 | }
884 | finally
885 | {
886 | try
887 | {
888 | metadataRetriever.release();
889 | }
890 | catch( Exception e )
891 | {
892 | }
893 | }
894 | }
895 |
896 | @TargetApi( Build.VERSION_CODES.Q )
897 | public static String GetVideoThumbnail( Context context, final String path, final String savePath, final boolean saveAsJpeg, int maxSize, double captureTime )
898 | {
899 | Bitmap bitmap = null;
900 | FileOutputStream out = null;
901 |
902 | try
903 | {
904 | if( captureTime < 0.0 && maxSize <= 1024 )
905 | {
906 | try
907 | {
908 | if( Build.VERSION.SDK_INT < Build.VERSION_CODES.Q )
909 | bitmap = ThumbnailUtils.createVideoThumbnail( path, maxSize > 512 ? MediaStore.Video.Thumbnails.FULL_SCREEN_KIND : MediaStore.Video.Thumbnails.MINI_KIND );
910 | else
911 | bitmap = ThumbnailUtils.createVideoThumbnail( new File( path ), maxSize > 512 ? new Size( 1024, 786 ) : new Size( 512, 384 ), null );
912 | }
913 | catch( Exception e )
914 | {
915 | Log.e( "Unity", "Exception:", e );
916 | }
917 | }
918 |
919 | if( bitmap == null )
920 | {
921 | MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
922 | try
923 | {
924 | metadataRetriever.setDataSource( path );
925 |
926 | try
927 | {
928 | int width = Integer.parseInt( metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH ) );
929 | int height = Integer.parseInt( metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT ) );
930 | if( maxSize > width && maxSize > height )
931 | maxSize = width > height ? width : height;
932 | }
933 | catch( Exception e )
934 | {
935 | }
936 |
937 | if( captureTime < 0.0 )
938 | captureTime = 0.0;
939 | else
940 | {
941 | try
942 | {
943 | double duration = Long.parseLong( metadataRetriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_DURATION ) ) / 1000.0;
944 | if( captureTime > duration )
945 | captureTime = duration;
946 | }
947 | catch( Exception e )
948 | {
949 | }
950 | }
951 |
952 | long frameTime = (long) ( captureTime * 1000000.0 );
953 | if( Build.VERSION.SDK_INT < 27 )
954 | bitmap = metadataRetriever.getFrameAtTime( frameTime, MediaMetadataRetriever.OPTION_CLOSEST_SYNC );
955 | else
956 | bitmap = metadataRetriever.getScaledFrameAtTime( frameTime, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, maxSize, maxSize );
957 | }
958 | finally
959 | {
960 | try
961 | {
962 | metadataRetriever.release();
963 | }
964 | catch( Exception e )
965 | {
966 | }
967 | }
968 | }
969 |
970 | if( bitmap == null )
971 | return "";
972 |
973 | out = new FileOutputStream( savePath );
974 | if( saveAsJpeg )
975 | bitmap.compress( Bitmap.CompressFormat.JPEG, 100, out );
976 | else
977 | bitmap.compress( Bitmap.CompressFormat.PNG, 100, out );
978 |
979 | return savePath;
980 | }
981 | catch( Exception e )
982 | {
983 | Log.e( "Unity", "Exception:", e );
984 | return "";
985 | }
986 | finally
987 | {
988 | if( bitmap != null )
989 | bitmap.recycle();
990 |
991 | try
992 | {
993 | if( out != null )
994 | out.close();
995 | }
996 | catch( Exception e )
997 | {
998 | }
999 | }
1000 | }
1001 | }
--------------------------------------------------------------------------------
/.github/AAR Source (Android)/java/com/yasirkula/unity/NativeCameraVideoFragment.java:
--------------------------------------------------------------------------------
1 | package com.yasirkula.unity;
2 |
3 | import android.app.Activity;
4 | import android.app.Fragment;
5 | import android.content.ActivityNotFoundException;
6 | import android.content.Intent;
7 | import android.content.pm.PackageManager;
8 | import android.database.Cursor;
9 | import android.net.Uri;
10 | import android.os.Build;
11 | import android.os.Bundle;
12 | import android.os.Environment;
13 | import android.provider.MediaStore;
14 | import android.util.Log;
15 | import android.widget.Toast;
16 |
17 | import java.io.File;
18 | import java.util.Locale;
19 |
20 | /**
21 | * Created by yasirkula on 22.04.2018.
22 | */
23 |
24 | public class NativeCameraVideoFragment extends Fragment
25 | {
26 | private static final int CAMERA_VIDEO_CODE = 554777;
27 |
28 | public static final String VIDEO_NAME = "VID_camera";
29 | public static final String DEFAULT_CAMERA_ID = "UNCV_DEF_CAMERA";
30 | public static final String AUTHORITY_ID = "UNCV_AUTHORITY";
31 | public static final String QUALITY_ID = "UNCV_QUALITY";
32 | public static final String MAX_DURATION_ID = "UNCV_DURATION";
33 | public static final String MAX_SIZE_ID = "UNCV_SIZE";
34 |
35 | public static boolean provideExtraOutputOnAndroidQ = true;
36 |
37 | private final NativeCameraMediaReceiver mediaReceiver;
38 | private String fileTargetPath;
39 | private int lastVideoId = Integer.MAX_VALUE;
40 |
41 | public NativeCameraVideoFragment()
42 | {
43 | mediaReceiver = null;
44 | }
45 |
46 | public NativeCameraVideoFragment( final NativeCameraMediaReceiver mediaReceiver )
47 | {
48 | this.mediaReceiver = mediaReceiver;
49 | }
50 |
51 | @Override
52 | public void onCreate( Bundle savedInstanceState )
53 | {
54 | super.onCreate( savedInstanceState );
55 | if( mediaReceiver == null )
56 | {
57 | Log.e( "Unity", "NativeCameraVideoFragment.mediaReceiver became null in onCreate!" );
58 | onActivityResult( CAMERA_VIDEO_CODE, Activity.RESULT_CANCELED, null );
59 | }
60 | else
61 | {
62 | int defaultCamera = getArguments().getInt( DEFAULT_CAMERA_ID );
63 | String authority = getArguments().getString( AUTHORITY_ID );
64 | int quality = getArguments().getInt( QUALITY_ID );
65 | int maxDuration = getArguments().getInt( MAX_DURATION_ID );
66 | long maxSize = getArguments().getLong( MAX_SIZE_ID );
67 |
68 | // Credit: https://stackoverflow.com/a/8555925/2373034
69 | // Get the id of the newest video in the Gallery
70 | Cursor videoCursor = null;
71 | try
72 | {
73 | videoCursor = getActivity().getContentResolver().query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
74 | new String[] { MediaStore.Video.Media._ID }, null, null, MediaStore.Video.Media._ID + " DESC" );
75 | if( videoCursor != null )
76 | {
77 | if( videoCursor.moveToFirst() )
78 | lastVideoId = videoCursor.getInt( videoCursor.getColumnIndex( MediaStore.Video.Media._ID ) );
79 | else if( videoCursor.getCount() <= 0 )
80 | {
81 | // If there are currently no videos in the Gallery, after the video is captured, querying the Gallery with
82 | // "_ID > lastVideoId" will return the newly captured video since its ID will always be greater than Integer.MIN_VALUE
83 | lastVideoId = Integer.MIN_VALUE;
84 | }
85 | }
86 | }
87 | catch( Exception e )
88 | {
89 | Log.e( "Unity", "Exception:", e );
90 | }
91 | finally
92 | {
93 | if( videoCursor != null )
94 | videoCursor.close();
95 | }
96 |
97 | Intent intent = new Intent( MediaStore.ACTION_VIDEO_CAPTURE );
98 |
99 | // Setting a "EXTRA_OUTPUT" can stop the video from also appearing in the Gallery
100 | // but it is reported that while doing so, the camera app may stop functioning properly
101 | // on old devices or Nexus devices. Since we can delete any file from the Gallery on devices
102 | // that run on Android 9 or earlier, don't use EXTRA_OUTPUT on those devices for maximum compatibility
103 | // with older devices. Use EXTRA_OUTPUT on only Android 10 which restricts our access to the Gallery,
104 | // otherwise we can't delete the copies of the captured video on those devices
105 | if( provideExtraOutputOnAndroidQ && android.os.Build.VERSION.SDK_INT >= 29 && !Environment.isExternalStorageLegacy() )
106 | {
107 | File videoFile = new File( getActivity().getCacheDir(), VIDEO_NAME + ".mp4" );
108 | try
109 | {
110 | if( videoFile.exists() )
111 | NativeCameraUtils.ClearFileContents( videoFile );
112 | else
113 | videoFile.createNewFile();
114 | }
115 | catch( Exception e )
116 | {
117 | Log.e( "Unity", "Exception:", e );
118 | onActivityResult( CAMERA_VIDEO_CODE, Activity.RESULT_CANCELED, null );
119 | return;
120 | }
121 |
122 | fileTargetPath = videoFile.getAbsolutePath();
123 | NativeCameraUtils.SetOutputUri( getActivity(), intent, authority, videoFile );
124 | }
125 | else
126 | fileTargetPath = null;
127 |
128 | if( quality >= 0 )
129 | intent.putExtra( MediaStore.EXTRA_VIDEO_QUALITY, quality <= 1 ? quality : 1 );
130 | if( maxDuration > 0 )
131 | intent.putExtra( MediaStore.EXTRA_DURATION_LIMIT, maxDuration );
132 | if( maxSize > 0L )
133 | intent.putExtra( MediaStore.EXTRA_SIZE_LIMIT, maxSize );
134 |
135 | if( defaultCamera == 0 )
136 | NativeCameraUtils.SetDefaultCamera( intent, true );
137 | else if( defaultCamera == 1 )
138 | NativeCameraUtils.SetDefaultCamera( intent, false );
139 |
140 | if( NativeCamera.QuickCapture )
141 | intent.putExtra( "android.intent.extra.quickCapture", true );
142 |
143 | try
144 | {
145 | // MIUI devices have issues with Intent.createChooser on at least Android 11 (https://stackoverflow.com/questions/67785661/taking-and-picking-photos-on-poco-x3-with-android-11-does-not-work)
146 | if( NativeCamera.UseDefaultCameraApp || ( Build.VERSION.SDK_INT == 30 && NativeCameraUtils.IsXiaomiOrMIUI() ) )
147 | startActivityForResult( intent, CAMERA_VIDEO_CODE );
148 | else
149 | startActivityForResult( Intent.createChooser( intent, "" ), CAMERA_VIDEO_CODE );
150 | }
151 | catch( ActivityNotFoundException e )
152 | {
153 | Toast.makeText( getActivity(), "No apps can perform this action.", Toast.LENGTH_LONG ).show();
154 | onActivityResult( CAMERA_VIDEO_CODE, Activity.RESULT_CANCELED, null );
155 | }
156 | }
157 | }
158 |
159 | @Override
160 | public void onActivityResult( int requestCode, int resultCode, Intent data )
161 | {
162 | if( requestCode != CAMERA_VIDEO_CODE )
163 | return;
164 |
165 | File result = null;
166 | if( resultCode == Activity.RESULT_OK )
167 | {
168 | if( data != null )
169 | {
170 | String path = NativeCameraUtils.GetPathFromURIOrCopyFile( getActivity(), data.getData(), fileTargetPath );
171 | if( path != null && path.length() > 0 )
172 | result = new File( path );
173 | }
174 |
175 | if( ( result == null || !result.exists() || result.length() == 0 ) && fileTargetPath != null && fileTargetPath.length() > 0 )
176 | result = new File( fileTargetPath );
177 |
178 | if( lastVideoId != 0L ) // it got reset somehow?
179 | {
180 | // Credit: https://stackoverflow.com/a/8555925/2373034
181 | // Check if the video is saved to the Gallery
182 | Cursor videoCursor = null;
183 | try
184 | {
185 | final String[] videoColumns = { MediaStore.Video.Media.DATA, MediaStore.Video.Media.SIZE, MediaStore.Video.Media._ID };
186 | videoCursor = getActivity().getContentResolver().query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, videoColumns,
187 | MediaStore.Video.Media._ID + ">?", new String[] { "" + lastVideoId }, MediaStore.Video.Media._ID + " DESC" );
188 | while( videoCursor != null && videoCursor.moveToNext() )
189 | {
190 | String path = videoCursor.getString( videoCursor.getColumnIndex( MediaStore.Video.Media.DATA ) );
191 | if( path != null && path.length() > 0 )
192 | {
193 | long size = videoCursor.getLong( videoCursor.getColumnIndex( MediaStore.Video.Media.SIZE ) );
194 | if( result == null || !result.exists() || size == result.length() )
195 | {
196 | try
197 | {
198 | String id = "" + videoCursor.getInt( videoCursor.getColumnIndex( MediaStore.Video.Media._ID ) );
199 | String extension = "";
200 | int extensionIndex = path.lastIndexOf( '.' );
201 | if( extensionIndex > 0 && extensionIndex < path.length() - 1 && extensionIndex > path.lastIndexOf( File.separatorChar ) )
202 | extension = path.substring( extensionIndex ).toLowerCase( Locale.US );
203 |
204 | File copiedFile = new File( getActivity().getCacheDir(), VIDEO_NAME + extension );
205 | Uri contentUri;
206 | try
207 | {
208 | contentUri = Uri.withAppendedPath( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id );
209 | }
210 | catch( Exception e )
211 | {
212 | Log.e( "Unity", "Exception:", e );
213 | contentUri = null;
214 | }
215 |
216 | NativeCameraUtils.CopyFile( getActivity(), new File( path ), copiedFile, contentUri );
217 |
218 | if( copiedFile.length() > 1L )
219 | {
220 | result = copiedFile;
221 |
222 | if( !NativeCamera.KeepGalleryReferences )
223 | {
224 | Log.d( "Unity", "Trying to delete duplicate gallery item: " + path );
225 |
226 | getActivity().getContentResolver().delete( MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
227 | MediaStore.Video.Media._ID + "=?", new String[] { id } );
228 | }
229 | }
230 | }
231 | catch( Exception e )
232 | {
233 | Log.e( "Unity", "Exception:", e );
234 | }
235 |
236 | break;
237 | }
238 | }
239 | }
240 | }
241 | catch( Exception e )
242 | {
243 | Log.e( "Unity", "Exception:", e );
244 | }
245 | finally
246 | {
247 | if( videoCursor != null )
248 | videoCursor.close();
249 | }
250 | }
251 | }
252 |
253 | Log.d( "Unity", "NativeCameraVideoFragment.onActivityResult: " + ( ( result == null ) ? "null" : ( ( result.exists() ? result.length() : -1 ) + " " + result.getAbsolutePath() ) ) );
254 | if( mediaReceiver != null )
255 | mediaReceiver.OnMediaReceived( ( result != null && result.exists() && result.length() > 1L ) ? result.getAbsolutePath() : "" );
256 | else
257 | Log.e( "Unity", "NativeCameraVideoFragment.mediaReceiver became null in onActivityResult!" );
258 |
259 | getFragmentManager().beginTransaction().remove( this ).commitAllowingStateLoss();
260 | }
261 | }
--------------------------------------------------------------------------------
/.github/AAR Source (Android)/proguard.txt:
--------------------------------------------------------------------------------
1 | -keep class com.yasirkula.unity.* { *; }
--------------------------------------------------------------------------------
/.github/README.md:
--------------------------------------------------------------------------------
1 | # Unity Native Camera Plugin
2 |
3 | **Available on Asset Store:** https://assetstore.unity.com/packages/tools/integration/native-camera-for-android-ios-117802
4 |
5 | **Forum Thread:** https://forum.unity.com/threads/native-camera-for-android-ios-open-source.529560/
6 |
7 | **Discord:** https://discord.gg/UJJt549AaV
8 |
9 | **[GitHub Sponsors ☕](https://github.com/sponsors/yasirkula)**
10 |
11 | This plugin helps you take pictures/record videos natively with your device's camera on Android & iOS (other platforms aren't supported). It has built-in support for runtime permissions, as well.
12 |
13 | ## INSTALLATION
14 |
15 | There are 5 ways to install this plugin:
16 |
17 | - import [NativeCamera.unitypackage](https://github.com/yasirkula/UnityNativeCamera/releases) via *Assets-Import Package*
18 | - clone/[download](https://github.com/yasirkula/UnityNativeCamera/archive/master.zip) this repository and move the *Plugins* folder to your Unity project's *Assets* folder
19 | - import it from [Asset Store](https://assetstore.unity.com/packages/tools/integration/native-camera-for-android-ios-117802)
20 | - *(via Package Manager)* click the + button and install the package from the following git URL:
21 | - `https://github.com/yasirkula/UnityNativeCamera.git`
22 | - *(via [OpenUPM](https://openupm.com))* after installing [openupm-cli](https://github.com/openupm/openupm-cli), run the following command:
23 | - `openupm add com.yasirkula.nativecamera`
24 |
25 | ### Android Setup
26 |
27 | NativeCamera no longer requires any manual setup on Android. For reference, the legacy manual setup documentation is available at: https://github.com/yasirkula/UnityNativeCamera/wiki/Manual-Setup-for-Android
28 |
29 | ### iOS Setup
30 |
31 | There are two ways to set up the plugin on iOS:
32 |
33 | - **a. Automated Setup:** *(optional)* change the values of **Camera Usage Description** and **Microphone Usage Description** at *Project Settings/yasirkula/Native Camera*
34 | - **b. Manual Setup:** see: https://github.com/yasirkula/UnityNativeCamera/wiki/Manual-Setup-for-iOS
35 |
36 | ## FAQ
37 |
38 | - **Audio is muted on iOS after calling NativeCamera.RecordVideo**
39 |
40 | Please see: https://forum.unity.com/threads/native-camera-for-android-ios-open-source.529560/page-9#post-8207157
41 |
42 | - **Plugin doesn't work in a Windows/Mac/Linux build**
43 |
44 | Only Android & iOS platforms are supported. Editor functionality is for preview purposes only and uses Unity's [Editor-only API](https://docs.unity3d.com/ScriptReference/EditorUtility.OpenFilePanelWithFilters.html).
45 |
46 | - **Can't use the camera, it says "java.lang.ClassNotFoundException: com.yasirkula.unity.NativeCamera" in Logcat**
47 |
48 | If you are sure that your plugin is up-to-date, then enable **Custom Proguard File** option from *Player Settings* and add the following line to that file: `-keep class com.yasirkula.unity.* { *; }`
49 |
50 | - **NativeCamera functions return Permission.Denied even though I've granted the permission"**
51 |
52 | Declare `WRITE_EXTERNAL_STORAGE` permission manually in your **Plugins/Android/AndroidManifest.xml** with the `tools:node="replace"` attribute as follows: ``.
53 |
54 | ## HOW TO
55 |
56 | ### A. Accessing Camera
57 |
58 | `NativeCamera.TakePicture( CameraCallback callback, int maxSize = -1, PreferredCamera preferredCamera = PreferredCamera.Default )`: opens the camera and waits for user to take a picture.
59 | - This operation is **asynchronous**! After user takes a picture or cancels the operation, the **callback** is called (on main thread). **CameraCallback** takes a *string* parameter which stores the path of the captured image, or *null* if the operation is canceled
60 | - **maxSize** determines the maximum size of the returned image in pixels on iOS. A larger image will be down-scaled for better performance. If untouched, its value will be set to *SystemInfo.maxTextureSize*. Has no effect on Android
61 | - **saveAsJPEG** determines whether the image is saved as JPEG or PNG. Has no effect on Android
62 | - **preferredCamera** determines whether the rear camera or the front camera should be opened by default. Please note that the functionality of this parameter depends on whether the device vendor has added this capability to the camera or not. So, this parameter may not have any effect on some devices (see https://github.com/yasirkula/UnityNativeCamera/issues/126)
63 |
64 | `NativeCamera.RecordVideo( CameraCallback callback, Quality quality = Quality.Default, int maxDuration = 0, long maxSizeBytes = 0L, PreferredCamera preferredCamera = PreferredCamera.Default )`: opens the camera and waits for user to record a video.
65 | - **quality** determines the quality of the recorded video. Available values are: *Default*, *Low*, *Medium*, *High*
66 | - **maxDuration** determines the maximum duration, in seconds, for the recorded video. If untouched, there will be no limit. Please note that the functionality of this parameter depends on whether the device vendor has added this capability to the camera or not. So, this parameter may not have any effect on some devices
67 | - **maxSizeBytes** determines the maximum size, in bytes, for the recorded video. If untouched, there will be no limit. This parameter has no effect on iOS. Please note that the functionality of this parameter depends on whether the device vendor has added this capability to the camera or not. So, this parameter may not have any effect on some devices
68 |
69 | `NativeCamera.DeviceHasCamera()`: returns false if the device doesn't have a camera. In this case, TakePicture and RecordVideo functions will not execute.
70 |
71 | `NativeCamera.IsCameraBusy()`: returns true if the camera is currently open. In that case, another TakePicture or RecordVideo request will simply be ignored.
72 |
73 | Note that TakePicture and RecordVideo functions automatically call *NativeCamera.RequestPermissionAsync*. More details available below.
74 |
75 | ### B. Runtime Permissions
76 |
77 | Beginning with *6.0 Marshmallow*, Android apps must request runtime permissions before accessing certain services, similar to iOS. There are two functions to handle permissions with this plugin:
78 |
79 | `bool NativeCamera.CheckPermission( bool isPicturePermission )`: checks whether the app has access to camera or not.
80 | - **isPicturePermission** determines whether we're checking permission to take a picture or record a video. Has no effect on iOS
81 |
82 | `void NativeCamera.RequestPermissionAsync( PermissionCallback callback, bool isPicturePermission )`: requests permission to access the camera from the user and returns the result asynchronously. It is recommended to show a brief explanation before asking the permission so that user understands why the permission is needed and doesn't click Deny or worse, "Don't ask again". Note that TakePicture and RecordVideo functions call RequestPermissionAsync internally and execute only if the permission is granted.
83 | - **PermissionCallback** takes `NativeCamera.Permission permission` parameter
84 |
85 | **NativeCamera.Permission** is an enum that can take 3 values:
86 | - **Granted**: we have the permission to access the camera
87 | - **ShouldAsk**: permission is denied but we can ask the user for permission once again. On Android, as long as the user doesn't select "Don't ask again" while denying the permission, ShouldAsk is returned
88 | - **Denied**: we don't have permission and we can't ask the user for permission. In this case, user has to give the permission from Settings. This happens when user denies the permission on iOS (can't request permission again on iOS), when user selects "Don't ask again" while denying the permission on Android or when user is not allowed to give that permission (parental controls etc.)
89 |
90 | `Task NativeCamera.RequestPermissionAsync( bool isPicturePermission )`: Task-based overload of *RequestPermissionAsync*.
91 |
92 | `NativeCamera.OpenSettings()`: opens the settings for this app, from where the user can manually grant permission in case current permission state is *Permission.Denied* (Android requires *Storage* and, if declared in AndroidManifest, *Camera* permissions; iOS requires *Camera* permission).
93 |
94 | ### C. Utility Functions
95 |
96 | `NativeCamera.ImageProperties NativeCamera.GetImageProperties( string imagePath )`: returns an *ImageProperties* instance that holds the width, height, mime type and EXIF orientation information of an image file without creating a *Texture2D* object. Mime type will be *null*, if it can't be determined.
97 |
98 | `NativeCamera.VideoProperties NativeCamera.GetVideoProperties( string videoPath )`: returns a *VideoProperties* instance that holds the width, height, duration (in milliseconds) and rotation information of a video file. To play a video in correct orientation, you should rotate it by *rotation* degrees clockwise. For a 90-degree or 270-degree rotated video, values of *width* and *height* should be swapped to get the display size of the video.
99 |
100 | `Texture2D NativeCamera.LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )`: creates a Texture2D from the specified image file in correct orientation and returns it. Returns *null*, if something goes wrong.
101 | - **maxSize** determines the maximum size of the returned Texture2D in pixels. Larger textures will be down-scaled. If untouched, its value will be set to *SystemInfo.maxTextureSize*. It is recommended to set a proper maxSize for better performance
102 | - **markTextureNonReadable** marks the generated texture as non-readable for better memory usage. If you plan to modify the texture later (e.g. *GetPixels*/*SetPixels*), set its value to *false*
103 | - **generateMipmaps** determines whether texture should have mipmaps or not
104 | - **linearColorSpace** determines whether texture should be in linear color space or sRGB color space
105 |
106 | `async Task NativeCamera.LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true )`: asynchronous variant of *LoadImageAtPath*. Whether or not the returned Texture2D has mipmaps enabled depends on *UnityWebRequestTexture*'s implementation on the target Unity version. Note that it isn't possible to load multiple images simultaneously using this function.
107 |
108 | `Texture2D NativeCamera.GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )`: creates a Texture2D thumbnail from a video file and returns it. Returns *null*, if something goes wrong.
109 | - **maxSize** determines the maximum size of the returned Texture2D in pixels. Larger thumbnails will be down-scaled. If untouched, its value will be set to *SystemInfo.maxTextureSize*. It is recommended to set a proper maxSize for better performance
110 | - **captureTimeInSeconds** determines the frame of the video that the thumbnail is captured from. If untouched, OS will decide this value
111 | - **markTextureNonReadable** (see *LoadImageAtPath*)
112 |
113 | `async Task NativeCamera.GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true )`: asynchronous variant of *GetVideoThumbnail*. Whether or not the returned Texture2D has mipmaps enabled depends on *UnityWebRequestTexture*'s implementation on the target Unity version. Note that it isn't possible to generate multiple video thumbnails simultaneously using this function.
114 |
115 | ## EXAMPLE CODE
116 |
117 | The following code has two functions:
118 |
119 | - if you click left half of the screen, the camera is opened and after a picture is taken, it is displayed on a temporary quad that is placed in front of the camera
120 | - if you click right half of the screen, the camera is opened and after a video is recorded, it is played using the *Handheld.PlayFullScreenMovie* function
121 |
122 | ```csharp
123 | void Update()
124 | {
125 | if( Input.GetMouseButtonDown( 0 ) )
126 | {
127 | // Don't attempt to use the camera if it is already open
128 | if( NativeCamera.IsCameraBusy() )
129 | return;
130 |
131 | if( Input.mousePosition.x < Screen.width / 2 )
132 | {
133 | // Take a picture with the camera
134 | // If the captured image's width and/or height is greater than 512px, down-scale it
135 | TakePicture( 512 );
136 | }
137 | else
138 | {
139 | // Record a video with the camera
140 | RecordVideo();
141 | }
142 | }
143 | }
144 |
145 | private void TakePicture( int maxSize )
146 | {
147 | NativeCamera.TakePicture( ( path ) =>
148 | {
149 | Debug.Log( "Image path: " + path );
150 | if( path != null )
151 | {
152 | // Create a Texture2D from the captured image
153 | Texture2D texture = NativeCamera.LoadImageAtPath( path, maxSize );
154 | if( texture == null )
155 | {
156 | Debug.Log( "Couldn't load texture from " + path );
157 | return;
158 | }
159 |
160 | // Assign texture to a temporary quad and destroy it after 5 seconds
161 | GameObject quad = GameObject.CreatePrimitive( PrimitiveType.Quad );
162 | quad.transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2.5f;
163 | quad.transform.forward = Camera.main.transform.forward;
164 | quad.transform.localScale = new Vector3( 1f, texture.height / (float) texture.width, 1f );
165 |
166 | Material material = quad.GetComponent().material;
167 | if( !material.shader.isSupported ) // happens when Standard shader is not included in the build
168 | material.shader = Shader.Find( "Legacy Shaders/Diffuse" );
169 |
170 | material.mainTexture = texture;
171 |
172 | Destroy( quad, 5f );
173 |
174 | // If a procedural texture is not destroyed manually,
175 | // it will only be freed after a scene change
176 | Destroy( texture, 5f );
177 | }
178 | }, maxSize );
179 | }
180 |
181 | private void RecordVideo()
182 | {
183 | NativeCamera.RecordVideo( ( path ) =>
184 | {
185 | Debug.Log( "Video path: " + path );
186 | if( path != null )
187 | {
188 | // Play the recorded video
189 | Handheld.PlayFullScreenMovie( "file://" + path );
190 | }
191 | } );
192 | }
193 | ```
194 |
--------------------------------------------------------------------------------
/.github/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasirkula/UnityNativeCamera/72f5ebba26c219b4c4cda202bff2f59bfdaa47f8/.github/screenshots/1.png
--------------------------------------------------------------------------------
/.github/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasirkula/UnityNativeCamera/72f5ebba26c219b4c4cda202bff2f59bfdaa47f8/.github/screenshots/2.png
--------------------------------------------------------------------------------
/.github/screenshots/AndroidManifest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasirkula/UnityNativeCamera/72f5ebba26c219b4c4cda202bff2f59bfdaa47f8/.github/screenshots/AndroidManifest.png
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Süleyman Yasir KULA
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LICENSE.txt.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 5c24a0aae91808f4587811abccea7bed
3 | TextScriptImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Plugins.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 4b3220d76ab685149aefa67134758726
3 | folderAsset: yes
4 | DefaultImporter:
5 | externalObjects: {}
6 | userData:
7 | assetBundleName:
8 | assetBundleVariant:
9 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: e2f21cd7d33ee2b4a9aa49675b556cb4
3 | folderAsset: yes
4 | timeCreated: 1525098654
5 | licenseType: Free
6 | DefaultImporter:
7 | userData:
8 | assetBundleName:
9 | assetBundleVariant:
10 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 5fc918beb0f7cfe49a7f596d5b6b8768
3 | folderAsset: yes
4 | timeCreated: 1525098661
5 | licenseType: Free
6 | DefaultImporter:
7 | userData:
8 | assetBundleName:
9 | assetBundleVariant:
10 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android/NCCallbackHelper.cs:
--------------------------------------------------------------------------------
1 | #if UNITY_EDITOR || UNITY_ANDROID
2 | using System;
3 | using UnityEngine;
4 |
5 | namespace NativeCameraNamespace
6 | {
7 | public class NCCallbackHelper : MonoBehaviour
8 | {
9 | private bool autoDestroyWithCallback;
10 | private Action mainThreadAction = null;
11 |
12 | public static NCCallbackHelper Create( bool autoDestroyWithCallback )
13 | {
14 | NCCallbackHelper result = new GameObject( "NCCallbackHelper" ).AddComponent();
15 | result.autoDestroyWithCallback = autoDestroyWithCallback;
16 | DontDestroyOnLoad( result.gameObject );
17 | return result;
18 | }
19 |
20 | public void CallOnMainThread( Action function )
21 | {
22 | lock( this )
23 | {
24 | mainThreadAction += function;
25 | }
26 | }
27 |
28 | private void Update()
29 | {
30 | if( mainThreadAction != null )
31 | {
32 | try
33 | {
34 | Action temp;
35 | lock( this )
36 | {
37 | temp = mainThreadAction;
38 | mainThreadAction = null;
39 | }
40 |
41 | temp();
42 | }
43 | finally
44 | {
45 | if( autoDestroyWithCallback )
46 | Destroy( gameObject );
47 | }
48 | }
49 | }
50 | }
51 | }
52 | #endif
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android/NCCallbackHelper.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: b2bbe0051e738ea4585119c46d863f19
3 | timeCreated: 1545147258
4 | licenseType: Free
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs:
--------------------------------------------------------------------------------
1 | #if UNITY_EDITOR || UNITY_ANDROID
2 | using UnityEngine;
3 |
4 | namespace NativeCameraNamespace
5 | {
6 | public class NCCameraCallbackAndroid : AndroidJavaProxy
7 | {
8 | private readonly NativeCamera.CameraCallback callback;
9 | private readonly NCCallbackHelper callbackHelper;
10 |
11 | public NCCameraCallbackAndroid( NativeCamera.CameraCallback callback ) : base( "com.yasirkula.unity.NativeCameraMediaReceiver" )
12 | {
13 | this.callback = callback;
14 | callbackHelper = NCCallbackHelper.Create( true );
15 | }
16 |
17 | [UnityEngine.Scripting.Preserve]
18 | public void OnMediaReceived( string path )
19 | {
20 | callbackHelper.CallOnMainThread( () => callback( !string.IsNullOrEmpty( path ) ? path : null ) );
21 | }
22 | }
23 | }
24 | #endif
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 3cc8df584d2a4344b929a4f13a53723a
3 | timeCreated: 1519060539
4 | licenseType: Free
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs:
--------------------------------------------------------------------------------
1 | #if UNITY_EDITOR || UNITY_ANDROID
2 | using UnityEngine;
3 |
4 | namespace NativeCameraNamespace
5 | {
6 | public class NCPermissionCallbackAndroid : AndroidJavaProxy
7 | {
8 | private readonly NativeCamera.PermissionCallback callback;
9 | private readonly NCCallbackHelper callbackHelper;
10 |
11 | public NCPermissionCallbackAndroid( NativeCamera.PermissionCallback callback ) : base( "com.yasirkula.unity.NativeCameraPermissionReceiver" )
12 | {
13 | this.callback = callback;
14 | callbackHelper = NCCallbackHelper.Create( true );
15 | }
16 |
17 | [UnityEngine.Scripting.Preserve]
18 | public void OnPermissionResult( int result )
19 | {
20 | callbackHelper.CallOnMainThread( () => callback( (NativeCamera.Permission) result ) );
21 | }
22 | }
23 | }
24 | #endif
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: bafa24bbc8c455f44a2b98dcbe6451bd
3 | timeCreated: 1519060539
4 | licenseType: Free
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android/NativeCamera.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasirkula/UnityNativeCamera/72f5ebba26c219b4c4cda202bff2f59bfdaa47f8/Plugins/NativeCamera/Android/NativeCamera.aar
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Android/NativeCamera.aar.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 284037eba2526f54d9cf51b5d9bffcfa
3 | timeCreated: 1569764737
4 | licenseType: Free
5 | PluginImporter:
6 | serializedVersion: 2
7 | iconMap: {}
8 | executionOrder: {}
9 | isPreloaded: 0
10 | isOverridable: 0
11 | platformData:
12 | data:
13 | first:
14 | Android: Android
15 | second:
16 | enabled: 1
17 | settings: {}
18 | data:
19 | first:
20 | Any:
21 | second:
22 | enabled: 0
23 | settings: {}
24 | data:
25 | first:
26 | Editor: Editor
27 | second:
28 | enabled: 0
29 | settings:
30 | DefaultValueInitialized: true
31 | userData:
32 | assetBundleName:
33 | assetBundleVariant:
34 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Editor.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 16fe39fd709533a4eba946790a8e3123
3 | folderAsset: yes
4 | timeCreated: 1521452097
5 | licenseType: Free
6 | DefaultImporter:
7 | userData:
8 | assetBundleName:
9 | assetBundleVariant:
10 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Editor/NCPostProcessBuild.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using UnityEngine;
3 | using UnityEditor;
4 | #if UNITY_IOS
5 | using UnityEditor.Callbacks;
6 | using UnityEditor.iOS.Xcode;
7 | #endif
8 |
9 | namespace NativeCameraNamespace
10 | {
11 | [System.Serializable]
12 | public class Settings
13 | {
14 | private const string SAVE_PATH = "ProjectSettings/NativeCamera.json";
15 |
16 | public bool AutomatedSetup = true;
17 | public string CameraUsageDescription = "The app requires access to the camera to take pictures or record videos with it.";
18 | public string MicrophoneUsageDescription = "The app will capture microphone input in the recorded video.";
19 |
20 | private static Settings m_instance = null;
21 | public static Settings Instance
22 | {
23 | get
24 | {
25 | if( m_instance == null )
26 | {
27 | try
28 | {
29 | if( File.Exists( SAVE_PATH ) )
30 | m_instance = JsonUtility.FromJson( File.ReadAllText( SAVE_PATH ) );
31 | else
32 | m_instance = new Settings();
33 | }
34 | catch( System.Exception e )
35 | {
36 | Debug.LogException( e );
37 | m_instance = new Settings();
38 | }
39 | }
40 |
41 | return m_instance;
42 | }
43 | }
44 |
45 | public void Save()
46 | {
47 | File.WriteAllText( SAVE_PATH, JsonUtility.ToJson( this, true ) );
48 | }
49 |
50 | [SettingsProvider]
51 | public static SettingsProvider CreatePreferencesGUI()
52 | {
53 | return new SettingsProvider( "Project/yasirkula/Native Camera", SettingsScope.Project )
54 | {
55 | guiHandler = ( searchContext ) => PreferencesGUI(),
56 | keywords = new System.Collections.Generic.HashSet() { "Native", "Camera", "Android", "iOS" }
57 | };
58 | }
59 |
60 | public static void PreferencesGUI()
61 | {
62 | EditorGUI.BeginChangeCheck();
63 |
64 | Instance.AutomatedSetup = EditorGUILayout.Toggle( "Automated Setup", Instance.AutomatedSetup );
65 |
66 | EditorGUI.BeginDisabledGroup( !Instance.AutomatedSetup );
67 | Instance.CameraUsageDescription = EditorGUILayout.DelayedTextField( "Camera Usage Description", Instance.CameraUsageDescription );
68 | Instance.MicrophoneUsageDescription = EditorGUILayout.DelayedTextField( "Microphone Usage Description", Instance.MicrophoneUsageDescription );
69 | EditorGUI.EndDisabledGroup();
70 |
71 | if( EditorGUI.EndChangeCheck() )
72 | Instance.Save();
73 | }
74 | }
75 |
76 | public class NCPostProcessBuild
77 | {
78 | #if UNITY_IOS
79 | [PostProcessBuild]
80 | public static void OnPostprocessBuild( BuildTarget target, string buildPath )
81 | {
82 | if( !Settings.Instance.AutomatedSetup )
83 | return;
84 |
85 | if( target == BuildTarget.iOS )
86 | {
87 | string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath );
88 | string plistPath = Path.Combine( buildPath, "Info.plist" );
89 |
90 | PBXProject pbxProject = new PBXProject();
91 | pbxProject.ReadFromFile( pbxProjectPath );
92 |
93 | string targetGUID = pbxProject.GetUnityFrameworkTargetGuid();
94 | pbxProject.AddFrameworkToProject( targetGUID, "MobileCoreServices.framework", false );
95 | pbxProject.AddFrameworkToProject( targetGUID, "ImageIO.framework", false );
96 |
97 | File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() );
98 |
99 | PlistDocument plist = new PlistDocument();
100 | plist.ReadFromString( File.ReadAllText( plistPath ) );
101 |
102 | PlistElementDict rootDict = plist.root;
103 | if( !string.IsNullOrEmpty( Settings.Instance.CameraUsageDescription ) )
104 | rootDict.SetString( "NSCameraUsageDescription", Settings.Instance.CameraUsageDescription );
105 | if( !string.IsNullOrEmpty( Settings.Instance.MicrophoneUsageDescription ) )
106 | rootDict.SetString( "NSMicrophoneUsageDescription", Settings.Instance.MicrophoneUsageDescription );
107 |
108 | File.WriteAllText( plistPath, plist.WriteToString() );
109 | }
110 | }
111 | #endif
112 | }
113 | }
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Editor/NCPostProcessBuild.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: fa3b57e342928704cb910789ae4dde20
3 | timeCreated: 1521452119
4 | licenseType: Free
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Editor/NativeCamera.Editor.asmdef:
--------------------------------------------------------------------------------
1 | {
2 | "name": "NativeCamera.Editor",
3 | "references": [],
4 | "includePlatforms": [
5 | "Editor"
6 | ],
7 | "excludePlatforms": [],
8 | "allowUnsafeCode": false,
9 | "overrideReferences": false,
10 | "precompiledReferences": [],
11 | "autoReferenced": true,
12 | "defineConstraints": [],
13 | "versionDefines": [],
14 | "noEngineReferences": false
15 | }
--------------------------------------------------------------------------------
/Plugins/NativeCamera/Editor/NativeCamera.Editor.asmdef.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 31117d0234af0084b91a7e53b3d9e0a3
3 | AssemblyDefinitionImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/NativeCamera.Runtime.asmdef:
--------------------------------------------------------------------------------
1 | {
2 | "name": "NativeCamera.Runtime"
3 | }
4 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/NativeCamera.Runtime.asmdef.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: b107fd1956cb3e04985108f5ee29e115
3 | AssemblyDefinitionImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/NativeCamera.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.IO;
4 | using UnityEngine;
5 | using System.Threading.Tasks;
6 | using UnityEngine.Networking;
7 | #if UNITY_ANDROID || UNITY_IOS
8 | using NativeCameraNamespace;
9 | #endif
10 | using Object = UnityEngine.Object;
11 |
12 | public static class NativeCamera
13 | {
14 | public struct ImageProperties
15 | {
16 | public readonly int width;
17 | public readonly int height;
18 | public readonly string mimeType;
19 | public readonly ImageOrientation orientation;
20 |
21 | public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation )
22 | {
23 | this.width = width;
24 | this.height = height;
25 | this.mimeType = mimeType;
26 | this.orientation = orientation;
27 | }
28 | }
29 |
30 | public struct VideoProperties
31 | {
32 | public readonly int width;
33 | public readonly int height;
34 | public readonly long duration;
35 | public readonly float rotation;
36 |
37 | public VideoProperties( int width, int height, long duration, float rotation )
38 | {
39 | this.width = width;
40 | this.height = height;
41 | this.duration = duration;
42 | this.rotation = rotation;
43 | }
44 | }
45 |
46 | public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
47 | public enum Quality { Default = -1, Low = 0, Medium = 1, High = 2 };
48 | public enum PreferredCamera { Default = -1, Rear = 0, Front = 1 }
49 |
50 | // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered)
51 | public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 };
52 |
53 | public delegate void PermissionCallback( Permission permission );
54 | public delegate void CameraCallback( string path );
55 |
56 | #region Platform Specific Elements
57 | #if !UNITY_EDITOR && UNITY_ANDROID
58 | private static AndroidJavaClass m_ajc = null;
59 | private static AndroidJavaClass AJC
60 | {
61 | get
62 | {
63 | if( m_ajc == null )
64 | m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeCamera" );
65 |
66 | return m_ajc;
67 | }
68 | }
69 |
70 | private static AndroidJavaObject m_context = null;
71 | private static AndroidJavaObject Context
72 | {
73 | get
74 | {
75 | if( m_context == null )
76 | {
77 | using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
78 | {
79 | m_context = unityClass.GetStatic( "currentActivity" );
80 | }
81 | }
82 |
83 | return m_context;
84 | }
85 | }
86 | #elif !UNITY_EDITOR && UNITY_IOS
87 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
88 | private static extern int _NativeCamera_CheckPermission();
89 |
90 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
91 | private static extern void _NativeCamera_RequestPermission();
92 |
93 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
94 | private static extern void _NativeCamera_OpenSettings();
95 |
96 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
97 | private static extern int _NativeCamera_HasCamera();
98 |
99 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
100 | private static extern void _NativeCamera_TakePicture( string imageSavePath, int maxSize, int preferredCamera );
101 |
102 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
103 | private static extern void _NativeCamera_RecordVideo( int quality, int maxDuration, int preferredCamera );
104 |
105 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
106 | private static extern string _NativeCamera_GetImageProperties( string path );
107 |
108 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
109 | private static extern string _NativeCamera_GetVideoProperties( string path );
110 |
111 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
112 | private static extern string _NativeCamera_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds );
113 |
114 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
115 | private static extern string _NativeCamera_LoadImageAtPath( string path, string temporaryFilePath, int maxSize );
116 | #endif
117 |
118 | #if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )
119 | private static string m_temporaryImagePath = null;
120 | private static string TemporaryImagePath
121 | {
122 | get
123 | {
124 | if( m_temporaryImagePath == null )
125 | {
126 | m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" );
127 | Directory.CreateDirectory( Application.temporaryCachePath );
128 | }
129 |
130 | return m_temporaryImagePath;
131 | }
132 | }
133 | #endif
134 |
135 | #if !UNITY_EDITOR && UNITY_IOS
136 | private static string m_iOSSelectedImagePath = null;
137 | private static string IOSSelectedImagePath
138 | {
139 | get
140 | {
141 | if( m_iOSSelectedImagePath == null )
142 | {
143 | m_iOSSelectedImagePath = Path.Combine( Application.temporaryCachePath, "CameraImg" );
144 | Directory.CreateDirectory( Application.temporaryCachePath );
145 | }
146 |
147 | return m_iOSSelectedImagePath;
148 | }
149 | }
150 | #endif
151 | #endregion
152 |
153 | #region Runtime Permissions
154 | public static bool CheckPermission( bool isPicturePermission )
155 | {
156 | #if !UNITY_EDITOR && UNITY_ANDROID
157 | return AJC.CallStatic( "CheckPermission", Context, isPicturePermission ) == 1;
158 | #elif !UNITY_EDITOR && UNITY_IOS
159 | return _NativeCamera_CheckPermission() == 1;
160 | #else
161 | return true;
162 | #endif
163 | }
164 |
165 | public static void RequestPermissionAsync( PermissionCallback callback, bool isPicturePermission )
166 | {
167 | #if !UNITY_EDITOR && UNITY_ANDROID
168 | NCPermissionCallbackAndroid nativeCallback = new( callback );
169 | AJC.CallStatic( "RequestPermission", Context, nativeCallback, isPicturePermission );
170 | #elif !UNITY_EDITOR && UNITY_IOS
171 | NCPermissionCallbackiOS.Initialize( callback );
172 | _NativeCamera_RequestPermission();
173 | #else
174 | callback( Permission.Granted );
175 | #endif
176 | }
177 |
178 | public static Task RequestPermissionAsync( bool isPicturePermission )
179 | {
180 | TaskCompletionSource tcs = new TaskCompletionSource();
181 | RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), isPicturePermission );
182 | return tcs.Task;
183 | }
184 |
185 | public static void OpenSettings()
186 | {
187 | #if !UNITY_EDITOR && UNITY_ANDROID
188 | AJC.CallStatic( "OpenSettings", Context );
189 | #elif !UNITY_EDITOR && UNITY_IOS
190 | _NativeCamera_OpenSettings();
191 | #endif
192 | }
193 | #endregion
194 |
195 | #region Camera Functions
196 | public static void TakePicture( CameraCallback callback, int maxSize = -1, bool saveAsJPEG = true, PreferredCamera preferredCamera = PreferredCamera.Default )
197 | {
198 | RequestPermissionAsync( ( permission ) =>
199 | {
200 | if( permission != Permission.Granted || IsCameraBusy() )
201 | {
202 | callback?.Invoke( null );
203 | return;
204 | }
205 |
206 | #if UNITY_EDITOR
207 | string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select image", "", new string[] { "Image files", "png,jpg,jpeg", "All files", "*" } );
208 |
209 | if( callback != null )
210 | callback( pickedFile != "" ? pickedFile : null );
211 | #elif UNITY_ANDROID
212 | AJC.CallStatic( "TakePicture", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera );
213 | #elif UNITY_IOS
214 | if( maxSize <= 0 )
215 | maxSize = SystemInfo.maxTextureSize;
216 |
217 | NCCameraCallbackiOS.Initialize( callback );
218 | _NativeCamera_TakePicture( IOSSelectedImagePath + ( saveAsJPEG ? ".jpeg" : ".png" ), maxSize, (int) preferredCamera );
219 | #else
220 | if( callback != null )
221 | callback( null );
222 | #endif
223 | }, true );
224 | }
225 |
226 | public static void RecordVideo( CameraCallback callback, Quality quality = Quality.Default, int maxDuration = 0, long maxSizeBytes = 0L, PreferredCamera preferredCamera = PreferredCamera.Default )
227 | {
228 | RequestPermissionAsync( ( permission ) =>
229 | {
230 | if( permission != Permission.Granted || IsCameraBusy() )
231 | {
232 | callback?.Invoke( null );
233 | return;
234 | }
235 |
236 | #if UNITY_EDITOR
237 | string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select video", "", new string[] { "Video files", "mp4,mov,webm,avi", "All files", "*" } );
238 |
239 | if( callback != null )
240 | callback( pickedFile != "" ? pickedFile : null );
241 | #elif UNITY_ANDROID
242 | AJC.CallStatic( "RecordVideo", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera, (int) quality, maxDuration, maxSizeBytes );
243 | #elif UNITY_IOS
244 | NCCameraCallbackiOS.Initialize( callback );
245 | _NativeCamera_RecordVideo( (int) quality, maxDuration, (int) preferredCamera );
246 | #else
247 | if( callback != null )
248 | callback( null );
249 | #endif
250 | }, false );
251 | }
252 |
253 | public static bool DeviceHasCamera()
254 | {
255 | #if !UNITY_EDITOR && UNITY_ANDROID
256 | return AJC.CallStatic( "HasCamera", Context );
257 | #elif !UNITY_EDITOR && UNITY_IOS
258 | return _NativeCamera_HasCamera() == 1;
259 | #else
260 | return true;
261 | #endif
262 | }
263 |
264 | public static bool IsCameraBusy()
265 | {
266 | #if !UNITY_EDITOR && UNITY_IOS
267 | return NCCameraCallbackiOS.IsBusy;
268 | #else
269 | return false;
270 | #endif
271 | }
272 | #endregion
273 |
274 | #region Utility Functions
275 | public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
276 | {
277 | if( string.IsNullOrEmpty( imagePath ) )
278 | throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
279 |
280 | if( !File.Exists( imagePath ) )
281 | throw new FileNotFoundException( "File not found at " + imagePath );
282 |
283 | if( maxSize <= 0 )
284 | maxSize = SystemInfo.maxTextureSize;
285 |
286 | #if !UNITY_EDITOR && UNITY_ANDROID
287 | string loadPath = AJC.CallStatic( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize );
288 | #elif !UNITY_EDITOR && UNITY_IOS
289 | string loadPath = _NativeCamera_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize );
290 | #else
291 | string loadPath = imagePath;
292 | #endif
293 |
294 | string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
295 | TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
296 |
297 | Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace );
298 |
299 | try
300 | {
301 | if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
302 | {
303 | Debug.LogWarning( "Couldn't load image at path: " + loadPath );
304 |
305 | Object.DestroyImmediate( result );
306 | return null;
307 | }
308 | }
309 | catch( Exception e )
310 | {
311 | Debug.LogException( e );
312 |
313 | Object.DestroyImmediate( result );
314 | return null;
315 | }
316 | finally
317 | {
318 | if( loadPath != imagePath )
319 | {
320 | try
321 | {
322 | File.Delete( loadPath );
323 | }
324 | catch { }
325 | }
326 | }
327 |
328 | return result;
329 | }
330 |
331 | public static async Task LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true )
332 | {
333 | if( string.IsNullOrEmpty( imagePath ) )
334 | throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
335 |
336 | if( !File.Exists( imagePath ) )
337 | throw new FileNotFoundException( "File not found at " + imagePath );
338 |
339 | if( maxSize <= 0 )
340 | maxSize = SystemInfo.maxTextureSize;
341 |
342 | #if !UNITY_EDITOR && UNITY_ANDROID
343 | string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
344 | string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) );
345 | #elif !UNITY_EDITOR && UNITY_IOS
346 | string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
347 | string loadPath = await Task.Run( () => _NativeCamera_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) );
348 | #else
349 | string loadPath = imagePath;
350 | #endif
351 |
352 | Texture2D result = null;
353 |
354 | using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable ) )
355 | {
356 | UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest();
357 | while( !asyncOperation.isDone )
358 | await Task.Yield();
359 |
360 | if( www.result != UnityWebRequest.Result.Success )
361 | Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error );
362 | else
363 | result = DownloadHandlerTexture.GetContent( www );
364 | }
365 |
366 | if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong
367 | {
368 | string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
369 | TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
370 |
371 | result = new Texture2D( 2, 2, format, true, false );
372 |
373 | try
374 | {
375 | if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
376 | {
377 | Debug.LogWarning( "Couldn't load image at path: " + loadPath );
378 |
379 | Object.DestroyImmediate( result );
380 | return null;
381 | }
382 | }
383 | catch( Exception e )
384 | {
385 | Debug.LogException( e );
386 |
387 | Object.DestroyImmediate( result );
388 | return null;
389 | }
390 | finally
391 | {
392 | if( loadPath != imagePath )
393 | {
394 | try
395 | {
396 | File.Delete( loadPath );
397 | }
398 | catch { }
399 | }
400 | }
401 | }
402 |
403 | return result;
404 | }
405 |
406 | public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
407 | {
408 | if( maxSize <= 0 )
409 | maxSize = SystemInfo.maxTextureSize;
410 |
411 | #if !UNITY_EDITOR && UNITY_ANDROID
412 | string thumbnailPath = AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds );
413 | #elif !UNITY_EDITOR && UNITY_IOS
414 | string thumbnailPath = _NativeCamera_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds );
415 | #else
416 | string thumbnailPath = null;
417 | #endif
418 |
419 | if( !string.IsNullOrEmpty( thumbnailPath ) )
420 | return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace );
421 | else
422 | return null;
423 | }
424 |
425 | public static async Task GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true )
426 | {
427 | if( maxSize <= 0 )
428 | maxSize = SystemInfo.maxTextureSize;
429 |
430 | #if !UNITY_EDITOR && UNITY_ANDROID
431 | string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
432 | string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) );
433 | #elif !UNITY_EDITOR && UNITY_IOS
434 | string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
435 | string thumbnailPath = await Task.Run( () => _NativeCamera_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) );
436 | #else
437 | string thumbnailPath = null;
438 | #endif
439 |
440 | if( !string.IsNullOrEmpty( thumbnailPath ) )
441 | return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable );
442 | else
443 | return null;
444 | }
445 |
446 | #if UNITY_ANDROID
447 | private static async Task TryCallNativeAndroidFunctionOnSeparateThread( Func function )
448 | {
449 | T result = default( T );
450 | bool hasResult = false;
451 |
452 | await Task.Run( () =>
453 | {
454 | if( AndroidJNI.AttachCurrentThread() != 0 )
455 | Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" );
456 | else
457 | {
458 | try
459 | {
460 | result = function();
461 | hasResult = true;
462 | }
463 | finally
464 | {
465 | AndroidJNI.DetachCurrentThread();
466 | }
467 | }
468 | } );
469 |
470 | return hasResult ? result : function();
471 | }
472 | #endif
473 |
474 | public static ImageProperties GetImageProperties( string imagePath )
475 | {
476 | if( !File.Exists( imagePath ) )
477 | throw new FileNotFoundException( "File not found at " + imagePath );
478 |
479 | #if !UNITY_EDITOR && UNITY_ANDROID
480 | string value = AJC.CallStatic( "GetImageProperties", Context, imagePath );
481 | #elif !UNITY_EDITOR && UNITY_IOS
482 | string value = _NativeCamera_GetImageProperties( imagePath );
483 | #else
484 | string value = null;
485 | #endif
486 |
487 | int width = 0, height = 0;
488 | string mimeType = null;
489 | ImageOrientation orientation = ImageOrientation.Unknown;
490 | if( !string.IsNullOrEmpty( value ) )
491 | {
492 | string[] properties = value.Split( '>' );
493 | if( properties != null && properties.Length >= 4 )
494 | {
495 | if( !int.TryParse( properties[0].Trim(), out width ) )
496 | width = 0;
497 | if( !int.TryParse( properties[1].Trim(), out height ) )
498 | height = 0;
499 |
500 | mimeType = properties[2].Trim();
501 | if( mimeType.Length == 0 )
502 | {
503 | string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
504 | if( extension == ".png" )
505 | mimeType = "image/png";
506 | else if( extension == ".jpg" || extension == ".jpeg" )
507 | mimeType = "image/jpeg";
508 | else if( extension == ".gif" )
509 | mimeType = "image/gif";
510 | else if( extension == ".bmp" )
511 | mimeType = "image/bmp";
512 | else
513 | mimeType = null;
514 | }
515 |
516 | int orientationInt;
517 | if( int.TryParse( properties[3].Trim(), out orientationInt ) )
518 | orientation = (ImageOrientation) orientationInt;
519 | }
520 | }
521 |
522 | return new ImageProperties( width, height, mimeType, orientation );
523 | }
524 |
525 | public static VideoProperties GetVideoProperties( string videoPath )
526 | {
527 | if( !File.Exists( videoPath ) )
528 | throw new FileNotFoundException( "File not found at " + videoPath );
529 |
530 | #if !UNITY_EDITOR && UNITY_ANDROID
531 | string value = AJC.CallStatic( "GetVideoProperties", Context, videoPath );
532 | #elif !UNITY_EDITOR && UNITY_IOS
533 | string value = _NativeCamera_GetVideoProperties( videoPath );
534 | #else
535 | string value = null;
536 | #endif
537 |
538 | int width = 0, height = 0;
539 | long duration = 0L;
540 | float rotation = 0f;
541 | if( !string.IsNullOrEmpty( value ) )
542 | {
543 | string[] properties = value.Split( '>' );
544 | if( properties != null && properties.Length >= 4 )
545 | {
546 | if( !int.TryParse( properties[0].Trim(), out width ) )
547 | width = 0;
548 | if( !int.TryParse( properties[1].Trim(), out height ) )
549 | height = 0;
550 | if( !long.TryParse( properties[2].Trim(), out duration ) )
551 | duration = 0L;
552 | if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) )
553 | rotation = 0f;
554 | }
555 | }
556 |
557 | if( rotation == -90f )
558 | rotation = 270f;
559 |
560 | return new VideoProperties( width, height, duration, rotation );
561 | }
562 | #endregion
563 | }
--------------------------------------------------------------------------------
/Plugins/NativeCamera/NativeCamera.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: ff758a73b21d4a04aa6f95679b3da605
3 | timeCreated: 1498722610
4 | licenseType: Pro
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/README.txt:
--------------------------------------------------------------------------------
1 | = Native Camera for Android & iOS (v1.5.0) =
2 |
3 | Documentation: https://github.com/yasirkula/UnityNativeCamera
4 | FAQ: https://github.com/yasirkula/UnityNativeCamera#faq
5 | Example code: https://github.com/yasirkula/UnityNativeCamera#example-code
6 | E-mail: yasirkula@gmail.com
--------------------------------------------------------------------------------
/Plugins/NativeCamera/README.txt.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 5a88d1b1b9d7b904b862304c20ed4db4
3 | timeCreated: 1563308465
4 | licenseType: Free
5 | TextScriptImporter:
6 | userData:
7 | assetBundleName:
8 | assetBundleVariant:
9 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/iOS.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 5576acd2a06eb72409e6ec4b7f204a4e
3 | folderAsset: yes
4 | timeCreated: 1498722622
5 | licenseType: Pro
6 | DefaultImporter:
7 | userData:
8 | assetBundleName:
9 | assetBundleVariant:
10 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/iOS/NCCameraCallbackiOS.cs:
--------------------------------------------------------------------------------
1 | #if UNITY_EDITOR || UNITY_IOS
2 | using UnityEngine;
3 |
4 | namespace NativeCameraNamespace
5 | {
6 | public class NCCameraCallbackiOS : MonoBehaviour
7 | {
8 | private static NCCameraCallbackiOS instance;
9 | private NativeCamera.CameraCallback callback;
10 |
11 | private float nextBusyCheckTime;
12 |
13 | public static bool IsBusy { get; private set; }
14 |
15 | [System.Runtime.InteropServices.DllImport( "__Internal" )]
16 | private static extern int _NativeCamera_IsCameraBusy();
17 |
18 | public static void Initialize( NativeCamera.CameraCallback callback )
19 | {
20 | if( IsBusy )
21 | return;
22 |
23 | if( instance == null )
24 | {
25 | instance = new GameObject( "NCCameraCallbackiOS" ).AddComponent();
26 | DontDestroyOnLoad( instance.gameObject );
27 | }
28 |
29 | instance.callback = callback;
30 |
31 | instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
32 | IsBusy = true;
33 | }
34 |
35 | private void Update()
36 | {
37 | if( IsBusy )
38 | {
39 | if( Time.realtimeSinceStartup >= nextBusyCheckTime )
40 | {
41 | nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
42 |
43 | if( _NativeCamera_IsCameraBusy() == 0 )
44 | {
45 | IsBusy = false;
46 |
47 | NativeCamera.CameraCallback _callback = callback;
48 | callback = null;
49 |
50 | if( _callback != null )
51 | _callback( null );
52 | }
53 | }
54 | }
55 | }
56 |
57 | [UnityEngine.Scripting.Preserve]
58 | public void OnMediaReceived( string path )
59 | {
60 | IsBusy = false;
61 |
62 | if( string.IsNullOrEmpty( path ) )
63 | path = null;
64 |
65 | NativeCamera.CameraCallback _callback = callback;
66 | callback = null;
67 |
68 | if( _callback != null )
69 | _callback( path );
70 | }
71 | }
72 | }
73 | #endif
--------------------------------------------------------------------------------
/Plugins/NativeCamera/iOS/NCCameraCallbackiOS.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: d8f19d5713752dc41bd377562677d8ee
3 | timeCreated: 1519060539
4 | licenseType: Free
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs:
--------------------------------------------------------------------------------
1 | #if UNITY_EDITOR || UNITY_IOS
2 | using UnityEngine;
3 |
4 | namespace NativeCameraNamespace
5 | {
6 | public class NCPermissionCallbackiOS : MonoBehaviour
7 | {
8 | private static NCPermissionCallbackiOS instance;
9 | private NativeCamera.PermissionCallback callback;
10 |
11 | public static void Initialize( NativeCamera.PermissionCallback callback )
12 | {
13 | if( instance == null )
14 | {
15 | instance = new GameObject( "NCPermissionCallbackiOS" ).AddComponent();
16 | DontDestroyOnLoad( instance.gameObject );
17 | }
18 | else if( instance.callback != null )
19 | instance.callback( NativeCamera.Permission.ShouldAsk );
20 |
21 | instance.callback = callback;
22 | }
23 |
24 | [UnityEngine.Scripting.Preserve]
25 | public void OnPermissionRequested( string message )
26 | {
27 | NativeCamera.PermissionCallback _callback = callback;
28 | callback = null;
29 |
30 | if( _callback != null )
31 | _callback( (NativeCamera.Permission) int.Parse( message ) );
32 | }
33 | }
34 | }
35 | #endif
--------------------------------------------------------------------------------
/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 1efd0cf9fb7457142b76fb1bc8672e8d
3 | timeCreated: 1519060539
4 | licenseType: Free
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Plugins/NativeCamera/iOS/NativeCamera.mm:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import
4 | #import
5 | #import
6 |
7 | extern UIViewController* UnityGetGLViewController();
8 |
9 | @interface UNativeCamera:NSObject
10 | + (int)checkPermission;
11 | + (void)requestPermission;
12 | + (void)openSettings;
13 | + (int)hasCamera;
14 | + (void)openCamera:(BOOL)imageMode defaultCamera:(int)defaultCamera savePath:(NSString *)imageSavePath maxImageSize:(int)maxImageSize videoQuality:(int)videoQuality maxVideoDuration:(int)maxVideoDuration;
15 | + (int)isCameraBusy;
16 | + (char *)getImageProperties:(NSString *)path;
17 | + (char *)getVideoProperties:(NSString *)path;
18 | + (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime;
19 | + (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize;
20 | @end
21 |
22 | @implementation UNativeCamera
23 |
24 | static NSString *pickedMediaSavePath;
25 | static UIImagePickerController *imagePicker;
26 | static int cameraMaxImageSize = -1;
27 | static int imagePickerState = 0; // 0 -> none, 1 -> showing, 2 -> finished
28 | static BOOL recordingVideo = NO;
29 | static AVAudioSessionCategory unityAudioSessionCategory = AVAudioSessionCategoryAmbient;
30 | static NSUInteger unityAudioSessionCategoryOptions = 1;
31 | static AVAudioSessionMode unityAudioSessionMode = AVAudioSessionModeDefault;
32 |
33 | // Credit: https://stackoverflow.com/a/20464727/2373034
34 | + (int)checkPermission
35 | {
36 | AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
37 | if( status == AVAuthorizationStatusAuthorized )
38 | return 1;
39 | else if( status == AVAuthorizationStatusNotDetermined )
40 | return 2;
41 | else
42 | return 0;
43 | }
44 |
45 | // Credit: https://stackoverflow.com/a/20464727/2373034
46 | + (void)requestPermission
47 | {
48 | AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
49 | if( status == AVAuthorizationStatusAuthorized )
50 | UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", "1" );
51 | else if( status == AVAuthorizationStatusNotDetermined )
52 | {
53 | [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted )
54 | {
55 | UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", granted ? "1" : "0" );
56 | }];
57 | }
58 | else
59 | UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", "0" );
60 | }
61 |
62 | // Credit: https://stackoverflow.com/a/25453667/2373034
63 | + (void)openSettings
64 | {
65 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
66 | }
67 |
68 | + (int)hasCamera
69 | {
70 | return [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] ? 1 : 0;
71 | }
72 |
73 | // Credit: https://stackoverflow.com/a/10531752/2373034
74 | + (void)openCamera:(BOOL)imageMode defaultCamera:(int)defaultCamera savePath:(NSString *)imageSavePath maxImageSize:(int)maxImageSize videoQuality:(int)videoQuality maxVideoDuration:(int)maxVideoDuration
75 | {
76 | if( ![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] )
77 | {
78 | NSLog( @"Device has no registered cameras!" );
79 |
80 | UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" );
81 | return;
82 | }
83 |
84 | if( ( imageMode && ![[UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera] containsObject:(NSString*)kUTTypeImage] ) ||
85 | ( !imageMode && ![[UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera] containsObject:(NSString*)kUTTypeMovie] ) )
86 | {
87 | NSLog( @"Camera does not support this operation!" );
88 |
89 | UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" );
90 | return;
91 | }
92 |
93 | imagePicker = [[UIImagePickerController alloc] init];
94 | imagePicker.delegate = (id) self;
95 | imagePicker.allowsEditing = NO;
96 | imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
97 |
98 | if( imageMode )
99 | imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];
100 | else
101 | {
102 | imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeMovie];
103 |
104 | if( maxVideoDuration > 0 )
105 | imagePicker.videoMaximumDuration = maxVideoDuration;
106 |
107 | if( videoQuality == 0 )
108 | imagePicker.videoQuality = UIImagePickerControllerQualityTypeLow;
109 | else if( videoQuality == 1 )
110 | imagePicker.videoQuality = UIImagePickerControllerQualityTypeMedium;
111 | else if( videoQuality == 2 )
112 | imagePicker.videoQuality = UIImagePickerControllerQualityTypeHigh;
113 | }
114 |
115 | if( defaultCamera == 0 && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear] )
116 | imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
117 | else if( defaultCamera == 1 && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront] )
118 | imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceFront;
119 |
120 | // Bugfix for https://github.com/yasirkula/UnityNativeCamera/issues/45
121 | if( !imageMode )
122 | {
123 | unityAudioSessionCategory = [[AVAudioSession sharedInstance] category];
124 | unityAudioSessionCategoryOptions = [[AVAudioSession sharedInstance] categoryOptions];
125 | unityAudioSessionMode = [[AVAudioSession sharedInstance] mode];
126 | }
127 |
128 | recordingVideo = !imageMode;
129 | pickedMediaSavePath = imageSavePath;
130 | cameraMaxImageSize = maxImageSize;
131 |
132 | imagePickerState = 1;
133 | [UnityGetGLViewController() presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }];
134 | }
135 |
136 | + (int)isCameraBusy
137 | {
138 | if( imagePickerState == 2 )
139 | return 1;
140 |
141 | if( imagePicker != nil )
142 | {
143 | if( imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController() )
144 | return 1;
145 |
146 | imagePicker = nil;
147 | [self restoreAudioSession];
148 |
149 | return 0;
150 | }
151 |
152 | return 0;
153 | }
154 |
155 | + (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
156 | {
157 | NSString *path = nil;
158 | if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage] )
159 | {
160 | NSLog( @"UIImagePickerController finished taking picture" );
161 |
162 | UIImage *image = info[UIImagePickerControllerEditedImage] ?: info[UIImagePickerControllerOriginalImage];
163 | if( image == nil )
164 | path = nil;
165 | else
166 | {
167 | NSString *extension = [pickedMediaSavePath pathExtension];
168 | BOOL saveAsJPEG = [extension caseInsensitiveCompare:@"jpg"] == NSOrderedSame || [extension caseInsensitiveCompare:@"jpeg"] == NSOrderedSame;
169 |
170 | // Try to save the image with metadata
171 | // CANCELED: a number of users reported that this method results in 90-degree rotated images, uncomment at your own risk
172 | // Credit: https://stackoverflow.com/a/15858955
173 | /*NSDictionary *metadata = [info objectForKey:UIImagePickerControllerMediaMetadata];
174 | NSMutableDictionary *mutableMetadata = nil;
175 | CFDictionaryRef metadataRef;
176 | CFStringRef imageType;
177 |
178 | if( saveAsJPEG )
179 | {
180 | mutableMetadata = [metadata mutableCopy];
181 | [mutableMetadata setObject:@(1.0) forKey:(__bridge NSString *)kCGImageDestinationLossyCompressionQuality];
182 |
183 | metadataRef = (__bridge CFDictionaryRef) mutableMetadata;
184 | imageType = kUTTypeJPEG;
185 | }
186 | else
187 | {
188 | metadataRef = (__bridge CFDictionaryRef) metadata;
189 | imageType = kUTTypePNG;
190 | }
191 |
192 | CGImageDestinationRef imageDestination = CGImageDestinationCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:pickedMediaSavePath], imageType , 1, NULL );
193 | if( imageDestination == NULL )
194 | NSLog( @"Failed to create image destination" );
195 | else
196 | {
197 | CGImageDestinationAddImage( imageDestination, image.CGImage, metadataRef );
198 | if( CGImageDestinationFinalize( imageDestination ) )
199 | path = pickedMediaSavePath;
200 | else
201 | NSLog( @"Failed to finalize the image" );
202 |
203 | CFRelease( imageDestination );
204 | }*/
205 |
206 | if( path == nil )
207 | {
208 | //NSLog( @"Attempting to save the image without metadata as fallback" );
209 |
210 | if( ( saveAsJPEG && [UIImageJPEGRepresentation( [self scaleImage:image maxSize:cameraMaxImageSize], 1.0 ) writeToFile:pickedMediaSavePath atomically:YES] ) ||
211 | ( !saveAsJPEG && [UIImagePNGRepresentation( [self scaleImage:image maxSize:cameraMaxImageSize] ) writeToFile:pickedMediaSavePath atomically:YES] ) )
212 | path = pickedMediaSavePath;
213 | else
214 | {
215 | NSLog( @"Error saving image without metadata" );
216 | path = nil;
217 | }
218 | }
219 | }
220 | }
221 | else
222 | {
223 | NSLog( @"UIImagePickerController finished recording video" );
224 |
225 | #pragma clang diagnostic push
226 | #pragma clang diagnostic ignored "-Wdeprecated-declarations"
227 | NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL];
228 | #pragma clang diagnostic pop
229 | if( mediaUrl == nil )
230 | path = nil;
231 | else
232 | path = [mediaUrl path];
233 | }
234 |
235 | imagePicker = nil;
236 | imagePickerState = 2;
237 | UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", [self getCString:path] );
238 |
239 | [picker dismissViewControllerAnimated:NO completion:nil];
240 | [self restoreAudioSession];
241 | }
242 |
243 | + (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
244 | {
245 | NSLog( @"UIImagePickerController cancelled" );
246 |
247 | imagePicker = nil;
248 | UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" );
249 |
250 | [picker dismissViewControllerAnimated:NO completion:nil];
251 | [self restoreAudioSession];
252 | }
253 |
254 | // Bugfix for https://github.com/yasirkula/UnityNativeCamera/issues/45
255 | + (void)restoreAudioSession
256 | {
257 | if( recordingVideo )
258 | {
259 | NSError *error = nil;
260 | if( ![[AVAudioSession sharedInstance] setCategory:unityAudioSessionCategory mode:unityAudioSessionMode options:unityAudioSessionCategoryOptions error:&error] )
261 | {
262 | if( error != nil )
263 | NSLog( @"Error setting audio session category back to %@ with mode %@ and options %lu: %@", unityAudioSessionCategory, unityAudioSessionMode, (unsigned long) unityAudioSessionCategoryOptions, error );
264 | else
265 | NSLog( @"Error setting audio session category back to %@ with mode %@ and options %lu", unityAudioSessionCategory, unityAudioSessionMode, (unsigned long) unityAudioSessionCategoryOptions );
266 | }
267 | }
268 | }
269 |
270 | // Credit: https://stackoverflow.com/a/4170099/2373034
271 | + (NSArray *)getImageMetadata:(NSString *)path
272 | {
273 | int width = 0;
274 | int height = 0;
275 | int orientation = -1;
276 |
277 | CGImageSourceRef imageSource = CGImageSourceCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:path], nil );
278 | if( imageSource != nil )
279 | {
280 | NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:(__bridge NSString *)kCGImageSourceShouldCache];
281 | CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex( imageSource, 0, (__bridge CFDictionaryRef) options );
282 | CFRelease( imageSource );
283 |
284 | CGFloat widthF = 0.0f, heightF = 0.0f;
285 | if( imageProperties != nil )
286 | {
287 | if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelWidth ) )
288 | CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelWidth ), kCFNumberCGFloatType, &widthF );
289 |
290 | if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelHeight ) )
291 | CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelHeight ), kCFNumberCGFloatType, &heightF );
292 |
293 | if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyOrientation ) )
294 | {
295 | CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyOrientation ), kCFNumberIntType, &orientation );
296 |
297 | if( orientation > 4 )
298 | {
299 | // Landscape image
300 | CGFloat temp = widthF;
301 | widthF = heightF;
302 | heightF = temp;
303 | }
304 | }
305 |
306 | CFRelease( imageProperties );
307 | }
308 |
309 | width = (int) roundf( widthF );
310 | height = (int) roundf( heightF );
311 | }
312 |
313 | return [[NSArray alloc] initWithObjects:[NSNumber numberWithInt:width], [NSNumber numberWithInt:height], [NSNumber numberWithInt:orientation], nil];
314 | }
315 |
316 | + (char *)getImageProperties:(NSString *)path
317 | {
318 | NSArray *metadata = [self getImageMetadata:path];
319 |
320 | int orientationUnity;
321 | int orientation = [metadata[2] intValue];
322 |
323 | // To understand the magic numbers, see ImageOrientation enum in NativeCamera.cs
324 | // and http://sylvana.net/jpegcrop/exif_orientation.html
325 | if( orientation == 1 )
326 | orientationUnity = 0;
327 | else if( orientation == 2 )
328 | orientationUnity = 4;
329 | else if( orientation == 3 )
330 | orientationUnity = 2;
331 | else if( orientation == 4 )
332 | orientationUnity = 6;
333 | else if( orientation == 5 )
334 | orientationUnity = 5;
335 | else if( orientation == 6 )
336 | orientationUnity = 1;
337 | else if( orientation == 7 )
338 | orientationUnity = 7;
339 | else if( orientation == 8 )
340 | orientationUnity = 3;
341 | else
342 | orientationUnity = -1;
343 |
344 | return [self getCString:[NSString stringWithFormat:@"%d>%d> >%d", [metadata[0] intValue], [metadata[1] intValue], orientationUnity]];
345 | }
346 |
347 | + (char *)getVideoProperties:(NSString *)path
348 | {
349 | CGSize size = CGSizeZero;
350 | float rotation = 0;
351 | long long duration = 0;
352 |
353 | AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
354 | if( asset != nil )
355 | {
356 | duration = (long long) round( CMTimeGetSeconds( [asset duration] ) * 1000 );
357 | CGAffineTransform transform = [asset preferredTransform];
358 | NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
359 | if( videoTracks != nil && [videoTracks count] > 0 )
360 | {
361 | size = [[videoTracks objectAtIndex:0] naturalSize];
362 | transform = [[videoTracks objectAtIndex:0] preferredTransform];
363 | }
364 |
365 | rotation = atan2( transform.b, transform.a ) * ( 180.0 / M_PI );
366 | }
367 |
368 | return [self getCString:[NSString stringWithFormat:@"%d>%d>%lld>%f", (int) roundf( size.width ), (int) roundf( size.height ), duration, rotation]];
369 | }
370 |
371 | + (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime
372 | {
373 | AVAssetImageGenerator *thumbnailGenerator = [[AVAssetImageGenerator alloc] initWithAsset:[[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil]];
374 | thumbnailGenerator.appliesPreferredTrackTransform = YES;
375 | thumbnailGenerator.maximumSize = CGSizeMake( (CGFloat) maximumSize, (CGFloat) maximumSize );
376 | thumbnailGenerator.requestedTimeToleranceBefore = kCMTimeZero;
377 | thumbnailGenerator.requestedTimeToleranceAfter = kCMTimeZero;
378 |
379 | if( captureTime < 0.0 )
380 | captureTime = 0.0;
381 | else
382 | {
383 | AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
384 | if( asset != nil )
385 | {
386 | double videoDuration = CMTimeGetSeconds( [asset duration] );
387 | if( videoDuration > 0.0 && captureTime >= videoDuration - 0.1 )
388 | {
389 | if( captureTime > videoDuration )
390 | captureTime = videoDuration;
391 |
392 | thumbnailGenerator.requestedTimeToleranceBefore = CMTimeMakeWithSeconds( 1.0, 600 );
393 | }
394 | }
395 | }
396 |
397 | NSError *error = nil;
398 | CGImageRef image = [thumbnailGenerator copyCGImageAtTime:CMTimeMakeWithSeconds( captureTime, 600 ) actualTime:nil error:&error];
399 | if( image == nil )
400 | {
401 | if( error != nil )
402 | NSLog( @"Error generating video thumbnail: %@", error );
403 | else
404 | NSLog( @"Error generating video thumbnail..." );
405 |
406 | return [self getCString:@""];
407 | }
408 |
409 | UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image];
410 | CGImageRelease( image );
411 |
412 | if( ![UIImagePNGRepresentation( thumbnail ) writeToFile:savePath atomically:YES] )
413 | {
414 | NSLog( @"Error saving thumbnail image" );
415 | return [self getCString:@""];
416 | }
417 |
418 | return [self getCString:savePath];
419 | }
420 |
421 | + (UIImage *)scaleImage:(UIImage *)image maxSize:(int)maxSize
422 | {
423 | CGFloat width = image.size.width;
424 | CGFloat height = image.size.height;
425 |
426 | UIImageOrientation orientation = image.imageOrientation;
427 | if( width <= maxSize && height <= maxSize && orientation != UIImageOrientationDown &&
428 | orientation != UIImageOrientationLeft && orientation != UIImageOrientationRight &&
429 | orientation != UIImageOrientationLeftMirrored && orientation != UIImageOrientationRightMirrored &&
430 | orientation != UIImageOrientationUpMirrored && orientation != UIImageOrientationDownMirrored )
431 | return image;
432 |
433 | CGFloat scaleX = 1.0f;
434 | CGFloat scaleY = 1.0f;
435 | if( width > maxSize )
436 | scaleX = maxSize / width;
437 | if( height > maxSize )
438 | scaleY = maxSize / height;
439 |
440 | // Credit: https://github.com/mbcharbonneau/UIImage-Categories/blob/master/UIImage%2BAlpha.m
441 | CGImageAlphaInfo alpha = CGImageGetAlphaInfo( image.CGImage );
442 | BOOL hasAlpha = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast;
443 |
444 | CGFloat scaleRatio = scaleX < scaleY ? scaleX : scaleY;
445 | CGRect imageRect = CGRectMake( 0, 0, width * scaleRatio, height * scaleRatio );
446 | UIGraphicsImageRendererFormat *format = [image imageRendererFormat];
447 | format.opaque = !hasAlpha;
448 | format.scale = image.scale;
449 | UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageRect.size format:format];
450 | image = [renderer imageWithActions:^( UIGraphicsImageRendererContext* _Nonnull myContext )
451 | {
452 | [image drawInRect:imageRect];
453 | }];
454 |
455 | return image;
456 | }
457 |
458 | + (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize
459 | {
460 | // Check if the image can be loaded by Unity without requiring a conversion to PNG
461 | // Credit: https://stackoverflow.com/a/12048937/2373034
462 | NSString *extension = [path pathExtension];
463 | BOOL conversionNeeded = [extension caseInsensitiveCompare:@"jpg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"jpeg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"png"] != NSOrderedSame;
464 |
465 | if( !conversionNeeded )
466 | {
467 | // Check if the image needs to be processed at all
468 | NSArray *metadata = [self getImageMetadata:path];
469 | int orientationInt = [metadata[2] intValue]; // 1: correct orientation, [1,8]: valid orientation range
470 | if( orientationInt == 1 && [metadata[0] intValue] <= maximumSize && [metadata[1] intValue] <= maximumSize )
471 | return [self getCString:path];
472 | }
473 |
474 | UIImage *image = [UIImage imageWithContentsOfFile:path];
475 | if( image == nil )
476 | return [self getCString:path];
477 |
478 | UIImage *scaledImage = [self scaleImage:image maxSize:maximumSize];
479 | if( conversionNeeded || scaledImage != image )
480 | {
481 | if( ![UIImagePNGRepresentation( scaledImage ) writeToFile:tempFilePath atomically:YES] )
482 | {
483 | NSLog( @"Error creating scaled image" );
484 | return [self getCString:path];
485 | }
486 |
487 | return [self getCString:tempFilePath];
488 | }
489 | else
490 | return [self getCString:path];
491 | }
492 |
493 | // Credit: https://stackoverflow.com/a/37052118/2373034
494 | + (char *)getCString:(NSString *)source
495 | {
496 | if( source == nil )
497 | source = @"";
498 |
499 | const char *sourceUTF8 = [source UTF8String];
500 | char *result = (char*) malloc( strlen( sourceUTF8 ) + 1 );
501 | strcpy( result, sourceUTF8 );
502 |
503 | return result;
504 | }
505 |
506 | @end
507 |
508 | extern "C" int _NativeCamera_CheckPermission()
509 | {
510 | return [UNativeCamera checkPermission];
511 | }
512 |
513 | extern "C" void _NativeCamera_RequestPermission()
514 | {
515 | [UNativeCamera requestPermission];
516 | }
517 |
518 | extern "C" void _NativeCamera_OpenSettings()
519 | {
520 | [UNativeCamera openSettings];
521 | }
522 |
523 | extern "C" int _NativeCamera_HasCamera()
524 | {
525 | return [UNativeCamera hasCamera];
526 | }
527 |
528 | extern "C" void _NativeCamera_TakePicture( const char* imageSavePath, int maxSize, int preferredCamera )
529 | {
530 | [UNativeCamera openCamera:YES defaultCamera:preferredCamera savePath:[NSString stringWithUTF8String:imageSavePath] maxImageSize:maxSize videoQuality:-1 maxVideoDuration:-1];
531 | }
532 |
533 | extern "C" void _NativeCamera_RecordVideo( int quality, int maxDuration, int preferredCamera )
534 | {
535 | [UNativeCamera openCamera:NO defaultCamera:preferredCamera savePath:nil maxImageSize:4096 videoQuality:quality maxVideoDuration:maxDuration];
536 | }
537 |
538 | extern "C" int _NativeCamera_IsCameraBusy()
539 | {
540 | return [UNativeCamera isCameraBusy];
541 | }
542 |
543 | extern "C" char* _NativeCamera_GetImageProperties( const char* path )
544 | {
545 | return [UNativeCamera getImageProperties:[NSString stringWithUTF8String:path]];
546 | }
547 |
548 | extern "C" char* _NativeCamera_GetVideoProperties( const char* path )
549 | {
550 | return [UNativeCamera getVideoProperties:[NSString stringWithUTF8String:path]];
551 | }
552 |
553 | extern "C" char* _NativeCamera_GetVideoThumbnail( const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds )
554 | {
555 | return [UNativeCamera getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds];
556 | }
557 |
558 | extern "C" char* _NativeCamera_LoadImageAtPath( const char* path, const char* temporaryFilePath, int maxSize )
559 | {
560 | return [UNativeCamera loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize];
561 | }
--------------------------------------------------------------------------------
/Plugins/NativeCamera/iOS/NativeCamera.mm.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: f71ce2f3d3a5dbd46af575e628ed9d6e
3 | timeCreated: 1498722774
4 | licenseType: Pro
5 | PluginImporter:
6 | serializedVersion: 2
7 | iconMap: {}
8 | executionOrder: {}
9 | isPreloaded: 0
10 | isOverridable: 0
11 | platformData:
12 | data:
13 | first:
14 | Any:
15 | second:
16 | enabled: 0
17 | settings: {}
18 | data:
19 | first:
20 | Editor: Editor
21 | second:
22 | enabled: 0
23 | settings:
24 | DefaultValueInitialized: true
25 | data:
26 | first:
27 | iPhone: iOS
28 | second:
29 | enabled: 1
30 | settings: {}
31 | userData:
32 | assetBundleName:
33 | assetBundleVariant:
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.yasirkula.nativecamera",
3 | "displayName": "Native Camera",
4 | "version": "1.5.0",
5 | "documentationUrl": "https://github.com/yasirkula/UnityNativeCamera",
6 | "changelogUrl": "https://github.com/yasirkula/UnityNativeCamera/releases",
7 | "licensesUrl": "https://github.com/yasirkula/UnityNativeCamera/blob/master/LICENSE.txt",
8 | "description": "This plugin helps you take pictures/record videos natively with your device's camera on Android and iOS. It has built-in support for runtime permissions, as well."
9 | }
10 |
--------------------------------------------------------------------------------
/package.json.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 16b0316fc31bcf540a33a6b4363ec8bf
3 | PackageManifestImporter:
4 | externalObjects: {}
5 | userData:
6 | assetBundleName:
7 | assetBundleVariant:
8 |
--------------------------------------------------------------------------------