Monday, March 31, 2014

iOSAPP - ShoppingList - Fast & Simple V:1.0.0

ShoppingList - Fast & Simple


Very simple shopping list that is designed to support fast inserts, checks and deletes. 

Build your own suggestion list! Shopping list saves all your inputs so that you don't need to type them twice 

Features: 
- Fast insert due to single view and suggestion list 
- Create and manage your own suggestions 
- Swipe right to check an item in the list 
- Swipe left to delete it 
- Synchronised on all your devices over the iCloud

Wednesday, March 12, 2014

iOSAPP - MoneyBunny V:1.0.0

Today finally my first APP got approved by the Apple APP Store.

MoneyBunny


The MoneyBunny helps you to track all your daily expenses, in order to know where your money went at the end of a month. 

Set yourself a budget on things you buy regularly and track them over a longer time period. 

MoneyBunny is a powerful, but simple tool to keep an eye on your expenses.

Thank you all for downloading it!

Friday, February 21, 2014

iOS - Asynchronous image loading

There are some tasks in APP programming that occur in almost every APP and then usually more then once. One of these tasks is image loading, which means not images that are contained in the assets of the APP, but coming from external sources.

We have some APPs in our stores that load images for being displayed in UIWebViews others for UIImageViews. The latter can be a bit tricky when they are contained in a UITableViewCell because then they need to be loaded in a way that doesn't intercept the scrolling of the table. Furthermore if the users is scrolling from the top to the bottom of the table and back u'll probably want to load the images just once and have them cached for their subsequent displays.

In short we are talking here about asynchronous loading and caching.

  • Asynchronous loading is not a big problem, but it usually needs some coding effort, but done once for your cell u'll have the smooth scrolling of your table back
  • Caching in short can be just a simple Dictionary with URLs and images (in reality its a bit more complex, but the general idea is that.
Fortunately we as iOS developers are lucky as this functionality exists as a custom library on github.

The SDWebImage-library does a lot more things that are worth to explore, but I'll focus just on the asynchronous loading possibility of images in this post. Lets make it short and show directly some code


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    Module *module =self.model.modules[indexPath.row];
    
    static NSString *CellIdentifier = @"ModuleTableCell";
    ModuleTableCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    
    [..]

    if (module.imageUrl) {
        [cell.coverImageIndicatorView startAnimating];
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@/rest%@",kCasServiceServer,kCasServicePath,module.imageUrl]];
        [cell.coverImageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"loadingCover-Module"] options:0
                                    progress:^(NSUInteger receivedSize, long long expectedSize) {
                                        // progression tracking code
                                    }
                                   completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
                                       [cell.coverImageIndicatorView stopAnimating];
                                   }];
    } else {
        [cell.coverImageView setImage:[UIImage imageNamed:@"noCover-Module"]];
    }
    return cell;
}


It's not the complete method body, but the interesting parts. I've even put the class names that I recently used in one of my projects, so don't be confused; "Module" and "ModuleTableCell" are custom classes.

So what I did here... The "setImageWithURL" method comes from the SDWebImage framework. The url is used automatically for caching the image and during the load it will show the placeholder image. Furthermore during the loading you'll have the possibility to show a progress indicator which can be removed on completing.

Actually in most cases you're done now, however most images that I had to display need to provide an authentication token in the header of the request. That's a problem, because the system does not offer this functionality. SDWebImage has another function where u can manually download and cache an image which allows to set headers, but it is not as comfortable as this single method call.

To achieve this I was using a custom fork that allowed me to add custom headers to the request. Fortunately they added this feature in version 3.5 of SDWebImage which made my fork obsolete. Actually u can define in a static place (or wherever u have access to your ticket creation system) a method like this.

    // This code adds the header to a specific request, so the ticket is validated
    SDWebImageManager.sharedManager.imageDownloader.headersFilter  = ^NSDictionary *(NSURL *url, NSDictionary *headers)
    {
        NSMutableDictionary *mutableHeaders = [headers mutableCopy];
        [mutableHeaders removeObjectForKey:@"Authorization"];
        NSString *pt = [ProxyTicketHelper.sharedHelper proxyTicket:url];
        DLog(@"Add Authorization sapped header (%lu) for URL %@",(unsigned long)[mutableHeaders count], [url description]);
        [mutableHeaders setValue:[NSString stringWithFormat:@"PTAUTH %@", pt] forKey:@"Authorization"];
        
        return mutableHeaders;
    };

This block is added just once, but executed on every request just a few instructions before the real request is sent. That's crucial in my case because the tickets are one-time-tickets and need to reach the server in the right order.

The server in my case evaluates the ticket, obtains the user information and sends me the image if I'm allowed to get it. That's it!

Tuesday, February 04, 2014

OSX - Safari Push Notifications

Recently Apple has expanded the push notification service to its computers, as the OSX users among you may have noticed. After some discussions we tried to implement this on our server, so for our website too.

Unfortunately Apple provides just some php samples files, which is of course a help, but as our server is written in Java it was a bit complex to get it running. I'll provide in this post the main parts of the source needed to get it.

This sample code uses the default ZIP functions of java and the signing functions provided by BouncyCastle. Most parts of the code are kind of obvious and therefore omitted (any case, if you need them just drop me a message)

Let's start with the Zipper:
private class Zipper {

private final ByteArrayOutputStream bos;
private final ZipOutputStream zipfile;
private final Map manifest;

public Zipper() {
bos = new ByteArrayOutputStream();
zipfile = new ZipOutputStream(bos);
manifest = new HashMap();
}

public byte[] getManifest() throws UnsupportedEncodingException {
Set keys = manifest.keySet();
int i = 0;
StringBuilder sb = new StringBuilder();
for (String key : keys) {
sb.append(i <= 0 ? "{" : ",");
sb.append("\"" + key + "\": \"" + manifest.get(key) + "\"");
i++;
}
sb.append("}");
return sb.toString().getBytes("UTF8");
}

public byte[] finalizeZip() throws IOException {
zipfile.finish();
bos.flush();
zipfile.close();
manifest.clear();
return bos.toByteArray();
}

public void addFileToZip(String path, String filename, byte[] file, boolean addToManifest) throws IOException {
if (file != null) {
String completeFilename = path.length() > 0 ? path + "/" + filename : filename;
ZipEntry zipEntry = new ZipEntry(completeFilename);
CRC32 crc = new CRC32();
crc.update(file);
zipEntry.setCrc(crc.getValue());
zipfile.putNextEntry(zipEntry);
zipfile.write(file, 0, file.length);
zipfile.flush();
zipfile.closeEntry();

if (addToManifest) {
try {
manifest.put(completeFilename, SHAsum(file));
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} else {
LOGGER.error("File " + filename + " was null!");
}
}

}

It is done as a private class within the class that actually returns the ZIP to the client. It does the zipping and immediately calculates the checksums that are needed for the manifest. For completeness here the simple Checksum creation methods

public static String SHAsum(byte[] convertme) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return byteArray2Hex(md.digest(convertme));
}

private static String byteArray2Hex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String formattedstring = formatter.toString();
formatter.close();
return formattedstring;

}

This manifest will be created with the method "getManifest()" in the zipper class and signed using the next class which is again private and called PKCS7Encrypter

private class PKCS7Encrypter {

private final byte[] _store;
private final String _storepass;

public PKCS7Encrypter(byte[] store, String storepass) {
_store = store;
_storepass = storepass;
}

private KeyStore getKeystore() throws KeyStoreException, NoSuchAlgorithmException, CertificateException,
IOException {
if (_store == null) {
LOGGER.error("Could not find store file (.p12)");
return null;
}
// First load the keystore object by providing the p12 file path
KeyStore clientStore = KeyStore.getInstance("PKCS12");
// replace testPass with the p12 password/pin
clientStore.load(new ByteArrayInputStream(_store), _storepass.toCharArray());
return clientStore;
}

private X509CertificateHolder getCert(KeyStore keystore, String aliaz) throws GeneralSecurityException,
IOException {
java.security.cert.Certificate c = keystore.getCertificate(aliaz);
return new X509CertificateHolder(c.getEncoded());
}

private PrivateKey getPrivateKey(KeyStore keystore, String aliaz) throws GeneralSecurityException, IOException {
return (PrivateKey) keystore.getKey(aliaz, _storepass.toCharArray());
}

public byte[] sign(byte[] dataToSign) throws IOException, GeneralSecurityException, OperatorCreationException,
CMSException {
KeyStore clientStore = getKeystore();
if (clientStore == null) {
return null;
}
Enumeration aliases = clientStore.aliases();
String aliaz = "";
while (aliases.hasMoreElements()) {
aliaz = aliases.nextElement();
if (clientStore.isKeyEntry(aliaz)) {
break;
}
}

CMSTypedData msg = new CMSProcessableByteArray(dataToSign); // Data to sign

X509CertificateHolder x509Certificate = getCert(clientStore, aliaz);
List certList = new ArrayList();
certList.add(x509Certificate); // Adding the X509 Certificate

Store certs = new JcaCertStore(certList);

CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
// Initializing the the BC's Signer
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(
getPrivateKey(clientStore, aliaz));

gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder()
.setProvider("BC").build()).build(sha1Signer, x509Certificate));
// adding the certificate
gen.addCertificates(certs);
// Getting the signed data
CMSSignedData sigData = gen.generate(msg, false);
return sigData.getEncoded();
}


}

This class needs to be fed with the .p12-file that you can extract from the Mac OSX keystore. It shouldn't be just the private key, but the certificate with private key; and the password used to export it.

With this methods (and some simple methods to create the rest of the .pushPackage file you'll now be able to create the ZIP and send it as a response

// Create the ZIP file
Zipper zip = new Zipper();
zip.addFileToZip("icon.iconset", "icon_16x16.png", getResource("images/icons/icon_16x16.png"));
zip.addFileToZip("icon.iconset", "icon_16x16@2x.png", getResource("images/icons/icon_16x16@2x.png"));
zip.addFileToZip("icon.iconset", "icon_32x32.png", getResource("images/icons/icon_32x32.png"));
zip.addFileToZip("icon.iconset", "icon_32x32@2x.png", getResource("images/icons/icon_32x32@2x.png"));
zip.addFileToZip("icon.iconset", "icon_128x128.png", getResource("images/icons/icon_128x128.png"));
zip.addFileToZip("icon.iconset", "icon_128x128@2x.png", getResource("images/icons/icon_128x128@2x.png"));

zip.addFileToZip("", "website.json", getWebsiteJson(""));
byte[] manifest = zip.getManifest();
zip.addFileToZip("", "manifest.json", manifest, false);
try {
PKCS7Encrypter encrypter = new PKCS7Encrypter(getResource(STOREPATH), STOREPASS);
zip.addFileToZip("", "signature", encrypter.sign(manifest));
} catch (Exception e) {
LOGGER.error("Signature Error: " + e.getLocalizedMessage());
e.printStackTrace();
}


getBinaryFile(zip.finalizeZip(), "MyPage.pushpackage", response);

One last thing is missing! Java by default does not accept keys in the length we'll need them for this usage. So you'll end up in getting "java.security.InvalidKeyException:illegal Key Size" exceptions.

Fortunately Oracle provides a solution to this. You'll need to download the "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 6" zip(be aware of taking the right ones for your JDK). Behind this download you'll find 2 files which are "local_policy.jar" and "US_export_policy.jar". These need to end up in your "$JAVA_HOME/jre/lib/security" folder. Both of them will already be in this folder and need to be replaced (Save the original files before doing that)

BTW When you test all this implement the logging REST callback provided by Apple.
@SuppressWarnings("unchecked")
@RequestMapping(method = RequestMethod.POST, value = "/log")
public void logErrors(@PathVariable("version") String version, @RequestBody Map json,
HttpServletRequest request, HttpServletResponse response) throws IOException {
if (LOGGER.isDebugEnabled())
LOGGER.debug("logErrors()");

Object logs = json.get("logs");
if (logs != null) {
for (String logEntry : (ArrayList) logs) {
LOGGER.error("Safari Push messages error: " + logEntry);
}
}

}
This will save you hours of searching for errors

Tuesday, September 24, 2013

CAS: Sample Client for Java

As the client for Java is missing at the jasig homepage I'll provide my solution here. Of course it has the "Domain" addition that I described in my earlier posts, but it is simple and quite obvious how to remove it.

So, here it is:

package cas;

import java.io.IOException;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;

public final class Client {
private static final Logger LOG = Logger.getLogger(Client.class.getName());

private Client() {
// static-only access
}

public static String getTicket(final String server, final String domain, final String username,
final String password, final String service) {
notNull(server, "server must not be null");
notNull(domain, "domain must not be null");
notNull(username, "username must not be null");
notNull(password, "password must not be null");
notNull(service, "service must not be null");

return getServiceTicket(server, getTicketGrantingTicket(server, domain, username, password), service);
}

private static String getServiceTicket(final String server, final String ticketGrantingTicket, final String service) {
if (ticketGrantingTicket == null)
return null;
final HttpClient client = new HttpClient();
final PostMethod post = new PostMethod(server + "/" + ticketGrantingTicket);
post.setRequestBody(new NameValuePair[] { new NameValuePair("service", service) });
try {
client.executeMethod(post);
final String response = post.getResponseBodyAsString();
switch (post.getStatusCode()) {
case 200:
return response;
default:
LOG.warning("Invalid response code (" + post.getStatusCode() + ") from CAS server!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
}
catch (final IOException e) {
LOG.warning(e.getMessage());
}
finally {
post.releaseConnection();
}
return null;
}

private static String getTicketGrantingTicket(final String server, final String domain, final String username,
final String password) {
final HttpClient client = new HttpClient();
final PostMethod post = new PostMethod(server);
post.setRequestBody(new NameValuePair[] { new NameValuePair("domain", domain),
new NameValuePair("username", username), new NameValuePair("password", password) });
try {
client.executeMethod(post);
final String response = post.getResponseBodyAsString();
switch (post.getStatusCode()) {
case 201: {
final Matcher matcher = Pattern.compile(".*action=\".*/(.*?)\".*").matcher(response);

if (matcher.matches())
return matcher.group(1);

LOG.warning("Successful ticket granting request, but no ticket found!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
default:
LOG.warning("Invalid response code (" + post.getStatusCode() + ") from CAS server!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
}
catch (final IOException e) {
LOG.warning(e.getMessage());
}
finally {
post.releaseConnection();
}
return null;
}

private static void notNull(final Object object, final String message) {
if (object == null)
throw new IllegalArgumentException(message);
}

public static void main(final String[] args) {
final String server = "https://login.myServer.org/v1/tickets";
final String domain = "www.myserver.org";
final String username = "JohnDoe";
final String password = "mySecret";

final String service = "https://www.myServiceServer.org/";

LOG.info(getTicket(server, domain, username, password, service));
}
}

btw: I have implemented the same procedure for iOS and put it on GitHub

Wednesday, September 18, 2013

Install CocoaPods on OSX-Mavericks (DP8)

Today I tried to upgrade CocoaPods in order to prepare my APPs to the upcoming changes in iOS7. Unfortunately I got an error

ERROR:  Error installing cocoapods:
ERROR: Failed to build gem native extension.


    /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby extconf.rb

I'm currently using "xCode5 GM" which is using the "OSX 10.8 SDK" which apparently is delivered without ruby version 2.0.

The solution was to download "xCode5 DP6" (containing the SDK 10.9), switch the command line tools to the version supplied by this release ("xcode-select") and run the following commands as a shell script

sys_rb_usr=/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr
sdk_rb_usr=`xcode-select -p`/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr

sudo cp -r $sdk_rb_usr/include $sys_rb_usr/include

And then update CocoaPods.

Wednesday, July 03, 2013

TestflightSDK - Beta testing for iOS and Android

We now introduced a new Testing system for our APPs in our company: Testflight

This system allows us to distribute our BETAs and RCs to our dedicated testers without bothering them any time we have a new version to come to us, connect the device and so on.

Beside this very useful function we can embed the TestFlightSDK directly in our APPs to monitor how the user is actually moving in the APP (right now only for iOS, but Android is coming soon)

Install the SDK
That's not very tricky... One way would be to download it and add it to your xCode, or the way I prefer is using Cocoapods to automatically inject it to my project.

As we only need it during development time we don't want to add the "Testflight.h" header in every single class where we use the monitoring. So we inject it using the "", and not in any case, but only during development

#ifdef __OBJC__
    #import
    #import
    #ifdef TESTFLIGHT
        #import
    #endif
#endif

Now we can enable it by going to the targets build settings and add a "-DTESTFLIGHT" to the "other C Flags" in the section "Apple LLVM compiler 4.2 - Language"

Activate it
That's quite simple too. Just go to your AppDelegate and add the following lines

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#ifdef TESTFLIGHT
    // !!!: Use the next line only during beta
    [TestFlight setDeviceIdentifier:[[[UIDevice currentDevice] identifierForVendor] UUIDString]];
    [TestFlight takeOff:@""];
#endif
    return YES;
}

And the basic features are already activated.

You see we are again using the makro to remove TestFlightSDK on release time

Checkpoints
Nothing simpler and useful than that. Basically with this function you can check if users are passing points in the code. Set a Checkpoint as follows

#ifdef TESTFLIGHT
    [TestFlight passCheckpoint:@""];
#endif

In your TestFlight console you'll get a notification whenever a user passes this point in the execution

Feedback
Nice feature, even though the standard form is not usable in most cases. Fortunately they offer the possibility to use a custom form and just send the text to them (or better to your page on their website).

There are still a couple of interesting features in this SDK like questionnaires and remote logging that are worth to explore, and as soon as I had the time to do that I'll post them here.

Update March 2014:
TestFlight has been bought by Apple, and one of the first things they did was to announce that the Android support will be terminated by march 21th :-(