-
Notifications
You must be signed in to change notification settings - Fork 298
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
Changes from 3 commits
61f44af
f0ccfa1
21c6bdf
c5e2bdd
4857d46
cdab029
5f8a42e
cb158b0
ade7ae0
0ea324a
7d93384
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import android.os.AsyncTask; | ||
import android.os.Bundle; | ||
import android.os.ParcelFileDescriptor; | ||
import android.webkit.MimeTypeMap; | ||
import android.widget.Toast; | ||
|
||
import androidx.appcompat.app.AlertDialog; | ||
|
@@ -26,6 +27,7 @@ | |
import com.automattic.simplenote.utils.AuthUtils; | ||
import com.automattic.simplenote.utils.BrowserUtils; | ||
import com.automattic.simplenote.utils.CrashUtils; | ||
import com.automattic.simplenote.utils.DateTimeUtils; | ||
import com.automattic.simplenote.utils.HtmlCompat; | ||
import com.automattic.simplenote.utils.PrefUtils; | ||
import com.simperium.Simperium; | ||
|
@@ -35,10 +37,17 @@ | |
import com.simperium.client.User; | ||
|
||
import org.json.JSONArray; | ||
import org.json.JSONException; | ||
import org.json.JSONObject; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.FileOutputStream; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.InputStreamReader; | ||
import java.lang.ref.WeakReference; | ||
import java.util.ArrayList; | ||
import java.util.Calendar; | ||
import java.util.Collections; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
|
@@ -66,6 +75,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Use | |
|
||
private static final int REQUEST_EXPORT_DATA = 9001; | ||
private static final int REQUEST_EXPORT_UNSYNCED = 9002; | ||
private static final int REQUEST_IMPORT_DATA = 9003; | ||
|
||
private Bucket<Preferences> mPreferencesBucket; | ||
private SwitchPreferenceCompat mAnalyticsSwitch; | ||
|
@@ -147,6 +157,18 @@ public boolean onPreferenceClick(Preference preference) { | |
} | ||
}); | ||
|
||
findPreference("pref_key_import").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { | ||
@Override | ||
public boolean onPreferenceClick(Preference preference) { | ||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); | ||
intent.addCategory(Intent.CATEGORY_OPENABLE); | ||
intent.setType("*/*"); | ||
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"text/*", "application/json"}); | ||
startActivityForResult(intent, REQUEST_IMPORT_DATA); | ||
return true; | ||
} | ||
}); | ||
|
||
findPreference("pref_key_export").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { | ||
@Override | ||
public boolean onPreferenceClick(Preference preference) { | ||
|
@@ -321,6 +343,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent resultData) | |
case REQUEST_EXPORT_UNSYNCED: | ||
exportData(resultData.getData(), true); | ||
break; | ||
case REQUEST_IMPORT_DATA: | ||
importData(resultData.getData()); | ||
break; | ||
} | ||
} | ||
|
||
|
@@ -456,6 +481,101 @@ public int compare(String text1, String text2) { | |
} | ||
} | ||
|
||
private void importData(Uri uri) { | ||
try { | ||
String exportContents = readFile(uri); | ||
switch (getFileExtension(uri)) { | ||
case "json": | ||
importJsonExport(exportContents); | ||
break; | ||
case "txt": | ||
importPlaintextFile(exportContents); | ||
break; | ||
case "md": | ||
importPlaintextFile(exportContents); | ||
break; | ||
default: | ||
Toast.makeText(requireContext(), getString(R.string.import_unknown), Toast.LENGTH_SHORT).show(); | ||
return; | ||
} | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. this catch is uncomfortably broad. can we separate the exceptions from There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Good, thank you too! |
||
} | ||
|
||
private void importPlaintextFile(String exportContents) { | ||
Simplenote currentApp = (Simplenote) requireActivity().getApplication(); | ||
Bucket<Note> noteBucket = currentApp.getNotesBucket(); | ||
Note note = noteBucket.newObject(); | ||
note.setContent(exportContents); | ||
note.setCreationDate(Calendar.getInstance()); | ||
note.setModificationDate(note.getCreationDate()); | ||
note.save(); | ||
} | ||
|
||
private void importJsonExport(String exportContents) throws Exception { | ||
JSONObject jsonData = new JSONObject(exportContents); | ||
JSONArray activeNotes = jsonData.getJSONArray("activeNotes"); | ||
JSONArray trashedNotes = jsonData.getJSONArray("trashedNotes"); | ||
List<String> trashedIds = new ArrayList<>(); | ||
JSONArray notesArray = new JSONArray(); | ||
for (int i = 0; i < activeNotes.length(); i++) { | ||
notesArray.put(activeNotes.getJSONObject(i)); | ||
} | ||
for (int j = 0; j < trashedNotes.length(); j++) { | ||
notesArray.put(trashedNotes.getJSONObject(j)); | ||
trashedIds.add(trashedNotes.getJSONObject(j).getString("id")); | ||
} | ||
for (int k = 0; k < notesArray.length(); k++) { | ||
JSONObject noteJson = notesArray.getJSONObject(k); | ||
noteFromJson(noteJson, trashedIds).save(); | ||
} | ||
} | ||
|
||
private Note noteFromJson(JSONObject noteJson, List<String> trashedIds) throws Exception { | ||
Simplenote currentApp = (Simplenote) requireActivity().getApplication(); | ||
Bucket<Note> noteBucket = currentApp.getNotesBucket(); | ||
Note note = noteBucket.newObject(); | ||
note.setContent(noteJson.has("content") ? noteJson.getString("content") : ""); | ||
note.setCreationDate(noteJson.has("creationDate") ? DateTimeUtils.getDateCalendar(noteJson.getString("creationDate")) : Calendar.getInstance()); | ||
note.setModificationDate(noteJson.has("lastModified") ? DateTimeUtils.getDateCalendar(noteJson.getString("lastModified")) : Calendar.getInstance()); | ||
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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. So, what about merge? 😀 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! |
||
return note; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looking at this now it does feel like it belongs in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
private List<String> jsonToList(JSONArray json) { | ||
List<String> list = new ArrayList<>(); | ||
for (int i = 0; i < json.length(); i++) { | ||
try { | ||
list.add(json.getString(i)); | ||
} catch (JSONException e) { | ||
e.printStackTrace(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 new Note(activeNotes.getObject(i)).save(); There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Sure, no problem! |
||
} | ||
} | ||
return list; | ||
} | ||
|
||
private String readFile(Uri uri) throws IOException { | ||
StringBuilder stringBuilder = new StringBuilder(); | ||
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri); | ||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); | ||
String line; | ||
while ((line = reader.readLine()) != null) { | ||
stringBuilder.append(line); | ||
stringBuilder.append("\n"); | ||
} | ||
inputStream.close(); | ||
return stringBuilder.toString(); | ||
} | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. these two might be great in a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Cool, thank you! 😀 |
||
|
||
private void trackSortOrder(String label) { | ||
AnalyticsTracker.track( | ||
AnalyticsTracker.Stat.SETTINGS_SEARCH_SORT_MODE, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
import android.text.format.DateFormat; | ||
import android.text.format.DateUtils; | ||
|
||
import java.text.ParseException; | ||
import java.text.SimpleDateFormat; | ||
import java.util.Calendar; | ||
import java.util.Locale; | ||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. this looks like an ISO8601 string. I see There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'll look for solutions! |
||
SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, Locale.getDefault()); | ||
Calendar date = Calendar.getInstance(); | ||
date.setTime(dateFormat.parse(json)); | ||
return date; | ||
} | ||
} |
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
andtext/*
, 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 forgetExtensionFromMimeType(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
toimportPlaintextFile(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 curioustrue
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 flagThere 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