/*
 * Copyright (c) 2015, Nordic Semiconductor
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package no.nordicsemi.android.nrftoolbox.dfu;

import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.LoaderManager.LoaderCallbacks;
import android.app.NotificationManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.Loader;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import no.nordicsemi.android.dfu.DfuProgressListener;
import no.nordicsemi.android.dfu.DfuProgressListenerAdapter;
import no.nordicsemi.android.dfu.DfuServiceInitiator;
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;
import no.nordicsemi.android.nrftoolbox.AppHelpFragment;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.dfu.adapter.FileBrowserAppsAdapter;
import no.nordicsemi.android.nrftoolbox.dfu.fragment.UploadCancelFragment;
import no.nordicsemi.android.nrftoolbox.dfu.fragment.ZipInfoFragment;
import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsActivity;
import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsFragment;
import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment;

/**
 * DfuActivity is the main DFU activity It implements DFUManagerCallbacks to receive callbacks from
 * DfuManager class It implements DeviceScannerFragment.OnDeviceSelectedListener callback to receive callback when device is selected from scanning dialog The activity supports portrait and
 * landscape orientations
 */
public class DfuActivity extends AppCompatActivity implements LoaderCallbacks<Cursor>, ScannerFragment.OnDeviceSelectedListener,
		UploadCancelFragment.CancelFragmentListener {
	private static final String TAG = "DfuActivity";

	private static final String PREFS_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_DEVICE_NAME";
	private static final String PREFS_FILE_NAME = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_NAME";
	private static final String PREFS_FILE_TYPE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_TYPE";
	private static final String PREFS_FILE_SCOPE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_SCOPE";
	private static final String PREFS_FILE_SIZE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_SIZE";

	private static final String DATA_DEVICE = "device";
	private static final String DATA_FILE_TYPE = "file_type";
	private static final String DATA_FILE_TYPE_TMP = "file_type_tmp";
	private static final String DATA_FILE_PATH = "file_path";
	private static final String DATA_FILE_STREAM = "file_stream";
	private static final String DATA_INIT_FILE_PATH = "init_file_path";
	private static final String DATA_INIT_FILE_STREAM = "init_file_stream";
	private static final String DATA_STATUS = "status";
	private static final String DATA_SCOPE = "scope";
	private static final String DATA_DFU_COMPLETED = "dfu_completed";
	private static final String DATA_DFU_ERROR = "dfu_error";

	private static final String EXTRA_URI = "uri";

	private static final int ENABLE_BT_REQ = 0;
	private static final int SELECT_FILE_REQ = 1;
	private static final int SELECT_INIT_FILE_REQ = 2;

	private TextView deviceNameView;
	private TextView fileNameView;
	private TextView fileTypeView;
	private TextView fileScopeView;
	private TextView fileSizeView;
	private TextView fileStatusView;
	private TextView textPercentage;
	private TextView textUploading;
	private ProgressBar progressBar;

	private Button selectFileButton, uploadButton, connectButton;

	private BluetoothDevice selectedDevice;
	private String filePath;
	private Uri fileStreamUri;
	private String initFilePath;
	private Uri initFileStreamUri;
	private int fileType;
	private int fileTypeTmp; // This value is being used when user is selecting a file not to overwrite the old value (in case he/she will cancel selecting file)
	private Integer scope;
	private boolean statusOk;
	/** Flag set to true in {@link #onRestart()} and to false in {@link #onPause()}. */
	private boolean resumed;
	/** Flag set to true if DFU operation was completed while {@link #resumed} was false. */
	private boolean dfuCompleted;
	/** The error message received from DFU service while {@link #resumed} was false. */
	private String dfuError;

	/**
	 * The progress listener receives events from the DFU Service.
	 * If is registered in onCreate() and unregistered in onDestroy() so methods here may also be called
	 * when the screen is locked or the app went to the background. This is because the UI needs to have the
	 * correct information after user comes back to the activity and this information can't be read from the service
	 * as it might have been killed already (DFU completed or finished with error).
	 */
	private final DfuProgressListener dfuProgressListener = new DfuProgressListenerAdapter() {
		@Override
		public void onDeviceConnecting(@NonNull final String deviceAddress) {
			progressBar.setIndeterminate(true);
			textPercentage.setText(R.string.dfu_status_connecting);
		}

		@Override
		public void onDfuProcessStarting(@NonNull final String deviceAddress) {
			progressBar.setIndeterminate(true);
			textPercentage.setText(R.string.dfu_status_starting);
		}

		@Override
		public void onEnablingDfuMode(@NonNull final String deviceAddress) {
			progressBar.setIndeterminate(true);
			textPercentage.setText(R.string.dfu_status_switching_to_dfu);
		}

		@Override
		public void onFirmwareValidating(@NonNull final String deviceAddress) {
			progressBar.setIndeterminate(true);
			textPercentage.setText(R.string.dfu_status_validating);
		}

		@Override
		public void onDeviceDisconnecting(@NonNull final String deviceAddress) {
			progressBar.setIndeterminate(true);
			textPercentage.setText(R.string.dfu_status_disconnecting);
		}

		@Override
		public void onDfuCompleted(@NonNull final String deviceAddress) {
			textPercentage.setText(R.string.dfu_status_completed);
			if (resumed) {
				// let's wait a bit until we cancel the notification. When canceled immediately it will be recreated by service again.
				new Handler().postDelayed(() -> {
					onTransferCompleted();

					// if this activity is still open and upload process was completed, cancel the notification
					final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
					manager.cancel(DfuService.NOTIFICATION_ID);
				}, 200);
			} else {
				// Save that the DFU process has finished
				dfuCompleted = true;
			}
		}

		@Override
		public void onDfuAborted(@NonNull final String deviceAddress) {
			textPercentage.setText(R.string.dfu_status_aborted);
			// let's wait a bit until we cancel the notification. When canceled immediately it will be recreated by service again.
			new Handler().postDelayed(() -> {
				onUploadCanceled();

				// if this activity is still open and upload process was completed, cancel the notification
				final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
				manager.cancel(DfuService.NOTIFICATION_ID);
			}, 200);
		}

		@Override
		public void onProgressChanged(@NonNull final String deviceAddress, final int percent,
									  final float speed, final float avgSpeed,
									  final int currentPart, final int partsTotal) {
			progressBar.setIndeterminate(false);
			progressBar.setProgress(percent);
			textPercentage.setText(getString(R.string.dfu_uploading_percentage, percent));
			if (partsTotal > 1)
				textUploading.setText(getString(R.string.dfu_status_uploading_part, currentPart, partsTotal));
			else
				textUploading.setText(R.string.dfu_status_uploading);
		}

		@Override
		public void onError(@NonNull final String deviceAddress, final int error, final int errorType, final String message) {
			if (resumed) {
				showErrorMessage(message);

				// We have to wait a bit before canceling notification. This is called before DfuService creates the last notification.
				new Handler().postDelayed(() -> {
					// if this activity is still open and upload process was completed, cancel the notification
					final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
					manager.cancel(DfuService.NOTIFICATION_ID);
				}, 200);
			} else {
				dfuError = message;
			}
		}
	};

	@Override
	protected void onCreate(final Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_feature_dfu);
		isBLESupported();
		if (!isBLEEnabled()) {
			showBLEDialog();
		}
		setGUI();

		// restore saved state
		fileType = DfuService.TYPE_AUTO; // Default
		if (savedInstanceState != null) {
			fileType = savedInstanceState.getInt(DATA_FILE_TYPE);
			fileTypeTmp = savedInstanceState.getInt(DATA_FILE_TYPE_TMP);
			filePath = savedInstanceState.getString(DATA_FILE_PATH);
			fileStreamUri = savedInstanceState.getParcelable(DATA_FILE_STREAM);
			initFilePath = savedInstanceState.getString(DATA_INIT_FILE_PATH);
			initFileStreamUri = savedInstanceState.getParcelable(DATA_INIT_FILE_STREAM);
			selectedDevice = savedInstanceState.getParcelable(DATA_DEVICE);
			statusOk = statusOk || savedInstanceState.getBoolean(DATA_STATUS);
			scope = savedInstanceState.containsKey(DATA_SCOPE) ? savedInstanceState.getInt(DATA_SCOPE) : null;
			uploadButton.setEnabled(selectedDevice != null && statusOk);
			dfuCompleted = savedInstanceState.getBoolean(DATA_DFU_COMPLETED);
			dfuError = savedInstanceState.getString(DATA_DFU_ERROR);
		}

		DfuServiceListenerHelper.registerProgressListener(this, dfuProgressListener);
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
		DfuServiceListenerHelper.unregisterProgressListener(this, dfuProgressListener);
	}

	@Override
	protected void onSaveInstanceState(@NonNull final Bundle outState) {
		super.onSaveInstanceState(outState);
		outState.putInt(DATA_FILE_TYPE, fileType);
		outState.putInt(DATA_FILE_TYPE_TMP, fileTypeTmp);
		outState.putString(DATA_FILE_PATH, filePath);
		outState.putParcelable(DATA_FILE_STREAM, fileStreamUri);
		outState.putString(DATA_INIT_FILE_PATH, initFilePath);
		outState.putParcelable(DATA_INIT_FILE_STREAM, initFileStreamUri);
		outState.putParcelable(DATA_DEVICE, selectedDevice);
		outState.putBoolean(DATA_STATUS, statusOk);
		if (scope != null) outState.putInt(DATA_SCOPE, scope);
		outState.putBoolean(DATA_DFU_COMPLETED, dfuCompleted);
		outState.putString(DATA_DFU_ERROR, dfuError);
	}

	private void setGUI() {
        final Toolbar toolbar = findViewById(R.id.toolbar_actionbar);
        setSupportActionBar(toolbar);
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);

		deviceNameView = findViewById(R.id.device_name);
		fileNameView = findViewById(R.id.file_name);
		fileTypeView = findViewById(R.id.file_type);
		fileScopeView = findViewById(R.id.file_scope);
		fileSizeView = findViewById(R.id.file_size);
		fileStatusView = findViewById(R.id.file_status);
		selectFileButton = findViewById(R.id.action_select_file);
		uploadButton = findViewById(R.id.action_upload);
		connectButton = findViewById(R.id.action_connect);
		textPercentage = findViewById(R.id.textviewProgress);
		textUploading = findViewById(R.id.textviewUploading);
		progressBar = findViewById(R.id.progressbar_file);

		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
		if (isDfuServiceRunning()) {
			// Restore image file information
			deviceNameView.setText(preferences.getString(PREFS_DEVICE_NAME, ""));
			fileNameView.setText(preferences.getString(PREFS_FILE_NAME, ""));
			fileTypeView.setText(preferences.getString(PREFS_FILE_TYPE, ""));
			fileScopeView.setText(preferences.getString(PREFS_FILE_SCOPE, ""));
			fileSizeView.setText(preferences.getString(PREFS_FILE_SIZE, ""));
			fileStatusView.setText(R.string.dfu_file_status_ok);
			statusOk = true;
			showProgressBar();
		}
	}

	@Override
	protected void onResume() {
		super.onResume();
		resumed = true;
		if (dfuCompleted)
			onTransferCompleted();
		if (dfuError != null)
			showErrorMessage(dfuError);
		if (dfuCompleted || dfuError != null) {
			// if this activity is still open and upload process was completed, cancel the notification
			final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
			manager.cancel(DfuService.NOTIFICATION_ID);
			dfuCompleted = false;
			dfuError = null;
		}
	}

	@Override
	protected void onPause() {
		super.onPause();
		resumed = false;
	}

	private void isBLESupported() {
		if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
			showToast(R.string.no_ble);
			finish();
		}
	}

	private boolean isBLEEnabled() {
		final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
		return adapter != null && adapter.isEnabled();
	}

	private void showBLEDialog() {
		final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
		startActivityForResult(enableIntent, ENABLE_BT_REQ);
	}

	private void showDeviceScanningDialog() {
		final ScannerFragment dialog = ScannerFragment.getInstance(null); // Device that is advertising directly does not have the GENERAL_DISCOVERABLE nor LIMITED_DISCOVERABLE flag set.
		dialog.show(getSupportFragmentManager(), "scan_fragment");
	}

	@Override
	public boolean onCreateOptionsMenu(final Menu menu) {
		getMenuInflater().inflate(R.menu.settings_and_about, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(final MenuItem item) {
		switch (item.getItemId()) {
			case android.R.id.home:
				onBackPressed();
				break;
			case R.id.action_about:
				final AppHelpFragment fragment = AppHelpFragment.getInstance(R.string.dfu_about_text);
				fragment.show(getSupportFragmentManager(), "help_fragment");
				break;
			case R.id.action_settings:
				final Intent intent = new Intent(this, SettingsActivity.class);
				startActivity(intent);
				break;
		}
		return true;
	}

	@Override
	protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		if (resultCode != RESULT_OK)
			return;

		switch (requestCode) {
			case SELECT_FILE_REQ: {
				// clear previous data
				fileType = fileTypeTmp;
				filePath = null;
				fileStreamUri = null;

				// and read new one
				final Uri uri = data.getData();
				/*
				 * The URI returned from application may be in 'file' or 'content' schema. 'File' schema allows us to create a File object and read details from if
				 * directly. Data from 'Content' schema must be read by Content Provider. To do that we are using a Loader.
				 */
				if (uri.getScheme().equals("file")) {
					// the direct path to the file has been returned
					final String path = uri.getPath();
					final File file = new File(path);
					filePath = path;

					updateFileInfo(file.getName(), file.length(), fileType);
				} else if (uri.getScheme().equals("content")) {
					// an Uri has been returned
					fileStreamUri = uri;
					// if application returned Uri for streaming, let's us it. Does it works?
					// FIXME both Uris works with Google Drive app. Why both? What's the difference? How about other apps like DropBox?
					final Bundle extras = data.getExtras();
					if (extras != null && extras.containsKey(Intent.EXTRA_STREAM))
						fileStreamUri = extras.getParcelable(Intent.EXTRA_STREAM);

					// file name and size must be obtained from Content Provider
					final Bundle bundle = new Bundle();
					bundle.putParcelable(EXTRA_URI, uri);
					getLoaderManager().restartLoader(SELECT_FILE_REQ, bundle, this);
				}
				break;
			}
			case SELECT_INIT_FILE_REQ: {
				initFilePath = null;
				initFileStreamUri = null;

				// and read new one
				final Uri uri = data.getData();
				/*
				 * The URI returned from application may be in 'file' or 'content' schema. 'File' schema allows us to create a File object and read details from if
				 * directly. Data from 'Content' schema must be read by Content Provider. To do that we are using a Loader.
				 */
				if (uri.getScheme().equals("file")) {
					// the direct path to the file has been returned
					initFilePath = uri.getPath();
					fileStatusView.setText(R.string.dfu_file_status_ok_with_init);
				} else if (uri.getScheme().equals("content")) {
					// an Uri has been returned
					initFileStreamUri = uri;
					// if application returned Uri for streaming, let's us it. Does it works?
					// FIXME both Uris works with Google Drive app. Why both? What's the difference? How about other apps like DropBox?
					final Bundle extras = data.getExtras();
					if (extras != null && extras.containsKey(Intent.EXTRA_STREAM))
						initFileStreamUri = extras.getParcelable(Intent.EXTRA_STREAM);
					fileStatusView.setText(R.string.dfu_file_status_ok_with_init);
				}
				break;
			}
			default:
				break;
		}
	}

	@Override
	public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
		final Uri uri = args.getParcelable(EXTRA_URI);
		/*
		 * Some apps, f.e. Google Drive allow to select file that is not on the device. There is no "_data" column handled by that provider. Let's try to obtain
		 * all columns and than check which columns are present.
		 */
		// final String[] projection = new String[] { MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.DATA };
		return new CursorLoader(this, uri, null /* all columns, instead of projection */, null, null, null);
	}

	@Override
	public void onLoaderReset(final Loader<Cursor> loader) {
		fileNameView.setText(null);
		fileTypeView.setText(null);
		fileSizeView.setText(null);
		filePath = null;
		fileStreamUri = null;
		statusOk = false;
	}

	@Override
	public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
		if (data != null && data.moveToNext()) {
			/*
			 * Here we have to check the column indexes by name as we have requested for all. The order may be different.
			 */
			final String fileName = data.getString(data.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)/* 0 DISPLAY_NAME */);
			final int fileSize = data.getInt(data.getColumnIndex(MediaStore.MediaColumns.SIZE) /* 1 SIZE */);
			String filePath = null;
			final int dataIndex = data.getColumnIndex(MediaStore.MediaColumns.DATA);
			if (dataIndex != -1)
				filePath = data.getString(dataIndex /* 2 DATA */);
			if (!TextUtils.isEmpty(filePath))
				this.filePath = filePath;

			updateFileInfo(fileName, fileSize, fileType);
		} else {
			fileNameView.setText(null);
			fileTypeView.setText(null);
			fileSizeView.setText(null);
			filePath = null;
			fileStreamUri = null;
			fileStatusView.setText(R.string.dfu_file_status_error);
			statusOk = false;
		}
	}

	/**
	 * Updates the file information on UI
	 *
	 * @param fileName file name
	 * @param fileSize file length
	 */
	private void updateFileInfo(final String fileName, final long fileSize, final int fileType) {
		fileNameView.setText(fileName);
		switch (fileType) {
			case DfuService.TYPE_AUTO:
				fileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[0]);
				break;
			case DfuService.TYPE_SOFT_DEVICE:
				fileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[1]);
				break;
			case DfuService.TYPE_BOOTLOADER:
				fileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[2]);
				break;
			case DfuService.TYPE_APPLICATION:
				fileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[3]);
				break;
		}
		fileSizeView.setText(getString(R.string.dfu_file_size_text, fileSize));
		fileScopeView.setText(getString(R.string.not_available));
		final String extension = this.fileType == DfuService.TYPE_AUTO ? "(?i)ZIP" : "(?i)HEX|BIN"; // (?i) =  case insensitive
		final boolean statusOk = this.statusOk = MimeTypeMap.getFileExtensionFromUrl(fileName).matches(extension);
		fileStatusView.setText(statusOk ? R.string.dfu_file_status_ok : R.string.dfu_file_status_invalid);
		uploadButton.setEnabled(selectedDevice != null && statusOk);

		// Ask the user for the Init packet file if HEX or BIN files are selected. In case of a ZIP file the Init packets should be included in the ZIP.
		if (statusOk) {
			if (fileType != DfuService.TYPE_AUTO) {
				scope = null;
				fileScopeView.setText(getString(R.string.not_available));
				new AlertDialog.Builder(this)
						.setTitle(R.string.dfu_file_init_title)
						.setMessage(R.string.dfu_file_init_message)
						.setNegativeButton(R.string.no, (dialog, which) -> {
							initFilePath = null;
							initFileStreamUri = null;
						})
						.setPositiveButton(R.string.yes, (dialog, which) -> {
							final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
							intent.setType(DfuService.MIME_TYPE_OCTET_STREAM);
							intent.addCategory(Intent.CATEGORY_OPENABLE);
							startActivityForResult(intent, SELECT_INIT_FILE_REQ);
						})
						.show();
			} else {
				new AlertDialog.Builder(this).setTitle(R.string.dfu_file_scope_title).setCancelable(false)
						.setSingleChoiceItems(R.array.dfu_file_scope, 0, (dialog, which) -> {
							switch (which) {
								case 0:
									scope = null;
									break;
								case 1:
									scope = DfuServiceInitiator.SCOPE_SYSTEM_COMPONENTS;
									break;
								case 2:
									scope = DfuServiceInitiator.SCOPE_APPLICATION;
									break;
							}
						}).setPositiveButton(R.string.ok, (dialogInterface, i) -> {
							int index;
							if (scope == null) {
								index = 0;
							} else if (scope == DfuServiceInitiator.SCOPE_SYSTEM_COMPONENTS) {
								index = 1;
							} else {
								index = 2;
							}
							fileScopeView.setText(getResources().getStringArray(R.array.dfu_file_scope)[index]);
						}).show();
			}
		}
	}

	/**
	 * Called when the question mark was pressed
	 *
	 * @param view a button that was pressed
	 */
	public void onSelectFileHelpClicked(final View view) {
		new AlertDialog.Builder(this)
				.setTitle(R.string.dfu_help_title)
				.setMessage(R.string.dfu_help_message)
				.setPositiveButton(R.string.ok, null)
				.show();
	}

	/**
	 * Called when Select File was pressed
	 *
	 * @param view a button that was pressed
	 */
	public void onSelectFileClicked(final View view) {
		fileTypeTmp = fileType;
		int index = 0;
		switch (fileType) {
			case DfuService.TYPE_AUTO:
				index = 0;
				break;
			case DfuService.TYPE_SOFT_DEVICE:
				index = 1;
				break;
			case DfuService.TYPE_BOOTLOADER:
				index = 2;
				break;
			case DfuService.TYPE_APPLICATION:
				index = 3;
				break;
		}
		// Show a dialog with file types
		new AlertDialog.Builder(this)
				.setTitle(R.string.dfu_file_type_title)
				.setSingleChoiceItems(R.array.dfu_file_type, index, (dialog, which) -> {
					switch (which) {
						case 0:
							fileTypeTmp = DfuService.TYPE_AUTO;
							break;
						case 1:
							fileTypeTmp = DfuService.TYPE_SOFT_DEVICE;
							break;
						case 2:
							fileTypeTmp = DfuService.TYPE_BOOTLOADER;
							break;
						case 3:
							fileTypeTmp = DfuService.TYPE_APPLICATION;
							break;
					}
				})
				.setPositiveButton(R.string.ok, (dialog, which) -> openFileChooser())
				.setNeutralButton(R.string.dfu_file_info, (dialog, which) -> {
					final ZipInfoFragment fragment = new ZipInfoFragment();
					fragment.show(getSupportFragmentManager(), "help_fragment");
				})
				.setNegativeButton(R.string.cancel, null)
				.show();
	}

	private void openFileChooser() {
		final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
		intent.setType(fileTypeTmp == DfuService.TYPE_AUTO ? DfuService.MIME_TYPE_ZIP : DfuService.MIME_TYPE_OCTET_STREAM);
		intent.addCategory(Intent.CATEGORY_OPENABLE);
		if (intent.resolveActivity(getPackageManager()) != null) {
			// file browser has been found on the device
			startActivityForResult(intent, SELECT_FILE_REQ);
		} else {
			// there is no any file browser app, let's try to download one
			final View customView = getLayoutInflater().inflate(R.layout.app_file_browser, null);
			final ListView appsList = customView.findViewById(android.R.id.list);
			appsList.setAdapter(new FileBrowserAppsAdapter(this));
			appsList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
			appsList.setItemChecked(0, true);
			new AlertDialog.Builder(this)
					.setTitle(R.string.dfu_alert_no_filebrowser_title)
					.setView(customView)
					.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss())
					.setPositiveButton(R.string.ok, (dialog, which) -> {
						final int pos = appsList.getCheckedItemPosition();
						if (pos >= 0) {
							final String query = getResources().getStringArray(R.array.dfu_app_file_browser_action)[pos];
							final Intent storeIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(query));
							startActivity(storeIntent);
						}
					})
					.show();
		}
	}

	/**
	 * Callback of UPDATE/CANCEL button on DfuActivity
	 */
	public void onUploadClicked(final View view) {
		if (isDfuServiceRunning()) {
			showUploadCancelDialog();
			return;
		}

		// Check whether the selected file is a HEX file (we are just checking the extension)
		if (!statusOk) {
			Toast.makeText(this, R.string.dfu_file_status_invalid_message, Toast.LENGTH_LONG).show();
			return;
		}

		// Save current state in order to restore it if user quit the Activity
		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
		final SharedPreferences.Editor editor = preferences.edit();
		editor.putString(PREFS_DEVICE_NAME, selectedDevice.getName());
		editor.putString(PREFS_FILE_NAME, fileNameView.getText().toString());
		editor.putString(PREFS_FILE_TYPE, fileTypeView.getText().toString());
		editor.putString(PREFS_FILE_SCOPE, fileScopeView.getText().toString());
		editor.putString(PREFS_FILE_SIZE, fileSizeView.getText().toString());
		editor.apply();

		showProgressBar();

		final boolean keepBond = preferences.getBoolean(SettingsFragment.SETTINGS_KEEP_BOND, false);
		final boolean forceDfu = preferences.getBoolean(SettingsFragment.SETTINGS_ASSUME_DFU_NODE, false);
		final boolean enablePRNs = preferences.getBoolean(SettingsFragment.SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, Build.VERSION.SDK_INT < Build.VERSION_CODES.M);
		String value = preferences.getString(SettingsFragment.SETTINGS_NUMBER_OF_PACKETS, String.valueOf(DfuServiceInitiator.DEFAULT_PRN_VALUE));
		int numberOfPackets;
		try {
			numberOfPackets = Integer.parseInt(value);
		} catch (final NumberFormatException e) {
			numberOfPackets = DfuServiceInitiator.DEFAULT_PRN_VALUE;
		}

		final DfuServiceInitiator starter = new DfuServiceInitiator(selectedDevice.getAddress())
				.setDeviceName(selectedDevice.getName())
				.setKeepBond(keepBond)
				.setForceDfu(forceDfu)
				.setPacketsReceiptNotificationsEnabled(enablePRNs)
				.setPacketsReceiptNotificationsValue(numberOfPackets)
				.setPrepareDataObjectDelay(400)
				.setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true);
		if (fileType == DfuService.TYPE_AUTO) {
			starter.setZip(fileStreamUri, filePath);
			if (scope != null)
				starter.setScope(scope);
		} else {
			starter.setBinOrHex(fileType, fileStreamUri, filePath).setInitFile(initFileStreamUri, initFilePath);
		}
		starter.start(this, DfuService.class);
	}

	private void showUploadCancelDialog() {
		final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
		final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION);
		pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_PAUSE);
		manager.sendBroadcast(pauseAction);

		final UploadCancelFragment fragment = UploadCancelFragment.getInstance();
		fragment.show(getSupportFragmentManager(), TAG);
	}

	/**
	 * Callback of CONNECT/DISCONNECT button on DfuActivity
	 */
	public void onConnectClicked(final View view) {
		if (isBLEEnabled()) {
			showDeviceScanningDialog();
		} else {
			showBLEDialog();
		}
	}

	@Override
	public void onDeviceSelected(@NonNull final BluetoothDevice device, final String name) {
		selectedDevice = device;
		uploadButton.setEnabled(statusOk);
		deviceNameView.setText(name != null ? name : getString(R.string.not_available));
	}

	@Override
	public void onDialogCanceled() {
		// do nothing
	}

	private void showProgressBar() {
		progressBar.setVisibility(View.VISIBLE);
		textPercentage.setVisibility(View.VISIBLE);
		textPercentage.setText(null);
		textUploading.setText(R.string.dfu_status_uploading);
		textUploading.setVisibility(View.VISIBLE);
		connectButton.setEnabled(false);
		selectFileButton.setEnabled(false);
		uploadButton.setEnabled(true);
		uploadButton.setText(R.string.dfu_action_upload_cancel);
	}

	private void onTransferCompleted() {
		clearUI(true);
		showToast(R.string.dfu_success);
	}

	public void onUploadCanceled() {
		clearUI(false);
		showToast(R.string.dfu_aborted);
	}

	@Override
	public void onCancelUpload() {
		progressBar.setIndeterminate(true);
		textUploading.setText(R.string.dfu_status_aborting);
		textPercentage.setText(null);
	}

	private void showErrorMessage(final String message) {
		clearUI(false);
		showToast("Upload failed: " + message);
	}

	private void clearUI(final boolean clearDevice) {
		progressBar.setVisibility(View.INVISIBLE);
		textPercentage.setVisibility(View.INVISIBLE);
		textUploading.setVisibility(View.INVISIBLE);
		connectButton.setEnabled(true);
		selectFileButton.setEnabled(true);
		uploadButton.setEnabled(false);
		uploadButton.setText(R.string.dfu_action_upload);
		if (clearDevice) {
			selectedDevice = null;
			deviceNameView.setText(R.string.dfu_default_name);
		}
		// Application may have lost the right to these files if Activity was closed during upload (grant uri permission). Clear file related values.
		fileNameView.setText(null);
		fileTypeView.setText(null);
		fileScopeView.setText(null);
		fileSizeView.setText(null);
		fileStatusView.setText(R.string.dfu_file_status_no_file);
		filePath = null;
		fileStreamUri = null;
		initFilePath = null;
		initFileStreamUri = null;
		statusOk = false;
	}

	private void showToast(final int messageResId) {
		Toast.makeText(this, messageResId, Toast.LENGTH_SHORT).show();
	}

	private void showToast(final String message) {
		Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
	}

	private boolean isDfuServiceRunning() {
		final ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
		for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
			if (DfuService.class.getName().equals(service.service.getClassName())) {
				return true;
			}
		}
		return false;
	}
}