public class AppSingleton {

    private static AppSingleton sSingleton;
    private final Context mContext;
    private DatabaseHelper mDatabaseHelper;
    private FiveCallsApi mFiveCallsApi;

    public static final AppSingleton getInstance(Context context) {
        if (sSingleton == null) {
            sSingleton = new AppSingleton(context);
        }
        return sSingleton;
    }

    private AppSingleton(Context context) {
        mContext = context;
    }

    public DatabaseHelper getDatabaseHelper() {
        if (mDatabaseHelper == null) {
            mDatabaseHelper = new DatabaseHelper(mContext);
        }
        return mDatabaseHelper;
    }

    public FiveCallsApi getJsonController() {
        if (mFiveCallsApi == null) {
            mFiveCallsApi = new FiveCallsApi(mContext);
        }
        return mFiveCallsApi;
    }
} public class FiveCallsApplication extends Application {
    private Tracker mTracker;
    private int mRunningActivities;
    private Thread.UncaughtExceptionHandler mDefaultHandler;

    public FiveCallsApplication() {
        super();
        mRunningActivities = 0;
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

            }

            @Override
            public void onActivityStarted(Activity activity) {
                mRunningActivities++;
            }

            @Override
            public void onActivityResumed(Activity activity) {
                // Clear reminder notifications whenever the app is re-opened.
                NotificationUtils.clearNotifications(activity);
            }

            @Override
            public void onActivityPaused(Activity activity) {

            }

            @Override
            public void onActivityStopped(Activity activity) {
                mRunningActivities--;
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {

            }
        });
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // We could automatically report all exceptions with an XML change to the configuration file
        // but that wouldn't respect the user's setting to share data with 5 calls.
        if (AccountManager.Instance.allowAnalytics(getApplicationContext())) {
            enableAnalyticsHandler();
        }

        // Set up OneSignal.
        OneSignalNotificationController.setUp(this);

    }

    /**
     * Gets the default {@link Tracker} for this {@link Application}.
     * @return tracker
     */
    synchronized public Tracker getDefaultTracker() {
        if (mTracker == null) {
            GoogleAnalytics analytics = GoogleAnalytics.getInstance(this);
            // To enable debug logging use: adb shell setprop log.tag.GAv4 DEBUG
            mTracker = analytics.newTracker(R.xml.global_tracker);
        }
        return mTracker;
    }

    public boolean isRunning() {
        return mRunningActivities > 0;
    }

    public void enableAnalyticsHandler() {
        if (mDefaultHandler == null) {
            mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        }
        Thread.UncaughtExceptionHandler analyticsHandler = new ExceptionReporter(
                getDefaultTracker(),
                Thread.getDefaultUncaughtExceptionHandler(),
                getApplicationContext());

        // Make myHandler the new default uncaught exception handler.
        Thread.setDefaultUncaughtExceptionHandler(analyticsHandler);
    }

    public void disableAnalyticsHandler() {
        if (mDefaultHandler != null) {
            // In this case, Analytics handler was enabled, so disable it.
            Thread.setDefaultUncaughtExceptionHandler(mDefaultHandler);
        }
    }
} public class OutcomeAdapter extends RecyclerView.Adapter<OutcomeAdapter.ViewHolder> {

    public static final List<Outcome> DEFAULT_OUTCOMES = Arrays.asList(
            new Outcome(Outcome.Status.UNAVAILABLE),
            new Outcome(Outcome.Status.VOICEMAIL),
            new Outcome(Outcome.Status.CONTACT));

    private List<Outcome> outcomes;
    private boolean enabled;
    private Callback callback;

    public OutcomeAdapter(List<Outcome> outcomes, boolean enabled, Callback callback) {
        this.outcomes = outcomes;
        this.enabled = enabled;
        this.callback = callback;
    }

    public OutcomeAdapter(List<Outcome> outcomes, Callback callback) {
        this(outcomes, true, callback);
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
        notifyDataSetChanged();
    }

    @Override
    public OutcomeAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ViewHolder(LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_outcome, parent, false));
    }

    @Override
    public void onBindViewHolder(OutcomeAdapter.ViewHolder holder, int position) {
        ViewHolder.bind(holder, outcomes.get(position), enabled, callback);
    }

    @Override
    public int getItemCount() {
        return outcomes.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        Button outcomeButton;

        public ViewHolder(View itemView) {
            super(itemView);
            outcomeButton = (Button) itemView.findViewById(R.id.outcome_button);
        }

        public static void bind(ViewHolder holder, final Outcome outcome, boolean enabled,
                                final Callback callback) {
            holder.outcomeButton.setText(Outcome.getDisplayString(holder.itemView.getContext(),
                    outcome.label));
            holder.outcomeButton.setEnabled(enabled);
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (callback != null) {
                        callback.onOutcomeClicked(outcome);
                    }
                }
            });
        }
    }

    public interface Callback {
        void onOutcomeClicked(Outcome outcome);
    }
} public enum AccountManager {
    Instance;

    private static final String PREFS_FILE = "fiveCallsPrefs";

    // Do not change these key values. They reference SharedPreference values.
    private static final String KEY_INITIALIZED = "prefsKeyInitialized";
    public static final String KEY_ALLOW_ANALYTICS = "prefsKeyAllowAnalytics";
    private static final String KEY_USER_ADDRESS = "prefsKeyUserZip";
    private static final String KEY_LATITUDE = "prefsKeyLatitude";
    private static final String KEY_LONGITUDE = "prefsKeyLongitude";
    private static final String KEY_DATABASE_SAVES_CONTACTS = "prefsKeyDbSavesContacts";
    public static final String KEY_REMINDER_MINUTES = "prefsKeyNotificationMinutes";
    public static final String KEY_REMINDER_DAYS = "prefsKeyReminderDays";
    public static final String KEY_ALLOW_REMINDERS = "prefsKeyEnableReminders";
    private static final String KEY_REMINDERS_INFO_SHOWN = "prefsKeyRemindersInfoShown";
    public static final String KEY_NOTIFICATIONS = "prefsKeyNotifications";
    private static final String KEY_NOTIFICATION_DIALOG_SHOWN = "prefsKeyNotificationDialog";

    // Default to 11 am.
    public static final int DEFAULT_REMINDER_MINUTES = 60 * 11;

    // Default to Monday, Wednesday and Friday only.
    public static final Set<String> DEFAULT_REMINDER_DAYS =
            new HashSet<>(Arrays.asList("2", "4", "6"));

    // Default to no notifications.
    public static final String DEFAULT_NOTIFICATION_SELECTION = "2";

    public boolean hasLocation(Context context) {
        // If there's a lat/lng or an address.
        return (!TextUtils.isEmpty(getLat(context)) && !TextUtils.isEmpty(getLng(context)))
                || !TextUtils.isEmpty(getAddress(context));
    }

    // Defaults to true, we'eve already seen the tutorial.
    public boolean isTutorialSeen(Context context) {
        return getSharedPrefs(context).getBoolean(KEY_INITIALIZED, /* not seen yet */ false);
    }

    public void setTutorialSeen(Context context, boolean tutorialSeen) {
        getSharedPrefs(context).edit().putBoolean(KEY_INITIALIZED, tutorialSeen).apply();
    }

    public boolean isRemindersInfoShown(Context context) {
        return getSharedPrefs(context).getBoolean(KEY_REMINDERS_INFO_SHOWN,
                /* not shown yet */ false);
    }

    public void setRemindersInfoShown(Context context, boolean remindersInfoShown) {
        getSharedPrefs(context).edit().putBoolean(KEY_REMINDERS_INFO_SHOWN, remindersInfoShown)
                .apply();
    }

    public String getAddress(Context context) {
        return getSharedPrefs(context).getString(KEY_USER_ADDRESS, "");
    }

    public void setAddress(Context context, String address) {
        getSharedPrefs(context).edit().putString(KEY_USER_ADDRESS, address).apply();
    }

    public String getLat(Context context) {
        return getSharedPrefs(context).getString(KEY_LATITUDE, "");
    }

    public void setLat(Context context, String lat) {
        getSharedPrefs(context).edit().putString(KEY_LATITUDE, lat).apply();
    }

    @Nullable
    public String getLng(Context context) {
        return getSharedPrefs(context).getString(KEY_LONGITUDE, "");
    }

    public void setLng(Context context, String lng) {
        getSharedPrefs(context).edit().putString(KEY_LONGITUDE, lng).apply();
    }

    // Defaults to true
    public boolean allowAnalytics(Context context) {
        return getSharedPrefs(context).getBoolean(KEY_ALLOW_ANALYTICS, true);
    }

    public void setAllowAnalytics(Context context, boolean shouldAllow) {
        getSharedPrefs(context).edit().putBoolean(KEY_ALLOW_ANALYTICS, shouldAllow).apply();
    }

    public boolean getDatabaseSavesContacts(Context context) {
        return getSharedPrefs(context).getBoolean(KEY_DATABASE_SAVES_CONTACTS, true);
    }

    public void setDatabaseSavesContacts(Context context, boolean savesContacts) {
        getSharedPrefs(context).edit().putBoolean(KEY_DATABASE_SAVES_CONTACTS,
                savesContacts).apply();
    }

    public boolean getAllowReminders(Context context) {
        return getSharedPrefs(context).getBoolean(KEY_ALLOW_REMINDERS, true);
    }

    public void setAllowReminders(Context context, boolean allowReminders) {
        getSharedPrefs(context).edit().putBoolean(KEY_ALLOW_REMINDERS, allowReminders).apply();
    }

    public int getReminderMinutes(Context context) {
        return getSharedPrefs(context).getInt(KEY_REMINDER_MINUTES, DEFAULT_REMINDER_MINUTES);
    }

    public void setReminderMinutes(Context context, int reminderMinutes) {
        getSharedPrefs(context).edit().putInt(KEY_REMINDER_MINUTES, reminderMinutes).apply();
    }

    public Set<String> getReminderDays(Context context) {
        return getSharedPrefs(context).getStringSet(KEY_REMINDER_DAYS, DEFAULT_REMINDER_DAYS);
    }

    public void setReminderDays(Context context, Set<String> reminderDays) {
        getSharedPrefs(context).edit().putStringSet(KEY_REMINDER_DAYS, reminderDays).apply();
    }

    public void setNotificationPreference(Context context, String preference) {
        getSharedPrefs(context).edit().putString(KEY_NOTIFICATIONS, preference).apply();
    }

    public String getNotificationPreference(Context context) {
        return getSharedPrefs(context).getString(KEY_NOTIFICATIONS, DEFAULT_NOTIFICATION_SELECTION);
    }

    public boolean isNotificationDialogShown(Context context) {
        return getSharedPrefs(context).getBoolean(KEY_NOTIFICATION_DIALOG_SHOWN,
                /* not seen yet */ false);
    }

    public void setNotificationDialogShown(Context context, boolean shown) {
        getSharedPrefs(context).edit().putBoolean(KEY_NOTIFICATION_DIALOG_SHOWN, shown).apply();
    }

    private SharedPreferences getSharedPrefs(Context context) {
        return context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
    }
} public class Contact implements Parcelable {
    public String id;
    public String name;
    public String phone;
    public String photoURL;
    public String party;
    public String state;
    public String reason;
    public String area;
    public FieldOffice[] field_offices;

    protected Contact(Parcel in) {
        id = in.readString();
        name = in.readString();
        phone = in.readString();
        photoURL = in.readString();
        party = in.readString();
        state = in.readString();
        reason = in.readString();
        area = in.readString();
        field_offices = in.createTypedArray(FieldOffice.CREATOR);
    }

    public static final Creator<Contact> CREATOR = new Creator<Contact>() {
        @Override
        public Contact createFromParcel(Parcel in) {
            return new Contact(in);
        }

        @Override
        public Contact[] newArray(int size) {
            return new Contact[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(id);
        dest.writeString(name);
        dest.writeString(phone);
        dest.writeString(photoURL);
        dest.writeString(party);
        dest.writeString(state);
        dest.writeString(reason);
        dest.writeString(area);
        dest.writeTypedArray(field_offices, PARCELABLE_WRITE_RETURN_VALUE);
    }
} public class Issue implements Parcelable {
    public String id;
    public String name;
    public String reason;
    public String script;
    public boolean active;
    public String link;
    public String linkTitle;

    public List<Contact> contacts;
    public List<String> contactAreas;
    public List<Outcome> outcomeModels;
    public Category[] categories;
    public boolean isSplit;

    protected Issue(Parcel in) {
        id = in.readString();
        name = in.readString();
        reason = in.readString();
        script = in.readString();
        link = in.readString();
        linkTitle = in.readString();
        active = in.readInt() != 0;
        isSplit = in.readInt() != 0;
        contacts = in.createTypedArrayList(Contact.CREATOR);
        contactAreas = in.createStringArrayList();
        outcomeModels = in.createTypedArrayList(Outcome.CREATOR);
        categories = in.createTypedArray(Category.CREATOR);
    }

    public static final Creator<Issue> CREATOR = new Creator<Issue>() {
        @Override
        public Issue createFromParcel(Parcel in) {
            return new Issue(in);
        }

        @Override
        public Issue[] newArray(int size) {
            return new Issue[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(id);
        dest.writeString(name);
        dest.writeString(reason);
        dest.writeString(script);
        dest.writeString(link);
        dest.writeString(linkTitle);
        dest.writeInt(active ? 1 : 0);
        dest.writeInt(isSplit ? 1 : 0);
        dest.writeTypedList(contacts);
        dest.writeStringList(contactAreas);
        dest.writeTypedList(outcomeModels);
        dest.writeTypedArray(categories, PARCELABLE_WRITE_RETURN_VALUE);
    }
} public class Outcome implements Parcelable {

    public String label;
    public Status status;

    public Outcome(String label, Status status) {
        this.label = label;
        this.status = status;
    }

    public Outcome(Status status) {
        this(status.toString(), status);
    }

    protected Outcome(Parcel in) {
        label = in.readString();
        status = Status.fromString(in.readString());
    }

    public static final Creator<Outcome> CREATOR = new Creator<Outcome>() {
        @Override
        public Outcome createFromParcel(Parcel in) {
            return new Outcome(in);
        }

        @Override
        public Outcome[] newArray(int size) {
            return new Outcome[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(label);
        dest.writeString(status.toString());
    }

    public enum Status {
        UNAVAILABLE("unavailable"),
        VOICEMAIL("voicemail"),
        CONTACT("contact"),
        SKIP("skip"),
        EMPTY("empty"),
        VM("vm"), // deprecated
        CONTACTED("contacted"); // deprecated

        String status;

        Status(String status) {
            this.status = status;
        }

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

        public static Status fromString(String input) {
            for (Status status : values()) {
                if (status.toString().equals(input)) {
                    return status;
                }
            }

            return EMPTY;
        }
    }

    public static String getDisplayString(Context context, String label) {
        String result;

        int id = context.getResources().getIdentifier("outcome_" + label,
                "string", context.getPackageName());

        if (id > 0) {
            result = context.getResources().getString(id);
        } else {
            result = label;
        }

        return result;
    }

    public static List<Issue> filterSkipOutcomes(List<Issue> issues) {
        List<Issue> result = new ArrayList<>();

        if (issues != null) {
            for (Issue issue : issues) {
                if (issue.outcomeModels != null) {
                    List<Outcome> filteredOutcomes = new ArrayList<>();

                    for (Outcome outcome : issue.outcomeModels) {
                        if (!Outcome.Status.SKIP.equals(outcome.status)) {
                            filteredOutcomes.add(outcome);
                        }
                    }

                    issue.outcomeModels = filteredOutcomes;
                }

                result.add(issue);
            }
        }

        return result;
    }
} public class FiveCallsApi {
    private static final String TAG = "FiveCallsApi";

    // Set TESTING "true" to set a parameter to the count call request which marks it as a test
    // request on the server. This will only work on debug builds.
    private static final boolean TESTING = true;

    private static final String GET_ISSUES_REQUEST = "https://api.5calls.org/v1/issues"; //"https://5calls.org/issues/?address=";

    private static final String GET_CONTACTS_REQUEST = "https://api.5calls.org/v1/reps?location=";

    private static final String GET_REPORT = "https://api.5calls.org/v1/report";

    public interface CallRequestListener {
        void onRequestError();

        void onJsonError();

        void onCallCount(int count);

        void onCallReported();
    }

    public interface IssuesRequestListener {
        void onRequestError();

        void onJsonError();

        void onIssuesReceived(List<Issue> issues);
    }

    public interface ContactsRequestListener {
        void onRequestError();

        void onJsonError();

        void onAddressError();

        void onContactsReceived(String locationName, List<Contact> contacts);
    }

    private RequestQueue mRequestQueue;
    private Gson mGson;
    private List<CallRequestListener> mCallRequestListeners = new ArrayList<>();
    private List<IssuesRequestListener> mIssuesRequestListeners = new ArrayList<>();
    private List<ContactsRequestListener> mContactsRequestListeners = new ArrayList<>();

    public FiveCallsApi(Context context) {
        // TODO: Using OkHttpClient and OkHttpStack cause failures on multiple types of Samsung
        // Galaxy devices.
        //mRequestQueue = Volley.newRequestQueue(context, new OkHttpStack(new OkHttpClient()));
        mRequestQueue = Volley.newRequestQueue(context);
        mGson = new GsonBuilder()
                .serializeNulls()
                .registerTypeAdapter(Outcome.Status.class, new OutcomeStatusTypeAdapter())
                .create();
    }

    public void registerCallRequestListener(CallRequestListener callRequestListener) {
        mCallRequestListeners.add(callRequestListener);
    }

    public void unregisterCallRequestListener(CallRequestListener callRequestListener) {
        if (mCallRequestListeners.contains(callRequestListener)) {
            mCallRequestListeners.remove(callRequestListener);
        }
    }

    public void registerIssuesRequestListener(IssuesRequestListener issuesRequestListener) {
        mIssuesRequestListeners.add(issuesRequestListener);
    }

    public void unregisterIssuesRequestListener(IssuesRequestListener issuesRequestListener) {
        if (mIssuesRequestListeners.contains(issuesRequestListener)) {
            mIssuesRequestListeners.remove(issuesRequestListener);
        }
    }

    public void registerContactsRequestListener(ContactsRequestListener contactsRequestListener) {
        mContactsRequestListeners.add(contactsRequestListener);
    }

    public void unregisterContactsRequestListener(ContactsRequestListener contactsRequestListener) {
        if (mContactsRequestListeners.contains(contactsRequestListener)) {
            mContactsRequestListeners.remove(contactsRequestListener);
        }
    }

    public void onDestroy() {
        mRequestQueue.cancelAll(TAG);
        mRequestQueue.stop();
        mRequestQueue = null;
    }

    public void getIssues() {
        buildIssuesRequest(GET_ISSUES_REQUEST, mIssuesRequestListeners);
    }

    public void getContacts(String address) {
        buildContactsRequest(GET_CONTACTS_REQUEST + URLEncoder.encode(address), mContactsRequestListeners);
    }

    private void buildIssuesRequest(String url, final List<IssuesRequestListener> listeners) {
        // Request a JSON Object response from the provided URL.
        JsonArrayRequest issuesRequest = new JsonArrayRequest(
                Request.Method.GET, url, null, new Response.Listener<JSONArray>() {
            @Override
            public void onResponse(JSONArray response) {
                if (response == null) {
                    for (IssuesRequestListener listener : listeners) {
                        listener.onJsonError();
                    }
                    return;
                }
                Type listType = new TypeToken<ArrayList<Issue>>(){}.getType();
                List<Issue> issues = mGson.fromJson(response.toString(),
                        listType);

                issues = Outcome.filterSkipOutcomes(issues);

                // TODO: Sanitize contact IDs here
                for (IssuesRequestListener listener : listeners) {
                    listener.onIssuesReceived(issues);
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                for (IssuesRequestListener listener : listeners) {
                    listener.onRequestError();
                }
            }
        });
        issuesRequest.setTag(TAG);
        // Add the request to the RequestQueue.
        mRequestQueue.add(issuesRequest);
    }

    private void buildContactsRequest(String url, final List<ContactsRequestListener> listeners) {
            // Request a JSON Object response from the provided URL.
            JsonObjectRequest contactsRequest = new JsonObjectRequest(
                    Request.Method.GET, url, null, new Response.Listener<JSONObject>() {
                @Override
                public void onResponse(JSONObject response) {
                    if (response != null) {
                        String locationName = "";
                        try {
                            locationName = response.getString("location");
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                        JSONArray jsonArray = response.optJSONArray("representatives");
                        if (jsonArray == null) {
                            for (ContactsRequestListener listener : listeners) {
                                listener.onJsonError();
                            }
                            return;
                        }
                        Type listType = new TypeToken<ArrayList<Contact>>(){}.getType();
                        List<Contact> contacts = mGson.fromJson(jsonArray.toString(), listType);
                        for (ContactsRequestListener listener : listeners) {
                            listener.onContactsReceived(locationName, contacts);
                        }
                    }
                }
            }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    for (ContactsRequestListener listener : listeners) {
                        listener.onRequestError();
                    }
                }
            });
            contactsRequest.setTag(TAG);
            // Add the request to the RequestQueue.
            mRequestQueue.add(contactsRequest);
    }

    public void getCallCount() {
        String getReport = GET_REPORT;
        JsonObjectRequest reportRequest = new JsonObjectRequest(
                Request.Method.GET, getReport, null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                try {
                    int count = response.getInt("count");
                    for (CallRequestListener listener : mCallRequestListeners) {
                        listener.onCallCount(count);
                    }
                } catch (JSONException e) {
                    for (CallRequestListener listener : mCallRequestListeners) {
                        listener.onJsonError();
                    }
                    e.printStackTrace();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                onRequestError(error);
            }
        });
        reportRequest.setTag(TAG); // TODO: same tag OK?
        // Add the request to the RequestQueue.
        mRequestQueue.add(reportRequest);
    }

    // Result is "VOICEMAIL", "unavailable", or "contacted"
    // https://github.com/5calls/5calls/blob/master/static/js/main.js#L221
    public void reportCall(final String issueId, final String contactId, final String result,
                           final String zip) {
        String getReport = GET_REPORT;
        StringRequest request = new StringRequest(Request.Method.POST, getReport,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        for (CallRequestListener listener : mCallRequestListeners) {
                            listener.onCallReported();
                        }
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                onRequestError(error);
            }
        }) {
            @Override
            protected Map<String, String> getParams() {
                Map<String, String> params = new HashMap<String, String>();
                params.put("issueid", issueId);
                params.put("result", result);
                params.put("contactid", contactId);
                params.put("location", zip);
                params.put("via", (BuildConfig.DEBUG && TESTING) ? "test" : "android");
                return params;
            }

            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                Map<String, String> params = new HashMap<>();
                params.put("Content-Type", "application/x-www-form-urlencoded");
                return params;
            }
        };
        request.setTag(TAG);
        // Add the request to the RequestQueue.
        mRequestQueue.add(request);
    }

    private void onRequestError(VolleyError error) {
        for (CallRequestListener listener : mCallRequestListeners) {
            listener.onRequestError();
        }
        if (error.getMessage() == null) {
            Log.d("Error", "no message");
        } else {
            Log.d("Error", error.getMessage());
        }
    }
} public class MarkdownUtil {
    /**
     * Temporary work-around for Markwon.setMarkdown(view, script); while LinkMovementMethod
     * crashes during selection.
     * See https://github.com/noties/Markwon/issues/41
     * and https://github.com/noties/Markwon/tree/v1.0.6#quick-start.
     */
    public static void setUpScript(TextView view, String script, Context context) {
        // create a Parser instance (can be done manually)
        // internally creates default Parser instance & registers `strike-through` & `tables` extension
        final Parser parser = Markwon.createParser();

        // core class to display markdown, can be obtained via this method,
        // which creates default instance (no images handling though),
        // or via `builder` method, which lets you to configure this instance
        //
        // `this` refers to a Context instance
        final SpannableConfiguration configuration = SpannableConfiguration.create(context);

        // it's better **not** to re-use this class between multiple calls
        final SpannableRenderer renderer = new SpannableRenderer();

        final Node node = parser.parse(script);
        final CharSequence text = renderer.render(configuration, node);

        // for links in markdown to be clickable
        view.setMovementMethod(BetterLinkMovementMethod.getInstance());

        // we need these due to the limited nature of Spannables to invalidate TextView
        Markwon.unscheduleDrawables(view);
        Markwon.unscheduleTableRows(view);

        view.setText(text);

        Markwon.scheduleDrawables(view);
        Markwon.scheduleTableRows(view);
    }
} public class GridItemDecoration extends RecyclerView.ItemDecoration {

    private int gridSpacingPx;
    private int gridSize;

    public GridItemDecoration(int gridSpacingPx, int gridSize) {
        this.gridSpacingPx = gridSpacingPx;
        this.gridSize = gridSize;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int itemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition();

        if (itemPosition < gridSize) {
            outRect.top = 0;
        } else {
            outRect.top = gridSpacingPx / 2;
        }

        if (itemPosition % gridSize == 0) {
            outRect.left = 0;
            outRect.right = gridSpacingPx / 2;
        } else if ((itemPosition + 1) % gridSize == 0) {
            outRect.right = 0;
            outRect.left = gridSpacingPx / 2;
        } else {
            outRect.left = gridSpacingPx / 2;
            outRect.right = gridSpacingPx / 2;
        }

        outRect.bottom = gridSpacingPx / 2;
    }
} public static final String KEY_ISSUE = "key_issue";
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.NavUtils;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.support.v4.widget.NestedScrollView;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;
import org.a5calls.android.a5calls.AppSingleton;
import org.a5calls.android.a5calls.FiveCallsApplication;
import org.a5calls.android.a5calls.R;
import org.a5calls.android.a5calls.adapter.OutcomeAdapter;
import org.a5calls.android.a5calls.model.AccountManager;
import org.a5calls.android.a5calls.model.Contact;
import org.a5calls.android.a5calls.model.Issue;
import org.a5calls.android.a5calls.model.Outcome;
import org.a5calls.android.a5calls.net.FiveCallsApi;
import org.a5calls.android.a5calls.util.MarkdownUtil;
import org.a5calls.android.a5calls.view.GridItemDecoration;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import me.saket.bettermovementmethod.BetterLinkMovementMethod;
import ru.noties.markwon.Markwon;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.renderer.SpannableRenderer;
import static org.a5calls.android.a5calls.controller.IssueActivity.KEY_ISSUE;

        mStatusListener = new FiveCallsApi.CallRequestListener() {
            @Override
            public void onRequestError() {
                outcomeAdapter.setEnabled(true);
                showError(R.string.request_error_db_recorded_anyway);
            }

            @Override
            public void onJsonError() {
                outcomeAdapter.setEnabled(true);
                showError(R.string.json_error_db_recorded_anyway);
            }

            @Override
            public void onCallCount(int count) {
                // unused
            }

            @Override
            public void onCallReported() {
                // Note: Skips are not reported.
                Log.d(TAG, "call reported successfully!");
                returnToIssue();
            }
        };
        FiveCallsApi controller = AppSingleton.getInstance(getApplicationContext())
                .getJsonController();
        controller.registerCallRequestListener(mStatusListener);

        // The markdown view gets focus unless we let the scrollview take it back.
        scrollView.setFocusableInTouchMode(true);
        scrollView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);

        MarkdownUtil.setUpScript(callScript, mIssue.script, getApplicationContext());

        boolean expandLocalOffices = false;
        if (savedInstanceState != null) {
            expandLocalOffices = savedInstanceState.getBoolean(KEY_LOCAL_OFFICES_EXPANDED,
                    false);
        }
        setupContactUi(mActiveContactIndex, expandLocalOffices);

        // If the Issue's Outcome list is somehow empty, use default outcomes
        // See: https://github.com/5calls/android/issues/107
        List<Outcome> issueOutcomes;
        if (mIssue.outcomeModels == null || mIssue.outcomeModels.isEmpty()) {
            issueOutcomes = OutcomeAdapter.DEFAULT_OUTCOMES;
        } else {
            issueOutcomes = mIssue.outcomeModels;
        }

        outcomeAdapter = new OutcomeAdapter(issueOutcomes, new OutcomeAdapter.Callback() {
            @Override
            public void onOutcomeClicked(Outcome outcome) {
                reportEvent(outcome.label);
                reportCall(outcome, address);
            }
        });

        outcomeList.setLayoutManager(
                new GridLayoutManager(this, getSpanCount(RepCallActivity.this)));
        outcomeList.setAdapter(outcomeAdapter);

        int gridPadding = (int) getResources().getDimension(R.dimen.grid_padding);
        outcomeList.addItemDecoration(new GridItemDecoration(gridPadding,
                getSpanCount(RepCallActivity.this)));

        // We allow Analytics opt-out.
        if (accountManager.allowAnalytics(this)) {
            // Obtain the shared Tracker instance.
            FiveCallsApplication application = (FiveCallsApplication) getApplication();
            mTracker = application.getDefaultTracker();
        }
    }

    @Override
    protected void onDestroy() {
        AppSingleton.getInstance(getApplicationContext()).getJsonController()
                .unregisterCallRequestListener(mStatusListener);
        super.onDestroy();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(KEY_ISSUE, mIssue);
        outState.putBoolean(KEY_LOCAL_OFFICES_EXPANDED,
                localOfficeSection.getVisibility() == View.VISIBLE);
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mTracker != null) {
            mTracker.setScreenName(TAG);
            mTracker.send(new HitBuilders.ScreenViewBuilder().build());
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                returnToIssue();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void reportCall(Outcome outcome, String address) {
        outcomeAdapter.setEnabled(false);
        AppSingleton.getInstance(getApplicationContext()).getDatabaseHelper().addCall(mIssue.id,
                mIssue.name, mIssue.contacts.get(mActiveContactIndex).id,
                mIssue.contacts.get(mActiveContactIndex).name, outcome.status.toString(), address);
        AppSingleton.getInstance(getApplicationContext()).getJsonController().reportCall(
                mIssue.id, mIssue.contacts.get(mActiveContactIndex).id, outcome.label, address);
    }

    private void setupContactUi(int index, boolean expandLocalSection) {
        final Contact contact = mIssue.contacts.get(index);