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 class SurveyorApplication extends Application {

    /**
     * The singleton instance of this application
     */
    private static SurveyorApplication s_this;

    /**
     * Service for network operations
     */
    private TembaService tembaService = null;

    /**
     * Service for local org operations
     */
    private OrgService orgService = null;

    /**
     * Service for local submission operations
     */
    private SubmissionService submissionService = null;

    /**
     * Gets the singleton instance of this application
     *
     * @return the instance
     */
    public static SurveyorApplication get() {
        return s_this;
    }

    @Override
    public void onCreate() {
        Logger.d("Creating Surveyor application...");

        super.onCreate();

        Logger.d("External storage dir=" + Environment.getExternalStorageDirectory() + " state=" + Environment.getExternalStorageState() + " emulated=" + Environment.isExternalStorageEmulated());

        s_this = this;

        tembaService = new TembaService(getTembaHost());

        try {
            orgService = new OrgService(getOrgsDirectory());
            submissionService = new SubmissionService(getSubmissionsDirectory());
        } catch (IOException e) {
            Logger.e("Unable to create directory based services", e);
        }
    }

    /**
     * Gets the name of the preferences file
     *
     * @return the name of the preferences
     */
    public String getPreferencesName() {
        return "default";
    }

    /**
     * Gets the shared preferences for this application
     *
     * @return the preferences
     */
    public SharedPreferences getPreferences() {
        return s_this.getSharedPreferences(getPreferencesName(), Context.MODE_PRIVATE);
    }

    /**
     * Saves a string shared preference for this application
     *
     * @param key   the preference key
     * @param value the preference value
     */
    public void setPreference(String key, String value) {
        getPreferences().edit().putString(key, value).apply();
    }

    /**
     * Saves a string-set shared preference for this application
     *
     * @param key    the preference key
     * @param values the preference value
     */
    public void setPreference(String key, Set<String> values) {
        getPreferences().edit().putStringSet(key, values).apply();
    }

    /**
     * Clears a shared preference for this application
     *
     * @param key the preference key
     */
    public void clearPreference(String key) {
        getPreferences().edit().remove(key).apply();
    }

    /**
     * Gets the base URL of the Temba instance we're connected to
     *
     * @return the base URL
     */
    public String getTembaHost() {
        String host = getPreferences().getString(SurveyorPreferences.HOST, getString(R.string.pref_default_host));

        // strip any trailing slash
        if (host.endsWith("/")) {
            host = host.substring(0, host.length() - 1);
        }

        return host;
    }

    /**
     * Called when our host setting has changed
     */
    public void onTembaHostChanged() {
        String newHost = getTembaHost();

        Logger.d("Host changed to " + newHost);

        clearPreference(SurveyorPreferences.AUTH_USERNAME);
        clearPreference(SurveyorPreferences.AUTH_ORGS);
        try {
            getSubmissionService().clearAll();
        } catch (IOException e) {
            Logger.e("Unable to clear submissions", e);
        }

        tembaService = new TembaService(newHost);
    }

    /**
     * Returns the Temba API service
     *
     * @return the service
     */
    public TembaService getTembaService() {
        return tembaService;
    }

    /**
     * Returns the local orgs service
     *
     * @return the service
     */
    public OrgService getOrgService() {
        return orgService;
    }

    /**
     * Returns the local submissions service
     *
     * @return the service
     */
    public SubmissionService getSubmissionService() {
        return submissionService;
    }

    /**
     * Gets the directory for org configurations
     *
     * @return the directory
     */
    public File getOrgsDirectory() throws IOException {
        return SurveyUtils.mkdir(getFilesDir(), "orgs");
    }

    /**
     * Gets the directory for user collected data
     *
     * @return the directory
     */
    public File getUserDirectory() {
        return getExternalFilesDir(null);
    }

    /**
     * Gets the submissions storage directory
     *
     * @return the directory
     */
    protected File getSubmissionsDirectory() throws IOException {
        return SurveyUtils.mkdir(getUserDirectory(), "submissions");
    }

    /**
     * Gets the URI for the given file using our application's file provider
     *
     * @param file the file
     * @return the URI
     */
    public Uri getUriForFile(File file) {
        return FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", file);
    }

    /**
     * Generate dump of the Android log
     *
     * @return the URI of the dump file
     */
    public Uri generateLogDump() throws IOException {
        // log our build and device details
        Logger.d("Version: " + BuildConfig.VERSION_NAME + "; " + BuildConfig.VERSION_CODE);
        Logger.d("OS: " + System.getProperty("os.version") + " (API " + Build.VERSION.SDK_INT + ")");
        Logger.d("Model: " + android.os.Build.MODEL + " (" + android.os.Build.DEVICE + ")");

        // dump log to file and return URI
        File outputFile = new File(getUserDirectory(), "bug-report.txt");
        Runtime.getRuntime().exec("logcat -d -f " + outputFile.getAbsolutePath() + " \"Surveyor:* *:E\"");
        return getUriForFile(outputFile);
    }
} public class OrgAssets {
    private List<FieldAsset> fields;
    private List<GroupAsset> groups;
    private List<LocationAsset> locations;
    private List<RawJson> flows;

    /**
     * Constructs a new set of org assets (fields, groups, locations and flows)
     */
    private OrgAssets(List<FieldAsset> fields, List<GroupAsset> groups, List<LocationAsset> locations, List<RawJson> flows) {
        this.fields = fields;
        this.groups = groups;
        this.locations = locations;
        this.flows = flows;
    }

    /**
     * Constructs a new set of org assets from the data returned from the Temba API
     */
    public static OrgAssets fromTemba(List<Field> fields, List<Group> groups, List<Boundary> boundaries, List<RawJson> flows) {
        List<FieldAsset> fieldAssets = new ArrayList<>(fields.size());
        for (Field field : fields) {
            fieldAssets.add(FieldAsset.fromTemba(field));
        }

        List<GroupAsset> groupAssets = new ArrayList<>(groups.size());
        for (Group group : groups) {
            groupAssets.add(GroupAsset.fromTemba(group));
        }

        List<LocationAsset> locationAssets = new ArrayList<>();
        if (boundaries.size() > 0) {
            LocationAsset location = LocationAsset.fromTemba(boundaries);
            locationAssets = Collections.singletonList(location);
        }

        return new OrgAssets(fieldAssets, groupAssets, locationAssets, flows);
    }

    /**
     * Extract the flow summaries from this set of org assets
     */
    public List<Flow> getFlows() {
        List<Flow> summaries = new ArrayList<>(this.flows.size());
        for (RawJson flow : this.flows) {
            summaries.add(Flow.extract(flow));
        }
        return summaries;
    }
} public class TembaException extends SurveyorException {
    public TembaException(String message) {
        super(message);
    }

    public TembaException(String message, Exception e) {
        super(message, e);
    }
} public class TembaService {

    private TembaAPI api;

    public TembaService(String host) {
        this.api = createRetrofit(host).create(TembaAPI.class);
    }

    /**
     * Utility to create a Authorization header value from a token
     */
    private static String asAuth(String token) {
        return "Token " + token;
    }

    private static Retrofit createRetrofit(String host) {

        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .readTimeout(60, TimeUnit.SECONDS)
                .connectTimeout(60, TimeUnit.SECONDS);

        // add extra logging for debug mode
        if (BuildConfig.DEBUG) {
            builder.addInterceptor(interceptor);
        }

        final OkHttpClient okHttpClient = builder.build();

        try {
            return new Retrofit.Builder()
                    .baseUrl(host)
                    .addConverterFactory(GsonConverterFactory.create(JsonUtils.getGson()))
                    .client(okHttpClient)
                    .build();
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Calls RapidPro's authenticate endpoint to give us the list of tokens and orgs we can access
     *
     * @param username the username
     * @param password the password
     */
    public void authenticate(String username, String password, Callback<TokenResults> callback) {
        api.authenticate(username, password, "S").enqueue(callback);
    }

    /**
     * Gets all of the admin boundaries
     *
     * @param token the authentication token
     */
    public List<Boundary> getBoundaries(final String token) throws TembaException {
        return fetchAllPages(new PageCaller<Boundary>() {
            @Override
            public Call<PaginatedResults<Boundary>> createCall(String cursor) {
                return api.getBoundaries(asAuth(token), cursor);
            }
        });
    }

    /**
     * Gets the org associated with the given token
     *
     * @param token the authentication token
     */
    public Org getOrg(String token) throws TembaException {
        try {
            Response<Org> response = api.getOrg(asAuth(token)).execute();
            checkResponse(response);

            return response.body();
        } catch (IOException e) {
            throw new TembaException("Unable to fetch org", e);
        }
    }

    /**
     * Gets all of the contact fields
     *
     * @param token the authentication token
     */
    public List<Field> getFields(final String token) throws TembaException {
        return fetchAllPages(new PageCaller<Field>() {
            @Override
            public Call<PaginatedResults<Field>> createCall(String cursor) {
                return api.getFields(asAuth(token), cursor);
            }
        });
    }

    /**
     * Gets all of the non-archived surveyor flows
     *
     * @param token the authentication token
     */
    public List<Flow> getFlows(final String token) throws TembaException {
        return fetchAllPages(new PageCaller<Flow>() {
            @Override
            public Call<PaginatedResults<Flow>> createCall(String cursor) {
                return api.getFlows(asAuth(token), "survey", false, cursor);
            }
        });
    }

    /**
     * Gets all of the contact groups
     *
     * @param token the authentication token
     */
    public List<Group> getGroups(final String token) throws TembaException {
        return fetchAllPages(new PageCaller<Group>() {
            @Override
            public Call<PaginatedResults<Group>> createCall(String cursor) {
                return api.getGroups(asAuth(token), cursor);
            }
        });
    }

    /**
     * Gets full definitions for the given flows
     *
     * @param token the authentication token
     * @param flows the list of flows
     */
    public List<RawJson> getDefinitions(final String token, final List<Flow> flows) throws TembaException {
        // gather up flow UUIDs
        final List<String> flowUUIDs = new ArrayList<>(flows.size());
        for (Flow flow : flows) {
            flowUUIDs.add(flow.getUuid());
        }

        try {
            Response<Definitions> response = api.getDefinitions(asAuth(token), flowUUIDs, "none").execute();
            checkResponse(response);

            Definitions definitions = response.body();
            return definitions.getFlows();

        } catch (IOException e) {
            throw new TembaException("Unable to fetch definitions", e);
        }
    }

    /**
     * Uploads a media file and returns the remove URL
     *
     * @param token the authentication token
     * @param uri   the local file to upload
     * @return the new media URL
     */
    public String uploadMedia(String token, Uri uri) throws TembaException {
        String uriString = uri.toString();
        String baseName = FilenameUtils.getBaseName(uriString);
        String extension = FilenameUtils.getExtension(uriString);

        // build multipart request
        Map<String, RequestBody> map = new HashMap<>();
        map.put("extension", RequestBody.create(MediaType.parse("text/plain"), extension));

        try {
            InputStream stream = SurveyorApplication.get().getContentResolver().openInputStream(uri);
            byte[] bytes = IOUtils.toByteArray(stream);

            RequestBody fileBody = RequestBody.create(MediaType.parse("multipart/form-data"), bytes);
            map.put("media_file\"; filename=\"" + baseName, fileBody);

            Response<JsonObject> result = api.uploadMedia(asAuth(token), map).execute();
            checkResponse(result);

            return result.body().get("location").getAsString();

        } catch (IOException e) {
            throw new TembaException("Error uploading media", e);
        }
    }

    /**
     * Submits a submission payload
     *
     * @param token      the authentication token
     * @param submission the payload
     */
    public void submit(String token, SubmissionPayload submission) throws TembaException {
        try {
            Response<JsonObject> result = api.submit(asAuth(token), submission).execute();
            checkResponse(result);

        } catch (IOException e) {
            throw new TembaException("Error submitting", e);
        }
    }

    /**
     * Utility for fetching all pages of a given type
     */
    private <T> List<T> fetchAllPages(PageCaller<T> caller) throws TembaException {
        try {
            List<T> all = new ArrayList<>();
            PaginatedResults<T> page = null;

            do {
                String cursor = page != null ? page.getNextCursor() : null;
                Call<PaginatedResults<T>> call = caller.createCall(cursor);
                Response<PaginatedResults<T>> response = call.execute();

                if (response.isSuccessful()) {
                    page = response.body();
                    all.addAll(page.getResults());
                } else {
                    throw new TembaException("Server returned non-200 response for " + call.request().url().toString());
                }

            } while (page.hasNext());

            return all;
        } catch (IOException e) {
            throw new TembaException("Unable to fetch page from API", e);
        }
    }

    private void checkResponse(Response<?> response) throws TembaException {

        if (!response.isSuccessful()) {

            String errorBody;
            try {
                errorBody = response.errorBody().string();
            } catch (Exception e) {
                throw new TembaException("Unable to extract error body", e);
            }

            // make a note of the error in our log
            Logger.w(errorBody);

            // see if the server had anything interesting to say
            Gson gson = new Gson();
            JsonObject error = gson.fromJson(errorBody, JsonObject.class);
            if (error != null) {
                JsonElement detail = error.get("detail");
                if (detail != null) {

                    String message = detail.getAsString();
                    if (message.equals("Invalid token")) {
                        message = "Login failure, please logout and try again.";
                    }
                    throw new TembaException(message);
                }
            }

            throw new TembaException("Error reading response");
        }
    }

    /**
     * Utility for fetching all pages of a given type
     */
    private interface PageCaller<T> {
        Call<PaginatedResults<T>> createCall(String cursor);
    }
} public class Boundary {
    @SerializedName("osm_id")
    private String osmID;

    private String name;
    private Reference parent;
    private int level;
    private String[] aliases;

    public String getOsmID() {
        return osmID;
    }

    public String getName() {
        return name;
    }

    public Reference getParent() {
        return parent;
    }

    public int getLevel() {
        return level;
    }

    public String[] getAliases() {
        return aliases;
    }

    public static class Reference {
        @SerializedName("osm_id")
        private String osmID;

        private String name;

        public String getOsmID() {
            return osmID;
        }

        public String getName() {
            return name;
        }
    }
} public class Field {
    private String key;
    private String label;

    @SerializedName("value_type")
    private String valueType;

    public String getKey() {
        return key;
    }

    public String getLabel() {
        return label;
    }

    public String getValueType() {
        return valueType;
    }
} public class Group {
    private String uuid;
    private String name;
    private String query;

    public String getUuid() {
        return uuid;
    }

    public String getName() {
        return name;
    }

    public String getQuery() {
        return query;
    }
} public class JsonUtils {
    private static Gson s_gson = new GsonBuilder()
            //.setPrettyPrinting()
            .registerTypeAdapter(RawJson.class, new RawJson.Adapter())
            .create();

    public static Gson getGson() {
        return s_gson;
    }

    public static String marshal(Object src) {
        return s_gson.toJson(src);
    }

    public static <T> T unmarshal(String json, Class<T> clazz) {
        return s_gson.fromJson(json, clazz);
    }

    public static <T> T unmarshal(String json, TypeToken type) {
        return s_gson.fromJson(json, type.getType());
    }
} public class RawJson {
    private String data;

    public RawJson(String data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return this.data;
    }

    /**
     * Type adapter to tell Gson how to use fields of this type
     */
    public static class Adapter extends TypeAdapter<RawJson> {
        @Override
        public void write(JsonWriter out, RawJson value) throws IOException {
            out.jsonValue(value.toString());
        }

        @Override
        public RawJson read(JsonReader in) throws IOException {
            // TODO ideally this wouldn't parse the entire tree into memory
            // see https://github.com/google/gson/issues/1368
            JsonElement parsed = new JsonParser().parse(in);

            return new RawJson(parsed.toString());
        }
    }
}
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import io.rapidpro.surveyor.Logger;
import io.rapidpro.surveyor.SurveyorApplication;
import io.rapidpro.surveyor.engine.OrgAssets;
import io.rapidpro.surveyor.net.TembaException;
import io.rapidpro.surveyor.net.TembaService;
import io.rapidpro.surveyor.net.responses.Boundary;
import io.rapidpro.surveyor.net.responses.Field;
import io.rapidpro.surveyor.net.responses.Group;
import io.rapidpro.surveyor.utils.JsonUtils;
import io.rapidpro.surveyor.utils.RawJson;
package io.rapidpro.surveyor.data;





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