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 Lifeshield {
    // Operation Lifeshield official URL
    public static String WEBSITE_LINK = "http://www.operationlifeshield.org/";
} public class Logging {
    // Tag for all logcat logs
    public static String TAG = "RedAlert";
} public class Support {
    // Contact e-mails get sent to this address
    public static String CONTACT_EMAIL = "support@redalert.me";

    // App website button opens this URL
    public static String WEBSITE_LINK = "https://redalert.me/";
} public class Testing {
    // Max time in seconds to wait for a test push to arrive
    public static int PUSH_GATEWAY_TIMEOUT_SECONDS = 8;

    // Max time in seconds to wait for an external device to connect
    public static int DEVICE_CONNECTION_TIMEOUT_SECONDS = 15;
} public class AlertLogic {
    public static void processIncomingAlert(String citiesPSVString, String alertType, Context context) {
        // No cities?
        if (StringUtils.stringIsNullOrEmpty(citiesPSVString)) {
            return;
        }

        // Log the cities
        Log.i(Logging.TAG, "Received alert (" + alertType + "): " + citiesPSVString);

        // Get alert cities as list
        List<String> cityList = LocationData.explodeCitiesPSV(citiesPSVString);

        // Loop over cities
        for (String city : cityList) {
            // Store default alert type in variable, override it later
            String overrideAlertType = alertType;

            // Is this a relevant city?
            if (isRelevantCity(overrideAlertType, city, context)) {
                // Secondary alert?
                if (isSecondaryCity(overrideAlertType, city, context)) {
                    // Set type
                    overrideAlertType = AlertTypes.SECONDARY;
                }

                // Not a test alert?
                if (!overrideAlertType.contains("test")) {
                    // Did we recently notify for this city?
                    if (cityRecentlyNotified(city, context)) {
                        // Log that we're skipping this one
                        Log.i(Logging.TAG, "Ignoring recently notified city: " + city);
                        continue;
                    }

                    // Save city last alert timestamp to prevent duplicate alerts
                    AppPreferences.setCityLastAlertTime(city, DateTime.getUnixTimestamp(), context);
                }

                // Localize city name
                String localizedCityName = LocationData.getLocalizedCityName(city, context);

                // Get impact countdown
                String zoneWithCountdown = LocationData.getLocalizedZoneWithCountdown(city, context);

                // Issue the notification
                RocketNotifications.notify(context, city, localizedCityName, zoneWithCountdown, overrideAlertType, null);
            }
        }
    }

    static boolean cityRecentlyNotified(String city, Context context) {
        // Buffer time in between alerts for same city (to prevent duplicate alerts)
        long recentCutoffTimestamp = DateTime.getUnixTimestamp() - Alerts.DUPLICATE_ALERTS_PADDING_TIME;

        // Check that enough time passed
        return AppPreferences.getCityLastAlert(city, context) > recentCutoffTimestamp;
    }

    public static boolean isRelevantCity(String alertType, String city, Context context) {
        // Test or system message?
        if (isSystemTestAlert(alertType, context)) {
            return true;
        }

        // Did user select this city? Either via entire zones or cities
        if (isCitySelectedPrimarily(city, context)) {
            return true;
        }

        // Did user select this city?
        if (isSecondaryCitySelected(city, context)) {
            return true;
        }

        // Are we nearby?
        if (isNearby(city, context)) {
            return true;
        }

        // Irrelevant
        return false;
    }

    public static boolean isSecondaryCity(String alertType, String city, Context context) {
        // System or test?
        if (isSystemTestAlert(alertType, context)) {
            return false;
        }

        // Did user select this area?
        if (isCitySelectedPrimarily(city, context)) {
            return false;
        }

        // Not a nearby city?
        if (isNearby(city, context)) {
            return false;
        }

        // Did user select this city?
        if (isSecondaryCitySelected(city, context)) {
            return true;
        }

        // If we are here, not selected
        return false;
    }

    public static boolean isCitySelectedPrimarily(String city, Context context) {
        // Get enabled / disabled setting
        boolean notificationsEnabled = AppPreferences.getNotificationsEnabled(context);

        // Are real time alerts disabled?
        if (!notificationsEnabled) {
            return false;
        }

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

        // All are selected?
        if (StringUtils.stringIsNullOrEmpty(selectedZones) || selectedZones.equals(context.getString(R.string.all))) {
            return true;
        }

        // Explode selected zones into array
        List<String> selectedZonesList = Arrays.asList(selectedZones.split("\\|"));

        // Get zone by city name
        String cityZone = LocationData.getZoneByCityName(city, context);

        // Check for containment
        if (selectedZonesList.contains(cityZone)) {
            return true;
        }

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

        // All selected?
        if (StringUtils.stringIsNullOrEmpty(selectedCities) || selectedCities.equals(context.getString(R.string.all))) {
            return true;
        }

        // Explode into array
        List<String> selectedCitiesList = LocationData.explodeCitiesPSV(selectedCities);

        // Selected this city?
        if (selectedCitiesList.contains(city)) {
            // We got a match!
            return true;
        }

        // No match
        return false;
    }

    public static boolean isSecondaryCitySelected(String city, Context context) {
        // Get main enabled setting
        boolean notificationsEnabled = AppPreferences.getNotificationsEnabled(context);

        // Are real time alerts enabled?
        if (!notificationsEnabled) {
            return false;
        }

        // Get secondary enabled setting
        notificationsEnabled = AppPreferences.getSecondaryNotificationsEnabled(context);

        // Are secondary alerts enabled?
        if (!notificationsEnabled) {
            return false;
        }

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

        // All selected?
        if (StringUtils.stringIsNullOrEmpty(secondaryCities) || secondaryCities.equals(context.getString(R.string.all))) {
            return true;
        }

        // Get selected cities as list
        List<String> selectedCities = LocationData.explodeCitiesPSV(secondaryCities);

        // Did we select this city?
        if (selectedCities.contains(city)) {
            return true;
        }

        // No match
        return false;
    }

    public static boolean isNearby(String cityName, Context context) {
        // Get nearby alerts enabled setting
        boolean nearbyEnabled = Singleton.getSharedPreferences(context).getBoolean(context.getString(R.string.locationAlertsPref), false);

        // Are nearby alerts enabled?
        if (!nearbyEnabled) {
            return false;
        }

        // Get current location (last known)
        Location myLocation = LocationLogic.getLocation(context);

        // No recent location?
        if (myLocation == null) {
            return false;
        }

        // Get city geolocation
        Location location = LocationData.getCityLocation(cityName, context);

        // No match?
        if (location == null) {
            return false;
        }

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

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

        // Distance is less than max?
        if (distance <= maxDistance) {
            // We are nearby!
            return true;
        }

        // No match
        return false;
    }

    public static boolean isSystemTestAlert(String alertType, Context context) {
        // Self-test?
        if (alertType.equals(AlertTypes.TEST)) {
            return true;
        }

        // System message?
        if (alertType.equals(AlertTypes.SYSTEM)) {
            return true;
        }

        // Not system nor test
        return false;
    }
} public class AlertTypes {
    public static final String SYSTEM = "system";
    public static final String PRIMARY = "alert";
    public static final String SECONDARY = "secondary_alert";

    public static final String TEST = "test";
    public static final String TEST_SOUND = "test_sound";
    public static final String TEST_SECONDARY_SOUND = "test_secondary_sound";
} public class LocationSelectionEvents {
    public static final String LOCATIONS_UPDATED = "LocationsUpdated";
} public class SelfTestEvents {
    public static final String FCM_TEST_PASSED = "FCMTestPassed";
    public static final String PUSHY_TEST_PASSED = "PushyTestPassed";
} public class SettingsEvents {
    public static final String SHOW_ZONE_SELECTION = "ShowZoneSelection";
} 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 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 SelfTestRequest {
    @JsonProperty("token")
    public String token;

    @JsonProperty("platform")
    public String platform;

    @JsonProperty("locale")
    public String locale;

    public SelfTestRequest(String token, String locale, String platform) {
        // Set FCM/Pushy registration token
        this.token = token;

        // Set the push type
        this.platform = platform;

        // Set the language for localization
        this.locale = locale;

    }
} public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
    private AppCompatDelegate mDelegate;

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

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        getDelegate().onPostCreate(savedInstanceState);
    }

    public ActionBar getSupportActionBar() {
        return getDelegate().getSupportActionBar();
    }

    public void setSupportActionBar(@Nullable Toolbar toolbar) {
        getDelegate().setSupportActionBar(toolbar);
    }

    @Override
    public MenuInflater getMenuInflater() {
        return getDelegate().getMenuInflater();
    }

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

    @Override
    public void setContentView(View view) {
        getDelegate().setContentView(view);
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getDelegate().setContentView(view, params);
    }

    @Override
    public void addContentView(View view, ViewGroup.LayoutParams params) {
        getDelegate().addContentView(view, params);
    }

    @Override
    protected void onPostResume() {
        super.onPostResume();
        getDelegate().onPostResume();
    }

    @Override
    protected void onTitleChanged(CharSequence title, int color) {
        super.onTitleChanged(title, color);
        getDelegate().setTitle(title);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        getDelegate().onConfigurationChanged(newConfig);
    }

    @Override
    protected void onStop() {
        super.onStop();
        getDelegate().onStop();
    }

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

    public void invalidateOptionsMenu() {
        getDelegate().invalidateOptionsMenu();
    }

    private AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, null);
        }
        return mDelegate;
    }
} 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 SearchableMultiSelectPreference extends ListPreference {
    private static final String DEFAULT_SEPARATOR = "OV=I=XseparatorX=I=VO";
    private String separator;
    private String checkAllKey = null;
    private boolean[] mClickedDialogEntryIndices;

    // Constructor
    public SearchableMultiSelectPreference(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchableMultiSelectPreference);
        checkAllKey = a.getString(R.styleable.SearchableMultiSelectPreference_checkAll);
        String s = a.getString(R.styleable.SearchableMultiSelectPreference_separator);
        if (s != null) {
            separator = s;
        }
        else {
            separator = DEFAULT_SEPARATOR;
        }

        if (getEntries() != null) {
            // Initialize the array of boolean to the same size as number of entries
            mClickedDialogEntryIndices = new boolean[getEntries().length];
        }
    }

    // Credits to kurellajunior on this post http://snippets.dzone.com/posts/show/91
    protected static String join(Iterable<? extends Object> pColl, String separator) {
        Iterator<? extends Object> oIter;
        if (pColl == null || (!(oIter = pColl.iterator()).hasNext()))
            return "";
        StringBuilder oBuilder = new StringBuilder(String.valueOf(oIter.next()));
        while (oIter.hasNext())
            oBuilder.append(separator).append(oIter.next());
        return oBuilder.toString();
    }

    /**
     * @param straw     String to be found
     * @param haystack  Raw string that can be read direct from preferences
     * @param separator Separator string. If null, static default separator will be used
     * @return boolean True if the straw was found in the haystack
     */
    public static boolean contains(String straw, String haystack, String separator) {
        if (separator == null) {
            separator = DEFAULT_SEPARATOR;
        }
        String[] vals = haystack.split(Pattern.quote(separator));
        for (int i = 0; i < vals.length; i++) {
            if (vals[i].equals(straw)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void setEntries(CharSequence[] entries) {
        super.setEntries(entries);

        // No entries?
        if (entries == null) {
            return;
        }

        // Initialize the array of boolean to the same size as number of entries
        mClickedDialogEntryIndices = new boolean[entries.length];
    }

    @Override
    protected void onPrepareDialogBuilder(Builder builder) {
        final CharSequence[] entries = getEntries();
        final CharSequence[] entryValues = getEntryValues();
        final String[] areaCodes = LocationData.getAllCityZones(getContext());

        if (entries == null || entryValues == null || entries.length != entryValues.length) {
            throw new IllegalStateException(
                    "ListPreference requires an entries array and an entryValues array which are both the same length");
        }

        restoreCheckedEntries();

        final List<ListItemWithIndex> allItems = new ArrayList<ListItemWithIndex>();
        final List<ListItemWithIndex> filteredItems = new ArrayList<ListItemWithIndex>();

        final boolean isCitySelection = entryValues.length == areaCodes.length;

        for (int i = 0; i < entries.length; i++) {
            final Object obj = entries[i];

            boolean isDefault = i == 0;
            boolean checked = mClickedDialogEntryIndices[i];

            String areaCode = null;

            if (isCitySelection) {
                areaCode = areaCodes[i];
            }

            final ListItemWithIndex listItemWithIndex = new ListItemWithIndex(i, obj.toString(), areaCode, checked, isDefault);
            allItems.add(listItemWithIndex);
            filteredItems.add(listItemWithIndex);
        }

        if (isCitySelection) {
            Collections.sort(filteredItems);
        }

        final ArrayAdapter<ListItemWithIndex> objectsAdapter = new ArrayAdapter<ListItemWithIndex>(getContext(), R.layout.multiselect_checkable, filteredItems) {
            @Override
            public View getView(final int position, View v, final ViewGroup parent) {
                final ViewHolder viewHolder;

                if (v == null) {
                    LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                    v = vi.inflate(R.layout.multiselect_checkable, null);

                    viewHolder = new ViewHolder();

                    viewHolder.label = (TextView) v.findViewById(R.id.label);
                    viewHolder.subLabel = (TextView) v.findViewById(R.id.subLabel);
                    viewHolder.checkbox = (CheckBox) v.findViewById(R.id.checkbox);

                    v.setTag(viewHolder);
                }
                else {
                    viewHolder = (ViewHolder) v.getTag();
                }


                final ListItemWithIndex item = filteredItems.get(position);
                final String name = item.value;
                final String code = item.areaCode;

                viewHolder.label.setText(name);
                viewHolder.subLabel.setText(code);

                if (StringUtils.stringIsNullOrEmpty(code) || code.equals(getContext().getString(R.string.all))) {
                    viewHolder.subLabel.setVisibility(View.GONE);
                }
                else {
                    viewHolder.subLabel.setVisibility(View.VISIBLE);
                }

                viewHolder.checkbox.setOnCheckedChangeListener(null);
                viewHolder.checkbox.setChecked(mClickedDialogEntryIndices[getRealPosition(name)]);

                v.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        viewHolder.checkbox.setChecked(!viewHolder.checkbox.isChecked());
                    }
                });

                viewHolder.checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                    @Override
                    public void onCheckedChanged(CompoundButton compoundButton, boolean val) {
                        // Checked all?

                        if (mClickedDialogEntryIndices[0] == true) {
                            ((ListView) parent).setItemChecked(0, false);
                            mClickedDialogEntryIndices[0] = false;
                        }

                        int realPosition = getRealPosition(name);

                        if (isCheckAllValue(realPosition) == true) {
                            checkAll(getDialog(), val);
                        }

                        mClickedDialogEntryIndices[realPosition] = val;
                        viewHolder.checkbox.setChecked(val);

                        item.checked = val;

                        canCheckAll();

                        // Invalidate list views (to update checkbox display)
                        ListView lv = getDialog().findViewById(R.id.searchListView);
                        lv.invalidateViews();
                    }
                });

                return v;
            }

            void canCheckAll() {
                for (int i = 1; i < mClickedDialogEntryIndices.length; i++) {
                    if (!mClickedDialogEntryIndices[i]) {
                        return;
                    }
                }

                // Still here? Check all
                checkAll(getDialog(), true);

                return;
            }

            int getRealPosition(String name) {
                for (int i = 0; i < entries.length; i++) {
                    if (entries[i].equals(name)) {
                        return i;
                    }
                }

                return 0;
            }

            @Override
            public Filter getFilter() {
                return new Filter() {
                    @SuppressWarnings("unchecked")
                    @Override
                    protected void publishResults(final CharSequence constraint, final FilterResults results) {
                        filteredItems.clear();
                        filteredItems.addAll((List<ListItemWithIndex>) results.values);

                        if (isCitySelection) {
                            Collections.sort(filteredItems);
                        }

                        notifyDataSetChanged();
                    }

                    @Override
                    protected Filter.FilterResults performFiltering(final CharSequence constraint) {
                        final FilterResults results = new FilterResults();

                        final String filterString = constraint.toString().toLowerCase();
                        final ArrayList<ListItemWithIndex> list = new ArrayList<ListItemWithIndex>();
                        for (final ListItemWithIndex obj : allItems) {
                            final String objStr = obj.toString().toLowerCase();
                            if (StringUtils.stringIsNullOrEmpty(filterString)
                                    || objStr.contains(filterString)
                                    || (obj.areaCode != null && obj.areaCode.toLowerCase().equals(filterString))
                                    ) {
                                list.add(obj);
                            }
                        }

                        results.values = list;
                        results.count = list.size();
                        return results;
                    }
                };
            }
        };

        LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        LinearLayout parent = (LinearLayout) vi.inflate(R.layout.multiselect_search_box, null);
        final EditText searchEditText = (EditText) parent.findViewById(R.id.searchEditText);

        searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
                InputMethodManager inputManager = (InputMethodManager)
                        getContext().getSystemService(Context.INPUT_METHOD_SERVICE);

                inputManager.hideSoftInputFromWindow(textView.getWindowToken(),
                        InputMethodManager.HIDE_NOT_ALWAYS);

                return false;
            }
        });
        searchEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void onTextChanged(final CharSequence arg0, final int arg1, final int arg2, final int arg3) {
            }

            @Override
            public void beforeTextChanged(final CharSequence arg0, final int arg1, final int arg2, final int arg3) {
            }

            @Override
            public void afterTextChanged(final Editable arg0) {
                objectsAdapter.getFilter().filter(searchEditText.getText());
            }
        });

        final ListView listView = (ListView) parent.findViewById(R.id.searchListView);
        listView.setAdapter(objectsAdapter);

        builder.setView(parent);

        //builder.setView()
        /*builder.setMultiChoiceItems(entries, mClickedDialogEntryIndices,
                new DialogInterface.OnMultiChoiceClickListener() {
                    public void onClick(DialogInterface dialog, int which, boolean val) {
                        if( isCheckAllValue( which ) == true ) {
                            checkAll( dialog, val );
                        }
                        mClickedDialogEntryIndices[which] = val;
                    }
                });  */
    }

    private boolean isCheckAllValue(int which) {
        final CharSequence[] entryValues = getEntryValues();
        if (checkAllKey != null) {
            return entryValues[which].equals(checkAllKey);
        }
        return false;
    }

    private void checkAll(DialogInterface dialog, boolean val) {
        if (dialog == null) {
            return;
        }

        ListView lv = (ListView) ((AlertDialog) dialog).findViewById(R.id.searchListView);
        int size = lv.getCount();
        for (int i = 0; i < size; i++) {
            lv.setItemChecked(i, val);
            mClickedDialogEntryIndices[i] = val;
        }
    }

    public String[] parseStoredValue(CharSequence val) {
        if ("".equals(val)) {
            return null;
        }
        else {
            return ((String) val).split(Pattern.quote(separator));
        }
    }

    private void restoreCheckedEntries() {
        CharSequence[] entryValues = getEntryValues();

        boolean checkAll = false;

        // Explode the string read in sharedpreferences
        String[] vals = parseStoredValue(getValue());

        if (vals == null) {
            checkAll = true;
            vals = new String[]{""};
        }

        List<String> valuesList = Arrays.asList(vals);

        if (valuesList.size() == 1) {
            if (valuesList.get(0).equals(getContext().getString(R.string.all))) {
                checkAll = true;
            }
        }

        for (int i = 0; i < entryValues.length; i++) {
            CharSequence entry = entryValues[i];
            if (valuesList.contains(entry) || checkAll) {
                mClickedDialogEntryIndices[i] = true;
            }
            else {
                mClickedDialogEntryIndices[i] = false;
            }
        }
    }

    @Override
    protected void onDialogClosed(boolean positiveResult) {
        ArrayList<String> values = new ArrayList<String>();

        CharSequence[] entryValues = getEntryValues();

        if (positiveResult && entryValues != null) {
            // Check all selected?

            if (entryValues.length > 1) {
                if (entryValues[0].equals(getContext().getString(R.string.all))) {
                    if (mClickedDialogEntryIndices[0] == true) {
                        // Set empty value
                        setValue(join(values, separator));
                        Broadcasts.publish(getContext(), LocationSelectionEvents.LOCATIONS_UPDATED);
                        return;
                    }
                }
            }

            // Cities selected?
            for (int i = 0; i < entryValues.length; i++) {
                if (mClickedDialogEntryIndices[i] == true) {
                    // Don't save the state of check all option - if any
                    String val = (String) entryValues[i];
                    if (checkAllKey == null || (val.equals(checkAllKey) == false)) {
                        values.add(val);
                    }
                }
            }

            // Nothing selected
            if (values.size() == 0) {
                values.add(getContext().getString(R.string.nullString));
            }

            //if (callChangeListener(value)) {
            setValue(join(values, separator));
            //}
        }

        if (positiveResult) {
            Broadcasts.publish(getContext(), LocationSelectionEvents.LOCATIONS_UPDATED);
        }
    }

    public void showDialog() {
        super.showDialog(null);
    }

    public static class ViewHolder {
        public TextView label;
        public TextView subLabel;
        public CheckBox checkbox;
    }

    // TODO: Would like to keep this static but separator then needs to be put in by hand or use default separator "OV=I=XseparatorX=I=VO"...

    private static final class ListItemWithIndex implements Comparable {
        public boolean checked;
        public boolean isDefault;

        public int index;
        public String value;
        public String areaCode;

        public ListItemWithIndex(int index, String value, String areaCode, boolean checked, boolean isDefault) {
            super();
            this.index = index;
            this.value = value;
            this.areaCode = areaCode;
            this.checked = checked;
            this.isDefault = isDefault;
        }

        @Override
        public String toString() {
            return value;
        }

        @Override
        public int compareTo(Object o) {
            ListItemWithIndex compare = (ListItemWithIndex) o;

            if (isDefault) {
                return -1;
            }

            if (compare.isDefault) {
                return 1;
            }

            if (checked && !compare.checked) {
                return -1;
            }

            if (!checked && compare.checked) {
                return 1;
            }

            if (checked && compare.checked) {
                return value.compareTo(compare.value);
            }

            return value.compareTo(compare.value);
        }
    }
} 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 GooglePlay {
    public static void openAppListingPage(Context context) {
        // Initialize Google Play intent
        Intent rateAppIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getApplicationContext().getPackageName()));

        // Is Google Play installed?
        if (context.getPackageManager().queryIntentActivities(rateAppIntent, 0).size() > 0) {
            try {
                // Try to open Google Play and navigate to app page
                context.startActivity(rateAppIntent);
            }
            catch (Exception exc) {
                // Log it
                Log.e(Logging.TAG, "Rate activity launch failed", exc);
            }
        }
        else {
            // Log it
            Log.e(Logging.TAG, "Can't rate. Google Play app not installed");
        }
    }
} 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.Dialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import androidx.core.view.MenuItemCompat;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.red.alert.R;
import com.red.alert.config.API;
import com.red.alert.config.Lifeshield;
import com.red.alert.config.Logging;
import com.red.alert.config.Support;
import com.red.alert.config.Testing;
import com.red.alert.logic.alerts.AlertLogic;
import com.red.alert.logic.alerts.AlertTypes;
import com.red.alert.logic.communication.broadcasts.LocationSelectionEvents;
import com.red.alert.logic.communication.broadcasts.SelfTestEvents;
import com.red.alert.logic.communication.broadcasts.SettingsEvents;
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.settings.AppPreferences;
import com.red.alert.model.req.SelfTestRequest;
import com.red.alert.ui.activities.AppCompatPreferenceActivity;
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.elements.SearchableMultiSelectPreference;
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.marketing.GooglePlay;
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;
package com.red.alert.activities.settings;



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);