/* * Copyright (C) 2014-2017 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo Flow. * * Akvo Flow is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Akvo Flow is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Akvo Flow. If not, see <http://www.gnu.org/licenses/>. */ package org.akvo.flow.activity; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.view.Window; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import org.akvo.flow.R; import org.akvo.flow.data.preference.Prefs; import org.akvo.flow.util.ConnectivityStateManager; import org.akvo.flow.util.FileUtil; import org.akvo.flow.util.FileUtil.FileType; import org.akvo.flow.util.PlatformUtil; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.net.HttpURLConnection; import java.net.URL; import timber.log.Timber; public class AppUpdateActivity extends Activity { public static final String EXTRA_URL = "url"; public static final String EXTRA_VERSION = "version"; public static final String EXTRA_CHECKSUM = "md5Checksum"; private static final int IO_BUFFER_SIZE = 8192; private static final int MAX_PROGRESS = 100; private Button mInstallBtn; private ProgressBar mProgress; private UpdateAsyncTask mTask; private String mUrl; private String mVersion; private String mMd5Checksum; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.app_update_activity); mUrl = getIntent().getStringExtra(EXTRA_URL); mVersion = getIntent().getStringExtra(EXTRA_VERSION); mMd5Checksum = getIntent().getStringExtra(EXTRA_CHECKSUM); mInstallBtn = (Button) findViewById(R.id.install_btn); mProgress = (ProgressBar) findViewById(R.id.progress); mProgress.setMax(MAX_PROGRESS);// Values will be in percentage // If the file is already downloaded, just prompt the install text final String filename = checkLocalFile(); if (filename != null) { TextView updateTV = (TextView) findViewById(R.id.update_text); updateTV.setText(R.string.clicktoinstall); mInstallBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { PlatformUtil.installAppUpdate(AppUpdateActivity.this, filename); } }); } else { mInstallBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mInstallBtn.setEnabled(false); mTask = new UpdateAsyncTask(AppUpdateActivity.this, mUrl, mVersion, mMd5Checksum); mTask.execute(); } }); } Button cancelBtn = (Button) findViewById(R.id.cancel_btn); cancelBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { cancel(); } }); } /** * Check out previously downloaded files. If the APK update is already downloaded, * and the MD5 checksum matches, the file is considered downloaded. * * @return filename of the already downloaded file, if exists. Null otherwise */ private String checkLocalFile() { final String latestVersion = FileUtil.checkDownloadedVersions(); if (latestVersion != null) { if (mMd5Checksum != null) { // The file was found, but we need to ensure the checksum matches, // to ensure the download succeeded File file = new File(latestVersion); if (!mMd5Checksum.equals(FileUtil.hexMd5(file))) { file.delete();// Wipe corrupted files return null; } } return latestVersion; } return null; } private void cancel() { if (isRunning()) { mTask.cancel(true);// Stop the update process } finish(); } @Override public void onDestroy() { if (isRunning()) { mTask.cancel(true); } super.onDestroy(); } private boolean isRunning() { return mTask != null && mTask.getStatus() == AsyncTask.Status.RUNNING; } private static class UpdateAsyncTask extends AsyncTask<Void, Integer, String> { private final Prefs prefs; private final String mUrl; private final String mVersion; private final WeakReference<AppUpdateActivity> activityWeakReference; private final ConnectivityStateManager connectivityStateManager; private String mMd5Checksum; public UpdateAsyncTask(AppUpdateActivity context, String mUrl, String mVersion, String mMd5Checksum) { this.prefs = new Prefs(context); this.mUrl = mUrl; this.mVersion = mVersion; this.activityWeakReference = new WeakReference<>(context); this.connectivityStateManager = new ConnectivityStateManager(context); this.mMd5Checksum = mMd5Checksum; } @Override protected String doInBackground(Void... params) { // Create parent directories, and delete files, if necessary String filename = createFile(mUrl, mVersion).getAbsolutePath(); boolean syncOver3GAllowed = prefs .getBoolean(Prefs.KEY_CELL_UPLOAD, Prefs.DEFAULT_VALUE_CELL_UPLOAD); if (!connectivityStateManager.isConnectionAvailable(syncOver3GAllowed)) { Timber.d("No internet connection available. Can't perform the requested operation"); } else if (downloadApk(mUrl, filename) && !isCancelled()) { return filename; } // Clean up sd-card to ensure no corrupted file is leaked. cleanupDownloads(mVersion); return null; } @Override protected void onProgressUpdate(Integer... progress) { int bytesWritten = progress[0]; int totalBytes = progress[1]; int percentComplete = 0; if (bytesWritten > 0 && totalBytes > 0) { percentComplete = (int) ((bytesWritten) / ((float) totalBytes) * 100); } if (percentComplete > MAX_PROGRESS) { percentComplete = MAX_PROGRESS; } Timber.d("onProgressUpdate() - APK update: " + percentComplete + "%"); notifyProgress(percentComplete); } private void notifyProgress(int percentComplete) { AppUpdateActivity appUpdateActivity = activityWeakReference.get(); if (appUpdateActivity != null) { appUpdateActivity.updateDownloadProgress(percentComplete); } } @Override protected void onPostExecute(String filename) { AppUpdateActivity appUpdateActivity = activityWeakReference.get(); if (TextUtils.isEmpty(filename)) { if (appUpdateActivity != null) { appUpdateActivity.onDownloadError(); } return; } if (appUpdateActivity != null) { appUpdateActivity.onDownloadSuccess(filename); } } @Override protected void onCancelled() { Timber.d("onCancelled() - APK update task cancelled"); notifyProgress(0); cleanupDownloads(mVersion); } private void cleanupDownloads(String version) { File directory = new File(FileUtil.getFilesDir(FileType.APK), version); FileUtil.deleteFilesInDirectory(directory, true); } /** * Wipe any existing apk file, and create a new File for the new one, according to the * given version * * @param location * @param version * @return */ private File createFile(String location, String version) { cleanupDownloads(version); String fileName = location.substring(location.lastIndexOf('/') + 1); File directory = new File(FileUtil.getFilesDir(FileType.APK), version); if (!directory.exists()) { directory.mkdir(); } return new File(directory, fileName); } /** * Downloads the apk file and stores it on the file system * After the download, a new notification will be displayed, requesting * the user to 'click to installAppUpdate' */ private boolean downloadApk(String location, String localPath) { Timber.i("App Update: Downloading new version " + mVersion + " from " + mUrl); boolean ok = false; InputStream in = null; OutputStream out = null; HttpURLConnection conn = null; try { URL url = new URL(location); conn = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(conn.getInputStream()); out = new BufferedOutputStream(new FileOutputStream(localPath)); int bytesWritten = 0; byte[] b = new byte[IO_BUFFER_SIZE]; final int fileSize = conn.getContentLength(); Timber.d("APK size: " + fileSize); int read; while ((read = in.read(b)) != -1) { if (isCancelled()) { return false; // No need to continue the download } out.write(b, 0, read); bytesWritten += read; publishProgress(bytesWritten, fileSize); } out.flush(); final int status = conn.getResponseCode(); if (status == HttpURLConnection.HTTP_OK) { final String checksum = FileUtil.hexMd5(new File(localPath)); if (TextUtils.isEmpty(checksum)) { throw new IOException("Downloaded file is not available"); } if (mMd5Checksum == null) { // If we don't have a checksum yet, try to get it form the ETag header String etag = conn.getHeaderField("ETag"); mMd5Checksum = etag != null ? etag.replaceAll("\"", "") : null;// Remove quotes } // Compare the MD5, if found. Otherwise, rely on the 200 status code ok = mMd5Checksum == null || mMd5Checksum.equals(checksum); } else { Timber.e("Wrong status code: " + status); ok = false; } } catch (IOException e) { Timber.e(e, e.getMessage()); } finally { if (conn != null) { conn.disconnect(); } FileUtil.close(in); FileUtil.close(out); } return ok; } } private void onDownloadSuccess(String filename) { PlatformUtil.installAppUpdate(AppUpdateActivity.this, filename); finish(); } private void onDownloadError() { Toast.makeText(this, R.string.apk_upgrade_error, Toast.LENGTH_SHORT).show(); mInstallBtn.setText(R.string.retry); mInstallBtn.setEnabled(true); } private void updateDownloadProgress(int percentComplete) { mProgress.setProgress(percentComplete); } }