tag:blogger.com,1999:blog-59547473446132705242024-02-08T09:08:20.967-08:00The Software RogueA glimpse into the world of programming from one man's perspective.Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.comBlogger11125tag:blogger.com,1999:blog-5954747344613270524.post-75388860087903838832015-12-21T11:29:00.000-08:002015-12-21T11:29:00.480-08:00Lollipop external sdcard access with ACTION_OPEN_DOCUMENT_TREEThis is a very late post on the subject of the closure of external sdcard access. The initial release in 4.4 was so very poor. (<a href="https://code.google.com/p/android/issues/detail?id=67570">https://code.google.com/p/android/issues/detail?id=67570</a>) The above thread was closed with their last post on the subject.<br />
<br />
So let's talk about OPEN_DOCUMENT_TREE. On the surface it seems to do what an app would like. On the technical side, it does accomplish it with lots of problems. However, it leads to a poor user experience which is probably the worst of the issues.<br />
<br />
It is easy to kick off the selection process, simply add:<br />
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);<br />
startActivityForResult(i, RESPONSE_ID)<br />
<br />
You can give it a list of mimetypes in order to limit the possible locations:<br />
i.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);<br />
<br />
This open a 'picker' app that is system implemented. It really looks crappy, imo. What's worse is that many devices default to not showing the external sdcard. It only shows the internal flash. The user has to go to the overflow menu and 'unhide' the external sdcard. While there are many users that understand technology, many of them don't and would not expect this. At this point, the typical user will simply select some new directory on the internal flash which doesn't accomplish the goal at all. (Most of the time users are trying to move items to external flash to preserve space of internal flash.)<br />
<br />
Issue #2 is that you can't provide a hint to the directory that you would like access to. There are some issues with trying to implement it (more on that later) but it would be nice if you could indicate the storage drive to use and possible path. That way the user can just look at what the app suggests and hit ok or find another location.<br />
<br />
Let me give an example here of why would you want it. In my case, I want to provide access to external sdcard due to space requirements. So the app is currently using internal flash and the user would like to use the external flash. The Picker app is bad enough that the user many not realize they are simply picking a new internal flash spot. <br />
<br />
Issue #3, you must use a ContentProvider in order to do things like inapp sharing. Unfortunately, many apps, including Google apps, have no standard way of providing it. Some require hard paths to files which is not possible with the new sdcard system. (Note, copying the file to a temp location is not an answer). The destination app crashes most of the time if you give a DocumentFile uri. So you end up having to provide a hodge podge of solutions based upon the request. It is getting better as new versions of the OS come out. <br />
<br />
I know Google claimed security when this was implemented but they were not thinking of the user when they did. They didn't implement security for both internal and external flash. It was a very poorly thought out system. And really it was made to enhance Google services rather than help developers or the user. It's unfortunate that they took this track. I like Android and that it is mostly open. <br />
<br />
So, enough of the rant, what would I like to see:<br />
<br />
1) Ability to provide a path to the directory you would like to use. <br />
2) Indicate the flash to use (there could be more than one external flash drive)<br />
3) Support for URIs in all file handling - some tools require a path to a file and simply cannot work with URIs (that are also DocumentFile uris). Image processing, etc., to name a few. <br />
4) Instead of a uri, allow getting the file path so that the hundreds of libraries out there that use file paths will work. The current method is to retrieve an InputStream to the file and pass it to the library. This probably needs some type of method to determine whether the file is local since the URI could be pointing to some cloud item. The URI won't work for many libraries, they just don't have this ability.<br />
5) Easy way to determine the amount of storage currently used in the device.<br />
<br />
<br />
<br />
<br />
<br />Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com0tag:blogger.com,1999:blog-5954747344613270524.post-11485667220106638582013-09-10T01:30:00.005-07:002013-09-12T09:48:22.084-07:00How to build a better Android JNI library using NDKI've been using Android for a few years now and just recently had to build a fairly substantial library in C++. I've always hated the typical structure of the JNI layer, at least the one I learned originally. The new library that needed integrating was completely devoid of any JNI structure. Given the nature of the project, I didn't want to hard code the paths.<br />
<br />
I looked at a variety of jni code generators. There weren't many restrictions, it did have to build on windows, mac, and pc. After looking at them, I decided my project wasn't that big so it seemed overkill to put in a code generator that would need to be maintained.<br />
<br />
In the end, I chose a simple method that gets the job done with minimal frustration. The only real requirement is that you must specify the calling paramters and return code. Once you have done a few, you get used to it and the rest are easy. A code generator would eliminate that need. (Checkout the bottom of this page for some possible generators: <a href="http://en.wikipedia.org/wiki/Java_Native_Interface">jni generator info</a>.)<br />
<br />
Typically, it looks something like this:<br />
<br />
Java code:<br />
<br />
public class FooUtil {<br />
try {<br />
System.loadLibrary("awesomeFooLib");<br />
native_init = true;<br />
} catch (UnsatisfiedLinkerError e) {<br />
}<br />
<br />
private native int fooMeN(int blah);<br />
<br />
public int fooMe(int blah) {<br />
if (native_init) {<br />
return fooMe(blah);<br />
}<br />
return -1;<br />
}<br />
}<br />
<br />
The C layer looks like:<br />
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {<br />
return JNI_VERSION_1_6;<br />
}<br />
JNIEXPORT jint JNICALL Java_com_mtterra_android_utils_fooMeN(<br />
JNIEnv *env, jobject self, jint blah)<br />
{<br />
// do something interesting<br />
return 0;<br />
}<br />
<br />
The problem with the above is all the hardcoded paths and the general fuglyness of the whole thing. You can use javah -jni <classfile> to generate a header file with the long method names. As much as I would like it to read better, JNI coding doesn't lend itself well for that. However, it can be better. Note: if you are using JNI you should make sure to read <a href="http://developer.android.com/training/articles/perf-jni.html">JNI Tips</a>.<br />
<br />
The new method is to register your JNI functions when the library is loaded instead of hard coding the paths. This allows for much better readability and somewhat better portability. The Java class portion stays the same, only the JNI changes. I used C calling convention above, using C++ (below) just requires wrapping the code with extern "C" and calling functions off of the env pointer is simpler.<br />
<br />
#ifdef __cplusplus<br />
extern "C" {<br />
#endif<br />
<br />
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {<br />
JNIEnv *env;<br />
jint ok = vm->GetEnv((void **)& env, JNI_VERSION_1_6);<br />
<br />
// Add a function to register all of the jni calls<br />
if (ok == 0) {<br />
installJNIFunctions(env);<br />
}<br />
<br />
return JNI_VERSION_1_6;<br />
}<br />
<br />
// Add your normal-looking functions<br />
static int JNICALL fooMe(int blah) {<br />
return 42;<br />
}<br />
<br />
// Define all the functions here that need to be registered<br />
static JNINativeMethod fooLibraryMethods[] = {<br />
// Calling parameters are pretty easy, the end of this post has more info.<br />
// The first paramter is the name that your java defines as the native call.<br />
// Then the method signature then the local function to be called.<br />
{ (char * const) "fooMeN", "(I)I", (void *)&fooMe) }<br />
};<br />
<br />
<br />
void installJNIFunctions(JNIEnv *env) {<br />
// This part defines the linkage to your app. It is a hard coded path but it is also<br />
// very easy to change if you move the code to another app or put it in a library at some<br />
// point.<br />
// In your .h file you would add<br />
// #define FOO_LIBRARY "com/mtterra/android/utils"<br />
// substituting your package name obviously.<br />
jclass fooLibCls = env->FindClass(FOO_LIBRARY);<br />
<br />
// Make sure the class was found<br />
if (fooLibCls == NULL) {<br />
env->fatalError("Unable to find " FOO_LIBRARY);<br />
return;<br />
}<br />
<br />
// get the number of methods available<br />
int noofMethods= sizeof(fooLibraryMethods) / sizeof(fooLibraryMethods[0]);<br />
<br />
// finally, register the methods<br />
env->RegisterNatives(fooLibCls, fooLibraryMethods, noof Methods);<br />
<br />
//done....reap the rewards of unmangle names<br />
}<br />
<br />
#ifdef __cplusplus<br />
} // extern "C"<br />
#endif<br />
<br />
The hardest part is writing the method signatures. In the above example, I is standard for int. So (I)I is an int argument to the method with a return type of int. Looking at a more interesting example:<br />
<br />
long foo(String s, int i, String t); This would translate to a method signature of (Ljava/lang/String;ILjava/lang/String)J<br />
<br />
To see the full list of java types that can be used and more examples see <a href="http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html">this</a>. As far as the ndk build tools, there aren't any needed changes to compile the code. <br />
<br />
Using the above method makes the code very readable and keeps from complicating the build environment. Of course, if you have hundreds of jni calls then maybe a wrapper or jni code generator would be a better way to go. In general, I have lots of libraries but there are few interfacing points where jni is involved. JNI calls are rather expensive so cutting down the number of calls is a good thing.<br />
<br />
---------------------------------------------------------------------------------------------------<br />
<br />
<b><span style="color: blue;">QuickLogger Fitness - super quick fitness tracking</span></b><br />
<a href="https://play.google.com/store/apps/details?id=com.mtterra.quicklogger">
<img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" />
</a>
<br />
<br />
<br />Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com0tag:blogger.com,1999:blog-5954747344613270524.post-48705889146847974912012-03-27T07:10:00.001-07:002013-09-12T09:50:23.047-07:00Google IO registration day, now!Ready....set....wait..... more spinner.....more spinner.. sigh<br />
<br />
Yeah! Finally got in on my mobile device. See ya all there!
<br />
<br />
<br />
<br />
<b><span style="color: blue;">QuickLogger Fitness - super quick fitness tracking</span></b><br />
<a href="https://play.google.com/store/apps/details?id=com.mtterra.quicklogger">
<img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" />
</a>
<br />Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com0tag:blogger.com,1999:blog-5954747344613270524.post-42817598475267623512011-11-18T13:34:00.001-08:002011-12-30T10:07:58.190-08:00Building Android ICS (ice cream sandwich) with Suse 12.1The main information for setup and install is best found here: <a href="http://source.android.com/source/initializing.html">http://source.android.com/source/initializing.html</a>.<br />
<br />
That said, there are always build issues since everyone has a slightly different environment. I am currently running Suse 11.4 that has been upgraded over time from 11.1. This is all run under a VM in a laptop set for 3/4 GB of RAM available to it on a 3yr old core 2 processor. Needless to say, it takes a while to build. <br />
<br />
There have been several issues that have come up that others may run into. <br />
First, GCC 4.5.0 that comes in 11.3, didn't work. It came up with an internal compiler error in caller-save.c. I heard there were issues with 4.6 and that the best compiler for it is probably 4.4.3. I decided to upgrade to 4.5.1 which comes in 11.4 Suse distro. This fixed the problem.<br />
<br />
3/4GB of RAM is a bit low for doing this build. I bumped it to 2GB and it didn't have any issues. I have approximately 20GB of free disk space which seems to be enough. It takes around 6 or so to do the build. I used 2G cache size for the build. <br />
<br />
Also, make needs to be version 3.81. Apparently, 3.82 has an bug in it that causes some issues.<br />
<br />
The next big issue is with OpenJDK. There is at least one error:<br />
<br />
cts/apps/CtsVerifier/src/com/android/cts/verifier/PassFailButtons.java:191: onCreateDialog(int,android.os.Bundle) in android.app.Activity cannot implement onCreateDialog(int,android.os.Bundle) in com.android.cts.verifier.PassFailButtons.PassFailActivity; attempting to assign weaker access privileges; was public<br />
private static <T extends android.app.Activity & PassFailActivity><br />
^<br />
1 error<br />
make: *** [out/target/common/obj/APPS/CtsVerifier_intermediates/classes-full-debug.jar] Error 41<br />
<br />
Digging a bit showed that OpenJDK was the problem. I went and downloaded the Sun JDK which is what is suggested. Download info is part of the top installation link. ICS is built with Java 1.6. Previously, 1.5 was the required java compiler. So setting the location of the JDK is important depending on which type of build you want. <br />
<br />
Some additional packages are required. I also did this with a Suse 12.1 64 bit clean install which require even more packages with 4.6.2 as the compiler vs 4.5.1. The full list is outline below. <br />
<br />
bison - used by 'lunch' command<br />
glibc-devel libraries (will see missing gnu-stupbs-32.h)<br />
flex<br />
mesa libraries<br />
zlib devel (32 bit)<br />
ncurses devel (32 bit)<br />
gperf<br />
perl-Switch module<br />
libX11-devel 32 bit<br />
<br />
I had the gcc-4.6.2 64 bit version installed. You'll need to install gcc & gcc-c++ 32bit as well.<br />
<br />
Make sure to load the envsetup.sh before the next step: (source build/envsetup.sh).<br />
<br />
After installing, setup the path correctly. For bash, this is:<br />
'export PATH=$PATH;/usr/java/jdk1.6.0_29/bin'<br />
which isn't good enough, had to set the real flag:<br />
'export ANDROID_JAVA_HOME=/usr/java/jdk1.6.0_29'.<br />
<br />
<br />
In order to get an older revision (the current is 1.7 which won't work), you must signup for an oracle account.<br />
<br />
Before you do a make, open development/tools/emulator/opngl/host/renderer/Android.mk. Double check that it has LOCAL_LDLIBS += -lX11 added. This fixes a build error that results in 'undefined reference to XInitThreads'. (Courtesy of Diego Stamigni)<br />
<br />
WIth the build 4.0.3 of ICS, there were some build problems using 4.6.2.<br />
<br />
- Issue with external/oprofile/libpp/format_output.h, line 94. Change:<br />
mutable count & counts; to just 'count & counts;' With latest ics version this has likely been changed.<br />
<br />
- ptrdiff_t does not name a type<br />
Add #include <cstddef> in file external/gtest/include/gtest/internal/gtest-param-util.h<br />
<br />
- ParamName set but not used<br />
Comment out the line in frameworks/compile/slang/slang_rs_export_foreach.cpp or you can modified the make with -Werror.<br />
<br />
Now do a make -j4 and you should be all set.<br />
<br />
NOTE: Using Suse 11.4 with gcc 4.5.1, I could build full-magura but there were a number of problems with full-eng. With Suse 12.1 and gcc 4.6.2, full-eng built without problems.<br />
<br />
To run the full-eng emulator, export ANDRIOD_PRODUCT_OUT=~/<android_root>/out/target/product/generic then run the emulator located in out/host/linux-x86/bin/emulator. <br />
<br />
Good luck!<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div>
<br /></div>Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com1tag:blogger.com,1999:blog-5954747344613270524.post-51888831104366493552010-12-14T01:08:00.000-08:002010-12-14T01:09:10.900-08:00Skipping entry in package table 0 because it is not complexI saw this message for a while before investigating it. A couple of google search led me to nowhere. Basically, there were some questions on it but no answers. Given that there are probably a few ways to get into this, its probably better to figure out how to track it down.<br />
<br />
So the message looks like the following:<br />
<blockquote>Skipping entry 0x7f090003 in package table 0 because it is not complex!</blockquote><br />
A great message if there ever was one. Anyway, the first thing to do is look at the generated output. In eclipse, its under <project>/gen/<pkgname>/R.java. The R.java file looks like the following:<br />
<br />
<blockquote>/* AUTO-GENERATED FILE. DO NOT MODIFY.<br />
*<br />
* This class was automatically generated by the<br />
* aapt tool from the resource data it found. It<br />
* should not be modified by hand.<br />
*/<br />
<br />
package fi.eye.android;<br />
<br />
public final class R {<br />
public static final class anim {<br />
public static final int boxanimation=0x7f040000;<br />
public static final int fade_in=0x7f040001;<br />
public static final int fade_out=0x7f040002;<br />
public static final int home_grid_fade=0x7f040003;<br />
public static final int slide_left=0x7f040004;<br />
public static final int slide_right=0x7f040005;<br />
<br />
continues for a long time......</blockquote><br />
So its easy to just search for the offending value to determine what value to look for. In my case, it was a style attribute. Some items require a style with a one or more specific items defined. So in the your res/values/styles.xml file you might have something like:<br />
<br />
<style name="Theme.myTheme" parent="android:style/Theme.White"><br />
<item name="android:....">value</item><br />
</style><br />
<br />
The 'value' is the important bit. some item names refer to more complicated values, such as android:textViewStyle. So you can either define another style with the set of common values or replace it with a specific value such as <item name="android:textSize"><item name="android:textSize">14sp</item></item>. <br />
<br />
After a reload it should be good to go.Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com0tag:blogger.com,1999:blog-5954747344613270524.post-20891266870634771712010-11-04T23:53:00.000-07:002010-11-04T23:56:04.789-07:00Android ScrollView workaround funThere are lots of little things that can be rather...headache worthy. ScrollView is a great little widget that with one simple entry, you get scrolling. However, depending on what you put in it, can lead to some interesting results. For instance, don't stick things like Lists in it. In my case, it was a simple EditText box that caused some issues.<br />
<br />
Upon launch, EditText grabbed the focus causing the virtual keyboard to come up. This was due to having no available focuses, it chose my EditText box. Digging into it, the obvious solution that has many posts android:focusable="false" and the android:focusableInTouchMode="false". Of course, that doesn't help if you really want to focus on it.<br />
<br />
The end result was to set these to false and upon click, change the modes. All to get around the issue with focus firing on this text box. Something like:<br />
<br />
<blockquote>public OnTouchListener mNotesTouchListener = new OnTouchListener() {<br />
<br />
public boolean onTouch(View arg0, MotionEvent arg1) {<br />
notes.setFocusableInTouchMode(true);<br />
notes.setFocusable(true);<br />
return false;<br />
}<br />
};</blockquote><br />
And the good thing is none of it affects using the trackball and DPad. Granted this seems like a minor issue. At least it can be worked around.Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com0tag:blogger.com,1999:blog-5954747344613270524.post-55054770707604479582010-07-04T14:31:00.000-07:002010-07-06T22:32:41.424-07:00ADB reporting unknown target deviceA quick note for developers updating to Froyo 2.2. After upgrading my Nexus One to version 2.2 (FRF91), the Android Device Chooser that pops up when you try to debug your application indicates the device is an unknown target rather than version 2.2. <br />
<br />
The Android SDK and AVD manager will indicate all has been updated (assuming you did that). The solution is to make sure you upgraded Eclipse with the latest version of the adb/ddms plugin. In Eclipse, go to Help->Check For Updates should take care of the issue.<br />
<br />
An easy thing to overlook and my first check on Google didn't pull up anything. So maybe this will help someone else.<br />
<br />
UPDATE: One caveat, only one usb port actually works for this. Nothing worked while trying to get the other USB ports working. I'll have to dig up another usb cable to try. There have been reports of some dodgy cables.<br />
<br />
CheersBruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com1tag:blogger.com,1999:blog-5954747344613270524.post-11690282698174354142010-05-11T00:50:00.000-07:002013-09-12T09:51:03.317-07:00Porting of cURL to Android OS using NDKIn continuing my journey into Android territory, I decided it would be useful to understand the NDK development kit. Given I want to transfer some files and possibly do a couple of other projects requiring low level work, I selected the cURL kit to port. I've used cURL for a number of projects and it is my hope it runs well under Android. The project, curltest, was run under Android 2.1 but it should work for other versions as well. Given that most Android phones will be getting upgrades rolled out over the next couple of months to version 2.1, seems like a reasonable target. I will be added HTTPS capability as soon as I port openssl, for now, this is strictly unsecured transfers.<br />
<br />
I did see some examples of partial porting and even in the curl library source there is an Android.mk which describes how to set it up although not in enough detail. Both were helpful but didn't actually get me to the holy land of a usable test application. (http://www.mail-archive.com/curl-library@cool.haxx.se/msg03433.html was helpful but didn't work for me.)<br />
<br />
I found it worth the exercise of downloading the full android sources and building them. Look at the build process, the bionic library, and well as what's in external. The external directory is where you would add repositories to do things like run configure to generate config.h and Makefiles.<br />
<br />
You won't actually need Linux since I have included the config file here but in case you want to follow way it was created, I've included it.<br />
<br />
Summary of the steps to take for this port:<br />
- Lots of searches and view the current state of curl porting<br />
- Setup source repository (I used Linux, you could try windows using cygwin) <br />
- Create curl_config.h under the full android source tree<br />
- Untar curltest.tar.gz under $NDKROOT or Create NDK JNI App on Windows<br />
- Cross-link or add to eclipse project to build Java front-end<br />
- Unpack cURL into $NDKROOT/apps/curltest/jni/curl<br />
- Copy curltest/jni/curlbuild.h to curltest/jni/curl/include/curl<br />
- Copy curltest/jni/curl_config.h to curltest/jni/curl/lib<br />
- Build and test <br />
<br />
<h3>
Setup source repository on Linux </h3>
It's one thing to describe doing X, quite another to actually do it. Anyway, I ended up creating my own android repository to start the process. While I was using Windows 7 for my NDK development, I switched to Linux to create the curl_config.h file needed in the build. What this means it that you run a couple of scripts. First up, repo. Created a directory and cd to it. Download the repo script 'wget https://android.git.kernel.org/repo'. Move repo script to somewhere in your path and issue 'repo init -u git://android.git.kernel.org/platform/manifest.git' then after run 'repo sync'. These two should get you all the goodies. I had some stalls where I had to restart the sync process but after a long while it completed successfully.<br />
<br />
<h3>
Create curl_config.h under the full android source tree</h3>
Next, I created a script to call the configure command to generated the curl_config.h. CD to $ANDROID_ROOT/external. Untar curl libraries, I renamed my from curl-7.20.1 to just curl. cURL has an Android.mk inside of it. In the file were some instructions for generating the curl_config.h. I found them helpful as a start but ultimately I had to fiddle with a number of things to get this to actually work. Here is the script I used:<br />
<br />
<div style="font-size: 12px; height: 200px; overflow: auto; width: 700px;">
#!/bin/sh<br />
<br />
ANDROID_ROOT="$HOME/android_src" &amp;&amp; \<br />
TOOLCHAIN_VER="4.4.0" \<br />
PLATFORM_VER="5" \<br />
CROSS_COMPILE=arm-eabi- \<br />
PATH=$ANDROID_ROOT/prebuilt/linux-x86/toolchain/arm-eabi-$TOOLCHAIN_VER/bin:$PATH &amp;&amp; \<br />
CPPFLAGS="-I $ANDROID_ROOT/system/core/include -I$ANDROID_ROOT/bionic/libc/include -I$ANDROID_ROOT/ndk/build/platforms/android-5/arch-arm/usr/include -I$ANDROID_ROOT/bionic/libc/kernel/arch-arm -L $ANDROID_ROOT/prebuilt/linux-x86/toolchain/arm-eabi-$TOOLCHAIN_VER/lib/gcc/arm-eabi/$TOOLCHAIN_VER/interwork -L$ANDROID_ROOT/ndk/build/platforms/android-$PLATFORM_VER/arch-arm/usr/lib -L$ANDROID_ROOT/out/target/product/generic/system/lib " \<br />
CFLAGS="-fno-exceptions -Wno-multichar -mthumb -mthumb-interwork -nostdlib -lc -ldl -lm " \<br />
./configure CC=arm-eabi-gcc --host=arm-linux --disable-tftp --disable-sspi --disable-ipv6 --disable-ldaps --disable-ldap --disable-telnet --disable-pop3 --disable-ftp --without-ssl --disable-imap --disable-smtp --disable-pop3 --disable-rtsp --disable-ares --without-ca-bundle --disable-warnings --disable-manual --without-nss --enable-shared --without-zlib --without-random<br />
<br />
# openssl/zlib version<br />
#./configure CC=arm-eabi-gcc --host=arm-linux --disable-tftp --disable-sspi --disable-ipv6 --disable-ldaps --disable-ldap --disable-telnet --disable-pop3 --disable-ftp --with-ssl=$ANDROID_ROOT/external/openssl --disable-imap --disable-smtp --disable-pop3 --disable-rtsp --disable-ares --without-ca-bundle --disable-warnings --disable-manual --without-nss --enable-shared --with-zlib=$ANDROID_ROOT/external/zlib --without-random<br />
<br /></div>
<br />
Run the script in the top curl directory (where configure resides) and it should go through many tests before ultimately creating the lib/curl_config.h file.<br />
<br />
<h3>
Create NDK JNI App on Windows</h3>
Untar curltest.tar.gz under $NDKROOT. This has the Java side application already created. Or you can simply create a new Application under Eclipse which will generated the src &amp; res directories that will need to be modified. I'm not going into App generation except that you will target $NDKROOT/apps/curltest/project as your directory tree.<br />
<br />
Create library name under $NDKROOT/apps/, I used curltest.<br />
CD to curltest and create the Application.mk file.<br />
APP_PROJECT_PATH := $(call my-dir)/project<br />
APP_MODULES := curltest libcurl<br />
<br />
Once that is done, cd to project and create your standard Java AndroidManifest.xml file. If you created an App from Eclipse you'll need to modify this file. Make sure to give it INTERNET permissions. The rest depends on what's in your Java code. <br />
<br />
<div style="font-size: 12px; height: 100px; overflow: auto; width: 700px;">
<?xml version="1.0" encoding="utf-8"?><br />
<manifest xmlns:android="http://schemas.android.com/apk/res/android"<br />
android:versionCode="1"<br />
android:versionName="1.0" package="com.mtterra.curltest"><br />
<application android:icon="@drawable/icon" android:label="@string/app_name"><br />
<activity android:name=".curltest"<br />
android:label="@string/app_name"><br />
<intent-filter><br />
<action android:name="android.intent.action.MAIN" /><br />
<category android:name="android.intent.category.LAUNCHER" /><br />
</intent-filter><br />
</activity><br />
<br />
</application><br />
<uses-sdk android:minSdkVersion="5"/><br />
<uses-permission android:name="android.permission.INTERNET"/><br />
<br />
</manifest><br />
<br />
<br /></div>
<br />
<br />
Then cd to jni and create the Android.mk file. This is where the one from cURL would go. However, I didn't use it as anything more than a reference.<br />
<br />
Now for the actual JNI interfacing. There are lots of complaints here because everything needs to be specified up front and you need different packages for different hardware. Except for some dynamic registration mechanism, I'm not sure you can get away with writing to hardware AND getting independence from it. Actually, I am sure, you can't. <br />
<br />
Here is my curltest.c interface:<br />
<div style="font-size: 12px; height: 200px; overflow: auto; width: 700px;">
<br />
typedef struct pageInfo_t {<br />
char *data;<br />
int len;<br />
} pageInfo_t;<br />
<br />
static size_t<br />
HTTPData(void *buffer, size_t size, size_t nmemb, void *userData) {<br />
int len = size * nmemb;<br />
pageInfo_t *page = (pageInfo_t *)userData;<br />
<br />
<br />
if (buffer &amp;&amp; page-&gt;data &amp;&amp; (page-&gt;len + len &lt; (16 * 1024)) ) { memcpy(&amp;page-&gt;data[page-&gt;len], buffer, len);<br />
page-&gt;len += len;<br />
}<br />
return len;<br />
}<br />
<br />
//Interface function that will receive web page from Java<br />
jstring<br />
Java_com_mtterra_curltest_curltest_JNIGetWebpage( JNIEnv* env,<br />
jobject entryObject,<br />
jstring webpageJStr )<br />
{<br />
pageInfo_t page;<br />
CURL *curl;<br />
CURLcode res;<br />
char *buffer;<br />
<br />
const jbyte *webpage;<br />
webpage = (*env)-&gt;GetStringUTFChars(env, webpageJStr, NULL);<br />
if (webpage == NULL) {<br />
return NULL; /* OutOfMemoryError already thrown */<br />
}<br />
<br />
page.data = (char *)malloc(16 * 1024);<br />
page.len = 0;<br />
if (page.data)<br />
memset(page.data, 32, 16 * 1024);<br />
<br />
buffer = (char *)malloc(1024);<br />
<br />
curl = curl_easy_init();<br />
if(curl) {<br />
curl_easy_setopt(curl, CURLOPT_URL, webpage);<br />
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HTTPData);<br />
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &amp;page);<br />
<br />
res = curl_easy_perform(curl);<br />
/* always cleanup */<br />
curl_easy_cleanup(curl);<br />
(*env)-&gt;ReleaseStringUTFChars(env, webpageJStr, webpage);<br />
if(res == 0) {<br />
if (buffer) {<br />
page.data[page.len &lt; 256 ? page.len : 256] = '\0'; sprintf(buffer, "pagedata(%d): %s. done.\n", page.len, page.data); return (*env)-&gt;NewStringUTF(env, buffer);<br />
}<br />
}<br />
sprintf(buffer, "Result %d", res);<br />
return (*env)-&gt;NewStringUTF(env, buffer);<br />
} else {<br />
return (*env)-&gt;NewStringUTF(env, "Unable to init cURL");<br />
}<br />
} </div>
<br />
<br />
<br />
<br />
Finally, it's time to setup Eclipse to build. There are some assumptions here, like having a functioning Eclipse build environment. In order to get your environment over to Eclipse, you can use File-&gt;Import then select 'Existing Projects into Workspace' function.<br />
<br />
Unpack cURL into $NDKROOT/apps/curltest/jni/curl. Move the curl_config.h file created above to $NDKROOT/apps/curltest/jni/curl/lib. <br />
<br />
Finally, we create the Android.mk file in curltest/jni and build it. The Android.mk use is as follows:<br />
<br />
<div style="font-size: 12px; height: 150px; overflow: auto; width: 600px;">
LOCAL_PATH:= $(call my-dir)<br />
<br />
CFLAGS := -Wpointer-arith -Wwrite-strings -Wunused -Winline \<br />
-Wnested-externs -Wmissing-declarations -Wmissing-prototypes -Wno-long-long \<br />
-Wfloat-equal -Wno-multichar -Wsign-compare -Wno-format-nonliteral \<br />
-Wendif-labels -Wstrict-prototypes -Wdeclaration-after-statement \<br />
-Wno-system-headers -DHAVE_CONFIG_H<br />
<br />
include $(CLEAR_VARS)<br />
include $(LOCAL_PATH)/curl/lib/Makefile.inc<br />
<br />
<br />
LOCAL_SRC_FILES := $(addprefix curl/lib/,$(CSOURCES))<br />
LOCAL_CFLAGS += $(CFLAGS)<br />
LOCAL_C_INCLUDES += $(LOCAL_PATH)/curl/include/<br />
<br />
LOCAL_COPY_HEADERS_TO := libcurl<br />
LOCAL_COPY_HEADERS := $(addprefix curl/include/curl/,$(HHEADERS))<br />
<br />
LOCAL_MODULE:= libcurl<br />
<br />
include $(BUILD_STATIC_LIBRARY)<br />
<br />
# Build shared library now<br />
# curltest<br />
<br />
include $(CLEAR_VARS)<br />
<br />
LOCAL_MODULE := curltest<br />
LOCAL_SRC_FILES := curltest.c<br />
LOCAL_STATIC_LIBRARIES := libcurl<br />
LOCAL_C_INCLUDES += $(LOCAL_PATH)/curl/include<br />
include $(BUILD_SHARED_LIBRARY) </div>
<br />
Going back to your $NDKROOT you should be able to build the library with 'make APP=curltest -B V=1'. The -B means rebuilt it all and V=1 makes it verbose. <br />
<br />
After this, you should be able to build the application. When I get back to Eclipse I'll hit F5 to refresh the files as a habit although likely not necessary. Another interesting thing is that some people have indicated that you need to do an 'adb push' to the emulator so the library is there. I haven't found this is the case. Basically, if it doesn't find your library the interface name or some other part is broken and you need to find the issue.<br />
<br />
The finished result looks something like:<br />
<img src="http://mtterra.com/files/curltestpic.jpg" /><br />
<br />
<br />
Here is the <a href="http://mtterra.com/files/curltest.tar.gz">NDK project files </a> used. However, you'll have to add the curl sources to this.<br />
<br />
The Software Rogue - <a href="http://thesoftwarerogue.blogspot.com/">http://thesoftwarerogue.blogspot.com </a>
<br /><br />
<br />
<b><span style="color: blue;">QuickLogger Fitness - super quick fitness tracking</span></b><br />
<a href="https://play.google.com/store/apps/details?id=com.mtterra.quicklogger">
<img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" />
</a>
<br />Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com10tag:blogger.com,1999:blog-5954747344613270524.post-49563531174818289442010-04-29T00:34:00.000-07:002010-05-11T10:53:34.698-07:00Android Emulator and Mock Location problemAs a foray into Android development it seemed a simple task to take my previous Palm Pre map application and "port" it to this environment. Given the development environments are completely different, only little bits could actually be used. Android is Java-based. <br />
<br />
So the first step is simply getting the basic app framework going that receives GPS data. In this case, from a mock location provider that can be updated via DDMS which is the basic debugger that communicates with the emulator. I haven't touched Java in years and didn't do much with it when I did. It has the typical object oriented approach and syntax is something close to C++ so very easy to pick up. One particular irritating part of Java is the one public class/one file approach. The Java religion seems to like this aspect of the language; that of defining your organization as part of the language implementation. <br />
<br />
It didn't take long to craft the first draft and promptly run into issues. A simple one; no mock location data. Searching the web didn't net much except that I added the Google APIs along with the standard Android ones when creating the emulator image. Once I did that, I ended up with the "Not updating R.layout.main" error after updating the project with the right API set. After cleaning, building, building, searching, eventually the solution was to delete all the import libraries and re-add them.<br />
<br />
Ok, back to the mock location issue. The short solution - when you launch the image, click the "wipe user data" box. For whatever reason, there is some history that prevented location info from working. Possibly using a previous version of the emulator, not sure and at this point, don't care. After that, sending GPS locations via GPX, KML, or single fixes worked great. Google maps also started working again. I'll run through the steps in gory detail for the rest of the app soon. Hopefully, much sooner than my last post so long ago.<br />
<br />
Now, off more Java fun....Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com0tag:blogger.com,1999:blog-5954747344613270524.post-34508184016757721352009-09-23T11:05:00.000-07:002009-09-24T23:23:03.863-07:00Discover application building for Palm Pre - Part 1<div xmlns="http://www.w3.org/1999/xhtml">This is part 1 in a multipart series on coding an application for the Palm Pre. I'll start with an application that is fairly simple - a map application then expand it with a graphics overlay to display a compass heading using the canvas element. Finally, I'll add some details to flesh it out. If you are just starting out, you might want to review <a href="http://thesoftwarerogue.blogspot.com/2009/09/palm-pre-first-pass-in-debugging.html">Palm Pre - First Pass in Debugging</a>. <br />
<br />
This is a pretty basic app but we'll dig deeper as the series continues. I'll probably tie in some backend work so we can store data on the device and on the web but for now I'll keep it simple. Not everything went smoothly. The Pre is being improved just like any other brand new product so expect a bit of frustration when things aren't working they way you think. I certainly spun my wheels a bit with some fairly simple problems such as my canvas not rotating without smudging. I'll get to that in a bit. <br />
<br />
I could have used Google maps as the API but Yahoo has few restrictions and I can use it with a general application. So the first thing to do is get your Yahoo MAP API key. This is pretty easy and you can do 50,000 queries per day with it. You can get your key <a href="https://developer.apps.yahoo.com/wsregapp" target="_blank">here</a>. <br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="http://1.bp.blogspot.com/_po3KnTbGmIc/SrphG4pJj6I/AAAAAAAAABM/Qrx2QwucRP4/s1600-h/app_setup.gif" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/_po3KnTbGmIc/SrphG4pJj6I/AAAAAAAAABM/Qrx2QwucRP4/s200/app_setup.gif" /></a><br />
</div>Next, we generate an application. At this point, you ahould have already installed all the Palm development tools such as the SDK and Eclipse. To generate an application, go to File->New->Project. Select 'Mojo Application'. You should get a screen like this:<br />
<br />
Fill out the information, nothing is all that important. I used TestApp for the name but you can use anything. The rest of the information you can just leave or change as you see fit. <br />
<br />
Once the project has been created, we need the first scene. In Palm-speak, this is simply a single screen. The project creates a directory structure as follows:<br />
<br />
workspace/TestApp/app/assistants<br />
workspace/TestApp/app/views<br />
workspace/TestApp/app/stylesheets<br />
workspace/TestApp/app/images<br />
<br />
<br />
Most of the directories are self-explanatory but assistants and views warrant some comments. A view is a scene's html structure. It uses the CSS in stylesheets. If you look at the 'stylematters' sample application, you'll see how CSS can be loaded within the application. Otherwise, just store what you need in a single .css template under stylesheets and include it in the index.html. As anything, if the application is complex with lots of screens then having small, fast .css files to load instead of one big one might be an advantage but generally this won't be required. <br />
<br />
Assistants contain the javascript to implement a scene. The first scene for the application is called by the stage_controller. This is also stored in the assistants directory. Usually each scene is in its own subdirectory. I hate the number of directories and how everything spread out but Eclipse does make it easier to get to it. Unfortunately, I pretty much stay in dos and use vi with some batch files that use the command line operations and only use Eclipse occasionally.. In case someone is interested, there are a number of links at the bottom of this blog to download them. <br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="http://1.bp.blogspot.com/_po3KnTbGmIc/SrphWk2SmaI/AAAAAAAAABU/n1ZXN4Dmbts/s1600-h/basic_app.gif" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"><img border="0" src="http://1.bp.blogspot.com/_po3KnTbGmIc/SrphWk2SmaI/AAAAAAAAABU/n1ZXN4Dmbts/s320/basic_app.gif" /></a><br />
</div>Scenes are created through Eclipse or by using the command line tool, palm-generate. The tool sets up the basic structure and adds appropriate info in the sources.json file. For Eclipse, simply click on the phone-looking icon and select New Mojo Scene or use the menu File->Open other->Mojo scene. The command line would look like 'palm-generate -t new_scene -p "name=compass" compass. Now go ahead and launch the Palm Emulator. Sometimes there are problems if Eclipse is already running so close it and run the Emulator first if you do have problems. Run the application and if all went well, you should see this. In this example, I used the name compass so the .js file will be named compass-assistant.js and the view will be named compass-scene.html. <br />
<br />
Now we can integrate the application code. The stage controller sets up the first scene so simply adding a call to pushScene('compass') will kick off the program. The rest of the program will be stored in compass-assistant.js. The html code under app/view/testapp/compass-scene.html is pretty simple.<br />
<br />
<div id="main"><br />
<canvas id="canvas" class="CompassPtr" height=100> </canvas><br />
</div><br />
<div id="map" width=320 height=450></div><br />
<br />
Basically, there is just one view with a canvas used for the future compass pointer. The .css file should have one entry for the compass pointer. <br />
<br />
.CompassPtr { <br />
position:fixed; <br />
background:transparent; <br />
left:225px ; <br />
top:25px; <br />
width:50px; <br />
height:50px; <br />
z-index:5;<br />
} <br />
<br />
A map wouldn't be very interesting if we didn't leverage some of the services that the Pre provides. An obvious one is the GPS service. So here I'll setup the service, put a marker where we are, and pan as we move. <br />
<br />
// Setup listener for gps events<br />
this.trackingHandle = this.controller.serviceRequest("palm://com.palm.location", <br />
{ method: "startTracking",<br />
parameters: {subscribe: true},<br />
onSuccess: this.trackingSuccess.bind(this),<br />
onFailure: this.trackingFailure.bind(this)<br />
});<br />
<br />
This is basically requesting that every second the trackingSuccess should be called with our current location information. When the first location is generated, that's when the marker is setup. <br />
<br />
if (this.firstTrack) {<br />
this.myloc = new YMarker(pt);<br />
this.mapCtrl.drawZoomAndCenter(pt, 3);<br />
this.mapCtrl.addOverlay(this.myloc);<br />
this.firstTrack = false;<br />
} <br />
First, the marker is created, then take our first location point and display it, and finally add the marker to the map. This is almost done. Each time our location updates, we need to update the map. Redrawing the map is expensive so there is another good function to allow panning.<br />
<br />
this.myloc.setYGeoPoint(pt);<br />
this.mapCtrl.panToLatLon(pt);<br />
<br />
This changes the marker position then scrolls the map. The map won't pan unless it moves significantly. You can get the project files <a href="http://mtterra.com/files/testapp.zip">here</a>. Good luck! Up next will be adding the compass information and more. <br />
<div class="separator" style="clear: both; text-align: center;"><a href="http://3.bp.blogspot.com/_po3KnTbGmIc/SrphoSfhRxI/AAAAAAAAABc/5wDqKXDN7bs/s1600-h/part1_app.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/_po3KnTbGmIc/SrphoSfhRxI/AAAAAAAAABc/5wDqKXDN7bs/s400/part1_app.gif" /></a><br />
</div><br />
Project Files: <a href="http://mtterra.com/files/testapp.zip">TestApp.zip</a><br />
<a href="http://mtterra.com/files/dbg.bat">dbg.bat</a> - stick in app/assistants to use command line.<br />
<br />
The Software Rogue - <a href="http://thesoftwarerogue.blogspot.com/">http://thesoftwarerogue.blogspot.com </a><br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div class="zemanta-pixie"><img alt="" class="zemanta-pixie-img" src="http://img.zemanta.com/pixy.gif?x-id=60c0d1f9-136e-86b1-961f-da56480e4739" /><br />
</div></div>Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com0tag:blogger.com,1999:blog-5954747344613270524.post-5273915205529850772009-09-16T02:51:00.000-07:002009-09-24T18:26:55.481-07:00Palm Pre - First Pass in DebuggingThe Palm® Pre was introduced a couple of months ago with much fanfare and touted easy development via HTML, Javascript, and CSS. This, of course, meant that the number of potential developers was much higher than for something like the iPhone. This piqued my interest as well as it being the underdog to the already established iPhone. I had a few questions at this point. What kind of development environment did it have? As a fellow Rogue, the first step is to head to the developer.palm.com page and get the SDK. The product is just too new to get any kind of reliable answers on the ease of development for this platform. <br />
<br />
There are a few tools needed before doing much. I don’t use IDE’s in general but I did download Eclipse. It simply made life easier in launching my test application. (I decided to use Windows rather than Linux since my Linux version is pretty old on my laptop and I don’t want to change it just for this.) The best resource is simply to go to developer.palm.com and grab the SDK and all the tools/plugins, etc. You’ll need ssh - I use putty which you can get from <a href="http://www.chiark.greenend.org.uk/%7Esgtatham/putty">www.chiark.greenend.org.uk</a>. Set the host to 127.0.0.1, port to 5522 and a dos command window will popup. In here you login as root with no password (just hit enter). The tool <i>novacom</i> can be used as well to issue commands. This comes with the SDK.<br />
<br />
Log into the emulator - login name: root, no password. There are a couple of tools to use; the Palm Debugger - a gdb-style debugger creatively called ‘debug’ and the debug log output. Palm’s developer site has details on the usage of the debug tool. The file ‘/var/log/messages’ contains the debug output or simply use tail -f /var/log/messages to see them scroll by as it is running. Log output is highly valuable but since this is Javascript, you may not get any output if there is an error in the code.<br />
<br />
Using the Palm Debugger was less than satisfying. Getting output for simple errors with no information on finding those errors is almost worse than nothing at all. Almost. For instance, leaving an else off of an if statement gets you this (at least in my case):<br />
<blockquote>Uncaught: SyntaxError: Unexpected token else - (empty stack)<br />
</blockquote>This is a clue as to what’s happening. In a small program, it would be trivial but in a larger program, it’s almost no help. However, when the debugger is running, the output in /var/log/messages does contain the necessary info the find the error.<br />
<blockquote><br />
2009-09-15T06:02:54.138411Z [3581] qemux86 user.err LunaSysMgr: {LunaSysMgrJS} com.yourapp.compass: Uncaught SyntaxError: Unexpected token else, file:///var/usr/palm/applications/com.yourapp.compass/app/assistants/compass-assistant.js:29 <br />
<br />
</blockquote>I certainly hope Palm puts some development into the debugger. Tab completion, easy breakpoints rather than full path, and output that is complete would be a good start. Your application source resides in /var/usr/palm/applications. You’ll need to get a full path to the script then line number to issue a breakpoint. Some commands like <span style="font-style: italic;">list</span> won’t put out any info until you hit a breakpoint. There aren’t many commands but enough to do the job. You’ll probably find yourself relying on logging rather than the debugger as I did.<br />
<br />
The Inspector requires a few flags to be set and is rather finicky to use. To set it up, first run the emulator then Eclipse and open your project. Under the run menu, select Debug Configurations and make sure ‘Inspectable’ is selected. I would enable 'Mojo Debugging' as well. Alternatively, you can run the command line. <br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="http://2.bp.blogspot.com/_po3KnTbGmIc/SrOoEwxlsDI/AAAAAAAAAAU/DAd6DZK_elc/s1600-h/ScrnShot_04+Sep.+18,+2009+08.32.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/_po3KnTbGmIc/SrOoEwxlsDI/AAAAAAAAAAU/DAd6DZK_elc/s400/ScrnShot_04+Sep.+18,+2009+08.32.gif" /></a><br />
</div><br />
The -i in the command turns on the <i>Inspectable </i>flag while the rest of the command turns on Mojo debugging. Save and run the debug configuration. At this point, you can run the Inspector. Here is a sample of what it looks like. <br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="http://3.bp.blogspot.com/_po3KnTbGmIc/SrOolqKX0eI/AAAAAAAAAAk/h-GZrxyOp-s/s1600-h/ScrnShot_01+Sep.+18,+2009+08.25.gif" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" src="http://3.bp.blogspot.com/_po3KnTbGmIc/SrOolqKX0eI/AAAAAAAAAAk/h-GZrxyOp-s/s320/ScrnShot_01+Sep.+18,+2009+08.25.gif" /></a><br />
</div><br />
The emulator reset a number of times while trying to use the Inspector. There are notes on the Palm developer site about it crashing while trying to use it. You can modify elements as well if you don't mind the occasional detrimental effects forcing a relaunch. I found between the crashing and long delays in views that it just wasn't all that useful.<br />
<br />
In summary, the Pre looks interesting as a platform and might even be easy to develop on if the tools were better. For now, if you can do your development outside of the device, you’ll be much better off. The documentation is light and spread out so it does take a bit of effort to get going on it. This gets easier pretty quickly. Of course, the <a href="http://www.amazon.com/gp/product/0596155255?ie=UTF8&tag=thesofrog-20&linkCode=as2&camp=1789&creative=9325&creativeASIN=0596155255">Palm webOS</a><img alt="" border="0" class=" junlyregftvdlbeevpyn junlyregftvdlbeevpyn junlyregftvdlbeevpyn junlyregftvdlbeevpyn junlyregftvdlbeevpyn junlyregftvdlbeevpyn junlyregftvdlbeevpyn junlyregftvdlbeevpyn" height="1" src="http://www.assoc-amazon.com/e/ir?t=thesofrog-20&l=as2&o=1&a=0596155255" style="border: medium none ! important; margin: 0px ! important;" width="1" /> book offers a good start. It’s certainly worth getting to help bootstrap applications although not required by any means.<br />
<br />
The Software Rogue - <a href="http://thesoftwarerogue.blogspot.com/">http://thesoftwarerogue.blogspot.com </a>Bruce Smithhttp://www.blogger.com/profile/06792466739598536586noreply@blogger.com0