public class IconDrawable extends Drawable {

    public static final int ANDROID_ACTIONBAR_ICON_SIZE_DP = 24;

    private Context context;

    private Icon icon;

    private TextPaint paint;

    private int size = -1;

    private int alpha = 255;

    /**
     * Create an IconDrawable.
     * @param context Your activity or application context.
     * @param iconKey The icon key you want this drawable to display.
     * @throws IllegalArgumentException if the key doesn't match any icon.
     */
    public IconDrawable(Context context, String iconKey) {
        Icon icon = Iconify.findIconForKey(iconKey);
        if (icon == null) {
            throw new IllegalArgumentException("No icon with that key \"" + iconKey + "\".");
        }
        init(context, icon);
    }

    /**
     * Create an IconDrawable.
     * @param context Your activity or application context.
     * @param icon    The icon you want this drawable to display.
     */
    public IconDrawable(Context context, Icon icon) {
        init(context, icon);
    }

    private void init(Context context, Icon icon) {
        this.context = context;
        this.icon = icon;
        paint = new TextPaint();
        IconFontDescriptorWrapper descriptor = Iconify.findTypefaceOf(icon);
        if (descriptor == null) {
            throw new IllegalStateException("Unable to find the module associated " +
                    "with icon " + icon.key() + ", have you registered the module " +
                    "you are trying to use with Iconify.with(...) in your Application?");
        }
        paint.setTypeface(descriptor.getTypeface(context));
        paint.setStyle(Paint.Style.FILL);
        paint.setTextAlign(Paint.Align.CENTER);
        paint.setUnderlineText(false);
        paint.setColor(Color.BLACK);
        paint.setAntiAlias(true);
    }

    /**
     * Set the size of this icon to the standard Android ActionBar.
     * @return The current IconDrawable for chaining.
     */
    public IconDrawable actionBarSize() {
        return sizeDp(ANDROID_ACTIONBAR_ICON_SIZE_DP);
    }

    /**
     * Set the size of the drawable.
     * @param dimenRes The dimension resource.
     * @return The current IconDrawable for chaining.
     */
    public IconDrawable sizeRes(int dimenRes) {
        return sizePx(context.getResources().getDimensionPixelSize(dimenRes));
    }

    /**
     * Set the size of the drawable.
     * @param size The size in density-independent pixels (dp).
     * @return The current IconDrawable for chaining.
     */
    public IconDrawable sizeDp(int size) {
        return sizePx(convertDpToPx(context, size));
    }

    /**
     * Set the size of the drawable.
     * @param size The size in pixels (px).
     * @return The current IconDrawable for chaining.
     */
    public IconDrawable sizePx(int size) {
        this.size = size;
        setBounds(0, 0, size, size);
        invalidateSelf();
        return this;
    }

    /**
     * Set the color of the drawable.
     * @param color The color, usually from android.graphics.Color or 0xFF012345.
     * @return The current IconDrawable for chaining.
     */
    public IconDrawable color(int color) {
        paint.setColor(color);
        invalidateSelf();
        return this;
    }

    /**
     * Set the color of the drawable.
     * @param colorRes The color resource, from your R file.
     * @return The current IconDrawable for chaining.
     */
    public IconDrawable colorRes(int colorRes) {
        paint.setColor(context.getResources().getColor(colorRes));
        invalidateSelf();
        return this;
    }

    /**
     * Set the alpha of this drawable.
     * @param alpha The alpha, between 0 (transparent) and 255 (opaque).
     * @return The current IconDrawable for chaining.
     */
    public IconDrawable alpha(int alpha) {
        setAlpha(alpha);
        invalidateSelf();
        return this;
    }

    @Override
    public int getIntrinsicHeight() {
        return size;
    }

    @Override
    public int getIntrinsicWidth() {
        return size;
    }

    @Override
    public void draw(Canvas canvas) {
        Rect bounds = getBounds();
        int height = bounds.height();
        paint.setTextSize(height);
        Rect textBounds = new Rect();
        String textValue = String.valueOf(icon.character());
        paint.getTextBounds(textValue, 0, 1, textBounds);
        int textHeight = textBounds.height();
        float textBottom = bounds.top + (height - textHeight) / 2f + textHeight - textBounds.bottom;
        canvas.drawText(textValue, bounds.exactCenterX(), textBottom, paint);
    }

    @Override
    public boolean isStateful() {
        return true;
    }

    @Override
    public boolean setState(int[] stateSet) {
        int oldValue = paint.getAlpha();
        int newValue = isEnabled(stateSet) ? alpha : alpha / 2;
        paint.setAlpha(newValue);
        return oldValue != newValue;
    }

    @Override
    public void setAlpha(int alpha) {
        this.alpha = alpha;
        paint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        paint.setColorFilter(cf);
    }

    @Override
    public void clearColorFilter() {
        paint.setColorFilter(null);
    }

    @Override
    public int getOpacity() {
        return this.alpha;
    }

    /**
     * Sets paint style.
     * @param style to be applied
     */
    public void setStyle(Paint.Style style) {
        paint.setStyle(style);
    }

    // Util
    private boolean isEnabled(int[] stateSet) {
        for (int state : stateSet)
            if (state == android.R.attr.state_enabled)
                return true;
        return false;
    }

    // Util
    private int convertDpToPx(Context context, float dp) {
        return (int) TypedValue.applyDimension(
                COMPLEX_UNIT_DIP, dp,
                context.getResources().getDisplayMetrics());
    }
} public enum FontAwesomeIcons implements Icon {
    fa_glass('\uf000'),
    fa_music('\uf001'),
    fa_search('\uf002'),
    fa_envelope_o('\uf003'),
    fa_heart('\uf004'),
    fa_star('\uf005'),
    fa_star_o('\uf006'),
    fa_user('\uf007'),
    fa_film('\uf008'),
    fa_th_large('\uf009'),
    fa_th('\uf00a'),
    fa_th_list('\uf00b'),
    fa_check('\uf00c'),
    fa_remove('\uf00d'),
    fa_close('\uf00d'),
    fa_times('\uf00d'),
    fa_search_plus('\uf00e'),
    fa_search_minus('\uf010'),
    fa_power_off('\uf011'),
    fa_signal('\uf012'),
    fa_gear('\uf013'),
    fa_cog('\uf013'),
    fa_trash_o('\uf014'),
    fa_home('\uf015'),
    fa_file_o('\uf016'),
    fa_clock_o('\uf017'),
    fa_road('\uf018'),
    fa_download('\uf019'),
    fa_arrow_circle_o_down('\uf01a'),
    fa_arrow_circle_o_up('\uf01b'),
    fa_inbox('\uf01c'),
    fa_play_circle_o('\uf01d'),
    fa_rotate_right('\uf01e'),
    fa_repeat('\uf01e'),
    fa_refresh('\uf021'),
    fa_list_alt('\uf022'),
    fa_lock('\uf023'),
    fa_flag('\uf024'),
    fa_headphones('\uf025'),
    fa_volume_off('\uf026'),
    fa_volume_down('\uf027'),
    fa_volume_up('\uf028'),
    fa_qrcode('\uf029'),
    fa_barcode('\uf02a'),
    fa_tag('\uf02b'),
    fa_tags('\uf02c'),
    fa_book('\uf02d'),
    fa_bookmark('\uf02e'),
    fa_print('\uf02f'),
    fa_camera('\uf030'),
    fa_font('\uf031'),
    fa_bold('\uf032'),
    fa_italic('\uf033'),
    fa_text_height('\uf034'),
    fa_text_width('\uf035'),
    fa_align_left('\uf036'),
    fa_align_center('\uf037'),
    fa_align_right('\uf038'),
    fa_align_justify('\uf039'),
    fa_list('\uf03a'),
    fa_dedent('\uf03b'),
    fa_outdent('\uf03b'),
    fa_indent('\uf03c'),
    fa_video_camera('\uf03d'),
    fa_photo('\uf03e'),
    fa_image('\uf03e'),
    fa_picture_o('\uf03e'),
    fa_pencil('\uf040'),
    fa_map_marker('\uf041'),
    fa_adjust('\uf042'),
    fa_tint('\uf043'),
    fa_edit('\uf044'),
    fa_pencil_square_o('\uf044'),
    fa_share_square_o('\uf045'),
    fa_check_square_o('\uf046'),
    fa_arrows('\uf047'),
    fa_step_backward('\uf048'),
    fa_fast_backward('\uf049'),
    fa_backward('\uf04a'),
    fa_play('\uf04b'),
    fa_pause('\uf04c'),
    fa_stop('\uf04d'),
    fa_forward('\uf04e'),
    fa_fast_forward('\uf050'),
    fa_step_forward('\uf051'),
    fa_eject('\uf052'),
    fa_chevron_left('\uf053'),
    fa_chevron_right('\uf054'),
    fa_plus_circle('\uf055'),
    fa_minus_circle('\uf056'),
    fa_times_circle('\uf057'),
    fa_check_circle('\uf058'),
    fa_question_circle('\uf059'),
    fa_info_circle('\uf05a'),
    fa_crosshairs('\uf05b'),
    fa_times_circle_o('\uf05c'),
    fa_check_circle_o('\uf05d'),
    fa_ban('\uf05e'),
    fa_arrow_left('\uf060'),
    fa_arrow_right('\uf061'),
    fa_arrow_up('\uf062'),
    fa_arrow_down('\uf063'),
    fa_mail_forward('\uf064'),
    fa_share('\uf064'),
    fa_expand('\uf065'),
    fa_compress('\uf066'),
    fa_plus('\uf067'),
    fa_minus('\uf068'),
    fa_asterisk('\uf069'),
    fa_exclamation_circle('\uf06a'),
    fa_gift('\uf06b'),
    fa_leaf('\uf06c'),
    fa_fire('\uf06d'),
    fa_eye('\uf06e'),
    fa_eye_slash('\uf070'),
    fa_warning('\uf071'),
    fa_exclamation_triangle('\uf071'),
    fa_plane('\uf072'),
    fa_calendar('\uf073'),
    fa_random('\uf074'),
    fa_comment('\uf075'),
    fa_magnet('\uf076'),
    fa_chevron_up('\uf077'),
    fa_chevron_down('\uf078'),
    fa_retweet('\uf079'),
    fa_shopping_cart('\uf07a'),
    fa_folder('\uf07b'),
    fa_folder_open('\uf07c'),
    fa_arrows_v('\uf07d'),
    fa_arrows_h('\uf07e'),
    fa_bar_chart_o('\uf080'),
    fa_bar_chart('\uf080'),
    fa_twitter_square('\uf081'),
    fa_facebook_square('\uf082'),
    fa_camera_retro('\uf083'),
    fa_key('\uf084'),
    fa_gears('\uf085'),
    fa_cogs('\uf085'),
    fa_comments('\uf086'),
    fa_thumbs_o_up('\uf087'),
    fa_thumbs_o_down('\uf088'),
    fa_star_half('\uf089'),
    fa_heart_o('\uf08a'),
    fa_sign_out('\uf08b'),
    fa_linkedin_square('\uf08c'),
    fa_thumb_tack('\uf08d'),
    fa_external_link('\uf08e'),
    fa_sign_in('\uf090'),
    fa_trophy('\uf091'),
    fa_github_square('\uf092'),
    fa_upload('\uf093'),
    fa_lemon_o('\uf094'),
    fa_phone('\uf095'),
    fa_square_o('\uf096'),
    fa_bookmark_o('\uf097'),
    fa_phone_square('\uf098'),
    fa_twitter('\uf099'),
    fa_facebook_f('\uf09a'),
    fa_facebook('\uf09a'),
    fa_github('\uf09b'),
    fa_unlock('\uf09c'),
    fa_credit_card('\uf09d'),
    fa_feed('\uf09e'),
    fa_rss('\uf09e'),
    fa_hdd_o('\uf0a0'),
    fa_bullhorn('\uf0a1'),
    fa_bell('\uf0f3'),
    fa_certificate('\uf0a3'),
    fa_hand_o_right('\uf0a4'),
    fa_hand_o_left('\uf0a5'),
    fa_hand_o_up('\uf0a6'),
    fa_hand_o_down('\uf0a7'),
    fa_arrow_circle_left('\uf0a8'),
    fa_arrow_circle_right('\uf0a9'),
    fa_arrow_circle_up('\uf0aa'),
    fa_arrow_circle_down('\uf0ab'),
    fa_globe('\uf0ac'),
    fa_wrench('\uf0ad'),
    fa_tasks('\uf0ae'),
    fa_filter('\uf0b0'),
    fa_briefcase('\uf0b1'),
    fa_arrows_alt('\uf0b2'),
    fa_group('\uf0c0'),
    fa_users('\uf0c0'),
    fa_chain('\uf0c1'),
    fa_link('\uf0c1'),
    fa_cloud('\uf0c2'),
    fa_flask('\uf0c3'),
    fa_cut('\uf0c4'),
    fa_scissors('\uf0c4'),
    fa_copy('\uf0c5'),
    fa_files_o('\uf0c5'),
    fa_paperclip('\uf0c6'),
    fa_save('\uf0c7'),
    fa_floppy_o('\uf0c7'),
    fa_square('\uf0c8'),
    fa_navicon('\uf0c9'),
    fa_reorder('\uf0c9'),
    fa_bars('\uf0c9'),
    fa_list_ul('\uf0ca'),
    fa_list_ol('\uf0cb'),
    fa_strikethrough('\uf0cc'),
    fa_underline('\uf0cd'),
    fa_table('\uf0ce'),
    fa_magic('\uf0d0'),
    fa_truck('\uf0d1'),
    fa_pinterest('\uf0d2'),
    fa_pinterest_square('\uf0d3'),
    fa_google_plus_square('\uf0d4'),
    fa_google_plus('\uf0d5'),
    fa_money('\uf0d6'),
    fa_caret_down('\uf0d7'),
    fa_caret_up('\uf0d8'),
    fa_caret_left('\uf0d9'),
    fa_caret_right('\uf0da'),
    fa_columns('\uf0db'),
    fa_unsorted('\uf0dc'),
    fa_sort('\uf0dc'),
    fa_sort_down('\uf0dd'),
    fa_sort_desc('\uf0dd'),
    fa_sort_up('\uf0de'),
    fa_sort_asc('\uf0de'),
    fa_envelope('\uf0e0'),
    fa_linkedin('\uf0e1'),
    fa_rotate_left('\uf0e2'),
    fa_undo('\uf0e2'),
    fa_legal('\uf0e3'),
    fa_gavel('\uf0e3'),
    fa_dashboard('\uf0e4'),
    fa_tachometer('\uf0e4'),
    fa_comment_o('\uf0e5'),
    fa_comments_o('\uf0e6'),
    fa_flash('\uf0e7'),
    fa_bolt('\uf0e7'),
    fa_sitemap('\uf0e8'),
    fa_umbrella('\uf0e9'),
    fa_paste('\uf0ea'),
    fa_clipboard('\uf0ea'),
    fa_lightbulb_o('\uf0eb'),
    fa_exchange('\uf0ec'),
    fa_cloud_download('\uf0ed'),
    fa_cloud_upload('\uf0ee'),
    fa_user_md('\uf0f0'),
    fa_stethoscope('\uf0f1'),
    fa_suitcase('\uf0f2'),
    fa_bell_o('\uf0a2'),
    fa_coffee('\uf0f4'),
    fa_cutlery('\uf0f5'),
    fa_file_text_o('\uf0f6'),
    fa_building_o('\uf0f7'),
    fa_hospital_o('\uf0f8'),
    fa_ambulance('\uf0f9'),
    fa_medkit('\uf0fa'),
    fa_fighter_jet('\uf0fb'),
    fa_beer('\uf0fc'),
    fa_h_square('\uf0fd'),
    fa_plus_square('\uf0fe'),
    fa_angle_double_left('\uf100'),
    fa_angle_double_right('\uf101'),
    fa_angle_double_up('\uf102'),
    fa_angle_double_down('\uf103'),
    fa_angle_left('\uf104'),
    fa_angle_right('\uf105'),
    fa_angle_up('\uf106'),
    fa_angle_down('\uf107'),
    fa_desktop('\uf108'),
    fa_laptop('\uf109'),
    fa_tablet('\uf10a'),
    fa_mobile_phone('\uf10b'),
    fa_mobile('\uf10b'),
    fa_circle_o('\uf10c'),
    fa_quote_left('\uf10d'),
    fa_quote_right('\uf10e'),
    fa_spinner('\uf110'),
    fa_circle('\uf111'),
    fa_mail_reply('\uf112'),
    fa_reply('\uf112'),
    fa_github_alt('\uf113'),
    fa_folder_o('\uf114'),
    fa_folder_open_o('\uf115'),
    fa_smile_o('\uf118'),
    fa_frown_o('\uf119'),
    fa_meh_o('\uf11a'),
    fa_gamepad('\uf11b'),
    fa_keyboard_o('\uf11c'),
    fa_flag_o('\uf11d'),
    fa_flag_checkered('\uf11e'),
    fa_terminal('\uf120'),
    fa_code('\uf121'),
    fa_mail_reply_all('\uf122'),
    fa_reply_all('\uf122'),
    fa_star_half_empty('\uf123'),
    fa_star_half_full('\uf123'),
    fa_star_half_o('\uf123'),
    fa_location_arrow('\uf124'),
    fa_crop('\uf125'),
    fa_code_fork('\uf126'),
    fa_unlink('\uf127'),
    fa_chain_broken('\uf127'),
    fa_question('\uf128'),
    fa_info('\uf129'),
    fa_exclamation('\uf12a'),
    fa_superscript('\uf12b'),
    fa_subscript('\uf12c'),
    fa_eraser('\uf12d'),
    fa_puzzle_piece('\uf12e'),
    fa_microphone('\uf130'),
    fa_microphone_slash('\uf131'),
    fa_shield('\uf132'),
    fa_calendar_o('\uf133'),
    fa_fire_extinguisher('\uf134'),
    fa_rocket('\uf135'),
    fa_maxcdn('\uf136'),
    fa_chevron_circle_left('\uf137'),
    fa_chevron_circle_right('\uf138'),
    fa_chevron_circle_up('\uf139'),
    fa_chevron_circle_down('\uf13a'),
    fa_html5('\uf13b'),
    fa_css3('\uf13c'),
    fa_anchor('\uf13d'),
    fa_unlock_alt('\uf13e'),
    fa_bullseye('\uf140'),
    fa_ellipsis_h('\uf141'),
    fa_ellipsis_v('\uf142'),
    fa_rss_square('\uf143'),
    fa_play_circle('\uf144'),
    fa_ticket('\uf145'),
    fa_minus_square('\uf146'),
    fa_minus_square_o('\uf147'),
    fa_level_up('\uf148'),
    fa_level_down('\uf149'),
    fa_check_square('\uf14a'),
    fa_pencil_square('\uf14b'),
    fa_external_link_square('\uf14c'),
    fa_share_square('\uf14d'),
    fa_compass('\uf14e'),
    fa_toggle_down('\uf150'),
    fa_caret_square_o_down('\uf150'),
    fa_toggle_up('\uf151'),
    fa_caret_square_o_up('\uf151'),
    fa_toggle_right('\uf152'),
    fa_caret_square_o_right('\uf152'),
    fa_euro('\uf153'),
    fa_eur('\uf153'),
    fa_gbp('\uf154'),
    fa_dollar('\uf155'),
    fa_usd('\uf155'),
    fa_rupee('\uf156'),
    fa_inr('\uf156'),
    fa_cny('\uf157'),
    fa_rmb('\uf157'),
    fa_yen('\uf157'),
    fa_jpy('\uf157'),
    fa_ruble('\uf158'),
    fa_rouble('\uf158'),
    fa_rub('\uf158'),
    fa_won('\uf159'),
    fa_krw('\uf159'),
    fa_bitcoin('\uf15a'),
    fa_btc('\uf15a'),
    fa_file('\uf15b'),
    fa_file_text('\uf15c'),
    fa_sort_alpha_asc('\uf15d'),
    fa_sort_alpha_desc('\uf15e'),
    fa_sort_amount_asc('\uf160'),
    fa_sort_amount_desc('\uf161'),
    fa_sort_numeric_asc('\uf162'),
    fa_sort_numeric_desc('\uf163'),
    fa_thumbs_up('\uf164'),
    fa_thumbs_down('\uf165'),
    fa_youtube_square('\uf166'),
    fa_youtube('\uf167'),
    fa_xing('\uf168'),
    fa_xing_square('\uf169'),
    fa_youtube_play('\uf16a'),
    fa_dropbox('\uf16b'),
    fa_stack_overflow('\uf16c'),
    fa_instagram('\uf16d'),
    fa_flickr('\uf16e'),
    fa_adn('\uf170'),
    fa_bitbucket('\uf171'),
    fa_bitbucket_square('\uf172'),
    fa_tumblr('\uf173'),
    fa_tumblr_square('\uf174'),
    fa_long_arrow_down('\uf175'),
    fa_long_arrow_up('\uf176'),
    fa_long_arrow_left('\uf177'),
    fa_long_arrow_right('\uf178'),
    fa_apple('\uf179'),
    fa_windows('\uf17a'),
    fa_android('\uf17b'),
    fa_linux('\uf17c'),
    fa_dribbble('\uf17d'),
    fa_skype('\uf17e'),
    fa_foursquare('\uf180'),
    fa_trello('\uf181'),
    fa_female('\uf182'),
    fa_male('\uf183'),
    fa_gittip('\uf184'),
    fa_gratipay('\uf184'),
    fa_sun_o('\uf185'),
    fa_moon_o('\uf186'),
    fa_archive('\uf187'),
    fa_bug('\uf188'),
    fa_vk('\uf189'),
    fa_weibo('\uf18a'),
    fa_renren('\uf18b'),
    fa_pagelines('\uf18c'),
    fa_stack_exchange('\uf18d'),
    fa_arrow_circle_o_right('\uf18e'),
    fa_arrow_circle_o_left('\uf190'),
    fa_toggle_left('\uf191'),
    fa_caret_square_o_left('\uf191'),
    fa_dot_circle_o('\uf192'),
    fa_wheelchair('\uf193'),
    fa_vimeo_square('\uf194'),
    fa_turkish_lira('\uf195'),
    fa_try('\uf195'),
    fa_plus_square_o('\uf196'),
    fa_space_shuttle('\uf197'),
    fa_slack('\uf198'),
    fa_envelope_square('\uf199'),
    fa_wordpress('\uf19a'),
    fa_openid('\uf19b'),
    fa_institution('\uf19c'),
    fa_bank('\uf19c'),
    fa_university('\uf19c'),
    fa_mortar_board('\uf19d'),
    fa_graduation_cap('\uf19d'),
    fa_yahoo('\uf19e'),
    fa_google('\uf1a0'),
    fa_reddit('\uf1a1'),
    fa_reddit_square('\uf1a2'),
    fa_stumbleupon_circle('\uf1a3'),
    fa_stumbleupon('\uf1a4'),
    fa_delicious('\uf1a5'),
    fa_digg('\uf1a6'),
    fa_pied_piper('\uf1a7'),
    fa_pied_piper_alt('\uf1a8'),
    fa_drupal('\uf1a9'),
    fa_joomla('\uf1aa'),
    fa_language('\uf1ab'),
    fa_fax('\uf1ac'),
    fa_building('\uf1ad'),
    fa_child('\uf1ae'),
    fa_paw('\uf1b0'),
    fa_spoon('\uf1b1'),
    fa_cube('\uf1b2'),
    fa_cubes('\uf1b3'),
    fa_behance('\uf1b4'),
    fa_behance_square('\uf1b5'),
    fa_steam('\uf1b6'),
    fa_steam_square('\uf1b7'),
    fa_recycle('\uf1b8'),
    fa_automobile('\uf1b9'),
    fa_car('\uf1b9'),
    fa_cab('\uf1ba'),
    fa_taxi('\uf1ba'),
    fa_tree('\uf1bb'),
    fa_spotify('\uf1bc'),
    fa_deviantart('\uf1bd'),
    fa_soundcloud('\uf1be'),
    fa_database('\uf1c0'),
    fa_file_pdf_o('\uf1c1'),
    fa_file_word_o('\uf1c2'),
    fa_file_excel_o('\uf1c3'),
    fa_file_powerpoint_o('\uf1c4'),
    fa_file_photo_o('\uf1c5'),
    fa_file_picture_o('\uf1c5'),
    fa_file_image_o('\uf1c5'),
    fa_file_zip_o('\uf1c6'),
    fa_file_archive_o('\uf1c6'),
    fa_file_sound_o('\uf1c7'),
    fa_file_audio_o('\uf1c7'),
    fa_file_movie_o('\uf1c8'),
    fa_file_video_o('\uf1c8'),
    fa_file_code_o('\uf1c9'),
    fa_vine('\uf1ca'),
    fa_codepen('\uf1cb'),
    fa_jsfiddle('\uf1cc'),
    fa_life_bouy('\uf1cd'),
    fa_life_buoy('\uf1cd'),
    fa_life_saver('\uf1cd'),
    fa_support('\uf1cd'),
    fa_life_ring('\uf1cd'),
    fa_circle_o_notch('\uf1ce'),
    fa_ra('\uf1d0'),
    fa_rebel('\uf1d0'),
    fa_ge('\uf1d1'),
    fa_empire('\uf1d1'),
    fa_git_square('\uf1d2'),
    fa_git('\uf1d3'),
    fa_y_combinator_square('\uf1d4'),
    fa_yc_square('\uf1d4'),
    fa_hacker_news('\uf1d4'),
    fa_tencent_weibo('\uf1d5'),
    fa_qq('\uf1d6'),
    fa_wechat('\uf1d7'),
    fa_weixin('\uf1d7'),
    fa_send('\uf1d8'),
    fa_paper_plane('\uf1d8'),
    fa_send_o('\uf1d9'),
    fa_paper_plane_o('\uf1d9'),
    fa_history('\uf1da'),
    fa_circle_thin('\uf1db'),
    fa_header('\uf1dc'),
    fa_paragraph('\uf1dd'),
    fa_sliders('\uf1de'),
    fa_share_alt('\uf1e0'),
    fa_share_alt_square('\uf1e1'),
    fa_bomb('\uf1e2'),
    fa_soccer_ball_o('\uf1e3'),
    fa_futbol_o('\uf1e3'),
    fa_tty('\uf1e4'),
    fa_binoculars('\uf1e5'),
    fa_plug('\uf1e6'),
    fa_slideshare('\uf1e7'),
    fa_twitch('\uf1e8'),
    fa_yelp('\uf1e9'),
    fa_newspaper_o('\uf1ea'),
    fa_wifi('\uf1eb'),
    fa_calculator('\uf1ec'),
    fa_paypal('\uf1ed'),
    fa_google_wallet('\uf1ee'),
    fa_cc_visa('\uf1f0'),
    fa_cc_mastercard('\uf1f1'),
    fa_cc_discover('\uf1f2'),
    fa_cc_amex('\uf1f3'),
    fa_cc_paypal('\uf1f4'),
    fa_cc_stripe('\uf1f5'),
    fa_bell_slash('\uf1f6'),
    fa_bell_slash_o('\uf1f7'),
    fa_trash('\uf1f8'),
    fa_copyright('\uf1f9'),
    fa_at('\uf1fa'),
    fa_eyedropper('\uf1fb'),
    fa_paint_brush('\uf1fc'),
    fa_birthday_cake('\uf1fd'),
    fa_area_chart('\uf1fe'),
    fa_pie_chart('\uf200'),
    fa_line_chart('\uf201'),
    fa_lastfm('\uf202'),
    fa_lastfm_square('\uf203'),
    fa_toggle_off('\uf204'),
    fa_toggle_on('\uf205'),
    fa_bicycle('\uf206'),
    fa_bus('\uf207'),
    fa_ioxhost('\uf208'),
    fa_angellist('\uf209'),
    fa_cc('\uf20a'),
    fa_shekel('\uf20b'),
    fa_sheqel('\uf20b'),
    fa_ils('\uf20b'),
    fa_meanpath('\uf20c'),
    fa_buysellads('\uf20d'),
    fa_connectdevelop('\uf20e'),
    fa_dashcube('\uf210'),
    fa_forumbee('\uf211'),
    fa_leanpub('\uf212'),
    fa_sellsy('\uf213'),
    fa_shirtsinbulk('\uf214'),
    fa_simplybuilt('\uf215'),
    fa_skyatlas('\uf216'),
    fa_cart_plus('\uf217'),
    fa_cart_arrow_down('\uf218'),
    fa_diamond('\uf219'),
    fa_ship('\uf21a'),
    fa_user_secret('\uf21b'),
    fa_motorcycle('\uf21c'),
    fa_street_view('\uf21d'),
    fa_heartbeat('\uf21e'),
    fa_venus('\uf221'),
    fa_mars('\uf222'),
    fa_mercury('\uf223'),
    fa_intersex('\uf224'),
    fa_transgender('\uf224'),
    fa_transgender_alt('\uf225'),
    fa_venus_double('\uf226'),
    fa_mars_double('\uf227'),
    fa_venus_mars('\uf228'),
    fa_mars_stroke('\uf229'),
    fa_mars_stroke_v('\uf22a'),
    fa_mars_stroke_h('\uf22b'),
    fa_neuter('\uf22c'),
    fa_genderless('\uf22d'),
    fa_facebook_official('\uf230'),
    fa_pinterest_p('\uf231'),
    fa_whatsapp('\uf232'),
    fa_server('\uf233'),
    fa_user_plus('\uf234'),
    fa_user_times('\uf235'),
    fa_hotel('\uf236'),
    fa_bed('\uf236'),
    fa_viacoin('\uf237'),
    fa_train('\uf238'),
    fa_subway('\uf239'),
    fa_medium('\uf23a'),
    fa_yc('\uf23b'),
    fa_y_combinator('\uf23b'),
    fa_optin_monster('\uf23c'),
    fa_opencart('\uf23d'),
    fa_expeditedssl('\uf23e'),
    fa_battery_4('\uf240'),
    fa_battery_full('\uf240'),
    fa_battery_3('\uf241'),
    fa_battery_three_quarters('\uf241'),
    fa_battery_2('\uf242'),
    fa_battery_half('\uf242'),
    fa_battery_1('\uf243'),
    fa_battery_quarter('\uf243'),
    fa_battery_0('\uf244'),
    fa_battery_empty('\uf244'),
    fa_mouse_pointer('\uf245'),
    fa_i_cursor('\uf246'),
    fa_object_group('\uf247'),
    fa_object_ungroup('\uf248'),
    fa_sticky_note('\uf249'),
    fa_sticky_note_o('\uf24a'),
    fa_cc_jcb('\uf24b'),
    fa_cc_diners_club('\uf24c'),
    fa_clone('\uf24d'),
    fa_balance_scale('\uf24e'),
    fa_hourglass_o('\uf250'),
    fa_hourglass_1('\uf251'),
    fa_hourglass_start('\uf251'),
    fa_hourglass_2('\uf252'),
    fa_hourglass_half('\uf252'),
    fa_hourglass_3('\uf253'),
    fa_hourglass_end('\uf253'),
    fa_hourglass('\uf254'),
    fa_hand_grab_o('\uf255'),
    fa_hand_rock_o('\uf255'),
    fa_hand_stop_o('\uf256'),
    fa_hand_paper_o('\uf256'),
    fa_hand_scissors_o('\uf257'),
    fa_hand_lizard_o('\uf258'),
    fa_hand_spock_o('\uf259'),
    fa_hand_pointer_o('\uf25a'),
    fa_hand_peace_o('\uf25b'),
    fa_trademark('\uf25c'),
    fa_registered('\uf25d'),
    fa_creative_commons('\uf25e'),
    fa_gg('\uf260'),
    fa_gg_circle('\uf261'),
    fa_tripadvisor('\uf262'),
    fa_odnoklassniki('\uf263'),
    fa_odnoklassniki_square('\uf264'),
    fa_get_pocket('\uf265'),
    fa_wikipedia_w('\uf266'),
    fa_safari('\uf267'),
    fa_chrome('\uf268'),
    fa_firefox('\uf269'),
    fa_opera('\uf26a'),
    fa_internet_explorer('\uf26b'),
    fa_tv('\uf26c'),
    fa_television('\uf26c'),
    fa_contao('\uf26d'),
    fa_500px('\uf26e'),
    fa_amazon('\uf270'),
    fa_calendar_plus_o('\uf271'),
    fa_calendar_minus_o('\uf272'),
    fa_calendar_times_o('\uf273'),
    fa_calendar_check_o('\uf274'),
    fa_industry('\uf275'),
    fa_map_pin('\uf276'),
    fa_map_signs('\uf277'),
    fa_map_o('\uf278'),
    fa_map('\uf279'),
    fa_commenting('\uf27a'),
    fa_commenting_o('\uf27b'),
    fa_houzz('\uf27c'),
    fa_vimeo('\uf27d'),
    fa_black_tie('\uf27e'),
    fa_fonticons('\uf280'),
    fa_reddit_alien('\uf281'),
    fa_edge('\uf282'),
    fa_credit_card_alt('\uf283'),
    fa_codiepie('\uf284'),
    fa_modx('\uf285'),
    fa_fort_awesome('\uf286'),
    fa_usb('\uf287'),
    fa_product_hunt('\uf288'),
    fa_mixcloud('\uf289'),
    fa_scribd('\uf28a'),
    fa_pause_circle('\uf28b'),
    fa_pause_circle_o('\uf28c'),
    fa_stop_circle('\uf28d'),
    fa_stop_circle_o('\uf28e'),
    fa_shopping_bag('\uf290'),
    fa_shopping_basket('\uf291'),
    fa_hashtag('\uf292'),
    fa_bluetooth('\uf293'),
    fa_bluetooth_b('\uf294'),
    fa_percent('\uf295');

    char character;

    FontAwesomeIcons(char character) {
        this.character = character;
    }

    @Override
    public String key() {
        return name().replace('_', '-');
    }

    @Override
    public char character() {
        return character;
    }
} public class GrblSettingMessageEvent {

    private final GrblLookups lookups;
    private final String message;
    private final static Pattern MESSAGE_REGEX = Pattern.compile("(\\$\\d+)=([^ ]*)\\s?\\(?([^)]*)?\\)?");

    private String setting;
    private String value;
    private String units;
    private String description;
    private String shortDescription;


    public GrblSettingMessageEvent(GrblLookups lookups, String message) {
        this.lookups = lookups;
        this.message = message;
        parse();
    }

    @Override
    public String toString() {
        String descriptionStr = "";
        if (!TextUtils.isEmpty(description)) {
            if (!TextUtils.isEmpty(units)) {
                descriptionStr = " (" + shortDescription + ", " + units + ")";
            } else {
                descriptionStr = " (" + description + ")";
            }
        }

        return String.format("%s = %s   %s", setting, value, descriptionStr);
    }

    public String getSetting() {
        return setting;
    }

    public String getUnits() {
        return units;
    }

    public String getValue() {
        return value;
    }

    public String getDescription() {
        return description;
    }

    private void parse() {
        Matcher m = MESSAGE_REGEX.matcher(message);
        if (m.find()) {
            setting = m.group(1);
            value = m.group(2);

            String[] lookup = lookups.lookup(setting);
            if (lookup != null) {
                units = lookup[2];
                description = lookup[3];
                shortDescription = lookup[1];
            }
        }
    }

} public class JogCommandEvent {

    private String command;
    private String status;

    public JogCommandEvent(String command){
        this.command = command;
    }

    public String getCommand(){ return this.command; }
    public void setCommand(String command){ this.command = command; }

    public String getStatus(){ return this.status; }
    public void setStatus(String status){ this.status = status; }

} public class StreamingCompleteEvent {

    private String message;
    private String fileName;
    private int rowsSent;
    private long timeMillis;
    private String timeTaken;

    public StreamingCompleteEvent(String message){
        this.message = message;
    }

    public StreamingCompleteEvent(String message, String fileName, int rowsSent, long timeMillis, String timeTaken){
        this.message = message;
        this.fileName = fileName;
        this.rowsSent = rowsSent;
        this.timeMillis = timeMillis;
        this.timeTaken = timeTaken;
    }

    public String getMessage(){ return this.message; }
    public void setMessage(String message){ this.message = message; }

    public String getFileName(){ return this.fileName; }
    public void setFileName(String fileName){ this.fileName = fileName; }

    public int getRowsSent(){ return this.rowsSent; }
    public void setRowsSent(int rowsSent){ this.rowsSent = rowsSent; }

    public long getTimeMillis(){ return this.timeMillis; }
    public void setTimeMillis(long timeMillis){ this.timeMillis = timeMillis; }

    public String getTimeTaken(){ return this.timeTaken; }
    public void setTimeTaken(String timeTaken){ this.timeTaken = timeTaken; }

} public class MachineStatusListener extends BaseObservable {

    private static final String TAG = MachineStatusListener.class.getSimpleName();

    private final String emptyString = "";
    private final Double DEFAULT_FEED_RATE          = Constants.DEFAULT_JOGGING_FEED_RATE;

    public static final String STATE_IDLE           = Constants.MACHINE_STATUS_IDLE;
    public static final String STATE_JOG            = Constants.MACHINE_STATUS_JOG;
    public static final String STATE_RUN            = Constants.MACHINE_STATUS_RUN;
    public static final String STATE_HOLD           = Constants.MACHINE_STATUS_HOLD;
    public static final String STATE_ALARM          = Constants.MACHINE_STATUS_ALARM;
    public static final String STATE_CHECK          = Constants.MACHINE_STATUS_CHECK;
    public static final String STATE_SLEEP          = Constants.MACHINE_STATUS_SLEEP;
    public static final String STATE_DOOR           = Constants.MACHINE_STATUS_DOOR;
    public static final String STATE_HOME           = Constants.MACHINE_STATUS_HOME;
    public static final String STATE_NOT_CONNECTED  = Constants.MACHINE_STATUS_NOT_CONNECTED;

    private String state;
    private Integer plannerBuffer = 0;
    private Integer serialRxBuffer = 0;
    private Double feedRate = 0.0;
    private Double spindleSpeed = 0.0;
    private Position lastProbePosition = null;
    private Double toolLengthOffset = 0.0;

    private Position machinePosition = new Position(0.00, 0.00, 0.00);
    private Position workPosition = new Position(0.00, 0.00, 0.00);
    private Position workCoordsOffset = new Position(0.00, 0.00, 0.00);
    private Jogging jogging = new Jogging(0.00, 0.00, DEFAULT_FEED_RATE, false);
    private OverridePercents overridePercents = new OverridePercents(100, 100, 100);
    private EnabledPins enabledPins = new EnabledPins(emptyString);
    private AccessoryStates accessoryStates = new AccessoryStates(emptyString);
    private BuildInfo buildInfo = null;
    private CompileTimeOptions compileTimeOptions = null;
    private ParserState parserState = null;
    private Boolean verboseOutput = false;
	private Boolean laserModeEnabled = false;
	private Boolean ignoreError20 = false;
	private int usbBaudRate = 115200;
	private Boolean singleStepMode = false;

	private String customStartUpString = "";

    private static MachineStatusListener machineStatus = null;
    public static MachineStatusListener getInstance(){
        if(machineStatus == null) machineStatus = new MachineStatusListener();
        return machineStatus;
    }

    public static void resetClass(){
        machineStatus = new MachineStatusListener();
    }

    private MachineStatusListener(){
        this.state = STATE_NOT_CONNECTED;
        this.compileTimeOptions = new CompileTimeOptions(emptyString, Constants.DEFAULT_PLANNER_BUFFER, Constants.DEFAULT_SERIAL_RX_BUFFER);
        this.parserState = new ParserState("G0 G54 G17 G21 G90 G94");
    }

    @Bindable
    public String getCustomStartUpString(){ return this.customStartUpString; }
    public void setCustomStartUpString(String message){
        this.customStartUpString = message;
        notifyPropertyChanged(BR.customStartUpString);
    }

    @Bindable
    public Boolean getIgnoreError20(){ return this.ignoreError20; }
    public void setIgnoreError20(Boolean ignoreError20){
        if(this.ignoreError20 != ignoreError20){
            this.ignoreError20 = ignoreError20;
            notifyPropertyChanged(BR.ignoreError20);
        }
    }

    @Bindable
    public Boolean getSingleStepMode(){ return this.singleStepMode; }
    public void setSingleStepMode(Boolean singleStepMode){
        if(this.singleStepMode != singleStepMode){
            this.singleStepMode = singleStepMode;
            notifyPropertyChanged(BR.singleStepMode);
        }
    }

    @Bindable
    public Boolean getVerboseOutput(){ return this.verboseOutput; }
    public void setVerboseOutput(Boolean verboseOutput){
        if(this.verboseOutput != verboseOutput){
            this.verboseOutput = verboseOutput;
            notifyPropertyChanged(BR.verboseOutput);
        }
    }

    @Bindable
    public Boolean getLaserModeEnabled(){ return this.laserModeEnabled; }
    public void setLaserModeEnabled(Boolean laserModeEnabled){
        this.laserModeEnabled = laserModeEnabled;
        notifyPropertyChanged(BR.laserModeEnabled);
    }

    @Bindable
    public String getState(){ return this.state; }
    public void setState(String state){
        if(!this.state.equals(state)){
            this.state = state;
            notifyPropertyChanged(BR.state);
        }
    }

    @Bindable
    public Integer getPlannerBuffer(){ return this.plannerBuffer; }
    public void setPlannerBuffer(Integer plannerBuffer){
        if(this.plannerBuffer.compareTo(plannerBuffer) !=  0){
            this.plannerBuffer = plannerBuffer;
            notifyPropertyChanged(BR.plannerBuffer);
        }
    }

    @Bindable
    public Integer getSerialRxBuffer(){ return this.serialRxBuffer; }
    public void setSerialRxBuffer(Integer serialRxBuffer){
        if(this.serialRxBuffer.compareTo(serialRxBuffer) != 0){
            this.serialRxBuffer = serialRxBuffer;
            notifyPropertyChanged(BR.serialRxBuffer);
        }
    }

    @Bindable
    public Double getFeedRate(){ return this.feedRate; }
    public void setFeedRate(double feedRate){
        if(this.feedRate.compareTo(feedRate) != 0){
            this.feedRate = feedRate;
            notifyPropertyChanged(BR.feedRate);
        }
    }

    @Bindable
    public Double getSpindleSpeed(){ return this.spindleSpeed; }
    public void setSpindleSpeed(double spindleSpeed){
        if(this.spindleSpeed.compareTo(spindleSpeed) != 0){
            this.spindleSpeed = spindleSpeed;
            notifyPropertyChanged(BR.spindleSpeed);
        }
    }

    @Bindable
    public Position getLastProbePosition(){ return this.lastProbePosition; }
    public void setLastProbePosition(Position lastProbePosition){
        this.lastProbePosition = lastProbePosition;
        notifyPropertyChanged(BR.lastProbePosition);
    }

    @Bindable
    public Double getToolLengthOffset(){ return this.toolLengthOffset; }
    public void setToolLengthOffset(double toolLengthOffset){
        this.toolLengthOffset = toolLengthOffset;
        notifyPropertyChanged(BR.toolLengthOffset);
    }


    @Bindable
    public Position getWorkCoordsOffset(){ return this.workCoordsOffset; }
    public void setWorkCoordsOffset(Position workCoordsOffset){
        if(this.workCoordsOffset.hasChanged(workCoordsOffset)){
            this.workCoordsOffset = workCoordsOffset;
            notifyPropertyChanged(BR.workCoordsOffset);
        }
    }

    @Bindable
    public Position getWorkPosition() { return this.workPosition; }
    public void setWorkPosition(Position workPosition){
        if(this.workPosition.hasChanged(workPosition)){
            this.workPosition = workPosition;
            notifyPropertyChanged(BR.workPosition);
        }
    }

    @Bindable
    public Position getMachinePosition(){ return this.machinePosition; }
    public void setMachinePosition(Position machinePosition){
        if(this.machinePosition.hasChanged(machinePosition)){
            this.machinePosition = machinePosition;
            notifyPropertyChanged(BR.machinePosition);
        }
    }

    @Bindable
    public Jogging getJogging(){ return this.jogging; }
    public void setJogging(double stepXY, double stepZ, double feed, boolean inches){
        Jogging jogging = new Jogging(stepXY, stepZ, feed, inches);
        if(this.jogging.hasChanged(jogging)){
            this.jogging = jogging;
            notifyPropertyChanged(BR.jogging);
        }
    }


    @Bindable
    public OverridePercents getOverridePercents(){ return this.overridePercents; }
    public void setOverridePercents(int feed, int rapid, int spindle){
        OverridePercents v = new OverridePercents(feed, rapid, spindle);
        if(this.overridePercents.hasChanged(v)){
            this.overridePercents = v;
            notifyPropertyChanged(BR.overridePercents);
        }
    }

    @Bindable
    public EnabledPins getEnabledPins(){ return this.enabledPins; }
    public void setEnabledPins(String enabled){
        EnabledPins e = new EnabledPins(enabled);

        if(this.enabledPins.hasChanged(e)){
            this.enabledPins = e;
            notifyPropertyChanged(BR.enabledPins);
        }
    }

    @Bindable
    public AccessoryStates getAccessoryStates(){ return this.accessoryStates; }
    public void setAccessoryStates(String enabled){
        AccessoryStates a = new AccessoryStates(enabled);
        if(this.accessoryStates.hasChanged(a)){
            this.accessoryStates = a;
            notifyPropertyChanged(BR.accessoryStates);
        }
    }

    @Bindable
    public BuildInfo getBuildInfo(){ return this.buildInfo; }
    public void setBuildInfo(BuildInfo buildInfo){
        this.buildInfo = buildInfo;
        notifyPropertyChanged(BR.buildInfo);
    }

    @Bindable
    public CompileTimeOptions getCompileTimeOptions(){ return this.compileTimeOptions; }
    public void setCompileTimeOptions(CompileTimeOptions compileTimeOptions){
        this.compileTimeOptions = compileTimeOptions;
        notifyPropertyChanged(BR.compileTimeOptions);
    }

    @Bindable
    public ParserState getParserState(){ return this.parserState; }
    public void setParserState(String parserState){
        this.parserState = new ParserState(parserState);
        notifyPropertyChanged(BR.parserState);
    }

    public int getUsbBaudRate() {
        return usbBaudRate;
    }

    public void setUsbBaudRate(int usbBaudRate) {
        this.usbBaudRate = usbBaudRate;
    }

    public static class BuildInfo{
        public final Double versionDouble;
        public final Character versionLetter;

        public BuildInfo(double d, char l){
            this.versionDouble = d;
            this.versionLetter = l;
        }
    }

    public static class Jogging{
        public final Double stepXY;
        public final Double stepZ;
        public final Double feed;
        public final Boolean inches;

        public Jogging(double stepXY, double stepZ, double f, boolean i){
            this.stepXY = stepXY; this.stepZ = stepZ; this.feed = f; this.inches = i;
        }

        public boolean hasChanged(Jogging jogging){
            boolean stepChanged = ((jogging.stepXY.compareTo(this.stepXY) != 0) || (jogging.stepZ.compareTo(this.stepZ) != 0));
            boolean feedChanged = (jogging.feed.compareTo(this.feed) != 0);
            boolean inchesChanged = (jogging.inches.compareTo(this.inches) != 0);

            return (stepChanged || feedChanged || inchesChanged);
        }
    }

    public static class OverridePercents{
        public final Integer feed;
        public final Integer rapid;
        public final Integer spindle;

        public OverridePercents(int f, int r, int s){
            this.feed = f; this.rapid = r; this.spindle = s;
        }

        public boolean hasChanged(OverridePercents overridePercents){
            boolean feedChanged = (overridePercents.feed.compareTo(this.feed) != 0);
            boolean rapidChanged = (overridePercents.rapid.compareTo(this.rapid) != 0);
            boolean spindleChanged = (overridePercents.spindle.compareTo(this.spindle) != 0);

            return (feedChanged || rapidChanged || spindleChanged);
        }
    }

    public static class ParserState{
        public final String motionType;
        public final String coordinateSystem;
        public final String planeSelection;
        public final String unitSelection;
        public final String distanceMode;
        public final String feedRateMode;

        public ParserState(String parserState){
            String[] parts = parserState.split("\\s+");

            motionType = parts[0].toUpperCase();
            coordinateSystem = parts[1].toUpperCase();
            planeSelection = parts[2].toUpperCase();
            unitSelection = parts[3].toUpperCase();
            distanceMode = parts[4].toUpperCase();
            feedRateMode = parts[5].toUpperCase();
        }
    }

    public static class EnabledPins{
        final public boolean x;
        final public boolean y;
        final public boolean z;
        final public boolean probe;
        final public boolean door;
        final public boolean hold;
        final public boolean softReset;
        final public boolean cycleStart;

        public EnabledPins(String enabled){
            String enabledUpper = enabled.toUpperCase();
            x = enabledUpper.contains("X");
            y = enabledUpper.contains("Y");
            z = enabledUpper.contains("Z");
            probe = enabledUpper.contains("P");
            door = enabledUpper.contains("D");
            hold = enabledUpper.contains("H");
            softReset = enabledUpper.contains("R");
            cycleStart = enabledUpper.contains("S");
        }

        public boolean hasChanged(EnabledPins enabledPins){
            boolean xChanged = (enabledPins.x != this.x);
            boolean yChanged = (enabledPins.y != this.y);
            boolean zChanged = (enabledPins.z != this.z);
            boolean probeChanged = (enabledPins.probe != this.probe);
            boolean doorChanged = (enabledPins.door != this.door);
            boolean holdChanged = (enabledPins.hold != this.hold);
            boolean softResetChanged = (enabledPins.softReset != this.softReset);
            boolean cycleStartChanged = (enabledPins.cycleStart != this.cycleStart);

            return (xChanged || yChanged || zChanged || probeChanged || doorChanged || holdChanged || softResetChanged || cycleStartChanged);
        }

    }

    public static class AccessoryStates {
        final public boolean spindleCW;
        final public boolean spindleCCW;
        final public boolean flood;
        final public boolean mist;

        public AccessoryStates(String enabled) {
            String enabledUpper = enabled.toUpperCase();
            spindleCW = enabledUpper.contains("S");
            spindleCCW = enabledUpper.contains("C");
            flood = enabledUpper.contains("F");
            mist = enabledUpper.contains("M");
        }

        public boolean hasChanged(AccessoryStates accessoryStates){
            boolean spindleCWChanged = (accessoryStates.spindleCW != this.spindleCW);
            boolean spindleCCWChanged = (accessoryStates.spindleCCW != this.spindleCCW);
            boolean floodChanged = (accessoryStates.flood != this.flood);
            boolean mistChanged = (accessoryStates.mist != mist);

            return (spindleCWChanged || spindleCCWChanged ||floodChanged || mistChanged);
        }
    }

    public static class CompileTimeOptions{

        public final boolean variableSpindle;
        public final boolean lineNumbers;
        public final boolean mistCoolant;
        public final boolean coreXY;
        public final boolean parkingMotion;
        public final boolean homingForceOrigin;
        public final boolean homingSingleAxis;
        public final boolean twoLimitSwitchesOnAxis;
        public final boolean feedRageOverrideInProbeCycles;
        public final boolean restoreAllRom;
        public final boolean restoreRomSettings;
        public final boolean restoreRomParameterData;
        public final boolean writeUserString;
        public final boolean forceSyncOnRomWrite;
        public final boolean forceSyncOnCoordinateChange;
        public final boolean homingInitLock;
        public final int plannerBuffer;
        public final int serialRxBuffer;

        public CompileTimeOptions(String enabled, int plannerBf, int serialRxBf){
            String enabledUpper = enabled.toUpperCase();

            variableSpindle = enabledUpper.contains("V");
            lineNumbers = enabledUpper.contains("N");
            mistCoolant = enabledUpper.contains("M");
            coreXY = enabledUpper.contains("C");
            parkingMotion = enabledUpper.contains("P");
            homingForceOrigin = enabledUpper.contains("Z");
            homingSingleAxis = enabledUpper.contains("H");
            twoLimitSwitchesOnAxis = enabledUpper.contains("T");
            feedRageOverrideInProbeCycles = enabledUpper.contains("A");
            restoreAllRom = enabledUpper.contains("*");
            restoreRomSettings = enabledUpper.contains("$");
            restoreRomParameterData = enabledUpper.contains("#");
            writeUserString = enabledUpper.contains("I");
            forceSyncOnRomWrite = enabledUpper.contains("E");
            forceSyncOnCoordinateChange = enabledUpper.contains("W");
            homingInitLock = enabledUpper.contains("L");
            plannerBuffer = plannerBf;
            serialRxBuffer = serialRxBf;
        }

    }
} public interface Constants {

    double MIN_SUPPORTED_VERSION        = 1.1;

    long GRBL_STATUS_UPDATE_INTERVAL    = 150;
    String USB_BAUD_RATE = "115200";

    // Message types sent from the BluetoothService Handler
    int MESSAGE_STATE_CHANGE            = 1;
    int MESSAGE_READ                    = 2;
    int MESSAGE_WRITE                   = 3;
    int MESSAGE_DEVICE_NAME             = 4;
    int MESSAGE_TOAST                   = 5;
    int REQUEST_READ_PERMISSIONS        = 6;
    int PROBE_TYPE_NORMAL               = 7;
    int PROBE_TYPE_TOOL_OFFSET          = 8;
    int CONNECT_DEVICE_SECURE           = 9;
    int CONNECT_DEVICE_INSECURE         = 10;
    int FILE_PICKER_REQUEST_CODE        = 11;

    int CONSOLE_LOGGER_MAX_SIZE         = 256;
    double DEFAULT_JOGGING_FEED_RATE    = 2400.0;
    int DEFAULT_PLANNER_BUFFER          = 15;
    int DEFAULT_SERIAL_RX_BUFFER        = 128;
    int PROBING_FEED_RATE               = 50;
    int PROBING_PLATE_THICKNESS         = 20;
    int PROBING_DISTANCE                = 15;

    String MACHINE_STATUS_IDLE          = "Idle";
    String MACHINE_STATUS_JOG           = "Jog";
    String MACHINE_STATUS_RUN           = "Run";
    String MACHINE_STATUS_HOLD          = "Hold";
    String MACHINE_STATUS_ALARM         = "Alarm";
    String MACHINE_STATUS_CHECK         = "Check";
    String MACHINE_STATUS_SLEEP         = "Sleep";
    String MACHINE_STATUS_DOOR          = "Door";
    String MACHINE_STATUS_HOME          = "Home";
    String MACHINE_STATUS_NOT_CONNECTED = "Unknown";

    String[] SUPPORTED_FILE_TYPES       = {".tap",".gcode", ".nc", ".ngc", ".fnc", ".txt"};
    String SUPPORTED_FILE_TYPES_STRING  = "^.*\\.(tap|gcode|nc|ngc|cnc|txt|ncc|fnc|dnc|fan|gc|txt|ncg|ncp|fgc)$";

    String JUST_STOP_STREAMING          = "0";
    String STOP_STREAMING_AND_RESET     = "1";
    String DEVICE_NAME                  = "device_name";
    String TOAST                        = "toast";

    String SERIAL_CONNECTION_TYPE_BLUETOOTH         = "bluetooth";
    String SERIAL_CONNECTION_TYPE_USB_OTG           = "usbotg";

    int BLUETOOTH_SERVICE_NOTIFICATION_ID           = 100;
    int FILE_STREAMING_NOTIFICATION_ID              = 101;
    int USB_OTG_SERVICE_NOTIFICATION_ID             = 102;

    String TEXT_CATEGORY_UPDATE                     = "update";
    String TEXT_CATEGORY_LINK                       = "link";
    String TEXT_CATEGORY_PROMOTION                  = "promotion";

} public class FileStreamerIntentService extends IntentService{

    private static final String TAG = FileStreamerIntentService.class.getSimpleName();

    public static final String CHECK_MODE_ENABLED = "CHECK_MODE_ENABLED";
    public static final String SERIAL_CONNECTION_TYPE = "SERIAL_CONNECTION_TYPE";
    private static int MAX_RX_SERIAL_BUFFER = Constants.DEFAULT_SERIAL_RX_BUFFER - 3;
    private static int CURRENT_RX_SERIAL_BUFFER = 0;

    private final LinkedList<Integer> activeCommandSizes = new LinkedList<>();
    private final BlockingQueue<Integer> completedCommands = new ArrayBlockingQueue<>(Constants.DEFAULT_SERIAL_RX_BUFFER);

    private static volatile boolean isServiceRunning = false;
    private static volatile boolean shouldContinue = true;

    public synchronized static boolean getIsServiceRunning(){ return isServiceRunning; }
    private synchronized static void setIsServiceRunning(boolean running){ isServiceRunning = running; }

    public synchronized static boolean getShouldContinue(){ return shouldContinue; }
    public synchronized static void setShouldContinue(boolean b){ shouldContinue = b; }

    private FileSenderListener fileSenderListener;
    private MachineStatusListener machineStatusListener;
    private final Timer jobTimer = new Timer();

    public FileStreamerIntentService() {
        super(FileStreamerIntentService.class.getName());
    }

    @Override
    public void onCreate(){
        super.onCreate();
        EventBus.getDefault().register(this);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();
        clearBuffers();
        jobTimer.cancel();
        setIsServiceRunning(false);
        if(fileSenderListener != null) fileSenderListener.setStatus(FileSenderListener.STATUS_IDLE);
        stopForeground(true);
        EventBus.getDefault().unregister(this);
    }

    @Override
    protected void onHandleIntent(Intent intent){
        fileSenderListener = FileSenderListener.getInstance();
        if(fileSenderListener.getGcodeFile() == null){
            EventBus.getDefault().post(new UiToastEvent(getString(R.string.text_no_gcode_file_selected), true, true));
            return;
        }

        machineStatusListener = MachineStatusListener.getInstance();

        MachineStatusListener.CompileTimeOptions compileTimeOptions = machineStatusListener.getCompileTimeOptions();
        if(compileTimeOptions.serialRxBuffer > 0) MAX_RX_SERIAL_BUFFER = compileTimeOptions.serialRxBuffer - 3;
        Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);

        boolean isCheckMode = intent.getBooleanExtra(CHECK_MODE_ENABLED, false);
        String defaultConnectionType = intent.getStringExtra(SERIAL_CONNECTION_TYPE);

        PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        PowerManager.WakeLock wakeLock = null;
        if (powerManager != null) {
            wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "GrblController:WakeLockTag");
            wakeLock.acquire(24*60*60*1000);
        }


        clearBuffers();
        fileSenderListener.setRowsSent(0);
        fileSenderListener.setJobStartTime(System.currentTimeMillis());

        try {
            jobTimer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    int elapsedTimeSeconds = (int) (System.currentTimeMillis() - fileSenderListener.getJobStartTime())/1000;
                    fileSenderListener.setElapsedTime(String.format(Locale.US ,"%02d:%02d:%02d", elapsedTimeSeconds / 3600, (elapsedTimeSeconds % 3600) / 60, (elapsedTimeSeconds % 60)));
                }
            }, 0, 1000);
        }catch (IllegalStateException ignored){

        }

        fileSenderListener.setStatus(FileSenderListener.STATUS_STREAMING);
        setIsServiceRunning(true);
        EventBus.getDefault().post(new StreamingStartedEvent());

        if(isCheckMode){
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1){
                startForeground(Constants.FILE_STREAMING_NOTIFICATION_ID, getNotification(getString(R.string.text_file_checking_started), fileSenderListener.getGcodeFile().getName()));
            }

            if(defaultConnectionType != null && defaultConnectionType.equals(Constants.SERIAL_CONNECTION_TYPE_BLUETOOTH)){
                this.checkGcodeFile();
            }else{
                this.startStreaming(555);
            }
        }else{
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1){
                startForeground(Constants.FILE_STREAMING_NOTIFICATION_ID, getNotification(getString(R.string.text_file_streaming_started), fileSenderListener.getGcodeFile().getName()));
            }

            this.startStreaming(5);
        }

        this.waitUntilBufferRunOut(true);

        jobTimer.cancel();
        fileSenderListener.setJobEndTime(System.currentTimeMillis());
        fileSenderListener.setStatus(FileSenderListener.STATUS_IDLE);

        if(!getShouldContinue() && machineStatusListener.getLaserModeEnabled()){
            // Stop the laser in case of emergency button is pressed
			EventBus.getDefault().post(new GcodeCommand("M5"));
        }

        clearBuffers();
        setIsServiceRunning(false);

        StreamingCompleteEvent streamingCompleteEvent = new StreamingCompleteEvent("Streaming Completed");
        streamingCompleteEvent.setFileName(fileSenderListener.getGcodeFileName());
        streamingCompleteEvent.setRowsSent(fileSenderListener.getRowsSent());
        streamingCompleteEvent.setTimeMillis(fileSenderListener.getJobEndTime() - fileSenderListener.getJobStartTime());
        streamingCompleteEvent.setTimeTaken(fileSenderListener.getElapsedTime());

        EventBus.getDefault().post(streamingCompleteEvent);

        if(wakeLock != null){
            try{
                wakeLock.release();
            }catch (RuntimeException ignored){}
        }

        stopSelf();
    }

    private void startStreaming(int statusUpdateInterval){

        BufferedReader br; String sCurrentLine;

        try{
            br = new BufferedReader(new FileReader(fileSenderListener.getGcodeFile()));
            int linesSent = 0;
            GcodeCommand gcodeCommand = new GcodeCommand();
            while ((sCurrentLine = br.readLine()) != null) {
                if(!shouldContinue) break;

                gcodeCommand.setCommand(sCurrentLine);
                if(gcodeCommand.getSize() > 1){

                    if(gcodeCommand.getHasRomAccess()){
                        this.waitUntilBufferRunOut(true);
                        streamLine(gcodeCommand);
                        this.waitUntilBufferRunOut();
                        streamLine(new GcodeCommand(GrblUtils.GRBL_VIEW_PARSER_STATE_COMMAND));
                    }else{
                        streamLine(gcodeCommand);
                    }

                    linesSent++;
                }

                if(linesSent%statusUpdateInterval == 0){
                    fileSenderListener.setRowsSent(linesSent);
                }

            }

            br.close();
            fileSenderListener.setRowsSent(linesSent);

        }catch (IOException | NullPointerException ignored){}

    }

    private void checkGcodeFile(){

        try{

            BufferedReader br = new BufferedReader(new FileReader(fileSenderListener.getGcodeFile()));
            int linesSent = 0;
            String sCurrentLine;
            GcodeCommand gcodeCommand = new GcodeCommand();

            while ((sCurrentLine = br.readLine()) != null) {
                if(!shouldContinue) break;
                gcodeCommand.setCommand(sCurrentLine);
                if(gcodeCommand.getCommandString().length() > 0){
                    EventBus.getDefault().post(gcodeCommand);
                    linesSent++;
                }
                if(linesSent%555 == 0) fileSenderListener.setRowsSent(linesSent);
            }
            br.close();

            fileSenderListener.setRowsSent(linesSent);

        }catch (IOException | NullPointerException e){
            Log.e(TAG, e.getMessage(), e);
        }
    }

    private void waitUntilBufferRunOut(){
        this.waitUntilBufferRunOut(false);
    }

    private void waitUntilBufferRunOut(boolean dwell){
        if(dwell) streamLine(new GcodeCommand("G4P0.01"));

        while(CURRENT_RX_SERIAL_BUFFER > 0){
            try {
                completedCommands.take();
                if(activeCommandSizes.size() > 0) CURRENT_RX_SERIAL_BUFFER -= activeCommandSizes.removeFirst();
            } catch (Exception e) {
                Log.e(TAG, e.getMessage(), e);
                return;
            }
        }
    }

    private void streamLine(GcodeCommand gcodeCommand){

        if(machineStatusListener.getSingleStepMode()){
            try {
                EventBus.getDefault().post(gcodeCommand);
                completedCommands.take();
            } catch (InterruptedException ignored) {}
        }else{
            // Wait until there is room, if necessary.
            while (MAX_RX_SERIAL_BUFFER < (CURRENT_RX_SERIAL_BUFFER + gcodeCommand.getSize())) {
                try {
                    completedCommands.take();
                    if(activeCommandSizes.size() > 0) CURRENT_RX_SERIAL_BUFFER -= activeCommandSizes.removeFirst();
                } catch (InterruptedException e) {
                    Log.e(TAG, e.getMessage(), e);
                    return;
                }
            }

            if(getShouldContinue()){
                activeCommandSizes.offer(gcodeCommand.getSize());
                CURRENT_RX_SERIAL_BUFFER += gcodeCommand.getSize();
                EventBus.getDefault().post(gcodeCommand);
            }
        }
    }

    private void clearBuffers(){
        CURRENT_RX_SERIAL_BUFFER = 0;
        if(activeCommandSizes.size() > 0) activeCommandSizes.clear();
        if(completedCommands.size() > 0) completedCommands.clear();
    }

    private Notification getNotification(String title, String message){
        return new NotificationCompat.Builder(getApplicationContext(), NotificationHelper.CHANNEL_SERVICE_ID)
                .setContentTitle(title)
                //.setContentText(message)
                .setStyle(new NotificationCompat.BigTextStyle().bigText(message))
                .setSmallIcon(R.drawable.ic_stat_ic_notification)
                .setColor(getResources().getColor(R.color.colorPrimary))
                .setAutoCancel(true).build();
    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onGrblOkEvent(GrblOkEvent event){
        try {
            completedCommands.put(1);
        } catch (InterruptedException ignored) {}
    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onGrblErrorEvent(GrblErrorEvent event){
        setShouldContinue(event.getErrorCode() == 20 && machineStatusListener.getIgnoreError20());
    }

} public class GrblUsbSerialService extends Service {

    private final String TAG = GrblUsbSerialService.class.getSimpleName();

    public static final String ACTION_USB_READY = "com.felhr.connectivityservices.USB_READY";
    public static final String ACTION_USB_ATTACHED = "android.hardware.usb.action.USB_DEVICE_ATTACHED";
    public static final String ACTION_USB_DETACHED = "android.hardware.usb.action.USB_DEVICE_DETACHED";
    public static final String ACTION_USB_NOT_SUPPORTED = "com.felhr.usbservice.USB_NOT_SUPPORTED";
    public static final String ACTION_NO_USB = "com.felhr.usbservice.NO_USB";
    public static final String ACTION_USB_PERMISSION_GRANTED = "com.felhr.usbservice.USB_PERMISSION_GRANTED";
    public static final String ACTION_USB_PERMISSION_NOT_GRANTED = "com.felhr.usbservice.USB_PERMISSION_NOT_GRANTED";
    public static final String ACTION_USB_DISCONNECTED = "com.felhr.usbservice.USB_DISCONNECTED";
    public static final String ACTION_CDC_DRIVER_NOT_WORKING = "com.felhr.connectivityservices.ACTION_CDC_DRIVER_NOT_WORKING";
    public static final String ACTION_USB_DEVICE_NOT_WORKING = "com.felhr.connectivityservices.ACTION_USB_DEVICE_NOT_WORKING";
    public static final int MESSAGE_FROM_SERIAL_PORT = 0;
    public static final int CTS_CHANGE = 1;
    public static final int DSR_CHANGE = 2;
    private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
    public static boolean SERVICE_CONNECTED = false;
    private static final byte[] BYTE_NEW_LINE = { 0x0A };

    private final IBinder binder = new UsbSerialBinder();

    private Context context;
    private Handler mHandler;
    private UsbManager usbManager;
    private UsbDevice device;
    private UsbDeviceConnection connection;
    private UsbSerialDevice serialPort;

    private boolean serialPortConnected;
    public static volatile boolean isGrblFound = false;

    private SerialUsbCommunicationHandler serialUsbCommunicationHandler;

    private long statusUpdatePoolInterval = Constants.GRBL_STATUS_UPDATE_INTERVAL;

    /*
     * onCreate will be executed when service is started. It configures an IntentFilter to listen for
     * incoming Intents (USB ATTACHED, USB DETACHED...) and it tries to open a serial port.
     */
    @Override
    public void onCreate() {
        super.onCreate();

        this.context = this;
        serialPortConnected = false;
        GrblUsbSerialService.SERVICE_CONNECTED = true;

        setFilter();
        usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
        findSerialPortDevice();

        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1){
            startForeground(Constants.USB_OTG_SERVICE_NOTIFICATION_ID, this.getNotification(null));
        }

        serialUsbCommunicationHandler = new SerialUsbCommunicationHandler(this);
        EventBus.getDefault().register(this);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return Service.START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        unregisterReceiver(usbReceiver);
        GrblUsbSerialService.isGrblFound = false;
        GrblUsbSerialService.SERVICE_CONNECTED = false;

        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1){
            stopForeground(true);
        }

        EventBus.getDefault().unregister(this);
    }

    public void serialWriteByte(byte b){
        byte[] c = {b};
        serialWriteBytes(c);
    }

    public void serialWriteBytes(byte[] data) {
        if (serialPort != null) serialPort.write(data);
    }

    public void serialWriteString(String s){
        this.serialWriteBytes(s.getBytes());
        this.serialWriteBytes(BYTE_NEW_LINE);
        //Log.d("SERIAL WRITE", s);
        serialUsbCommunicationHandler.obtainMessage(Constants.MESSAGE_WRITE, s.length(), -1, s).sendToTarget();
    }


    public void setMessageHandler(Handler grblServiceMessageHandler) {
        this.mHandler = grblServiceMessageHandler;
    }


    /*
     *  Data received from serial port will be received here. Just populate onReceivedData with your code
     *  In this particular example. byte stream is converted to String and send to UI thread to
     *  be treated there.
     */
    private final UsbSerialInterface.UsbReadCallback mCallback = new UsbSerialInterface.UsbReadCallback() {

        private final StringBuffer rxBuffer = new StringBuffer();

        @Override
        public void onReceivedData(byte[] arg0) {
            try{
                int newLineIndex;
                rxBuffer.append(new String(arg0, "UTF-8"));

                while((newLineIndex = rxBuffer.indexOf("\n")) != -1){
                    String readMessage = rxBuffer.substring(0, newLineIndex);
                    rxBuffer.delete(0, newLineIndex+1);
                    serialUsbCommunicationHandler.obtainMessage(Constants.MESSAGE_READ, readMessage.length(), -1, readMessage).sendToTarget();
                }

//                int newLineIndex  = rxBuffer.indexOf("\n");
//                if(newLineIndex > 0){
//                    String readMessage = rxBuffer.substring(0, newLineIndex);
//                    rxBuffer.delete(0, newLineIndex+1);
//                    serialUsbCommunicationHandler.obtainMessage(Constants.MESSAGE_READ, readMessage.length(), -1, readMessage).sendToTarget();
//                }

            }catch (Exception ignored){

            }
        }
    };


    /*
     * State changes in the CTS line will be received here
     */
    private UsbSerialInterface.UsbCTSCallback ctsCallback = new UsbSerialInterface.UsbCTSCallback() {
        @Override
        public void onCTSChanged(boolean state) {
            if(mHandler != null) mHandler.obtainMessage(CTS_CHANGE).sendToTarget();
        }
    };

    /*
     * State changes in the DSR line will be received here
     */
    private UsbSerialInterface.UsbDSRCallback dsrCallback = new UsbSerialInterface.UsbDSRCallback() {
        @Override
        public void onDSRChanged(boolean state) {
            if(mHandler != null) mHandler.obtainMessage(DSR_CHANGE).sendToTarget();
        }
    };

    /*
     * Different notifications from OS will be received here (USB attached, detached, permission responses...)
     * About BroadcastReceiver: http://developer.android.com/reference/android/content/BroadcastReceiver.html
     */
    private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context arg0, Intent arg1){
            if(Objects.equals(arg1.getAction(), ACTION_USB_PERMISSION)){
                boolean granted = Objects.requireNonNull(arg1.getExtras()).getBoolean(UsbManager.EXTRA_PERMISSION_GRANTED);
                if(granted){
                    Intent intent = new Intent(ACTION_USB_PERMISSION_GRANTED);
                    arg0.sendBroadcast(intent);
                    connection = usbManager.openDevice(device);
                    new ConnectionThread().start();
                }else{
                    Intent intent = new Intent(ACTION_USB_PERMISSION_NOT_GRANTED);
                    arg0.sendBroadcast(intent);
                }
            }else if(Objects.equals(arg1.getAction(), ACTION_USB_ATTACHED)) {
                if(!serialPortConnected) findSerialPortDevice();
            } else if (Objects.equals(arg1.getAction(), ACTION_USB_DETACHED)) {
                Intent intent = new Intent(ACTION_USB_DISCONNECTED);
                arg0.sendBroadcast(intent);
                serialUsbCommunicationHandler.stopGrblStatusUpdateService();
                if(serialPortConnected){
                    serialPort.close();
                }
                serialPortConnected = false;
            }
        }
    };

    private void findSerialPortDevice() {
        // This snippet will try to open the first encountered usb device connected, excluding usb root hubs
        try {
            HashMap<String, UsbDevice> usbDevices = usbManager.getDeviceList();
            if (!usbDevices.isEmpty()) {
                boolean keep = true;
                for (Map.Entry<String, UsbDevice> entry : usbDevices.entrySet()) {
                    device = entry.getValue();
                    int deviceVID = device.getVendorId();
                    int devicePID = device.getProductId();

                    if (deviceVID != 0x1d6b && (devicePID != 0x0001 && devicePID != 0x0002 && devicePID != 0x0003) && deviceVID != 0x5c6 && devicePID != 0x904c) {

                        // There is a device connected to our Android device. Try to open it as a Serial Port.
                        requestUserPermission();
                        keep = false;
                    } else {
                        connection = null;
                        device = null;
                    }

                    if (!keep)
                        break;
                }
                if (!keep) {
                    // There is no USB devices connected (but usb host were listed). Send an intent to MainActivity.
                    Intent intent = new Intent(ACTION_NO_USB);
                    sendBroadcast(intent);
                }
            } else {
                // There is no USB devices connected. Send an intent to MainActivity
                Intent intent = new Intent(ACTION_NO_USB);
                sendBroadcast(intent);
            }
        }catch (NullPointerException e){
            // There is no USB devices connected. Send an intent to MainActivity
            Intent intent = new Intent(ACTION_NO_USB);
            sendBroadcast(intent);
        }
    }

    private void setFilter() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_USB_PERMISSION);
        filter.addAction(ACTION_USB_DETACHED);
        filter.addAction(ACTION_USB_ATTACHED);
        registerReceiver(usbReceiver, filter);
    }

    /*
     * Request user permission. The response will be received in the BroadcastReceiver
     */
    private void requestUserPermission() {
        PendingIntent mPendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
        usbManager.requestPermission(device, mPendingIntent);
    }

    public class UsbSerialBinder extends Binder {
        public GrblUsbSerialService getService() {
            return GrblUsbSerialService.this;
        }
    }

    /*
    * A simple thread to open a serial port.
    * Although it should be a fast operation. moving usb operations away from UI thread is a good thing.
    */
    private class ConnectionThread extends Thread {
        @Override
        public void run() {
            serialPort = UsbSerialDevice.createUsbSerialDevice(device, connection);
            if (serialPort != null) {
                if (serialPort.open()) {
                    serialPortConnected = true;
                    serialPort.setBaudRate(MachineStatusListener.getInstance().getUsbBaudRate());
                    serialPort.setDataBits(UsbSerialInterface.DATA_BITS_8);
                    serialPort.setStopBits(UsbSerialInterface.STOP_BITS_1);
                    serialPort.setParity(UsbSerialInterface.PARITY_NONE);

                    /*
                     * Current flow control Options:
                     * UsbSerialInterface.FLOW_CONTROL_OFF
                     * UsbSerialInterface.FLOW_CONTROL_RTS_CTS only for CP2102 and FT232
                     * UsbSerialInterface.FLOW_CONTROL_DSR_DTR only for CP2102 and FT232
                     */
                    serialPort.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF);
                    serialPort.read(mCallback);
                    //serialPort.getCTS(ctsCallback);
                    //serialPort.getDSR(dsrCallback);

                    //
                    // Some Arduinos would need some sleep because firmware wait some time to know whether a new sketch is going
                    // to be uploaded or not

                    try {
                        Thread.sleep(2000); // sleep some. YMMV with different chips.
                    } catch (InterruptedException ignored) {

                    }

                    // Everything went as expected. Send an intent to MainActivity
                    serialWriteByte(GrblUtils.GRBL_RESET_COMMAND);
                    Intent intent = new Intent(ACTION_USB_READY);
                    context.sendBroadcast(intent);
                } else {
                    // Serial port could not be opened, maybe an I/O error or if CDC driver was chosen, it does not really fit
                    // Send an Intent to Main Activity
                    if (serialPort instanceof CDCSerialDevice) {
                        Intent intent = new Intent(ACTION_CDC_DRIVER_NOT_WORKING);
                        context.sendBroadcast(intent);
                    } else {
                        Intent intent = new Intent(ACTION_USB_DEVICE_NOT_WORKING);
                        context.sendBroadcast(intent);
                    }
                }
            } else {
                // No driver for given device, even generic CDC driver could not be loaded
                Intent intent = new Intent(ACTION_USB_NOT_SUPPORTED);
                context.sendBroadcast(intent);
            }
        }
    }

    public long getStatusUpdatePoolInterval(){
        return this.statusUpdatePoolInterval;
    }

    public void setStatusUpdatePoolInterval(long poolInterval){
        this.statusUpdatePoolInterval = poolInterval;
    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onGrblGcodeSendEvent(GcodeCommand event){
        serialWriteString(event.getCommandString());
    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onGrblRealTimeCommandEvent(GrblRealTimeCommandEvent grblRealTimeCommandEvent){
        serialWriteByte(grblRealTimeCommandEvent.getCommand());
    }

    private Notification getNotification(String message){

        if(message == null) message = getString(R.string.text_usb_service_foreground_message);

        return new NotificationCompat.Builder(getApplicationContext(), NotificationHelper.CHANNEL_SERVICE_ID)
                .setContentTitle(getString(R.string.text_usb_service))
                .setContentText(message)
                .setSmallIcon(R.drawable.ic_stat_ic_notification)
                .setColor(getResources().getColor(R.color.colorPrimary))
                .setAutoCancel(true).build();
    }

} public class GrblUtils {

    // Real time
    public static final byte GRBL_PAUSE_COMMAND = '!';
    public static final byte GRBL_RESUME_COMMAND = '~';
    public static final byte GRBL_STATUS_COMMAND = '?';
    public static final byte GRBL_DOOR_COMMAND = (byte)0x84;
    public static final byte GRBL_JOG_CANCEL_COMMAND = (byte)0x85;
    public static final byte GRBL_RESET_COMMAND = 0x18;
    // Non real time
    public static final String GRBL_KILL_ALARM_LOCK_COMMAND = "$X";
    public static final String GRBL_TOGGLE_CHECK_MODE_COMMAND = "$C";
    public static final String GRBL_VIEW_PARSER_STATE_COMMAND = "$G";
    public static final String GRBL_VIEW_SETTINGS_COMMAND = "$$";
    public static final String GRBL_RUN_HOMING_CYCLE = "$H";
    public static final String GRBL_SLEEP_COMMAND = "$SLP";
    public static final String GRBL_BUILD_INFO_COMMAND = "$I";
    public static final String GRBL_VIEW_GCODE_PARAMETERS_COMMAND = "$#";

    // Gcode Commands
    public static final String GCODE_RESET_COORDINATES_TO_ZERO = "G10 L20 P0 X0Y0Z0";
    public static final String GCODE_RESET_COORDINATE_TO_ZERO = "G10 P0 L20 %c0";
    public static final String GCODE_CANCEL_TOOL_OFFSETS = "G49";

    private static final String GCODE_RETURN_TO_ZERO_LOCATION_XY = "G90 G0 X0 Y0";
    private static final String GCODE_RETURN_TO_ZERO_LOCATION_Z0 = "G90 G0 Z0";
    private static final String GCODE_RETURN_TO_ZERO_LOCATION_Z0_IN_MACHINE_CORDS = "G53 G0 Z0";

    public static Boolean isGrblVersionString(final String response) {
        Boolean version = response.toLowerCase().startsWith("grbl");
        return version && (getVersionDouble(response) != -1);
    }

    public static Boolean isSmoothieBoard(final String response){
        return response.toLowerCase().startsWith("smoothie") || response.toLowerCase().startsWith("carbide");
    }

    private final static String VERSION_DOUBLE_REGEX = "[0-9]*\\.[0-9]*";
    private final static Pattern VERSION_DOUBLE_PATTERN = Pattern.compile(VERSION_DOUBLE_REGEX);
    public static double getVersionDouble(final String response) {
        double retValue = -1;
        Matcher matcher = VERSION_DOUBLE_PATTERN.matcher(response);
        if (matcher.find()) {
            retValue = Double.parseDouble(matcher.group(0));
        }
        return retValue;
    }

    private final static String VERSION_LETTER_REGEX = "(?<=[0-9]\\.[0-9])[a-zA-Z]";
    private final static Pattern VERSION_LETTER_PATTERN = Pattern.compile(VERSION_LETTER_REGEX);
    public static Character getVersionLetter(final String response) {
        Character retValue = null;

        // Search for a version.
        Matcher matcher = VERSION_LETTER_PATTERN.matcher(response);
        if (matcher.find()) {
            retValue = matcher.group(0).charAt(0);
            //retValue = Double.parseDouble(matcher.group(0));
        }

        return retValue;
    }

    private static final Pattern TLO_PATTERN = Pattern.compile("^\\[TLO:(.*)]$");
    public static boolean isGrblToolLengthOffsetMessage(final String response){
        return TLO_PATTERN.matcher(response).find();
    }

    public static Double getToolLengthOffset(final String response){
        Matcher matcher = TLO_PATTERN.matcher(response);
        return matcher.find() ? Double.valueOf(matcher.group(1)) : 0.0;
    }

    private static final String PROBE_REGEX = "^\\[PRB:(.*)]$";
    private static final Pattern PROBE_PATTERN = Pattern.compile(PROBE_REGEX);
    public static boolean isGrblProbeMessage(final String response) {
        return PROBE_PATTERN.matcher(response).find();
    }

    public static String getProbeString(final String response){
        Matcher matcher = PROBE_PATTERN.matcher(response);
        return matcher.find() ? matcher.group(1) : null;
    }

    private static final String STATUS_REGEX = "^<.*>$";
    private static final Pattern STATUS_PATTERN = Pattern.compile(STATUS_REGEX);
    public static boolean isGrblStatusString(final String response) {
        return STATUS_PATTERN.matcher(response).find();
    }

    private static final String FEEDBACK_REGEX = "^\\[MSG:.*]$";
    private static final Pattern FEEDBACK_PATTERN = Pattern.compile(FEEDBACK_REGEX);
    public static boolean isGrblFeedbackMessage(final String response) {
        return FEEDBACK_PATTERN.matcher(response).find();
    }

    private static final String BUILD_OPTIONS_REGEX = "^\\[OPT:(.*)]$";
    private static final Pattern BUILD_OPTIONS_PATTERN = Pattern.compile(BUILD_OPTIONS_REGEX);
    public static boolean isBuildOptionsMessage(final String response) {
        return BUILD_OPTIONS_PATTERN.matcher(response).find();
    }

    public static String getBuildOptionString(final String response){
        Matcher matcher = BUILD_OPTIONS_PATTERN.matcher(response);
        return matcher.find() ? matcher.group(1) : "";
    }

    private static final String PARSER_STATE_REGEX = "^\\[GC:(.*)]$";
    private static final Pattern PARSER_STATE_PATTERN = Pattern.compile(PARSER_STATE_REGEX);
    public static boolean isParserStateMessage(final String response){
        return PARSER_STATE_PATTERN.matcher(response).find();
    }

    public static String getParserStateString(final String response){
        Matcher matcher = PARSER_STATE_PATTERN.matcher(response);
        return matcher.find() ? matcher.group(1) : "";
    }

    private static final String SETTING_REGEX = "^\\$\\d+=.+";
    private static final Pattern SETTING_PATTERN = Pattern.compile(SETTING_REGEX);
    public static boolean   isGrblSettingMessage(final String response) {
        return SETTING_PATTERN.matcher(response).find();
    }

    public static boolean isGrblErrorMessage(final String response){
        return response.toLowerCase().startsWith("error:");
    }

    public static boolean isGrblOkMessage(final String response){
        return response.toLowerCase().startsWith("ok");
    }

    public static boolean isGrblAlarmMessage(final String response){
        return response.toLowerCase().startsWith("alarm:");
    }

    public static final Pattern machinePattern = Pattern.compile("(?<=MPos:)(-?\\d*\\..\\d*),(-?\\d*\\..\\d*),(-?\\d*\\..\\d*)");
    public static final Pattern workPattern = Pattern.compile("(?<=WPos:)(-?\\d*\\..\\d*),(-?\\d*\\..\\d*),(-?\\d*\\..\\d*)");
    public static final Pattern wcoPattern = Pattern.compile("(?<=WCO:)(-?\\d*\\..\\d*),(-?\\d*\\..\\d*),(-?\\d*\\..\\d*)");
    public static Position getPositionFromStatusString(final String status, final Pattern pattern) {
        Matcher matcher = pattern.matcher(status);
        if (matcher.find()) {
            return new Position(Double.parseDouble(matcher.group(1)), Double.parseDouble(matcher.group(2)), Double.parseDouble(matcher.group(3)));
        }
        return null;
    }

    public static ArrayList<String> getReturnToHomeCommands(Position currentPosition){
        ArrayList<String> commands = new ArrayList<>();
        //if(currentPosition.getCordZ() != 0) commands.add(GrblUtils.GCODE_RETURN_TO_ZERO_LOCATION_Z0_IN_MACHINE_CORDS);
        commands.add(GrblUtils.GCODE_RETURN_TO_ZERO_LOCATION_Z0_IN_MACHINE_CORDS);
        commands.add(GrblUtils.GCODE_RETURN_TO_ZERO_LOCATION_XY);
        commands.add(GrblUtils.GCODE_RETURN_TO_ZERO_LOCATION_Z0);

        return commands;
    }

    public static CompileTimeOptions getCompileTimeOptionsFromString(String buildOptions){
        int plannerBuffer = 15; int serialRxBuffer = 128;

        if (buildOptions.indexOf(",") > 0) {
            String[] parts = buildOptions.split(",");
            if (parts.length >= 3) {
                int tmpPBuffer = Integer.parseInt(parts[1]);
                if (tmpPBuffer > 0) plannerBuffer = tmpPBuffer;

                int tmpRxBuffer = Integer.parseInt(parts[2]);
                if (tmpRxBuffer > 0) serialRxBuffer = tmpRxBuffer;
            }
        }

        return new CompileTimeOptions(buildOptions, plannerBuffer, serialRxBuffer);
    }

    private final static String EEPROM_COMMAND_PATTERN = "G10|G28|G30|\\$x=|\\$I|\\$N|\\$RST=|G5[456789]|\\$\\$|\\$#";
    private final static Pattern EEPROM_COMMAND = Pattern.compile(EEPROM_COMMAND_PATTERN, Pattern.CASE_INSENSITIVE);
    public static boolean hasRomAccess(String command){
        return EEPROM_COMMAND.matcher(command).find();
    }

    public static String implode(String glue, String[] array) {
        if(array == null || array.length == 0) return "";

        StringBuilder sb = new StringBuilder();
        sb.append(array[0]);
        for(int i = 1; i < array.length; i++) sb.append(glue).append(array[i]);

        return sb.toString();
    }


    static public Byte getOverrideForEnum(final Overrides command) {
        switch (command) {
            //CMD_DEBUG_REPORT, // 0x85 // Only when DEBUG enabled, sends debug report in '{}' braces.
            case CMD_FEED_OVR_RESET:
                return (byte)0x90; // Restores feed override value to 100%.
            case CMD_FEED_OVR_COARSE_PLUS:
                return (byte)0x91;
            case CMD_FEED_OVR_COARSE_MINUS:
                return (byte)0x92;
            case CMD_FEED_OVR_FINE_PLUS :
                return (byte)0x93;
            case CMD_FEED_OVR_FINE_MINUS :
                return (byte)0x94;
            case CMD_RAPID_OVR_RESET:
                return (byte)0x95;
            case CMD_RAPID_OVR_MEDIUM:
                return (byte)0x96;
            case CMD_RAPID_OVR_LOW:
                return (byte)0x97;
            case CMD_SPINDLE_OVR_RESET:
                return (byte)0x99; // Restores spindle override value to 100%.
            case CMD_SPINDLE_OVR_COARSE_PLUS:
                return (byte)0x9A;
            case CMD_SPINDLE_OVR_COARSE_MINUS:
                return (byte)0x9B;
            case CMD_SPINDLE_OVR_FINE_PLUS:
                return (byte)0x9C;
            case CMD_SPINDLE_OVR_FINE_MINUS:
                return (byte)0x9D;
            case CMD_TOGGLE_SPINDLE:
                return (byte)0x9E;
            case CMD_TOGGLE_FLOOD_COOLANT:
                return (byte)0xA0;
            case CMD_TOGGLE_MIST_COOLANT:
                return (byte)0xA1;
        }
        return null;
    }


}
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import com.joanzapata.iconify.IconDrawable;
import com.joanzapata.iconify.fonts.FontAwesomeIcons;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.lang.ref.WeakReference;
import in.co.gorest.grblcontroller.events.GrblSettingMessageEvent;
import in.co.gorest.grblcontroller.events.JogCommandEvent;
import in.co.gorest.grblcontroller.events.StreamingCompleteEvent;
import in.co.gorest.grblcontroller.listeners.MachineStatusListener;
import in.co.gorest.grblcontroller.model.Constants;
import in.co.gorest.grblcontroller.service.FileStreamerIntentService;
import in.co.gorest.grblcontroller.service.GrblUsbSerialService;
import in.co.gorest.grblcontroller.util.GrblUtils;
    }

    private void setFilters() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(GrblUsbSerialService.ACTION_USB_PERMISSION_GRANTED);
        filter.addAction(GrblUsbSerialService.ACTION_NO_USB);
        filter.addAction(GrblUsbSerialService.ACTION_USB_DISCONNECTED);
        filter.addAction(GrblUsbSerialService.ACTION_USB_NOT_SUPPORTED);
        filter.addAction(GrblUsbSerialService.ACTION_USB_PERMISSION_NOT_GRANTED);
        registerReceiver(mUsbReceiver, filter);
    }

    /*
     * This handler will be passed to UsbService. Data received from serial port is displayed through this handler
     */
    private static class GrblServiceMessageHandler extends Handler {
        private final WeakReference<UsbConnectionActivity> mActivity;

        public GrblServiceMessageHandler(UsbConnectionActivity activity) {
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case GrblUsbSerialService.MESSAGE_FROM_SERIAL_PORT:
                    String data = (String) msg.obj;

                    break;
                case GrblUsbSerialService.CTS_CHANGE:

                    break;
                case GrblUsbSerialService.DSR_CHANGE:

                    break;
            }
        }
    }

    /*
     * Notifications from UsbService will be received here.
     */
    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {

            String intentAction = intent.getAction();
            if(intentAction == null){
                grblToast("Unknown error", true, true);
                return;
            }

            switch (intentAction) {
                case GrblUsbSerialService.ACTION_USB_PERMISSION_GRANTED: // USB PERMISSION GRANTED
                    if(getSupportActionBar() != null) getSupportActionBar().setSubtitle(getString(R.string.text_connected));
                    grblToast(getString(R.string.text_usb_device_connected));
                    break;
                case GrblUsbSerialService.ACTION_USB_PERMISSION_NOT_GRANTED: // USB PERMISSION NOT GRANTED
                    if(getSupportActionBar() != null) getSupportActionBar().setSubtitle(R.string.text_no_usb_permission);
                    grblToast(getString(R.string.text_usb_permission_not_granted), true, true);
                    break;
                case GrblUsbSerialService.ACTION_NO_USB: // NO USB CONNECTED
                    if(getSupportActionBar() != null) getSupportActionBar().setSubtitle(R.string.text_no_usb_device);
                    //grblToast("USB device not connected");
                    break;
                case GrblUsbSerialService.ACTION_USB_DISCONNECTED: // USB DISCONNECTED
                    if(getSupportActionBar() != null) getSupportActionBar().setSubtitle(getString(R.string.text_not_connected));
                    MachineStatusListener.getInstance().setState(Constants.MACHINE_STATUS_NOT_CONNECTED);
                    if(FileStreamerIntentService.getIsServiceRunning()){
                        FileStreamerIntentService.setShouldContinue(false);
                        stopService(new Intent(getApplicationContext(), FileStreamerIntentService.class));
                    }
                    grblToast(getString(R.string.text_usb_device_disconnected), true, true);
                    break;
                case GrblUsbSerialService.ACTION_USB_NOT_SUPPORTED: // USB NOT SUPPORTED
                    if(getSupportActionBar() != null) getSupportActionBar().setSubtitle(R.string.text_usb_device_not_supported);
                    MachineStatusListener.getInstance().setState(Constants.MACHINE_STATUS_NOT_CONNECTED);
                    grblToast(getString(R.string.text_usb_device_not_supported), true, true);
                    break;
            }
        }
    };

    private final ServiceConnection usbConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
            grblUsbSerialService = ((GrblUsbSerialService.UsbSerialBinder) service).getService();
            mBound = true;
            grblUsbSerialService.setMessageHandler(grblServiceMessageHandler);
            grblUsbSerialService.setStatusUpdatePoolInterval(Long.valueOf(sharedPref.getString(getString(R.string.preference_update_pool_interval), String.valueOf(Constants.GRBL_STATUS_UPDATE_INTERVAL))));
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            mBound = false;
            grblUsbSerialService = null;
        }
    };


    @Override
    public void onGcodeCommandReceived(String command) {
        if(grblUsbSerialService != null){
            grblUsbSerialService.serialWriteString(command);
        }
    }

    @Override
    public void onGrblRealTimeCommandReceived(byte command) {
        if(grblUsbSerialService != null) grblUsbSerialService.serialWriteByte(command);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onJogCommandEvent(JogCommandEvent event){
        if(machineStatus.getState().equals(Constants.MACHINE_STATUS_IDLE) || machineStatus.getState().equals(Constants.MACHINE_STATUS_JOG)){
            if(machineStatus.getPlannerBuffer() > 5) onGcodeCommandReceived(event.getCommand());
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onGrblSettingMessageEvent(GrblSettingMessageEvent event){