package net.hockeyapp.android; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.View; import android.view.View.OnClickListener; import android.webkit.WebView; import android.widget.Button; import android.widget.TextView; import net.hockeyapp.android.listeners.DownloadFileListener; import net.hockeyapp.android.objects.ErrorObject; import net.hockeyapp.android.tasks.DownloadFileTask; import net.hockeyapp.android.tasks.GetFileSizeTask; import net.hockeyapp.android.utils.AsyncTaskUtils; import net.hockeyapp.android.utils.HockeyLog; import net.hockeyapp.android.utils.Util; import net.hockeyapp.android.utils.VersionHelper; import java.util.Locale; /** * <h3>Description</h3> * * Activity to show update information and start the download * process if the user taps the corresponding button. * **/ public class UpdateActivity extends Activity implements UpdateActivityInterface, UpdateInfoListener, OnClickListener { /** * Parameter to supply the download URL of the update's APK */ public static final String EXTRA_URL = "url"; /** * Parameter to supply metadata about the update in JSON format */ public static final String EXTRA_JSON = "json"; private static final int DIALOG_ERROR_ID = 0; /** * Task to download the .apk file. */ protected DownloadFileTask mDownloadTask; /** * Helper for version management. */ protected VersionHelper mVersionHelper; private ErrorObject mError; private Context mContext; /** * Called when the activity is starting. Sets the title and content view. * Configures the list view adapter. Attaches itself to a previously * started download task. * * @param savedInstanceState Data it most recently supplied in * onSaveInstanceState(Bundle) */ @SuppressWarnings("deprecation") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle("App Update"); setContentView(getLayoutView()); mContext = this; mVersionHelper = new VersionHelper(this, getIntent().getStringExtra(EXTRA_JSON), this); configureView(); mDownloadTask = (DownloadFileTask) getLastNonConfigurationInstance(); if (mDownloadTask != null) { mDownloadTask.attach(this); } } /** * Detaches the activity from the download task and returns the task * as last instance. This way the task is restored when the activity * is immediately re-created. * * @return The download task if present. */ @Override public Object onRetainNonConfigurationInstance() { if (mDownloadTask != null) { mDownloadTask.detach(); } return mDownloadTask; } @Override protected Dialog onCreateDialog(int id) { return onCreateDialog(id, null); } @Override protected Dialog onCreateDialog(int id, Bundle args) { switch (id) { case DIALOG_ERROR_ID: return new AlertDialog.Builder(this) .setMessage("An error has occured") .setCancelable(false) .setTitle("Error") .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { mError = null; dialog.cancel(); } }).create(); } return null; } @Override protected void onPrepareDialog(int id, Dialog dialog) { switch (id) { case DIALOG_ERROR_ID: AlertDialog messageDialogError = (AlertDialog) dialog; if (mError != null) { /** If the ErrorObject is not null, display the ErrorObject message */ messageDialogError.setMessage(mError.getMessage()); } else { /** If the ErrorObject is null, display the general error message */ messageDialogError.setMessage("An unknown error has occured."); } break; } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { enableUpdateButton(); if (permissions.length == 0 || grantResults.length == 0) { // User cancelled permissions dialog -> don't do anything. return; } if (requestCode == Constants.UPDATE_PERMISSIONS_REQUEST) { // Check for the grant result on write permission if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission granted, re-invoke download process prepareDownload(); } else { // Permission denied, show user alert HockeyLog.warn("User denied write permission, can't continue with updater task."); UpdateManagerListener listener = UpdateManager.getLastListener(); if (listener != null) { listener.onUpdatePermissionsNotGranted(); } else { final UpdateActivity updateActivity = this; new AlertDialog.Builder(mContext) .setTitle(getString(R.string.hockeyapp_permission_update_title)) .setMessage(getString(R.string.hockeyapp_permission_update_message)) .setNegativeButton(getString(R.string.hockeyapp_permission_dialog_negative_button), null) .setPositiveButton(getString(R.string.hockeyapp_permission_dialog_positive_button), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { updateActivity.prepareDownload(); } }) .create() .show(); } } } } /** * Returns the current version of the app. * * @return The version code as integer. */ public int getCurrentVersionCode() { int currentVersionCode = -1; try { currentVersionCode = getPackageManager().getPackageInfo(this.getPackageName(), PackageManager.GET_META_DATA).versionCode; } catch (NameNotFoundException e) { } return currentVersionCode; } /** * Creates and returns a new instance of the update view. * * @return Update view */ @SuppressLint("InflateParams") public View getLayoutView() { return getLayoutInflater().inflate(R.layout.hockeyapp_activity_update, null); } /** * Called when the download button is tapped. Starts the download task and * disables the button to avoid multiple taps. */ public void onClick(View v) { prepareDownload(); v.setEnabled(false); } /** * Configures the content view by setting app name, the current version * and the listener for the download button. */ protected void configureView() { TextView nameLabel = (TextView) findViewById(R.id.label_title); nameLabel.setText(getAppName()); nameLabel.setContentDescription(getAppName()); final TextView versionLabel = (TextView) findViewById(R.id.label_version); final String versionString = "Version " + mVersionHelper.getVersionString(); final String fileDate = mVersionHelper.getFileDateString(); String appSizeString = "Unknown size"; long appSize = mVersionHelper.getFileSizeBytes(); if (appSize >= 0L) { appSizeString = String.format(Locale.US, "%.2f", appSize / (1024.0f * 1024.0f)) + " MB"; } else { GetFileSizeTask task = new GetFileSizeTask(this, getIntent().getStringExtra(EXTRA_URL), new DownloadFileListener() { @Override public void downloadSuccessful(DownloadFileTask task) { if (task instanceof GetFileSizeTask) { long appSize = ((GetFileSizeTask) task).getSize(); String appSizeString = String.format(Locale.US, "%.2f", appSize / (1024.0f * 1024.0f)) + " MB"; versionLabel.setText(getString(R.string.hockeyapp_update_version_details_label, versionString, fileDate, appSizeString)); versionLabel.setContentDescription(versionLabel.getText()); } } }); AsyncTaskUtils.execute(task); } versionLabel.setText(getString(R.string.hockeyapp_update_version_details_label, versionString, fileDate, appSizeString)); Button updateButton = (Button) findViewById(R.id.button_update); updateButton.setOnClickListener(this); WebView webView = (WebView) findViewById(R.id.web_update_details); webView.clearCache(true); webView.destroyDrawingCache(); webView.loadDataWithBaseURL(Constants.BASE_URL, getReleaseNotes(), "text/html", "utf-8", null); } /** * Returns the release notes as HTML. * * @return String with release notes. */ protected String getReleaseNotes() { return mVersionHelper.getReleaseNotes(false); } /** * Starts the download task for the app and sets the listener * for a successful download, a failed download, and configuration * strings. */ protected void startDownloadTask() { String url = getIntent().getStringExtra("url"); startDownloadTask(url); } /** * Starts the download task and sets the listener for a successful * download, a failed download, and configuration strings. * * @param url URL of file that should be downloaded */ protected void startDownloadTask(String url) { createDownloadTask(url, new DownloadFileListener() { public void downloadFailed(DownloadFileTask task, Boolean userWantsRetry) { if (userWantsRetry) { startDownloadTask(); } else { enableUpdateButton(); } } public void downloadSuccessful(DownloadFileTask task) { enableUpdateButton(); } }); AsyncTaskUtils.execute(mDownloadTask); } protected void createDownloadTask(String url, DownloadFileListener listener) { mDownloadTask = new DownloadFileTask(this, url, listener); } /** * Enables the download button. */ public void enableUpdateButton() { View updateButton = findViewById(R.id.button_update); updateButton.setEnabled(true); } /** * Returns the app's name. * * @return The app's name as a String. */ public String getAppName() { try { PackageManager pm = getPackageManager(); ApplicationInfo applicationInfo = pm.getApplicationInfo(getPackageName(), 0); return pm.getApplicationLabel(applicationInfo).toString(); } catch (NameNotFoundException exception) { return ""; } } /** * Checks if WRITE_EXTERNAL_STORAGE permission was added to the {@link Manifest} file * * @param context * @return */ private boolean isWriteExternalStorageSet(Context context) { String permission = "android.permission.WRITE_EXTERNAL_STORAGE"; int res = context.checkCallingOrSelfPermission(permission); return (res == PackageManager.PERMISSION_GRANTED); } /** * Checks if Unknown Sources is checked from {@link Settings} * * @return */ @SuppressLint("InlinedApi") @SuppressWarnings("deprecation") private boolean isUnknownSourcesChecked() { try { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { return (Settings.Global.getInt(getContentResolver(), Settings.Global.INSTALL_NON_MARKET_APPS) == 1); } else { return (Settings.Secure.getInt(getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS) == 1); } } catch (Settings.SettingNotFoundException e) { return true; } } protected void prepareDownload() { if (!Util.isConnectedToNetwork(mContext)) { mError = new ErrorObject(); mError.setMessage(getString(R.string.hockeyapp_error_no_network_message)); runOnUiThread(new Runnable() { @SuppressWarnings("deprecation") public void run() { showDialog(DIALOG_ERROR_ID); } }); return; } if (!isWriteExternalStorageSet(mContext)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Only if we're running on Android M or later we can request permissions at runtime requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Constants.UPDATE_PERMISSIONS_REQUEST); return; } mError = new ErrorObject(); mError.setMessage("The permission to access the external storage permission is not set. Please contact the developer."); runOnUiThread(new Runnable() { @SuppressWarnings("deprecation") @Override public void run() { showDialog(DIALOG_ERROR_ID); } }); return; } if (!isUnknownSourcesChecked()) { mError = new ErrorObject(); mError.setMessage("The installation from unknown sources is not enabled. Please check the device settings."); runOnUiThread(new Runnable() { @SuppressWarnings("deprecation") @Override public void run() { showDialog(DIALOG_ERROR_ID); } }); return; } startDownloadTask(); } }