Java tutorial
/* * Copyright 2013 Gerhard Klostermeier * * 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 de.syss.MifareClassicTool.Activities; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; import android.net.Uri; import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.Settings; import android.support.v4.app.ActivityCompat; import android.text.Html; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import de.syss.MifareClassicTool.Common; import de.syss.MifareClassicTool.R; /** * Main App entry point showing the main menu. * Some stuff about the App: * <ul> * <li>Error/Debug messages (Log.e()/Log.d()) are hard coded</li> * <li>This is my first App, so please by decent with me ;)</li> * </ul> * @author Gerhard Klostermeier */ public class MainMenu extends Activity { private static final String LOG_TAG = MainMenu.class.getSimpleName(); private final static int FILE_CHOOSER_DUMP_FILE = 1; private final static int FILE_CHOOSER_KEY_FILE = 2; private static final int REQUEST_WRITE_STORAGE_CODE = 1; private AlertDialog mEnableNfc; private Button mReadTag; private Button mWriteTag; private Button mKeyEditor; private Button mDumpEditor; private boolean mResume = true; private Intent mOldIntent = null; /** * Check for NFC hardware, Mifare Classic support and for external storage. * If the directory structure and the std. keys files is not already there * it will be created. Also, at the first run of this App, a warning * notice and a donate message will be displayed. * @see #copyStdKeysFilesIfNecessary() */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main_menu); // Show App version and footer. TextView tv = (TextView) findViewById(R.id.textViewMainFooter); tv.setText(getString(R.string.app_version) + ": " + Common.getVersionCode()); // Add the context menu to the tools button. Button tools = (Button) findViewById(R.id.buttonMainTools); registerForContextMenu(tools); // Bind main layout buttons. mReadTag = (Button) findViewById(R.id.buttonMainReadTag); mWriteTag = (Button) findViewById(R.id.buttonMainWriteTag); mKeyEditor = (Button) findViewById(R.id.buttonMainEditKeyDump); mDumpEditor = (Button) findViewById(R.id.buttonMainEditCardDump); // Check if the user granted the app write permissions. if (Common.hasWritePermissionToExternalStorage(this)) { initFolders(); } else { enableMenuButtons(false); // Request the permission. ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, REQUEST_WRITE_STORAGE_CODE); } // Check if there is an NFC hardware component. Common.setNfcAdapter(NfcAdapter.getDefaultAdapter(this)); if (Common.getNfcAdapter() == null) { createNfcEnableDialog(); mEnableNfc.show(); mReadTag.setEnabled(false); mWriteTag.setEnabled(false); mResume = false; } // Show first usage notice. SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); final Editor sharedEditor = sharedPref.edit(); boolean isFirstRun = sharedPref.getBoolean("is_first_run", true); if (isFirstRun) { new AlertDialog.Builder(this).setTitle(R.string.dialog_first_run_title) .setIcon(android.R.drawable.ic_dialog_alert).setMessage(R.string.dialog_first_run) .setPositiveButton(R.string.action_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); } }).setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { if (Common.IS_DONATE_VERSION) { mResume = true; checkNfc(); } sharedEditor.putBoolean("is_first_run", false); sharedEditor.apply(); } }).show(); mResume = false; } if (Common.IS_DONATE_VERSION) { // Do not show the donate dialog. return; } // Show donate dialog. int currentVersion = 0; try { currentVersion = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode; } catch (NameNotFoundException e) { Log.d(LOG_TAG, "Version not found."); } int lastVersion = sharedPref.getInt("mct_version", currentVersion - 1); boolean showDonateDialog = sharedPref.getBoolean("show_donate_dialog", true); if (lastVersion < currentVersion || showDonateDialog) { // This is either a new version of MCT or the user wants to see // the donate dialog. if (lastVersion < currentVersion) { // Update the version. sharedEditor.putInt("mct_version", currentVersion); sharedEditor.putBoolean("show_donate_dialog", true); sharedEditor.apply(); } View dialogLayout = getLayoutInflater().inflate(R.layout.dialog_donate, (ViewGroup) findViewById(android.R.id.content), false); final CheckBox showDonateDialogCheckBox = (CheckBox) dialogLayout .findViewById(R.id.checkBoxDonateDialog); new AlertDialog.Builder(this).setTitle(R.string.dialog_donate_title) .setIcon(android.R.drawable.ic_dialog_info).setView(dialogLayout) .setPositiveButton(R.string.action_beer_sounds_fine, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Open Google Play for the donate version of MCT. Uri uri = Uri.parse("market://details?id=de." + "syss.MifareClassicToolDonate"); Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri); try { startActivity(goToMarket); } catch (ActivityNotFoundException e) { startActivity( new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store" + "/apps/details?id=de.syss.Mifare" + "ClassicToolDonate"))); } if (showDonateDialogCheckBox.isChecked()) { // Do not show the donate dialog again. sharedEditor.putBoolean("show_donate_dialog", false); sharedEditor.apply(); } mResume = true; checkNfc(); } }).setNegativeButton(R.string.action_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); } }).setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { if (showDonateDialogCheckBox.isChecked()) { // Do not show the donate dialog again. sharedEditor.putBoolean("show_donate_dialog", false); sharedEditor.apply(); } mResume = true; checkNfc(); } }).show(); mResume = false; } // Check if there is Mifare Classic support. if (!Common.useAsEditorOnly() && !Common.hasMifareClassicSupport()) { // Disable read/write tag options. mReadTag.setEnabled(false); mWriteTag.setEnabled(false); CharSequence styledText = Html.fromHtml(getString(R.string.dialog_no_mfc_support_device)); AlertDialog ad = new AlertDialog.Builder(this).setTitle(R.string.dialog_no_mfc_support_device_title) .setMessage(styledText).setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(R.string.action_exit_app, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { finish(); } }).setNegativeButton(R.string.action_continue, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { mResume = true; checkNfc(); } }).setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { finish(); } }).show(); // Make links clickable. ((TextView) ad.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); mResume = false; } } /** * Create the directories needed by MCT and clean out the tmp folder. */ private void initFolders() { if (Common.isExternalStorageWritableErrorToast(this)) { // Create keys directory. File path = new File( Environment.getExternalStoragePublicDirectory(Common.HOME_DIR) + "/" + Common.KEYS_DIR); if (!path.exists() && !path.mkdirs()) { // Could not create directory. Log.e(LOG_TAG, "Error while creating '" + Common.HOME_DIR + "/" + Common.KEYS_DIR + "' directory."); return; } // Create dumps directory. path = new File( Environment.getExternalStoragePublicDirectory(Common.HOME_DIR) + "/" + Common.DUMPS_DIR); if (!path.exists() && !path.mkdirs()) { // Could not create directory. Log.e(LOG_TAG, "Error while creating '" + Common.HOME_DIR + "/" + Common.DUMPS_DIR + "' directory."); return; } // Create tmp directory. path = new File(Environment.getExternalStoragePublicDirectory(Common.HOME_DIR) + "/" + Common.TMP_DIR); if (!path.exists() && !path.mkdirs()) { // Could not create directory. Log.e(LOG_TAG, "Error while creating '" + Common.HOME_DIR + Common.TMP_DIR + "' directory."); return; } // Clean up tmp directory. for (File file : path.listFiles()) { file.delete(); } // Create std. key file if there is none. copyStdKeysFilesIfNecessary(); } } /** * Add a menu with "preferences", "about", etc. to the Activity. */ @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.general_options, menu); return true; } /** * Add the menu with the tools. * It will be shown if the user clicks on "Tools". */ @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); menu.setHeaderTitle(R.string.dialog_tools_menu_title); menu.setHeaderIcon(android.R.drawable.ic_menu_preferences); inflater.inflate(R.menu.tools, menu); // Enable/Disable tag info tool depending on NFC availability. menu.findItem(R.id.menuMainTagInfo) .setEnabled(Common.hasMifareClassicSupport() && !Common.useAsEditorOnly()); // Enable/Disable diff tool depending on write permissions. menu.findItem(R.id.menuMainDiffTool).setEnabled(Common.hasWritePermissionToExternalStorage(this)); } /** * If resuming is allowed because all dependencies from * {@link #onCreate(Bundle)} are satisfied, call * {@link #checkNfc()} * @see #onCreate(Bundle) * @see #checkNfc() */ @Override public void onResume() { super.onResume(); if (Common.hasWritePermissionToExternalStorage(this)) { if (!Common.hasMifareClassicSupport() || Common.useAsEditorOnly()) { mDumpEditor.setEnabled(true); mKeyEditor.setEnabled(true); } else { enableMenuButtons(true); } } else { enableMenuButtons(false); } if (mResume) { checkNfc(); } } /** * Check if NFC adapter is enabled. If not, show the user a dialog and let * him choose between "Goto NFC Setting", "Use Editor Only" and "Exit App". * Also enable NFC foreground dispatch system. * @see Common#enableNfcForegroundDispatch(Activity) */ private void checkNfc() { // Check if the NFC hardware is enabled. if (Common.getNfcAdapter() != null && !Common.getNfcAdapter().isEnabled()) { // NFC is disabled. // Use as editor only? if (!Common.useAsEditorOnly()) { // Show dialog. if (mEnableNfc == null) { createNfcEnableDialog(); } mEnableNfc.show(); } // Disable read/write tag options. mReadTag.setEnabled(false); mWriteTag.setEnabled(false); } else { // NFC is enabled. Hide dialog and enable NFC // foreground dispatch. if (mOldIntent != getIntent()) { int typeCheck = Common.treatAsNewTag(getIntent(), this); if (typeCheck == -1 || typeCheck == -2) { // Device or tag does not support Mifare Classic. // Run the only thing that is possible: The tag info tool. Intent i = new Intent(this, TagInfoTool.class); startActivity(i); } mOldIntent = getIntent(); } Common.enableNfcForegroundDispatch(this); Common.setUseAsEditorOnly(false); if (mEnableNfc == null) { createNfcEnableDialog(); } mEnableNfc.hide(); if (Common.hasMifareClassicSupport() && Common.hasWritePermissionToExternalStorage(this)) { mReadTag.setEnabled(true); mWriteTag.setEnabled(true); } } } /** * Create a dialog that send user to NFC settings if NFC is off (and save * the dialog in {@link #mEnableNfc}). Alternatively the user can choos to * use the App in editor only mode or exit the App. */ private void createNfcEnableDialog() { mEnableNfc = new AlertDialog.Builder(this).setTitle(R.string.dialog_nfc_not_enabled_title) .setMessage(R.string.dialog_nfc_not_enabled).setIcon(android.R.drawable.ic_dialog_info) .setPositiveButton(R.string.action_nfc, new DialogInterface.OnClickListener() { @Override @SuppressLint("InlinedApi") public void onClick(DialogInterface dialog, int which) { // Goto NFC Settings. if (Build.VERSION.SDK_INT >= 16) { startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); } else { startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS)); } } }).setNeutralButton(R.string.action_editor_only, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Only use Editor. Common.setUseAsEditorOnly(true); } }).setNegativeButton(R.string.action_exit_app, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { // Exit the App. finish(); } }).create(); } /** * Disable NFC foreground dispatch system. * @see Common#disableNfcForegroundDispatch(Activity) */ @Override public void onPause() { super.onPause(); Common.disableNfcForegroundDispatch(this); } /** * Handle new Intent as a new tag Intent and if the tag/device does not * support Mifare Classic, then run {@link TagInfoTool}. * @see Common#treatAsNewTag(Intent, android.content.Context) * @see TagInfoTool */ @Override public void onNewIntent(Intent intent) { int typeCheck = Common.treatAsNewTag(intent, this); if (typeCheck == -1 || typeCheck == -2) { // Device or tag does not support Mifare Classic. // Run the only thing that is possible: The tag info tool. Intent i = new Intent(this, TagInfoTool.class); startActivity(i); } } /** * Handle answered permission requests. Until now, the app only asks for * the permission to access the external storage. */ @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case REQUEST_WRITE_STORAGE_CODE: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { initFolders(); mDumpEditor.setEnabled(true); mKeyEditor.setEnabled(true); } else { Toast.makeText(this, R.string.info_write_permission, Toast.LENGTH_LONG).show(); } break; } } /** * Enable or disable all menu buttons which provide functionality that * uses the external storage. * @param enable True to enable the buttons. False to disable them. */ private void enableMenuButtons(boolean enable) { mWriteTag.setEnabled(enable); mReadTag.setEnabled(enable); mKeyEditor.setEnabled(enable); mDumpEditor.setEnabled(enable); } /** * Show the {@link ReadTag}. * @param view The View object that triggered the method * (in this case the read tag button). * @see ReadTag */ public void onShowReadTag(View view) { Intent intent = new Intent(this, ReadTag.class); startActivity(intent); } /** * Show the {@link WriteTag}. * @param view The View object that triggered the method * (in this case the write tag button). * @see WriteTag */ public void onShowWriteTag(View view) { Intent intent = new Intent(this, WriteTag.class); startActivity(intent); } /** * Show the {@link HelpAndInfo}. * @param view The View object that triggered the method * (in this case the help/info button). */ public void onShowHelp(View view) { Intent intent = new Intent(this, HelpAndInfo.class); startActivity(intent); } /** * Show the tools menu (as context menu). * @param view The View object that triggered the method * (in this case the tools button). */ public void onShowTools(View view) { openContextMenu(view); } /** * Open a file chooser ({@link FileChooser}). The * Activity result will be processed in * {@link #onActivityResult(int, int, Intent)}. * If the dump files folder is empty display an additional error * message. * @param view The View object that triggered the method * (in this case the show/edit tag dump button). * @see FileChooser * @see #onActivityResult(int, int, Intent) */ public void onOpenTagDumpEditor(View view) { String dumpsDir = Environment.getExternalStoragePublicDirectory(Common.HOME_DIR) + "/" + Common.DUMPS_DIR; if (Common.isExternalStorageWritableErrorToast(this)) { File file = new File(dumpsDir); if (file.isDirectory() && (file.listFiles() == null || file.listFiles().length == 0)) { Toast.makeText(this, R.string.info_no_dumps, Toast.LENGTH_LONG).show(); } Intent intent = new Intent(this, FileChooser.class); intent.putExtra(FileChooser.EXTRA_DIR, dumpsDir); intent.putExtra(FileChooser.EXTRA_TITLE, getString(R.string.text_open_dump_title)); intent.putExtra(FileChooser.EXTRA_BUTTON_TEXT, getString(R.string.action_open_dump_file)); intent.putExtra(FileChooser.EXTRA_ENABLE_DELETE_FILE, true); startActivityForResult(intent, FILE_CHOOSER_DUMP_FILE); } } /** * Open a file chooser ({@link FileChooser}). The * Activity result will be processed in * {@link #onActivityResult(int, int, Intent)}. * @param view The View object that triggered the method * (in this case the show/edit key button). * @see FileChooser * @see #onActivityResult(int, int, Intent) */ public void onOpenKeyEditor(View view) { if (Common.isExternalStorageWritableErrorToast(this)) { Intent intent = new Intent(this, FileChooser.class); intent.putExtra(FileChooser.EXTRA_DIR, Environment.getExternalStoragePublicDirectory(Common.HOME_DIR) + "/" + Common.KEYS_DIR); intent.putExtra(FileChooser.EXTRA_TITLE, getString(R.string.text_open_key_file_title)); intent.putExtra(FileChooser.EXTRA_BUTTON_TEXT, getString(R.string.action_open_key_file)); intent.putExtra(FileChooser.EXTRA_ENABLE_NEW_FILE, true); intent.putExtra(FileChooser.EXTRA_ENABLE_DELETE_FILE, true); startActivityForResult(intent, FILE_CHOOSER_KEY_FILE); } } /** * Show the {@link Preferences}. */ private void onShowPreferences() { Intent intent = new Intent(this, Preferences.class); startActivity(intent); } /** * Show the about dialog. */ private void onShowAboutDialog() { CharSequence styledText = Html.fromHtml(getString(R.string.dialog_about_mct, Common.getVersionCode())); AlertDialog ad = new AlertDialog.Builder(this).setTitle(R.string.dialog_about_mct_title) .setMessage(styledText).setIcon(R.mipmap.ic_launcher) .setPositiveButton(R.string.action_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Do nothing. } }).create(); ad.show(); // Make links clickable. ((TextView) ad.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); } /** * Handle the user input from the general options menu * (e.g. show the about dialog). * @see #onShowAboutDialog() * @see #onShowPreferences() */ @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection. switch (item.getItemId()) { case R.id.menuMainPreferences: onShowPreferences(); return true; case R.id.menuMainAbout: onShowAboutDialog(); return true; default: return super.onOptionsItemSelected(item); } } /** * Handle (start) the selected tool from the tools menu. * @see TagInfoTool * @see ValueBlockTool * @see AccessConditionTool */ @Override public boolean onContextItemSelected(MenuItem item) { Intent intent; switch (item.getItemId()) { case R.id.menuMainTagInfo: intent = new Intent(this, TagInfoTool.class); startActivity(intent); return true; case R.id.menuMainValueBlockTool: intent = new Intent(this, ValueBlockTool.class); startActivity(intent); return true; case R.id.menuMainAccessConditionTool: intent = new Intent(this, AccessConditionTool.class); startActivity(intent); return true; case R.id.menuMainDiffTool: intent = new Intent(this, DiffTool.class); startActivity(intent); return true; case R.id.menuMainBccTool: intent = new Intent(this, BccTool.class); startActivity(intent); return true; default: return super.onContextItemSelected(item); } } /** * Run the {@link DumpEditor} or the {@link KeyEditor} * if file chooser result is O.K. * @see DumpEditor * @see KeyEditor * @see #onOpenTagDumpEditor(View) * @see #onOpenKeyEditor(View) */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case FILE_CHOOSER_DUMP_FILE: if (resultCode == Activity.RESULT_OK) { Intent intent = new Intent(this, DumpEditor.class); intent.putExtra(FileChooser.EXTRA_CHOSEN_FILE, data.getStringExtra(FileChooser.EXTRA_CHOSEN_FILE)); startActivity(intent); } break; case FILE_CHOOSER_KEY_FILE: if (resultCode == Activity.RESULT_OK) { Intent intent = new Intent(this, KeyEditor.class); intent.putExtra(FileChooser.EXTRA_CHOSEN_FILE, data.getStringExtra(FileChooser.EXTRA_CHOSEN_FILE)); startActivity(intent); } break; } } /** * Copy the standard key files ({@link Common#STD_KEYS} and * {@link Common#STD_KEYS_EXTENDED}) form assets to {@link Common#KEYS_DIR}. * Key files are simple text files. Any plain text editor will do the trick. * All key and dump data from this App is stored in * getExternalStoragePublicDirectory(Common.HOME_DIR) to remain * there after App uninstallation. * @see Common#KEYS_DIR * @see Common#HOME_DIR * @see Common#copyFile(InputStream, OutputStream) */ private void copyStdKeysFilesIfNecessary() { File std = new File(Environment.getExternalStoragePublicDirectory(Common.HOME_DIR) + "/" + Common.KEYS_DIR, Common.STD_KEYS); File extended = new File( Environment.getExternalStoragePublicDirectory(Common.HOME_DIR) + "/" + Common.KEYS_DIR, Common.STD_KEYS_EXTENDED); AssetManager assetManager = getAssets(); if (!std.exists()) { // Copy std.keys. try { InputStream in = assetManager.open(Common.KEYS_DIR + "/" + Common.STD_KEYS); OutputStream out = new FileOutputStream(std); Common.copyFile(in, out); in.close(); out.flush(); out.close(); } catch (IOException e) { Log.e(LOG_TAG, "Error while copying 'std.keys' from assets " + "to external storage."); } } if (!extended.exists()) { // Copy extended-std.keys. try { InputStream in = assetManager.open(Common.KEYS_DIR + "/" + Common.STD_KEYS_EXTENDED); OutputStream out = new FileOutputStream(extended); Common.copyFile(in, out); in.close(); out.flush(); out.close(); } catch (IOException e) { Log.e(LOG_TAG, "Error while copying 'extended-std.keys' " + "from assets to external storage."); } } } }