I get above mentioned error on some devices (very rarely, 2 times until now only):
android.database.sqlite.SQLiteDiskIOException: disk I/O error (code 3850)
at android.database.sqlite.SQLiteConnection.nativeExecuteForString(Native Method)
at android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:679)
at android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:361)
at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:236)
at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:200)
at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:463)
at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:185)
at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:177)
at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:806)
at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:791)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:694)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:669)
at ***.a(***.java:115)
The line 115 in my code is following:
// here the exception occurs
SQLiteDatabase db = SQLiteDatabase.openDatabase(pathApp, null, SQLiteDatabase.OPEN_READONLY);
// ...
db.close();
Story
What I do is following:
app has root rights
it copies the database from another app into it's own directory
it tries to open this database and read some data
it close the file again
That's all. This is working on thousands of devices. My app for sure only accesses the database at on place, I'm completely sure about that
Question
Does anyone know what could cause this problem?
Maybe interesting facts
the 2 devices are a OnePlus2
one guy told me that the problem occurred after updating to Oxygen 2.1
I came across a similar problem recently where my app runs fine on all Android devices except the OnePlus Two running OxygenOS 2.1.
After looking into the issue, it appears this specific combination is very sensitive and crashes on a database lock of some sort. In my case, my app was copying a new database to the device on the first run then checking the database version to see if it requires an update. On the check it crashed on these devices. I realized in my code I am opening a separate instance to the database instead of using the currently opened database when checking the version. After changing the code to avoid opening a separate instance of the database, the app stopped crashing on the OnePlus.
My advice is to Not open multiple instances of the database in your app or try closing the database first before opening it again (perhaps your copy method didn't close the database or is using another instance of the same database).
Just encountered this in development. It triggered when the phone ran out of batteries and shut down. Presumably, you just have to be grabbing the database lock when that happens.
Here's the working solution (already tested in production for 2 months now or so) and this works on the OnePlus2 and on Android 6 as well:
add the sqlite binaries to your app (can be found here: http://forum.xda-developers.com/showthread.php?t=2730422). Just put them into your apps assets folder
Then in your app, try to read an external database like following:
Find out which binary is working. I tried a few methods (via calling /proc/cpuinfofor example) but could not find a reliable solution, so I do it via trial and error like following:
First I copy the binary from my assets folder to my apps files folder
Then I try to dump the desired database via <path_to_SQLite3_bnary> <path_to_database> \.dump '<tablename>'\n and read the result
I check the result, if it contains error: I know the binary is not working, if it starts with INSERT INTO I know it works
I repeat this with my binaries until I found a working binary
Then I just read the result. The result will contain an error or be some csv similar content. I either handle the error or convert every line to valid csv via row.replace("INSERT INTO \"" + tablename + "\" VALUES(", "").replace(");", ""), so that I get the content in a csv format. I use opencsv's CSVReader to parse the data then...
That's it, this works (until know) on all devices without a problem
Code - copied from my sources, adopt it a little bit to fit your needs
public static List<String> readCSVData(String pathDatabase, String tableName) throws InterruptedException, TimeoutException, RootDeniedException, IOException
{
List<String> res = null;
List<String> csvData = null;
final String[] architectures = new String[]{
"armv7",
"armv7-pie",
"armv6",
"armv6-nofpu"
};
for (int i = 0; i < architectures.length; i++)
{
res = readDatabase(architectures[i], pathDatabase, tableName, rootMethod);
if (res.toString().contains("[error:"))
{
L.d(RootNetworkUtil.class, "Trial and Error - ERROR: " + architectures[i]);
}
else
{
int maxLength = (res.toString().length() < 100) ? res.toString().length() : 100;
L.d(RootNetworkUtil.class, "Trial and Error - RESULT: " + res.toString().substring(0, maxLength));
L.d(RootNetworkUtil.class, "Architecture found via trial and error: " + architectures[i]);
csvData = res;
break;
}
}
return csvData;
}
private static List<String> readDatabase(String architecture, String pathDB, String tablename) throws InterruptedException, TimeoutException, RootDeniedException, IOException {
String sqlite = "sqlite3." + architecture;
String pathSQLite3 = getSQLitePath(architecture, tablename);
// OWN class, just copy the file from the assets to the sqlite3 path!!!
AssetUtil.copyAsset(sqlite, pathSQLite3);
String[] cmd = new String[]{
"su\n",
//"chown root.root " + pathSQLite3 + "\n",
"chmod 777 " + pathSQLite3 + "\n",
pathSQLite3 + " " + pathDB + " \".dump '" + tablename + "'\"\n"
};
List<String> res = new ArrayList<>();
List<String> temp = RootUtils.execute(cmd);
for (int i = 0; i < temp.size(); i++)
{
// Fehlerzeilen behalten!!!
if (temp.get(i).contains("error:"))
res.add(temp.get(i));
else if (temp.get(i).startsWith("INSERT INTO \"" + tablename + "\""))
res.add(temp.get(i).replace("INSERT INTO \"" + tablename + "\" VALUES(", "").replace(");", ""));
}
return res;
}
public static String getSQLitePath(String architecture, String addon)
{
String sqlite = "sqlite3." + architecture;
String pathSQLite3 = "/data/data/" + MainApp.get().getPackageName() + "/files/" + sqlite + addon;
return pathSQLite3;
}
Related
I have a device running Android 11 connected to my Windows 7 laptop via USB cable. I can see the device using Windows Explorer and drill down through the folders. The file names, types, and modify dates all show up nicely in Explorer. I'm writing software in Visual Foxpro which will put the file list into a listbox and allow the user to select which ones to copy to the PC. When I run it, the names and types are good but the date shows up as a zero date which in VFP shows up as 12/30/1899 00:00:00.
The code I'm using looks something like this ("iX14" is the name of the Android device) :
* Search the PC's namespace for the iX14 folder and set the name space variable
oFOLDER = oSHELLAPP.Namespace("::{20D04FE0-3AEA-1069-A2D8-08002B30309D}")
for each oITEM in oFOLDER.Items
if oITEM.Name = "iX14"
mNAMESPACE = oITEM.Path
exit
endif
next
* reset oFOLDER to the particular folder in the iX14 which has the files we need
oFOLDER = oShellApp.Namespace(mNAMESPACE + "\Internal shared storage\Download\AutoLog")
for each oITEM in oFOLDER.Items
thisform.listbox.AddItem(oITEM.ModifyDate)
At this point I have nowhere else to go since I'm a database guy and my focus is within databases and I know very little about using the Win32API and other OS level tasks. I tried stepping through using the debugger to try to see exactly what was being returned from oITEM.ModifyDate but the debugger just shows up what I'm already getting, 12/30/1899 00:00:00.
Would you please test this code on your system:
Local mNAMESPACE
oShellApp = Createobject("shell.application")
* Search the PC's namespace for the iX14 folder and set the name space variable
oFOLDER = oShellApp.Namespace("::{20D04FE0-3AEA-1069-A2D8-08002B30309D}")
For Each oITEM In oFOLDER.Items
If oITEM.Name = "iX14"
mNAMESPACE = oITEM.Path
Exit
Endif
Next
If !Empty(m.mNAMESPACE)
Local lcPath
lcPath = Addbs(m.mNAMESPACE) + "Internal shared storage\Download\AutoLog"
GetTree(m.lcPath, '*.*')
Select ;
cast(filepath As Varchar(250)) As filepath, ;
cast(filename As Varchar(250)) As filename, ;
filesize, fattr, createtime, lastacc, lastwrite ;
From filelist ;
ORDER By 1,2
Endif
Function GetTree
Lparameters tcStartDir, tcSkeleton
tcSkeleton = Evl(m.tcSkeleton, "*.*")
Create Cursor filelist ;
(filepath m, filename m, filesize i, ;
fattr c(8), createtime T, lastacc T, lastwrite T)
oFiler = Createobject('filer.fileutil')
With oFiler
.SearchPath = m.tcStartDir
.Subfolder = 1
.FileExpression = m.tcSkeleton
.Find(0)
For ix=1 To .Files.Count
With .Files(ix)
Insert Into filelist ;
(filepath, filename, filesize, fattr, createtime, lastacc, lastwrite) ;
values ;
(.Path, .Name, .Size, Attr2Char(.Attr), ;
Num2Time2(.Datetime), Num2Time2(.LastAccessTime), Num2Time2(.LastWriteTime) )
Endwith
Endfor
Return .Files.Count
Endwith
Endfunc
Function Num2Time2
Lparameters tnFloat
Return Dtot(Date(1899,12,30) + m.tnFloat)
Endfunc
Function Attr2Char
Lparameters tnAttr
Return ;
IIF(Bittest(m.tnAttr,0),'RO','RW')+;
IIF(Bittest(m.tnAttr,1),'H','_')+;
IIF(Bittest(m.tnAttr,2),'S','_')+;
IIF(Bittest(m.tnAttr,4),'D','_')+;
IIF(Bittest(m.tnAttr,5),'A','_')+;
IIF(Bittest(m.tnAttr,6),'E','_')+;
IIF(Bittest(m.tnAttr,7),'N','_')
Endfunc
The app im working with is getting data from a .csv (20k-30k records) from a server and it needs to persist the data into an SQLiteDatabase.
It works but some records are missing and appeared that it have been skipped.
I/Choreographer( 2555): Skipped 46 frames! The application may be doing too much work on its main thread.
I know that this error says that the memory consumption is very high due to heavy load. Is there a more efficient way in persisting data in SQLiteDatabase rather than the classic accessing of CSV and processing it from there?
Code for writing in DB
String sql = "INSERT INTO " + tableName
+ " VALUES (?,?,?,?,?,?);";
SQLiteDatabase db = openHelper.getWritableDatabase();
SQLiteStatement statement = db.compileStatement(sql);
try {
db.beginTransaction();
String[] sa = null;
for (final String csvline : arrCSV) {
statement.clearBindings();
sa = csvline.split(",");
if(sa.length==6){
statement.bindString(1, sa[0]);
statement.bindString(2, sa[1]);
statement.bindString(3, sa[2]);
statement.bindString(4, sa[3]);
statement.bindString(5, sa[4]);
statement.bindString(6, sa[5]);
}
statement.execute();
}
db.setTransactionSuccessful();
Log.d("Transaction", "Successful");
}catch(Exception e){
e.printStackTrace();
}finally {
statement.releaseReference();
statement.close();
db.endTransaction();
db.releaseMemory();
}
UPDATE
The missing records were not loaded in the Collection.
Is the skipping of frames the culprit here?
The loading in the collection is just a simple parsing of a csv file and
non replicable at times so Im assuming it is due to the skipping of frames.
I believe the issue is not linked to skipping frames and < 100 frames is considered a small/insignificant number. At least according to The application may be doing too much work on its main thread.
I frequently see it and has never been the cause of any issues. I've even seen it basically doing nothing other than returning a result from an activity to an activity that just starts the second activity.
As you have commented, the number of elements that result from the split is on occasion not 6. The issue is likely that the insert is not happening on such an occasion, perhaps due to constraints (without seeing how the columns are defined only guesses could be made).
However, you appear to consider that each line in csvline should be split into 6 elements. You should thus investigate as to why not?
To investigate I'd suggest getting details of the original data before the split and the resultant data after the split whenever the number of elements created by the split is not 6. e.g. by changing :-
sa = csvline.split(",");
if(sa.length==6){
statement.bindString(1, sa[0]);
statement.bindString(2, sa[1]);
statement.bindString(3, sa[2]);
statement.bindString(4, sa[3]);
statement.bindString(5, sa[4]);
statement.bindString(6, sa[5]);
}
statement.execute();
to
sa = csvline.split(",");
if(sa.length==6){
statement.bindString(1, sa[0]);
statement.bindString(2, sa[1]);
statement.bindString(3, sa[2]);
statement.bindString(4, sa[3]);
statement.bindString(5, sa[4]);
statement.bindString(6, sa[5]);
} else {
Log.d("NOT6SPLIT","CSVLINE WAS ===>" + csvline + "<===");
Log.d("NOT6SPLIT","CSVLINE WAS SPLIT INTO " + Integer.toString(sa.length) + " ELEMENTS :-");
for(String s: sa) {
Log.d("NOT6SPLIT","\tElement Value ===>" + s + "<===");
}
}
statement.execute();
Changing statement.execute() to :-
if (statement.executeInsert() < 1) {
Log.d("INSERTFAIL","Couldn't insert where CSVLINE was ===>" + csvline + "<===");
}
May also assist ('executeInsert' returns the rowid of the inserted record, else -1, not sure of the consequences of a table defined with WITHOUT ROWID).
It wouldn't surprise me at all if the issue boils down to your data containing characters that split considers special or metacharaceters:-
there are 12
characters with
special meanings:
the backslash \,
the caret ^,
the dollar sign $,
the period or dot .,
the vertical bar or pipe symbol |,
the question mark ?,
the asterisk or star *,
the plus sign +,
the opening parenthesis (,
the closing parenthesis ),
the opening square bracket [,
and the opening curly brace {,
These special characters are often called "metacharacters". Most
of them are errors when used alone.
I am trying to create a android app using GreenDAO, this is ERP kind of project so practically it is not possible to post all code here, but i am going to share only related code
QueryBuilder<PartyCommunication> partyCommQueryBuilder = partyCommunicationDao.queryBuilder();
partyCommQueryBuilder = partyCommQueryBuilder.where(
PartyCommunicationDao.Properties.CommType.eq("ALERT"),
PartyCommunicationDao.Properties.ReferenceCategory.eq("Low Stock"));
List<PartyCommunication> listOfPartyComm = partyCommQueryBuilder.list();
PartyCommunication daoPartyCommunication = listItr.next();
Long reference_entity_key = daoPartyCommunication.getReferenceEntityKey();
Product product = daoSessionUni.getProductDao().load(reference_entity_key);
ProductDetail productDetail = new ProductDetail(product);
Integer inventoryQOH= productDetail.getInventoryQOH();
I am getting exception in this line
Product product = daoSessionUni.getProductDao().load(reference_entity_key);
When i debug our application i found that it throwing exception from one of our DAO class as below
public Product readEntity(Cursor cursor, int offset) {
Product entity = new Product( //
.
.
.
.
cursor.getShort(offset + 23) != 0, // isSync
cursor.getShort(offset + 24) != 0, // isDeleted
cursor.getString(offset + 25), // createdBy
cursor.getString(offset + 26), // modifiedBy
cursor.isNull(offset + 27) ? null : new java.util.Date(cursor.getLong(offset + 27)), // lastSyncTime
new java.util.Date(cursor.getLong(offset + 28)), // created
new java.util.Date(cursor.getLong(offset + 29)) // modified
);
return entity;
}
In this line
cursor.getString(offset + 25), // createdBy
This is our 25th column of table.I know that all code is not enough to understand what is going wrong and why i am getting this exception , so i am also going to post logcat output
java.lang.IllegalStateException: get field slot from row 0 col 25 failed
at net.sqlcipher.CursorWindow.getString_native(Native Method)
at net.sqlcipher.CursorWindow.getString(CursorWindow.java:382)
at net.sqlcipher.AbstractWindowedCursor.getString(AbstractWindowedCursor.java:51)
at android.database.CursorWrapper.getString(CursorWrapper.java:114)
at com.tarnea.kareeb.model.ProductDao.readEntity(ProductDao.java:273)
at com.tarnea.kareeb.model.ProductDao.readEntity(ProductDao.java:1)
at de.greenrobot.dao.AbstractDao.loadCurrent(AbstractDao.java:417)
at de.greenrobot.dao.AbstractDao.loadUnique(AbstractDao.java:163)
at de.greenrobot.dao.AbstractDao.loadUniqueAndCloseCursor(AbstractDao.java:150)
at de.greenrobot.dao.AbstractDao.load(AbstractDao.java:139)
at com.tarnea.android.DataCleaningService.adjustLowStockAlertTable(DataCleaningService.java:115)
at com.tarnea.sync.kareeb.pharma.syncadapter.SyncManager.performSync(SyncManager.java:970)
at com.tarnea.sync.kareeb.pharma.syncadapter.SyncAdapter.onPerformSync(SyncAdapter.java:96)
at android.content.AbstractThreadedSyncAdapter$SyncThread.run(AbstractThreadedSyncAdapter.java:254
Thanks in advance to all.If anybody have some interest to solve my problem then please please ask any further information whatever you want.
Your problem isn't the query. The SQLite-Table simply does only contain 24 columns.
Probably you modified your database schema by adding new columns like createdBy.
Either you forgot to update your db using OpenHelper
Or you had an error in your ALTER TABLE-statements
Or you ran your app with the new schema-version but without your ALTER TABLE-statements yet included (which is practically the same as 1.
Have a look at your database using SqliteManager, to verify your tables have the columns you expect.
If you cannot get your database-file you can simply delete your app with all data or increase your schema-version and use the DevOpenHelper to recreate the whole database.
i am trying to retrieve alarm information from content provider using following code
final String tag_alarm = "tag_alarm";
Uri uri = Uri.parse("content://com.android.deskclock/alarm")
Cursor c = getContentResolver().query(uri, null, null, null, null);
Log.i(tag_alarm, "no of records are" + c.getCount());
Log.i(tag_alarm, "no of columns are" + c.getColumnCount());
if (c != null) {
String names[] = c.getColumnNames();
for (String temp : names) {
System.out.println(temp);
}
if (c.moveToFirst()) {
do {
for (int j = 0; j < c.getColumnCount(); j++) {
Log.i(tag_alarm, c.getColumnName(j);
+ " which has value " + c.getString(j));
}
} while (c.moveToNext());
}
}
it is giving me error permission denial i copied this code from curious answer from query Get alarm infomation
in the Nguyen's comment he pointed a solution "If i embed this code in Android source code and run image file, it can pass "permission denied" error and retrieve alarm information. Anyway, thanks your tip :) " how to embed a code in android source code and run image file ?? please explain i always create a project in eclipse and then code and run it as run application.please explain this trick
In my opinion, because of each manufacturer had implemented their own Clock App,
So default AlarmClockApp of Android will be replaced depend on each manufacturer, that makes your code can not be run successful if Android Os had been modified by Manufacturers.
So I think we can not handle all the devices in this case, instead of that, we should handler it by manufacturer of devices.
With Samsung devices, it's ClockPackage and in androidManifest :
<provider
android:name=".alarm.AlarmProvider"
android:authorities="com.samsung.sec.android.clockpackage"
android:exported="true"
android:readPermission="com.sec.android.app.clockpackage.permission.READ_ALARM"
android:writePermission="com.sec.android.app.clockpackage.permission.WRITE_ALARM" >
</provider>
So we can read data of alarm in Samsung devices by :
add permission in manifest:
<uses-permission android:name="com.sec.android.app.clockpackage.permission.READ_ALARM" />
then get Uri by below :
Uri uri = Uri.parse("content://com.samsung.sec.android.clockpackage/alarm");
Use Uri :
Cursor c = getContentResolver().query(uri, null, null, null, null);
if (c == null) { // that mean devices is not belong to Samsung manufacturer,
// we should use an other uri (don't for get to add permission)
AlarmLog.w("Can not read cursor");
}
AlarmLog.i(tag_alarm, "no of records are " + c.getCount());
AlarmLog.i(tag_alarm, "no of columns are " + c.getColumnCount());
if (c != null) {
String names[] = c.getColumnNames();
for (String temp : names) {
AlarmLog.d(tag_alarm, temp);
}
if (c.moveToFirst()) {
do {
for (int j = 0; j < c.getColumnCount(); j++) {
AlarmLog.i(tag_alarm, c.getColumnName(j)
+ " which has value " + c.getString(j));
}
} while (c.moveToNext());
}
}
Hope it's helpful and receive code for others manufactures.
Look at the definition of the content provider in AndroidManifest.xml
<provider android:name="AlarmProvider"
android:authorities="com.android.deskclock"
android:exported="false" />
The exported is false, which means a 3rd-party app cannot access it. Permission denial as a result.
how to embed a code in android source code and run image file
It means you modify the Android source(Provided by google). I don't think it's useful in your case.
You can do that in a rooted device, by directly modify the contents in sqlite database. I don't think there is a solution to work on all existing Android platforms.
In general, sqlite database files are under /data/data/app-package-name/databases/database-name, so in this example, it should be /data/data/com.android.deskclock/databases/com.android.deskclock or something similar. You can pull the file out by adb pull and open it using SqliteExplorer to check whether it is what you want.
For how to modify this db file, check Using your own SQLite database in Android applications
As said there's no way to do this without root, but you can monitor when the next alarm is and when the value changes with the following value:
Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED).toString()
This will give you a string with the next alarm.
My app reads in large amounts of data from text files assets and displays them on-screen in a TextView. (The largest is ~450k.) I read the file in, line-by-line into a SpannableStringBuffer (since there is some metadata I remove, such as section names). This approach has worked without complaints in the two years that I've had the app on the market (over 7k active device installs), so I know that the code is reasonably correct.
However, I got a recent report from a user on a LG Lucid (LGE VS840 4G, Android 2.3.6) that the text is truncated. From log entries, my app only got 9,999 characters in the buffer. Is this a known issue with a SpannableStringBuffer? Are there other recommended ways to build a large Spannable buffer? Any suggested workarounds?
Other than keeping a separate expected length that I update each time I append to the SpannableStringBuilder, I don't even have a good way to detect the error, since the append interface returns the object, not an error!
My code that reads in the data is:
currentOffset = 0;
try {
InputStream is = getAssets().open(filename);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
ssb.clear();
jumpOffsets.clear();
ArrayList<String> sectionNamesList = new ArrayList<String>();
sectionOffsets.clear();
int offset = 0;
while (br.ready()) {
String s = br.readLine();
if (s.length() == 0) {
ssb.append("\n");
++offset;
} else if (s.charAt(0) == '\013') {
jumpOffsets.add(offset);
String name = s.substring(1);
if (name.length() > 0) {
sectionNamesList.add(name);
sectionOffsets.add(offset);
if (showSectionNames) {
ssb.append(name);
ssb.append("\n");
offset += name.length() + 1;
}
}
} else {
if (!showNikud) {
// Remove nikud based on Unicode character ranges
// Does not replace combined characters (\ufb20-\ufb4f)
// See
// http://en.wikipedia.org/wiki/Unicode_and_HTML_for_the_Hebrew_alphabet
s = s. replaceAll("[\u05b0-\u05c7]", "");
}
if (!showMeteg) {
// Remove meteg based on Unicode character ranges
// Does not replace combined characters (\ufb20-\ufb4f)
// See
// http://en.wikipedia.org/wiki/Unicode_and_HTML_for_the_Hebrew_alphabet
s = s.replaceAll("\u05bd", "");
}
ssb.append(s);
ssb.append("\n");
offset += s.length() + 1;
}
}
sectionNames = sectionNamesList.toArray(new String[0]);
currentFilename = filename;
Log.v(TAG, "ssb.length()=" + ssb.length() +
", daavenText.getText().length()=" +
daavenText.getText().length() +
", showNikud=" + showNikud +
", showMeteg=" + showMeteg +
", showSectionNames=" + showSectionNames +
", currentFilename=" + currentFilename
);
After looking over the interface, I plan to replace the showNikud and showMeteg cases with InputFilters.
Is this a known issue with a SpannableStringBuffer?
I see nothing in the source code to suggest a hard limit on the size of a SpannableStringBuffer. Given your experiences, my guess is that this is a problem particular to that device, due to a stupid decision by an engineer at the device manufacturer.
Any suggested workarounds?
If you are distributing through the Google Play Store, block this device in your console.
Or, don't use one massive TextView, but instead use several smaller TextView widgets in a ListView (so they can be recycled), perhaps one per paragraph. This should have the added benefit of reducing your memory footprint.
Or, generate HTML and display the content in a WebView.
After writing (and having the user run) a test app, it appears that his device has this arbitrary limit for SpannableStringBuilder, but not StringBuilder or StringBuffer. I tested a quick change to read into a StringBuilder and then create a SpannableString from the result. Unfortunately, that means that I can't create the spans until it is fully read in.
I have to consider using multiple TextView objects in a ListView, as well as using Html.FromHtml to see if that works better for my app's long term plans.