public class General extends AppCompatPreferenceActivity {
    boolean mIsTesting;
    boolean mIsDestroyed;
    boolean mFcmTestPassed;
    boolean mPushyTestPassed;

    String mPreviousZones;
    String mPreviousCities;

    MenuItem mLoadingItem;

    Preference mRate;
    Preference mWebsite;
    Preference mContact;
    Preference mAdvanced;
    Preference mTestAlert;
    Preference mLifeshield;
    CheckBoxPreference mNotificationsEnabled;

    PreferenceCategory mMainCategory;
    ListPreference mLanguageSelection;

    SearchableMultiSelectPreference mCitySelection;
    SearchableMultiSelectPreference mZoneSelection;
    SharedPreferences.OnSharedPreferenceChangeListener mBroadcastListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(SharedPreferences Preferences, String Key) {
            // Asked for reload?
            if (Key.equalsIgnoreCase(LocationSelectionEvents.LOCATIONS_UPDATED)) {
                // Refresh city/region descriptions
                refreshAreaValues();

                // Update subscriptions on the server-side
                new UpdateSubscriptionsAsync().execute();
            }

            // FCM test passed?
            if (Key.equalsIgnoreCase(SelfTestEvents.FCM_TEST_PASSED)) {
                mFcmTestPassed = true;
            }

            // Pushy test passed?
            if (Key.equalsIgnoreCase(SelfTestEvents.PUSHY_TEST_PASSED)) {
                mPushyTestPassed = true;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Support for RTL languages
        RTLSupport.mirrorActionBar(this);

        // Load UI elements
        initializeUI();

        // Handle extra parameters
        handleIntentExtras();

        // Volume keys should control alert volume
        Volume.setVolumeKeysAction(this);
    }

    void handleIntentExtras() {
        // Get extras
        Bundle extras = getIntent().getExtras();

        // None sent?
        if (extras == null) {
            return;
        }

        // Get zone selection boolean
        boolean showZoneSelection = extras.getBoolean(SettingsEvents.SHOW_ZONE_SELECTION);

        // Show selection?
        if (showZoneSelection) {
            mZoneSelection.showDialog();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();

        // Unregister for broadcasts
        Broadcasts.unsubscribe(this, mBroadcastListener);
    }

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

        // Avoid hiding invalid dialogs
        mIsDestroyed = true;
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Support for RTL languages
        RTLSupport.mirrorActionBar(this);

        // Clear notifications and stop any playing sounds
        AppNotifications.clearAll(this);

        // Register for broadcasts
        Broadcasts.subscribe(this, mBroadcastListener);
    }

    void selfTestCompleted(int result) {
        // Use builder to create dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(this);

        // Use builder to create dialog
        builder.setTitle(getString(R.string.test)).setMessage(getString(result)).setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int id) {
                // Stop siren (and clear notifications)
                AppNotifications.clearAll(General.this);

                // Close dialog
                dialogInterface.dismiss();
            }
        });

        // Avoid cancellation
        builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface Dialog) {
                // Stop siren (and clear notifications)
                AppNotifications.clearAll(General.this);
            }
        });

        try {
            // Build it
            Dialog dialog = builder.create();

            // Show it
            dialog.show();

            // Support for RTL languages
            RTLSupport.mirrorDialog(dialog, this);
        }
        catch (Exception exc) {
            // Show toast instead
            Toast.makeText(this, getString(result), Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onConfigurationChanged(android.content.res.Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        // Support for RTL languages
        RTLSupport.mirrorActionBar(this);
    }

    void initializeUI() {
        // Allow click on home button
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        // Load settings from XML There is no non-deprecated way to do it on API Level 7
        addPreferencesFromResource(R.xml.settings);

        // Cache resource IDs
        mRate = findPreference(getString(R.string.ratePref));
        mWebsite = findPreference(getString(R.string.websitePref));
        mContact = findPreference(getString(R.string.contactPref));
        mAdvanced = findPreference(getString(R.string.advancedPref));
        mTestAlert = findPreference(getString(R.string.selfTestPref));
        mLifeshield = findPreference(getString(R.string.lifeshieldPref));
        mMainCategory = (PreferenceCategory) findPreference(getString(R.string.mainCategoryPref));
        mCitySelection = ((SearchableMultiSelectPreference) findPreference(getString(R.string.selectedCitiesPref)));
        mZoneSelection = ((SearchableMultiSelectPreference) findPreference(getString(R.string.selectedZonesPref)));
        mLanguageSelection = (ListPreference) findPreference(getString(R.string.langPref));
        mNotificationsEnabled = (CheckBoxPreference)findPreference(getString(R.string.enabledPref));

        // Populate setting values
        initializeSettings();

        // Set up listeners
        initializeListeners();
    }

    void initializeSettings() {
        // Set entries & values
        mZoneSelection.setEntries(getResources().getStringArray(R.array.zoneNames));
        mZoneSelection.setEntryValues(getResources().getStringArray(R.array.zoneValues));

        // Set entries & values
        mCitySelection.setEntries(LocationData.getAllCityNames(this));
        mCitySelection.setEntryValues(LocationData.getAllCityValues(this));

        // Not registered yet?
        if (!RedAlertAPI.isRegistered(this)) {
            mNotificationsEnabled.setEnabled(false);
        }

        // Refresh area values
        refreshAreaValues();
    }

    void refreshAreaValues() {
        // Get selected cities
        String selectedCities = Singleton.getSharedPreferences(this).getString(getString(R.string.selectedCitiesPref), getString(R.string.none));

        // Update summary text
        mCitySelection.setSummary(getString(R.string.cityDesc) + "\r\n(" + LocationData.getSelectedCityNamesByValues(this, selectedCities, mCitySelection.getEntries(), mCitySelection.getEntryValues()) + ")");

        // Get selected zones
        String selectedZones = Singleton.getSharedPreferences(this).getString(getString(R.string.selectedZonesPref), getString(R.string.none));

        // Update summary text
        mZoneSelection.setSummary(getString(R.string.zonesDesc) + "\r\n(" + LocationData.getSelectedCityNamesByValues(this, selectedZones, mZoneSelection.getEntries(), mZoneSelection.getEntryValues()) + ")");

        // Save in case the update subscriptions request fails
        if (mPreviousZones == null && mPreviousCities == null) {
            mPreviousZones = selectedZones;
            mPreviousCities = selectedCities;
        }
    }

    void initializeListeners() {
        // Language preference select
        mLanguageSelection.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
            @Override
            public boolean onPreferenceChange(Preference preference, Object o) {
                // Show instructions toast
                Toast.makeText(General.this, R.string.langSelectionSuccess, Toast.LENGTH_LONG).show();

                // Update the preference
                return true;
            }
        });

        // Test push
        mTestAlert.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                // Did we enable a device integration but Bluetooth is disabled?
                if (BluetoothIntegration.isIntegrationEnabled(General.this) && !BluetoothIntegration.isBluetoothEnabled()) {
                    // Ask user politely to enable Bluetooth
                    BluetoothDialogs.showEnableBluetoothDialog(General.this);

                    // Don't run the test yet
                    return false;
                }

                // Not already testing?
                if (!mIsTesting) {
                    // Send a test push
                    new PerformSelfTestAsync().execute();
                }

                // Consume event
                return true;
            }
        });

        // Website button
        mWebsite.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                // Initialize browser intent
                Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(Support.WEBSITE_LINK));

                try {
                    // Open browser
                    startActivity(browser);
                }
                catch (ActivityNotFoundException ex) {
                    // Do nothing
                }

                // Consume event
                return true;
            }
        });

        // Lifeshield website button
        mLifeshield.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                // Initialize browser intent
                Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(Lifeshield.WEBSITE_LINK));

                try {
                    // Open browser
                    startActivity(browser);
                }
                catch (ActivityNotFoundException ex) {
                    // Do nothing
                }

                // Consume event
                return true;
            }
        });

        // Notification toggle listener
        mNotificationsEnabled.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
            @Override
            public boolean onPreferenceChange(Preference preference, Object newValue) {
                // Update notification preferences on the server-side
                new UpdateNotificationsAsync().execute();

                // Tell Android to persist new checkbox value
                return true;
            }
        });

        // Rate button
        mRate.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                // Open app page
                GooglePlay.openAppListingPage(General.this);

                // Consume event
                return true;
            }
        });

        // Set up contact click listener
        mContact.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                // Add debug flags
                String body = getContactEmailBody();

                // Set up contact click listener
                Intent selectorIntent = new Intent(Intent.ACTION_SENDTO);
                selectorIntent.setData(Uri.parse("mailto:"));

                // Prepare e-mail intent
                final Intent emailIntent = new Intent(Intent.ACTION_SEND);

                // Add e-mail, subject & debug body
                emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{Support.CONTACT_EMAIL});
                emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.appName));
                emailIntent.putExtra(Intent.EXTRA_TEXT, body);

                // Limit to mail apps only
                emailIntent.setSelector(selectorIntent);

                try {
                    // Try to open user's e-mail app
                    startActivity(Intent.createChooser(emailIntent, getString(R.string.contact)));
                }
                catch (ActivityNotFoundException exc) {
                    // Show a toast instead
                    Toast.makeText(General.this, R.string.manualContact, Toast.LENGTH_LONG).show();
                }

                // Consume event
                return true;
            }
        });

        // Set up advanced settings listener
        mAdvanced.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                // Prepare new intent
                Intent advanced = new Intent();

                // Set advanced class
                advanced.setClass(General.this, Advanced.class);

                // Show advanced settings
                startActivity(advanced);

                // Consume event
                return true;
            }
        });
    }

    String getContactEmailBody() {
        // Add default problem description text
        String body = getString(R.string.problemDesc);

        // Break a few lines
        body += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n";

        // Add sent via and app version
        body += getString(R.string.sentVia) + " " + getString(R.string.appName) + " " + AppVersion.getVersionName(General.this);

        // Break 2 lines
        body += "\r\n\r\n";

        // Add selected zones
        body += getString(R.string.selectedZones) + ": " + LocationData.getSelectedCityNamesByValues(this, Singleton.getSharedPreferences(this).getString(getString(R.string.selectedZonesPref), ""), getResources().getStringArray(R.array.zoneNames), getResources().getStringArray(R.array.zoneValues));

        // Break 1 line
        body += "\r\n";

        // Add selected cities
        body += getString(R.string.selectedCities) + ": " + LocationData.getSelectedCityNamesByValues(this, Singleton.getSharedPreferences(this).getString(getString(R.string.selectedCitiesPref), ""), LocationData.getAllCityNames(this), LocationData.getAllCityValues(this));

        // Break 2 lines
        body += "\r\n\r\n";

        // Add debug info
        body += getString(R.string.debugInfo) + ": ";

        // Add setting values & push notification device information
        body += "user.id=" + RedAlertAPI.getUserId(this) + ", ";
        body += "user.hash=" + RedAlertAPI.getUserHash(this) + ", ";
        body += "primary.enabled=" + AppPreferences.getNotificationsEnabled(this) + ", ";
        body += "secondary.enabled=" + AppPreferences.getNotificationsEnabled(this) + ", ";
        body += "location.enabled=" + AppPreferences.getLocationAlertsEnabled(this) + ", ";
        body += "volume.primary=" + AppPreferences.getPrimaryAlertVolume(this, -1) + ", ";
        body += "volume.secondary=" + AppPreferences.getSecondaryAlertVolume(this, -1) + ", ";
        body += "fcm=" + FCMRegistration.isRegistered(this) + ", ";
        body += "fcm.token=" + FCMRegistration.getRegistrationToken(this) + ", ";
        body += "pushy=" + PushyRegistration.isRegistered(this) + ", ";
        body += "pushy.token=" + PushyRegistration.getRegistrationToken(this) + ", ";
        body += "android.sdk=" + Build.VERSION.SDK_INT + ", ";
        body += "android.version=" + Build.VERSION.RELEASE + ", ";
        body += "phone.manufacturer=" + Build.MANUFACTURER + ", ";
        body += "phone.model=" + Build.MODEL;

        // Break 2 lines
        body += "\r\n\r\n";

        // Return body
        return body;
    }

    void sendTestPushViaPushy() throws Exception {
        // Log to logcat
        Log.d(Logging.TAG, "Testing Pushy...");

        // Reset test flag
        mPushyTestPassed = false;

        // Not registered?
        if (!PushyRegistration.isRegistered(this)) {
            // No need for localization - only shows up in logcat for now
            throw new Exception("The device is not registered for push notifications via Pushy.");
        }

        // Grab registration token
        String token = PushyRegistration.getRegistrationToken(this);

        // Get user's locale
        String locale = getResources().getConfiguration().locale.getLanguage();

        // Create registration request
        SelfTestRequest test = new SelfTestRequest(token, locale, API.PUSHY_PLATFORM_IDENTIFIER);

        // Send the request to our API
        HTTP.post(API.API_ENDPOINT + "/test", Singleton.getJackson().writeValueAsString(test));
    }

    void sendTestPushViaFcm() throws Exception {
        // Log to logcat
        Log.d(Logging.TAG, "Testing FCM...");

        // Reset test flag
        mFcmTestPassed = false;

        // Not registered?
        if (!FCMRegistration.isRegistered(this)) {
            // No need for localization - only shows up in logcat for now
            throw new Exception("The device is not registered for push notifications via FCM.");
        }

        // Grab registration token
        String token = FCMRegistration.getRegistrationToken(this);

        // Get user's locale
        String locale = getResources().getConfiguration().locale.getLanguage();

        // Create registration request
        SelfTestRequest test = new SelfTestRequest(token, locale, API.PLATFORM_IDENTIFIER);

        // Send the request to our API
        HTTP.post(API.API_ENDPOINT + "/test", Singleton.getJackson().writeValueAsString(test));
    }

    boolean didPassFcmTest() {
        // Calculate the max timestamp
        long maxTimestamp = System.currentTimeMillis() + Testing.PUSH_GATEWAY_TIMEOUT_SECONDS * 1000;

        // Wait until boolean value changes or enough time passes
        while (!mFcmTestPassed && System.currentTimeMillis() < maxTimestamp) {
            // Sleep to relieve the thread
            try {
                Thread.sleep(100);
            }
            catch (Exception exc) {
            }
        }

        // Return the outcome of the test
        return mFcmTestPassed;
    }

    boolean didPassPushyTest() {
        // Calculate the max timestamp
        long maxTimestamp = System.currentTimeMillis() + Testing.PUSH_GATEWAY_TIMEOUT_SECONDS * 1000;

        // Wait until boolean value changes or enough time passes
        while (!mPushyTestPassed && System.currentTimeMillis() < maxTimestamp) {
            // Sleep to relieve the thread
            try {
                Thread.sleep(100);
            }
            catch (Exception exc) {
            }
        }

        // Return the outcome of the test
        return mPushyTestPassed;
    }

    void toggleProgressBarVisibility(boolean visibility) {
        // Set loading visibility
        if (mLoadingItem != null) {
            mLoadingItem.setVisible(visibility);
        }
    }

    void initializeLoadingIndicator(Menu OptionsMenu) {
        // Add refresh in Action Bar
        mLoadingItem = OptionsMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, getString(R.string.signing_up));

        // Set up the view
        MenuItemCompat.setActionView(mLoadingItem, R.layout.loading);

        // Specify the show flags
        MenuItemCompat.setShowAsAction(mLoadingItem, MenuItem.SHOW_AS_ACTION_ALWAYS);

        // Hide by default
        mLoadingItem.setVisible(false);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu OptionsMenu) {
        // Add loading indicator
        initializeLoadingIndicator(OptionsMenu);

        // Add settings button
        initializeShareButton(OptionsMenu);

        // Show the menu
        return true;
    }

    void initializeShareButton(Menu OptionsMenu) {
        // Add share button
        MenuItem shareItem = OptionsMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, getString(R.string.share));

        // Set up the view
        shareItem.setIcon(R.drawable.ic_share);

        // Specify the show flags
        MenuItemCompat.setShowAsAction(shareItem, MenuItem.SHOW_AS_ACTION_ALWAYS);

        // On click, open share
        shareItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                // Prepare share intent
                Intent shareIntent = new Intent(Intent.ACTION_SEND);

                // Set as text/plain
                shareIntent.setType("text/plain");

                // Add text
                shareIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.shareMessage));

                // Show chooser
                startActivity(Intent.createChooser(shareIntent, getString(R.string.shareDesc)));

                // Consume event
                return true;
            }
        });
    }

    public boolean onOptionsItemSelected(final MenuItem Item) {
        // Check item ID
        switch (Item.getItemId()) {
            // Home button?
            case android.R.id.home:
                onBackPressed();
        }

        return super.onOptionsItemSelected(Item);
    }

    public class PerformSelfTestAsync extends AsyncTaskAdapter<Integer, String, Integer> {
        ProgressDialog mLoading;

        public PerformSelfTestAsync() {
            // Prevent concurrent testing
            mIsTesting = true;

            // Fix progress dialog appearance on old devices
            mLoading = ProgressDialogCompat.getStyledProgressDialog(General.this);

            // Prevent cancel
            mLoading.setCancelable(false);

            // Set default message
            mLoading.setMessage(getString(R.string.loading));

            // Show the progress dialog
            mLoading.show();
        }

        @Override
        protected void onProgressUpdate(String... value) {
            super.onProgressUpdate(value);

            // Update progress dialog
            mLoading.setMessage(value[0]);
        }

        @Override
        protected Integer doInBackground(Integer... Parameter) {
            // Test results
            boolean fcmFailed = false, pushyFailed = false;

            // IsTesting FCM
            publishProgress(getString(R.string.testingFCM));

            try {
                // Send a test push
                sendTestPushViaFcm();
            }
            catch (Exception exc) {
                // FCM test is done
                fcmFailed = true;

                // Log to console
                Log.e(Logging.TAG, "FCM test failed", exc);
            }

            // Wait for test push to arrive
            if (!fcmFailed) {
                if (!didPassFcmTest()) {
                    fcmFailed = true;
                }
            }

            // Done with FCM, testing Pushy
            publishProgress(getString(R.string.testingPushy));

            try {
                // Send a test push
                sendTestPushViaPushy();
            }
            catch (Exception exc) {
                // We failed
                pushyFailed = true;

                // Log to console
                Log.e(Logging.TAG, "Pushy test failed", exc);
            }

            // Wait for test push to arrive
            if (!pushyFailed) {
                if (!didPassPushyTest()) {
                    pushyFailed = true;
                }
            }

            // At least one passed?
            if (!fcmFailed || !pushyFailed) {
                // Display "successful test" message
                AlertLogic.processIncomingAlert(getString(R.string.testSuccessful), AlertTypes.TEST, General.this);
            }

            // Both succeeded?
            if (!fcmFailed && !pushyFailed) {
                return R.string.testSuccessfulLong;
            }

            // Both failed?
            else if (fcmFailed && pushyFailed) {
                return R.string.testFailed;
            }

            // Only FCM failed?
            else if (fcmFailed) {
                return R.string.fcmTestFailed;
            }

            // Only Pushy failed?
            else if (pushyFailed) {
                return R.string.pushyTestFailed;
            }

            return 0;
        }

        @Override
        protected void onPostExecute(Integer result) {
            // No longer testing
            mIsTesting = false;

            // Activity dead?
            if (isFinishing() || mIsDestroyed) {
                return;
            }

            // Hide progress dialog
            if (mLoading.isShowing()) {
                mLoading.dismiss();
            }

            // Hide loading indicator
            toggleProgressBarVisibility(false);

            // Show result dialog
            selfTestCompleted(result);
        }
    }

    public class UpdateSubscriptionsAsync extends AsyncTaskAdapter<Integer, String, Exception> {
        ProgressDialog mLoading;

        public UpdateSubscriptionsAsync() {
            // Fix progress dialog appearance on old devices
            mLoading = ProgressDialogCompat.getStyledProgressDialog(General.this);

            // Prevent cancel
            mLoading.setCancelable(false);

            // Set default message
            mLoading.setMessage(getString(R.string.loading));

            // Show the progress dialog
            mLoading.show();
        }

        @Override
        protected Exception doInBackground(Integer... Parameter) {
            try {
                // Update alert subscriptions
                RedAlertAPI.subscribe(General.this);
            }
            catch (Exception exc) {
                // Return exception to onPostExecute
                return exc;
            }

            // Success
            return null;
        }

        @Override
        protected void onPostExecute(Exception exc) {
            // Failed?
            if (exc != null) {
                // Log it
                Log.e(Logging.TAG, "Updating subscriptions failed", exc);

                // Restore previous city/region selections
                SharedPreferences.Editor editor = Singleton.getSharedPreferences(General.this).edit();

                // Restore original values
                editor.putString(getString(R.string.selectedZonesPref), mPreviousZones);
                editor.putString(getString(R.string.selectedCitiesPref), mPreviousCities);

                // Save and flush to disk
                editor.commit();
            }

            // Activity dead?
            if (isFinishing() || mIsDestroyed) {
                return;
            }

            // Hide loading dialog
            if (mLoading.isShowing()) {
                mLoading.dismiss();
            }

            // Show error if failed
            if (exc != null) {
                // Build an error message
                String errorMessage = getString(R.string.apiRequestFailed) + "\n\n" + exc.getMessage() + "\n\n" + exc.getCause();

                // Build the dialog
                AlertDialogBuilder.showGenericDialog(getString(R.string.error), errorMessage, General.this, null);
            }
            else {
                // Clear previously cached values
                mPreviousZones = null;
                mPreviousCities = null;
            }

            // Refresh city/region setting values
            refreshAreaValues();
        }
    }

    public class UpdateNotificationsAsync extends AsyncTaskAdapter<Integer, String, Exception> {
        ProgressDialog mLoading;

        public UpdateNotificationsAsync() {
            // Fix progress dialog appearance on old devices
            mLoading = ProgressDialogCompat.getStyledProgressDialog(General.this);

            // Prevent cancel
            mLoading.setCancelable(false);

            // Set default message
            mLoading.setMessage(getString(R.string.loading));

            // Show the progress dialog
            mLoading.show();
        }

        @Override
        protected Exception doInBackground(Integer... Parameter) {
            try {
                // Update notification preferences
                RedAlertAPI.updateNotificationPreferences(General.this);
            }
            catch (Exception exc) {
                // Return exception to onPostExecute
                return exc;
            }

            // Success
            return null;
        }

        @Override
        protected void onPostExecute(Exception exc) {
            // Failed?
            if (exc != null) {
                // Log it
                Log.e(Logging.TAG, "Updating notification preferences failed", exc);

                // Restore previous notification toggle state
                SharedPreferences.Editor editor = Singleton.getSharedPreferences(General.this).edit();

                // Restore original values
                editor.putBoolean(getString(R.string.enabledPref), ! AppPreferences.getNotificationsEnabled(General.this));

                // Save and flush to disk
                editor.commit();
            }

            // Activity dead?
            if (isFinishing() || mIsDestroyed) {
                return;
            }

            // Hide loading dialog
            if (mLoading.isShowing()) {
                mLoading.dismiss();
            }

            // Show error if failed
            if (exc != null) {
                // Build an error message
                String errorMessage = getString(R.string.apiRequestFailed) + "\n\n" + exc.getMessage() + "\n\n" + exc.getCause();

                // Build the dialog
                AlertDialogBuilder.showGenericDialog(getString(R.string.error), errorMessage, General.this, null);
            }

            // Refresh checkbox with new value
            mNotificationsEnabled.setChecked(AppPreferences.getNotificationsEnabled(General.this));
        }
    }
} public class API {
    // RedAlert API URL (no trailing slash)
    //public static String API_ENDPOINT = "http://192.168.43.48:3000";
    public static String API_ENDPOINT = "https://redalert.me";

    // Define platform to be sent by our API
    public static String PLATFORM_IDENTIFIER = "android";

    // Define platform for Pushy self test
    public static String PUSHY_PLATFORM_IDENTIFIER = "pushy";
} public class Alerts {
    // Date format for recent alerts display
    public static final String DATE_FORMAT = "HH:mm:ss (dd/MM/yy)";

    // Time to wait before calling finish() on AlertPopup after POST_IMPACT_WAIT_MINUTES passed
    public static final int ALERT_POPUP_DONE_PADDING = 10;

    // Time that must pass (in seconds) in between alerts for the same zone (to avoid duplicate alerts)
    public static final int DUPLICATE_ALERTS_PADDING_TIME = 60 * 2;

} public class Integrations {
    // WhatsApp package for I'm Safe integration - automatic share via WhatsApp
    public static String WHATSAPP_PACKAGE = "com.whatsapp";
} public class Logging {
    // Tag for all logcat logs
    public static String TAG = "RedAlert";
} public class RecentAlerts {
    // Recent alerts polling interval (in seconds)
    public static int RECENT_ALERTS_POLLING_INTERVAL_SEC = 10;
} public class SettingsEvents {
    public static final String SHOW_ZONE_SELECTION = "ShowZoneSelection";
} public class AlertViewParameters {
    public static final String ALERT_CITY = "AlertZone";
    public static final String ALERT_DATE_STRING = "AlertDateString";
} public class MainActivityParameters {
    public static final String RELOAD_RECENT_ALERTS = "ReloadRecentAlerts";
} public class BluetoothIntegration {
    public static void notifyDevices(String alertType, Context context) {
        // Type must be an "alert" or "test"
        if (alertType.equals(AlertTypes.PRIMARY) || alertType.equals(AlertTypes.TEST)) {
            // Check for BLE support + enabled Bluetooth controller
            if (!isBLESupported(context) || !isBluetoothEnabled()) {
                // Stop execution
                return;
            }

            // Vibrate + LED for Mi Band (if enabled)
            MiBandIntegration.notifyMiBand(context);
        }
    }

    public static boolean isIntegrationEnabled(Context context) {
        // Check if Mi Band integration is enabled (and add more devices in the future)
        return AppPreferences.getMiBandIntegrationEnabled(context);
    }

    public static boolean isBLESupported(Context context) {
        // Check whether Bluetooth is supported on the device
        if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) {
            // No Bluetooth support
            return false;
        }

        // Check whether BLE is supported on the device
        if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            // No BLE support
            return false;
        }

        // Check Android version (BluetoothGatt requires API Level 18+)
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            return false;
        }

        // We're good
        return true;
    }

    public static boolean isBluetoothEnabled() {
        // Must call Looper.prepare() due to ICS bug
        // And we can't call the function from UI thread when we receive a push notification)
        // http://stackoverflow.com/questions/5920578/bluetoothadapter-getdefault-throwing-runtimeexception-while-not-in-activity

        if (Looper.myLooper() == null) {
            Looper.prepare();
        }

        // Get bluetooth adapter
        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        // Null means device doesn't support Bluetooth
        if (bluetoothAdapter == null) {
            // No Bluetooth support
            return false;
        }
        else {
            // Check if it's enabled and return the result
            return bluetoothAdapter.isEnabled();
        }
    }
} public class FCMRegistration {
    public static String registerForPushNotifications(final Context context) throws Exception {
        // Make sure we have Google Play Services
        if (!GooglePlayServices.isAvailable(context)) {
            // Throw exception
            throw new Exception(context.getString(R.string.noGooglePlayServices));
        }

        // Get an FCM registration token
        final String token = FirebaseInstanceId.getInstance().getToken(FCMGateway.SENDER_ID, FCMGateway.SCOPE);

        // Log to logcat
        Log.d(Logging.TAG, "FCM registration success: " + token);

        // Unsubscribe from alerts topic (FCM Pub/Sub is unreliable for mission-critical alerts)
        FirebaseMessaging.getInstance().unsubscribeFromTopic(FCMGateway.ALERTS_TOPIC)
                .addOnCompleteListener(new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        if (!task.isSuccessful()) {
                            Log.e(Logging.TAG, "FCM unsubscribe failed: ", task.getException());
                            return;
                        }

                        // Log it
                        Log.d(Logging.TAG, "FCM unsubscribe success: " + FCMGateway.ALERTS_TOPIC);
                    }
                });

        // Return token for saving and processing
        return token;
    }

    public static String getRegistrationToken(Context context) {
        // Get it from SharedPreferences (may be null)
        return Singleton.getSharedPreferences(context).getString(context.getString(R.string.fcmTokenPref), null);
    }

    public static boolean isRegistered(Context context) {
        // Check whether it's null (in which case we never successfully registered)
        return getRegistrationToken(context) != null;
    }

    public static void saveRegistrationToken(Context context, String registrationToken) {
        // Edit shared preferences
        SharedPreferences.Editor editor = Singleton.getSharedPreferences(context).edit();

        // Store boolean value
        editor.putString(context.getString(R.string.fcmTokenPref), registrationToken);

        // Save and flush
        editor.commit();
    }
} public class PushyRegistration {
    public static void registerForPushNotifications(Context context) throws Exception {
        // Acquire a unique registration ID for this device
        String token = Pushy.register(context);

        // Log to logcat
        Log.d(Logging.TAG, "Pushy registration success: " + token);

        // Subscribe to global alerts topic (we need this for location alerts to work)
        Pushy.subscribe(PushyGateway.ALERTS_TOPIC, context);

        // Log it
        Log.d(Logging.TAG, "Pushy subscribe success: " + PushyGateway.ALERTS_TOPIC);


        // Persist it locally (no need to send it to our API)
        saveRegistrationToken(context, token);
    }

    public static String getRegistrationToken(Context context) {
        // Get it from SharedPreferences (may be null)
        return Singleton.getSharedPreferences(context).getString(context.getString(R.string.pushyTokenPref), null);
    }

    public static boolean isRegistered(Context context) {
        // Check whether it's null (in which case we never successfully registered)
        return getRegistrationToken(context) != null;
    }

    public static void saveRegistrationToken(Context context, String registrationToken) {
        // Edit shared preferences
        SharedPreferences.Editor editor = Singleton.getSharedPreferences(context).edit();

        // Store boolean value
        editor.putString(context.getString(R.string.pushyTokenPref), registrationToken);

        // Save and flush
        editor.commit();
    }
} public class ServiceManager {
    public static void startAppServices(Context context) {
        // Start the Pushy push service
        startPushyService(context);

        // Start the location service
        startLocationService(context);
    }

    public static void startLocationService(Context context) {
        // Can we request location?
        if (!LocationLogic.shouldRequestLocationUpdates(context)) {
            return;
        }

        // Start the location service
        context.startService(new Intent(context, LocationService.class));
    }

    public static void startPushyService(Context context) {
        // Set custom heartbeat interval before calling Pushy.listen()
        Pushy.setHeartbeatInterval(PushyGateway.SOCKET_HEARTBEAT_INTERVAL, context);

        // Start external service
        Pushy.listen(context);
    }
} public class AppPreferences {
    public static boolean getNotificationsEnabled(Context context) {
        // Get saved preference
        return Singleton.getSharedPreferences(context).getBoolean(context.getString(R.string.enabledPref), true);
    }

    public static boolean getLocationAlertsEnabled(Context context) {
        // Get saved preference
        return Singleton.getSharedPreferences(context).getBoolean(context.getString(R.string.locationAlertsPref), false);
    }

    public static boolean getSecondaryNotificationsEnabled(Context context) {
        // Get saved preference
        return Singleton.getSharedPreferences(context).getBoolean(context.getString(R.string.secondaryEnabledPref), false);
    }

    public static boolean getMiBandIntegrationEnabled(Context context) {
        // Get saved preference
        return Singleton.getSharedPreferences(context).getBoolean(context.getString(R.string.miBandPref), false);
    }

    public static boolean getTutorialDisplayed(Context context) {
        // Get saved preference
        return Singleton.getSharedPreferences(context).getBoolean(context.getString(R.string.tutorialPref), false);
    }

    public static boolean getPopupEnabled(Context context) {
        // Get saved preference
        return Singleton.getSharedPreferences(context).getBoolean(context.getString(R.string.alertPopupPref), true);
    }

    public static void setTutorialDisplayed(Context context) {
        // Update stored value
        Singleton.getSharedPreferences(context).edit().putBoolean(context.getString(R.string.tutorialPref), true).commit();
    }

    public static void setCityLastAlertTime(String city, long timestamp, Context context) {
        // Update last alert timestamp for this city
        Singleton.getSharedPreferences(context).edit().putLong(city, timestamp).commit();
    }

    public static long getCityLastAlert(String city, Context context) {
        // Get last alert timestamp for this city
        return Singleton.getSharedPreferences(context).getLong(city, 0);
    }

    public static float getPrimaryAlertVolume(Context context, float overrideValue) {
        // Override?
        if (overrideValue != -1) {
            return overrideValue;
        }

        // Get stored value
        return Singleton.getSharedPreferences(context).getFloat(context.getString(R.string.volumePref), 1.0f);
    }

    public static float getSecondaryAlertVolume(Context context, float overrideValue) {
        // Override?
        if (overrideValue != -1) {
            return overrideValue;
        }

        // Get stored value
        return Singleton.getSharedPreferences(context).getFloat(context.getString(R.string.secondaryVolumePref), 1.0f);
    }
} public class Alert {
    @JsonProperty("date")
    public long date;

    @JsonProperty("area")
    public String city;
    public String localizedCity;

    public String desc;
    public String dateString;

    @Override
    public String toString() {
        return city;
    }
} public class VersionInfo {
    @JsonProperty("version")
    public String version;

    @JsonProperty("version_code")
    public int versionCode;

    @JsonProperty("show_dialog")
    public boolean showDialog;

    @Override
    public String toString() {
        return version;
    }
} public class AlertAdapter extends ArrayAdapter<Alert> {
    Activity mActivity;
    List<Alert> mAlerts;

    public AlertAdapter(Activity activity, List<Alert> alerts) {
        // Call super function with the item layout and initial alerts
        super(activity, R.layout.alert, alerts);

        // Set data members
        this.mAlerts = alerts;
        this.mActivity = activity;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // Prepare view holder
        ViewHolder viewHolder;

        // Don't have a cached view?
        if (convertView == null) {
            // Get inflater service
            LayoutInflater layoutInflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            // Inflate the alert layout
            convertView = layoutInflater.inflate(R.layout.alert, null);

            // Create a new view holder
            viewHolder = new ViewHolder();

            // Cache the view resources
            viewHolder.time = (TextView) convertView.findViewById(R.id.time);
            viewHolder.title = (TextView) convertView.findViewById(R.id.alertTitle);
            viewHolder.desc = (TextView) convertView.findViewById(R.id.alertDesc);

            // Store it in tag
            convertView.setTag(viewHolder);
        }
        else {
            // Get cached convert view
            viewHolder = (ViewHolder) convertView.getTag();
        }

        // Retrieve the alert
        Alert alert = mAlerts.get(position);

        // Got an alert?
        if (alert != null) {
            // Set area localized zone
            viewHolder.title.setText(alert.localizedCity);

            // Set area names
            viewHolder.desc.setText(alert.desc);

            // Set user-friendly time
            viewHolder.time.setText(alert.dateString);
        }

        // Return the view
        return convertView;
    }

    public boolean hasStableIds() {
        // IDs are unique
        return true;
    }

    public static class ViewHolder {
        public TextView time;
        public TextView title;
        public TextView desc;
    }
} public class ProgressDialogCompat {
    public static ProgressDialog getStyledProgressDialog(Context context) {
        // Only for pre-Honeycomb devices
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return new ProgressDialog(context, R.style.ProgressDialogCompat);
        }

        // Return a basic ProgressDialog
        return new ProgressDialog(context);
    }
} public class AlertDialogBuilder {
    public static final void showGenericDialog(String title, String message, Context context, DialogInterface.OnClickListener clickListener) {
        // Use builder to create dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(context);

        // Use builder to create dialog
        builder.setTitle(title).setMessage(message).setCancelable(false).setPositiveButton(R.string.okay, clickListener);

        // Prevent cancellation
        builder.setCancelable(false);

        try {
            // Build it
            AlertDialog dialog = builder.create();

            // Show it
            dialog.show();

            // Support for RTL languages
            RTLSupport.mirrorDialog(dialog, context);
        }
        catch (Exception exc) {
            // Show toast instead
            Toast.makeText(context, title + " - " + message, Toast.LENGTH_LONG).show();
        }
    }
} public class BluetoothDialogs {
    private static AlertDialog mEnableBluetoothDialog;

    public static void showEnableBluetoothDialog(final Context context) {
        // Already have a historical dialog?
        if (mEnableBluetoothDialog != null) {
            try {
                // Try to dismiss it
                mEnableBluetoothDialog.hide();
            }
            catch (Exception exc) {
                // The dialog is probably no longer valid
                mEnableBluetoothDialog = null;
            }
        }

        // Use builder to create dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(context);

        // Use builder to create dialog
        builder.setTitle(context.getString(R.string.enableBluetooth)).setMessage(context.getString(R.string.enableBluetoothDesc));

        // Set positive button
        builder.setPositiveButton(R.string.enable, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                // Ask user to enable it via built-in Android dialog
                context.startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE));
            }
        });

        // Set negative button
        builder.setNegativeButton(R.string.notNow, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                // Hide dialog
                dialog.dismiss();
            }
        });

        try {
            // Create the dialog
            mEnableBluetoothDialog = builder.create();

            // Show dialog
            mEnableBluetoothDialog.show();

            // Support for RTL languages
            RTLSupport.mirrorDialog(mEnableBluetoothDialog, context);

        }
        catch (Exception exc) {
            // Show toast instead
            Toast.makeText(context, context.getString(R.string.enableBluetoothDesc), Toast.LENGTH_LONG).show();
        }
    }

    public static void showBLENotSupportedDialog(final Activity activity) {
        // Use builder to create dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(activity);

        // Use builder to create dialog
        builder.setTitle(activity.getString(R.string.bleNotSupported)).setCancelable(false).setMessage(activity.getString(R.string.bleNotSupportedDesc));

        // Set positive button
        builder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                // Dismiss dialog
                dialogInterface.dismiss();

                // Still alive?
                if (!activity.isFinishing()) {
                    // Close the activity
                    activity.finish();
                }
            }
        });

        try {
            // Create the dialog
            AlertDialog dialog = builder.create();

            // Show dialog
            dialog.show();

            // Support for RTL languages
            RTLSupport.mirrorDialog(dialog, activity);

        }
        catch (Exception exc) {
            // Show toast instead
            Toast.makeText(activity, activity.getString(R.string.bleNotSupportedDesc), Toast.LENGTH_LONG).show();
        }
    }
} public class LocationDialogs {
    private static AlertDialog mLocationServicesDialog;

    public static void requestEnableLocationServices(final Activity context) {
        // Location alerts disabled?
        if (!AppPreferences.getLocationAlertsEnabled(context)) {
            return;
        }

        // Check permissions
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestLocationPermissionIfNecessary(context);
        }

        // Get location manager
        LocationManager locationManager = (LocationManager) context.getSystemService(context.LOCATION_SERVICE);

        // GPS enabled?
        if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            return;
        }

        // Network triangulation enabled?
        if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
            return;
        }

        // Already have a historical dialog?
        if (mLocationServicesDialog != null) {
            try {
                // Try to dismiss it
                mLocationServicesDialog.hide();
            }
            catch (Exception exc) {
                // The dialog is probably no longer valid
                mLocationServicesDialog = null;
            }
        }

        // Use builder to create dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(context);

        // Use builder to create dialog
        builder.setTitle(context.getString(R.string.locationAlerts)).setMessage(context.getString(R.string.enableGPSDesc));

        // Set positive button
        builder.setPositiveButton(R.string.enable, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                // Display the GPS setting screen
                context.startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
            }
        });

        // Set negative button
        builder.setNegativeButton(R.string.notNow, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                // Hide dialog
                dialog.dismiss();
            }
        });

        try {
            // Create the dialog
            mLocationServicesDialog = builder.create();

            // Show dialog
            mLocationServicesDialog.show();

            // Support for RTL languages
            RTLSupport.mirrorDialog(mLocationServicesDialog, context);

        }
        catch (Exception exc) {
            // Show toast instead
            Toast.makeText(context, context.getString(R.string.enableGPSDesc), Toast.LENGTH_LONG).show();
        }
    }

    private static void requestLocationPermissionIfNecessary(Activity context) {
        // Check if user hasn't yet granted permission
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
                ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // Display permission dialog
            ActivityCompat.requestPermissions(context,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION},
                    100);
        }
    }
} public class UpdateDialogs {
    public static void showUpdateDialog(final Context context, String newVersion) {
        // Use builder to create dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(context);

        // Insert version into message
        String message = String.format(context.getString(R.string.updateDesc), newVersion);

        // Use builder to create dialog
        builder.setTitle(context.getString(R.string.update)).setMessage(message);

        // Set positive button
        builder.setPositiveButton(R.string.updatePositive, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                // Open app page
                GooglePlay.openAppListingPage(context);
            }
        });

        // Set negative button
        builder.setNegativeButton(R.string.notNow, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                // Close dialog
                dialogInterface.dismiss();
            }
        });

        try {
            // Create the dialog
            AlertDialog dialog = builder.create();

            // Show dialog
            dialog.show();

            // Support for RTL languages
            RTLSupport.mirrorDialog(dialog, context);
        }
        catch (Exception exc) {
            // Show toast instead
            Toast.makeText(context, message, Toast.LENGTH_LONG).show();
        }
    }
} public class RTLSupport {
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    public static void mirrorActionBar(Activity activity) {
        // Hebrew only
        if (!Localization.isHebrewLocale(activity)) {
            return;
        }

        // Must be Jellybean or newer
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            // Set RTL layout direction
            activity.getWindow().getDecorView().setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
        }
    }

    @SuppressLint("NewApi")
    public static void mirrorDialog(Dialog dialog, Context context) {
        // Hebrew only
        if (!Localization.isHebrewLocale(context)) {
            return;
        }

        try {
            // Get message text view
            TextView message = (TextView) dialog.findViewById(android.R.id.message);

            // Defy gravity
            if (message != null) {
                message.setGravity(Gravity.RIGHT);
            }

            // Get the title of text view
            TextView title = (TextView) dialog.findViewById(context.getResources().getIdentifier("alertTitle", "id", "android"));

            // Defy gravity
            title.setGravity(Gravity.RIGHT);

            // Get list view (may not exist)
            ListView listView = (ListView) dialog.findViewById(context.getResources().getIdentifier("select_dialog_listview", "id", "android"));

            // Check if list & set RTL mode
            if (listView != null) {
                listView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
            }

            // Get title's parent layout
            LinearLayout parent = ((LinearLayout) title.getParent());

            // Get layout params
            LinearLayout.LayoutParams originalParams = (LinearLayout.LayoutParams) parent.getLayoutParams();

            // Set width to WRAP_CONTENT
            originalParams.width = LinearLayout.LayoutParams.WRAP_CONTENT;

            // Defy gravity
            originalParams.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;

            // Set layout params
            parent.setLayoutParams(originalParams);
        }
        catch (Exception exc) {
            // Log failure to logcat
            Log.d(Logging.TAG, "RTL failed", exc);
        }
    }
} public class AppNotifications {
    public static void clearAll(Context context) {
        // Get notification manager
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(context.NOTIFICATION_SERVICE);

        // Cancel all notifications
        notificationManager.cancelAll();

        // Stop alert sound if playing
        SoundLogic.stopSound(context);
    }
} public class RedAlertAPI {
    public static void register(String token, Context context) throws Exception {
        // Prepare an object to store and send the FCM device token to the API
        RegistrationRequest register = new RegistrationRequest(token, API.PLATFORM_IDENTIFIER);

        // Send the request to our API
        String response;

        try {
            // Execute request
            response = HTTP.post(API.API_ENDPOINT + "/register", Singleton.getJackson().writeValueAsString(register));
        }
        catch (Exception exc) {
            // Log failure
            throw new Exception("Registration failed", exc);
        }

        // Convert response into JSON object
        RegistrationResponse user;

        try {
            // Convert JSON to object
            user = Singleton.getJackson().readValue(response, RegistrationResponse.class);
        }
        catch (Exception exc) {
            // Log error
            throw new Exception("Parsing registration response failed", exc);
        }

        // Bad response?
        if (user.id == 0 || StringUtils.stringIsNullOrEmpty(user.hash)) {
            // Log error
            throw new Exception("Parsing registration response failed: " + response);
        }

        // Persist FCM token locally
        FCMRegistration.saveRegistrationToken(context, token);

        // Persist user ID and hash to SharedPreferences
        SharedPreferences.Editor editor = Singleton.getSharedPreferences(context).edit();

        // Store user info
        editor.putLong(context.getString(R.string.userIdPref), user.id);
        editor.putString(context.getString(R.string.userHashPref), user.hash);

        // Save and flush to disk
        editor.commit();

        // Log success
        Log.d(Logging.TAG, "RedAlert user registration success: " + user.id);
    }

    public static boolean isRegistered(Context context) {
        // Check for positive user ID
        return getUserId(context) > 0;
    }

    public static boolean isSubscribed(Context context) {
        // Check for subscribed boolean flag
        return Singleton.getSharedPreferences(context).getBoolean(context.getString(R.string.isSubscribedPref), false);
    }

    public static long getUserId(Context context) {
        // Query shared preferences
        return Singleton.getSharedPreferences(context).getLong(context.getString(R.string.userIdPref), 0);
    }

    public static String getUserHash(Context context) {
        // Query shared preferences
        return Singleton.getSharedPreferences(context).getString(context.getString(R.string.userHashPref), "");
    }

    public static void subscribe(Context context) throws Exception {
        List<String> primarySubscriptions = new ArrayList<>();
        List<String> secondarySubscriptions = new ArrayList<>();

        // Get user's selected primary zones
        String selectedZones = Singleton.getSharedPreferences(context).getString(context.getString(R.string.selectedZonesPref), context.getString(R.string.none));

        // Anything selected?
        if (!StringUtils.stringIsNullOrEmpty(selectedZones)) {
            // Explode selected zones into array and push into primarySubs
            primarySubscriptions.addAll(LocationData.explodeCitiesPSV(selectedZones));
        }
        else {
            // Empty value means all regions
            primarySubscriptions.add("all");
        }

        // Get user's selected cities
        String selectedCities = Singleton.getSharedPreferences(context).getString(context.getString(R.string.selectedCitiesPref), context.getString(R.string.none));

        // Anything selected?
        if (!StringUtils.stringIsNullOrEmpty(selectedCities)) {
            // Explode selected cities into array and push into primarySubs
            primarySubscriptions.addAll(LocationData.explodeCitiesPSV(selectedCities));
        }
        else {
            // Empty value means all cities
            primarySubscriptions.add("all");
        }

        // Get user's secondary cities
        String secondaryCities = Singleton.getSharedPreferences(context).getString(context.getString(R.string.selectedSecondaryCitiesPref), context.getString(R.string.all));

        // Anything selected?
        if (!StringUtils.stringIsNullOrEmpty(secondaryCities)) {
            // Explode selected cities into array and push into secondarySubs
            secondarySubscriptions.addAll(LocationData.explodeCitiesPSV(secondaryCities));
        }
        else {
            // Empty value means all cities
            secondarySubscriptions.add("all");
        }

        // Prepare an object to send a subscription request to the API
        SubscribeRequest subscribe = new SubscribeRequest(getUserId(context), getUserHash(context), cleanSubscriptions(primarySubscriptions), cleanSubscriptions(secondarySubscriptions));

        // Send the request to our API
        String responseJson;

        try {
            // Execute request
            responseJson = HTTP.post(API.API_ENDPOINT + "/subscribe", Singleton.getJackson().writeValueAsString(subscribe));
        }
        catch (Exception exc) {
            // Log failure
            throw new Exception("Subscribe request failed", exc);
        }

        // Convert response into JSON object
        GenericResponse response;

        try {
            // Convert JSON to object
            response = Singleton.getJackson().readValue(responseJson, GenericResponse.class);
        }
        catch (Exception exc) {
            // Log error
            throw new Exception("Parsing subscribe response failed", exc);
        }

        // Failed?
        if (!response.success) {
            // Log error
            throw new Exception("Failed to subscribe to alerts: " + response.error);
        }

        // Persist subscription flag to SharedPreferences
        SharedPreferences.Editor editor = Singleton.getSharedPreferences(context).edit();

        // Set is_subscribed to true
        editor.putBoolean(context.getString(R.string.isSubscribedPref), true);

        // Save and flush to disk
        editor.commit();
    }

    public static void updateNotificationPreferences(Context context) throws Exception {
        // Prepare an object to send the notification update request to the API
        UpdateNotificationsRequest updateNotifications = new UpdateNotificationsRequest(getUserId(context), getUserHash(context), AppPreferences.getNotificationsEnabled(context), AppPreferences.getSecondaryNotificationsEnabled(context));

        // Send the request to our API
        String responseJson;

        try {
            // Execute request
            responseJson = HTTP.post(API.API_ENDPOINT + "/update/notifications", Singleton.getJackson().writeValueAsString(updateNotifications));
        }
        catch (Exception exc) {
            // Log failure
            throw new Exception("Notification setting update failed", exc);
        }

        // Convert response into JSON object
        GenericResponse response;

        try {
            // Convert JSON to object
            response = Singleton.getJackson().readValue(responseJson, GenericResponse.class);
        }
        catch (Exception exc) {
            // Log error
            throw new Exception("Parsing notification preferences response failed", exc);
        }

        // Failed?
        if (!response.success) {
            // Log error
            throw new Exception("Failed to update notification preferences: " + response.error);
        }
    }

    public static void updateToken(String token, Context context) throws Exception {
        // Prepare an object to send the new token to the API
        UpdateTokenRequest updateRequest = new UpdateTokenRequest(getUserId(context), getUserHash(context), token);

        // Send the request to our API
        String responseJson;

        try {
            // Execute request
            responseJson = HTTP.post(API.API_ENDPOINT + "/update/token", Singleton.getJackson().writeValueAsString(updateRequest));
        }
        catch (Exception exc) {
            // Log failure
            throw new Exception("Push token update request failed", exc);
        }

        // Convert response into JSON object
        GenericResponse response;

        try {
            // Convert JSON to object
            response = Singleton.getJackson().readValue(responseJson, GenericResponse.class);
        }
        catch (Exception exc) {
            // Log error
            throw new Exception("Parsing push token request failed", exc);
        }

        // Failed?
        if (!response.success) {
            // Log error
            throw new Exception("Failed to update push token: " + response.error);
        }

        // Persist FCM token locally
        FCMRegistration.saveRegistrationToken(context, token);

        // Log success
        Log.d(Logging.TAG, "FCM token update success");
    }

    static List<String> cleanSubscriptions(List<String> subscriptions) {
        // Traverse items
        for (String item: subscriptions) {
            // All selected at least once?
            if (item.equals("all") || item.equals("")) {
                return Arrays.asList("all");
            }

            // "None" selected?
            if (item.equals("none")) {
                return new ArrayList<>();
            }
        }

        // Return original list
        return subscriptions;
    }
} public class Singleton {
    static ObjectMapper mMapper;
    static SharedPreferences mSettings;

    public static SharedPreferences getSharedPreferences(Context context) {
        // First time?
        if (mSettings == null) {
            // Open shared preferences
            mSettings = PreferenceManager.getDefaultSharedPreferences(context);
        }

        // Return cached object
        return mSettings;
    }

    public static ObjectMapper getJackson() {
        // First time?
        if (mMapper == null) {
            // Get Jackson instance
            mMapper = new ObjectMapper();

            // Ignore unknown properties
            mMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        }

        // Return cached object
        return mMapper;
    }
} public class Broadcasts {
    public static void subscribe(Context context, SharedPreferences.OnSharedPreferenceChangeListener listener) {
        // Listen for preference changes
        Singleton.getSharedPreferences(context).registerOnSharedPreferenceChangeListener(listener);
    }

    public static void unsubscribe(Context context, SharedPreferences.OnSharedPreferenceChangeListener listener) {
        // Unsubscribe from preference changes
        Singleton.getSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(listener);
    }

    public static void publish(Context context, String property) {
        // Edit shared preferences
        SharedPreferences.Editor edit = Singleton.getSharedPreferences(context).edit();

        // Store current ms in settings with the given property,
        // so that listeners will be notified
        edit.putLong(property, System.currentTimeMillis());

        // Save and flush
        edit.commit();
    }
} public class Volume {
    public static void setVolumeKeysAction(Activity context) {
        // Set the appropriate alert stream
        context.setVolumeControlStream(Sound.STREAM_TYPE);
    }
} public class StringUtils {
    public static boolean stringIsNullOrEmpty(String inputString) {
        // String is null? return true
        if (inputString == null) {
            return true;
        }

        // String is empty? true
        if (inputString.trim().equals("")) {
            return true;
        }

        // String is not empty
        return false;
    }

    public static String implode(String separator, List<String> data) {
        // No data?
        if (data.size() == 0) {
            return "";
        }

        // Create temp StringBuilder
        StringBuilder builder = new StringBuilder();

        // Loop over list except last item
        for (int i = 0; i < data.size() - 1; i++) {
            // Append current item
            builder.append(data.get(i));

            // Append separator
            builder.append(separator);
        }

        // Add last item without separator
        builder.append(data.get(data.size() - 1));

        // Return string
        return builder.toString();
    }

    public static String regexMatch(String source, String compilePattern) {
        // Compile regex
        Pattern compiledPattern = Pattern.compile(compilePattern);

        // Match against source input
        Matcher matcher = compiledPattern.matcher(source);

        // Prepare match object
        String match = "";

        // Did we find anything?
        if (matcher.find()) {
            // Get first group
            match = matcher.group(1);
        }

        // Return match
        return match;
    }
} public class GooglePlayServices {
    public static boolean isAvailable(Context context) {
        // Get availability checker
        GoogleApiAvailability playServices = GoogleApiAvailability.getInstance();

        // Check whether services are available
        int result = playServices.isGooglePlayServicesAvailable(context);

        // Are we good?
        return result == ConnectionResult.SUCCESS;
    }
} public class WhatsApp {
    public static boolean isAppInstalled(Context context) {
        // Get package manager
        PackageManager packageManager = context.getPackageManager();

        try {
            // Locate package by name
            packageManager.getPackageInfo(Integrations.WHATSAPP_PACKAGE, PackageManager.GET_ACTIVITIES);

            // If we are still here, the app exists
            return true;
        }
        catch (Exception exc) {
            // No such package
            return false;
        }
    }
} public class AppVersion {
    public static int getVersionCode(Context context) {
        try {
            // Try to query for the package
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);

            // Return version code
            return packageInfo.versionCode;
        }
        catch (Exception exc) {
            // Return fallback version
            return -1;
        }
    }

    public static String getVersionName(Context context) {
        try {
            // Try to query for the package
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);

            // Return version code
            return packageInfo.versionName;
        }
        catch (Exception exc) {
            // Return fallback version
            return "";
        }
    }
} public class LocationData {
    private static List<City> mCities;

    /*
    // Zone = דן 161
    // Zone Code = 161
    // City Name = תל אביב
    // City Countdown = 15 שניות
     */

    public static String[] getAllCityNames(Context context) {
        // Check for english locale
        boolean isHebrew = Localization.isHebrewLocale(context);

        // Get all city objects
        List<City> cities = getAllCities(context);

        // Prepare names array
        List<String> names = new ArrayList<>();

        // Loop over cities
        for (City city : cities) {
            // Hebrew?
            if (isHebrew) {
                // Add to list
                names.add(city.name);
            }
            else {
                // Add english name to list
                names.add(city.nameEnglish);
            }
        }

        // Return array
        return names.toArray(new String[names.size()]);
    }

    public static String[] getAllCityValues(Context context) {
        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Prepare array
        List<String> values = new ArrayList<String>();

        // Loop over cities
        for (City city : cities) {
            // Add to list
            values.add(city.value);
        }

        // Return array
        return values.toArray(new String[values.size()]);
    }

    public static String[] getAllCityZones(Context context) {
        // Get user's locale
        boolean isHebrew = Localization.isHebrewLocale(context);

        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Prepare array
        List<String> values = new ArrayList<String>();

        // Loop over cities
        for (City city : cities) {
            // Hebrew?
            if (isHebrew) {
                // Add to list
                values.add(city.zone);
            }
            else {
                // Add to list
                values.add(city.zoneEnglish);
            }
        }

        // Return array
        return values.toArray(new String[values.size()]);
    }

    public static List<City> getAllCities(Context context) {
        // Got it in cache?
        if (mCities != null) {
            return mCities;
        }

        // Initialize the list
        mCities = new ArrayList<>();

        try {
            // Open the cities.json for reading
            InputStream stream = context.getResources().openRawResource(R.raw.cities);

            // Create a buffered reader
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));

            // StringBuilder for efficiency
            StringBuilder builder = new StringBuilder();

            // A temporary variable to store current line
            String currentLine;

            // Read all lines
            while ((currentLine = reader.readLine()) != null) {
                // Append to builder
                builder.append(currentLine);
            }

            // Convert to string
            String json = builder.toString();

            // Convert to city objects
            mCities = Singleton.getJackson().readValue(json, new TypeReference<List<City>>() {
            });
        }
        catch (Exception exc) {
            // Log it
            Log.e(Logging.TAG, "Failed to load cities.json", exc);
        }

        // Return them
        return mCities;
    }

    public static String getSelectedCityNamesByValues(Context context, String selection, CharSequence[] names, CharSequence[] values) {
        // No value? All cities are selected
        if (StringUtils.stringIsNullOrEmpty(selection) || selection.equals(context.getString(R.string.all))) {
            return context.getString(R.string.allString);
        }

        // Value equals none | null?
        else if (selection.equals(context.getString(R.string.none)) || selection.equals(context.getString(R.string.nullString))) {
            return context.getString(R.string.noneString);
        }

        // Display names
        List<String> selectedCityNames = new ArrayList<String>();

        // Remove area code
        String[] selectedValues = selection.split("\\|");

        // Loop over values
        for (String selectedValue : selectedValues) {
            // Loop over values
            for (int i = 0; i < values.length; i++) {
                // Got the value?
                if (values[i].equals(selectedValue)) {
                    // Add the name to list
                    selectedCityNames.add(names[i].toString());
                }
            }
        }

        // Failed to find cities?
        if (selectedCityNames.size() == 0) {
            // Return "none"
            return context.getString(R.string.noneString);
        }

        // Implode into a CSV string
        String displayText = StringUtils.implode(", ", selectedCityNames);

        // Truncate if too long
        if (displayText.length() > 120) {
            displayText = displayText.substring(0, 120) + "...";
        }

        // Return display text
        return displayText;
    }

    public static String getLocalizedZoneWithCountdown(String cityName, Context context) {
        // Get user's locale
        boolean isHebrew = Localization.isHebrewLocale(context);

        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Loop over cities
        for (City city : cities) {
            // Got a match?
            if (city.name.equals(cityName)) {
                // Hebrew?
                if (isHebrew) {
                    // Return area countdown
                    return city.zone + " (" + city.time + ")";
                }
                else {
                    // Return area countdown in English
                    return city.zoneEnglish  + " (" + city.timeEnglish + ")";
                }
            }
        }

        // No match
        return "";
    }

    public static int getCityCountdown(String cityName, Context context) {
        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Loop over cities
        for (City city : cities) {
            // Got a match?
            if (city.name.equals(cityName)) {
                // Return countdown
                return city.countdown;
            }
        }

        // No match
        return 0;
    }

    public static String getLocalizedCityName(String cityName, Context context) {
        // Get user's locale
        boolean isHebrew = Localization.isHebrewLocale(context);

        // Hebrew?
        if (isHebrew) {
            return cityName;
        }

        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Loop over cities
        for (City city : cities) {
            // Got a match?
            if (city.name.equals(cityName)) {
                // Return english name
                return city.nameEnglish;
            }
        }

        // No match
        return cityName;
    }

    public static City getCityByName(String cityName, Context context) {
        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Loop over cities
        for (City city : cities) {
            // Got a match?
            if (city.name.equals(cityName)) {
                return city;
            }
        }

        // No match
        return null;
    }

    public static String getNearbyCityNames(Location myLocation, Context context) {
        // Get user's locale
        boolean isHebrew = Localization.isHebrewLocale(context);

        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Output array
        List<String> cityNames = new ArrayList<>();

        // Calculate max distance
        double maxDistance = LocationLogic.getMaxDistanceKilometers(context, -1);

        // String is not empty
        for (City city : cities) {
            // Prepare new location
            Location location = new Location("");

            // Set lat & long
            location.setLatitude(city.latitude);
            location.setLongitude(city.longitude);

            // Get distance to city in KM
            float distance = location.distanceTo(myLocation) / 1000;

            // Distance is less than max?
            if (distance <= maxDistance) {
                // Hebrew?
                if (isHebrew) {
                    // Add to list
                    cityNames.add(city.name);
                }
                else {
                    // Add to list
                    cityNames.add(city.nameEnglish);
                }
            }
        }

        // Join and add original code
        return StringUtils.implode(", ", cityNames);
    }

    public static Location getCityLocation(String cityName, Context context) {
        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Loop over cities
        for (City city : cities) {
            // Got a match?
            if (city.name.equals(cityName)) {
                // Prepare new location
                Location location = new Location(LocationManager.PASSIVE_PROVIDER);

                // Set lat & long
                location.setLatitude(city.latitude);
                location.setLongitude(city.longitude);

                // Add to list
                return location;
            }
        }

        // Fail
        return null;
    }

    public static List<String> explodeCitiesPSV(String citiesPSV) {
        // Unique cities list
        List<String> uniqueList = new ArrayList<>();

        // Explode into array
        List<String> psvList = Arrays.asList(citiesPSV.split("\\|"));

        // Loop over items
        for (String item : psvList) {
            // Not already added?
            if (!StringUtils.stringIsNullOrEmpty(item) && !uniqueList.contains(item) && !item.equals("none") && !item.equals("null")) {
                // Add it
                uniqueList.add(item);
            }
        }

        // Return them
        return uniqueList;
    }

    public static String getZoneByCityName(String cityName, Context context) {
        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Loop over cities
        for (City city : cities) {
            // Got a match?
            if (city.name.equals(cityName)) {
                return city.zone;
            }
        }

        // Unknown city
        return "";
    }

    public static String getLocalizedZoneByCityName(String cityName, Context context) {
        // Get user's locale
        boolean isHebrew = Localization.isHebrewLocale(context);

        // Prepare cities array
        List<City> cities = getAllCities(context);

        // Loop over cities
        for (City city : cities) {
            // Got a match?
            if (city.name.equals(cityName)) {
                return isHebrew ? city.zone: city.zoneEnglish;
            }
        }

        // Unknown city
        return "";
    }
} public class HTTP {
    public static String get(String urlString) throws Exception {
        // Create request
        HttpsURLConnection httpConnection = null;

        try {
            // Convert URL into object
            URL url = new URL(urlString);

            // Prepare request
            httpConnection = (HttpsURLConnection) url.openConnection();

            // Set general properties
            httpConnection.setUseCaches(false);
            httpConnection.setRequestMethod("GET");

            // Execute request
            httpConnection.connect();

            // Execute request and get response code
            int statusCode = httpConnection.getResponseCode();

            // Prepare input stream
            InputStream inputStream;

            // 200 OK?
            if (statusCode == 200) {
                // Use standard stream
                inputStream = httpConnection.getInputStream();
            }
            else {
                // Use error stream
                inputStream = httpConnection.getErrorStream();
            }

            // Prepare buffered reader
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

            // Prepare response buffer holder
            StringBuffer response = new StringBuffer();

            // Temporary line placeholder
            String tmpLine;

            // Read from reader until end of stream
            while ((tmpLine = reader.readLine()) != null) {
                // Append line
                response.append(tmpLine);
            }

            // Close reader
            reader.close();

            // Error?
            if (statusCode != 200) {
                // Throw it out
                throw new Exception("HTTP GET error: " + statusCode + "\n" + response.toString());
            }

            // Return response as string
            return response.toString();
        } catch (Exception exc) {
            // Throw connectivity exception
            throw new Exception(exc.toString() + exc.getStackTrace());
        }
        finally {
            // Got an open connection?
            if (httpConnection != null) {
                // Close it to avoid leaking resources
                httpConnection.disconnect();
            }
        }
    }

    public static String post(String urlString, String json) throws Exception {
        // Create request
        HttpsURLConnection httpConnection = null;

        try {
            // Convert URL into object
            URL url = new URL(urlString);

            // Prepare request
            httpConnection = (HttpsURLConnection) url.openConnection();

            // Set JSON headers
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");

            // Set general properties
            httpConnection.setDoInput(true);
            httpConnection.setDoOutput(true);
            httpConnection.setUseCaches(false);
            httpConnection.setRequestMethod("POST");

            // Send JSON post body
            OutputStream outputStream = httpConnection.getOutputStream();
            outputStream.write(json.getBytes("UTF-8"));
            outputStream.close();

            // Execute request and get response code
            int statusCode = httpConnection.getResponseCode();

            // Prepare input stream
            InputStream inputStream;

            // 200 OK?
            if (statusCode == 200) {
                // Use standard stream
                inputStream = httpConnection.getInputStream();
            }
            else {
                // Use error stream
                inputStream = httpConnection.getErrorStream();
            }

            // Prepare buffered reader
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

            // Prepare response buffer holder
            StringBuffer response = new StringBuffer();

            // Temporary line placeholder
            String tmpLine;

            // Read from reader until end of stream
            while ((tmpLine = reader.readLine()) != null) {
                // Append line
                response.append(tmpLine);
            }

            // Close reader
            reader.close();

            // Error?
            if (statusCode != 200) {
                // Throw it out
                throw new Exception("HTTP POST error: " + statusCode + "\n" + response.toString());
            }

            // Return response as string
            return response.toString();
        } catch (Exception exc) {
            // Throw connectivity exception
            throw new Exception(exc.toString() + exc.getStackTrace());
        }
        finally {
            // Got an open connection?
            if (httpConnection != null) {
                // Close it to avoid leaking resources
                httpConnection.disconnect();
            }
        }
    }
} public abstract class AsyncTaskAdapter<Params, Progress, Result> {
    private static final String LOG_TAG = "AsyncTaskAdapter";

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE = 1;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTaskAdapter #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);

    /**
     * An {@link Executor} that can be used to execute tasks in parallel.
     */
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
            TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

    /**
     * An {@link Executor} that executes tasks one at a time in serial
     * order.  This serialization is global to a particular process.
     */
    private static final int MESSAGE_POST_RESULT = 0x1;
    private static final int MESSAGE_POST_PROGRESS = 0x2;

    private static final InternalHandler sHandler = new InternalHandler();

    private static volatile Executor sDefaultExecutor = THREAD_POOL_EXECUTOR;
    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;
    private final AtomicBoolean mCancelled = new AtomicBoolean();
    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
    private volatile Status mStatus = Status.PENDING;

    /**
     * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
     */
    public AsyncTaskAdapter() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);

                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                //noinspection unchecked
                return postResult(doInBackground(mParams));
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                }
                catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                }
                catch (ExecutionException e) {
                    throw new RuntimeException("An error occured while executing doInBackground()",
                            e.getCause());
                }
                catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

    /**
     * @hide Used to force static handler to be created.
     */
    public static void init() {
        sHandler.getLooper();
    }

    /**
     * @hide
     */
    public static void setDefaultExecutor(Executor exec) {
        sDefaultExecutor = exec;
    }

    /**
     * Convenience version of {@link #execute(Object...)} for use with
     * a simple Runnable object. See {@link #execute(Object[])} for more
     * information on the order of execution.
     *
     * @see #execute(Object[])
     * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
     */
    public static void execute(Runnable runnable) {
        sDefaultExecutor.execute(runnable);
    }

    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

    private Result postResult(Result result) {
        @SuppressWarnings("unchecked")
        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskAdapterResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }

    /**
     * Returns the current status of this task.
     *
     * @return The current status.
     */
    public final Status getStatus() {
        return mStatus;
    }

    /**
     * Override this method to perform a computation on a background thread. The
     * specified parameters are the parameters passed to {@link #execute}
     * by the caller of this task.
     * <p/>
     * This method can call {@link #publishProgress} to publish updates
     * on the UI thread.
     *
     * @param params The parameters of the task.
     * @return A result, defined by the subclass of this task.
     * @see #onPreExecute()
     * @see #onPostExecute
     * @see #publishProgress
     */
    protected abstract Result doInBackground(Params... params);

    /**
     * Runs on the UI thread before {@link #doInBackground}.
     *
     * @see #onPostExecute
     * @see #doInBackground
     */
    protected void onPreExecute() {
    }

    /**
     * <p>Runs on the UI thread after {@link #doInBackground}. The
     * specified result is the value returned by {@link #doInBackground}.</p>
     * <p/>
     * <p>This method won't be invoked if the task was cancelled.</p>
     *
     * @param result The result of the operation computed by {@link #doInBackground}.
     * @see #onPreExecute
     * @see #doInBackground
     * @see #onCancelled(Object)
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected void onPostExecute(Result result) {
    }

    /**
     * Runs on the UI thread after {@link #publishProgress} is invoked.
     * The specified values are the values passed to {@link #publishProgress}.
     *
     * @param values The values indicating progress.
     * @see #publishProgress
     * @see #doInBackground
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected void onProgressUpdate(Progress... values) {
    }

    /**
     * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
     * {@link #doInBackground(Object[])} has finished.</p>
     * <p/>
     * <p>The default implementation simply invokes {@link #onCancelled()} and
     * ignores the result. If you write your own implementation, do not call
     * <code>super.onCancelled(result)</code>.</p>
     *
     * @param result The result, if any, computed in
     *               {@link #doInBackground(Object[])}, can be null
     * @see #cancel(boolean)
     * @see #isCancelled()
     */
    @SuppressWarnings({"UnusedParameters"})
    protected void onCancelled(Result result) {
        onCancelled();
    }

    /**
     * <p>Applications should preferably override {@link #onCancelled(Object)}.
     * This method is invoked by the default implementation of
     * {@link #onCancelled(Object)}.</p>
     * <p/>
     * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
     * {@link #doInBackground(Object[])} has finished.</p>
     *
     * @see #onCancelled(Object)
     * @see #cancel(boolean)
     * @see #isCancelled()
     */
    protected void onCancelled() {
    }

    /**
     * Returns <tt>true</tt> if this task was cancelled before it completed
     * normally. If you are calling {@link #cancel(boolean)} on the task,
     * the value returned by this method should be checked periodically from
     * {@link #doInBackground(Object[])} to end the task as soon as possible.
     *
     * @return <tt>true</tt> if task was cancelled before it completed
     * @see #cancel(boolean)
     */
    public final boolean isCancelled() {
        return mCancelled.get();
    }

    /**
     * <p>Attempts to cancel execution of this task.  This attempt will
     * fail if the task has already completed, already been cancelled,
     * or could not be cancelled for some other reason. If successful,
     * and this task has not started when <tt>cancel</tt> is called,
     * this task should never run. If the task has already started,
     * then the <tt>mayInterruptIfRunning</tt> parameter determines
     * whether the thread executing this task should be interrupted in
     * an attempt to stop the task.</p>
     * <p/>
     * <p>Calling this method will result in {@link #onCancelled(Object)} being
     * invoked on the UI thread after {@link #doInBackground(Object[])}
     * returns. Calling this method guarantees that {@link #onPostExecute(Object)}
     * is never invoked. After invoking this method, you should check the
     * value returned by {@link #isCancelled()} periodically from
     * {@link #doInBackground(Object[])} to finish the task as early as
     * possible.</p>
     *
     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
     *                              task should be interrupted; otherwise, in-progress tasks are allowed
     *                              to complete.
     * @return <tt>false</tt> if the task could not be cancelled,
     * typically because it has already completed normally;
     * <tt>true</tt> otherwise
     * @see #isCancelled()
     * @see #onCancelled(Object)
     */
    public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);
        return mFuture.cancel(mayInterruptIfRunning);
    }

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return The computed result.
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException    If the computation threw an exception.
     * @throws InterruptedException  If the current thread was interrupted
     *                               while waiting.
     */
    public final Result get() throws InterruptedException, ExecutionException {
        return mFuture.get();
    }

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result.
     *
     * @param timeout Time to wait before cancelling the operation.
     * @param unit    The time unit for the timeout.
     * @return The computed result.
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException    If the computation threw an exception.
     * @throws InterruptedException  If the current thread was interrupted
     *                               while waiting.
     * @throws TimeoutException      If the wait timed out.
     */
    public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
            ExecutionException, TimeoutException {
        return mFuture.get(timeout, unit);
    }

    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     * <p/>
     * <p>Note: this function schedules the task on a queue for a single background
     * thread or pool of threads depending on the platform version.  When first
     * introduced, AsyncTaskAdapters were executed serially on a single background thread.
     * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
     * to a pool of threads allowing multiple tasks to operate in parallel. Starting
     * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being
     * executed on a single thread to avoid common application errors caused
     * by parallel execution.  If you truly want parallel execution, you can use
     * the {@link #executeOnExecutor} version of this method
     * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings
     * on its use.
     * <p/>
     * <p>This method must be invoked on the UI thread.
     *
     * @param params The parameters of the task.
     * @return This instance of AsyncTaskAdapter.
     * @throws IllegalStateException If {@link #getStatus()} returns either
     *                               {@link AsyncTaskAdapter.Status#RUNNING} or {@link AsyncTaskAdapter.Status#FINISHED}.
     * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
     * @see #execute(Runnable)
     */
    public final AsyncTaskAdapter<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     * <p/>
     * <p>This method is typically used with {@link #THREAD_POOL_EXECUTOR} to
     * allow multiple tasks to run in parallel on a pool of threads managed by
     * AsyncTaskAdapter, however you can also use your own {@link Executor} for custom
     * behavior.
     * <p/>
     * <p><em>Warning:</em> Allowing multiple tasks to run in parallel from
     * a thread pool is generally <em>not</em> what one wants, because the order
     * of their operation is not defined.  For example, if these tasks are used
     * to modify any state in common (such as writing a file due to a button click),
     * there are no guarantees on the order of the modifications.
     * Without careful work it is possible in rare cases for the newer version
     * of the data to be over-written by an older one, leading to obscure data
     * loss and stability issues.  Such changes are best
     * executed in serial; to guarantee such work is serialized regardless of
     * <p/>
     * <p>This method must be invoked on the UI thread.
     *
     * @param exec   The executor to use.  {@link #THREAD_POOL_EXECUTOR} is available as a
     *               convenient process-wide thread pool for tasks that are loosely coupled.
     * @param params The parameters of the task.
     * @return This instance of AsyncTaskAdapter.
     * @throws IllegalStateException If {@link #getStatus()} returns either
     *                               {@link AsyncTaskAdapter.Status#RUNNING} or {@link AsyncTaskAdapter.Status#FINISHED}.
     * @see #execute(Object[])
     */
    public final AsyncTaskAdapter<Params, Progress, Result> executeOnExecutor(Executor exec,
                                                                              Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }

    /**
     * This method can be invoked from {@link #doInBackground} to
     * publish updates on the UI thread while the background computation is
     * still running. Each call to this method will trigger the execution of
     * {@link #onProgressUpdate} on the UI thread.
     * <p/>
     * {@link #onProgressUpdate} will not be called if the task has been
     * canceled.
     *
     * @param values The progress values to update the UI with.
     * @see #onProgressUpdate
     * @see #doInBackground
     */
    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskAdapterResult<Progress>(this, values)).sendToTarget();
        }
    }

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        }
        else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

    /**
     * Indicates the current status of the task. Each status will be set only once
     * during the lifetime of a task.
     */
    public enum Status {
        /**
         * Indicates that the task has not been executed yet.
         */
        PENDING,
        /**
         * Indicates that the task is running.
         */
        RUNNING,
        /**
         * Indicates that {@link AsyncTaskAdapter#onPostExecute} has finished.
         */
        FINISHED,
    }

    private static class InternalHandler extends Handler {
        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskAdapterResult result = (AsyncTaskAdapterResult) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

    @SuppressWarnings({"RawUseOfParameterizedType"})
    private static class AsyncTaskAdapterResult<Data> {
        final AsyncTaskAdapter mTask;
        final Data[] mData;

        AsyncTaskAdapterResult(AsyncTaskAdapter task, Data... data) {
            mTask = task;
            mData = data;
        }
    }
}
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import androidx.core.view.MenuItemCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Toast;
import me.pushy.sdk.lib.jackson.core.type.TypeReference;
import com.red.alert.R;
import com.red.alert.activities.settings.General;
import com.red.alert.config.API;
import com.red.alert.config.Alerts;
import com.red.alert.config.Integrations;
import com.red.alert.config.Logging;
import com.red.alert.config.RecentAlerts;
import com.red.alert.logic.communication.broadcasts.SettingsEvents;
import com.red.alert.logic.communication.intents.AlertViewParameters;
import com.red.alert.logic.communication.intents.MainActivityParameters;
import com.red.alert.logic.integration.BluetoothIntegration;
import com.red.alert.logic.push.FCMRegistration;
import com.red.alert.logic.push.PushyRegistration;
import com.red.alert.logic.services.ServiceManager;
import com.red.alert.logic.settings.AppPreferences;
import com.red.alert.model.Alert;
import com.red.alert.model.res.VersionInfo;
import com.red.alert.ui.adapters.AlertAdapter;
import com.red.alert.ui.compatibility.ProgressDialogCompat;
import com.red.alert.ui.dialogs.AlertDialogBuilder;
import com.red.alert.ui.dialogs.custom.BluetoothDialogs;
import com.red.alert.ui.dialogs.custom.LocationDialogs;
import com.red.alert.ui.dialogs.custom.UpdateDialogs;
import com.red.alert.ui.localization.rtl.RTLSupport;
import com.red.alert.ui.notifications.AppNotifications;
import com.red.alert.utils.backend.RedAlertAPI;
import com.red.alert.utils.caching.Singleton;
import com.red.alert.utils.communication.Broadcasts;
import com.red.alert.utils.feedback.Volume;
import com.red.alert.utils.formatting.StringUtils;
import com.red.alert.utils.integration.GooglePlayServices;
import com.red.alert.utils.integration.WhatsApp;
import com.red.alert.utils.metadata.AppVersion;
import com.red.alert.utils.metadata.LocationData;
import com.red.alert.utils.networking.HTTP;
import com.red.alert.utils.threading.AsyncTaskAdapter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

    void reloadRecentAlerts() {
        // Not already reloading?
        if (!mIsReloading) {
            // Get recent alerts async
            new GetRecentAlertsAsync().execute();
        }
    }

    void initializeSettingsButton(Menu OptionsMenu) {
        // Add refresh in Action Bar
        MenuItem settingsItem = OptionsMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, getString(R.string.settings));

        // Set up the view
        settingsItem.setIcon(R.drawable.ic_settings);

        // Specify the show flags
        MenuItemCompat.setShowAsAction(settingsItem, MenuItem.SHOW_AS_ACTION_ALWAYS);

        // On click, go to Settings
        settingsItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                // Start settings activity
                goToSettings(false);

                // Consume event
                return true;
            }
        });
    }

    void goToSettings(boolean showZoneSelection) {
        // Prepare new intent
        Intent settingsIntent = new Intent();

        // Set settings class
        settingsIntent.setClass(Main.this, General.class);

        // Add area selection boolean
        settingsIntent.putExtra(SettingsEvents.SHOW_ZONE_SELECTION, showZoneSelection);

        // Start settings activity
        startActivity(settingsIntent);
    }

    void initializeLoadingIndicator(Menu OptionsMenu) {
        // Add refresh in Action Bar
        mLoadingItem = OptionsMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, getString(R.string.loading));

        // Set up the view
        MenuItemCompat.setActionView(mLoadingItem, R.layout.loading);

        // Specify the show flags
        MenuItemCompat.setShowAsAction(mLoadingItem, MenuItem.SHOW_AS_ACTION_ALWAYS);

        // Hide by default
        mLoadingItem.setVisible(false);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu OptionsMenu) {
        // Add loading indicator
        initializeLoadingIndicator(OptionsMenu);

        // Add settings button
        initializeSettingsButton(OptionsMenu);

        // Show the menu
        return true;
    }

    private int getRecentAlerts() {
        // Store JSON as string initially
        String alertsJSON;

        try {
            // Get it from /alerts
            alertsJSON = HTTP.get(API.API_ENDPOINT + "/alerts");
        }
        catch (Exception exc) {
            // Log it
            Log.e(Logging.TAG, "Get recent alerts request failed", exc);

            // Return error code
            return R.string.apiRequestFailed;
        }

        // Prepare tmp object list
        List<Alert> recentAlerts;

        try {
            // Convert JSON to object
            recentAlerts = Singleton.getJackson().readValue(alertsJSON, new TypeReference<List<Alert>>() {
            });
        }
        catch (Exception exc) {
            // Log it
            Log.e(Logging.TAG, "Get recent alerts request failed", exc);

            // Return error code
            return R.string.jsonFailed;
        }

        // Initialize date format
        SimpleDateFormat dateFormat = new SimpleDateFormat(Alerts.DATE_FORMAT);

        // For debugging purposes
        // Alert fake = new Alert();
        // fake.city = "אבו סנאן";
        // fake.date = 1562037706;

        // recentAlerts.add(fake);

        // Loop over alerts
        for (Alert alert : recentAlerts) {
            // Convert date to string
            alert.dateString = dateFormat.format(alert.date * 1000);

            // Convert area to friendly name
            alert.desc = LocationData.getLocalizedZoneWithCountdown(alert.city, this);