I need to update contact first and last name from emulator contact list.
my code:
val contentResolver = APP_ACTIVITY?.contentResolver
val contentValues = ContentValues().apply {
put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,"$firstName")
put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,"$lastName")
}
Log.d("TAG", "editContactEmulator:$contentValues") //**prints: data2=Michael data3=Lou**
val contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId.toString().toLong())
Log.d("TAG", "editContactEmulator: $contactUri") // **prints:content://com.android.contacts/contacts/1**
val res = contentResolver?.update(contactUri,contentValues, null, null)
Log.d("TAG", "editContactEmulator : $res") **//RETURNS 0**
so, the new values ( first and last name) are correct, the uri is the correct uri of the contact I want to update. BUT res is still 0 which means it did not update any rows.
Any Ideas?
( I added READ_CONTACTS and WRITE_CONTACTS permission - so that not the problem)
Related
I'm struggling to update metadata in the mediastore with a DocumentFile/TreeUri.
This is how I tried to do it:
boolean canWrite = documentFile.canWrite(); //returns true
Uri mediaUri = MediaStore.getMediaUri(context, documentFile.getUri());
ContentValuesvalues = new ContentValues();
values.put(MediaStore.MediaColumns.DATE_MODIFIED, newLastModified);
boolean success = context.getContentResolver().update(mediaUri, values,null, null) > 0;
It fails and the logcat reads:
W/MediaProvider: Ignoring mutation of date_modified
What am I doing wrong? The DocumentFile is writable, it is received via Intent.ACTION_OPEN_DOCUMENT_TREE and takePersistableUriPermission was called on the folder.
I also tried updating the entry with IS_PENDING before and removed after:
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.IS_PENDING, 1);
boolean success = context.getContentResolver().update(mediaUri, values,
null, null) > 0; //returns false
returns for whatever reason also false, but I see that the file is prefixed with .pendingxxxx, so it seemed to work
ContentValuesvalues = new ContentValues();
values.put(MediaStore.MediaColumns.DATE_MODIFIED, newLastModified);
values.put(MediaStore.MediaColumns.IS_PENDING, 0);
boolean success = context.getContentResolver().update(mediaUri, values,null, null) > 0; //returns false
Indexed value of File#lastModified() extracted from this media item.
This constant represents a column name that can be used with a ContentProvider through a ContentValues or Cursor object. The values stored in this column are Cursor#FIELD_TYPE_INTEGER , and are read-only and cannot be mutated.
doc
you can't update this,just use File.setLastModified
I'm trying to update my old music player to support Android 10 and 11 (and 12 soon)
My code to rename and delete a playlist broke due to changes in security I guess ?? Here is what used to work :
private fun renamePlaylist(resolver: ContentResolver, playlist: Playlist, newName: String) {
val uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI
val selection = "${MediaStore.Audio.Playlists._ID}=${playlist.id}"
val c = ContentValues()
c.put(MediaStore.Audio.Playlists.NAME, newName)
resolver.update(uri, c, selection, null)
}
and
fun deletePlaylist(resolver: ContentResolver, id: Long) {
val uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI
val selection = "${MediaStore.Audio.Playlists._ID}=$id"
resolver.delete(uri, selection, null)
}
I have tested again on a Android 9 device and it works perfectly. Now I have this error
java.lang.IllegalArgumentException: Movement of content://media/external/audio/playlists which isn't part of well-defined collection not allowed
After digging the Android source code it looks like my URI is of type AUDIO_PLAYLISTS and it expects a type AUDIO_PLAYLISTS_ID. So i tried to cheat and append the playlist id to the URI to match the expected type. Now it doesn't crash but the playlist is not deleted either, nothing happens
Thanks for your help
Use the uri fromMediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) for Q and above and MediaStore.Audio.Media.EXTERNAL_CONTENT_URI for below Q
This is because of the recent scoped storage changes
I can easily get a list of every email address for every Contact using the following example snippets:
//...
private val getPerson = registerForActivityResult(PickContact()) {
it?.also { contactUri ->
val personDetails = ContactForPerson("", "", "")
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY, //String
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_PRIMARY,//String
ContactsContract.CommonDataKinds.Email.ADDRESS, //String
)
context?.contentResolver?.query(contactUri, projection, null, null, null)?.apply {
moveToFirst()
personDetails.apply {
uri = getStringOrNull(0)
name = getString(1)
email = getStringOrNull(2)
}
close()
}
}
}
//...
fab.setOnClickListener {
//...
getPerson.launch(0)
//...
}
//...
class PickContact : ActivityResultContract<Int, Uri?>() {
override fun createIntent(context: Context, input: Int?) =
Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI).also {
it.type = ContactsContract.CommonDataKinds.Email.CONTENT_TYPE
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? =
if (resultCode == RESULT_OK) intent?.data else null
}
The question is, since I already have some information about a Contact, is there a way for me to filter the giant list of every email address for every Contact to only show me the email addresses for a single Contact?
I noticed Get specific contact information from URI returned from Intent.ACTION_PICK, but the information is rather dated and it's not clear if the READ_CONTACTS permission is required, which is not desired.
Thank you.
It seems you're calling an EMAIL-PICKER and not a CONTACT-PICKER, by setting the intent type to Email.CONTENT_TYPE.
This means the user will be choosing a specific email in the device's default contacts app.
The result you're then getting from the picker is not a contactUri, rather a dataUri - i.e. a uri that points to a specific row in the Data table, which only allows you to get info about that specific row, in this case it must be an email row.
This also means your projection is a bit funny by using fields under CommonDataKinds.Phone.X, this doesn't matter too much as these fields are inherited from Data.CONTENT_URI, but to prevent confusion you should probably replace these with:
Data.LOOKUP_KEY,
Data.DISPLAY_NAME_PRIMARY,
Email.ADDRESS,
Now, if you want all the contact's email rather then a single one, you should not set the intent type - this will launch a contact picker which will allow you to get the contact's emails + phones + name by retrieving the Entity as shown here.
I am trying to get all the android contacts which been updated.
I am saving on firebase the last contact id i added and the last updated timestamp
I am using the next function to get back a cursor of all the updated contacts to compare with firebase server
private fun getUpdatedContacts(): Cursor? {
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER,
ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)
val selection = ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ? AND " +
ContactsContract.Contacts._ID + "<= ?"
val selectionArgs = arrayOf(mFireContactDetails!!.lcu_ms.toString(), mFireContactDetails!!.lcid.toString())
val sortOrder = ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " ASC"
return mContentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder)
}
But when i change one contact in my phone this cursor is returned MANY un-related contacts that i never used and mark them as changed. Last time when i just added a phone number to an existing contact, i got back from this cursor more than 50 contacts as been updated.
What is going on Android?? I am trying to sync contacts for the past 3 months now. Why is it so hard???
This is almost the same question with the same answer as your other question: When deleting a contact on android, other random contacts id's being changed
You have some assumptions on Contact IDs that you can't make - no one guarantees Contact IDs are incremental, and no one guarantees Contact IDs are stable, in fact they are definitely not.
You can use queried contact IDs while you're app is running, there is very small chance of them being changed within some minutes, but there is some chance of having IDs changed for existing users every once in a while.
Not only that, but the same ID can point to some contact today, and point to a completely different contact tomorrow.
If you keep some clone of the local contacts in the cloud, you should use the following composite IDs to reference contacts:
Contacts.CONTACT_ID, Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME
See my answer here for more details: How to uniquely identify a contact on ContactsContract.Contacts table
It's not a perfect solution, but it's the best we have
I been testing this solution for couple of days and it seems OK but i think i need to test it much more. If you using this method, do your own testing and above all, PLEASE LET ME KNOW IF I MISSED ANYTHING and don't be hurry to downgrade. Thx!
I built an App class that extents Application and implements the
ActivityLifecycleCallbacks. In which i create a ContactSync class for the
first time and activate it everytime the app goes to foregound
In ContactSync class, i am using Kotlin withContext(Dispatchers.IO) to suspend any code for easier flow
I use .get() to get all the contacts from firestore related to current user
at the .get() addOnSuccessListener, i add all the contacts to a HashMap with the normalized phone number as key and name + firestore id as values (using internal class)
While making the HashMap i also make sure there are no duplicates on firestore with smae phone number and if so delete them (using batch)
i then retrieve all the contacts from android phone. I sort them by NORMALIZED_NUMBER first and DISPLAY_NAME (will explain later)
I am now creating a batchArray with index and count to avoid exceeding the 500 limit
I start scanning through the contacts cursor,
I first get the normalized number, if not available (null) i create it my self using a function i made (it might be that a null value is only returned for phone numbers not in correct format, not sure)
I then compare the normalized number with previous cursor value. If the same i ignore it to avoid duplicates in firestore (remember the cursor is sorted by NORMALIZED_NUMBER)
I then check if the normalized number already in HashMap.
If in HashMap: i compare the name in HashMap to the cursor name. if different, i conclude the name was changed and i update the firestore contact in batch array (remember to increment counter and if exceeds 500 increase index). I then remove the normalized number from the HashMap to avoid its deletion later
If not in HashMap: i conclude the contact is new and i add it to firestore via batch
I iterate through all the cursor until completed.
When cursor complete i close it
Any remaining records found in HashMap are ones that were not found on firestore hence deleted. I iterate and delete them using batch
sync is done on the phone side
Now, since making the actual sync needs access to all users, i user firebase functions in node. I create 2 functions:
function that fires when new user is created (signed via phone)
function that fires when new contact document is created.
Both functions compare the users to the normalized number in document and if matching, writing the uid of that user to the firestore document "friend_uid" field.
Note you might have errors in these functions if you try to use them in free firebase plan. I suggest changing to Blaze plan and limit the charging to couple of dollars. By changing to Blaze, google also gives you free extras and avoid actual payment
By that, the sync is completed. The sync takes only couple of seconds
To display all the contacts which are users to the app, query all user contacts with "friend_uid" that are not null.
Some extra notes:
The .get() will retrieve all the contacts every time a sync is made. That might be a lot of reads if user has couple of hundreds contacts. To minimize, i use .get(Source.DEFAULT) when launching the app and .get(Source.CACHE) for the other times. Since these documents name and number only modified by user, i believe it will not be a problem most of the times (still testing)
To minimize the sync process as much as possible, i initiate it only if any contact changed its timestamp. I save the last timestamp to SharedPreferences and compare it. I found it mostly saves sync when app re-opened fast.
I also save the last user logged in. If any change in user, i re-initialize the current user contacts
Some source code (still testing, please let me know if any error):
private fun getContacts(): Cursor? {
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP)
//sort by NORMALIZED_NUMBER to detect duplicates and then by name to keep order and avoiding name change
val sortOrder = ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER + " ASC, " +
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
return mContentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projection,
null,
null,
sortOrder)
}
private suspend fun syncContactsAsync() = withContext(Dispatchers.IO) {
if (isAnythingChanged() || mFirstRun) {
if (getValues() == Result.SUCCESS) {
myPrintln("values retrieved success")
} else {
myPrintln("values retrieved failed. Aborting.")
return#withContext
}
val cursor: Cursor? = getContacts()
if (cursor == null) {
myPrintln("cursor cannot be null")
mFireContactHashMap.clear()
return#withContext
}
if (cursor.count == 0) {
cursor.close()
mFireContactHashMap.clear()
myPrintln("cursor empty")
return#withContext
}
var contactName: String?
var internalContact: InternalContact?
val batchArray = mutableListOf(FirebaseFirestore.getInstance().batch())
var batchIndex = 0
var batchCount = 0
var normalizedNumber:String?
var prevNumber = ""
var firestoreId: String
while (cursor.moveToNext()) {
normalizedNumber = cursor.getString(COLUMN_UPDATED_NORMALIZED_NUMBER)
if (normalizedNumber == null) {
normalizedNumber = cursor.getString(COLUMN_UPDATED_PHONE_NUMBER)
normalizedNumber = Phone.getParsedPhoneNumber(mDeviceCountryIso,normalizedNumber,mContext)
}
//cursor sorted by normalized numbers so if same as previous, do not check
if (normalizedNumber != prevNumber) {
prevNumber = normalizedNumber
contactName = cursor.getString(COLUMN_UPDATED_DISPLAY_NAME)
internalContact = mFireContactHashMap[normalizedNumber]
//if phone number exists on firestore
if (internalContact != null) {
//if name changed, update in firestore
if (internalContact.name != contactName) {
myPrintln("updating $normalizedNumber from name: ${internalContact.name} to: $contactName")
batchArray[batchIndex].update(
mFireContactRef.document(internalContact.id),
FireContact.COLUMN_NAME,
contactName)
batchCount++
}
//remove to avoid deletions
mFireContactHashMap.remove(normalizedNumber)
} else {
//New item. Insert
if (normalizedNumber != mUserPhoneNumber) {
myPrintln("adding $normalizedNumber / $contactName")
firestoreId = mFireContactRef.document().id
batchArray[batchIndex].set(mFireContactRef.document(firestoreId),
FireContact(firestoreId, -1, contactName,
cursor.getString(COLUMN_UPDATED_PHONE_NUMBER),
normalizedNumber))
batchCount++
}
}
if (BATCH_HALF_MAX < batchCount ) {
batchArray += FirebaseFirestore.getInstance().batch()
batchCount = 0
batchIndex++
}
}
}
cursor.close()
//Remaining contacts not found on cursor so assumed deleted. Delete from firestore
mFireContactHashMap.forEach { (key, value) ->
myPrintln("deleting ${value.name} / $key")
batchArray[batchIndex].delete(mFireContactRef.document(value.id))
batchCount++
if (BATCH_HALF_MAX < batchCount ) {
batchArray += FirebaseFirestore.getInstance().batch()
batchCount = 0
batchIndex++
}
}
//execute all batches
if ((batchCount > 0) || (batchIndex > 0)) {
myPrintln("committing changes...")
batchArray.forEach { batch ->
batch.commit()
}
} else {
myPrintln("no records to commit")
}
myPrintln("end sync")
mFireContactHashMap.clear()
mPreferenceManager.edit().putLong(PREF_LAST_TIMESTAMP,mLastContactUpdated).apply()
mFirstRun = false
} else {
myPrintln("no change in contacts")
}
}
private suspend fun putAllUserContactsToHashMap() : Result {
var result = Result.FAILED
val batchArray = mutableListOf(FirebaseFirestore.getInstance().batch())
var batchIndex = 0
var batchCount = 0
mFireContactHashMap.clear()
var source = Source.CACHE
if (mFirstRun) {
source = Source.DEFAULT
myPrintln("get contacts via Source.DEFAULT")
} else {
myPrintln("get contacts via Source.CACHE")
}
mFireContactRef.whereEqualTo( FireContact.COLUMN_USER_ID,mUid ).get(source)
.addOnSuccessListener {documents ->
var fireContact : FireContact
for (doc in documents) {
fireContact = doc.toObject(FireContact::class.java)
if (!mFireContactHashMap.containsKey(fireContact.paPho)) {
mFireContactHashMap[fireContact.paPho] = InternalContact(fireContact.na, doc.id)
} else {
myPrintln("duplicate will be removed from firestore: ${fireContact.paPho} / ${fireContact.na} / ${doc.id}")
batchArray[batchIndex].delete(mFireContactRef.document(doc.id))
batchCount++
if (BATCH_HALF_MAX < batchCount) {
batchArray += FirebaseFirestore.getInstance().batch()
batchCount = 0
batchIndex++
}
}
}
result = Result.SUCCESS
}.addOnFailureListener { exception ->
myPrintln("Error getting documents: $exception")
}.await()
//execute all batches
if ((batchCount > 0) || (batchIndex > 0)) {
myPrintln("committing duplicate delete... ")
batchArray.forEach { batch ->
batch.commit()
}
} else {
myPrintln("no duplicates to delete")
}
return result
}
Updating metadata of audio file in media store is not working in Android Q OS, it works in all other OS.
I am using content provider with uri specified as MediaStore.Audio.Media.EXTERNAL_CONTENT_URI. It is working fine in all below Android Q device. Below is the code that I am using to update track metadata.
ContentValues cv = new ContentValues();
ContentResolver resolver = getContentResolver();
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
cv.put(MediaStore.Audio.Media.TITLE, newTitle);
cv.put(MediaStore.Audio.Media.ALBUM, newAlbumName);
cv.put(MediaStore.Audio.Media.ARTIST, newArtistName);
int rowsUpdated = resolver.update(uri, cv,
MediaStore.Audio.Media._ID + " = ? ", new String[]{audioId});
For Android Q device, rowsUpdated is always 0 with no exception.
How are other music player updating tracks metadata in Android Q ?
Finally, it took some time but I figured that out.
First, you need to obtain access to file. Here you can read about that
Next, I found out that to update title or artist fields (maybe others to, I didn't test them) you need to set column MediaStore.Audio.Media.IS_PENDING value to 1. Like that:
val id = //Your audio file id
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 1)
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
contentResolver.update(uri, values, null, null)
And then you can edit fields that you need. Also to end the update process set MediaStore.Audio.Media.IS_PENDING to 0 again:
val id = //Your audio file id
val title = //New title
val artist = //New artist
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 0)
values.put(MediaStore.Audio.Media.TITLE, title)
values.put(MediaStore.Audio.Media.ARTIST, artist)
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
contentResolver.update(uri, values, null, null)
So in one function, it would look like this:
#RequiresApi(value = android.os.Build.VERSION_CODES.Q)
fun updateMetadata(contentResolver: ContentResolver, id: Long, title: String, artist: String) {
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 1)
contentResolver.update(uri, values, null, null)
values.clear()
values.put(MediaStore.Audio.Media.IS_PENDING, 0)
values.put(MediaStore.Audio.Media.TITLE, title)
values.put(MediaStore.Audio.Media.ARTIST, artist)
contentResolver.update(uri, values, null, null)
}
It's written in Kotlin but I think you will figure out how to do that in java.
UPDATE
By updating MediaStore you don't updating real file at any android version. That means, if a file would be updated (for example: renamed) and/or scanned by MediaScannerConnection your changes will be lost. This answer is right.
Using Android Q and beyond you have to first get the file
i.e
resolver.openInputStream(uri)?.use { stream -> outputFile.copyInputStreamToFile(stream) }
return outputFile.absolutePath
Helper Function
private fun File.copyInputStreamToFile(inputStream: InputStream?) {
this.outputStream().use { fileOut ->
inputStream?.copyTo(fileOut)
}
}
Then alter the metadata via a third party, I use J Audio Tagger
Then over write the old file
// From https://developer.android.com/reference/android/content/ContentProvider
// String: Access mode for the file. May be
// "r" for read-only access,
// "w" for write-only access (erasing whatever data is currently in the file),
// "wa" for write-only access to append to any existing data,
// "rw" for read and write access on any existing data, and
// "rwt" for read and write access that truncates any existing file. This value must never be null.
mContext.application.contentResolver.openOutputStream(uri, "w")?.use { stream ->
stream.write(file.readBytes())
}
This works fine when the file was created by your app
I've been updating meta data in the MediaStore through a ContentResolver, but this no longer works with Android Q (API 29). The following code gives me a warning, and the description is not updated:
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DESCRIPTION, "Some text");
res = getContext().getContentResolver().update(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values,
MediaStore.Images.Media._ID + "= ?", new String[]{sImageId});
android.process.media W/MediaProvider: Ignoring mutation of
description from com.example.android.someapp.app
This Medium post describes how Google has changed the API for accessing and updating files, but what about updating just the meta data? The warning seems to tell me Google no longer wants to allow third party apps to use the MediaStore, and I also found where the warning comes from:
https://android.googlesource.com/platform/packages/providers/MediaProvider/+/master/src/com/android/providers/media/MediaProvider.java#2960