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 4, 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

Golang setup PATH

Quite recently we startet in the company to use and write some Go programs. I love Go. It's easy to learn, read and modify. One of the m...