Skip to content
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

Merged
merged 11 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
131 changes: 131 additions & 0 deletions Simplenote/src/main/java/com/automattic/simplenote/Importer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.automattic.simplenote;

import android.net.Uri;

import androidx.fragment.app.Fragment;

import com.automattic.simplenote.models.Note;
import com.automattic.simplenote.models.Tag;
import com.automattic.simplenote.utils.FileUtils;
import com.automattic.simplenote.utils.TagUtils;
import com.simperium.client.Bucket;
import com.simperium.client.BucketObjectNameInvalid;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;

public class Importer {
private Bucket<Note> mNotesBucket;
private Bucket<Tag> mTagsBucket;

public Importer(Simplenote simplenote) {
mNotesBucket = simplenote.getNotesBucket();
mTagsBucket = simplenote.getTagsBucket();
}

public static void fromUri(Fragment fragment, Uri uri) throws ImportException {
try {
new Importer((Simplenote) fragment.getActivity().getApplication())
.dispatchFileImport(
FileUtils.getFileExtension(fragment.requireContext(), uri),
FileUtils.readFile(fragment.requireContext(), uri)
);
} catch (IOException e) {
throw new ImportException(FailureReason.FileError);
}
}

private void dispatchFileImport(String fileType, String content) throws ImportException {
switch (fileType) {
case "json":
importJsonFile(content);
break;
case "md":
importMarkdown(content);
break;
case "txt":
importPlaintext(content);
break;
default:
throw new ImportException(FailureReason.UnknownExportType);
}
}

private void importPlaintext(String content) {
addNote(Note.fromContent(mNotesBucket, content));
}

private void importMarkdown(String content) {
Note note = Note.fromContent(mNotesBucket, content);
note.enableMarkdown();

addNote(note);
}

private void addNote(Note note) {
for (String tagName : note.getTags()) {
try {
TagUtils.createTagIfMissing(mTagsBucket, tagName);
} catch (BucketObjectNameInvalid e) {
// if it can't be added then remove it, we can't keep it anyway
note.removeTag(tagName);
}
}

note.save();
}

private void importJsonFile(String content) throws ImportException {
try {
importJsonExport(new JSONObject(content));
} catch (JSONException | ParseException e) {
throw new ImportException(FailureReason.ParseError);
}
}

private void importJsonExport(JSONObject export) throws JSONException, ParseException {
JSONArray activeNotes = export.optJSONArray("activeNotes");
JSONArray trashedNotes = export.optJSONArray("trashedNotes");

ArrayList<Note> notesList = new ArrayList<>();

for (int i = 0; activeNotes != null && i < activeNotes.length(); i++) {
Note note = Note.fromExportedJson(mNotesBucket, activeNotes.getJSONObject(i));
notesList.add(note);
}

for (int j = 0; trashedNotes != null && j < trashedNotes.length(); j++) {
Note note = Note.fromExportedJson(mNotesBucket, trashedNotes.getJSONObject(j));
note.setDeleted(true);

notesList.add(note);
}

for (Note note : notesList) {
addNote(note);
}
}

public enum FailureReason {
FileError,
UnknownExportType,
ParseError
}

public static class ImportException extends Exception {
private FailureReason mReason;

ImportException(FailureReason reason) {
mReason = reason;
}

public FailureReason getReason() {
return mReason;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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;
Expand Down Expand Up @@ -121,7 +122,7 @@ public boolean onPreferenceClick(Preference preference) {
try {
BrowserUtils.launchBrowserOrShowError(requireContext(), "https://simplenote.com/help");
} catch (Exception e) {
Toast.makeText(getActivity(), R.string.no_browser_available, Toast.LENGTH_LONG).show();
toast(R.string.no_browser_available, Toast.LENGTH_LONG);
}
return true;
}
Expand All @@ -133,7 +134,7 @@ public boolean onPreferenceClick(Preference preference) {
try {
BrowserUtils.launchBrowserOrShowError(requireContext(), "http://simplenote.com");
} catch (Exception e) {
Toast.makeText(getActivity(), R.string.no_browser_available, Toast.LENGTH_LONG).show();
toast(R.string.no_browser_available, Toast.LENGTH_LONG);
}
return true;
}
Expand All @@ -147,6 +148,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) {
Expand Down Expand Up @@ -310,7 +323,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent resultData)
}

if (resultData.getData() == null) {
Toast.makeText(requireContext(), getString(R.string.export_message_failure), Toast.LENGTH_SHORT).show();
toast(R.string.export_message_failure);
return;
}

Expand All @@ -321,6 +334,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;
}
}

Expand Down Expand Up @@ -447,12 +463,31 @@ public int compare(String text1, String text2) {
FileOutputStream fileOutputStream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
fileOutputStream.write(account.toString(2).replace("\\/","/").getBytes());
parcelFileDescriptor.close();
Toast.makeText(requireContext(), getString(R.string.export_message_success), Toast.LENGTH_SHORT).show();
toast(R.string.export_message_success);
} else {
Toast.makeText(requireContext(), getString(R.string.export_message_failure), Toast.LENGTH_SHORT).show();
toast(R.string.export_message_failure);
}
} catch (Exception e) {
Toast.makeText(requireContext(), getString(R.string.export_message_failure), Toast.LENGTH_SHORT).show();
toast(R.string.export_message_failure);
}
}

private void importData(Uri uri) {
try {
Importer.fromUri(this, uri);
toast(R.string.import_message_success);
} catch (Importer.ImportException e) {
switch (e.getReason()) {
case FileError:
toast(R.string.import_error_file);
break;
case ParseError:
toast(R.string.import_error_parse);
break;
case UnknownExportType:
toast(R.string.import_unknown);
break;
}
}
}
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thank you! 😀


Expand Down Expand Up @@ -533,4 +568,12 @@ public void onClick(DialogInterface dialogInterface, int i) {
}
}
}

private void toast(int stringId) {
toast(stringId, Toast.LENGTH_SHORT);
}

private void toast(int stringId, int length) {
Toast.makeText(requireContext(), getString(stringId), length).show();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@
import androidx.annotation.NonNull;

import com.automattic.simplenote.R;
import com.automattic.simplenote.utils.DateTimeUtils;
import com.automattic.simplenote.utils.TagUtils;
import com.simperium.client.Bucket;
import com.simperium.client.BucketObject;
import com.simperium.client.BucketSchema;
import com.simperium.client.Query;
import com.simperium.client.Query.ComparisonType;
import com.simperium.util.Uuid;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
Expand Down Expand Up @@ -62,9 +66,12 @@ public class Note extends BucketObject {
protected String mTitle = null;
protected String mContentPreview = null;

public Note() {
this(Uuid.uuid());
}

public Note(String key) {
super(key, new JSONObject());
this(key, new JSONObject());
}

public Note(String key, JSONObject properties) {
Expand Down Expand Up @@ -99,6 +106,27 @@ public static Query<Note> allWithNoTag(Bucket<Note> noteBucket) {
.where(TAGS_PROPERTY, ComparisonType.EQUAL_TO, null);
}

public static Note fromContent(Bucket<Note> notesBucket, String content) {
Note note = notesBucket.newObject();
note.setContent(content);
note.setCreationDate(Calendar.getInstance());
note.setModificationDate(note.getCreationDate());

return note;
}

public static Note fromExportedJson(Bucket<Note> notesBucket, JSONObject noteJson) throws JSONException, ParseException {
Note note = notesBucket.newObject();
note.setContent(noteJson.optString("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") ? noteJson.getJSONArray("tags") : new JSONArray());
note.setPinned(noteJson.optBoolean("pinned", false));
note.setMarkdownEnabled(noteJson.optBoolean("markdown", false));

return note;
}

@SuppressWarnings("unused")
public static String dateString(Number time, boolean useShortFormat, Context context) {
Calendar c = numberToDate(time);
Expand Down Expand Up @@ -314,10 +342,27 @@ public List<String> getTags() {
return tagList;
}

public void removeTag(String removedTagName) {
JSONArray tags = new JSONArray();
String removedHash = TagUtils.hashTag(removedTagName);

for (String tagName : getTags()) {
if (!TagUtils.hashTag(tagName).equals(removedHash)) {
tags.put(tagName);
}
}

setTags(tags);
}

public void setTags(List<String> tags) {
setProperty(TAGS_PROPERTY, new JSONArray(tags));
}

public void setTags(JSONArray tags) {
setProperty(TAGS_PROPERTY, tags);
}

/**
* String of tags delimited by a space
*/
Expand Down Expand Up @@ -405,6 +450,10 @@ public boolean isMarkdownEnabled() {
return hasSystemTag(MARKDOWN_TAG);
}

public void enableMarkdown() {
setMarkdownEnabled(true);
}

public void setMarkdownEnabled(boolean isMarkdownEnabled) {
if (isMarkdownEnabled) {
addSystemTag(MARKDOWN_TAG);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'";
Copy link
Contributor

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?

Copy link
Contributor Author

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!

SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, Locale.getDefault());
Calendar date = Calendar.getInstance();
date.setTime(dateFormat.parse(json));
return date;
}
}