package au.com.museumvictoria.fieldguide.bunurong.ui;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.zip.CRC32;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Messenger;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import au.com.museumvictoria.fieldguide.bunurong.R;
import au.com.museumvictoria.fieldguide.bunurong.db.FieldGuideDatabase;
import au.com.museumvictoria.fieldguide.bunurong.service.FieldGuideDownloaderService;
import au.com.museumvictoria.fieldguide.bunurong.util.Utilities;
import com.actionbarsherlock.app.SherlockActivity;
import com.android.vending.expansion.zipfile.ZipResourceFile;
import com.android.vending.expansion.zipfile.ZipResourceFile.ZipEntryRO;
import com.google.android.vending.expansion.downloader.Constants;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import com.google.android.vending.expansion.downloader.IDownloaderService;
import com.google.android.vending.expansion.downloader.IStub;
/**
* <p>Does a lot of the initiations</p>
* <ul>
* <li>Loads up the splash image.</li>
* <li>Checks if it is a first run and data is available</li>
* <li>Check if there are data updates</li>
* <li>Download data if necessary from the Google Play store using the licence service</li>
* </ul>
*
* TODO: Need a better system to check for new data and clean up the old data
*
* @author Ajay Ranipeta <ajay.ranipeta@gmail.com>
*
*/
public class SplashActivity extends SherlockActivity implements IDownloaderClient {
private static final String TAG = "SplashScreenActivity";
private TextView mProgressText;
private ProgressBar mProgress;
private int mProgressStatus = 0;
private Handler mHandler = new Handler();
FieldGuideDatabase mDatabase;
Cursor mCursor;
private ProgressBar mPB;
private TextView mStatusText;
private TextView mProgressFraction;
private TextView mProgressPercent;
private TextView mAverageSpeed;
private TextView mTimeRemaining;
private View mDashboard;
private View progressDashboard;
private View mCellMessage;
private Button mPauseButton;
private Button mWiFiSettingsButton;
private boolean mStatePaused;
private int mState;
private IDownloaderService mRemoteService;
private IStub mDownloaderClientStub;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
// overridePendingTransition(R.anim.fragment_slide_left_enter,
// R.anim.fragment_slide_left_exit);
// Check if expansion files are available before going any further
if (!expansionFilesDelivered()) {
try {
Utilities.cleanUpOldData(getApplicationContext());
// Build an Intent to start this activity from the Notification
Intent notifierIntent = new Intent(this,
SplashActivity.this.getClass());
notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Start the download service (if required)
int startResult = DownloaderClientMarshaller
.startDownloadServiceIfRequired(this, pendingIntent,
FieldGuideDownloaderService.class);
// If download has started, initialize this activity to show
// download progress
if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
// Instantiate a member instance of IStub
// mDownloaderClientStub =
// DownloaderClientMarshaller.CreateStub(this,
// FieldGuideDownloaderService.class);
// Inflate layout that shows download progress
// setContentView(R.layout.downloader_ui);
/**
* Both downloading and validation make use of the
* "download" UI
*/
initializeDownloadUI();
return;
} // If the download wasn't necessary, fall through to start the
// app
} catch (NameNotFoundException e) {
Log.e(TAG, "Cannot find own package! MAYDAY!");
e.printStackTrace();
}
}
startApp();
}
@Override
protected void onResume() {
if (null != mDownloaderClientStub) {
mDownloaderClientStub.connect(this);
}
super.onResume();
}
@Override
protected void onStop() {
if (null != mDownloaderClientStub) {
mDownloaderClientStub.disconnect(this);
}
super.onStop();
}
@Override
protected void onDestroy() {
this.mCancelValidation = true;
super.onDestroy();
}
/**
* Load up the acitivity after 2 seconds of splash screen
*
*/
private void startFieldGuide() {
Log.d(TAG, "Starting Bunurong Field Guide");
Intent home = new Intent(getApplicationContext(), MainActivity.class);
home.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(home);
SharedPreferences settings = getSharedPreferences(Utilities.PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("firstRun", false);
editor.putInt("currentVersion", Utilities.MAIN_VERSION);
// Commit the edits!
editor.commit();
finish();
}
private void startApp() {
setContentView(R.layout.activity_splash);
mProgress = (ProgressBar) findViewById(R.id.progressBar);
mProgressText = (TextView) findViewById(R.id.textProgress);
// Restore preferences
SharedPreferences settings = getSharedPreferences(Utilities.PREFS_NAME, MODE_PRIVATE);
boolean isFirstRun = settings.getBoolean("firstRun", true);
int currentVersion = settings.getInt("currentVersion", 1);
Log.d(TAG, "From settings:\n\tisFirstRun: " + isFirstRun + "\n\tcurrentVersion: " + currentVersion);
// if not the first run, check that the data isn't updated.
// the MAIN_VERSION should be higher than the saved option
if (!isFirstRun) {
if (currentVersion < Utilities.MAIN_VERSION) {
isFirstRun = true;
}
}
Log.w(TAG, "Is first run? " + isFirstRun);
if (isFirstRun) {
// initialise the database for first run
mProgress.setVisibility(View.VISIBLE);
mProgressText.setVisibility(View.VISIBLE);
Log.w(TAG, "About to start loading data for first run...");
// Start lengthy operation in a background thread
new Thread(new Runnable() {
public void run() {
Log.d(TAG, "Getting Expansion files");
String[] expfiles = Utilities.getAPKExpansionFiles(getApplicationContext());
File dataDir = Utilities.getExternalDataPath(getApplicationContext());
if (!dataDir.exists()) {
mProgress.setIndeterminate(true);
mProgressText.setText("Expanding data... ");
dataDir.mkdirs();
Log.d(TAG, "Unzipping main expansion file to: "
+ dataDir.toString());
Utilities.unzipExpansionFile(expfiles[0], dataDir);
mProgress.setIndeterminate(false);
// mProgressText.setText("Expanding data... done");
}
// mDatabase = new
// FieldGuideDatabase(getApplicationContext());
mDatabase = FieldGuideDatabase
.getInstance(getApplicationContext());
mCursor = mDatabase.getSpeciesGroups();
while (mProgressStatus < 100) {
mProgressStatus = doLoadDatabase();
// Update the progress bar
mHandler.post(new Runnable() {
public void run() {
mProgress.setProgress(mProgressStatus);
if (mProgressStatus == -9999) {
mProgressText
.setText("Reading in field guide data...");
} else if (mProgressStatus == 0) {
mProgressText
.setText("Loading field guide data...");
} else if (mProgressStatus == 100) {
mProgressText
.setText("Loading species: Completed");
} else {
mProgressText.setText("Loading species: "
+ mProgressStatus + "% done");
}
}
});
}
// database now populated. Let's start
if (mProgressStatus >= 100) {
if (mCursor != null) {
mCursor.close();
}
mDatabase.close();
startFieldGuide();
}
}
}).start();
} else {
Log.w(TAG, "Data has already been loaded...");
mProgress.setVisibility(View.INVISIBLE);
mProgressText.setVisibility(View.INVISIBLE);
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
startFieldGuide();
}
}, 1000);
}
}
/**
* Load the database
*
* @return int Progress
*/
private int doLoadDatabase() {
double currCount = mDatabase.getCurrCount();
double totalCount = mDatabase.getTotalCount();
int percentage = 0;
try {
double dd = (currCount / totalCount);
percentage = (int) (dd * 100);
} catch (ArithmeticException ae) {
// TODO: catch divide by 0
} catch (Exception e) {
// TODO: handle exception
}
if (totalCount == 0) {
return -9999;
} else {
return percentage;
}
}
private void setState(int newState) {
if (mState != newState) {
mState = newState;
mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState));
}
}
private void setButtonPausedState(boolean paused) {
mStatePaused = paused;
int stringResourceID = paused ? R.string.text_button_resume :
R.string.text_button_pause;
mPauseButton.setText(stringResourceID);
}
/**
* This is a little helper class that demonstrates simple testing of an
* Expansion APK file delivered by Market. You may not wish to hard-code
* things such as file lengths into your executable... and you may wish to
* turn this code off during application development.
*/
private static class XAPKFile {
public final boolean mIsMain;
public final int mFileVersion;
public final long mFileSize;
XAPKFile(boolean isMain, int fileVersion, long fileSize) {
mIsMain = isMain;
mFileVersion = fileVersion;
mFileSize = fileSize;
}
}
/**
* Here is where you place the data that the validator will use to determine
* if the file was delivered correctly. This is encoded in the source code
* so the application can easily determine whether the file has been
* properly delivered without having to talk to the server. If the
* application is using LVL for licensing, it may make sense to eliminate
* these checks and to just rely on the server.
*/
private static final XAPKFile[] xAPKS = {
new XAPKFile(true, // true signifies a main file
Utilities.MAIN_VERSION, // the version of the APK that the file was uploaded against
Utilities.EXP_FILE_SIZE // the length of the file in bytes
) };
/**
* Go through each of the APK Expansion files defined in the structure above
* and determine if the files are present and match the required size. Free
* applications should definitely consider doing this, as this allows the
* application to be launched for the first time without having a network
* connection present. Paid applications that use LVL should probably do at
* least one LVL check that requires the network to be present, so this is
* not as necessary.
*
* @return true if they are present.
*/
boolean expansionFilesDelivered() {
for (XAPKFile xf : xAPKS) {
String fileName = Helpers.getExpansionAPKFileName(this, xf.mIsMain, xf.mFileVersion);
if (!Helpers.doesFileExist(this, fileName, xf.mFileSize, false))
return false;
}
return true;
}
/**
* Calculating a moving average for the validation speed so we don't get
* jumpy calculations for time etc.
*/
static private final float SMOOTHING_FACTOR = 0.005f;
/**
* Used by the async task
*/
private boolean mCancelValidation;
/**
* Go through each of the Expansion APK files and open each as a zip file.
* Calculate the CRC for each file and return false if any fail to match.
*
* @return true if XAPKZipFile is successful
*/
void validateXAPKZipFiles() {
AsyncTask<Object, DownloadProgressInfo, Boolean> validationTask = new AsyncTask<Object, DownloadProgressInfo, Boolean>() {
@Override
protected void onPreExecute() {
mDashboard.setVisibility(View.VISIBLE);
mCellMessage.setVisibility(View.GONE);
mStatusText.setText(R.string.text_verifying_download);
mPauseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mCancelValidation = true;
}
});
mPauseButton.setText(R.string.text_button_cancel_verify);
super.onPreExecute();
}
@Override
protected Boolean doInBackground(Object... params) {
for (XAPKFile xf : xAPKS) {
String fileName = Helpers.getExpansionAPKFileName(
SplashActivity.this,
xf.mIsMain, xf.mFileVersion);
if (!Helpers.doesFileExist(SplashActivity.this, fileName,
xf.mFileSize, false))
return false;
fileName = Helpers
.generateSaveFileName(SplashActivity.this, fileName);
ZipResourceFile zrf;
byte[] buf = new byte[1024 * 256];
try {
zrf = new ZipResourceFile(fileName);
ZipEntryRO[] entries = zrf.getAllEntries();
/**
* First calculate the total compressed length
*/
long totalCompressedLength = 0;
for (ZipEntryRO entry : entries) {
totalCompressedLength += entry.mCompressedLength;
}
float averageVerifySpeed = 0;
long totalBytesRemaining = totalCompressedLength;
long timeRemaining;
/**
* Then calculate a CRC for every file in the Zip file,
* comparing it to what is stored in the Zip directory.
* Note that for compressed Zip files we must extract
* the contents to do this comparison.
*/
for (ZipEntryRO entry : entries) {
if (-1 != entry.mCRC32) {
long length = entry.mUncompressedLength;
CRC32 crc = new CRC32();
DataInputStream dis = null;
try {
dis = new DataInputStream(
zrf.getInputStream(entry.mFileName));
long startTime = SystemClock.uptimeMillis();
while (length > 0) {
int seek = (int) (length > buf.length ? buf.length
: length);
dis.readFully(buf, 0, seek);
crc.update(buf, 0, seek);
length -= seek;
long currentTime = SystemClock.uptimeMillis();
long timePassed = currentTime - startTime;
if (timePassed > 0) {
float currentSpeedSample = (float) seek
/ (float) timePassed;
if (0 != averageVerifySpeed) {
averageVerifySpeed = SMOOTHING_FACTOR
* currentSpeedSample
+ (1 - SMOOTHING_FACTOR)
* averageVerifySpeed;
} else {
averageVerifySpeed = currentSpeedSample;
}
totalBytesRemaining -= seek;
timeRemaining = (long) (totalBytesRemaining / averageVerifySpeed);
this.publishProgress(
new DownloadProgressInfo(
totalCompressedLength,
totalCompressedLength
- totalBytesRemaining,
timeRemaining,
averageVerifySpeed)
);
}
startTime = currentTime;
if (mCancelValidation)
return true;
}
if (crc.getValue() != entry.mCRC32) {
Log.e(Constants.TAG,
"CRC does not match for entry: "
+ entry.mFileName);
Log.e(Constants.TAG,
"In file: " + entry.getZipFileName());
return false;
}
} finally {
if (null != dis) {
dis.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
return true;
}
@Override
protected void onProgressUpdate(DownloadProgressInfo... values) {
onDownloadProgress(values[0]);
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
mDashboard.setVisibility(View.VISIBLE);
mCellMessage.setVisibility(View.GONE);
mStatusText.setText(R.string.text_validation_complete);
mPauseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
mPauseButton.setText(android.R.string.ok);
progressDashboard.setVisibility(View.GONE);
} else {
mDashboard.setVisibility(View.VISIBLE);
mCellMessage.setVisibility(View.GONE);
mStatusText.setText(R.string.text_validation_failed);
mPauseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
mPauseButton.setText(android.R.string.cancel);
}
super.onPostExecute(result);
}
};
validationTask.execute(new Object());
}
/**
* If the download isn't present, we initialize the download UI. This ties
* all of the controls into the remote service calls.
*/
private void initializeDownloadUI() {
mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, FieldGuideDownloaderService.class);
setContentView(R.layout.downloader_ui);
mPB = (ProgressBar) findViewById(R.id.progressBar);
mStatusText = (TextView) findViewById(R.id.statusText);
mProgressFraction = (TextView) findViewById(R.id.progressAsFraction);
mProgressPercent = (TextView) findViewById(R.id.progressAsPercentage);
mAverageSpeed = (TextView) findViewById(R.id.progressAverageSpeed);
mTimeRemaining = (TextView) findViewById(R.id.progressTimeRemaining);
mDashboard = findViewById(R.id.downloaderDashboard);
progressDashboard = findViewById(R.id.progressDashboard);
mCellMessage = findViewById(R.id.approveCellular);
mPauseButton = (Button) findViewById(R.id.pauseButton);
mWiFiSettingsButton = (Button) findViewById(R.id.wifiSettingsButton);
mPauseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mStatePaused) {
mRemoteService.requestContinueDownload();
} else {
mRemoteService.requestPauseDownload();
}
setButtonPausedState(!mStatePaused);
}
});
mWiFiSettingsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
}
});
Button resumeOnCell = (Button) findViewById(R.id.resumeOverCellular);
resumeOnCell.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mRemoteService.setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR);
mRemoteService.requestContinueDownload();
mCellMessage.setVisibility(View.GONE);
}
});
}
@Override
public void onServiceConnected(Messenger m) {
mRemoteService = DownloaderServiceMarshaller.CreateProxy(m);
mRemoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
}
@Override
public void onDownloadStateChanged(int newState) {
setState(newState);
boolean showDashboard = true;
boolean showCellMessage = false;
boolean paused;
boolean indeterminate;
switch (newState) {
case IDownloaderClient.STATE_IDLE:
// STATE_IDLE means the service is listening, so it's
// safe to start making calls via mRemoteService.
paused = false;
indeterminate = true;
break;
case IDownloaderClient.STATE_CONNECTING:
case IDownloaderClient.STATE_FETCHING_URL:
showDashboard = true;
paused = false;
indeterminate = true;
break;
case IDownloaderClient.STATE_DOWNLOADING:
paused = false;
showDashboard = true;
indeterminate = false;
break;
case IDownloaderClient.STATE_FAILED_CANCELED:
case IDownloaderClient.STATE_FAILED:
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
case IDownloaderClient.STATE_FAILED_UNLICENSED:
paused = true;
showDashboard = false;
indeterminate = false;
break;
case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
showDashboard = false;
paused = true;
indeterminate = false;
showCellMessage = true;
break;
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
paused = true;
indeterminate = false;
break;
case IDownloaderClient.STATE_PAUSED_ROAMING:
case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
paused = true;
indeterminate = false;
break;
case IDownloaderClient.STATE_COMPLETED:
showDashboard = false;
paused = false;
indeterminate = false;
validateXAPKZipFiles();
return;
default:
paused = true;
indeterminate = true;
showDashboard = true;
}
int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE;
if (mDashboard.getVisibility() != newDashboardVisibility) {
mDashboard.setVisibility(newDashboardVisibility);
}
int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE;
if (mCellMessage.getVisibility() != cellMessageVisibility) {
mCellMessage.setVisibility(cellMessageVisibility);
}
mPB.setIndeterminate(indeterminate);
setButtonPausedState(paused);
}
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
mAverageSpeed.setText(getString(R.string.kilobytes_per_second,
Helpers.getSpeedString(progress.mCurrentSpeed)));
mTimeRemaining.setText(getString(R.string.time_remaining,
Helpers.getTimeRemaining(progress.mTimeRemaining)));
progress.mOverallTotal = progress.mOverallTotal;
mPB.setMax((int) (progress.mOverallTotal >> 8));
mPB.setProgress((int) (progress.mOverallProgress >> 8));
mProgressPercent.setText(Long.toString(progress.mOverallProgress
* 100 /
progress.mOverallTotal) + "%");
mProgressFraction.setText(Helpers.getDownloadProgressString
(progress.mOverallProgress,
progress.mOverallTotal));
}
}