<commit_before>package org.irmacard.cardemu.updates;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.irmacard.cardemu.ByteArrayToBase64TypeAdapter;
import org.irmacard.cardemu.HttpClient;
import org.irmacard.cardemu.HttpClient.HttpClientException;
import org.irmacard.cardemu.R;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * App updater class. Upon instantiation, the following happens:
 * - The updateVersionInfo() method fetches update info from the server.
 * - When that is done, if it found there is a new version, then
 *   * if the user is connected to WiFi, updateVersionInfo() calls downloadNewestVersion() to download the new version,
 *   * if not, the user is asked for permission for downloading, using askDownloadPermission(). If he consents,
 *     downloadNewestVersion() is called; if not, nothing further happens.
 * - If and when downloadNewestVersion() completes with the downloading, a dialog is shown to the user, using
 *   showUpdateDialog(), informing an update will happen and asking him to agree in the next screen (which is
 *   provided by Android, not by us).
 * - When the user presses OK (which is the only button in our dialog), an intent is fired pointing to the downloaded
 *   .apk, which will trigger Android's update screen.
 *
 * Every 10 minutes, the class checks if we should recheck for new versions. If 24 hours have passed (see the
 * updateCheckDelay variable) since we last checked, the process above runs again. The process can also be run by
 * calling updateVersionInfo(true).
 */
public class AppUpdater {
	public final static String TAG = "AppUpdater";
	public final static String appName = "cardemu";
	public final static String currentVersionFileName = appName + "-current.json";
	public final static String versionInfoFileName = appName + "-%d.json";
	public final static String apkFileName = appName + "-%d.apk";
	public final static int updateCheckDelay = 1000*60*60*24; // How often we should check if there are new versions

	// Local cache of version info's
	final private Map<Integer, AppVersionInfo> newVersions = new HashMap<>();
	// Reference to the latest version info for convenience
	private AppVersionInfo newestVersionInfo;
	private int newestVersionCode;

	private Context context;
	private String serverUrl;
	private HttpClient httpClient;
	private Handler handler = new Handler();

	/**
	 * Construct a new updater, and asynchroniously fetch version info from the update server
	 *
	 * @param context Needed for firing the update intent, getting version info, constructing dialog
	 * @param serverUrl The server hosting versions and version info
	 * @throws IllegalArgumentException if the serverUrl doesn't start with https
	 */
	public AppUpdater(final Context context, String serverUrl) throws IllegalArgumentException {
		if (!serverUrl.startsWith("https")) {
			throw new IllegalArgumentException("Non-HTTPS updating is a bad idea");
		}

		if (!serverUrl.endsWith("/")) {
			serverUrl = serverUrl + "/";
		}

		this.context = context;
		this.serverUrl = serverUrl;

		// Since we use ByteArrayToBase64TypeAdapter, the SHA1 hash of the file in the .json must be in Base64. This
		// can be generated for example by this line:
		// openssl dgst -binary -sha1 file.apk | base64
		Gson gson = new GsonBuilder()
				.registerTypeHierarchyAdapter(byte[].class, new ByteArrayToBase64TypeAdapter())
				.create();
		this.httpClient = new HttpClient(gson);

		updateVersionInfo(true);

		/**
		 * Check every 10 minutes if we should poll the server for new versions.
		 * Note that the counter that decides if those 10 minutes have passed runs only when the current thread is
		 * active, so we can't just put our desired check interval (24 hours) here, because then the time gap would
		 * only be actually 24 hours if the app is continuously in the foreground, which is not likely to happen.
		 */
		handler.postDelayed(new Runnable() {
			@Override
			public void run() {
				updateVersionInfo(false);
				handler.postDelayed(this, 1000*60*10);
			}
		}, 1000*60*10);
	}

	/**
	 * Asynchroniously fetch new version info. If new versions are found, the newest will be downloaded (asking the user
	 * for consent if he is on cellular), after which the update will be triggered.
	 * @param force if true the server will always be checked for new versions; otherwise, the server will only be
	 *              checked if the last time we checked was more than updateCheckDelay milliseconds ago.
	 */
	public void updateVersionInfo(boolean force) {
		updateVersionInfo(force, false);
	}

	public void updateVersionInfo(boolean force, final boolean toast) {
		// If it's not imperative that we check now, and we already checked recently, don't do anything
		if (!force && !shouldUpdateVersionInfo())
			return;

		// AsyncTasks like these can only be executed once, so we have to recreate it each time this method is called.
		new AsyncTask<Void, Void, Void>() {
			boolean noneFound = true;
			boolean serverError = false;

			@Override
			protected Void doInBackground(Void... params) {
				try {
					// If there is no connectivity this just throws an exception, which is then ignored. But checking
					// for connectivity in appropriate ways (e.g. using a ConnectivityManager) seems to be nontrivial.
					fetchAllVersionInfos();
				} catch (HttpClientException e) {
					if (e.status == 404)
						noneFound = true;
					else
						// Report this exception
						serverError = true;
				}
				return null;
			}

			@Override
			protected void onPostExecute(Void param) {
				// Store that we've updated the version info cache at this time
				context.getSharedPreferences(TAG, 0)
						.edit()
						.putLong("last_update_check", Calendar.getInstance().getTimeInMillis())
						.apply();

				if (newVersions.size() > 0) {
					noneFound = false;
					ConnectivityManager connManager
							= (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
					NetworkInfo wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

					if (newestVersionCode <= getCurrentVersionCode())
						return; // No need to do anything

					// We might already have downloaded the newest version earlier<commit_msg>Properly show toast if no new updates are found<commit_after>package org.irmacard.cardemu.updates;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.irmacard.cardemu.ByteArrayToBase64TypeAdapter;
import org.irmacard.cardemu.HttpClient;
import org.irmacard.cardemu.HttpClient.HttpClientException;
import org.irmacard.cardemu.R;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * App updater class. Upon instantiation, the following happens:
 * - The updateVersionInfo() method fetches update info from the server.
 * - When that is done, if it found there is a new version, then
 *   * if the user is connected to WiFi, updateVersionInfo() calls downloadNewestVersion() to download the new version,
 *   * if not, the user is asked for permission for downloading, using askDownloadPermission(). If he consents,
 *     downloadNewestVersion() is called; if not, nothing further happens.
 * - If and when downloadNewestVersion() completes with the downloading, a dialog is shown to the user, using
 *   showUpdateDialog(), informing an update will happen and asking him to agree in the next screen (which is
 *   provided by Android, not by us).
 * - When the user presses OK (which is the only button in our dialog), an intent is fired pointing to the downloaded
 *   .apk, which will trigger Android's update screen.
 *
 * Every 10 minutes, the class checks if we should recheck for new versions. If 24 hours have passed (see the
 * updateCheckDelay variable) since we last checked, the process above runs again. The process can also be run by
 * calling updateVersionInfo(true).
 */
public class AppUpdater {
	public final static String TAG = "AppUpdater";
	public final static String appName = "cardemu";
	public final static String currentVersionFileName = appName + "-current.json";
	public final static String versionInfoFileName = appName + "-%d.json";
	public final static String apkFileName = appName + "-%d.apk";
	public final static int updateCheckDelay = 1000*60*60*24; // How often we should check if there are new versions

	// Local cache of version info's
	final private Map<Integer, AppVersionInfo> newVersions = new HashMap<>();
	// Reference to the latest version info for convenience
	private AppVersionInfo newestVersionInfo;
	private int newestVersionCode;

	private Context context;
	private String serverUrl;
	private HttpClient httpClient;
	private Handler handler = new Handler();

	/**
	 * Construct a new updater, and asynchroniously fetch version info from the update server
	 *
	 * @param context Needed for firing the update intent, getting version info, constructing dialog
	 * @param serverUrl The server hosting versions and version info
	 * @throws IllegalArgumentException if the serverUrl doesn't start with https
	 */
	public AppUpdater(final Context context, String serverUrl) throws IllegalArgumentException {
		if (!serverUrl.startsWith("https")) {
			throw new IllegalArgumentException("Non-HTTPS updating is a bad idea");
		}

		if (!serverUrl.endsWith("/")) {
			serverUrl = serverUrl + "/";
		}

		this.context = context;
		this.serverUrl = serverUrl;

		// Since we use ByteArrayToBase64TypeAdapter, the SHA1 hash of the file in the .json must be in Base64. This
		// can be generated for example by this line:
		// openssl dgst -binary -sha1 file.apk | base64
		Gson gson = new GsonBuilder()
				.registerTypeHierarchyAdapter(byte[].class, new ByteArrayToBase64TypeAdapter())
				.create();
		this.httpClient = new HttpClient(gson);

		updateVersionInfo(true);

		/**
		 * Check every 10 minutes if we should poll the server for new versions.
		 * Note that the counter that decides if those 10 minutes have passed runs only when the current thread is
		 * active, so we can't just put our desired check interval (24 hours) here, because then the time gap would
		 * only be actually 24 hours if the app is continuously in the foreground, which is not likely to happen.
		 */
		handler.postDelayed(new Runnable() {
			@Override
			public void run() {
				updateVersionInfo(false);
				handler.postDelayed(this, 1000*60*10);
			}
		}, 1000*60*10);
	}

	/**
	 * Asynchroniously fetch new version info. If new versions are found, the newest will be downloaded (asking the user
	 * for consent if he is on cellular), after which the update will be triggered.
	 * @param force if true the server will always be checked for new versions; otherwise, the server will only be
	 *              checked if the last time we checked was more than updateCheckDelay milliseconds ago.
	 */
	public void updateVersionInfo(boolean force) {
		updateVersionInfo(force, false);
	}

	public void updateVersionInfo(boolean force, final boolean toast) {
		// If it's not imperative that we check now, and we already checked recently, don't do anything
		if (!force && !shouldUpdateVersionInfo())
			return;

		// AsyncTasks like these can only be executed once, so we have to recreate it each time this method is called.
		new AsyncTask<Void, Void, Void>() {
			boolean noneFound = true;
			boolean serverError = false;

			@Override
			protected Void doInBackground(Void... params) {
				try {
					// If there is no connectivity this just throws an exception, which is then ignored. But checking
					// for connectivity in appropriate ways (e.g. using a ConnectivityManager) seems to be nontrivial.
					fetchAllVersionInfos();
				} catch (HttpClientException e) {
					if (e.status == 404)
						noneFound = true;
					else
						// Report this exception
						serverError = true;
				}
				return null;
			}

			@Override
			protected void onPostExecute(Void param) {
				// Store that we've updated the version info cache at this time
				context.getSharedPreferences(TAG, 0)
						.edit()
						.putLong("last_update_check", Calendar.getInstance().getTimeInMillis())
						.apply();

				if (newVersions.size() > 0 && newestVersionCode > getCurrentVersionCode()) {
					noneFound = false;
					ConnectivityManager connManager
							= (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
					NetworkInfo wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

					// We might already have downloaded the newest version earlier
{'fraction_non_alphanumeric': 0.07927140255009107, 'fraction_numerical': 0.008014571948998178, 'mean_word_length': 4.053755522827688, 'pattern_counts': {'":': 0, '<': 10, '<?xml version=': 0, '>': 12, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3011221', 'n_tokens_mistral': 4092, 'n_tokens_neox': 3573, 'n_words': 1496}