Skip to content

Commit

Permalink
Convert Bundle to LargeObjectStorage
Browse files Browse the repository at this point in the history
To stop TransactionTooLargeException
  • Loading branch information
david-allison committed Apr 20, 2020
1 parent 07a0c30 commit dd8aafe
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 5 deletions.
12 changes: 9 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import com.ichi2.anki.widgets.PopupMenuWithIcons;
import com.ichi2.utils.AdaptionUtil;
import com.ichi2.utils.DeckComparator;
import com.ichi2.utils.LargeObjectStorage;
import com.ichi2.utils.NamedJSONComparator;
import com.ichi2.widget.WidgetStatus;

Expand Down Expand Up @@ -204,6 +205,8 @@ public class NoteEditor extends AnkiActivity {
// restoring the Activity.
private Bundle mSavedFields;

LargeObjectStorage mLargeObjectStorage = new LargeObjectStorage(this);

private CollectionTask.TaskListener mSaveNoteHandler = new CollectionTask.TaskListener() {
private boolean mCloseAfter = false;
private Intent mIntent;
Expand Down Expand Up @@ -1181,19 +1184,22 @@ private void setVisualEditorListener(ImageButton visualEditorButton, final int i
Timber.i("NoteEditor:: Multimedia button pressed for field %d", index);
try {
final Collection col = CollectionHelper.getInstance().getCol(NoteEditor.this);

// If the field already exists then we start the field editor, which figures out the type
// automatically
IField field = new TextField();
String value = mEditFields.get(index).getText().toString();
field.setFormattedString(col, value);
Intent i = new Intent(this, VisualEditorActivity.class);
i.putExtra(VisualEditorActivity.EXTRA_FIELD, field);
i.putExtra(VisualEditorActivity.EXTRA_ALL_FIELDS, mEditorNote.getFields());
//Note: Intent.getExtras is a copy of the bundle.
Bundle b = new Bundle();
mLargeObjectStorage.storeSingleInstance(VisualEditorActivity.STORAGE_CURRENT_FIELD.asData(field), b);
mLargeObjectStorage.storeSingleInstance(VisualEditorActivity.STORAGE_EXTRA_FIELDS.asData(mEditorNote.getFields()), b);
i.replaceExtras(b);
i.putExtra(VisualEditorActivity.EXTRA_MODEL_ID, getModelId());
i.putExtra(VisualEditorActivity.EXTRA_FIELD_INDEX, index);
startActivityForResultWithoutAnimation(i, REQUEST_VISUAL_EDIT);
} catch (Exception e) {
Timber.e(e, "Unable to open Visual Editor");
UIUtils.showThemedToast(this, "Unable to open Visual Editor", false);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import com.ichi2.libanki.Utils;
import com.ichi2.utils.AssetReader;
import com.ichi2.utils.JSONObject;
import com.ichi2.utils.LargeObjectStorage;
import com.ichi2.utils.LargeObjectStorage.StorageKey;
import com.ichi2.utils.WebViewDebugging;
import com.jaredrummler.android.colorpicker.ColorPickerDialog;
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener;
Expand Down Expand Up @@ -71,6 +73,16 @@ public class VisualEditorActivity extends AnkiActivity implements ColorPickerDia
/** All fields in a (string[]) */
public static final String EXTRA_ALL_FIELDS = "visual.card.ed.extra.all.fields";

public static final StorageKey<IField> STORAGE_CURRENT_FIELD = new StorageKey<>(
"visual.card.ed.extra.current.field",
"visualed_current_field",
"bin");

public static final StorageKey<String[]> STORAGE_EXTRA_FIELDS = new StorageKey<>(
"visual.card.ed.extra.extra.fields",
"visualed_extra_fields",
"bin");


private String mCurrentText;
private IField mField;
Expand All @@ -83,6 +95,7 @@ public class VisualEditorActivity extends AnkiActivity implements ColorPickerDia
private AssetReader mAssetReader = new AssetReader(this);
//Unsure if this is needed, or whether getCol will block until onCollectionLoaded completes.
private boolean mHasLoadedCol;
private LargeObjectStorage mLargeObjectStorage = new LargeObjectStorage(this);


@Override
Expand Down Expand Up @@ -370,10 +383,10 @@ private boolean setFieldsOnStartup() {
return false;
}

mField = (IField) extras.getSerializable(VisualEditorActivity.EXTRA_FIELD);
mField = mLargeObjectStorage.getSingleInstance(STORAGE_CURRENT_FIELD, extras);
Integer index = (Integer) extras.getSerializable(VisualEditorActivity.EXTRA_FIELD_INDEX);

this.mFields = (String[]) extras.getSerializable(VisualEditorActivity.EXTRA_ALL_FIELDS);
this.mFields = mLargeObjectStorage.getSingleInstance (STORAGE_EXTRA_FIELDS, extras);
Long modelId = (Long) extras.getSerializable(VisualEditorActivity.EXTRA_MODEL_ID);

if (mField == null) {
Expand Down
158 changes: 158 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/utils/LargeObjectStorage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
Copyright (c) 2020 David Allison <davidallisongithub@gmail.com>
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.utils;

import android.content.Context;
import android.os.Bundle;

import com.ichi2.compat.CompatHelper;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import androidx.annotation.Nullable;
import timber.log.Timber;

/**
* Class to allow the transfer of large data between activities (currently) using a static cache file.
*
* This is due to the data limit on a Bundle (TransactionTooLargeException) after ~500KB+ of data.
*
* Using a single file pre data item means we will not excessively use cached data.
*/
public class LargeObjectStorage {
private final Context mContext;

public LargeObjectStorage(Context context) {
mContext = context;
}

public <T extends Serializable> void storeSingleInstance(StorageData<T> data, Bundle bundle) throws IOException {
Timber.i("Setting '%s'", data.getBundleKey());
bundle.putString(data.getBundleKey(), "1");
writeFile(data);
}

private <T extends Serializable> void writeFile(StorageData<T> data) throws IOException {
ByteArrayInputStream inputStream = null;
try (ByteArrayOutputStream source = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(source)) {

objectOutputStream.writeObject(data.getData());
objectOutputStream.flush();
objectOutputStream.close();

inputStream = new ByteArrayInputStream(source.toByteArray());
File outputFile = new File(mContext.getCacheDir(), data.getFileName() + "." + data.getExtension());
CompatHelper.getCompat().copyFile(inputStream, outputFile.getAbsolutePath());
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
//ignore
}
}
}
}


@Nullable
public <T extends Serializable> T getSingleInstance(StorageKey<T> key, Bundle bundle) {
if (bundle.getString(key.getBundleKey(), null) == null) {
Timber.i("No data in bundle for key %s", key.getBundleKey());
return null;
}

File outputFile = new File(mContext.getCacheDir(), key.getFileName() + "." + key.getExtension());

try (
FileInputStream fis = new FileInputStream(outputFile.getAbsolutePath());
ObjectInputStream ois = new ObjectInputStream(fis)) {
//noinspection unchecked
return (T) ois.readObject();
} catch (Exception e) {
Timber.e(e, "Error deserializing field");
return null;
}
}


public static class StorageData<T extends Serializable> {
private final StorageKey<T> mKey;
private final T mData;

public StorageData(StorageKey<T> storageKey, T data) {
this.mKey = storageKey;
this.mData = data;
}

public String getBundleKey() {
return mKey.getBundleKey();
}

public T getData() {
return mData;
}

public String getFileName() {
return mKey.mFileName;
}

public String getExtension() {
return mKey.mExtension;
}
}

public static class StorageKey<T extends Serializable> {
private final String mBundleKey;
private final String mFileName;
private final String mExtension;


public StorageKey(String mBundleKey, String mFileName, String mExtension) {
this.mBundleKey = mBundleKey;
this.mFileName = mFileName;
this.mExtension = mExtension;
}


public StorageData<T> asData(T data) {
return new StorageData<>(this, data);
}


public String getFileName() {
return mFileName;
}

public String getExtension() {
return mExtension;
}


public String getBundleKey() {
return mBundleKey;
}
}
}

0 comments on commit dd8aafe

Please # to comment.