In a Xamarin Forms (3.0) application, what method(s) would I use to tell if a drawable resource exists in my Android project from shared project code?
In iOS, I can use NSFileManager to see if the file exists in my iOS project's "Resources" folder:
#if __IOS__
private bool DoesImageExist(string image)
{
//WORKS
return Foundation.NSFileManager.DefaultManager.FileExists(image);
}
#endif
In Android, I thought it should be part of the Assembly Resources, but that just returns my App.xaml file.
#if __ANDROID__
private bool DoesImageExist(string image)
{
//DOES NOT WORK
if(MyApp.Current?.GetType() is Type type)
foreach (var res in Assembly.GetAssembly(type).GetManifestResourceNames())
{
if (res.Equals(image, StringComparison.CurrentCultureIgnoreCase))
return true;
}
return false;
}
#endif
How to specifically check if a drawable exists by name in android would work like something this:
#if __ANDROID__
public bool DoesImageExist(string image)
{
var context = Android.App.Application.Context;
var resources = context.Resources;
var name = Path.GetFileNameWithoutExtension(image);
int resourceId = resources.GetIdentifier(name, "drawable", context.PackageName);
return resourceId != 0;
}
#endif
If your code is in a pcl or .net standard assembly, you will have to create an abstraction. Some kind of Ioc library works well for this. You can also have Android or iOS implement the abstract interface and make that available as a singleton somewhere. It's not as elegant, but it would work.
Basically you would implement something like this:
public interface IDrawableManager
{
bool DoesImageExist(string path);
}
Then have two implementations:
public class DroidDrawableManager : IDrawableManager
{
var context = Android.App.Application.Context;
var resources = context.Resources;
var name = Path.GetFileNameWithoutExtension(image);
int resourceId = resources.GetIdentifier(name, "drawable", context.PackageName);
return resourceId != 0;
}
public class IOSDrawableManager : IDrawableManager
{
public bool DoesImageExist(string image)
{
return Foundation.NSFileManager.DefaultManager.FileExists(image);
}
}
I've uploaded a working sample to github:
https://github.com/curtisshipley/ResourceExists
Related
I have a Xamarin application which plays remote(internet) audio files using the MediaPlayer with the following setup:
_mediaPlayer.SetDataSource(mediaUri);
_mediaPlayer.PrepareAsync();
Now I would like to change the implementation to also cache files. For the caching part I found a really nice library called MonkeyCache which saves the files in this JSON format:
{"$type":"System.Byte[], mscorlib","$value":"UklGRhAoAgBXQVZFZm10IBAAAAABAAIARKwAABCxAgAEABAAZGF0YdAnAgAAAAAAAAAAAAAAAAAA+P/4/+z/7P8KAAoA7//v//L/8v8JAAkA6f/p//j/+P8FAAUA6P/o/wEAAQD+//7/5//n ................."}
So my MediaPlayer setup has now changed to:
if (Barrel.Current.Exists(mediaUri)){
var audio = Barrel.Current.Get<byte[]>(mediaUri);
_mediaPlayer.SetDataSource(???);
}else{
using (var webClient = new WebClient()){
var downloadDataBytes = webClient.DownloadData(mediaUri);
if (downloadDataBytes != null && downloadDataBytes.Length > 0)
{
Barrel.Current.Add(mediaUri, downloadDataBytes, TimeSpan.FromDays(1));
_mediaPlayer.SetDataSource(???);
}
}
}
I would like to play the audio from a byte[] instead of the mediaUri.
Is there any way to actually play an in memory byte[]?
The only solutions that I could find were to create a FileInputStream out of a File by using a filepath, but the implementation of the MonkeyCache actually hashes the file name, before adding it:
static string Hash(string input){
var md5Hasher = MD5.Create();
var data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));
return BitConverter.ToString(data);
}
Therefore the downloaded bytes, will be saved under:
/data/data/com.package.name.example/cache/com.package.name.example/MonkeyCacheFS/81-D6-E8-62-F3-4D-F1-64-A6-A1-53-46-34-1E-FE-D1
Even if I were to use the same hashing logic to actually compute the file path myself and use the FileInputStream which might be working by what I've read, it would defeat the purpose of using the var audio = Barrel.Current.Get<byte[]>(mediaUri); functionality of the MonkeyCache.
However, if that is the only way, I will do it.
Edit: Even with my described approach, it would probably not work right away as even if I compute the right file name, it is still in JSON format.
Edit2: A working solution is:
var audio = Barrel.Current.Get<byte[]>(mediaUri);
var url = "data:audio/mp3;base64," + Convert.ToBase64String(audio);
_mediaPlayer.SetDataSource(url);
Finally figured it out:
The MediaPlayer.SetDataSource method has an overload which expects a MediaDataSource object.
MediaDataSource
For supplying media data to the framework. Implement
this if your app has special requirements for the way media data is
obtained
Therefore, you can use your implementation of the MediaDataSource and feed it your byte array.
public class StreamMediaDataSource : MediaDataSource
{
private byte[] _data;
public StreamMediaDataSource(byte[] data)
{
_data = data;
}
public override long Size
=> _data.Length;
public override void Close()
{
_data = null;
}
public override int ReadAt(long position, byte[] buffer, int offset, int size)
{
if (position >= _data.Length)
{
return -1;
}
if (position + size > _data.Length)
{
size -= (Convert.ToInt32(position) + size) - _data.Length;
}
Array.Copy(_data, position, buffer, offset, size);
return size;
}
}
which will lead to the MediaPlayer data source to be
_mediaPlayer.SetDataSource(new StreamMediaDataSource(downloadedDataBytes));
Edit: This only works for API >= 23. I still have to find a solution for API<23(which I still haven't).
Edit2: For API < 23 support I used the
var url = $"data:audio;base64,{Convert.ToBase64String(downloadedDataBytes)}";
_mediaPlayer.SetDataSource(url);
approach.
I.m trying to setup and build https://resources.samsungdevelopers.com/Gear_VR/030_Create_VR_App_or_Game_Using_a_Game_Engine/Exercise_2%3A_Create_a_Splash_Screen/Step_7%3A_Build_and_Run_the_Application
When I try and build the application it throws the following error:
Assets/Workshop/Scripts/TextureLoader.cs(114,31): error CS1105: `TextureLoader.GetFilesWithExtensions(this DirectoryInfo, params string[])': Extension methods must be declared static
// Gets the list of files in the directory by extention and filters for our specified ones (images)
public IEnumerable<FileInfo> GetFilesWithExtensions (this DirectoryInfo dir, params string[] extensions) {
IEnumerable<FileInfo> files = dir.GetFiles();
return files.Where(f => extensions.Contains(f.Extension));
}
public bool isFinishedLoadingFirstTexture() {
return mFinishedFirstTexture;
}
I have already changed it to a public static class in the header.
The class being static doesn't do much for the methods. You need to make your method static for the extensions to work!
public static IEnumerable<FileInfo> GetFilesWithExtensions (this DirectoryInfo dir, params string[] extensions) {
IEnumerable<FileInfo> files = dir.GetFiles();
return files.Where(f => extensions.Contains(f.Extension));
}
I wrote an extension method here if you need another example!
I order to make an extension method, the class must be public and static.
The function must be public and static too. You missed this part.
public static class TextureLoader
{
public static IEnumerable<FileInfo> GetFilesWithExtensions(this DirectoryInfo dir, params string[] extensions)
{
IEnumerable<FileInfo> files = dir.GetFiles();
return files.Where(f => extensions.Contains(f.Extension));
}
}
Modify your code this way, the error will disappear
private IEnumerable<FileInfo> GetFilesWithExtensions(DirectoryInfo dir, string[] extensions)
{
IEnumerable<FileInfo> files = dir.GetFiles();
return files.Where(f => extensions.Contains(f.Extension));
}
I want to use the light sensor on Android with Unity. Unity can't do this and there is no asset/plugin on the store who could do that. And google isn't so frendly with this question...
Simple version
This part explain how to use my code.
Put the .jar file into Assets/Plugins/Android folder
Then simply add the script LightSensorPluginScript.cs on the GameObject you wanted.
Then if you want to get the sensor value:
TextMesh tm;
LightSensorPluginScript test;
void Start() {
tm = transform.GetComponent<TextMesh>();
test = GetComponent<LightSensorPluginScript> ();
}
void Update() {
tm.text = test.getLux().ToString();
}
All the files can be found in this zip archive
Detailed version
This part explain how to create the plugin.
First of all you have to create an Android library. If you are using Android Studio, they are converted into .aar files. Extract them like a zip file and you will find a classes.jar file which is the correct .jar you want. You can rename it as you want, Unity didn't care.
Android Java code
public class LightSensorLib{
private SensorManager mSensorManager;
private Sensor mSensorRot;
private float lux = -1000;
public void init(Context context) {
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
mSensorRot = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
SensorEventListener mySensorEventListener = new SensorEventListener() {
#Override
public void onSensorChanged(SensorEvent event) {
float sensorData[];
if(event.sensor.getType()== Sensor.TYPE_LIGHT) {
sensorData = event.values.clone();
lux = sensorData[0];
}
}
#Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
mSensorManager.registerListener(mySensorEventListener, mSensorRot, 500);
}
public float getLux () {
return lux;
}
}
Then I have a C# script who does the bridge between Android Java and Unity
C# Bridge code
public class LightSensorPluginScript : MonoBehaviour {
private AndroidJavaObject activityContext = null;
private AndroidJavaObject jo = null;
AndroidJavaClass activityClass = null;
void Start () {
#if UNITY_ANDROID
activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
activityContext = activityClass.GetStatic<AndroidJavaObject>("currentActivity");
jo = new AndroidJavaObject("com.etiennefrank.lightsensorlib.LightSensorLib");
jo.Call("init", activityContext);
#endif
}
public float getLux() {
#if UNITY_ANDROID
return jo.Call<float>("getLux");
#endif
}
}
Now you can do what is explained in the Simple version part of this post to use the sensor.
If you want any precision I would be glad to answer and update the post.
For the IOs version I'm sorry but I have no mac to develop this... So if you want to lend me one I would be glad to do it.
I could add it as a plugin on the asset store but it feels a bit cumbersome. So I prefer to post it on StackOverflow where the post editor is really neat.
At last I hope it helps some developers.
using UnityEngine;
using System;
using System.Collections;
public class LightSensorPluginScript : MonoBehaviour {
private AndroidJavaObject activityContext = null;
private AndroidJavaObject jo = null;
AndroidJavaClass activityClass = null;
public TextMesh tm;
LightSensorPluginScript test;
void Start () {
#if UNITY_ANDROID
activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
activityContext = activityClass.GetStatic<AndroidJavaObject>("currentActivity");
tm = transform.GetComponent<TextMesh>();
test = GetComponent<LightSensorPluginScript>();
jo = new AndroidJavaObject("com.etiennefrank.lightsensorlib.LightSensorLib");
jo.Call("init", activityContext);
#endif
}
public float getLux() {
#if UNITY_ANDROID
return jo.Call<float>("getLux");
#endif
}
void Update()
{
tm.text = test.getLux().ToString();
}
}
boost::filesystem provides many interesting functions, for example, we can use boost::filesystem::exists to check whether one file exists or not. I am now using this functionality in Android. It succeeds in the following program:
int main()
{
using namespace std;
string file_name="/data/local/tmp/abc/def.txt";
boost::filesystem::path dddddd(file_name);
if(boost::filesystem.exists(dddddd))
{
std::cout<<"Succeed"<<std::endl;
}
else
{
std::cout<<"Failed"<<std::endl;
}
return 0;
}
However, if I use wstring instead:
int main()
{
using namespace std;
wstring file_name=L"/data/local/tmp/abc/def.txt";
boost::filesystem::wpath dddddd(file_name);
if(boost::filesystem.exists(dddddd))
{
std::cout<<"Succeed"<<std::endl;
}
else
{
std::cout<<"Failed"<<std::endl;
}
return 0;
}
It will fail. It seems to me that the reason is because Android does not handle wstring very well. Any idea how I can change in order to make sure boost::filesystem::wpath can work on Android?
I am developing a Cordova plugin for Android and I am having difficulty overcoming accessing project resources from within an activity - the plugin should be project independent, but accessing the resources (e.g. R.java) is proving tricky.
My plugin, for now, is made up of two very simple classes: RedLaser.java and RedLaserScanner.java.
RedLaser.java
Inherits from CordovaPlugin and so contains the execute method and looks similar to the following.
public class RedLaser extends CordovaPlugin {
private static final string SCAN_ACTION = "scan";
public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException {
if (action.equals(SCAN_ACTION)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
scan(args, callbackContext);
}
});
return true;
}
return false;
}
private void scan(JSONArray args, CallbackContext callbackContext) {
Intent intent = new Intent(this.cordova.getActivity().getApplicationContext(), RedLaserScanner.class);
this.cordova.startActivityForResult((CordovaPlugin) this, intent, 1);
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// Do something with the result
}
}
RedLaserScanner.java
The RedLaserScanner contains the Android Activity logic and inherits from BarcodeScanActivity (which is a RedLaser SDK class, presumably itself inherits from Activity);
A very simple structure is as follows:
public class RedLaserScanner extends BarcodeScanActivity {
#Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.preview_overlay_new_portrait);
}
}
I am having trouble because I need to access the project's resources to access R.layout.preview_overlay_new_portrait (which are scatted in the Eclipse project) - but I cannot do this unless I import com.myProject.myApp.R - which makes my plugin have a dependency on the project itself.
I did some investigation and found cordova.getActivity().getResources() which seems useful, but this is not accessible from within my RedLaserScanner - because it does not inherit from CordovaPlugin.
Can somebody please help me with some pointers?
Thanks
I just ran into the same issue and it turns out to be pretty easy to solve. RedLaserScanner extends an activity, so you can just call getResources() like this:
setContentView(getResources("preview_overlay_new_portrait", "layout", getPackageName()));
Hooks can be used to replace source file contents to remove wrong imports and/or add the right imports of resources.
I created a script that do it without needing to specify the files. It tries to find source files (with .java extension), removes any resource import already in it and then put the right resources import (if needed), using the Cordova application package name.
This is the script:
#!/usr/bin/env node
/*
* A hook to add resources class (R.java) import to Android classes which uses it.
*/
function getRegexGroupMatches(string, regex, index) {
index || (index = 1)
var matches = [];
var match;
if (regex.global) {
while (match = regex.exec(string)) {
matches.push(match[index]);
console.log('Match:', match);
}
}
else {
if (match = regex.exec(string)) {
matches.push(match[index]);
}
}
return matches;
}
module.exports = function (ctx) {
// If Android platform is not installed, don't even execute
if (ctx.opts.cordova.platforms.indexOf('android') < 0)
return;
var fs = ctx.requireCordovaModule('fs'),
path = ctx.requireCordovaModule('path'),
Q = ctx.requireCordovaModule('q');
var deferral = Q.defer();
var platformSourcesRoot = path.join(ctx.opts.projectRoot, 'platforms/android/src');
var pluginSourcesRoot = path.join(ctx.opts.plugin.dir, 'src/android');
var androidPluginsData = JSON.parse(fs.readFileSync(path.join(ctx.opts.projectRoot, 'plugins', 'android.json'), 'utf8'));
var appPackage = androidPluginsData.installed_plugins[ctx.opts.plugin.id]['PACKAGE_NAME'];
fs.readdir(pluginSourcesRoot, function (err, files) {
if (err) {
console.error('Error when reading file:', err)
deferral.reject();
return
}
var deferrals = [];
files.filter(function (file) { return path.extname(file) === '.java'; })
.forEach(function (file) {
var deferral = Q.defer();
var filename = path.basename(file);
var file = path.join(pluginSourcesRoot, filename);
fs.readFile(file, 'utf-8', function (err, contents) {
if (err) {
console.error('Error when reading file:', err)
deferral.reject();
return
}
if (contents.match(/[^\.\w]R\./)) {
console.log('Trying to get packages from file:', filename);
var packages = getRegexGroupMatches(contents, /package ([^;]+);/);
for (var p = 0; p < packages.length; p++) {
try {
var package = packages[p];
var sourceFile = path.join(platformSourcesRoot, package.replace(/\./g, '/'), filename)
if (!fs.existsSync(sourceFile))
throw 'Can\'t find file in installed platform directory: "' + sourceFile + '".';
var sourceFileContents = fs.readFileSync(sourceFile, 'utf8');
if (!sourceFileContents)
throw 'Can\'t read file contents.';
var newContents = sourceFileContents
.replace(/(import ([^;]+).R;)/g, '')
.replace(/(package ([^;]+);)/g, '$1 import ' + appPackage + '.R;');
fs.writeFileSync(sourceFile, newContents, 'utf8');
break;
}
catch (ex) {
console.log('Could not add import to "' + filename + '" using package "' + package + '". ' + ex);
}
}
}
});
deferrals.push(deferral.promise);
});
Q.all(deferrals)
.then(function() {
console.log('Done with the hook!');
deferral.resolve();
})
});
return deferral.promise;
}
Just add as an after_plugin_install hook (for Android platform) in your plugin.xml:
<hook type="after_plugin_install" src="scripts/android/addResourcesClassImport.js" />
Hope it helps someone!
I implemented a helper for this to keep things clean. It also helps when you create a plugin which takes config.xml arguments which you store in a string resource file in the plugin.
private int getAppResource(String name, String type) {
return cordova.getActivity().getResources().getIdentifier(name, type, cordova.getActivity().getPackageName());
}
You can use it as follows:
getAppResource("app_name", "string");
That would return the string resource ID for app_name, the actually value still needs to be retrieved by calling:
this.activity.getString(getAppResource("app_name", "string"))
Or for the situation in the original question:
setContentView(getAppResource("preview_overlay_new_portrait", "layout"));
These days I just create a helper which returns the value immediately from the the helper:
private String getStringResource(String name) {
return this.activity.getString(
this.activity.getResources().getIdentifier(
name, "string", this.activity.getPackageName()));
}
which in turn you'd call like this:
this.getStringResource("app_name");
I think it's important to point out that when you have the resource ID you're not always there yet.
try using android.R.layout.preview_overlay_new_portrait