New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add import data #1333
Add import data #1333
Conversation
// note.setPublished(?); | ||
break; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the note data structure has a relatively fixed form. do we need to iterate over all the keys? I think we'll want all these fields and directly assigning them would be clearer and terser. that is, we don't really want to use a note if it doesn't contain content
but this iteration+switch is happy to do just that.
note.setContent(noteJson.has("content") ? noteJson.getString("content") : "");
note.setCreationDate(noteJson.has("creationDate") ? DateTimeUtils.getDateCalendar(noteJson.getString("creationDate")) : now());
could be nice to move this into a new function such as noteFromJson(JSONValue json, List<String> trashedIds)
and then it'd be easier to test in isolation as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First wrote it like this, but then found a solution with an iterator better. I'll change it! And where should I place the noteFromJson(JSONValue json, List<String> trashedIds)
method? Ok if I leave it in a PreferencesFragment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. You might play around with it some and tell us what you think. I could see it having a good home in Note
as well. That would bring it closer to the rest of the note semantics/models.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, thank you, I will think about it.
} | ||
note.save(); | ||
} | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we move these different decoders into their own methods? that will leave the top-level dispatch looking clear and the separate decoders more specific
String exportContents = readFile(uri);
switch (getFileExtension(uri)) {
case "json":
return importJsonExport(exportContents);
case "txt":
return importPlaintextFile(exportContents);
case "md":
return importPlaintextFile(exportContents);
default:
Toast.makeText(…"unknown file format"…);
}
after this change it'll be easier to isolate those exception domains vs. having this broad catch-all
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, I will change this too
Note note = noteBucket.newObject(); | ||
note.setContent(noteContent); | ||
note.setCreationDate(Calendar.getInstance()); | ||
note.setModificationDate(note.getCreationDate()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have to check, but I believe that we'll want to fill in default values, such as an empty list of tags and system tags
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I thought that these fields are already filled by default when creating the note object, so I figured it might look like extra code. Should I add filling tags?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if they already get defaults it should be fine. looks like you beat me to confirming that 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😁 I'll check again just in case
final MimeTypeMap mime = MimeTypeMap.getSingleton(); | ||
extension = mime.getExtensionFromMimeType(requireContext().getContentResolver().getType(uri)); | ||
} else { | ||
extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(new File(uri.getPath())).toString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does Android do anything to more accurately estimate the MIME type from a document? if that's the case, if it does more than look at the extension, would it be more helpful if we used that vs. relying on the extension? or a combination of both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think I found a better solution, I'll fix it!
@@ -37,4 +38,12 @@ public static String getDateTextString(Context context, Calendar calendar) { | |||
); | |||
return new SimpleDateFormat(pattern, Locale.getDefault()).format(calendar.getTime()); | |||
} | |||
|
|||
public static Calendar getDateCalendar(String json) throws ParseException { | |||
String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks like an ISO8601 string. I see parseOffsetISO8601
in SDK 24 which I guess means we can't use that here. I looked briefly through androidx
and didn't see one. do you know if there's an existing ISO8601 parser available in one of the compatibility or other libraries we already use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll look for solutions!
@theo-jedi this is a great addition and I know people will love being able to import notes or accounts on their Android device. we're a bit backed up especially with Android so I apologize for our delays, which will be longer than usual. thanks for your contribution! I want to make sure we get you the proper attention on this so we can get it in, so please feel free to ping if we don't respond quickly enough. |
Of course, I understand, thanks a lot, I will try to make corrections as soon as possible. |
Now it looks much better, thank you! Unfortunately, I couldn't find better solution about ISO8601 string, but everything else seems to be fixed. |
} | ||
for (int k = 0; k < notesArray.length(); k++) { | ||
JSONObject noteJson = notesArray.getJSONObject(k); | ||
noteFromJson(noteJson, trashedIds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be preferable if this function returned a note (for testing and independence etc…). if we made that change this line would become
noteFromJson(noteJson, trashedIds).save();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I'll do it now
Done, thank you! 😀 Anything else? |
note.setTags(noteJson.has("tags") ? jsonToList(noteJson.getJSONArray("tags")) : new ArrayList<String>()); | ||
note.setPinned(noteJson.has("pinned") && noteJson.getBoolean("pinned")); | ||
note.setMarkdownEnabled(noteJson.has("markdown") && noteJson.getBoolean("markdown")); | ||
note.setDeleted(noteJson.has("id") && trashedIds.contains(noteJson.getString("id"))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not going to import any chords, tracks, or photos? 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😁 Thank you for your attention to my project! I really like Simplenote, that's why I took it as a basis. I use it every day, thank you very much for your work, I am very grateful to you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, what about merge? 😀
Toast.makeText(requireContext(), getString(R.string.import_message_success), Toast.LENGTH_SHORT).show(); | ||
} catch (Exception e) { | ||
Toast.makeText(requireContext(), getString(R.string.import_message_failure), Toast.LENGTH_SHORT).show(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this catch is uncomfortably broad. can we separate the exceptions from readFile
from the exceptions from the importers? additionally, we have an AppLog
that would be useful here for debugging issues that appear during the import. if you aren't sure how to add that we can do that. let's add a log when starting the import, indicating which type, and when it succeeds and fails. it would either fit as type ACTION
or we might create a new type, IMPORT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I add exception handling. I think it's better to add the logs to you, because I don't know how to do it most correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we'll take care of them, thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, thank you too!
note.setMarkdownEnabled(noteJson.has("markdown") && noteJson.getBoolean("markdown")); | ||
note.setDeleted(noteJson.has("id") && trashedIds.contains(noteJson.getString("id"))); | ||
return note; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking at this now it does feel like it belongs in Note.java
more than it does here. would you be willing to move it into that module and then import it? if now we can do that before merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is much better, but I want to note that I move it there as a static method.
note.setTags(noteJson.has("tags") ? jsonToList(noteJson.getJSONArray("tags")) : new ArrayList<String>()); | ||
note.setPinned(noteJson.has("pinned") && noteJson.getBoolean("pinned")); | ||
note.setMarkdownEnabled(noteJson.has("markdown") && noteJson.getBoolean("markdown")); | ||
note.setDeleted(noteJson.has("id") && trashedIds.contains(noteJson.getString("id"))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is going to have some issues as I don't know that we can or should rely on imported notes containing an id. I believe that it's not really necessary anyway, more a byproduct of the way we've chosen to build an intermediate list of notes on import.
instead we could traverse each category of notes once and avoid needing to pass in List<String> trashedIds
. there are a couple ways we can convey the trashed status but one easy way to do it would having to think about it much is to manually alter the JSON or the output note.
for (int i = 0; i < activeNotes.length(); i++) {
noteFromJson(activeNotes.getJSONObject(i)).save();
}
for (int i = 0; i < trashedNotes.length(); i++) {
Note note = noteFromJson(trashedNotes.getJSONObject(i));
note.setDeleted(true);
note.save();
}
(edit: corrected typo from notesArray
to activeNotes
/trashedNotes
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I had doubts about this piece of code, so much better, thanks!
try { | ||
list.add(json.getString(i)); | ||
} catch (JSONException e) { | ||
e.printStackTrace(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we'll want to match our error logging with the rest of the app vs. dumping a stack trace.
in addition, it looks like the Note
class stores its list of tags as a JSONArray
so we could expose a new method for setTags(JSONArray tags)
and skip the encode/decode loop.
on this point, are we able to directly instantiate a note with the existing Note class, without conversion? I doubt it works 100% but the constructor can take a JSON object. it may be the case that we can perform minimal transformation and/or slightly modify Note.java
so we can go straight from the imported JSON object to a note.
new Note(activeNotes.getObject(i)).save();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed tags adding, but I doubt about the note. In theory, we can use the following code:
Simplenote currentApp = (Simplenote) activity.getApplication();
Bucket<Note> noteBucket = currentApp.getNotesBucket();
Note note = new Note("note - " + UUID.randomUUID(), activeNotes.getJSONObject(i));
noteBucket.add(note);
note.save();
It looks much better, but I doubt this is a good idea, since I am not sure how correctly the Ыimperium alogyrythms will work in this case. It seems to me that it is preferable to use noteBucket.newObject();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll leave this as it was for now, and then we'll decide!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks. I might need to consult with a few people on this. please pardon the delay, but expect a few days gap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, no problem!
|
||
public String getFileExtension(Uri uri) { | ||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(requireContext().getContentResolver().getType(uri)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these two might be great in a FileUtils
class in the utils
folder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Wanted to do it but was afraid to create new files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all good. I know that fear, especially entering an open-source repo for new contributions.
we'll let you know if we want something to change, or we'll just end up changing it on merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, thank you! 😀
private void importData(Uri uri) { | ||
try { | ||
String exportContents = readFile(uri); | ||
switch (getFileExtension(uri)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried confirming the return values for getType(uri)
and didn't see anything helpful. did you see a list? I saw that it does chop off the first part of the mime type.
we probably want to add unit tests to cover this function for the expected file types we will support importing. can it really suggest md
as the extension? I guess if it detects plaintext and has .md
?
this is a nice update from before though. I like that there's some leeway if someone has a file with a missing or wrong file extension.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I misunderstood, but I guess it works a little differently. It seems to me that it makes no difference to us what extension the file is, except for json, of course. All the rest will definitely be text (this is exactly what mimes are for), because I set mime types application/json
and text/*
, which includes txt, md, html and other, but there's no difference for us between them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a list (section "common examples"): en.wikipedia.org/wiki/Media_type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my point was that I don't see how it's possible here to ever match on "md"
, unless Android is making a guess on that which I don't think it would?
there is a distinction that's possible, though we could handle all text/plain
files the same way, but that distinction is whether or not we toggle markdown support in the note. if we happen to know it's a .md
file and it's plaintext then why not go ahead and import it with markdown enabled. otherwise, if it's plaintext import it just as raw text.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I understand, I apologize for what you had to explain. However, unfortunately, I still did not understand what the problem was with the written code, I added the method getFileExtension(Context context, Uri uri)
that gets the file extension from uri. getType(uri)
returns not an extension, but a mimetype, which I'm using for getExtensionFromMimeType(mimeType)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here's the central question: can getExtensionFromMimeType(mimeType)
ever return "md"
?
the code is written as if it can, but I want to make sure that's the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, of course I checked!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, should I add a parameter boolean isMarkdownEnabled
to importPlaintextFile(String exportContents)
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I think so, or possibly better a string or enum WithMarkdown | WithoutMarkdown
to avoid dangling and curious true
values. I don't foresee any other options we'd want to pass in there such as tags or share urls so it should be good to introduce this one flag
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, did! It seemed to me, that enum will be better than a string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your continued work and patience. I ended up leaving more comments than I anticipated, but there were a few things I wanted to make sure we covered. We will also be performing testing on this as well as the merge. I don't know that it's totally ready for that now, but it looks fairly solid and after a few details are ironed out I think we'll be good.
Sure! Thank you, understood, I'll do it! |
Pushed the current changes, I'm waiting for decisions on other issues 😃 |
Thank you very much for your work again! |
Good afternoon, is there any news? 😀 |
@theo-jedi no news, apologies. will try and check in again next week. we haven't forgotten about you though. |
All good, don't worry, I understand, I'll wait, thank you! |
JSONArray trashedNotes = export.optJSONArray("trashedNotes"); | ||
|
||
for (int i = 0; activeNotes != null && i < activeNotes.length(); i++) { | ||
addNote(activeNotes.getJSONObject(i)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry for the error in my branch. by the way, won't the error still occur here on account of the fact that the export format is different than the Simperium data model? we can't shove in invalid data…
for example, the lines loading the time formats were there to convert to the internal representations (unless it was those representations that were wrong, as I know that Simplenote stores the data as seconds-since-epoch).
I like simplifying the code but I don't think it's a good direction to skip parsing - we'll just be asking for data corruption and crashes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, of course I understand... The problem is that this method simply creates an object with the given parameters. We expect the markdown field to be a boolean, but the user can replace it with a string and this will crash the application. Now I'll try to roll back the changes
Something like that. It works pretty well for me, what do you think? Do we need to do something else? Maybe it's time to add an |
You can test the changes on this Pull Request by downloading the APK here. |
Yeah @theo-jedi the code is looking good. I wish we didn't have to send the notes bucket into At this point most of the finishing work will likely be on our side. I'll try and figure out best how to do that since I haven't done it before, whether that's us merging onto this branch or merging this into one of ours. |
Great, thanks for the experience, it was a pleasure working with you! 😊 I suggest to send note object into Hope everything goes well, thank you so much again, keep in touch. |
that's something I considered as well, but then it seemed to be even more confusing of an interface than passing in the bucket itself. I think this butts up against the way this app interacts with the Simperium library and so I don't think we need to get hung up here in this PR on it. thanks for being open to change and feedback though!
Same back to you! |
Okay, got it! Thank you again! |
This all looks great, thanks @theo-jedi. UI is perfectly done.
If I can make a couple of suggestions, I think we could make the messages more user-friendly:
Our general error copy convention is "Cannot [action]":
Hopefully the above accurately represent what the error actually is, if not feel free to suggest something. Could you also clarify what would trigger
Echoing @sandymcfadden. Tags should be kept for trashed tags. |
Thank you so much! 😊
Yes, of course, I'll do it now!
Oh sorry, I forgot to remove this line, it was previously used instead of
Yes, @dmsnell and I came to the conclusion that the tags should be keeped for trashed notes, that's how it is now |
Good evening, let me know when you're ready to merge. If I need to make any other final edits (or any actions with git), I'm here 😉 |
Good afternoon, have any news? |
unfortunately not. I'm doing too many things simultaneously. this is a great contribution; it's all on us to get it moving faster. thanks for your patience. |
Green light from me design-wise. Thanks for the updates @theo-jedi |
Sure, all good! I just got worried because there was no response to the latest changes. Sorry for the unnecessary worry, I will wait, good luck with your work, and thank you! |
Great. Thank you for your attention to this code! 😊 |
Status update for you @theo-jedi:
|
Hey, good day @dmsnell! What about working on the merge of this pull request? Is it still not available? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @theo-jedi, I am going to merge this PR. Thank you for the implementation of this feature. I will be adding and checking a few details to make sure the feature is complete before release. I have created issue #1417 to keep track of the changes that need to be added.
Great, thank you so much @danilo04! I was really glad to help, very nice! 😊 |
Import feature
Added a function to import notes from json and plaintext