﻿B4A=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=7.3
@EndOfDesignText@
'Class module
'Based on ContactsUtils version: 1.20
'Updated by WM: added several methods and the types/variables they need; find 'WM' in this source to see what was added/changed.

'Changes:
'- 2021-04-29:
'  - Initial version
'- 2021-06-24:
'  - Added method DeleteContact2
'- 2021-10-12:
'  - Added maps GroupSourcesByName, GroupRows, GroupRowsByName
'  - Added methods FindContactsByGroupRowId, FindContactsByGroupName, GetAllGroupsByRowId, GetAllGroupsByRowName, GetAllGroupsBySourceId, GetAllGroupsBySourceName

Sub Class_Globals
	Type cuContact (Id As Long, DisplayName As String)
	Type cuEmail (Email As String, EmailType As String)
	Type cuPhone (Number As String, PhoneType As String)
	Type cuEvent (DateString As String, EventType As String)
	Type cuOrganization(Company As String, Title As String)
	Type cuPostalAddr (Address As String, PostalAddrType As String) ' WM - https://www.b4x.com/android/forum/threads/create-contact-missing-fields.40791
	Type cuContactName (GivenName As String, FamilyName As String, MiddleName As String, Prefix As String) ' WM - https://www.b4x.com/android/forum/threads/create-contact-missing-fields.40791
	Type cuWebsite (website As String, websiteType As String)
	Type cuIM (IM As String, IMtype As String) ' WM
	Type cuNickname (nickname As String, nicknameType As String) ' WM
	Type cuContactDetailsThunderbird(Id As Long, displayName As String, firstName As String, lastName As String, nickname As String, _
									fax As String, phoneCell As String, phoneHome As String, phoneWork As String, pager As String, _
									photoPresent As Boolean, _
									postalAddressHome As String, postalAddressWork As String, _
									company As String, jobTitle As String, note As String, _
									websiteHome As String, websiteWork As String, _
									emailHome As String, emailWork As String, _
									IMaim As String, IMmsn As String, IMyahoo As String, IMskype As String, IMqq As String, IMhangouts As String, IMicq As String, IMjabber As String, _
									birthDate As String) ' WM
	Private postalAddrTypes As Map ' WM - https://www.b4x.com/android/forum/threads/create-contact-missing-fields.40791
	Private websiteTypes, IMtypes, nicknameTypes As Map ' WM
	Private mailTypes, phoneTypes, eventTypes As Map
	Private cr As ContentResolver
	Private dataUri, contactUri, rawContactUri As Uri
	Private GroupSources As Map
	Private GroupSourcesByName As Map ' WM
	Private GroupRows As Map ' WM
	Private GroupRowsByName As Map ' WM
End Sub

Public Sub Initialize
	dataUri.Parse("content://com.android.contacts/data")
	contactUri.Parse("content://com.android.contacts/contacts")
	rawContactUri.Parse("content://com.android.contacts/raw_contacts")
	cr.Initialize("cr")
	
	mailTypes.Initialize
	mailTypes.Put("1", "home")
	mailTypes.Put("2", "work")
	mailTypes.Put("3", "other")
	mailTypes.Put("4", "mobile")
	
	phoneTypes.Initialize
	phoneTypes.Put("1", "home")
	phoneTypes.Put("2", "mobile")
	phoneTypes.Put("3", "work")
	phoneTypes.Put("4", "fax_work")
	phoneTypes.Put("5", "fax_home")
	phoneTypes.Put("6", "pager")
	phoneTypes.Put("7", "other")
	phoneTypes.Put("8", "callback")
	phoneTypes.Put("9", "car")
	phoneTypes.Put("10", "company_main")
	phoneTypes.Put("11", "isdn")
	phoneTypes.Put("12", "main")
	phoneTypes.Put("13", "other_fax")
	phoneTypes.Put("14", "radio")
	phoneTypes.Put("15", "telex")
	phoneTypes.Put("16", "tty_tdd")
	phoneTypes.Put("17", "work_mobile")
	phoneTypes.Put("18", "work_pager")
	phoneTypes.Put("19", "assistant")
	phoneTypes.Put("20", "mms")

	eventTypes.Initialize
	eventTypes.Put("1", "anniversary")
	eventTypes.Put("2", "other")
	eventTypes.Put("3", "birthday")

	' WM - https://www.b4x.com/android/forum/threads/create-contact-missing-fields.40791
	postalAddrTypes.Initialize 
	postalAddrTypes.Put("1", "home")
	postalAddrTypes.Put("2", "work")
	postalAddrTypes.Put("3", "other")
	postalAddrTypes.Put("4", "custom")
	
	websiteTypes.Initialize ' WM
	websiteTypes.Put("1", "homepage") ' WM
	websiteTypes.Put("2", "blog") ' WM
	websiteTypes.Put("3", "profile") ' WM
	websiteTypes.Put("4", "home") ' WM
	websiteTypes.Put("5", "work") ' WM
	websiteTypes.Put("6", "ftp") ' WM
	websiteTypes.Put("7", "other") ' WM

	IMtypes.Initialize ' WM
	IMtypes.Put("0", "aim") ' WM
	IMtypes.Put("1", "msn") ' WM
	IMtypes.Put("2", "yahoo") ' WM
	IMtypes.Put("3", "skype") ' WM
	IMtypes.Put("4", "qq") ' WM
	IMtypes.Put("5", "hangouts") ' WM
	IMtypes.Put("6", "icq") ' WM
	IMtypes.Put("7", "jabber") ' WM

	nicknameTypes.Initialize ' WM
	nicknameTypes.Put("1", "default") ' WM
	nicknameTypes.Put("2", "other") ' WM
	nicknameTypes.Put("3", "maidenname") ' WM
	nicknameTypes.Put("4", "shortname") ' WM
	nicknameTypes.Put("5", "initials") ' WM

End Sub

'Returns a List with cuContact items based on the given name.
'Name - Name to look for.
'Exact - Whether to search for the exact name or to search for names that contain the given value.
'VisibleOnly - Whether to return only visible contacts.
Public Sub FindContactsByName(Name As String, Exact As Boolean, VisibleOnly As Boolean) As List
	Return FindContactsIdFromData("vnd.android.cursor.item/name", "data1", Name, "=", Exact, VisibleOnly)
End Sub

'Similar to FindContactsByName. Finds contacts based on the mail address.
Public Sub FindContactsByMail(Mail As String, Exact As Boolean, VisibleOnly As Boolean) As List
	Return FindContactsIdFromData("vnd.android.cursor.item/email_v2", "data1", Mail, "=", Exact, VisibleOnly)
End Sub

'Similar to FindContactsByName. Finds contacts based on the notes field.
Public Sub FindContactsByNotes(Note As String, Exact As Boolean, VisibleOnly As Boolean) As List
	Return FindContactsIdFromData("vnd.android.cursor.item/note", "data1", Note, "=", Exact, VisibleOnly)
End Sub

'Similar to FindContactsByName. Finds contacts based on the phone number.
Public Sub FindContactsByPhone(PhoneNumber As String, Exact As Boolean, VisibleOnly As Boolean) As List
	Return FindContactsIdFromData("vnd.android.cursor.item/phone_v2", "data1", PhoneNumber, "=", Exact, VisibleOnly)
End Sub

'Returns the starred contacts.
Public Sub FindContactsByStarred(Starred As Boolean) As List
	Dim value As String
	If Starred Then value = "1" Else value = "0"
	Return FindContactsIdFromData("vnd.android.cursor.item/name", "starred", value,"=", True, True)
End Sub
'Returns all contacts.
Public Sub FindAllContacts(VisibleOnly As Boolean) As List
	Return FindContactsIdFromData("vnd.android.cursor.item/name", "data1", "null", "<>", True, VisibleOnly)
End Sub
'Returns all contacts with a photo.
Public Sub FindContactsWithPhotos As List
	Return FindContactsIdFromData("vnd.android.cursor.item/photo", "data15", "null", "<>", True, False)
End Sub

Public Sub GetNameFromId (id As String) As String
	Dim crsr As Cursor = cr.Query(dataUri, Array As String("contact_id", "display_name"), "contact_id = ?", _
		Array As String(id), "")
	Dim name As String
	If crsr.RowCount = 0 Then
		Log("Contact not found: " & id)
	Else
		crsr.Position = 0
		name = crsr.GetString2(1)
	End If
	crsr.Close
	Return name
End Sub

Private Sub FindContactsIdFromData (Mime As String, DataColumn As String, Value As String, Operator As String, _
	Exact As Boolean, VisibleOnly As Boolean) As List
	If Not(Exact) Then 
		Operator = "LIKE"
		Value = "%" & Value & "%"
	End If
	Dim selection As String = "mimetype = ? AND " & DataColumn & " " & Operator & " ? "
	If VisibleOnly Then selection = selection & " AND in_visible_group = 1"
	Dim crsr As Cursor = cr.Query(dataUri, Array As String("contact_id", "display_name"), selection, _
		Array As String(Mime, Value), "")
	Dim res As List
	res.Initialize
	Dim m As Map
	m.Initialize
	For i = 0 To crsr.RowCount - 1
		crsr.Position = i
		Dim cu As cuContact
		cu.Initialize
		cu.Id = crsr.GetLong("contact_id")
		cu.DisplayName = crsr.GetString("display_name")
		If m.ContainsKey(cu.Id) Then Continue
		m.Put(cu.Id, Null)
		res.Add(cu)
	Next
	crsr.Close
	Return res
End Sub

' Original version
'Public Sub GetOrganization(Id As Long) As cuOrganization
'	Dim organizations As List = GetData("vnd.android.cursor.item/organization", Array As String("data1", "data4"), _
'		Id, Null)
'	Dim o As cuOrganization
'	If organizations.Size > 0 Then
'		o.Initialize
'		Dim obj() As Object = organizations.Get(0)
'		o.Company = obj(0)
'		o.Title = obj(1)
'	End If
'	Return o
'End Sub

'Returns a List with cuEmail items.
Public Sub GetEmails(Id As Long) As List
	Dim res As List
	res.Initialize
	For Each obj() As Object In GetData("vnd.android.cursor.item/email_v2", Array As String("data1", "data2"), Id, Null)
		Dim e As cuEmail
		e.Initialize
		e.Email = obj(0)
		e.EmailType = mailTypes.Get(obj(1))
		res.Add(e)
	Next
	Return res
End Sub

'Returns a List with cuEvents items.
'Events are only available as of Android API level 5.
Public Sub GetEvents(Id As Long) As List
	Dim res As List
	res.Initialize
	For Each obj() As Object In GetData("vnd.android.cursor.item/contact_event", Array As String("data1", "data2"), Id, Null)
		Dim e As cuEvent
		e.Initialize
		e.DateString = obj(0)
		e.EventType = eventTypes.Get(obj(1))
		res.Add(e)
	Next
	Return res
End Sub

'Returns a List with cuPhone items.
Public Sub GetPhones(id As Long) As List
	Dim res As List
	res.Initialize
	For Each obj() As Object In GetData("vnd.android.cursor.item/phone_v2", Array As String("data1", "data2"), id, Null)
		Dim p As cuPhone
		p.Initialize
		p.Number = obj(0)
		p.PhoneType = phoneTypes.Get(obj(1))
		res.Add(p)
	Next
	Return res
End Sub

'Returns the note field.
Public Sub GetNote(id As Long) As String
	Dim raw As List = GetData("vnd.android.cursor.item/note", Array As String("data1"), id, Null)
	If raw.Size = 0 Then Return ""
	Dim obj() As Object = raw.Get(0)
	Return obj(0)
End Sub

'Returns the thumbnail photo of the given contact. Returns an uninitialized bitmap if no photo is available.
Public Sub GetPhoto(Id As Long) As Bitmap
	Dim raw As List = GetData("vnd.android.cursor.item/photo", Array As String("data15"), Id, Array As Boolean(True))
	Dim bmp As Bitmap
	If raw.Size > 0 Then
		Dim obj() As Object = raw.Get(0)
		Dim bytes() As Byte = obj(0)
		If bytes <> Null Then
			Dim In As InputStream
			In.InitializeFromBytesArray(bytes, 0, bytes.Length)
			bmp.Initialize2(In)
			In.Close
		End If
	End If
	Return bmp
End Sub

'Gets whether the contact is "starred"; returns False if the Id doesn't exist.
Public Sub GetStarred(Id As Long) As Boolean
	Dim crsr As Cursor = cr.Query(contactUri, Array As String("starred"), "_id = ?", Array As String(Id), "")
	If crsr.RowCount < 1 Then
		Return False
	Else
		crsr.Position = 0
		Dim starred As Boolean = crsr.GetInt("starred") = 1
		crsr.Close
		Return starred
	End If
End Sub

Private Sub GetData(Mime As String, DataColumns() As String, Id As Long, Blobs() As Boolean) As List
	Dim crsr As Cursor = cr.Query(dataUri, DataColumns, "mimetype = ? AND contact_id = ?", _
		Array As String(Mime, Id), "")
	Dim res As List
	res.Initialize
	For i = 0 To crsr.RowCount - 1
		crsr.Position = i
		Dim row(DataColumns.Length) As Object
		For c = 0 To DataColumns.Length - 1
			If Blobs <> Null And Blobs(c) = True Then
				row(c) = crsr.GetBlob2(c)
			Else
				row(c) = crsr.GetString2(c)
			End If
		Next
		res.Add(row)
	Next
	crsr.Close
	Return res
End Sub

'Sets the note field of the given id.
Public Sub SetNote(Id As Long, Note As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", Note)
	SetData("vnd.android.cursor.item/note", v, Id, True)
End Sub

Public Sub AddNote(Id As Long, Note As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", Note)
	SetData("vnd.android.cursor.item/note", v, Id, False)
End Sub

'Adds an email field to the given contact id.
'EmailType - One of the email types strings (see Initialize method).
Public Sub AddEmail(Id As Long, Email As String, EmailType As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", Email)
	v.PutInteger("data2", GetKeyFromValue(mailTypes, EmailType, 3))
	SetData("vnd.android.cursor.item/email_v2", v, Id, False)
End Sub

'Adds a phone field to the given contact id.
'PhoneType - One of the phone types strings (see Initialize method).
Public Sub AddPhone(Id As Long, PhoneNumber As String, PhoneType As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", PhoneNumber)
	v.PutInteger("data2", GetKeyFromValue(phoneTypes, PhoneType, 7))
	SetData("vnd.android.cursor.item/phone_v2", v, Id, False)
End Sub

'Deletes the given phone number.
Public Sub DeletePhone(Id As Long, PhoneNumber As String)
	DeleteData("vnd.android.cursor.item/phone_v2", PhoneNumber, Id)
End Sub
'Deletes the given email address.
Public Sub DeleteEmail(Id As Long,Email As String)
	DeleteData("vnd.android.cursor.item/email_v2", Email, Id)
End Sub

'Small utility to find the type integer value from the type name
Private Sub GetKeyFromValue(m As Map, v As String, defaultValue As Int) As Int
	Dim t As Int = defaultValue
	For i = 0 To m.Size - 1
		If m.GetValueAt(i) = v.ToLowerCase Then ' WM - made this case-insensitive
			t = m.GetKeyAt(i)
			Exit
		End If
	Next
	Return t
End Sub

Private Sub DeleteData(Mime As String, Data1Value As String, Id As Long)
	cr.Delete(dataUri, "mimetype = ? AND data1 = ? AND contact_id = ?", Array As String(Mime, Data1Value, Id))
End Sub
Private Sub SetData(Mime As String, Values As ContentValues, Id As Long, Update As Boolean) As Boolean ' WM: added return type
	If Update Then
		cr.Update(dataUri, Values, "mimetype = ? AND contact_id = ?", Array As String(Mime, Id))
	Else
		Dim crsr As Cursor = cr.Query(contactUri, Array As String("name_raw_contact_id"), _
			"_id = ?", Array As String(Id), "")
		If crsr.RowCount = 0 Then
			Log("Error getting raw_contact_id; Id=" & Id & "; Mime=" & Mime) ' WM: added Id and Mime
			crsr.Close
			Return False
		End If
		crsr.Position = 0
		Values.PutString("raw_contact_id", crsr.GetString("name_raw_contact_id"))
		crsr.Close
		Values.PutString("mimetype", Mime)
		cr.Insert(dataUri, Values)
	End If
	Return True
End Sub

'Sets the starred state of the given id.
Public Sub SetStarred (Id As Long, Starred As Boolean)
	Dim values As ContentValues
	values.Initialize
	values.PutBoolean("starred", Starred)
	cr.Update(contactUri, values, "_id = ?", Array As String(Id))
End Sub

'Deletes the contact with the given Id.
Public Sub DeleteContact(Id As Long) As Int ' WM - added return type: the number of contacts that were deleted
	Return cr.Delete(rawContactUri, "contact_id = ?", Array As String(Id))
End Sub

'Returns a Map. The keys are the account names and the values are the account types.
Public Sub GetAccounts(Id As Long) As Map
	Dim uri As Uri
	uri.Parse("content://com.android.contacts/contacts/" & Id & "/entities")
	Dim c As Cursor = cr.Query(uri, Array As String("account_name", "account_type"), "", Null, "")
	Dim m As Map
	m.Initialize
	For i = 0 To c.RowCount - 1
		c.Position = i
		m.Put(c.GetString("account_name"), c.GetString("account_type"))
	Next
	c.Close

	Return m
End Sub

Private Sub FillGroupSources ' WM - added processing for GroupSourcesByName, GroupRows, GroupRowsByName
	If GroupSources.IsInitialized = False Then
		GroupSources.Initialize
		GroupSourcesByName.Initialize
		GroupRows.Initialize
		GroupRowsByName.Initialize
		Dim gu As Uri
		gu.Parse("content://com.android.contacts/groups")
		Dim c As Cursor = cr.Query(gu, Array As String("_id", "sourceid", "title"), "", Null, "")
		For i = 0 To c.RowCount - 1
			c.Position = i
			GroupSources.Put(c.GetString("sourceid"), c.GetString("title"))
			GroupSourcesByName.Put(c.GetString("title").ToLowerCase, c.GetString("sourceid")) ' Note: title is downshifted to make it easier to look up values in the map
			GroupRows.Put(c.GetString("_id"), c.GetString("title"))
			GroupRowsByName.Put(c.GetString("title").ToLowerCase, c.GetString("_id")) ' Note: title is downshifted to make it easier to look up values in the map
		Next
	End If
End Sub

'Returns a List with Groups.
Public Sub GetGroups(Id As Long) As List
	FillGroupSources
	Dim uri As Uri
	uri.Parse("content://com.android.contacts/contacts/" & Id & "/entities")
	Dim c As Cursor = cr.Query(uri, Array As String("group_sourceid"), "", Null, "")
	Dim groups As List
	groups.Initialize
	For i = 0 To c.RowCount - 1
		c.Position = i
		If c.GetString("group_sourceid") <> Null Then
			Dim source As String = c.GetString("group_sourceid")
			If GroupSources.ContainsKey(source) Then
				groups.Add(GroupSources.Get(source))
			End If
		End If
	Next
	c.Close
	Return groups
End Sub

'Inserts a new contact and returns the cuContact object of this contact.
Public Sub InsertContact(Name As String, Phone As String) As cuContact
	Dim values As ContentValues
	values.Initialize
	values.PutNull("account_name")
	values.PutNull("account_type")
	Dim rawUri As Uri = cr.Insert(rawContactUri, values)
	Dim rawContactId As Long = rawUri.ParseId
	
	values.Initialize
	values.PutLong("raw_contact_id", rawContactId)
	values.PutString("mimetype", "vnd.android.cursor.item/phone_v2")
	values.PutString("data1", Phone)
	cr.Insert(dataUri, values)
	
	values.Initialize
	values.PutLong("raw_contact_id", rawContactId)
	values.PutString("mimetype", "vnd.android.cursor.item/name")
	values.PutString("data1", Name)
	cr.Insert(dataUri, values)
	Dim cu As cuContact
	cu.Initialize
	Dim crsr As Cursor = cr.Query(dataUri, Array As String("contact_id", "display_name"), "raw_contact_id = ?", _
		Array As String(rawContactId), "")
	crsr.Position = 0
	cu.DisplayName = crsr.GetString("display_name")
	cu.Id = crsr.GetLong("contact_id")
	Return cu
End Sub

'useful for debugging
Private Sub printCursor(c As Cursor) 'ignore
	For r = 0 To c.RowCount - 1
		c.Position = r
		For col = 0 To c.ColumnCount - 1
			Try
				Log(c.GetColumnName(col) & ": " & c.GetString2(col))
			Catch
				Log(c.GetColumnName(col) & ": " & LastException)
			End Try
		Next
		Log("***************")
	Next
End Sub

' ===================== Methods added by WM follow here ===================
' These come from or are based on: https://www.b4x.com/android/forum/threads/create-contact-missing-fields.40791/

'Inserts a new contact and returns the cuContact object of this contact.
Public Sub InsertContactWithDetailedName(ContactName As cuContactName, Phone As String, forceDisplayName As String) As cuContact 'WM - added 'forceDisplayName'
	Dim values As ContentValues
	values.Initialize
	values.PutNull("account_name")
	values.PutNull("account_type")
	Dim rawUri As Uri = cr.Insert(rawContactUri, values)
	Dim rawContactId As Long = rawUri.ParseId
	
	values.Initialize
	values.PutLong("raw_contact_id", rawContactId)
	values.PutString("mimetype", "vnd.android.cursor.item/phone_v2")
	values.PutString("data1", Phone)
	cr.Insert(dataUri, values)
	
	Dim DisplayName As String
	' DisplayName = ContactName.GivenName & " " & ContactName.FamilyName ' WM: this was the original version; hereunder is my version:
	If forceDisplayName <> "" Then
		DisplayName = forceDisplayName
	Else If ContactName.FamilyName = "" Then
		DisplayName = ContactName.GivenName
	Else If ContactName.GivenName = "" Then
		DisplayName = ContactName.FamilyName
	Else
		DisplayName = ContactName.FamilyName & ", " & ContactName.GivenName
	End If

	values.Initialize
	values.PutLong("raw_contact_id", rawContactId)
	values.PutString("mimetype", "vnd.android.cursor.item/name")
	values.PutString("data1", DisplayName)
	values.PutString("data2", ContactName.GivenName)
	values.PutString("data3", ContactName.FamilyName)
	values.PutString("data5", ContactName.MiddleName)
	values.PutString("data4", ContactName.Prefix)
	cr.Insert(dataUri, values)
	Dim cu As cuContact
	cu.Initialize
	Dim crsr As Cursor = cr.Query(dataUri, Array As String("contact_id", "display_name"), "raw_contact_id = ?", _
		Array As String(rawContactId), "")
	crsr.Position = 0
	cu.DisplayName = crsr.GetString("display_name")
	cu.Id = crsr.GetLong("contact_id")
	Return cu
End Sub

'Returns Detailed Contact Name as cuContactName.
Public Sub GetDetailedName(id As Long) As cuContactName
	Dim ContactName As cuContactName
	ContactName.Initialize

	Dim raw As List = GetData("vnd.android.cursor.item/name", Array As String("data2", "data3", "data5", "data4"), id, Null)
	If raw.Size = 0 Then
		ContactName.GivenName = ""
		ContactName.FamilyName = ""
		ContactName.MiddleName = ""
		ContactName.Prefix = ""
	Else
		Dim obj() As Object = raw.Get(0)
		ContactName.GivenName = obj(0)
		If ContactName.GivenName = "null" Then ContactName.GivenName = ""
		ContactName.FamilyName = obj(1)
		If ContactName.FamilyName = "null" Then ContactName.FamilyName = ""
		ContactName.MiddleName = obj(2)
		If ContactName.MiddleName = "null" Then ContactName.MiddleName = ""
		ContactName.Prefix = obj(3)
		If ContactName.Prefix = "null" Then ContactName.Prefix = ""
	End If
	Return ContactName
End Sub

' Returns a contact's organisation data as a cuOrganization item.
Public Sub GetOrganization(Id As Long) As cuOrganization
	Dim organizations As List = GetData("vnd.android.cursor.item/organization", Array As String("data1", "data4"), _
	Id, Null)
	Dim o As cuOrganization
	If organizations.Size > 0 Then
		o.Initialize
		Dim obj() As Object = organizations.Get(0)
		If obj(0) = Null Then
			o.Company = ""
		Else
			o.Company = obj(0)
		End If
		If obj(1) = Null Then
			o.Title = ""
		Else
			o.Title = obj(1)
		End If
	Else
		o.Company = ""
		o.Title = ""
	End If
	Return o
End Sub

'Returns a List with cuPostalAddr items.
Public Sub GetPostalAddresses(Id As Long) As List
	Dim res As List
	res.Initialize
	For Each obj() As Object In GetData("vnd.android.cursor.item/postal-address_v2", Array As String("data1", "data2"), Id, Null)
		Dim Addr As cuPostalAddr
		Addr.Initialize
		Addr.Address = obj(0)
		Addr.PostalAddrType = postalAddrTypes.Get(obj(1))
		res.Add(Addr)
	Next
	Return res
End Sub

'Returns a Map with the postal addresses as keys and the address types as values.
Public Sub GetPostalAddresses1(Id As Long) As Map
	Dim PostalAddrsMap As Map
	PostalAddrsMap.Initialize
	For Each obj() As Object In GetData("vnd.android.cursor.item/postal-address_v2", Array As String("data1", "data2"), Id, Null)
		Dim KeyStr As String
		KeyStr = obj(0)
		If KeyStr <> "" Then
			PostalAddrsMap.Put(obj(0), obj(1))
		End If
	Next
	Return PostalAddrsMap
End Sub

'Sets a contact's organisation.
Public Sub SetOrganization(ID As Long,Org As cuOrganization)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", Org.Company)
	v.PutString("data4", Org.Title)
	SetData("vnd.android.cursor.item/organization", v, ID, True)
End Sub

'Sets a contact's organisation.
Public Sub AddOrganization(ID As Long,Org As cuOrganization)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", Org.Company)
	v.PutString("data4", Org.Title)
	SetData("vnd.android.cursor.item/organization", v, ID, False)
End Sub

'Sets a contact's photo from a bitmap.
Public Sub AddPhoto(ID As Long, PhotoBitMap As Bitmap)
	Dim Out As OutputStream
	Dim bytes() As Byte
	Out.InitializeToBytesArray(1000) ' WM - moved this before the next line as 'Out' needed to be initialised first !
	PhotoBitMap.WriteToStream(Out,100,"PNG")
	bytes = Out.ToBytesArray
	Out.Close
	Dim v As ContentValues
	v.Initialize
	v.PutBytes("data15", bytes)
	SetData("vnd.android.cursor.item/photo", v, ID, False)
End Sub

'Sets a contact's photo from a bitmap.
Public Sub SetPhoto(ID As Long, PhotoBitMap As Bitmap)
	Dim Out As OutputStream
	Dim bytes() As Byte
	Out.InitializeToBytesArray(1000) ' WM - moved this before the next line as 'Out' needed to be initialised first !
	PhotoBitMap.WriteToStream(Out,100,"PNG")
	bytes = Out.ToBytesArray
	Out.Close
	Dim v As ContentValues
	v.Initialize
	v.PutBytes("data15", bytes)
	SetData("vnd.android.cursor.item/photo", v, ID, True)
End Sub

' Adds an unstructured postal address for a contact.
Public Sub AddPostalAddress(Id As Long, PostalAddr As String, PostalAddrType As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", PostalAddr)
	v.PutInteger("data2", GetKeyFromValue(postalAddrTypes, PostalAddrType, 2))
	SetData("vnd.android.cursor.item/postal-address_v2", v, Id, False)
End Sub

' Sets an unstructured postal address for a contact.
Public Sub SetPostalAddress(Id As Long, PostalAddr As String, PostalAddrType As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", PostalAddr)
	v.PutInteger("data2", GetKeyFromValue(postalAddrTypes, PostalAddrType, 2))
	SetData("vnd.android.cursor.item/postal-address_v2", v, Id, True)
End Sub

' Adds an unstructured postal address for a contact.
Public Sub AddPostalAddress1(Id As Long, PostalAddr As String, PostalAddrTypeInt As Int) ' WM - changed 'PostalAddrType' to 'PostalAddrTypeInt' for clarity
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", PostalAddr)
	v.PutInteger("data2", PostalAddrTypeInt)
	SetData("vnd.android.cursor.item/postal-address_v2", v, Id, False)
End Sub

' ===================== More methods added by WM follow here ===================

'Returns a List with cuWebsite items.
Public Sub GetWebsites(Id As Long) As List
	Dim res As List
	res.Initialize
	For Each obj() As Object In GetData("vnd.android.cursor.item/website", Array As String("data1", "data2"), Id, Null)
		Dim w As cuWebsite
		w.Initialize
		w.Website = obj(0)
		w.WebsiteType = websiteTypes.Get(obj(1))
		res.Add(w)
	Next
	Return res
End Sub

'Adds a website field to the given contact id.
'WebsiteType - One of the website type strings (see Initialize method).
Public Sub AddWebsite(Id As Long, Website As String, WebsiteType As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", Website)
	v.PutInteger("data2", GetKeyFromValue(websiteTypes, WebsiteType, 1))
	SetData("vnd.android.cursor.item/website", v, Id, False)
End Sub

'Deletes the given website.
Public Sub DeleteWebsite(Id As Long,Website As String)
	DeleteData("vnd.android.cursor.item/website", Website, Id)
End Sub

'Returns all contacts as a list of cuContactDetailsThunderbird items.
Public Sub GetAllContactsThunderbird(VisibleOnly As Boolean) As List

	Dim contactsListIn As List
	Dim contactsListOut As List
	contactsListIn.Initialize2(FindAllContacts(False))
	contactsListOut.Initialize
	For Each oneContact As cuContact In contactsListIn
		contactsListOut.Add(GetContactDetailsThunderbird(oneContact.Id))
	Next
	Return contactsListOut

End Sub

' Returns a contact's data as a cuContactDetailsThunderbird item.
Public Sub GetContactDetailsThunderbird(id As Long) As cuContactDetailsThunderbird

	Dim detls As cuContactDetailsThunderbird
	detls.Initialize
	detls.Id = id
	detls.note = GetNote(id)
	detls.photoPresent = GetPhoto(id).IsInitialized
	detls.displayName = GetNameFromId(id)

	Dim dn As cuContactName = GetDetailedName(id)
	detls.firstName = dn.GivenName
	detls.lastName = dn.FamilyName

	Dim dorg As cuOrganization = GetOrganization(id)
	detls.company = dorg.Company
	detls.jobTitle = dorg.Title

	Dim websites As List = GetWebsites(id)
	For Each dw As cuWebsite In websites
		Select Case dw.websiteType
			Case "home"
				detls.websiteHome = dw.website
			Case "work"
				detls.websiteWork = dw.website
		End Select
	Next

	Dim addrs As List = GetPostalAddresses(id)
	For Each da As cuPostalAddr In addrs
		Select Case da.PostalAddrType
			Case "home"
				detls.postalAddressHome = da.Address
			Case "work"
				detls.postalAddressWork = da.Address
		End Select
	Next

	Dim phones As List = GetPhones(id)
	For Each dph As cuPhone In phones
		Select Case dph.PhoneType
			Case "home"
				detls.phoneHome = dph.Number
			Case "work"
				detls.phoneWork = dph.Number
			Case "other_fax"
				detls.fax = dph.Number
			Case "mobile"
				detls.phoneCell = dph.Number
			Case "pager"
				detls.pager = dph.Number
		End Select
	Next

	Dim emails As List = GetEmails(id)
	For Each em As cuEmail In emails
		Select Case em.EmailType
			Case "home"
				detls.emailHome = em.Email
			Case "work"
				detls.emailWork = em.Email
		End Select
	Next

	Dim IMs As List = GetIMs(id)
	For Each im As cuIM In IMs
		Select Case im.IMtype
			Case "aim"
				detls.IMaim = im.IM
			Case "msn"
				detls.IMmsn = im.IM
			Case "yahoo"
				detls.IMyahoo = im.IM
			Case "skype"
				detls.IMskype = im.IM
			Case "qq"
				detls.IMqq = im.IM
			Case "hangouts"
				detls.IMhangouts = im.IM
			Case "icq"
				detls.IMicq = im.IM
			Case "jabber"
				detls.IMjabber = im.IM
		End Select
	Next

	Dim events As List = GetEvents(id)
	For Each ev As cuEvent In events
		If ev.EventType = "birthday" Then
			detls.birthDate = ev.DateString
			Exit
		End If
	Next

	Dim nicknames As List = GetNicknames(id)
	For Each nn As cuNickname In nicknames
		If nn.nicknameType = "default" Then
			detls.nickname = nn.nickname
			Exit
		End If
	Next

	Return detls

End Sub

'Returns the full size photo of the given contact. Returns an uninitialized bitmap if no photo is available.
Public Sub GetFullSizePhoto(id As Long) As Bitmap

	' Code from https://www.b4x.com/android/forum/threads/photo-contacts.11862/#post-264563

	Dim bmp As Bitmap
	Dim crsr As Cursor = cr.Query(dataUri, Array As String("photo_uri"), "contact_id = ?", _
     Array As String(id), "")
	If crsr.RowCount > 0 Then
		crsr.Position = 0
		Dim photoUri As String = crsr.GetString("photo_uri")
		Dim jo As JavaObject
		jo.InitializeStatic("anywheresoftware.b4a.objects.streams.File")
		Dim In As InputStream = File.OpenInput(jo.GetField("ContentDir"), photoUri)
		bmp.Initialize2(In)
		In.Close
	End If
	crsr.Close
	Return bmp

End Sub

'Sets a contact's photo from a file.
Public Sub SetPhotoFromFile(ID As Long, dir As String, fileName As String)

	' Code based on https://www.b4x.com/android/forum/threads/how-to-add-a-photograph-to-a-contact.76025/#post-499164

	Dim image() As Byte = Bit.InputStreamToBytes(File.OpenInput(dir, fileName))
	Dim v As ContentValues
	v.Initialize
	v.PutBytes("data15", image)
	Dim bmp As Bitmap = GetPhoto(ID)
	SetData("vnd.android.cursor.item/photo", v, ID, bmp.IsInitialized)

End Sub

' Adds a structured postal address
Public Sub AddPostalAddress2(Id As Long, PostalAddrType As String, address As String, address2 As String, city As String, country As String, state As String, zipCode As String)

	If (address = "") And (address2 = "") And _
		(city = "") And (country = "") And _
		(state = "") And (zipCode = "") Then Return

	Dim v As ContentValues
	v.Initialize
	v.PutInteger("data2", GetKeyFromValue(postalAddrTypes, PostalAddrType, 2))
	If address <> "" Then v.PutString("data4", address)
	If address2 <> "" Then v.PutString("data6", address2)
	If city <> "" Then v.PutString("data7", city)
	If state <> "" Then v.PutString("data8", state)
	If zipCode <> "" Then v.PutString("data9", zipCode)
	If country <> "" Then v.PutString("data10", country)
	SetData("vnd.android.cursor.item/postal-address_v2", v, Id, False)

End Sub

'Adds an IM field to the given contact id.
'IMType - One of the IM types strings (see Initialize method).
Public Sub AddIM(Id As Long, IMname As String, IMType As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", IMname)
	v.PutInteger("data5", GetKeyFromValue(IMtypes, IMType, -1))
	SetData("vnd.android.cursor.item/im", v, Id, False)
End Sub

'Adds a nickname field to the given contact id.
'NicknameType - One of the nickname types strings (see Initialize method).
Public Sub AddNickname(Id As Long, nickName As String, NicknameType As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", nickName)
	v.PutInteger("data2", GetKeyFromValue(nicknameTypes, NicknameType, 1))
	SetData("vnd.android.cursor.item/nickname", v, Id, False)
End Sub

'Returns a List with cuNickname items.
Public Sub GetNicknames(id As Long) As List
	Dim res As List
	res.Initialize
	For Each obj() As Object In GetData("vnd.android.cursor.item/nickname", Array As String("data1", "data2"), id, Null)
		Dim n As cuNickname
		n.Initialize
		n.nickname = obj(0)
		n.nicknameType = nicknameTypes.Get(obj(1))
		res.Add(n)
	Next
	Return res
End Sub

'Returns a List with cuIM items.
Public Sub GetIMs(Id As Long) As List
	Dim res As List
	res.Initialize
	For Each obj() As Object In GetData("vnd.android.cursor.item/im", Array As String("data1", "data5"), Id, Null)
		Dim i As cuIM
		i.Initialize
		i.IM = obj(0)
		i.IMtype = IMtypes.Get(obj(1))
		res.Add(i)
	Next
	Return res
End Sub

'Adds an event field to the given contact id.
'EventType - One of the event types strings (see Initialize method).
'Events are only available as of Android API level 5.
Public Sub AddEvent(Id As Long, DateString As String, EventType As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", DateString)
	v.PutInteger("data2", GetKeyFromValue(eventTypes, EventType, 2))
	SetData("vnd.android.cursor.item/contact_event", v, Id, False)
End Sub

' Sets an existing contact's DisplayName.
Public Sub SetDisplayName(Id As Long, newName As String)
	Dim v As ContentValues
	v.Initialize
	v.PutString("data1", newName)
	SetData("vnd.android.cursor.item/name", v, Id, False)
End Sub

'Deletes the contact with the given Id and returns the number of contacts that were deleted.
'Returns -1 if an exception occurred; the exception is then logged as well.
Public Sub DeleteContact2(Id As Long) As Int
	Dim numDeleted As Int
	Try
		numDeleted = cr.Delete(rawContactUri, "contact_id = ?", Array As String(Id))
	Catch
		Log("DeleteContact2, id " & Id & ": " & LastException)
		numDeleted = -1
	End Try
	Return numDeleted
End Sub

'Returns all contacts that are member of the specified group ID.
Public Sub FindContactsByGroupRowId(gid As Int, VisibleOnly As Boolean) As List
	Dim projCols() As String = Array As String("contact_id", "display_name", "data1")
	Return FindContactsIdFromData2("vnd.android.cursor.item/group_membership", "data1", gid, "=", True, VisibleOnly, projCols)
End Sub

'Returns all contacts that are member of the specified group name.
Public Sub FindContactsByGroupName(gname As String, VisibleOnly As Boolean) As List
	Dim l As List
	l.Initialize

	FillGroupSources
	If GroupRowsByName.ContainsKey(gname.ToLowerCase) = False Then ' Return an empty list
		Return l
	Else
		Dim i As Int
		If IsNumber(GroupRowsByName.Get(gname.ToLowerCase)) Then
			i = GroupRowsByName.Get(gname.ToLowerCase)
		Else
			Try
				i = Bit.ParseInt(GroupRowsByName.Get(gname.ToLowerCase), 16)
			Catch
				Return l
			End Try
		End If
		Return FindContactsByGroupRowId(i, VisibleOnly)
	End If
End Sub

' Returns a map with all groups; the key is the group's source ID, the value is the group name
' You probably shouldn't be using this one, but use GetAllGroupsByRowId instead
Public Sub GetAllGroupsBySourceId As Map
	FillGroupSources
	Return CopyMap(GroupSources) ' Don't return GroupSources itself as the caller might manipulate the returned object
End Sub

' Returns a map with all groups; the key is the group name, the value is the group source ID
' You probably shouldn't be using this one, but use GetAllGroupsByRowName instead
Public Sub GetAllGroupsBySourceName As Map
	FillGroupSources
	Return CopyMap(GroupSourcesByName) ' Don't return GroupSourcesByName itself as the caller might manipulate the returned object
End Sub

' Returns a map with all groups; the key is the group's Row ID, the value is the group name
Public Sub GetAllGroupsByRowId As Map
	FillGroupSources
	Return CopyMap(GroupRows) ' Don't return GroupRows itself as the caller might manipulate the returned object
End Sub

' Returns a map with all groups; the key is the group name, the value is the group Row ID
Public Sub GetAllGroupsByRowName As Map
	FillGroupSources
	Return CopyMap(GroupRowsByName) ' Don't return GroupRowsByName itself as the caller might manipulate the returned object
End Sub

' Copies a map to a new one
Private Sub CopyMap(m As Map) As Map

	Dim res As Map
	res.Initialize

	For Each k As Object In m.Keys
		res.Put(k, m.Get(k))
	Next

	Return res

End Sub

' Like FindContactsIdFromData, but the 'projection' columns are specified by the caller
Private Sub FindContactsIdFromData2(Mime As String, DataColumn As String, Value As String, Operator As String, _
	Exact As Boolean, VisibleOnly As Boolean, projectionColumns() As String) As List
	If Not(Exact) Then 
		Operator = "LIKE"
		Value = "%" & Value & "%"
	End If
	Dim selection As String = "mimetype = ? AND " & DataColumn & " " & Operator & " ? "
	If VisibleOnly Then selection = selection & " AND in_visible_group = 1"
	Dim crsr As Cursor = cr.Query(dataUri, projectionColumns, selection, _
		Array As String(Mime, Value), "")
	Dim res As List
	res.Initialize
	Dim m As Map
	m.Initialize
	For i = 0 To crsr.RowCount - 1
		crsr.Position = i
		Dim cu As cuContact
		cu.Initialize
		cu.Id = crsr.GetLong("contact_id")
		cu.DisplayName = crsr.GetString("display_name")
		If m.ContainsKey(cu.Id) Then Continue
		m.Put(cu.Id, Null)
		res.Add(cu)
	Next
	crsr.Close
	Return res
End Sub