514 lines
19 KiB
Java
514 lines
19 KiB
Java
|
// Kofax Custom UI Control Classes
|
||
|
// Copyright (c) 2017. All rights reserved.
|
||
|
// Kofax Confidential.
|
||
|
// Unauthorized use, duplication, or distribution, or disclosure is strictly prohibited.
|
||
|
//
|
||
|
|
||
|
package com.kofax.mobile.sdk.extract.id;
|
||
|
|
||
|
import android.content.Context;
|
||
|
import android.content.res.AssetManager;
|
||
|
|
||
|
import com.google.gson.Gson;
|
||
|
import com.kofax.R;
|
||
|
import com.kofax.kmc.kut.utilities.SdkVersion;
|
||
|
import com.kofax.mobile.sdk._internal.KeepProguard;
|
||
|
import com.kofax.mobile.sdk._internal.dagger.Injector;
|
||
|
import com.kofax.mobile.sdk.extract.id.bundle.IBundle;
|
||
|
import com.kofax.mobile.sdk.extract.id.bundle.IBundleCacheProvider;
|
||
|
import com.kofax.mobile.sdk.extract.id.bundle.IBundleFile;
|
||
|
import com.kofax.mobile.sdk.extract.id.bundle.ZipFileBundle;
|
||
|
|
||
|
import org.apache.commons.io.IOUtils;
|
||
|
import org.apache.commons.lang3.StringUtils;
|
||
|
|
||
|
import java.io.File;
|
||
|
import java.io.FileInputStream;
|
||
|
import java.io.FileOutputStream;
|
||
|
import java.io.IOException;
|
||
|
import java.io.InputStream;
|
||
|
import java.io.OutputStream;
|
||
|
import java.lang.ref.WeakReference;
|
||
|
import java.util.Enumeration;
|
||
|
import java.util.concurrent.Callable;
|
||
|
import java.util.concurrent.Executor;
|
||
|
import java.util.concurrent.Executors;
|
||
|
import java.util.zip.ZipFile;
|
||
|
|
||
|
import javax.inject.Inject;
|
||
|
|
||
|
import bolts.Continuation;
|
||
|
import bolts.Task;
|
||
|
|
||
|
/**
|
||
|
* A reference implementation of {@link IProjectProvider} that uses a project zip input stream, or
|
||
|
* reads the project zip from assets.
|
||
|
*/
|
||
|
@KeepProguard
|
||
|
public class LocalProjectProvider
|
||
|
implements IProjectProvider {
|
||
|
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
|
||
|
private static final String PROJECT_ZIP = "Project.zip";
|
||
|
private static final String VERSION_TABLE = "VersionTable.json";
|
||
|
private static final String VARIANT_ZIP = "Variant.zip";
|
||
|
private final WeakReference<Context> _contextWeakReference;
|
||
|
private final WeakReference<AssetManager> _assetManager;
|
||
|
@Inject
|
||
|
IBundleCacheProvider _cacheProvider;
|
||
|
private InputStream _inputStream;
|
||
|
private String _bundleVersion;
|
||
|
private File _tmpFile;
|
||
|
|
||
|
/**
|
||
|
* This constructor will create an instance using the default
|
||
|
* {@link com.kofax.mobile.sdk.extract.id.bundle.IBundleCacheProvider IBundleCacheProvider}.
|
||
|
* This implementation will assume a project zip exists in the root
|
||
|
* of the project's assets, named according to the project name. E.g.
|
||
|
* {@link IdRegion#getRegionName()}.zip ("USIDs.zip").
|
||
|
*
|
||
|
* @param ctx This context
|
||
|
*/
|
||
|
@Inject
|
||
|
public LocalProjectProvider(Context ctx) {
|
||
|
_contextWeakReference = new WeakReference<>(ctx);
|
||
|
_assetManager = new WeakReference<>(ctx.getAssets());
|
||
|
_inputStream = null;
|
||
|
Injector.getInjector(ctx).inject(this);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This constructor allows specification of the project input stream
|
||
|
* and a {@link com.kofax.mobile.sdk.extract.id.bundle.IBundleCacheProvider IBundleCacheProvider}.
|
||
|
* It's possible when using this constructor to place the project bundle anywhere on, or off the device,
|
||
|
* by providing its stream here.
|
||
|
* @note It is the callers responsibility to close the input stream object.
|
||
|
*
|
||
|
* @param ctx This context
|
||
|
* @param projectZipStream An input stream to the project zip file
|
||
|
* @param cacheProvider The cache provider used to store files
|
||
|
*/
|
||
|
@SuppressWarnings("unused")
|
||
|
public LocalProjectProvider(
|
||
|
Context ctx,
|
||
|
InputStream projectZipStream,
|
||
|
IBundleCacheProvider cacheProvider) {
|
||
|
_contextWeakReference = new WeakReference<>(ctx);
|
||
|
_assetManager = null;
|
||
|
_inputStream = projectZipStream;
|
||
|
_cacheProvider = cacheProvider;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void getHighestVersion(
|
||
|
final String projectName,
|
||
|
final String sdkVersion,
|
||
|
final ICompletionListener<String> listener) {
|
||
|
if (listener != null) {
|
||
|
getHighestVersionAsync(projectName)
|
||
|
.continueWith(
|
||
|
new ListenerCallbackContinuation<>(listener),
|
||
|
Task.UI_THREAD_EXECUTOR);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void getProject(
|
||
|
final String projectName,
|
||
|
final String version,
|
||
|
final ICompletionListener<File> listener) {
|
||
|
if (listener != null) {
|
||
|
getProjectAsync(projectName, version)
|
||
|
.continueWith(
|
||
|
new ListenerCallbackContinuation<>(listener),
|
||
|
Task.UI_THREAD_EXECUTOR);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void getVariant(
|
||
|
final String projectName,
|
||
|
final String variantName,
|
||
|
final String version,
|
||
|
final ICompletionListener<File> listener) {
|
||
|
if (listener != null) {
|
||
|
getVariantAsync(projectName, variantName, version)
|
||
|
.continueWith(
|
||
|
new ListenerCallbackContinuation<>(listener),
|
||
|
Task.UI_THREAD_EXECUTOR);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getLatestCachedModelDataBuildInfo(String projectName) {
|
||
|
if (_cacheProvider != null)
|
||
|
return _cacheProvider.getLatestModelDataBuildInfo(projectName);
|
||
|
else
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private Task<String> getHighestVersionAsync(final String projectName) {
|
||
|
return Task.call(new Callable<String>() {
|
||
|
@Override
|
||
|
public String call()
|
||
|
throws Exception {
|
||
|
return getBundleVersion(projectName);
|
||
|
}
|
||
|
}, EXECUTOR);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the project bundle's version string.
|
||
|
*
|
||
|
* @param projectName The name of the project to get the version for
|
||
|
* @return The version string
|
||
|
*/
|
||
|
protected String getBundleVersion(String projectName) {
|
||
|
if (_bundleVersion == null) {
|
||
|
getRegionBundle(projectName, new IBundleRunnable() {
|
||
|
@Override
|
||
|
public Void run(IBundle bundle) {
|
||
|
File versionTable = null;
|
||
|
|
||
|
try {
|
||
|
versionTable = find(VERSION_TABLE, bundle);
|
||
|
|
||
|
if (versionTable == null) {
|
||
|
throw new ProjectProviderException(
|
||
|
_contextWeakReference
|
||
|
.get()
|
||
|
.getString(R.string.ProjectProvider_CouldNotReadVersionTable));
|
||
|
}
|
||
|
|
||
|
_bundleVersion = readVersion(versionTable);
|
||
|
|
||
|
if (StringUtils.isEmpty(_bundleVersion)) {
|
||
|
throw new ProjectProviderException(
|
||
|
_contextWeakReference.get().getString(
|
||
|
R.string.ProjectProvider_CouldNotGetProjectZip));
|
||
|
}
|
||
|
} finally {
|
||
|
if (versionTable != null) {
|
||
|
//noinspection ResultOfMethodCallIgnored
|
||
|
versionTable.delete();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return _bundleVersion;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given the file of the version table, parses the table, and retrieves the version compatible
|
||
|
* with this SDK version.
|
||
|
* <p/>
|
||
|
* Normally, a project bundle will only include a single version. This implementation simply
|
||
|
* ensures the bundle is formatted properly.
|
||
|
*
|
||
|
* @param versionTable The version table
|
||
|
* @return The compatible version.
|
||
|
*/
|
||
|
protected String readVersion(File versionTable) {
|
||
|
try {
|
||
|
String jsonContent = IOUtils.toString(new FileInputStream(versionTable));
|
||
|
return readVersion(jsonContent);
|
||
|
} catch (IOException e) {
|
||
|
throw new ProjectProviderException(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected String readVersion(String jsonContent) {
|
||
|
VersionTable table = new Gson().fromJson(jsonContent, VersionTable.class);
|
||
|
if (table == null ||
|
||
|
table.VersionList == null ||
|
||
|
table.VersionList.length <= 0) {
|
||
|
throw new ProjectProviderException(
|
||
|
_contextWeakReference.get().getString(
|
||
|
R.string.ProjectProvider_CouldNotReadVersionTable));
|
||
|
}
|
||
|
|
||
|
String sdkVersion = sdkVersion();
|
||
|
String[] versionParts = sdkVersion.split("\\.");
|
||
|
String thisMajor = versionParts[0];
|
||
|
String thisMinor = versionParts[1];
|
||
|
|
||
|
String dataVersion = null;
|
||
|
|
||
|
for (VersionTable.VersionList version : table.VersionList) {
|
||
|
String bundleVersionMajor = version.sdkMajor;
|
||
|
String bundleVersionMinor = version.sdkMinor;
|
||
|
|
||
|
if (bundleVersionMajor.equals(thisMajor) &&
|
||
|
bundleVersionMinor.equals(thisMinor)) {
|
||
|
dataVersion = version.dataVersion;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (dataVersion == null) {
|
||
|
throw new ProjectProviderException(
|
||
|
_contextWeakReference.get().getString(
|
||
|
R.string.ProjectProvider_IncorrectBundleVersion));
|
||
|
}
|
||
|
|
||
|
return dataVersion;
|
||
|
}
|
||
|
|
||
|
protected String sdkVersion() {
|
||
|
return SdkVersion.getSdkVersion();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* An implementation of find that uses the filename to locate a file within a bundle. It is
|
||
|
* considered a match if the {@link IBundleFile#getAbsolutePath()} ends with the file name.
|
||
|
* <p/>
|
||
|
* One should ensure filenames being searched for are unique within the bundle to guarantee
|
||
|
* an accurate match. Otherwise, it's possible to send the end of a partial path to match; e.g.
|
||
|
* AK3_1/variant.zip
|
||
|
*
|
||
|
* @param pathEndsWith The path end part to search for
|
||
|
* @param bundle The bundle to search within
|
||
|
* @return A temporary file from the bundle, or null if none was found. <b>Note:</b> the file
|
||
|
* returned should be deleted to keep the system clean
|
||
|
*/
|
||
|
private File find(String pathEndsWith, IBundle bundle) {
|
||
|
Enumeration<? extends IBundleFile> bundleEnumeration = bundle.list();
|
||
|
while (bundleEnumeration.hasMoreElements()) {
|
||
|
IBundleFile file = bundleEnumeration.nextElement();
|
||
|
if (StringUtils.endsWithIgnoreCase(file.getAbsolutePath(), pathEndsWith)) {
|
||
|
return file.getFile();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
protected Task<File> getProjectAsync(final String projectName, final String version) {
|
||
|
return Task.call(new Callable<File>() {
|
||
|
@Override
|
||
|
public File call()
|
||
|
throws Exception {
|
||
|
String bundleVersion = getBundleVersion(projectName);
|
||
|
if (!version.equals(bundleVersion)) {
|
||
|
throw new ProjectProviderException(
|
||
|
_contextWeakReference.get().getString(
|
||
|
R.string.ProjectProvider_IncorrectBundleVersion));
|
||
|
}
|
||
|
File project = _cacheProvider.getProject(projectName, version);
|
||
|
|
||
|
if (project == null) {
|
||
|
project = cacheProject(projectName, version);
|
||
|
}
|
||
|
|
||
|
return project;
|
||
|
}
|
||
|
}, EXECUTOR);
|
||
|
}
|
||
|
|
||
|
private File cacheProject(final String projectName, final String versionName) {
|
||
|
return getRegionBundle(projectName, new IBundleRunnable<File>() {
|
||
|
@Override
|
||
|
public File run(IBundle bundle)
|
||
|
throws IOException {
|
||
|
File projectZip = null;
|
||
|
|
||
|
try {
|
||
|
projectZip = find(PROJECT_ZIP, bundle);
|
||
|
|
||
|
|
||
|
if (projectZip == null) {
|
||
|
throw new ProjectProviderException(
|
||
|
_contextWeakReference.get().getString(
|
||
|
R.string.ProjectProvider_CouldNotGetProjectZip));
|
||
|
}
|
||
|
|
||
|
IBundle projectBundle = new ZipFileBundle(new ZipFile(projectZip));
|
||
|
|
||
|
return _cacheProvider.cacheProject(
|
||
|
projectName,
|
||
|
projectBundle,
|
||
|
versionName);
|
||
|
} finally {
|
||
|
if (projectZip != null) {
|
||
|
//noinspection ResultOfMethodCallIgnored
|
||
|
projectZip.delete();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
protected Task<File> getVariantAsync(
|
||
|
final String projectName,
|
||
|
final String variantName,
|
||
|
final String version) {
|
||
|
return Task.call(new Callable<File>() {
|
||
|
@Override
|
||
|
public File call()
|
||
|
throws Exception {
|
||
|
String bundleVersion = getBundleVersion(projectName);
|
||
|
if (!version.equals(bundleVersion)) {
|
||
|
throw new ProjectProviderException(
|
||
|
_contextWeakReference.get().getString(
|
||
|
R.string.ProjectProvider_IncorrectBundleVersion));
|
||
|
}
|
||
|
File variant = _cacheProvider.getVariant(projectName, variantName, version);
|
||
|
|
||
|
if (variant == null) {
|
||
|
variant = cacheVariant(projectName, variantName, version);
|
||
|
}
|
||
|
|
||
|
return variant;
|
||
|
}
|
||
|
}, EXECUTOR);
|
||
|
}
|
||
|
|
||
|
private File cacheVariant(
|
||
|
final String projectName,
|
||
|
final String variantName,
|
||
|
final String versionName) {
|
||
|
return getRegionBundle(projectName, new IBundleRunnable<File>() {
|
||
|
@Override
|
||
|
public File run(IBundle bundle) {
|
||
|
File variantZip = null;
|
||
|
|
||
|
try {
|
||
|
variantZip = find("/" + variantName + "/" + VARIANT_ZIP, bundle);
|
||
|
if (variantZip == null) {
|
||
|
throw new ProjectProviderException(
|
||
|
_contextWeakReference.get().getString(
|
||
|
R.string.ProjectProvider_CouldNotLocateVariantZip));
|
||
|
}
|
||
|
|
||
|
IBundle variantBundle = new ZipFileBundle(new ZipFile(variantZip));
|
||
|
return _cacheProvider.cacheVariant(
|
||
|
projectName,
|
||
|
variantName,
|
||
|
variantBundle,
|
||
|
versionName);
|
||
|
} catch (IOException e) {
|
||
|
throw new ProjectProviderException(e);
|
||
|
} finally {
|
||
|
if (variantZip != null) {
|
||
|
//noinspection ResultOfMethodCallIgnored
|
||
|
variantZip.delete();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Responsible for retrieving the bundle from either the input stream passed to the constructor
|
||
|
* or from the assets. In either case, a temporary file is created once, and the bundle is
|
||
|
* copied to that temporary file. The finalizer deletes the file.
|
||
|
* <p/>
|
||
|
* Because bundles need to be closed, an {@link IBundleRunnable} is passed, so that after the
|
||
|
* runnable returns, the bundle is closed.
|
||
|
*
|
||
|
* @param projectName The project name to retrieve the bundle
|
||
|
* @param runnable The code block that will do work with the bundle
|
||
|
* @param <T> The type the block returns
|
||
|
* @return Returns the value returned from the runnable
|
||
|
*/
|
||
|
private <T> T getRegionBundle(String projectName, IBundleRunnable<T> runnable) {
|
||
|
boolean closeInputStream = false;
|
||
|
IBundle bundle = null;
|
||
|
OutputStream outputStream = null;
|
||
|
|
||
|
try {
|
||
|
if (_tmpFile == null) {
|
||
|
if (_inputStream == null) {
|
||
|
_inputStream = _assetManager.get().open(projectName + ".zip");
|
||
|
closeInputStream = true;
|
||
|
}
|
||
|
|
||
|
_tmpFile = File.createTempFile("ODE", ".zip");
|
||
|
outputStream = new FileOutputStream(_tmpFile);
|
||
|
IOUtils.copy(_inputStream, outputStream);
|
||
|
_inputStream = null;
|
||
|
}
|
||
|
|
||
|
bundle = new ZipFileBundle(new ZipFile(_tmpFile));
|
||
|
|
||
|
return runnable.run(bundle);
|
||
|
} catch (IOException e) {
|
||
|
if (_tmpFile != null) {
|
||
|
//noinspection ResultOfMethodCallIgnored
|
||
|
_tmpFile.delete();
|
||
|
}
|
||
|
throw new ProjectProviderException(e);
|
||
|
} finally {
|
||
|
if (closeInputStream) {
|
||
|
IOUtils.closeQuietly(_inputStream);
|
||
|
}
|
||
|
|
||
|
IOUtils.closeQuietly(bundle);
|
||
|
IOUtils.closeQuietly(outputStream);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deletes the temporary working file if one exists.
|
||
|
*
|
||
|
* @throws Throwable
|
||
|
*/
|
||
|
@Override
|
||
|
protected void finalize()
|
||
|
throws Throwable {
|
||
|
super.finalize();
|
||
|
|
||
|
if (_tmpFile != null) {
|
||
|
//noinspection ResultOfMethodCallIgnored
|
||
|
_tmpFile.delete();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Interface used when retrieving the project bundle
|
||
|
*
|
||
|
* @param <T>
|
||
|
*/
|
||
|
private interface IBundleRunnable<T> {
|
||
|
T run(IBundle inputStream)
|
||
|
throws IOException;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generic callback handler. This class is an adapter from {@link Task} to the callback-style
|
||
|
* used by the SDK.
|
||
|
*
|
||
|
* @param <T>
|
||
|
*/
|
||
|
private static class ListenerCallbackContinuation<T>
|
||
|
implements Continuation<T, Void> {
|
||
|
private final ICompletionListener<T> _completionListener;
|
||
|
|
||
|
private ListenerCallbackContinuation(ICompletionListener<T> completionListener) {
|
||
|
_completionListener = completionListener;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Void then(Task<T> task)
|
||
|
throws Exception {
|
||
|
_completionListener.onComplete(task.getResult(), task.getError());
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Represents the JSON layout of the version table for parsing with GSON.
|
||
|
*/
|
||
|
public static class VersionTable {
|
||
|
public VersionList[] VersionList;
|
||
|
|
||
|
public static class VersionList {
|
||
|
public String sdkMajor;
|
||
|
public String sdkMinor;
|
||
|
public String dataVersion;
|
||
|
}
|
||
|
}
|
||
|
}
|