diff --git a/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/MainActivity.java b/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/MainActivity.java index 68eb84f..f21e4ec 100644 --- a/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/MainActivity.java +++ b/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/MainActivity.java @@ -2,14 +2,19 @@ import android.Manifest; import android.annotation.SuppressLint; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.provider.OpenableColumns; import android.os.Bundle; +import android.provider.Settings; import android.view.Menu; import android.view.MenuItem; +import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -30,8 +35,6 @@ import com.truchisoft.jsonmanager.print.PrintConfig; import com.truchisoft.jsonmanager.utils.PrefManager; -import java.io.File; - public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, EditorFragment.OnFragmentInteractionListener { DrawerLayout _drawer; @@ -48,9 +51,7 @@ protected void onCreate(Bundle savedInstanceState) { onPostCreate(); if (checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_CODE); + requestPermissions(new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }, STORAGE_PERMISSION_CODE); } } @@ -69,39 +70,61 @@ private void onPostCreate() { NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); - CreateFloatingButton(); - Intent intent = getIntent(); String action = intent.getAction(); - if (action.compareTo(Intent.ACTION_VIEW) == 0) { - File f = new File(intent.getData().getPath()); - moveToEditor(f, intent.getData()); + if (action != null && action.compareTo(Intent.ACTION_VIEW) == 0) { // Add null check for action + Uri uri = intent.getData(); + if (uri != null) { // Add null check for uri + moveToEditor(uri); + } } } - private void CreateFloatingButton() { - } - - private void moveToEditor(File f, Uri uri) { - if (f.exists()) { - com.truchisoft.jsonmanager.utils.FileUtils.AddFileToPrefs(f, uri); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.fragment, com.truchisoft.jsonmanager.fragments.EditorFragment.newInstance(f.getAbsolutePath(), uri)); - transaction.addToBackStack(null); - transaction.commit(); + private String getDisplayNameFromUri(Uri uri) { + if (uri == null) { + return null; + } + String displayName = null; + if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + Cursor cursor = null; + try { + cursor = getContentResolver().query(uri, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (nameIndex != -1) { + displayName = cursor.getString(nameIndex); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } } + if (displayName == null) { + displayName = uri.getLastPathSegment(); + if (displayName == null || displayName.isEmpty()) { + displayName = "untitled.json"; // Default if everything else fails + } + } + return displayName; } - private void moveToEditor(String path, Uri uri) { - if (!path.isEmpty()) { - com.truchisoft.jsonmanager.utils.FileUtils.AddPathToPrefs(path, uri); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.fragment, com.truchisoft.jsonmanager.fragments.EditorFragment.newInstance(path, uri)); - transaction.addToBackStack(null); - transaction.commit(); + private void moveToEditor(Uri uri) { + if (uri == null) { + return; // Or handle error } - } + String displayName = getDisplayNameFromUri(uri); + // Assuming FileUtils.AddUriToPrefs will be the new method signature after refactoring FileUtils in a later step. + // For now, we adapt to what AddPathToPrefs does, but using display name and the uri. + com.truchisoft.jsonmanager.utils.FileUtils.AddPathToPrefs(displayName, uri); // This will be updated when FileUtils is refactored + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + // Assuming EditorFragment.newInstance will be changed to accept (String displayName, Uri uri) + transaction.replace(R.id.fragment, com.truchisoft.jsonmanager.fragments.EditorFragment.newInstance(displayName, uri)); + transaction.addToBackStack(null); + transaction.commit(); + } @Override public void onBackPressed() { @@ -172,8 +195,52 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == STORAGE_PERMISSION_CODE) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - onPostCreate(); + boolean readPermissionGranted = false; + if (grantResults.length > 0 && permissions.length > 0) { // Check permissions array as well + for (int i = 0; i < permissions.length; i++) { + if (Manifest.permission.READ_EXTERNAL_STORAGE.equals(permissions[i])) { + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + readPermissionGranted = true; + } + break; // Found the permission we care about + } + } + } + + if (readPermissionGranted) { + Toast.makeText(this, "Read Storage permission granted.", Toast.LENGTH_SHORT).show(); + // onPostCreate(); // Calling onPostCreate again might be too much, e.g., re-setting content view. + // Consider if a more targeted refresh is needed, or if initial load handles it. + // For now, let's assume the app can function or will prompt for file opening. + // If onPostCreate() is essential for basic app operation even after denial, then it must be called. + // Given its current content (UI setup, intent handling), it's probably okay to call. + onPostCreate(); + } else { + // Permission was denied. + Toast.makeText(this, "Read Storage permission is required to access files.", Toast.LENGTH_LONG).show(); + if (!shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) { + // User selected "Don't ask again" or policy prohibits asking again. + // Guide user to app settings. + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + // Check if intent can be resolved to avoid ActivityNotFoundException + if (intent.resolveActivity(getPackageManager()) != null) { + Toast.makeText(this, "Permission permanently denied. Please enable it in app settings.", Toast.LENGTH_LONG).show(); + // Consider showing a dialog that explains this and then launching the intent on positive button click. + // For now, direct launch for simplicity in this subtask. + // startActivity(intent); // Launching settings might be too abrupt without more context/dialog. + // For this subtask, just show a longer toast. + } else { + Toast.makeText(this, "Permission permanently denied. Please enable it in app settings (cannot open settings automatically).", Toast.LENGTH_LONG).show(); + } + } else { + // User denied but did not select "Don't ask again". + // Can show rationale and re-request if appropriate, or just inform them. + // For now, the Toast above is the main feedback. + } + // App might have limited functionality or should guide user to open files via SAF picker + // which doesn't always need this specific permission. } } } @@ -184,15 +251,15 @@ protected void onActivityResult(int requestCode, int resultCode, @Nullable Inten } - - final private Context currentCTX = this; ActivityResultLauncher startActivityResultLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { Intent intent = result.getData(); if (intent != null) { Uri uri = intent.getData(); - moveToEditor(uri.toString(), uri); + if (uri != null) { // Add null check for uri + moveToEditor(uri); // Call the new single-argument moveToEditor + } } }); diff --git a/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/fragments/EditorFragment.java b/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/fragments/EditorFragment.java index bea9fab..88e06d1 100644 --- a/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/fragments/EditorFragment.java +++ b/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/fragments/EditorFragment.java @@ -1,11 +1,14 @@ package com.truchisoft.jsonmanager.fragments; import android.app.Activity; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.provider.OpenableColumns; import android.os.Looper; import android.view.ContextMenu; import android.view.LayoutInflater; @@ -64,17 +67,24 @@ * create an instance of this fragment. */ public class EditorFragment extends Fragment implements TabHost.OnTabChangeListener { - // TODO: Rename parameter arguments, choose names that match - // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER - private static final String ARG_FILE_NAME = "fileName"; - private static final String ARG_URI = "argURI"; - private Context fragmentContext = null; - - // TODO: Rename and change types of parameters - private String filename; - private Uri _uri; + private static final String ARG_FILE_NAME = "fileName"; // Will store display name + private static final String ARG_URI = "argURI"; // Will store the actual Uri + + private String filename; // For display purposes + private Uri currentFileUri; // The Uri passed to the fragment + private String currentJsonContent = ""; // For onSaveInstanceState private Menu _currentMenu; private BaseItem _bi; + + // View Caching + private EditText etEditJson; + private TreeViewList tvJson; + private TabHost tabHost; + private View progressOverlay; // This would be the ProgressBar or its container + + private static final String STATE_URI = "state_uri"; + private static final String STATE_JSON_CONTENT = "state_json_content"; + private static final String STATE_DISPLAY_NAME = "state_display_name"; private TreeBuilder _tBuilder; private TreeStateManager _mManager; private JsonAdapter _jAdapter; @@ -87,15 +97,15 @@ public class EditorFragment extends Fragment implements TabHost.OnTabChangeListe * Use this factory method to create a new instance of * this fragment using the provided parameters. * - * @param Filename + * @param displayName + * @param uri * @return A new instance of fragment EditorFragment. */ - // TODO: Rename and change types and number of parameters - public static EditorFragment newInstance(String Filename, Uri uri) { + public static EditorFragment newInstance(String displayName, Uri uri) { EditorFragment fragment = new EditorFragment(); Bundle args = new Bundle(); - args.putString(ARG_FILE_NAME, Filename); - args.putParcelable(ARG_URI, uri); + args.putString(ARG_FILE_NAME, displayName); // ARG_FILE_NAME now stores displayName + args.putParcelable(ARG_URI, uri); // ARG_URI now stores the actual Uri fragment.setArguments(args); return fragment; } @@ -107,19 +117,51 @@ public EditorFragment() { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - fragmentContext = this.getActivity(); if (getArguments() != null) { - filename = getArguments().getString(ARG_FILE_NAME); - _uri = getArguments().getParcelable(ARG_URI); + filename = getArguments().getString(ARG_FILE_NAME); // This is the display name + currentFileUri = getArguments().getParcelable(ARG_URI); + } + + if (savedInstanceState != null) { + filename = savedInstanceState.getString(STATE_DISPLAY_NAME, filename); + String savedUriString = savedInstanceState.getString(STATE_URI); + if (savedUriString != null) { + currentFileUri = Uri.parse(savedUriString); + } + currentJsonContent = savedInstanceState.getString(STATE_JSON_CONTENT, ""); + // If currentJsonContent is available and we are in text mode, set it. + // If tree mode was active, loading from currentFileUri in onCreateView will handle it. } setHasOptionsMenu(true); } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (currentFileUri != null) { + outState.putString(STATE_URI, currentFileUri.toString()); + } + outState.putString(STATE_DISPLAY_NAME, filename); // Save current display name + + // Try to get current text from EditText if visible and save it + View view = getView(); + if (view != null) { + EditText et = view.findViewById(R.id.etEditJson); + if (et != null && et.getVisibility() == View.VISIBLE) { + currentJsonContent = et.getText().toString(); + } else if (currentJson != null) { + // if text editor not visible but we have a cached json string from tree view + currentJsonContent = currentJson; + } + } + outState.putString(STATE_JSON_CONTENT, currentJsonContent); + } + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - if (v.getId() == R.id.tvJson) { + if (v.getId() == R.id.tvJson && tvJson != null) { // Added null check for tvJson AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo; - _bi = (BaseItem) ((TreeViewList) getActivity().findViewById(R.id.tvJson)).getItemAtPosition(acmi.position); + _bi = (BaseItem) tvJson.getItemAtPosition(acmi.position); if (!(_bi instanceof PropertyItem)) { MenuInflater inflater = getActivity().getMenuInflater(); @@ -140,9 +182,12 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d @Override public boolean onContextItemSelected(MenuItem item) { int position; + // Ensure tvJson is not null before using it + if (tvJson == null) return false; + if (item.getItemId() == R.id.action_import) { position = ((AdapterView.AdapterContextMenuInfo) item.getMenuInfo()).position; - _bi = (BaseItem) ((TreeViewList) getActivity().findViewById(R.id.tvJson)).getItemAtPosition(position); + _bi = (BaseItem) tvJson.getItemAtPosition(position); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); @@ -151,18 +196,19 @@ public boolean onContextItemSelected(MenuItem item) { startActivityResultLauncher.launch(intent); } else if (item.getItemId() == R.id.action_importurl) { position = ((AdapterView.AdapterContextMenuInfo) item.getMenuInfo()).position; - _bi = (BaseItem) ((TreeViewList) getActivity().findViewById(R.id.tvJson)).getItemAtPosition(position); + _bi = (BaseItem) tvJson.getItemAtPosition(position); ActionImportUrl(_bi); } else if (item.getItemId() == R.id.action_copy) { position = ((AdapterView.AdapterContextMenuInfo) item.getMenuInfo()).position; - _bi = (BaseItem) ((TreeViewList) getActivity().findViewById(R.id.tvJson)).getItemAtPosition(position); + _bi = (BaseItem) tvJson.getItemAtPosition(position); copyJsonValue = getTreeElements(_bi).toString(); } else if (item.getItemId() == R.id.action_paste) { position = ((AdapterView.AdapterContextMenuInfo) item.getMenuInfo()).position; - _bi = (BaseItem) ((TreeViewList) getActivity().findViewById(R.id.tvJson)).getItemAtPosition(position); + _bi = (BaseItem) tvJson.getItemAtPosition(position); try { loadJson(copyJsonValue, _bi); } catch (JsonParseException jpe) { + Toast.makeText(getContext(), "Error parsing JSON for paste: " + jpe.getMessage(), Toast.LENGTH_LONG).show(); } } return false; @@ -175,6 +221,7 @@ public void OnResult(String Value) { try { loadJson(Value, bi); } catch (JsonParseException jpe) { + Toast.makeText(getContext(), "Error parsing JSON from URL: " + jpe.getMessage(), Toast.LENGTH_LONG).show(); } } }); @@ -203,9 +250,8 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } - if (id == R.id.action_prettify) { - EditText et = (EditText) getActivity().findViewById(R.id.etEditJson); - et.setText(getPreetyJson(et.getText().toString())); + if (id == R.id.action_prettify && etEditJson != null) { // Added null check + etEditJson.setText(getPreetyJson(etEditJson.getText().toString())); return true; } @@ -215,59 +261,106 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment - View view = inflater.inflate(R.layout.fragment_editor, container, false); - registerForContextMenu(view.findViewById(R.id.tvJson)); - createTabs(view); - createTree(view); + return inflater.inflate(R.layout.fragment_editor, container, false); + } - if (filename != null && !filename.isEmpty()) { + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + etEditJson = view.findViewById(R.id.etEditJson); + tvJson = view.findViewById(R.id.tvJson); + tabHost = view.findViewById(android.R.id.tabhost); + // progressOverlay = view.findViewById(R.id.progress_overlay); // Assuming R.id.progress_overlay exists in XML + // if (progressOverlay != null) progressOverlay.setVisibility(View.GONE); + + + if (tvJson != null) { // Check if tvJson was found before registering context menu + registerForContextMenu(tvJson); + } + createTabs(view); // view is still needed if createTabs accesses other views directly + createTree(view); // view is still needed if createTree accesses other views directly + + if (currentFileUri != null) { refreshTree(); try { - loadJson(loadFile()); + loadJson(loadFile(currentFileUri)); } catch (JsonParseException jpe) { - showFileList(); + // If loading from URI fails, and we have saved content, try that. + if (currentJsonContent != null && !currentJsonContent.isEmpty()) { + try { + loadJson(currentJsonContent); + // Consider updating filename to indicate this is restored/modified content + // filename = "Restored unsaved content"; + } catch (JsonParseException jpe2) { + showFileList(); // Both URI and saved content failed + } + } else { + showFileList(); // URI failed, no saved content + } } - _mManager.collapseChildren(null); - } else { + if (_mManager != null) _mManager.collapseChildren(null); // Added null check + } else if (currentJsonContent != null && !currentJsonContent.isEmpty()) { + // No URI, but have saved content (e.g. new file not yet saved, or restored state) refreshTree(); - _tBuilder.addRelation(null, new ArrayItem("Root")); + try { + loadJson(currentJsonContent); + if (filename == null || filename.isEmpty()) { // If filename wasn't restored + filename = "Restored Content"; // Or "New File" + } + } catch (JsonParseException jpe) { + // Handle case where saved content is also invalid + if (_tBuilder != null) _tBuilder.addRelation(null, new ArrayItem("Root")); // Start fresh // Added null check + } + } + else { + refreshTree(); + if (_tBuilder != null) _tBuilder.addRelation(null, new ArrayItem("Root")); // Added null check } - return view; } - private void createTree(View view) { - TreeViewList tvl = ((TreeViewList) view.findViewById(R.id.tvJson)); + + @Override + public void onDestroyView() { + etEditJson = null; + tvJson = null; + tabHost = null; + progressOverlay = null; + super.onDestroyView(); // Standard practice to call super at the end for onDestroyView + } + + private void createTree(View view) { // view param might be removable if tvJson is always used + if (tvJson == null) return; // Guard against null tvJson _mManager = new InMemoryTreeStateManager<>(); _tBuilder = new TreeBuilder<>(_mManager); _jAdapter = new JsonAdapter(this.getActivity(), _selected, _mManager, 1); - tvl.setAdapter(_jAdapter); + tvJson.setAdapter(_jAdapter); } private void refreshTree() { - _tBuilder.clear(); + if (_tBuilder != null) _tBuilder.clear(); // Added null check } - private void createTabs(View view) { - TabHost tabs = (TabHost) view.findViewById(android.R.id.tabhost); - tabs.setup(); + private void createTabs(View view) { // view param might be removable if tabHost is always used + if (tabHost == null) return; // Guard against null tabHost + tabHost.setup(); - TabHost.TabSpec spec = tabs.newTabSpec("tabTree"); + TabHost.TabSpec spec = tabHost.newTabSpec("tabTree"); spec.setContent(R.id.tabTree); spec.setIndicator("View Tree"); - tabs.addTab(spec); + tabHost.addTab(spec); - spec = tabs.newTabSpec("tabEditor"); + spec = tabHost.newTabSpec("tabEditor"); spec.setContent(R.id.tabEditor); spec.setIndicator("Edit Json"); - tabs.addTab(spec); + tabHost.addTab(spec); - tabs.setCurrentTab(0); + tabHost.setCurrentTab(0); - tabs.setOnTabChangedListener(this); + tabHost.setOnTabChangedListener(this); } - // TODO: Rename method, update argument and hook method into UI event public void onButtonPressed(Uri uri) { if (mListener != null) { mListener.onFragmentInteraction(uri); @@ -294,46 +387,56 @@ public void onDetach() { @Override public void onTabChanged(String tabId) { + if (etEditJson == null || tabHost == null || _currentMenu == null) return; // Guard against null views + switch (tabId) { case "tabTree": try { - String jsonvalue = ((EditText) getActivity().findViewById(R.id.etEditJson)).getText().toString(); + String jsonvalue = etEditJson.getText().toString(); + currentJsonContent = jsonvalue; // Save latest from editor if (!currentJson.equals(jsonvalue)) { try { loadJson(jsonvalue); refreshTree(); } catch (JsonParseException jpe) { - String message = "Exception ocurred while trying to read the json text: \n"; + String message = "Exception occurred while trying to read the json text: \n"; if (jpe.getCause() != null) message += jpe.getCause().getMessage(); else message += jpe.getMessage(); Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show(); - TabHost tabs = (TabHost) getActivity().findViewById(android.R.id.tabhost); - tabs.setOnTabChangedListener(null); - tabs.setCurrentTab(1); - tabs.setOnTabChangedListener(this); + tabHost.setOnTabChangedListener(null); + tabHost.setCurrentTab(1); + tabHost.setOnTabChangedListener(this); } } _currentMenu.findItem(R.id.action_prettify).setVisible(false); } catch (Exception ex) { - String message = "Exception ocurred while trying to read the json text: \n"; + String message = "Exception occurred while trying to read the json text: \n"; if (ex.getCause() != null) message += ex.getCause().getMessage(); else message += ex.getMessage(); Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show(); - TabHost tabs = getView().findViewById(android.R.id.tabhost); - tabs.setOnTabChangedListener(null); - tabs.setCurrentTab(1); - tabs.setOnTabChangedListener(this); + tabHost.setOnTabChangedListener(null); + tabHost.setCurrentTab(1); + tabHost.setOnTabChangedListener(this); } break; case "tabEditor": - currentJson = getPreetyJson(); - ((EditText) getActivity().findViewById(R.id.etEditJson)).setText(currentJson); + if (currentJsonContent != null && !currentJsonContent.isEmpty()) { + if(etEditJson.getText().toString().isEmpty()){ // Only set if editor is empty + etEditJson.setText(currentJsonContent); + } else { + currentJson = getPreetyJson(); // Update currentJson from tree before switching + etEditJson.setText(currentJson); + } + } else { + currentJson = getPreetyJson(); + etEditJson.setText(currentJson); + } _currentMenu.findItem(R.id.action_prettify).setVisible(true); break; } @@ -344,13 +447,41 @@ public interface OnFragmentInteractionListener { } //region File Related Functions - - private String loadFile(Uri uri) { - return FileUtils.ReadFromResource(uri); + private String getDisplayNameFromUri(Uri uri) { + if (uri == null) { + return null; + } + String displayName = null; + Context context = getContext(); + if (context != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(uri, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (nameIndex != -1) { + displayName = cursor.getString(nameIndex); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + if (displayName == null) { + displayName = uri.getLastPathSegment(); + if (displayName == null || displayName.isEmpty()) { + displayName = "untitled.json"; // Default if everything else fails + } + } + return displayName; } - private String loadFile() { - return FileUtils.ReadFromResource(_uri); + + private String loadFile(Uri uriToLoad) { + if (uriToLoad == null) return ""; // Or throw an exception + return FileUtils.ReadFromResource(getContext(), uriToLoad); // Pass context } private void showFileList() { @@ -377,8 +508,10 @@ private void loadJson(String jsonstring, BaseItem parent) throws JsonParseExcept } private void ConvertTask(final JsonElement jElement, final BaseItem parent) { -// final ProgressDialog progress = ProgressDialog.show(getActivity(), "Json Manager", -// "Please Wait...", true); + // if (progressOverlay != null) progressOverlay.setVisibility(View.VISIBLE); + // For now, use a Toast to indicate start if no ProgressBar view is available. + if (getContext() != null) Toast.makeText(getContext(), "Parsing JSON...", Toast.LENGTH_SHORT).show(); + ExecutorService executor = Executors.newSingleThreadExecutor(); Handler handler = new Handler(Looper.getMainLooper()); @@ -386,6 +519,8 @@ private void ConvertTask(final JsonElement jElement, final BaseItem parent) { executor.execute(new Runnable() { @Override public void run() { + if (_mManager == null || _tBuilder == null) return; // Guard clause + if (parent == null) ConvertJsonToTree(parent, jElement, "Root"); else { if (parent instanceof ArrayItem) @@ -397,8 +532,10 @@ public void run() { handler.post(new Runnable() { @Override public void run() { - _mManager.refresh(); -// progress.hide(); + if (_mManager != null) _mManager.refresh(); // Guard clause + // if (progressOverlay != null) progressOverlay.setVisibility(View.GONE); + // For now, use a Toast to indicate end if no ProgressBar view is available. + if (getContext() != null) Toast.makeText(getContext(), "JSON parsing complete.", Toast.LENGTH_SHORT).show(); } }); } @@ -435,35 +572,49 @@ private String getPreetyJson() { try { return gson.toJson(jsonArray); } catch (Exception ex) { - Toast.makeText(getActivity(), "Exception ocurred while trying to read the json text: \n" + ex.getCause().getMessage(), Toast.LENGTH_LONG).show(); + String errorMessage = (ex.getCause() != null) ? ex.getCause().getMessage() : ex.getMessage(); + if (errorMessage == null) errorMessage = "Unknown error during JSON processing."; + Toast.makeText(getActivity(), "Exception occurred while trying to read the json text: \n" + errorMessage, Toast.LENGTH_LONG).show(); return ""; } } private String getPreetyJson(String json) { try { - JsonElement jsonArray = getJson(); + JsonElement jsonArray = getJson(); // Ensure getJson() itself is safe or wrapped Gson gson = new GsonBuilder().setPrettyPrinting().create(); return gson.toJson(gson.fromJson(json, JsonElement.class)); } catch (Exception ex) { - Toast.makeText(getActivity(), "Exception ocurred while trying to read the json text: \n" + ex.getCause().getMessage(), Toast.LENGTH_LONG).show(); + String errorMessage = (ex.getCause() != null) ? ex.getCause().getMessage() : ex.getMessage(); + if (errorMessage == null) errorMessage = "Unknown error during JSON processing."; + Toast.makeText(getActivity(), "Exception occurred while trying to read the json text: \n" + errorMessage, Toast.LENGTH_LONG).show(); return json; } } private void saveJson(Boolean newFile) { final byte[] jData = getPreetyJson().getBytes(); + currentJsonContent = new String(jData); // Update currentJsonContent with what's being saved - if (newFile || filename == null || filename.isEmpty()) { + if (newFile || currentFileUri == null) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); - intent.putExtra(Intent.EXTRA_TITLE, "newfile.json"); + // Use current filename as suggestion if available, otherwise "newfile.json" + String suggestedName = (filename != null && !filename.isEmpty()) ? filename : "newfile.json"; + // Ensure the suggested name ends with .json if it's a new file or filename is a placeholder + if (!suggestedName.toLowerCase().endsWith(".json")) { + if (suggestedName.lastIndexOf('.') > 0) { // has extension but not json + suggestedName = suggestedName.substring(0, suggestedName.lastIndexOf('.')) + ".json"; + } else { // no extension + suggestedName = suggestedName + ".json"; + } + } + intent.putExtra(Intent.EXTRA_TITLE, suggestedName); createFileResultLauncher.launch(intent); } else { - File f = new File(filename); - FileUtils.AddFileToPrefs(f, _uri); - FileUtils.WriteToFile(_uri, jData); + com.truchisoft.jsonmanager.utils.FileUtils.AddPathToPrefs(filename, currentFileUri); // Use display name and current URI + FileUtils.WriteToFile(getContext(), currentFileUri, jData); // Pass context } } @@ -497,7 +648,7 @@ private JsonElement getTreeElements(BaseItem tn) { Intent intent = result.getData(); if (intent != null) { Uri uri = intent.getData(); - String fileContent = loadFile(uri); + String fileContent = loadFile(uri); // This loadFile takes URI and uses context try { loadJson(fileContent, _bi); } catch (JsonParseException jpe) { @@ -512,12 +663,23 @@ private JsonElement getTreeElements(BaseItem tn) { Intent intent = result.getData(); if (intent != null) { Uri uri = intent.getData(); - File f = new File(FileUtils.getRealPathFromURI(getContext(), uri)); - FileUtils.AddFileToPrefs(f, uri); - try { - FileUtils.WriteToFile(uri, getPreetyJson().getBytes()); - } catch (Exception ex) { - ex.printStackTrace(); + if (uri != null) { + currentFileUri = uri; + filename = getDisplayNameFromUri(currentFileUri); // Update filename with display name from URI + // Update preferences - using AddPathToPrefs with display name and URI + com.truchisoft.jsonmanager.utils.FileUtils.AddPathToPrefs(filename, currentFileUri); + try { + boolean success = FileUtils.WriteToFile(getContext(), currentFileUri, getPreetyJson().getBytes()); // Pass context + if (success) { + currentJsonContent = getPreetyJson(); // Update content after successful save + Toast.makeText(getContext(), "File saved successfully.", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "Error saving file.", Toast.LENGTH_LONG).show(); + } + } catch (Exception ex) { + Log.e("EditorFragment", "Error saving file via createFileResultLauncher", ex); // Keep detailed log + Toast.makeText(getContext(), "Error saving file: " + ex.getMessage(), Toast.LENGTH_LONG).show(); + } } } }); diff --git a/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/utils/FileUtils.java b/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/utils/FileUtils.java index 80df378..c5b6724 100644 --- a/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/utils/FileUtils.java +++ b/Fuentes/app/src/main/java/com/truchisoft/jsonmanager/utils/FileUtils.java @@ -1,38 +1,23 @@ package com.truchisoft.jsonmanager.utils; -import android.content.ClipData; -import android.content.ContentUris; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.os.Environment; -import android.provider.DocumentsContract; -import android.provider.MediaStore; +import android.provider.OpenableColumns; import android.util.Log; -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; - import com.truchisoft.jsonmanager.JsonManagerApp; import com.truchisoft.jsonmanager.data.FileData; import com.truchisoft.jsonmanager.data.FileType; import com.truchisoft.jsonmanager.data.StaticData; -import java.io.BufferedReader; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; import java.util.Date; import java.util.List; @@ -40,73 +25,143 @@ * Created by Maximiliano.Schmidt on 05/10/2015. */ public class FileUtils { - public static void AddFileToPrefs(File f, Uri uri) { - FileData fData = new FileData(); - fData.rawUri = uri.toString(); - fData.FileName = f.getAbsolutePath(); - fData.CreationDate = new Date(); - fData.FileType = FileType.Local; - if (!FileUtils.FileExists(uri)) - StaticData.getFiles().add(fData); - PrefManager.setFileData(JsonManagerApp.getContext(), StaticData.getFiles()); + + public static String getDisplayNameFromUri(Context context, Uri uri) { + if (uri == null) { + return "untitled.json"; // Or null, depending on desired error handling + } + String displayName = null; + if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(uri, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (nameIndex != -1) { + displayName = cursor.getString(nameIndex); + } + } + } catch (Exception e) { + // Log error, could be SecurityException or others + Log.e("FileUtils", "Error querying display name for content URI: " + uri, e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + if (displayName == null) { + displayName = uri.getLastPathSegment(); + } + // Basic cleanup for typical file name issues from lastPathSegment + if (displayName != null) { + int slashIndex = displayName.lastIndexOf('/'); + if (slashIndex != -1) { + displayName = displayName.substring(slashIndex + 1); + } + } + if (displayName == null || displayName.isEmpty()) { + displayName = "untitled.json"; // Default if everything else fails + } + return displayName; } - public static void AddPathToPrefs(String path, Uri uri) { + public static void AddUriToPrefs(Context context, String displayName, Uri uri) { + if (uri == null) return; FileData fData = new FileData(); - fData.FileName = path; fData.rawUri = uri.toString(); + fData.FileName = displayName; // Store the display name fData.CreationDate = new Date(); - fData.FileType = FileType.Local; - if (!FileUtils.FileExists(uri)) + fData.FileType = FileType.Local; // Or determine more accurately if possible + + // Ensure context is not null for PrefManager + Context appContext = (context != null) ? context.getApplicationContext() : JsonManagerApp.getContext(); + + if (!FileUtils.FileExists(uri)) { // FileExists checks StaticData StaticData.getFiles().add(fData); - PrefManager.setFileData(JsonManagerApp.getContext(), StaticData.getFiles()); + } else { + // Optional: Update existing FileData if found, e.g., timestamp or display name + for (FileData existingFd : StaticData.getFiles()) { + if (existingFd.rawUri.equals(uri.toString())) { + existingFd.FileName = displayName; // Update display name + existingFd.CreationDate = new Date(); // Update date + break; + } + } + } + PrefManager.setFileData(appContext, StaticData.getFiles()); } - public static void WriteToFile(Uri uri, byte[] data) { - FileOutputStream outputStream = null; - + public static boolean WriteToFile(Context context, Uri uri, byte[] data) { + if (uri == null || context == null) return false; + OutputStream outputStream = null; try { - Context ctx = JsonManagerApp.getContext(); - ctx.grantUriPermission(ctx.getPackageName(), uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - ctx.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - outputStream = new FileOutputStream(ctx.getContentResolver().openFileDescriptor(uri, "rwt").getFileDescriptor()); + // No need to call grantUriPermission here if permissions are handled by the caller (Activity/Fragment) + // The caller should ensure it has write permission, possibly through ACTION_CREATE_DOCUMENT or persisted permissions. + // context.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // Caller should manage this + outputStream = context.getContentResolver().openOutputStream(uri); + if (outputStream == null) { + Log.e("FileUtils", "Failed to open output stream for URI: " + uri); + return false; + } outputStream.write(data); - outputStream.close(); + return true; // Indicate success + } catch (FileNotFoundException e) { + Log.e("FileUtils", "File not found for URI (Write): " + uri, e); + return false; } catch (IOException e) { - Log.e("Exception", "File write failed: " + e.toString()); + Log.e("FileUtils", "IOException during write for URI: " + uri, e); + return false; + } catch (SecurityException e) { + Log.e("FileUtils", "SecurityException during write for URI: " + uri, e); + return false; + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + Log.e("FileUtils", "Error closing output stream for URI: " + uri, e); + } + } } } - public static String ReadFromResource(Uri uri) { - String ret = ""; + public static String ReadFromResource(Context context, Uri uri) { + if (uri == null || context == null) return ""; // Or null InputStream inputStream = null; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); try { - Context ctx = JsonManagerApp.getContext(); - ctx.grantUriPermission(ctx.getPackageName(), uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - ctx.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - inputStream = ctx.getContentResolver().openInputStream(uri); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - int i; - try { - i = inputStream.read(); - while (i != -1) { - byteArrayOutputStream.write(i); - i = inputStream.read(); - } - inputStream.close(); - } catch (IOException e) { - e.printStackTrace(); + // Similar to WriteToFile, caller should manage persistable permissions. + // context.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + Log.e("FileUtils", "Failed to open input stream for URI: " + uri); + return ""; // Or null + } + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead); } - ret = byteArrayOutputStream.toString(); } catch (FileNotFoundException e) { - Log.e("ReadFromFile", "File not found: " + e.toString()); + Log.e("FileUtils", "File not found for URI (Read): " + uri, e); + return ""; // Or null } catch (IOException e) { - Log.e("ReadFromFile", "Can not read file: " + e.toString()); + Log.e("FileUtils", "IOException during read for URI: " + uri, e); + return ""; // Or null } catch (SecurityException e) { - + Log.e("FileUtils", "SecurityException during read for URI: " + uri, e); + return ""; // Or null + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.e("FileUtils", "Error closing input stream for URI: " + uri, e); + } + } } - - return ret; + return byteArrayOutputStream.toString(); } public static boolean FileExists(Uri uri) { @@ -118,126 +173,4 @@ public static boolean FileExists(Uri uri) { } return vReturn; } - - public static String getRealPathFromURI(Context context, Uri uri) { - if (DocumentsContract.isDocumentUri(context, uri)) { - if (isExternalStorageDocument(uri)) { - String docId = DocumentsContract.getDocumentId(uri); - String[] split = docId.split(":"); - String type = split[0]; - if ("primary".equals(type)) { - if (split.length > 1) { - return Environment.getExternalStorageDirectory().toString() + "/" + split[1]; - } else { - return Environment.getExternalStorageDirectory().toString() + "/"; - } - } else { - return "storage" + "/" + docId.replace(":", "/"); - } - } else if (isDownloadsDocument(uri)) { - String fileName = getFilePath(context, uri); - if (fileName != null) { - return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName; - } - String id = DocumentsContract.getDocumentId(uri); - if (id.startsWith("raw:")) { - id = id.replaceFirst("raw:", ""); - File file = new File(id); - if (file.exists()) { - return id; - } - } - Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); - return getDataColumn(context, contentUri, null, null); - } else if (isMediaDocument(uri)) { - String docId = DocumentsContract.getDocumentId(uri); - String[] split = docId.split(":"); - String type = split[0]; - Uri contentUri = null; - switch (type) { - case "image": - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - break; - case "video": - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - break; - case "audio": - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - break; - } - String selection = "_id=?"; - String[] selectionArgs = new String[]{split[1]}; - return getDataColumn(context, contentUri, selection, selectionArgs); - } - } else if ("content".equals(uri.getScheme())) { - if (isGooglePhotosUri(uri)) { - return uri.getLastPathSegment(); - } else { - return getDataColumn(context, uri, null, null); - } - } else if ("file".equals(uri.getScheme())) { - return uri.getPath(); - } - return null; - } - - public static String getDataColumn(Context context, Uri uri, String selection, - String[] selectionArgs) { - Cursor cursor = null; - String column = "_data"; - String[] projection = new String[]{column}; - try { - if (uri == null) { - return null; - } - cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, - null); - if (cursor != null && cursor.moveToFirst()) { - int index = cursor.getColumnIndexOrThrow(column); - return cursor.getString(index); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - public static String getFilePath(Context context, Uri uri) { - Cursor cursor = null; - String[] projection = { - MediaStore.MediaColumns.DISPLAY_NAME - }; - try { - if (uri == null) return null; - cursor = context.getContentResolver().query(uri, projection, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - int index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME); - return cursor.getString(index); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - public static boolean isExternalStorageDocument(Uri uri) { - return "com.android.externalstorage.documents".equals(uri.getAuthority()); - } - - public static boolean isDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); - } - - public static boolean isMediaDocument(Uri uri) { - return "com.android.providers.media.documents".equals(uri.getAuthority()); - } - - public static boolean isGooglePhotosUri(Uri uri) { - return "com.google.android.apps.photos.content".equals(uri.getAuthority()); - } } -