/*
* Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.scvngr.levelup.core.ui.view;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.scvngr.levelup.core.annotation.LevelUpApi;
import com.scvngr.levelup.core.annotation.SlowOperation;
import com.scvngr.levelup.core.annotation.VisibleForTesting;
import com.scvngr.levelup.core.annotation.VisibleForTesting.Visibility;
import com.scvngr.levelup.core.ui.view.LevelUpQrCodeGenerator.LevelUpQrCodeImage;
import com.scvngr.levelup.core.ui.view.PendingImage.LoadCancelable;
import com.scvngr.levelup.core.ui.view.PendingImage.OnImageLoaded;
import com.scvngr.levelup.core.util.CryptographicHashUtil;
import com.scvngr.levelup.core.util.CryptographicHashUtil.Algorithms;
import java.util.HashMap;
/**
* Load a LevelUp payment QR code. Extend this to implement the asynchronous loading of QR codes. As
* this class inherently interacts with multiple threads, thread requirements are noted on the
* individual methods.
*/
@LevelUpApi(contract = LevelUpApi.Contract.PUBLIC)
public abstract class LevelUpCodeLoader implements LoadCancelable {
/**
* A map of load keys to the callbacks that need to be called when the image ready.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* protected */final HashMap<String, OnImageLoaded<LevelUpQrCodeImage>> mLoaderCallbacks =
new HashMap<String, PendingImage.OnImageLoaded<LevelUpQrCodeImage>>();
/* protected */final HashMap<String, PendingImage<LevelUpQrCodeImage>> mPendingImages =
new HashMap<String, PendingImage<LevelUpQrCodeImage>>();
/**
* The cache that will store the code once it's been generated. This has a one-to-one mapping
* with the cached data (it fully represents it) so it will never need to be invalidated.
*/
@NonNull
private final LevelUpCodeCache mCodeCache;
/**
* The generator/renderer of the QR code itself. This will be called on a background thread.
*/
@NonNull
private final LevelUpQrCodeGenerator mQrCodeGenerator;
/**
* @param qrCodeGenerator the QR code generator to render the codes.
* @param codeCache the cache to use for this code loader.
*/
public LevelUpCodeLoader(@NonNull final LevelUpQrCodeGenerator qrCodeGenerator,
@NonNull final LevelUpCodeCache codeCache) {
mQrCodeGenerator = qrCodeGenerator;
mCodeCache = codeCache;
}
@Override
public final void cancelLoad(@NonNull final String loadKey) {
unregisterOnImageLoadedCallback(loadKey);
onCancelLoad(loadKey);
}
/**
* Cancel all loads. To cancel an individual load, call {@link #cancelLoad(String)} or
* {@link PendingImage#cancelLoad()}. This must be called on the main thread.
*/
public final void cancelLoads() {
mLoaderCallbacks.clear();
onCancelLoads();
}
/**
* Gets a minimally-small bitmap from the cache whose contents encode {@code qrCodeContents}.
* This returns a {@link PendingImage}, which will either contain the resulting image if the
* code has been cached or will be loaded eventually and the {@code onImageLoaded} callback will
* be called. This must be called on the main thread.
*
* @param qrCodeContents the data to display to the user. This is the raw string to encode in
* the QR code.
* @param onImageLoaded callback that gets called when the image is loaded. This will always be
* called on the UI thread and is called even if this class returns a cached result.
* @return a minimally-small bitmap representing the given code data.
*/
@NonNull
public final PendingImage<LevelUpQrCodeImage> getLevelUpCode(
@NonNull final String qrCodeContents,
@Nullable final OnImageLoaded<LevelUpQrCodeImage> onImageLoaded) {
if (Looper.getMainLooper() != Looper.myLooper()) {
throw new AssertionError("Must be called from the main thread.");
}
final String key = getKey(qrCodeContents);
final PendingImage<LevelUpQrCodeImage> pendingImage =
new PendingImage<LevelUpQrCodeImage>(this, key);
final LevelUpQrCodeImage code = mCodeCache.getCode(key);
if (null != code) {
pendingImage.setImage(code);
if (null != onImageLoaded) {
onImageLoaded.onImageLoaded(key, code);
}
} else {
mPendingImages.put(key, pendingImage);
startLoadInBackground(qrCodeContents, key, onImageLoaded);
}
return pendingImage;
}
/**
* Generate and cache the code image. This is the same as
* {@link #getLevelUpCode(String, com.scvngr.levelup.core.ui.view.PendingImage.OnImageLoaded)},
* but does not return a result to the user. This can be used for pre-caching codes. This must
* be called on the main thread.
*
* @param codeData the data to display to the user.
*/
public final void loadLevelUpCode(@NonNull final String codeData) {
if (Looper.getMainLooper() != Looper.myLooper()) {
throw new AssertionError("Must be called from the main thread.");
}
startLoadInBackground(codeData, getKey(codeData), null);
}
/**
* Dispatches calls to a previously registered {@link OnImageLoaded}. The {@link OnImageLoaded}
* will be unregistered. This must be called after loading a QR code in the background. This
* must be called on the main thread.
*
* @param key the key under which the QR code is tracked.
* @param image the image of the QR code.
* @return true if the callback was called.
*/
protected final boolean dispatchOnImageLoaded(@NonNull final String key,
@NonNull final LevelUpQrCodeImage image) {
final OnImageLoaded<LevelUpQrCodeImage> imageLoaded = mLoaderCallbacks.get(key);
final PendingImage<LevelUpQrCodeImage> pendingImage = mPendingImages.get(key);
if (null != pendingImage) {
if (!pendingImage.isLoaded()) {
pendingImage.setImage(image);
}
mPendingImages.remove(key);
}
boolean callbackCalled = false;
if (null != imageLoaded) {
imageLoaded.onImageLoaded(key, image);
unregisterOnImageLoadedCallback(key);
callbackCalled = true;
}
return callbackCalled;
}
/**
* Generates the QR code using the supplied generator and caches the result. This should be
* called from a worker thread.
*
* @param key the key under which this QR code is tracked.
* @param qrCodeContents the contents of the QR code.
* @return the generated image.
*/
@NonNull
@SlowOperation
protected final LevelUpQrCodeImage generateQrCode(@NonNull final String key,
@NonNull final String qrCodeContents) {
final LevelUpQrCodeImage result = mQrCodeGenerator.generateLevelUpQrCode(qrCodeContents);
mCodeCache.putCode(key, result);
return result;
}
/**
* @return the code cache.
*/
@NonNull
protected final LevelUpCodeCache getCodeCache() {
return mCodeCache;
}
/**
* Creates a key that's unique to the given contents for use with all image load tracking.
*
* @param qrCodeContents the contents to create a key for.
* @return a unique key for the given contents.
*/
@NonNull
protected final String getKey(@NonNull final String qrCodeContents) {
return CryptographicHashUtil.getHexHash(qrCodeContents, Algorithms.SHA1);
}
/**
* @return the QR code generator.
*/
@NonNull
protected final LevelUpQrCodeGenerator getQrCodeGenerator() {
return mQrCodeGenerator;
}
/**
* Implement this to handle any load cancellation.
*
* @param loadKey the key under which the QR code is tracked.
*/
protected abstract void onCancelLoad(@NonNull String loadKey);
/**
* Cancel all loads. Implement this to stop any pending loads.
*/
protected abstract void onCancelLoads();
/**
* <p>
* Implement this to start the loading of the given QR code into an image in the background. If
* an existing request for the same content has been made, the second request will be dropped
* unless the previous request has been cancelled. This must be called on the main thread.
* </p>
* <p>
* Subclasses shouldn't call this method directly, instead call
* {@link #startLoadInBackground(String, String,
* com.scvngr.levelup.core.ui.view.PendingImage.OnImageLoaded)} )}.
* </p>
*
* @param qrCodeContents the contents to render into a QR code.
* @param key the key under which the QR code is tracked.
* @param onImageLoaded called when the image has been loaded.
*/
protected abstract void onStartLoadInBackground(@NonNull final String qrCodeContents,
@NonNull String key, @Nullable OnImageLoaded<LevelUpQrCodeImage> onImageLoaded);
/**
* Register an {@link OnImageLoaded} callback with the given key. This must be called on the
* main thread.
*
* @param key the key under which the image is tracked.
* @param onImageLoaded the callback to register.
*/
protected final void registerOnImageLoadedCallback(@NonNull final String key,
@NonNull final OnImageLoaded<LevelUpQrCodeImage> onImageLoaded) {
mLoaderCallbacks.put(key, onImageLoaded);
}
/**
* <p>
* Start the loading of the given QR code into an image in the background. If an existing
* request for the same content has been made, the second request will be dropped unless the
* previous request has been cancelled. This must be called on the main thread.
* </p>
*
* @param qrCodeContents the contents to render into a QR code.
* @param key the key under which the QR code is tracked.
* @param onImageLoaded called when the image has been loaded. Existing callbacks registered for
* this key will be overwritten unless this parameter is {@code null}.
*/
protected void startLoadInBackground(@NonNull final String qrCodeContents,
@NonNull final String key,
@Nullable final OnImageLoaded<LevelUpQrCodeImage> onImageLoaded) {
if (null != onImageLoaded) {
registerOnImageLoadedCallback(key, onImageLoaded);
}
onStartLoadInBackground(qrCodeContents, key, onImageLoaded);
}
/**
* Remove the given {@link OnImageLoaded} callbacks. This must be called on the main thread.
*
* @param key the key under which the image is tracked.
*/
protected final void unregisterOnImageLoadedCallback(@NonNull final String key) {
mLoaderCallbacks.remove(key);
}
}