Java tutorial
/* * Copyright (c) 2016, Psiphon Inc. * All rights reserved. * * This program 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. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. * */ package com.psiphon3.psiphonlibrary; import android.app.AlarmManager; import android.app.IntentService; import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.SystemClock; import android.support.v4.content.WakefulBroadcastReceiver; import android.util.Log; import com.psiphon3.R; import com.psiphon3.psiphonlibrary.Utils.MyLog; import net.grandcentrix.tray.AppPreferences; import java.util.Date; import java.util.List; import ca.psiphon.PsiphonTunnel; /* * Self-upgrading notes. * - UpgradeChecker is responsible for processing downloaded upgrade files (authenticate package, * check APK version -- via UpgradeManager), notifying users of upgrades, and invoking the OS installer. Only * UpgradeChecker will do these things, so we ensure theres only one upgrade notification, etc. * - Every X hours, an alarm will wake up UpgradeChecker and it will launch its own tunnel-core and * download an upgrade if no upgrade is pending. This achieves the Google Play-like * upgrade-when-not-running. * - The Psiphon app tunnel-core will also download upgrades, if no upgrade is pending. It will make * an untunneled check when it cant connect. Or it will download when handshake indicates an * upgrade is available. * - An upgrade is pending if a valid upgrade has been downloaded and is awaiting install. Both * tunnel-cores need to be configured to skip upgrades when theres a pending upgrade. In fact, * the UpgradeChecker tunnel-core need not be started at all in this case. * - When the Psiphon app tunnel-core downloads an upgrade, it notifies UpgradeChecker with an * intent. UpgradeChecker takes ownership of the downloaded file and proceeds as if it downloaded * the file. * - Because the app tunnel-core and UpgradeChecker download to the same filename, there's a * race condition to access the files -- partial download file, unverified file, verified file. * We will rely on file locking and package verification to keep the files sane. There is a * very tiny chance that a file could get deleted right before it's replaced, causing an error * when the user clicks the notification, but very tiny. */ public class UpgradeChecker extends WakefulBroadcastReceiver { private static final int ALARM_INTENT_REQUEST_CODE = 0; private static final String ALARM_INTENT_ACTION = UpgradeChecker.class.getName() + ":ALARM"; private static final String CREATE_ALARM_INTENT_ACTION = UpgradeChecker.class.getName() + ":CREATE_ALARM"; public static final String UPGRADE_FILE_AVAILABLE_INTENT_ACTION = UpgradeChecker.class.getName() + ":UPGRADE_AVAILABLE"; /** * Provides loggging functionality to the :UpgradeChecker process. Utilizes LoggingProvider. * May be called from any process or thread. * @param context * @param stringResID String resource ID. * @param sensitivity Log sensitivity level. * @param priority One of the log priority levels supported by MyLog. Like: Log.DEBUG, Log.INFO, Log.WARN, Log.ERROR, Log.VERBOSE * @param formatArgs Arguments to be formatted into the log string. */ private static void log(Context context, int stringResID, MyLog.Sensitivity sensitivity, int priority, Object... formatArgs) { /* //TODO: use MyLog? if(!MyLog.isSetLogger()) { final Context loggerCtx = context; MyLog.ILogger logger = new MyLog.ILogger() { @Override public Context getContext() { return loggerCtx; } }; MyLog.setLogger(logger); } */ String logJSON = LoggingProvider.makeStatusLogJSON(context, new Date(), stringResID, sensitivity, formatArgs, priority); if (logJSON == null) { // Fail silently return; } ContentValues values = new ContentValues(); values.put(LoggingProvider.LogDatabaseHelper.COLUMN_NAME_LOGJSON, logJSON); values.put(LoggingProvider.LogDatabaseHelper.COLUMN_NAME_IS_DIAGNOSTIC, false); context.getContentResolver().insert(LoggingProvider.INSERT_URI, values); } /** * Checks whether an upgrade check should be performed. False will be returned if there's already * an upgrade file downloaded. * May be called from any process or thread. * Side-effect: If an existing upgrade file is detected, the upgrade notification will be displayed. * Side-effect: Creates the UpgradeChecker alarm. * @param context the context * @return true if upgrade check is needed. */ public static boolean upgradeCheckNeeded(Context context) { Context appContext = context.getApplicationContext(); // The main process will call this when it tries to connect, so we will use this opportunity // to make sure our alarm is created. createAlarm(appContext); // Don't re-download the upgrade package when a verified upgrade file is // awaiting application by the user. A previous upgrade download will have // completed and have been extracted to this verified upgrade file. // Without this check, tunnel-core won't know that the upgrade is already // downloaded, as the file name differs from UpgradeDownloadFilename, and // so the entire upgrade will be re-downloaded on each tunnel connect until // the user actually applies the upgrade. // As a result of this check, a user that delays applying an upgrade until // after a subsequent upgrade is released will first apply a stale upgrade // and then download the next upgrade. // Note: depends on getAvailableCompleteUpgradeFile deleting VerifiedUpgradeFile // after upgrade is complete. Otherwise, no further upgrades would download. // TODO: implement version tracking for the verified upgrade file so that // we can proceed with downloading a newer upgrade when an outdated upgrade exists // on disk. if (!allowedToSelfUpgrade(context)) { log(context, R.string.upgrade_checker_no_upgrading, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); return false; } if (UpgradeManager.UpgradeInstaller.upgradeFileAvailable(appContext)) { log(context, R.string.upgrade_checker_upgrade_file_exists, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); // We know there's an upgrade file available, so send an intent about it. Intent intent = new Intent(appContext, UpgradeChecker.class); intent.setAction(UPGRADE_FILE_AVAILABLE_INTENT_ACTION); appContext.sendBroadcast(intent); return false; } // Verify if 'Download upgrades on WiFi only' user preference is on // but current network is not WiFi final AppPreferences multiProcessPreferences = new AppPreferences(appContext); if (multiProcessPreferences.getBoolean(context.getString(R.string.downloadWifiOnlyPreference), PsiphonConstants.DOWNLOAD_WIFI_ONLY_PREFERENCE_DEFAULT) && !Utils.isOnWiFi(appContext)) { log(context, R.string.upgrade_checker_upgrade_wifi_only, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); return false; } log(appContext, R.string.upgrade_checker_check_needed, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); return true; } /** * Checks if the current app installation is allowed to upgrade itself. * @param appContext The application context. * @return true if the app is allowed to self-upgrade, false otherwise. */ private static boolean allowedToSelfUpgrade(Context appContext) { if (EmbeddedValues.UPGRADE_URLS_JSON.length() == "[]".length()) { // We don't know where to find an upgrade. return false; } else if (!EmbeddedValues.hasEverBeenSideLoaded(appContext)) { // If the app hasn't been side-loaded, then it's a Play Store build. // Play Store Build instances must not use custom auto-upgrade, as it's a ToS violation. return false; } return true; } @Override public void onReceive(Context context, Intent intent) { // This service runs as a separate process, so it needs to initialize embedded values EmbeddedValues.initialize(context); // Make sure the alarm is created, regardless of which intent we received. createAlarm(context.getApplicationContext()); String action = intent.getAction(); if (action.equals(ALARM_INTENT_ACTION)) { log(context, R.string.upgrade_checker_alarm_intent_received, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); if (!upgradeCheckNeeded(context)) { return; } checkForUpgrade(context); } else if (action.equals(UPGRADE_FILE_AVAILABLE_INTENT_ACTION)) { log(context, R.string.upgrade_checker_upgrade_file_available_intent_received, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); // Create upgrade notification. User clicking the notification will trigger the install. UpgradeManager.UpgradeInstaller.notifyUpgrade(context.getApplicationContext()); } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { log(context, R.string.upgrade_checker_boot_completed_intent_received, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); // Pass. We created the alarm above, so nothing else to do (until the alarm runs). } else if (action.equals(CREATE_ALARM_INTENT_ACTION)) { log(context, R.string.upgrade_checker_create_alarm_intent_received, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); // Pass. We created the alarm above, so nothing else to do (until the alarm runs). } } /** * Creates the periodic alarm used to check for updates. Can be called unconditionally; it * handles cases when the alarm is already created. * @param appContext The application context. */ private static void createAlarm(Context appContext) { if (!allowedToSelfUpgrade(appContext)) { // Don't waste resources with an alarm if we can't possibly self-upgrade. log(appContext, R.string.upgrade_checker_no_alarm_no_selfupgrading, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); return; } Intent intent = new Intent(appContext, UpgradeChecker.class); intent.setAction(ALARM_INTENT_ACTION); boolean alarmExists = (PendingIntent.getBroadcast(appContext, ALARM_INTENT_REQUEST_CODE, intent, PendingIntent.FLAG_NO_CREATE) != null); if (alarmExists) { log(appContext, R.string.upgrade_checker_alarm_exists, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); return; } log(appContext, R.string.upgrade_checker_creating_alarm, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); PendingIntent alarmIntent = PendingIntent.getBroadcast(appContext, ALARM_INTENT_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager alarmMgr = (AlarmManager) appContext.getSystemService(Context.ALARM_SERVICE); alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_FIFTEEN_MINUTES, AlarmManager.INTERVAL_HALF_DAY, alarmIntent); } /** * Launches the upgrade checking service. Returns immediately. */ private void checkForUpgrade(Context context) { log(context, R.string.upgrade_checker_start_service, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); Intent service = new Intent(context, UpgradeCheckerService.class); startWakefulService(context, service); } /** * The service that does the upgrade checking, via tunnel-core. */ public static class UpgradeCheckerService extends IntentService implements PsiphonTunnel.HostService { /** * The tunnel-core instance. */ private PsiphonTunnel mTunnel; /** * The wakeful intent that was received to launch the upgrade checking. We hold on to it so * that we can release the wakelock when we're done. */ private Intent mWakefulIntent; /** * Used to post back to stop the tunnel, to avoid locking the thread. */ Handler mStopHandler = new Handler(); /** * Used to keep track of whether we've already sent the intent indicating that the * upgrade is available. */ private boolean mUpgradeDownloaded; public UpgradeCheckerService() { super("UpgradeCheckerService"); mTunnel = PsiphonTunnel.newPsiphonTunnel(this); } /** * Entry point for starting the upgrade service. * @param intent Must be passed to UpgradeChecker.completeWakefulIntent when the check is done. */ @Override protected void onHandleIntent(Intent intent) { log(this, R.string.upgrade_checker_check_start, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); if (mWakefulIntent != null) { // Already processing an intent. log(this, R.string.upgrade_checker_already_in_progress, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); // Not calling shutDownTunnel() because we don't want to interfere with the currently running request. UpgradeChecker.completeWakefulIntent(intent); return; } setWakefulIntent(intent); mUpgradeDownloaded = false; Utils.initializeSecureRandom(); try { mTunnel.startTunneling(TunnelManager.getServerEntries(this)); } catch (PsiphonTunnel.Exception e) { log(this, R.string.upgrade_checker_start_tunnel_failed, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN, e.getMessage()); // No need to call shutDownTunnel(). releaseWakefulIntent(); } } /** * Called when tunnel-core upgrade processing is finished (one way or another). * May be called more than once. */ protected void shutDownTunnel() { final Context context = this; mStopHandler.post(new Runnable() { @Override public void run() { log(context, R.string.upgrade_checker_done, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); mTunnel.stop(); } }); } protected void setWakefulIntent(Intent intent) { assert (mWakefulIntent == null); mWakefulIntent = intent; } /** * Complete the current wakeful intent. Note that this releases the wakelock and should be * called only when everything else is finished. */ protected void releaseWakefulIntent() { if (mWakefulIntent != null) { UpgradeChecker.completeWakefulIntent(mWakefulIntent); } mWakefulIntent = null; } /* * PsiphonTunnel.HostService implementation */ @Override public String getPsiphonConfig() { // Build a temporary tunnel config to use TunnelManager.Config tunnelManagerConfig = new TunnelManager.Config(); final AppPreferences multiProcessPreferences = new AppPreferences(this); tunnelManagerConfig.disableTimeouts = multiProcessPreferences .getBoolean(this.getString(R.string.disableTimeoutsPreference), false); String tunnelCoreConfig = TunnelManager.buildTunnelCoreConfig(this, // context tunnelManagerConfig, "upgradechecker", // tempTunnelName "Psiphon_UpgradeChecker_"); // clientPlatformPrefix return tunnelCoreConfig == null ? "" : tunnelCoreConfig; } /** * Called when the tunnel discovers that we're already on the latest version. This indicates * that we can start shutting down. */ @Override public void onClientIsLatestVersion() { log(this, R.string.upgrade_checker_client_is_latest_version, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); shutDownTunnel(); } /** * Called when the tunnel discovers that an upgrade has been downloaded. This indicates that * we should send an intent about it and start shutting down. */ @Override public void onClientUpgradeDownloaded(String filename) { log(this, R.string.upgrade_checker_client_upgrade_downloaded, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); if (mUpgradeDownloaded) { // Because tunnel-core may create multiple server connections and do multiple // handshakes, onClientUpgradeDownloaded may get called multiple times. // We want to avoid sending the intent each time. return; } mUpgradeDownloaded = true; Intent intent = new Intent(this, UpgradeChecker.class); intent.setAction(UPGRADE_FILE_AVAILABLE_INTENT_ACTION); this.sendBroadcast(intent); shutDownTunnel(); } /** * Called when the tunnel has finished shutting down. We'll all done and can release the wakeful intent. * May be due to a connection timeout, or simply an exit triggered by one of the shutDownTunnel() calls. */ @Override public void onExiting() { log(this, R.string.upgrade_checker_tunnel_exiting, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN); releaseWakefulIntent(); } @Override public void onDiagnosticMessage(String message) { log(this, R.string.upgrade_checker_tunnel_diagnostic_message, MyLog.Sensitivity.NOT_SENSITIVE, Log.WARN, message); } @Override public String getAppName() { return getString(R.string.app_name); } @Override public Context getContext() { return this; } @Override public void onConnected() { } @Override public Object getVpnService() { return null; } @Override public Object newVpnServiceBuilder() { return null; } @Override public void onAvailableEgressRegions(List<String> regions) { } @Override public void onSocksProxyPortInUse(int port) { } @Override public void onHttpProxyPortInUse(int port) { } @Override public void onListeningSocksProxyPort(int port) { } @Override public void onListeningHttpProxyPort(int port) { } @Override public void onUpstreamProxyError(String message) { } @Override public void onConnecting() { } @Override public void onHomepage(String url) { } @Override public void onClientRegion(String region) { } @Override public void onSplitTunnelRegion(String region) { } @Override public void onUntunneledAddress(String address) { } @Override public void onBytesTransferred(long sent, long received) { } @Override public void onStartedWaitingForNetworkConnectivity() { } @Override public void onClientVerificationRequired(String serverNonce, int ttlSeconds, boolean resetCache) { } } }