/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.mozstumbler.client;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import org.mozilla.mozstumbler.R;
import org.mozilla.mozstumbler.service.core.http.IHttpUtil;
import org.mozilla.mozstumbler.service.core.http.IResponse;
import org.mozilla.mozstumbler.service.utils.NetworkInfo;
import org.mozilla.mozstumbler.svclocator.ServiceLocator;
import org.mozilla.mozstumbler.svclocator.services.ISystemClock;
import org.mozilla.mozstumbler.svclocator.services.log.LoggerUtil;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
public class Updater {
private static final String LOG_TAG = LoggerUtil.makeLogTag(Updater.class);
private static final String LATEST_URL = "https://github.com/mozilla/MozStumbler/releases/latest";
private static final String APK_URL_FORMAT = "https://github.com/mozilla/MozStumbler/releases/download/v%s/MozStumbler-v%s.apk";
public static final long UPDATE_CHECK_FREQ_MS = 6 * 60 * 60 * 1000; // 6 hours
static long sLastUpdateCheck = 0;
public boolean wifiExclusiveAndUnavailable(Context c) {
return !new NetworkInfo(c).isWifiAvailable() &&
ClientPrefs.getInstance(c).getUseWifiOnly();
}
public boolean checkForUpdates(final Activity activity, String api_key) {
ISystemClock clock = (ISystemClock) ServiceLocator.getInstance().getService(ISystemClock.class);
if (clock.currentTimeMillis() - sLastUpdateCheck < UPDATE_CHECK_FREQ_MS) {
return false;
}
// No API Key means skip the update
if (api_key == null || api_key.equals("")) {
return false;
}
if (!new NetworkInfo(activity).isConnected()) {
return false;
}
// No wifi available and require the use of wifi only means skip
if (wifiExclusiveAndUnavailable(activity.getApplicationContext())) {
return false;
}
new AsyncTask<Void, Void, IResponse>() {
@Override
public IResponse doInBackground(Void... params) {
IHttpUtil httpClient = (IHttpUtil) ServiceLocator.getInstance().getService(IHttpUtil.class);
return httpClient.head(LATEST_URL, null);
}
@Override
public void onPostExecute(IResponse response) {
if (response == null) {
return;
}
Map<String, List<String>> headers = response.getHeaders();
if (headers == null) {
return;
}
Log.i(LOG_TAG, "Got headers: " + headers.toString());
if (headers.get("Location") == null) {
return;
}
String locationUrl = headers.get("Location").get(0);
if (locationUrl == null || locationUrl.length() < 1) {
return;
}
String[] parts = locationUrl.split("/");
if (parts.length < 2) {
return;
}
String tag = parts[parts.length - 1];
if (tag.length() < 2) {
return;
}
String latestVersion = tag.substring(1); // strip the 'v' from the beginning
String installedVersion = PackageUtils.getAppVersion(activity);
installedVersion = stripBuildHostName(installedVersion);
Log.d(LOG_TAG, "Installed version: " + installedVersion);
Log.d(LOG_TAG, "Latest version: " + latestVersion);
if (isVersionGreaterThan(latestVersion, installedVersion) && !activity.isFinishing()) {
showUpdateDialog(activity, installedVersion, latestVersion);
}
}
}.execute();
sLastUpdateCheck = clock.currentTimeMillis();
return true;
}
String stripBuildHostName(String installedVersion) {
// Some versions had the old buildhost stuff in there, we need
// to strip out anything pase the 3rd integer part.
String[] parts = installedVersion.split("\\.");
if (parts.length < 3) {
throw new RuntimeException("Unexpected version string: [" + installedVersion + "] parts:" + parts.length);
}
return parts[0] + "." + parts[1] + "." + parts[2];
}
private boolean isVersionGreaterThan(String a, String b) {
if (a == null) {
return false;
}
if (b == null) {
return true;
}
if (a.equals(b)) {
return false; // fast path
}
String[] as = a.split("\\.");
String[] bs = b.split("\\.");
int len = Math.min(as.length, bs.length);
try {
for (int i = 0; i < len; i++) {
int an = Integer.parseInt(as[i]);
int bn = Integer.parseInt(bs[i]);
if (an == bn) {
continue;
}
return (an > bn);
}
} catch (NumberFormatException e) {
Log.w(LOG_TAG, "a='" + a + "', b='" + b + "'", e);
return false;
}
// Strings have identical prefixes, so longest version string wins.
return as.length > bs.length;
}
private void showUpdateDialog(final Context context,
String installedVersion,
final String latestVersion) {
String msg = context.getString(R.string.update_message);
msg = String.format(msg, installedVersion, latestVersion);
if (installedVersion.startsWith("0.") &&
latestVersion.startsWith("1.")) {
// From 0.x to 1.0 and higher, the keystore changed
msg += " " + context.getString(R.string.must_uninstall_to_update);
}
final Dialog.OnCancelListener onCancel = new Dialog.OnCancelListener() {
@Override
public void onCancel(DialogInterface di) {
di.dismiss();
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(context.getString(R.string.update_title))
.setMessage(msg)
.setPositiveButton(context.getString(R.string.update_now),
new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface di, int which) {
Log.d(LOG_TAG, "Update Now");
di.dismiss();
downloadAndInstallUpdate(context, latestVersion);
}
})
.setNegativeButton(context.getString(R.string.update_later),
new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface di, int which) {
onCancel.onCancel(di);
}
})
.setOnCancelListener(onCancel);
builder.create().show();
}
private void downloadAndInstallUpdate(final Context context, final String version) {
new AsyncTask<Void, Void, File>() {
@Override
public File doInBackground(Void... params) {
URL apkURL = getUpdateURL(version);
File apk = downloadFile(context, apkURL);
if (apk == null || !apk.exists()) {
Log.e(LOG_TAG, "Update file not found!");
return null;
}
return apk;
}
@Override
public void onPostExecute(File result) {
if (result != null) {
installPackage(context, result);
}
}
}.execute();
}
private URL getUpdateURL(String version) {
String url = String.format(APK_URL_FORMAT, version, version);
try {
return new URL(url);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
private File downloadFile(Context context, URL url) {
Log.d(LOG_TAG, "Downloading: " + url);
File file;
File dir = context.getExternalFilesDir(null);
try {
file = File.createTempFile("update", ".apk", dir);
} catch (IOException e1) {
Log.e(LOG_TAG, "", e1);
return null;
}
try {
IHttpUtil httpClient = (IHttpUtil) ServiceLocator.getInstance().getService(IHttpUtil.class);
return httpClient.getUrlAsFile(url, file);
} catch (IOException e) {
Log.e(LOG_TAG, "", e);
file.delete();
return null;
}
}
private void installPackage(Context context, File apkFile) {
Uri apkURI = Uri.fromFile(apkFile);
Log.d(LOG_TAG, "Installing: " + apkURI);
//First stop the service so it is not running more
Intent service = new Intent();
service.setClass(context, ClientStumblerService.class);
context.stopService(service);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(apkURI, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
//And then kill the app to avoid any error
android.os.Process.killProcess(android.os.Process.myPid());
}
}