Wednesday, July 23, 2014

iOSAPP - MoneyBunny V:1.2.0


MoneyBunny


This morning Apple released version MoneyBunny 1.2.0. With this there is finally a dedicated version for the iPad together with a lot of smaller bugfixes.

Enjoy

Friday, May 30, 2014

UITableViewHeader done right

There are basically three possibilities to create headers in UITableViews
  1. Create them in the Code
  2. Prepare them as UITableViewCell in your storyboard and use them in the code
  3. Put them in separate .xib files and register these files as prototypes for headers or footers
(More details can be found here)

1) Create them in Code means that you'll need to define a UIView class and add subviews to that until you'll have the header you wanna see. If you don't use storyboard and don't want to use any additional .xib files this might be the solution. However this means that you'll need to define manually all constraints which might be quite painful if your header is complex in structure

2) The second method looks quite simple and straight forward (Described here more in detail), however it has some drawbacks. Actually in this case you're returning a Cell that you dequeued from the pool of cells the table holds internally. From iOS 7.1 on you'll get a warning in the log that you're accessing a cell without index path. Furthermore there is another issue causing this cells to disappear on partial reload of sections.
Obviously that's not acceptable in productive code, so the solution can be to return the "cell.contentView" instead of the cell itself. This is working and the results where really promising until I did a "longpress" on such a header. This action will cause the header to send a message to its cell saying that the gesture recognizer should begin to work. As the cell is not there anymore this will lead to a crash of the whole APP (BAD_ACCESS_ERR)
To fix this you'll need to remove all gesture recognizers from the contentView before returning it as header view as this stackoverflow answer points out.

3) This is actually the solution if you want to use xcodes graphical interface builder tools and the process is actually quite simple.
First create a class that inherits from UITableViewHeaderFooterView, then create a .xib file containing this view (I usually put the TableViewController as file owner ) as class with the complex header fields you need.
Next open your TableViewController and register this .xib as prototype for the header.
- (void)viewDidLoad {
    [super viewDidLoad];
    UINib *sectionHeaderNib = [UINib nibWithNibName:@"SectionHeaderView" bundle:nil];
    [self.tableView registerNib:sectionHeaderNib forHeaderFooterViewReuseIdentifier:SectionHeaderViewIdentifier];
}
In the viewForHeaderInSection method you can now dequeue it and use it
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

    SectionHeaderView *sectionHeaderView = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:SectionHeaderViewIdentifier];
    [..Fill the view with real values..]
    return sectionHeaderView;
}
For more details to this last method watch the complete sample program

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