public class Logger {

    private static final String TAG = "Surveyor";

    public static void e(String message, Throwable t) {
        Log.e(TAG, message, t);
    }

    public static void w(String message) {
        Log.w(TAG, message);
    }

    public static void d(String message) {
        Log.d(TAG, message);
    }
} public interface SurveyorIntent {
    String EXTRA_ORG_UUID = "surveyor.extra.org_uuid";
    String EXTRA_FLOW_UUID = "surveyor.extra.flow_uuid";

    String EXTRA_SUBMISSION_FILE = "surveyor.extra.submission_file";

    // where media files are to be stored
    String EXTRA_MEDIA_FILE = "surveyor.extra.media_file";

    String EXTRA_ERROR = "surveyor.extra.error";
    String EXTRA_CAMERA_DIRECTION = "surveyor.extra.camera_direction";
} public class Flow {
    private String uuid;

    private String name;

    @SerializedName("spec_version")
    private String specVersion;

    private int revision;

    @SerializedName("question_count")
    private int questionCount;

    /**
     * Creates a new flow
     *
     * @param uuid          the flow UUID
     * @param name          the flow name
     * @param specVersion   the flow spec version number
     * @param revision      the flow revision number
     * @param questionCount the number of questions
     */
    public Flow(String uuid, String name, String specVersion, int revision, int questionCount) {
        this.uuid = uuid;
        this.name = name;
        this.specVersion = specVersion;
        this.revision = revision;
        this.questionCount = questionCount;
    }

    /**
     * Extracts a flow summary from the given flow definition
     *
     * @param flow the flow definition
     * @return the flow summary
     */
    public static Flow extract(RawJson flow) {
        JsonObject definition = JsonUtils.unmarshal(flow.toString(), JsonObject.class);

        // flow is in 13+ format
        if (definition.get("uuid") != null) {
            String uuid = definition.get("uuid").getAsString();
            String name = definition.get("name").getAsString();
            String specVersion = definition.get("spec_version").getAsString();
            int revision = definition.get("revision").getAsInt();
            int questionCount = 0;

            JsonArray nodes = definition.get("nodes").getAsJsonArray();
            for (JsonElement elem : nodes) {
                JsonObject node = elem.getAsJsonObject();
                JsonElement routerElem = node.get("router");
                if (routerElem != null) {
                    JsonObject router = routerElem.getAsJsonObject();
                    if (router.get("wait") != null) {
                        questionCount++;
                    }
                }
            }

            return new Flow(uuid, name, specVersion, revision, questionCount);
        }

        // flow is in 11.x format
        JsonObject metadata = definition.get("metadata").getAsJsonObject();
        String uuid = metadata.get("uuid").getAsString();
        String name = metadata.get("name").getAsString();
        String specVersion = definition.get("version").getAsString();
        int revision = metadata.get("revision").getAsInt();
        int questionCount = 0;

        JsonArray ruleSets = definition.get("rule_sets").getAsJsonArray();
        for (JsonElement elem : ruleSets) {
            JsonObject ruleSet = elem.getAsJsonObject();
            String rulesetType = ruleSet.get("ruleset_type").getAsString();
            if (rulesetType.startsWith("wait_")) {
                questionCount++;
            }
        }

        return new Flow(uuid, name, specVersion, revision, questionCount);
    }

    public String getUuid() {
        return uuid;
    }

    public String getName() {
        return name;
    }

    public String getSpecVersion() {
        return specVersion;
    }

    public int getRevision() {
        return revision;
    }

    public int getQuestionCount() {
        return questionCount;
    }

    public FlowReference toReference() {
        return new FlowReference(uuid, name);
    }
} public class Org {
    /**
     * Contains the JSON representation of this org
     */
    private static final String DETAILS_FILE = "details.json";

    /**
     * Contains a goflow assets file with this org's flows, groups, fields etc
     */
    private static final String ASSETS_FILE = "assets.json";

    /**
     * Contains summaries of each flow available in this org
     */
    private static final String FLOWS_FILE = "flows.json";

    private String token;

    private String name;

    @SerializedName("primary_language")
    private String primaryLanguage;

    private String[] languages;

    private String timezone;

    private String country;

    @SerializedName("date_style")
    private String dateStyle;

    private boolean anon;

    private String legacySubmissionsDirectory;

    private transient File directory;

    private transient List<Flow> flows;

    /**
     * Creates an new empty org
     *
     * @param directory the directory
     * @param token     the API token
     * @return the org
     */
    public static Org create(File directory, String name, String token) throws IOException {
        directory.mkdirs();

        Org org = new Org();
        org.name = name;
        org.token = token;
        org.directory = directory;
        org.flows = new ArrayList<>();
        org.legacySubmissionsDirectory = null;

        FileUtils.writeStringToFile(new File(directory, DETAILS_FILE), "{\"name\":\"" + name + "\",\"token\":\"" + token + "\"}");
        FileUtils.writeStringToFile(new File(directory, FLOWS_FILE), "[]");
        return org;
    }

    /**
     * Loads an org from a directory
     *
     * @param directory the directory
     * @return the org
     */
    static Org load(File directory) throws IOException {
        if (!directory.exists() || !directory.isDirectory()) {
            throw new RuntimeException(directory.getPath() + " is not a valid org directory");
        }

        // read details.json
        String detailsJSON = FileUtils.readFileToString(new File(directory, DETAILS_FILE));
        Org org = JsonUtils.unmarshal(detailsJSON, Org.class);
        org.directory = directory;

        // read flows.json
        String flowsJson = FileUtils.readFileToString(new File(directory, FLOWS_FILE));

        TypeToken type = new TypeToken<List<Flow>>() {
        };
        org.flows = JsonUtils.unmarshal(flowsJson, type);
        return org;
    }

    /**
     * Gets the UUID of this org (i.e. the name of its directory)
     *
     * @return the UUID
     */
    public String getUuid() {
        return directory.getName();
    }

    /**
     * Gets the directory of this org
     *
     * @return the directory
     */
    public File getDirectory() {
        return directory;
    }

    /**
     * Gets the API token for this org
     *
     * @return the API token
     */
    public String getToken() {
        return token;
    }

    /**
     * Gets the name of this org
     *
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * Gets the country code of this org
     *
     * @return the country code
     */
    public String getCountry() {
        return country;
    }

    public String[] getLanguages() {
        return languages;
    }

    public String getTimezone() {
        return timezone;
    }

    public String getDateStyle() {
        return dateStyle;
    }

    public boolean isAnon() {
        return anon;
    }

    /**
     * Gets the directory of legacy submissions for this org (may be null)
     *
     * @return the directory
     */
    public String getLegacySubmissionsDirectory() {
        return legacySubmissionsDirectory;
    }

    public void setLegacySubmissionsDirectory(String legacySubmissionsDirectory) {
        this.legacySubmissionsDirectory = legacySubmissionsDirectory;
    }

    public List<Flow> getFlows() {
        return flows;
    }

    /**
     * Gets the flow with the given UUID
     *
     * @param uuid the flow UUID
     * @return the flow or null if no such flow exists
     */
    public Flow getFlow(String uuid) {
        for (Flow flow : flows) {
            if (flow.getUuid().equals(uuid)) {
                return flow;
            }
        }
        return null;
    }

    /**
     * Gets whether this org has downloaded assets
     *
     * @return true if org has assets
     */
    public boolean hasAssets() {
        return new File(directory, ASSETS_FILE).exists();
    }

    /**
     * Gets this org's downloaded assets
     *
     * @return the assets JSON
     */
    public String getAssets() throws IOException {
        return FileUtils.readFileToString(new File(directory, ASSETS_FILE));
    }

    /**
     * Refreshes this org from RapidPro
     */
    public void refresh(boolean includeAssets, RefreshProgress progress) throws TembaException, IOException {
        TembaService svc = SurveyorApplication.get().getTembaService();
        io.rapidpro.surveyor.net.responses.Org apiOrg = svc.getOrg(this.token);

        this.name = apiOrg.getName();
        this.languages = apiOrg.getLanguages();
        this.timezone = apiOrg.getTimezone();
        this.country = apiOrg.getCountry();
        this.dateStyle = apiOrg.getDateStyle();
        this.anon = apiOrg.isAnon();
        this.save();

        if (progress != null) {
            progress.reportProgress(10);
        }

        if (includeAssets) {
            refreshAssets(progress);
        }
    }

    public void save() throws IOException {
        // (re)write org fields to details.json
        String detailsJSON = JsonUtils.marshal(this);
        FileUtils.writeStringToFile(new File(directory, DETAILS_FILE), detailsJSON);
    }

    private void refreshAssets(RefreshProgress progress) throws TembaException, IOException {
        List<Field> fields = SurveyorApplication.get().getTembaService().getFields(getToken());

        progress.reportProgress(20);

        List<Group> groups = SurveyorApplication.get().getTembaService().getGroups(getToken());

        progress.reportProgress(30);

        List<io.rapidpro.surveyor.net.responses.Flow> flows = SurveyorApplication.get().getTembaService().getFlows(getToken());

        progress.reportProgress(40);

        List<RawJson> definitions = SurveyorApplication.get().getTembaService().getDefinitions(getToken(), flows);

        progress.reportProgress(60);

        List<Boundary> boundaries = SurveyorApplication.get().getTembaService().getBoundaries(getToken());

        progress.reportProgress(70);

        OrgAssets assets = OrgAssets.fromTemba(fields, groups, boundaries, definitions);
        String assetsJSON = JsonUtils.marshal(assets);

        FileUtils.writeStringToFile(new File(directory, ASSETS_FILE), assetsJSON);

        progress.reportProgress(80);

        // update the flow summaries
        this.flows.clear();
        this.flows.addAll(assets.getFlows());

        // and write that to flows.json as well
        String summariesJSON = JsonUtils.marshal(this.flows);
        FileUtils.writeStringToFile(new File(directory, FLOWS_FILE), summariesJSON);

        progress.reportProgress(100);

        Logger.d("Refreshed assets for org " + getUuid() + " (flows=" + flows.size() + ", fields=" + fields.size() + ", groups=" + groups.size() + ")");
    }

    public interface RefreshProgress {
        void reportProgress(int percent);
    }
} public class Submission {

    private static final String SESSION_FILE = "session.json";
    private static final String MODIFIERS_FILE = "modifiers.jsonl";
    private static final String EVENTS_FILE = "events.jsonl";
    private static final String COMPLETION_FILE = ".completed";
    private static final String MEDIA_DIR = "media";

    private Org org;
    private File directory;

    /**
     * Creates a new submission for the given org in the given directory
     *
     * @param org       the org
     * @param directory the directory
     */
    public Submission(Org org, File directory) {
        this.org = org;
        this.directory = directory;
    }

    /**
     * Gets the UUID of this org (i.e. the name of its directory)
     *
     * @return the UUID
     */
    public String getUuid() {
        return directory.getName();
    }

    /**
     * Gets the org this submission belongs to
     *
     * @return the org
     */
    public Org getOrg() {
        return org;
    }

    /**
     * Get's the directory this submission is stored in
     *
     * @return the directory
     */
    public File getDirectory() {
        return directory;
    }

    /**
     * Get's the directory this submission's media is stored in
     *
     * @return the directory
     */
    public File getMediaDirectory() throws IOException {
        return SurveyUtils.mkdir(directory, MEDIA_DIR);
    }

    /**
     * Gets whether this submission is complete
     *
     * @return true if complete
     */
    public boolean isCompleted() {
        return new File(directory, COMPLETION_FILE).exists();
    }

    /**
     * Saves the current session
     *
     * @param session the current session
     */
    public void saveSession(Session session) throws IOException, EngineException {
        FileUtils.writeStringToFile(new File(directory, SESSION_FILE), session.toJSON());
    }

    /**
     * Saves new modifiers to this submission
     *
     * @param modifiers the modifiers to save
     */
    public void saveNewModifiers(List<Modifier> modifiers) throws IOException {
        File file = new File(directory, MODIFIERS_FILE);

        BufferedWriter writer = new BufferedWriter(new FileWriter(file, true));

        for (Modifier mod : modifiers) {
            writer.write(mod.payload());
            writer.newLine();
        }

        writer.close();
    }

    /**
     * Saves new events to this submission
     *
     * @param events the events to save
     */
    public void saveNewEvents(List<Event> events) throws IOException {
        File file = new File(directory, EVENTS_FILE);

        BufferedWriter writer = new BufferedWriter(new FileWriter(file, true));

        for (Event event : events) {
            writer.write(event.payload());
            writer.newLine();
        }

        writer.close();
    }

    /**
     * Saves a new media file to this submission
     *
     * @param data      the media data
     * @param extension the file extension
     * @return the URI of the saved file
     */
    public Uri saveMedia(byte[] data, String extension) throws IOException {
        File file = new File(getMediaDirectory(), UUID.randomUUID().toString() + "." + extension);
        FileUtils.writeByteArrayToFile(file, data);
        return SurveyorApplication.get().getUriForFile(file);
    }

    /**
     * Saves a new media file to this submission
     *
     * @param src the file to copy
     * @return the URI of the saved file
     */
    public Uri saveMedia(File src) throws IOException {
        String extension = FilenameUtils.getExtension(src.getName());
        File file = new File(getMediaDirectory(), UUID.randomUUID().toString() + "." + extension);
        FileUtils.copyFile(src, file);
        return SurveyorApplication.get().getUriForFile(file);
    }

    /**
     * Marks this submission as completed
     */
    public void complete() throws IOException {
        FileUtils.writeStringToFile(new File(directory, COMPLETION_FILE), "");
    }

    /**
     * Deletes this submission from the file system
     */
    public void delete() {
        try {
            FileUtils.deleteDirectory(directory);
            directory = null;
        } catch (IOException e) {
            Logger.e("Unable to delete submission " + directory.getAbsolutePath(), e);
        }
    }

    public void submit() throws IOException, TembaException {
        Logger.d("Submitting submission " + getUuid() + "...");

        String session = FileUtils.readFileToString(new File(directory, SESSION_FILE));
        List<String> modifiers = FileUtils.readLines(new File(directory, MODIFIERS_FILE));
        List<String> events = FileUtils.readLines(new File(directory, EVENTS_FILE));

        // upload all media and get a new remote URL for each item
        Map<Uri, String> mediaUrls = uploadMedia();

        // convert the map to parallel arrays of strings for replacement
        String[] oldUris = new String[mediaUrls.size()];
        String[] newUrls = new String[mediaUrls.size()];
        int e = 0;
        for (Map.Entry<Uri, String> entry : mediaUrls.entrySet()) {
            oldUris[e] = entry.getKey().toString();
            newUrls[e] = entry.getValue();
            e++;
        }

        for (int i = 0; i < oldUris.length; i++) {
            Logger.d(oldUris[i] + " --> " + newUrls[i]);
        }

        RawJson sessionJson = new RawJson(StringUtils.replaceEach(session, oldUris, newUrls));
        List<RawJson> modifiersJson = new ArrayList<>(modifiers.size());
        for (String modifier : modifiers) {
            modifiersJson.add(new RawJson(modifier));
        }
        List<RawJson> eventsJson = new ArrayList<>(events.size());
        for (String event : events) {
            eventsJson.add(new RawJson(StringUtils.replaceEach(event, oldUris, newUrls)));
        }

        SubmissionPayload payload = new SubmissionPayload(sessionJson, modifiersJson, eventsJson);

        SurveyorApplication.get().getTembaService().submit(org.getToken(), payload);

        delete();
    }

    /**
     * Upload all media files for this submission and return a map of their new URLs
     *
     * @return the map of local URIs to remote URLs
     */
    private Map<Uri, String> uploadMedia() throws IOException, TembaException {
        if (!hasMedia()) {
            return Collections.emptyMap();
        }

        SurveyorApplication app = SurveyorApplication.get();
        Map<Uri, String> uploads = new HashMap<>();

        for (File mediaFile : getMediaDirectory().listFiles()) {
            Uri mediaUri = app.getUriForFile(mediaFile);
            String newUrl = app.getTembaService().uploadMedia(org.getToken(), mediaUri);

            uploads.put(mediaUri, newUrl);

            Logger.d("Uploaded media " + mediaUri + " to " + newUrl);
        }
        return uploads;
    }

    private boolean hasMedia() {
        return new File(directory, MEDIA_DIR).exists();
    }
} public class Contact {
    com.nyaruka.goflow.mobile.Contact target;

    private Contact(com.nyaruka.goflow.mobile.Contact target) {
        this.target = target;
    }

    public static Contact createEmpty(SessionAssets sa) {
        return new Contact(Mobile.newEmptyContact(sa));
    }
} public class Engine {
    /**
     * The engine singleton
     */
    private static Engine engine = new Engine();

    private com.nyaruka.goflow.mobile.Engine target;

    public static Engine getInstance() {
        return engine;
    }

    /**
     * Creates a new engine
     */
    private Engine() {
        this.target = new com.nyaruka.goflow.mobile.Engine();
    }

    /**
     * Gets the current spec version
     *
     * @return the spec version
     */
    public static Semver currentSpecVersion() {
        return new Semver(Mobile.currentSpecVersion(), Semver.SemverType.LOOSE);
    }

    /**
     * Gets whether the given flow spec version is supported by the flow engine
     *
     * @param ver the spec version
     * @return true if supported
     */
    public static boolean isSpecVersionSupported(String ver) {
        // for a while Surveyor was creating v12 flows which don't exist anywhere else
        if (ver.startsWith("12.")) {
            return false;
        }

        return Mobile.isVersionSupported(ver);
    }

    /**
     * Creates an engine environment from the given org
     *
     * @param org the org
     * @return the environment
     */
    public static Environment createEnvironment(Org org) {
        String dateFormat = org.getDateStyle().equals("day_first") ? "DD-MM-YYYY" : "MM-DD-YYYY";
        String timeformat = "tt:mm";
        StringSlice languages = listToSlice(Arrays.asList(org.getLanguages()));
        String redactionPolicy = org.isAnon() ? "urns" : "none";
        return new Environment(dateFormat, timeformat, org.getTimezone(), languages, org.getCountry(), redactionPolicy);
    }

    /**
     * Loads an assets source from the given JSON
     *
     * @param json the assets JSON
     * @return the source
     */
    public static AssetsSource loadAssets(String json) throws EngineException {
        try {
            return new AssetsSource(json);
        } catch (Exception e) {
            throw new EngineException(e);
        }
    }

    /**
     * Creates a new session assets instance from an assets source
     *
     * @param source the source
     * @return the session assets
     */
    public static SessionAssets createSessionAssets(Environment env, AssetsSource source) throws EngineException {
        try {
            return new SessionAssets(env, source);
        } catch (Exception e) {
            throw new EngineException(e);
        }
    }

    /**
     * Creates a new incoming message
     *
     * @param text the message text
     * @return the message
     */
    public static MsgIn createMsgIn(String text) {
        return new MsgIn(UUID.randomUUID().toString(), text, null);
    }

    /**
     * Creates a new incoming message with an attachment
     *
     * @param text       the message text
     * @param attachment the message attachment
     * @return the message
     */
    public static MsgIn createMsgIn(String text, String attachment) {
        return new MsgIn(UUID.randomUUID().toString(), text, listToSlice(Collections.singletonList(attachment)));
    }

    /**
     * Creates a new manual trigger
     *
     * @param env     the environment
     * @param contact the contact
     * @param flow    the flow reference
     * @return the trigger
     */
    public static Trigger createManualTrigger(Environment env, Contact contact, FlowReference flow) {
        return Mobile.newManualTrigger(env, contact != null ? contact.target : null, flow);
    }

    /**
     * Creates a new message resume
     *
     * @param env     the environment
     * @param contact the contact
     * @param msg     the message
     * @return the resume
     */
    public static Resume createMsgResume(Environment env, Contact contact, MsgIn msg) {
        return Mobile.newMsgResume(env, contact != null ? contact.target : null, msg);
    }

    static StringSlice listToSlice(List<String> items) {
        if (items == null) {
            return null;
        }
        StringSlice slice = new StringSlice(items.size());
        for (String item : items) {
            slice.add(item);
        }
        return slice;
    }

    /**
     * Creates and starts a new session
     *
     * @param assets the session assets
     */
    public Pair<Session, Sprint> newSession(SessionAssets assets, Trigger trigger) throws EngineException {
        try {
            SessionAndSprint ss = this.target.newSession(assets, trigger);
            return new ImmutablePair<>(
                    new Session(ss.session()),
                    Sprint.fromNative(ss.sprint())
            );
        } catch (Exception e) {
            throw new EngineException(e);
        }
    }

    /**
     * Reads an existing session from JSON
     *
     * @param assets the session assets
     * @param json   the JSON
     */
    public Session readSession(SessionAssets assets, String json) throws EngineException {
        try {
            return new Session(this.target.readSession(assets, json));
        } catch (Exception e) {
            throw new EngineException(e);
        }
    }
} public class EngineException extends SurveyorException {
    public EngineException(Exception e) {
        super(e.getMessage(), e);
    }
} public class Session {
    private com.nyaruka.goflow.mobile.Session target;

    /**
     * Creates a new session
     */
    Session(com.nyaruka.goflow.mobile.Session target) {
        this.target = target;
    }

    /**
     * Resumes this session with a resume
     *
     * @param resume the resume
     * @return the events
     */
    public Sprint resume(Resume resume) throws EngineException {
        try {
            return Sprint.fromNative(target.resume(resume));
        } catch (Exception e) {
            throw new EngineException(e);
        }
    }

    /**
     * Gets the assets used by this session
     *
     * @return the assets
     */
    public SessionAssets getAssets() {
        return target.assets();
    }

    /**
     * Gets the status of this session
     *
     * @return the status
     */
    public String getStatus() {
        return target.status();
    }

    /**
     * Gets whether this session is waiting for input
     *
     * @return true if session is waiting
     */
    public boolean isWaiting() {
        return getStatus().equals("waiting");
    }

    public Wait getWait() {
        return target.getWait();
    }

    /**
     * Marshals this session to JSON
     *
     * @return the JSON
     */
    public String toJSON() throws EngineException {
        try {
            return target.toJSON();
        } catch (Exception e) {
            throw new EngineException(e);
        }
    }
} public class Sprint {
    private List<Modifier> modifiers;

    private List<Event> events;

    private Sprint(List<Modifier> modifiers, List<Event> events) {
        this.modifiers = modifiers;
        this.events = events;
    }

    static Sprint fromNative(com.nyaruka.goflow.mobile.Sprint s) {
        return new Sprint(sliceToList(s.modifiers()), sliceToList(s.events()));
    }

    public List<Modifier> getModifiers() {
        return modifiers;
    }

    public List<Event> getEvents() {
        return events;
    }

    private static List<Event> sliceToList(EventSlice slice) {
        List<Event> list = new ArrayList<>((int) slice.length());
        for (int e = 0; e < slice.length(); e++) {
            list.add(slice.get(e));
        }
        return list;
    }

    private static List<Modifier> sliceToList(ModifierSlice slice) {
        List<Modifier> list = new ArrayList<>((int) slice.length());
        for (int e = 0; e < slice.length(); e++) {
            list.add(slice.get(e));
        }
        return list;
    }
} public class IconTextView extends TextView {
    public IconTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setTypeface(Typefaces.get(getContext(), "material.ttf"));
    }

    public void setIconColor(int color) {
        setTextColor(getResources().getColor(color));
    }
} public class ViewCache {

    private SparseArray<View> m_cache;
    private View m_parent;
    private Context m_context;

    public ViewCache(Context context, View parent) {
        m_cache = new SparseArray<>();
        m_parent = parent;
        m_context = context;
    }

    private View getCachedView(int id) {
        View view = m_cache.get(id);
        if (view == null) {
            view = m_parent.findViewById(id);
            m_cache.put(id, view);
        }
        return view;

    }

    public TextView getTextView(int id) {
        return (TextView) getCachedView(id);
    }

    public ImageView getImageView(int id) {
        return (ImageView) getCachedView(id);
    }

    public Object getSelectedItem(int id) {
        return ((Spinner) getCachedView(id)).getSelectedItem();
    }

    public String getText(int id) {
        return getTextView(id).getText().toString().trim();
    }

    public void setError(int id, int errorMessage) {
        getTextView(id).setError(m_parent.getContext().getString(errorMessage));
    }

    public void clearError(int id) {
        getTextView(id).setError(null);
    }

    public String getRequiredText(int id) {
        clearError(id);
        String text = getText(id);
        if (text.length() == 0) {
            setError(id, R.string.error_field_required);
            return null;
        }
        return text;
    }

    public void hide(int id) {
        hide(id, false);
    }

    public void hide(int id, boolean hideKeyboard) {
        View view = getCachedView(id);
        view.setVisibility(View.GONE);

        if (hideKeyboard) {
            InputMethodManager imm = (InputMethodManager) m_context.getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
        }
    }

    public void show(int id) {
        getCachedView(id).setVisibility(View.VISIBLE);
    }

    public void setText(int id, String text) {
        getTextView(id).setText(text);
    }

    public void setVisible(int id, boolean visible) {
        View view = getCachedView(id);
        if (view != null) {
            if (visible) {
                view.setVisibility(View.VISIBLE);
            } else {
                view.setVisibility(View.GONE);
            }
        }
    }

    public ListView getListView(int id) {
        return (ListView) getCachedView(id);
    }

    public ListAdapter getListViewAdapter(int list) {
        ListView listView = getListView(list);
        if (listView != null) {
            return listView.getAdapter();
        }
        return null;
    }

    public Button getButton(int id) {
        return (Button) getCachedView(id);
    }

    public void setButtonText(int id, String text) {
        Button button = getButton(id);
        if (button != null) {
            button.setText(text);
        }
    }

    public View getView(int id) {
        return getCachedView(id);
    }
} public class ImageUtils {

    /**
     * Encodes the given bitmap as a JPEG
     * @param bm the bitmap
     * @return the JSON data
     */
    public static byte[] convertToJPEG(Bitmap bm) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bm.compress(Bitmap.CompressFormat.JPEG, 80, baos);
        return baos.toByteArray();
    }

    /**
     * Scales a bitmap so that it's longest dimension is provided value
     */
    public static Bitmap scaleToMax(Bitmap bitmap, int max) {

        // landscape photos
        if (bitmap.getWidth() > bitmap.getHeight()) {
            double ratio = (double) max / (double) bitmap.getWidth();
            return Bitmap.createScaledBitmap(bitmap, max, (int) ((double) bitmap.getHeight() * ratio), false);

        } else {
            double ratio = (double) max / (double) bitmap.getHeight();
            return Bitmap.createScaledBitmap(bitmap, (int) ((double) bitmap.getWidth() * ratio), max, false);
        }
    }

    /**
     * Rotates a bitmap by the given number of degrees
     */
    public static Bitmap rotateImage(Bitmap img, int degrees) {
        Matrix matrix = new Matrix();
        matrix.postRotate(degrees);
        Bitmap rotatedImg = Bitmap.createBitmap(img, 0, 0, img.getWidth(), img.getHeight(), matrix, true);
        img.recycle();
        return rotatedImg;
    }

    /**
     * Gets the EXIF orientation (if set) in degrees
     */
    public static int getExifRotation(String path) {
        try {
            ExifInterface exif = new ExifInterface(path);
            int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    return 90;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    return 180;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    return 270;
            }
        } catch (IOException e) {
            Logger.d("Unable to read EXIF data from " + path);
        }
        return 0;
    }

    /**
     * Creates a thumbnail image from the given video file
     *
     * @param video the video file
     * @return the thumbnail bitmap
     */
    public static Bitmap thumbnailFromVideo(File video) {
        return ThumbnailUtils.createVideoThumbnail(video.getAbsolutePath(), MediaStore.Images.Thumbnails.MINI_KIND);
    }
} public class ChatBubbleView extends CachedLinearLayout {

    public ChatBubbleView(Context context) {
        super(context);
        init();
    }

    public ChatBubbleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ChatBubbleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    /**
     * Set the message to show in our chat bubble
     *
     * @param inbound true if it is coming from the flow engine
     * @param text    the message to set on the view
     */
    public void setMessage(String text, boolean inbound) {
        TextView tv = getTextView(R.id.text_message);
        tv.setText(text);
        tv.setMovementMethod(LinkMovementMethod.getInstance());

        if (!inbound) {
            tv.setBackground(getResources().getDrawable(R.drawable.chat_bubble_out));
        }

        addView(getSpacer(), inbound ? 1 : 0);
    }

    public void setThumbnail(Bitmap image, String url, int type) {
        ImageView imageView = getImageView(R.id.thumbnail);

        if (image != null) {
            imageView.setImageBitmap(image);
        } else {

        }

        View mediaView = getView(R.id.media_view);
        mediaView.setVisibility(VISIBLE);

        mediaView.setTag(R.string.tag_url, url);
        mediaView.setTag(R.string.tag_media_type, type);

        if (type == R.string.media_image) {
            getTextView(R.id.media_icon).setText(R.string.icon_photo);
        } else if (type == R.string.media_video) {
            getTextView(R.id.media_icon).setText(R.string.icon_play_arrow);
        } else if (type == R.string.media_audio || type == R.string.media_location) {
            IconTextView iconView = (IconTextView) getTextView(R.id.media_icon);

            if (type == R.string.media_audio) {
                iconView.setText(R.string.icon_volume_up);
            } else {
                iconView.setText(R.string.icon_place);
            }

            iconView.setIconColor(R.color.primary_lightest);
            getView(R.id.media_view).setBackground(null);
            final float scale = getContext().getResources().getDisplayMetrics().density;
            int pixels = (int) (70 * scale + 0.5f);
            ViewGroup.LayoutParams params = mediaView.getLayoutParams();
            params.height = pixels;
            mediaView.setLayoutParams(params);
        }

        show(R.id.media_view);
        hide(R.id.text_message);
        hide(R.id.spacer);
    }

    /**
     * Find and remove our spacer from the current layout
     */
    private View getSpacer() {

        // remove our spacer if it's in there
        View spacer = findViewById(R.id.spacer);
        if (spacer != null) {
            removeView(spacer);
        }
        return spacer;
    }
} public class IconLinkView extends CachedLinearLayout {

    public IconLinkView(Context context) {
        super(context);
        init();
    }

    public IconLinkView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public IconLinkView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    protected void init() {
        super.init();
    }

    public void initialize(String message, int type, String url) {

        View bubble = getView(R.id.chat_bubble);
        bubble.setTag(R.string.tag_url, url);
        bubble.setTag(R.string.tag_media_type, type);

        TextView tv = getTextView(R.id.text_message);
        tv.setText(message);

        IconTextView iconView = (IconTextView) getTextView(R.id.media_icon);

        if (type == R.string.media_audio) {
            iconView.setText(R.string.icon_volume_up);
        } else if (type == R.string.media_location) {
            iconView.setText(R.string.icon_place);
        }
    }
}
import android.Manifest;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.greysonparrelli.permiso.Permiso;
import com.nyaruka.goflow.mobile.Environment;
import com.nyaruka.goflow.mobile.Event;
import com.nyaruka.goflow.mobile.Hint;
import com.nyaruka.goflow.mobile.MsgIn;
import com.nyaruka.goflow.mobile.Resume;
import com.nyaruka.goflow.mobile.SessionAssets;
import com.nyaruka.goflow.mobile.Trigger;
import org.apache.commons.lang3.tuple.Pair;
import java.io.File;
import java.io.IOException;
import io.rapidpro.surveyor.Logger;
import io.rapidpro.surveyor.R;
import io.rapidpro.surveyor.SurveyorIntent;
import io.rapidpro.surveyor.data.Flow;
import io.rapidpro.surveyor.data.Org;
import io.rapidpro.surveyor.data.Submission;
import io.rapidpro.surveyor.engine.Contact;
import io.rapidpro.surveyor.engine.Engine;
import io.rapidpro.surveyor.engine.EngineException;
import io.rapidpro.surveyor.engine.Session;
import io.rapidpro.surveyor.engine.Sprint;
import io.rapidpro.surveyor.ui.IconTextView;
import io.rapidpro.surveyor.ui.ViewCache;
import io.rapidpro.surveyor.utils.ImageUtils;
import io.rapidpro.surveyor.widget.ChatBubbleView;
import io.rapidpro.surveyor.widget.IconLinkView;
package io.rapidpro.surveyor.activity;







public class RunActivity extends BaseActivity {

    // the different types of requests for media
    public static final String REQUEST_IMAGE = "image";
    public static final String REQUEST_AUDIO = "audio";
    public static final String REQUEST_VIDEO = "video";
    public static final String REQUEST_GPS = "geo";

    // custom request codes passed to media capture activities
    private static final int RESULT_IMAGE = 1;
    private static final int RESULT_VIDEO = 2;
    private static final int RESULT_AUDIO = 3;
    private static final int RESULT_GPS = 4;

    private static final int MAX_IMAGE_DIMENSION = 1024;
    private static final int MAX_THUMB_DIMENSION = 600;

    private LinearLayout chatHistory;
    private IconTextView sendButtom;
    private EditText chatCompose;
    private ScrollView scrollView;

    private Session session;
    private Submission submission;

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

        String orgUUID = getIntent().getStringExtra(SurveyorIntent.EXTRA_ORG_UUID);
        String flowUUID = getIntent().getStringExtra(SurveyorIntent.EXTRA_FLOW_UUID);

        setContentView(R.layout.activity_run);
        initUI();

        try {
            Org org = getSurveyor().getOrgService().get(orgUUID);
            Environment environment = Engine.createEnvironment(org);
            SessionAssets assets = Engine.createSessionAssets(environment, Engine.loadAssets(org.getAssets()));

            Flow flow = org.getFlow(flowUUID);
            setTitle(flow.getName());

            Trigger trigger = Engine.createManualTrigger(environment, Contact.createEmpty(assets), flow.toReference());