Bounty Award - The bounty will be awarded to an answer that gets from a populated Telephony.Sms.Inbox.PERSON value, to the associated Contact using only ContractsContact tables.
I'm reading SMS messages in the standard way in my application:
final String[] projection = {Telephony.Sms.Inbox.BODY,
Telephony.Sms.Inbox.ADDRESS,
Telephony.Sms.Inbox.READ,
Telephony.Sms.Inbox.DATE,
Telephony.Sms.Inbox.PERSON};
final Cursor cursor = ctx.getContentResolver().query(Telephony.Sms.Inbox.CONTENT_URI,
projection, null, null, Telephony.Sms.Inbox.DEFAULT_SORT_ORDER);
When populated, the id returned from the index Telephony.Sms.Inbox.PERSON relates to the id of the deprecated Contacts.People._ID and can be used to query further contact information in the following way:
final String[] projection = {Contacts.People.DISPLAY_NAME};
final String[] selectionArgs = {contactId};
final Cursor cursor = ctx.getContentResolver().query(Contacts.People.CONTENT_URI,
projection, Contacts.People._ID + " = ?", selectionArgs, null);
Why would the relatively new Telephony API use deprecated tables, instead of ContactsContract?
Telephony.Sms.Inbox.PERSON documentation states:
Type: INTEGER (reference to item in content://contacts/people)
I've tried unsuccessfully (but not unsurprisingly?) to find a mapping to the id in any of the ContactsContract id fields, so I'm left having to use deprecated APIs in order to resolve the queries I need to perform quickly.
Such queries include searching for messages by a particular contact, for which I only have the name. The contact could have multiple numbers, which may not be in the correct format to potentially match Telephony.Sms.Inbox.ADDRESS entries.....
The workaround of using Telephony.Sms.Inbox.ADDRESS and ContactsContract.PhoneLookup is not the end of the world when going from the number to the contact, but I still feel I must be missing something here?
Here is the process I'm using to get the messages for 'Joe Bloggs'.
1) Query the ContactsContract table to confirm a contact by the name of Joe Bloggs exists on the device - or get a close match if the contact is actually listed as 'Joe Blogs'.
2) Using the confirmed name, I query the deprecated Contact.People table to get all associated ids for the contact in the following way:
final String selection = Contacts.People.DISPLAY_NAME + " LIKE ?";
final String[] projection = {Contacts.People.DISPLAY_NAME,
Contacts.People._ID};
final String[] selectionArgs = {contactName};
final Cursor cursor = ctx.getContentResolver().query(Contacts.People.CONTENT_URI,
projection, selection, selectionArgs, null);
3) Using the list of deprecated contact ids, I query the message table as so:
final String[] referredArgs = new String[contactIdArray.size()];
for (int i = 0; i < contactIdArray.size(); i++) {
referredArgs[i] = contactIdArray.get(i);
}
final String referredSelection = Telephony.Sms.Inbox.PERSON + " IN "
+ "(" + TextUtils.join(",", referredArgs) + ")";
final String[] projection = {Telephony.Sms.Inbox.BODY,
Telephony.Sms.Inbox.ADDRESS,
Telephony.Sms.Inbox.READ,
Telephony.Sms.Inbox.DATE,
Telephony.Sms.Inbox.PERSON};
final Cursor cursor = ctx.getContentResolver().query(Telephony.Sms.Inbox.CONTENT_URI,
projection, referredSelection, null, Telephony.Sms.Inbox.DEFAULT_SORT_ORDER);
I'm hoping someone will tell me I'm going round the houses here and there is a more obvious solution using current APIs. I don't consider iterating the entire message table using ContactsContract.PhoneLookup an optimised solution.
Thanks in advance.
I wouldn't use the Telephony.Sms.Inbox.PERSON field, and definitely wouldn't query the deprecated People apis if I were you.
The People apis had been deprecated for so long you can't count on all devices our there to properly support it anymore.
First thing you need to understand is that there isn't a one-to-one link between sms and contacts.
An SMS can come from a non-contact phone number, a single contact, multiple contacts, a mixture of contacts and non-contacts, alpha-numeric ids, and even other, more rare options.
Next thing, you should read carefully the stock code and how it handles a properly called "Recipient ID" that you can get from the SMS collection, there's a collection called canonical-addresses (or canonical-address) that serves as a mapping between a phone number (or a comma-separated list of phones) and a recipient id.
The code does a single query on launch to cache the entire table in memory, and then uses it to map between phones and recipient-ids.
Here's the mapping class
Why would the relatively new Telephony API use deprecated tables, instead of ContactsContract?
What you are referring to is not new. In Telephony.java, you see it relies on the existing content://sms provider:
public static final class Inbox implements BaseColumns, TextBasedSmsColumns {
/**
* The {#code content://} style URL for this table.
*/
public static final Uri CONTENT_URI = Uri.parse("content://sms/inbox");
It was already there in Donut (and probably before, but I didn't check).
What's new in Kitkat is the ability to change the SMS app.
It's been five years and it's still relevant. You still need to do endlessly phoneLookup and hang up callbacks on contact tables if all you need to do is synchronize text messages.
I do not understand your concern properly but I am working on similar project, here is the basic code, and basic, important columns for fetching and display a message:
ContentResolver contentResolver = getContentResolver();
final String[] projection = new String[]{"*"};
Cursor SMSL = contentResolver.query(Telephony.Sms.CONTENT_URI, projection, null, null, "date ASC");
int msgscount = SMSL.getCount();
if (msgscount>0) {
msgs = new String[SMSL.getCount()][msgs_column_count];
int i = 0;
while (SMSL.moveToNext()) {
progress.setProgress(i);
msgs[i][0] = SMSL.getString(SMSL.getColumnIndex("address"));
msgs[i][1] = SMSL.getString(SMSL.getColumnIndex("date_sent"));
msgs[i][2] = SMSL.getString(SMSL.getColumnIndex("date"));
msgs[i][3] = SMSL.getString(SMSL.getColumnIndex("type"));
msgs[i][4] = SMSL.getString(SMSL.getColumnIndex("body"));
msgs[i][5] = SMSL.getString(SMSL.getColumnIndex("read"));
if (SMSL.getString(SMSL.getColumnIndex("service_center")) != null){
msgs[i][6] = SMSL.getString(SMSL.getColumnIndex("service_center"));
}else{
msgs[i][6] = "";
}
i++;
}
SMSL.close();
}else{
msgs = new String[0][0];
Toast.makeText(getApplicationContext(),"No messages found!",Toast.LENGTH_LONG).show();
}
If you want any help with this or fetching messages, let me know.
Related
I'm seeing a crash report for this occassionally:
Fatal Exception: java.lang.IllegalStateException: Couldn't read row 1127, col 0 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
at android.database.CursorWindow.nativeGetLong(CursorWindow.java)
at android.database.CursorWindow.getLong(CursorWindow.java:511)
at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:75)
at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:220)
at android.database.AbstractCursor.moveToNext(AbstractCursor.java:245)
at android.database.CursorWrapper.moveToNext(CursorWrapper.java:166)
at com.anthonymandra.util.ImageUtils.cleanDatabase(SourceFile:381)
Apparently the moveToNext is failing mid-loop (note row 1127). The loop removes entries that represent files that can no longer be found.
final ArrayList<ContentProviderOperation> operations = new ArrayList<>();
try( Cursor cursor = c.getContentResolver().query(Meta.CONTENT_URI, null, null, null, null))
{
if (cursor == null)
return;
final int uriColumn = cursor.getColumnIndex(Meta.URI);
final int idColumn = cursor.getColumnIndex(BaseColumns._ID);
while (cursor.moveToNext())
{
String uriString = cursor.getString(uriColumn);
if (uriString == null) // we've got some bogus data, just remove
{
operations.add(ContentProviderOperation.newDelete(
Uri.withAppendedPath(Meta.CONTENT_URI, cursor.getString(idColumn))).build());
continue;
}
Uri uri = Uri.parse(uriString);
UsefulDocumentFile file = UsefulDocumentFile.fromUri(c, uri);
if (!file.exists())
{
operations.add(ContentProviderOperation.newDelete(Meta.CONTENT_URI)
.withSelection(getWhere(), new String[]{uriString}).build());
}
}
}
c.getContentResolver().applyBatch(Meta.AUTHORITY, operations);
Any idea how a cursor could fail mid-loop like that?
You appear to be making a fairly large query: at least 1127 rows, and for all possible columns (despite the fact that you are only using two of them). And, during your work with that Cursor, you are doing disk I/O and/or IPC back to the ContentProvider, assuming that UsefulDocumentFile is related to Android's DocumentFile.
As Prakash notes, the Cursor that you get back may contain only a subset of the information. As soon as you try advancing past that point, the Cursor needs to go back to the data source and get the next window of results. I can see you running into this sort of problem if there has been a substantial change in the data while this work has been going on (e.g., there are now fewer than 1127 rows).
I suggest that you:
Constrain the columns that you get back to the subset that you need, and
Avoid the I/O during the loop (e.g., spin through the Cursor to build up an ArrayList<Pair> or something, close the Cursor, then iterate over the list)
may be you are srote file in database that's why you get java.lang.IllegalStateException
UsefulDocumentFile file = UsefulDocumentFile.fromUri(c, uri);
Android SQLite returns rows in cursor windows that have the maximum size of 2MB as specified by
config_cursorWindowSize.If your row exceeds this limit, you'll get this error.
Store files in filesystem and paths in database.
The following code throws the exception in the title on the third and fourth line. Am I missing something? I want to be able to sort by artist name.
public List<String> getAllArtists(Context context) {
List<String> artists = new ArrayList<String>();
String[] projection2 = {MediaStore.Audio.Media.ARTIST};
String sortOrder = MediaStore.Audio.Artists.ARTIST;
Uri songUri = Uri.parse("content://com.google.android.music.MusicContent/audio");
CursorLoader cl2 = new CursorLoader(context,
songUri, projection2, null, null, sortOrder);
cursor = cl2.loadInBackground();
while (cursor.moveToNext()) {
if (cursor.getString(0).length()>0){
if (!artists.contains(cursor.getString(0)))
artists.add(cursor.getString(0));
}}
cursor.close();
return artists;}
Here is the complete stack trace:
03-22 16:49:18.237 2594-2637/php_request E/AndroidRuntime: FATAL
EXCEPTION: IntentService[SongService] Process: php_request, PID: 2594
java.lang.IllegalArgumentException: sortOrder not supported
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:165)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:135)
at android.content.ContentProviderProxy.query(ContentProviderNative.java:421)
at android.content.ContentResolver.query(ContentResolver.java:500)
at android.content.CursorLoader.loadInBackground(CursorLoader.java:64)
at SongParser.getAllArtists(SongParser.java:41)
at SongService.onHandleIntent(SongService.java:60)
at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:66)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.os.HandlerThread.run(HandlerThread.java:61)
What you're using is a Uri handled specifically by Google Play Music app, not Android media system. (com.google.android.music is package name of Play Music.) I can imagine that
a) Play Music API cannot be mixed with Android MediaStore API,
b) Play Music content provider does not support sorting in SQL as the error suggests,
c) Play Music API is changing over time so don't use it anyway. (Don't quote me on this.)
Maybe you'd like to use one of Uri constants specified here http://developer.android.com/reference/android/provider/MediaStore.Audio.Media.html. These are:
MediaStore.Audio.Media.INTERNAL_CONTENT_URI
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
If using specified Uri is intended then I suggest removing sortOrder from content provider request and sorting the resulting List afterwards by calling
Collections.sort(artists);
as described here https://stackoverflow.com/a/708708/2444099.
Seems like perhaps sort order API has changed. Can you sort on ARTIST_KEY? I'm just looking at columns on http://developer.android.com/reference/android/provider/MediaStore.Audio.AudioColumns.html and that one says it's sortable. Also does your query work when you have null sort order?
For your case, you can use it for sorting,
String sortOrder = MediaStore.Audio.Albums.DEFAULT_SORT_ORDER;
For URI, you can use-
Uri uri = MediaStore.Audio.Artists.Albums.getContentUri("external", artistId);
The artistId is the value of the MediaStore.Audio.Artists._ID from a previous Artist query.
Here,
DEFAULT_SORT_ORDER: The default sort order for this table
Constant Value: "artist_key"
Hope it will help you.
What CursorLoader does?
CursorLoader do sort for you when it is created as CursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
So, to query for a list of artist, you would use MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI and use the artist name as your selection args.
For a list of albums, you now have the ARTIST_KEY which you can use to query MediaStore.Audio.Artists.Albums.EXTERNAL_CONTENT_URI, to obtain a list of albums for the given artist.
Resource Link: Media store default sort order
Sort order in Android list from Mediastore
For Video, you can use it
String orderBy = android.provider.MediaStore.Video.Media.DATE_TAKEN;
videocursor = managedQuery(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
proj, null, null, orderBy + " DESC");
Resource Link: sort order in android list from mediastore
Query Sorting using column name.
For specifying the sort order for the query to a content provider, it look like
new CursorLoader(context, RepresentativeEntityContentProvider.CONTENT_URI, null, null,
null, "COLUMN_NAME ASC");
Using this we can make it ascending order with COLUMN_NAME.
Android SQLite Sort order does not work with upper and lowercase letters
If you are using SQLite database, with a Cursor or Perhaps Content provider, you may have come across alphabetical sorting problems regarding upper and lowercase letters.
String[] projection = { Table.COLUMN_ID, Table.COLUMN_TITLE};
String sortOrder = Table.COLUMN_TITLE + " COLLATE NOCASE ASC";
CursorLoader cursorLoader = new CursorLoader(this,
YourProvider.CONTENT_URI,
projection,
null,
null,
sortOrder);
The solution to the alphabetical sorting problem is the COLLATE NOCASE in the part of the query where you specify the sorting.
Note that the ASC is optional to specify. You could also use DESC to reverse the sorting order.
How can Telephony.Sms.Conversations be used to retrieve convo information to String?
I tried:
ContentResolver cr = context.getContentResolver();
Cursor convo = cr.query(Telephony.Sms.Conversations.CONTENT_URI,
new String[] { Telephony.Sms.Conversations.ADDRESS,
Telephony.Sms.Conversations.PERSON },
null,
null,
Telephony.Sms.Conversations.DEFAULT_SORT_ORDER);
How ever I get error invalid column address. when I remove address I get invalid column person. What column's does this class provide? (I couldn't find anything on the API reference page or any examples online. btw I already have a working code to retrieve inbox and outbox but I would like to get conversations too (I mean title and num of msges), without matching inbox and outbox results)
You can only get "msg_count" and "snippet" values from Telephony.Sms.Conversations, and you can get the "address" value from Telephony.TextBasedSmsColumns.
private static final String[] SMS_CONVERSATIONS_PROJECTION = new String[]{"msg_count", "snippet"};
Cursor cursor = cr.query(Telephony.Sms.Conversations.CONTENT_URI,
SMS_CONVERSATIONS_PROJECTION, null, null,Telephony.Sms.Conversations.DEFAULT_SORT_ORDER);
while(cursor.moveToNext()) {
int msg_count = cursor.getInt(cursor.getColumnIndex("msg_count"));
String snippet = cursor.getString(cursor.getColumnIndex("snippet"));
}
cursor.close;
It's inconvenient for us without "address", "date" and so on.
Telephony.Sms.Conversation
I got this piece of code in a project I'm working on, in the implementation of public Loader onCreateLoader(int id, Bundle args):
Uri baseUri=Events.CONTENT_URI;
final String[] EVENT_PROJECTION = new String[] {Events.CALENDAR_ID,
Events.ORGANIZER,
Events.TITLE,
Events.EVENT_LOCATION,
Events.DTSTART,
Events.DTEND,
Events.EVENT_TIMEZONE,
Events.EVENT_END_TIMEZONE,
Events.DURATION,
Events.ALL_DAY,
Events.RRULE,
Events.RDATE,
};
String selection = "((" + "this.isCalendarAvailable("+Events.CALENDAR_ID+") = ?)) AND ("
+ "TimeUnit.MILLISECONDS.toDays(Long.parseLong("+Events.DTSTART+",10)) = ?))";
String[] selectionArgs = new String[]{"true",String.valueOf(selectedDate.getTimeInMillis())};
return new CursorLoader(this,baseUri,EVENT_PROJECTION, selection, selectionArgs, Events.DTSTART);
I wanted to get the details (specified in EVENT_PROJECTION) of all events that are found on available calendars (according to the boolean method isCalendarAvailable) and that start on selectedDate.
I tried to do that by initializing String selection as above, but it seems that it is not possible due to a misunderstanding the way this string should be written.
I already know that i can use "AND", "OR" and "=?".
But is there any other syntax rules for a string to be used as a parameter for cursorLoader constructor??
If not, is there any way for selecting what i need from the Uri according to some methods (as I tried above)?
Thanks a lot in advance
I'm developing a small application so I can get more experience of Android development. I wrote a method that list all of contact number and total messages (just like GoSMS or default SMS Application). But problem that I'm facing right now is slow performance. Below is what I have done to get the result.
Sample result: Mr AAA (10000)
Steps:
Get all SMS thread ID
Loop and get total messages belong to each thread ID.
Get the contact number belong to that thread.
Use that number and PhoneLookup to get contact name.
Here is the method:
public void populateContactList()
{
// Get all sms threads
Cursor smsAddressCursor = getContentResolver().query(
SMSCVar.CONTENT_URI,
new String[] { "DISTINCT "+SMSCVar.THREAD_ID},
null,
null,
null);
while(smsAddressCursor.moveToNext())
{
Contact c = new Contact();
// Get thread_id
String thread_id = smsAddressCursor.getString(smsAddressCursor.getColumnIndex(SMSCVar.THREAD_ID));
// Get total messages
Cursor totalMessage = getContentResolver().query(
SMSCVar.CONTENT_URI,
new String[] {"count("+SMSCVar.BODY+")"},
SMSCVar.THREAD_ID+" = ?",
new String[] {thread_id},
null);
totalMessage.moveToNext();
c.setNumberOfSMS(totalMessage.getInt(0));
totalMessage.close();
// Get number
Cursor number = getContentResolver().query(
SMSCVar.CONTENT_URI,
new String[] {SMSCVar.ADDRESS},
SMSCVar.THREAD_ID+" = ?",
new String[] {thread_id},
null);
number.moveToNext();
String pNumber = number.getString(0);
number.close();
// Get contact name
Uri uriPhonenumber = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI
, Uri.encode(pNumber));
Cursor contactDisplayName = getContentResolver().query(
uriPhonenumber,
new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME},
null,
null,
null);
// If cursor is not null and has at least one result
if(!contactDisplayName.isNull(0) && contactDisplayName.getCount() > 0)
{
// Get contact name for display
contactDisplayName.moveToNext();
c.setContactName(contactDisplayName
.getString(contactDisplayName
.getColumnIndex(ContactsContract.PhoneLookup.DISPLAY_NAME)));
}
else
{
// Get contact number for display
c.setContactName(pNumber);
}
// Don't get confuse here, setContactNumber method is not used for display purpose.
c.setContactNumber(pNumber);
contactListAdapter.add(c);
contactDisplayName.close();
}
smsAddressCursor.close();
}
I've coded to close cursor properly but I still get GARBAGE COLLECTOR messages and slow retrieving time (5 seconds). My SMS is over 11000 messages.
So please assist me!
Thank you!
P/S: I'm not an English native speaker so I've tried my best to make my question easy to understand.