Add mobile camera capture, polaroid preview, gallery save & native share
- カメラ撮影 → プレビュー(ポラロイド風白縁)→ 保存 / ネイティブ共有 - iOS / Android のネイティブ実装(共有・ギャラリー保存・FileProvider) - iOS は共有シートのプリウォームで初回表示を軽減 - iOS Info.plist 用途説明をビルド後処理で自動付与 - Android パッケージ名ガードを有効化(ビルド前チェック) - README を整備、.gitignore に slnx / .vscode / .utmp を追加 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 061b9862a51924ae483211b22a86e662
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,199 @@
|
||||
package jp.yourname.unity.nativeshare;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class NativeShareBridge
|
||||
{
|
||||
public static void shareImage(
|
||||
final Activity activity,
|
||||
final String filePath,
|
||||
final String text,
|
||||
final String subject
|
||||
)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.runOnUiThread(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (filePath == null || filePath.length() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(filePath);
|
||||
|
||||
if (!file.exists())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = NativeShareFileProvider.getUriForFile(activity, file);
|
||||
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("image/png");
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
|
||||
if (text != null && text.length() > 0)
|
||||
{
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, text);
|
||||
}
|
||||
|
||||
if (subject != null && subject.length() > 0)
|
||||
{
|
||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
}
|
||||
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
shareIntent.setClipData(
|
||||
ClipData.newUri(
|
||||
activity.getContentResolver(),
|
||||
file.getName(),
|
||||
uri
|
||||
)
|
||||
);
|
||||
|
||||
String chooserTitle =
|
||||
subject != null && subject.length() > 0
|
||||
? subject
|
||||
: "Share";
|
||||
|
||||
Intent chooser = Intent.createChooser(shareIntent, chooserTitle);
|
||||
|
||||
activity.startActivity(chooser);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void saveImageToGallery(
|
||||
final Activity activity,
|
||||
final String filePath,
|
||||
final String displayName
|
||||
)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.runOnUiThread(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (filePath == null || filePath.length() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(filePath);
|
||||
|
||||
if (!file.exists())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
String name =
|
||||
displayName != null && displayName.length() > 0
|
||||
? displayName
|
||||
: file.getName();
|
||||
|
||||
ContentResolver resolver = activity.getContentResolver();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.Images.Media.DISPLAY_NAME, name);
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
{
|
||||
values.put(
|
||||
MediaStore.Images.Media.RELATIVE_PATH,
|
||||
Environment.DIRECTORY_PICTURES
|
||||
);
|
||||
values.put(MediaStore.Images.Media.IS_PENDING, 1);
|
||||
}
|
||||
|
||||
Uri uri = resolver.insert(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
values
|
||||
);
|
||||
|
||||
if (uri == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OutputStream out = null;
|
||||
InputStream in = null;
|
||||
|
||||
try
|
||||
{
|
||||
out = resolver.openOutputStream(uri);
|
||||
in = new FileInputStream(file);
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int length;
|
||||
|
||||
while ((length = in.read(buffer)) > 0)
|
||||
{
|
||||
out.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
out.flush();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (in != null)
|
||||
{
|
||||
in.close();
|
||||
}
|
||||
|
||||
if (out != null)
|
||||
{
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
{
|
||||
values.clear();
|
||||
values.put(MediaStore.Images.Media.IS_PENDING, 0);
|
||||
resolver.update(uri, values, null, null);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91f684ffb946d402fa17a9827e6b4c5e
|
||||
@@ -0,0 +1,231 @@
|
||||
package jp.yourname.unity.nativeshare;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
public class NativeShareFileProvider extends ContentProvider
|
||||
{
|
||||
public static Uri getUriForFile(Context context, File file) throws IOException
|
||||
{
|
||||
File canonicalFile = file.getCanonicalFile();
|
||||
|
||||
if (!isFileInAllowedDir(context, canonicalFile))
|
||||
{
|
||||
throw new SecurityException("許可されていない場所のファイルです: " + canonicalFile);
|
||||
}
|
||||
|
||||
String authority = context.getPackageName() + ".native_share_fileprovider";
|
||||
|
||||
return new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(authority)
|
||||
.appendPath("share")
|
||||
.appendQueryParameter("path", canonicalFile.getAbsolutePath())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static boolean isFileInAllowedDir(Context context, File file) throws IOException
|
||||
{
|
||||
String filePath = file.getCanonicalPath();
|
||||
|
||||
File[] allowedRoots = new File[]
|
||||
{
|
||||
context.getCacheDir(),
|
||||
context.getFilesDir(),
|
||||
context.getExternalCacheDir()
|
||||
};
|
||||
|
||||
for (File root : allowedRoots)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
String rootPath = root.getCanonicalPath();
|
||||
|
||||
if (filePath.equals(rootPath) || filePath.startsWith(rootPath + File.separator))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private File getFileForUri(Uri uri) throws IOException
|
||||
{
|
||||
Context context = getContext();
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new IOException("Context が取得できません。");
|
||||
}
|
||||
|
||||
String path = uri.getQueryParameter("path");
|
||||
|
||||
if (TextUtils.isEmpty(path))
|
||||
{
|
||||
throw new FileNotFoundException("path が空です。");
|
||||
}
|
||||
|
||||
File file = new File(path).getCanonicalFile();
|
||||
|
||||
if (!isFileInAllowedDir(context, file))
|
||||
{
|
||||
throw new SecurityException("許可されていないファイルパスです: " + file);
|
||||
}
|
||||
|
||||
if (!file.exists())
|
||||
{
|
||||
throw new FileNotFoundException("ファイルが存在しません: " + file);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
File file = getFileForUri(uri);
|
||||
String name = file.getName();
|
||||
|
||||
int dotIndex = name.lastIndexOf('.');
|
||||
|
||||
if (dotIndex >= 0 && dotIndex < name.length() - 1)
|
||||
{
|
||||
String extension = name.substring(dotIndex + 1).toLowerCase();
|
||||
String mimeType =
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
|
||||
if (mimeType != null)
|
||||
{
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException
|
||||
{
|
||||
if (mode != null && !mode.equals("r") && !mode.equals("rt"))
|
||||
{
|
||||
throw new FileNotFoundException("読み取り専用です。");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File file = getFileForUri(uri);
|
||||
|
||||
return ParcelFileDescriptor.open(
|
||||
file,
|
||||
ParcelFileDescriptor.MODE_READ_ONLY
|
||||
);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new FileNotFoundException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(
|
||||
Uri uri,
|
||||
String[] projection,
|
||||
String selection,
|
||||
String[] selectionArgs,
|
||||
String sortOrder
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
File file = getFileForUri(uri);
|
||||
|
||||
String[] columns = projection;
|
||||
|
||||
if (columns == null)
|
||||
{
|
||||
columns = new String[]
|
||||
{
|
||||
OpenableColumns.DISPLAY_NAME,
|
||||
OpenableColumns.SIZE
|
||||
};
|
||||
}
|
||||
|
||||
Object[] values = new Object[columns.length];
|
||||
|
||||
for (int i = 0; i < columns.length; i++)
|
||||
{
|
||||
if (OpenableColumns.DISPLAY_NAME.equals(columns[i]))
|
||||
{
|
||||
values[i] = file.getName();
|
||||
}
|
||||
else if (OpenableColumns.SIZE.equals(columns[i]))
|
||||
{
|
||||
values[i] = file.length();
|
||||
}
|
||||
else
|
||||
{
|
||||
values[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(columns, 1);
|
||||
cursor.addRow(values);
|
||||
return cursor;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(
|
||||
Uri uri,
|
||||
ContentValues values,
|
||||
String selection,
|
||||
String[] selectionArgs
|
||||
)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45b951edcbe0f4d3fb0d3c657daad36f
|
||||
@@ -0,0 +1 @@
|
||||
パッケージ名に合わせてこのディレクトリのパスは変更してください
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1764d38227db645198bed9bdf9e11bcd
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user