diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a7509..6630d50 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,44 @@ + + + + diff --git a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/MainActivity.java b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/MainActivity.java index 184a6f7..6901837 100644 --- a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/MainActivity.java +++ b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/MainActivity.java @@ -1,20 +1,34 @@ package org.timecrafters.TimeCraftersConfigurationTool; +import android.Manifest; import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; import com.google.android.material.bottomnavigation.BottomNavigationView; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import androidx.navigation.NavController; import androidx.navigation.Navigation; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; +import org.timecrafters.TimeCraftersConfigurationTool.backend.Backend; +import org.timecrafters.TimeCraftersConfigurationTool.dialogs.Dialog; +import org.timecrafters.TimeCraftersConfigurationTool.dialogs.PermissionsRequestDialog; + +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + public class MainActivity extends AppCompatActivity { + private static final int REQUEST_WRITE_PERMISSION = 70; + private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); BottomNavigationView navView = findViewById(R.id.nav_view); // Passing each menu ID as a set of Ids because each @@ -26,6 +40,40 @@ public class MainActivity extends AppCompatActivity { NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); NavigationUI.setupWithNavController(navView, navController); + + if (!havePermissions()) { + new PermissionsRequestDialog().show(getSupportFragmentManager(), null); + } else { + new Backend(); + } } + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + switch (requestCode) { + case REQUEST_WRITE_PERMISSION: { + if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { + // Permission granted + new Backend(); + } else { + // Permission not given + new PermissionsRequestDialog().show(getSupportFragmentManager(), null); + } + } + } + } + + private boolean havePermissions() { + return ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED; + } + + public void requestStoragePermissions() { + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + REQUEST_WRITE_PERMISSION); + } + + public void close() { + finish(); + } } \ No newline at end of file diff --git a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Backend.java b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Backend.java index 477a9e6..2e623bc 100644 --- a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Backend.java +++ b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/backend/Backend.java @@ -1,11 +1,30 @@ package org.timecrafters.TimeCraftersConfigurationTool.backend; -import org.timecrafters.TimeCraftersConfigurationTool.tacnet.PacketHandler; +import android.util.Log; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.internal.LinkedTreeMap; + +import org.timecrafters.TimeCraftersConfigurationTool.serializers.SettingsDeserializer; +import org.timecrafters.TimeCraftersConfigurationTool.serializers.SettingsSerializer; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; public class Backend { + private static final String TAG = "Backend"; static private Backend instance; private TACNET tacnet; private Config config; @@ -16,7 +35,7 @@ public class Backend { instance = this; loadSettings(); - if (settings.config != null) { + if (!settings.config.isEmpty()) { loadConfig(settings.config); } tacnet = new TACNET(); @@ -84,28 +103,94 @@ public class Backend { settingsChanged = true; } - public boolean isSettingsChanged() { + public boolean haveSettingsChanged() { return settingsChanged; } - public boolean loadSettings() { - return false; + public void loadSettings() { + File settingsFile = new File(TAC.SETTINGS_PATH); + + if (!settingsFile.exists()) { + Log.i(TAG, "Writing default settings.json"); + writeDefaultSettings(); + } + + try { + settings = gsonForSettings().fromJson(new FileReader(settingsFile), Settings.class); + } catch (FileNotFoundException e) { + // TODO + Log.e(TAG, "Unable to load settings.json"); + } } public void saveSettings() { - + Log.i(TAG, "Settings: " + gsonForSettings().toJson(settings)); + writeToFile(TAC.SETTINGS_PATH, gsonForSettings().toJson(settings)); } public void writeDefaultSettings() { - /* - { - "data": - { - "hostname":TACNET.DEFAULT_HOSTNAME, - "port":TACNET.DEFAULT_PORT, - "config":null, - } + settings = new Settings(TACNET.DEFAULT_HOSTNAME, TACNET.DEFAULT_PORT, ""); + saveSettings(); + } + + private Gson gsonForSettings() { + return new GsonBuilder() + .registerTypeAdapter(Settings.class, new SettingsSerializer()) + .registerTypeAdapter(Settings.class, new SettingsDeserializer()) + .create(); + } + + private String readFromFile(String path) { + StringBuilder text = new StringBuilder(); + + try { + BufferedReader br = new BufferedReader( new FileReader(path) ); + String line; + + while((line = br.readLine()) != null) { + text.append(line); + text.append("\n"); + } + + br.close(); + } catch (IOException e) { + // TODO + } + + return text.toString(); + } + + protected boolean writeToFile(String filePath, String content) { + try { + if (filePath.startsWith(TAC.ROOT_PATH)) { + createFolders(filePath); + + FileWriter writer = new FileWriter(filePath); + writer.write(content); + writer.close(); + + return true; + } else { + Log.e(TAG, "writeToFile disallowed path: " + filePath); + return false; + } + + } catch (IOException e) { + Log.e(TAG, e.getLocalizedMessage()); + return false; + } + } + + private void createFolders(String filePath) throws IOException { + File rootPath = new File(TAC.ROOT_PATH); + File configsPath = new File(TAC.CONFIGS_PATH); + + if (!rootPath.exists()) { + rootPath.mkdir(); + } + + if (!configsPath.exists()) { + configsPath.mkdir(); } - */ } } diff --git a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/Dialog.java b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/Dialog.java index 2f88c0a..7c0a791 100644 --- a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/Dialog.java +++ b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/Dialog.java @@ -1,45 +1,43 @@ package org.timecrafters.TimeCraftersConfigurationTool.dialogs; -import android.app.ActionBar; -import android.content.Context; import android.graphics.Point; import android.os.Bundle; -import android.util.DisplayMetrics; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.widget.Button; import android.widget.ImageButton; -import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; import org.timecrafters.TimeCraftersConfigurationTool.R; -public class Dialog extends android.app.Dialog { - public Dialog(@NonNull Context context) { - super(context); +public class Dialog extends DialogFragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = View.inflate(getContext(), R.layout.dialog_base, null); + + ImageButton closeButton = v.findViewById(R.id.dialogCloseButton); + + if (isCancelable()) { + closeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + } else { + closeButton.setVisibility(View.GONE); + } + + return v; } @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.dialog_base); + public void onStart() { + super.onStart(); - ImageButton closeButton = findViewById(R.id.dialogCloseButton); - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - } - }); - } - - @Override - protected void onStart() { Point point = new Point(); - getWindow().getWindowManager().getDefaultDisplay().getSize(point); - getWindow().setLayout((int) (point.x * 0.75), ViewGroup.LayoutParams.WRAP_CONTENT); + getActivity().getWindowManager().getDefaultDisplay().getSize(point); + getDialog().getWindow().setLayout((int) (point.x * 0.8), ViewGroup.LayoutParams.WRAP_CONTENT); } } diff --git a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/PermissionsRequestDialog.java b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/PermissionsRequestDialog.java new file mode 100644 index 0000000..2b3b829 --- /dev/null +++ b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/PermissionsRequestDialog.java @@ -0,0 +1,53 @@ +package org.timecrafters.TimeCraftersConfigurationTool.dialogs; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import org.timecrafters.TimeCraftersConfigurationTool.MainActivity; +import org.timecrafters.TimeCraftersConfigurationTool.R; +import org.timecrafters.TimeCraftersConfigurationTool.backend.Backend; +import org.timecrafters.TimeCraftersConfigurationTool.backend.TAC; + +public class PermissionsRequestDialog extends Dialog { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + setCancelable(false); + + View v = super.onCreateView(inflater, container, savedInstanceState); + + ((TextView)v.findViewById(R.id.dialogTitle)).setText("Storage Permission Required"); + LinearLayout view = v.findViewById(R.id.dialogContent); + view.addView(getLayoutInflater().inflate(R.layout.dialog_permission_request, null)); + ((TextView)view.findViewById(R.id.message)).setText("Permission is required to write to external storage:\n\n" + TAC.ROOT_PATH); + + Button quitButton = view.findViewById(R.id.quit_button); + Button continueButton = view.findViewById(R.id.continue_button); + + quitButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + ((MainActivity) getActivity()).close(); + } + }); + + continueButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + ((MainActivity) getActivity()).requestStoragePermissions(); + } + }); + + + return v; + } +} diff --git a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/VariableDialog.java b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/VariableDialog.java new file mode 100644 index 0000000..f7a2493 --- /dev/null +++ b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/dialogs/VariableDialog.java @@ -0,0 +1,42 @@ +package org.timecrafters.TimeCraftersConfigurationTool.dialogs; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.timecrafters.TimeCraftersConfigurationTool.R; + +public class VariableDialog extends Dialog { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + setCancelable(false); + + View v = super.onCreateView(inflater, container, savedInstanceState); + + ((TextView)v.findViewById(R.id.dialogTitle)).setText("Add Variable"); + LinearLayout view = v.findViewById(R.id.dialogContent); + view.addView(getLayoutInflater().inflate(R.layout.dialog_edit_variable, null)); + + Button cancelButton = view.findViewById(R.id.cancel); + Button mutateButton = view.findViewById(R.id.mutate); + cancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + mutateButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + return v; + } +} diff --git a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsDeserializer.java b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsDeserializer.java new file mode 100644 index 0000000..9406c46 --- /dev/null +++ b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsDeserializer.java @@ -0,0 +1,25 @@ +package org.timecrafters.TimeCraftersConfigurationTool.serializers; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import org.timecrafters.TimeCraftersConfigurationTool.backend.Settings; + +import java.lang.reflect.Type; + +public class SettingsDeserializer implements JsonDeserializer { + @Override + public Settings deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + JsonObject data = jsonObject.get("data").getAsJsonObject(); + + return new Settings( + data.get("hostname").getAsString(), + data.get("port").getAsInt(), + data.get("config").getAsString() + ); + } +} diff --git a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java new file mode 100644 index 0000000..9a10bb8 --- /dev/null +++ b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/serializers/SettingsSerializer.java @@ -0,0 +1,30 @@ +package org.timecrafters.TimeCraftersConfigurationTool.serializers; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import org.timecrafters.TimeCraftersConfigurationTool.backend.Settings; + +import java.lang.reflect.Type; + +public class SettingsSerializer implements JsonSerializer { + @Override + public JsonElement serialize(Settings settings, Type type, JsonSerializationContext context) { + JsonObject container = new JsonObject(); + JsonObject result = new JsonObject(); + result.add("hostname", new JsonPrimitive(settings.hostname)); + result.add("port", new JsonPrimitive(settings.port)); + result.add("config", new JsonPrimitive(settings.config)); + + container.add("data", result); + + return container; + } +} + diff --git a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/ui/tacnet/TACNETFragment.java b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/ui/tacnet/TACNETFragment.java index 4567c53..92c8e5e 100644 --- a/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/ui/tacnet/TACNETFragment.java +++ b/app/src/main/java/org/timecrafters/TimeCraftersConfigurationTool/ui/tacnet/TACNETFragment.java @@ -5,8 +5,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -15,9 +13,7 @@ import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; import org.timecrafters.TimeCraftersConfigurationTool.R; -import org.timecrafters.TimeCraftersConfigurationTool.dialogs.Dialog; - -import static android.view.View.inflate; +import org.timecrafters.TimeCraftersConfigurationTool.dialogs.VariableDialog; public class TACNETFragment extends Fragment { @@ -38,12 +34,8 @@ public class TACNETFragment extends Fragment { connect.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Dialog dialog = new Dialog(getContext()); - dialog.show(); - - ((TextView)dialog.findViewById(R.id.dialogTitle)).setText("Add Variable"); - LinearLayout view = dialog.findViewById(R.id.dialogContent); - view.addView(getLayoutInflater().inflate(R.layout.dialog_edit_variable, null)); + VariableDialog dialog = new VariableDialog(); + dialog.show(getFragmentManager(), null); } }); diff --git a/app/src/main/res/layout/dialog_edit_variable.xml b/app/src/main/res/layout/dialog_edit_variable.xml index ff84b47..8f3f868 100644 --- a/app/src/main/res/layout/dialog_edit_variable.xml +++ b/app/src/main/res/layout/dialog_edit_variable.xml @@ -65,14 +65,14 @@ android:orientation="horizontal">